ALICE 2.5.3 (Missiles, Physics & More)

This bundle is marked as director's cut. It exceeds all expectations and excels in every regard.
RbeQz1.png


"After downloading the RPGminimap, I thought I could use ALICE to
improve my missile system since it’s now installed anyway. Best thing I could do,
now I'm having tremendous fun applying ALICE capabilities everywhere!"

—Xthreo


RbeQz1.png


"This is now part of the Lua Bible."
—Wrda


RbeQz1.png



RbeQz1.png


RxeHRb.gif
wqFF8T.gif
S974Nw.gif


RbeQz1.png


Overview

Features

Installation

Tutorial & Documentation

Change Log

Credits


Overview

A Limitless Interaction Caller Engine (A.L.I.C.E) takes Warcraft III mapmaking to the next level by providing a high performance framework for different types of systems, such as physics and missile systems, and allowing them to be interconnected easily. ALICE is the Unreal-engine of Warcraft 3 - it has a bit of a learning curve, but once you've mastered it, you will be able to create any kind of complex system and add new functionality with just a few lines of code.

So, let's enter wonderland!

Heads-Up

:peasant-victory:Do not get intimidated by the complexity of this resource! While there are a multitude of API functions, you can build your first systems with just a few lines of code, like it is done in the examples of the Test Map. ALICE is also packaged with ready-to use libraries that provide a solid starting point for physics and missile systems.

Also, check out these stand-alone systems built with ALICE:

How does it work?

ALICE attaches so-called actors to Warcraft 3 objects or to your own objects in order to mediate interactions between them and other nearby objects. Actors carry identifiers and function interfaces determining the types of actors they interact with. You fully define the functions that are called, so these interactions can be anything from collision detection, to healing auras, or gravitational pulls. ALICE's job is only to take care of the control structure around your code.

While in other systems, you would have to dig yourself into the internals to add new functionalities to the objects of those systems, with ALICE you can do everything externally. ALICE makes full use of Lua's dynamically-typed nature, allowing you full flexibility with the objects you pass into it.

The main task when working with ALICE is creating actors, its main internal object, and their interaction functions. An actor's properties are fully encoded in those tables and functions. For example, here is the code used to create the reflective barrier that Jaina uses in the showcase video:

Lua:
    --The callback function on collision with the barrier:
    local function ReflectProjectile(barrier, shell, x, y, z, normalSpeed, __, __)
        local phi, theta = ALICE_PairGetAngle3D()
        local shieldEffect = AddSpecialEffect("Abilities\\Spells\\Undead\\ReplenishMana\\ReplenishManaCasterOverhead.mdl", x, y)
        BlzSetSpecialEffectZ(shieldEffect, z)
        BlzSetSpecialEffectYaw(shieldEffect, phi)
        BlzSetSpecialEffectPitch(shieldEffect, -theta + bj_PI/2)
        BlzSetSpecialEffectAlpha(shieldEffect, math.min(128, normalSpeed // 2))
        BlzSetSpecialEffectScale(shieldEffect, 1.2)
        DestroyEffect(shieldEffect)
    end

    --This is the definition of the reflective barrier object:
    ReflectiveBarrier = {
        --These fields are used by ALICE itself.
        identifier = "reflectiveBarrier",
        interactions = {
            projectile = CAT_GizmoCollisionCheck3D --This is a collision detection function included in the collisions library.
        },
        radius = 200,
        bindToOrder = "starfall",
        zOffset = 50,
        --These are additional fields used by the collisions library.
        onGizmoCollision = {
			shell = CAT_GizmoBounce3D,
			other = CAT_GizmoDevour3D
		},
        onGizmoCallback = ReflectProjectile,
        mass = 500,
        collisionRadius = 160,
        elasticity = 0.2,
    }

    ReflectiveBarrier.__index = ReflectiveBarrier

    local function CreateReflectiveBarrier()
        if GetSpellAbilityId() ~= FourCC("Ashi") then
            return
        end

        local barrier = {}
        setmetatable(barrier, ReflectiveBarrier)

        local u = GetSpellAbilityUnit()
        barrier.anchor = u  --The barrier becomes attached to Jaina.

        --This creates the actor for the ReflectiveBarrier table.
        ALICE_Create(barrier)
    end

Motivation

Although the ALICE core was based on my particle system, my motivation for the development of this system was to create A.I. with it, where the interactions would be objects on the map sending data and being evaluated by a bot, who uses this data to calculate its next action. The endless possibilities of Lua soon got me hooked and I started expanding the system and now it is far beyond the scope I had originally intended (I can't stop, please send help!).

I realized that this engine could be used to solve one of the problems that has bugged me in Warcraft 3 for a long time - that you have to treat your own objects (such as particles, missiles) inherently differently from Warcraft 3 objects, since there is no native API, no enum functions, for them. Lua's dynamically typed nature provided an opportunity to remove this separation.

Unfortunately, the heavy reliance on dynamic typing means that ALICE cannot be translated into JASS. However, an interface for Lua-GUI would be a possible in the future.

Features

FeatureDescription
Automatic Type DetectionALICE supports any type of object passed into it, although most of its features specifically support units, destructables, items, and gizmos. Gizmos are defined as tables with coordinate fields and a special effect (missiles and such). ALICE automatically detects the types passed into it and uses the correct functions to retrieve their positions and other properties.
Debug APIALICE features an extensive Debug API. By typing "downtherabbithole", you can enable debug mode and select objects to get a tooltip that displays information about the actors attached to it. While selected, all interactions are shown by a green lightning effect whenever they are evaluated. You can also use hotkeys to pause the cycle or go step-by-step. You can combine the ALICE debug mode with Eikonium's Debug Utils.
High performanceUnlike most other systems, ALICE's core avoids using enum functions entirely. Most missile systems, for example, would search for collisions with units by calling GroupEnumUnitsInRange repeatedly. But calling natives is slow and the process of calling a function repeatedly, even if there is no unit close to the missile, is inefficient itself.

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

ALICE instead divides the map into a grid of cells (spatial hashing), checks interaction ranges with other objects individually, and uses variable interaction intervals to drastically improve performance.
Enum functionsWhile the native functions only allow enumeration of units, destructables, and items, ALICE's Enum API allows for efficient enumeration of any type of object. Although the enum functions are significantly slower than their native counterparts, the fact that they compile the objects into a Lua table instead of a group almost cancels out the speed difference, and even makes them faster in some situations. It also makes those functions much easier to work with.
Self-interactions and CallbacksAll restaurants serve water. Sometimes, we just want to execute code on a single object. ALICE allows for this with its self-interaction and callback APIs. A self-interaction is a periodic function that only affects an object itself.
Async modeSince ALICE is nothing but a giant table rearrangement machine, it can be run completely asynchronously (as long as the code executed with it is async-safe).
Extensive documentationAll API functions are fully documented and properly explained. In addition, there are manuals explaining both the basics as well as more advanced concepts.
Support from meIf you can't figure out how something works or you encounter a bug, leave me a note and I will try my best to fix it.


Installation

Copy the ALICE Config and Script section into your map. The rest of the ALICE files are documentation and it is up to you if you want to import them or not.


Next, make sure you have all the requirements imported. These are:

RequirementDescription
Total InitializationNeeds no explanation.
HookRequired for the default actor creation.
HandleTypeThis handy little library allows ALICE to determine the correct types of the objects passed into it. Since it caches the result, it is fast and efficient.
Precomputed Height Map
(optional)
Highly recommended if you want to do any kind of 3D interactions with ALICE as it improves performance and ensures synchronicity in multiplayer. Otherwise not needed.
CustomTooltip.toc
CustomTooltip.fdf
These are the frame definition files for the actor tooltips in debug mode. Included in the test maps.


Complementary ALICE Templates

CATs are templates for various systems, mostly focused on missiles and physics. They are not required; simply import the ones that you might want to work with. The CATs are:

CATDescription
ObjectsRequirement for all physics-related CATs. Includes the config and a collection of helper functions.
Gizmos, Missiles, Collisions,
Ballistics, Forces
These libraries contain various functions that you can use in the definitions of your objects.
MiceCreates an actor for each player's mouse cursor.
CamerasCreates an actor for each player's camera.
RectsContains helper function for the creation of actors from rects.


Tutorial & Documentation

Basics

Advanced

Debugging


Getting Started

Actors

Interaction Functions

Automatic Actor Creation

Flags


Getting Started

After installing ALICE and its requirements, go to the config and change the MAP_CREATORS field to your name. Preplace some units, trees, and/or items and launch your map. Type "downtherabbithole" to enable debug mode. Now you can click on any object to view the actors attached to it. A tooltip with all the relevant information should pop up when you do. If it doesn't, make sure you have .fdf and .toc files imported correctly.

The actor's description lists all of its identifiers. These identifiers are important when we create interaction functions to tell ALICE which type of objects they should affect. The tooltip should also tell you that these actors are currently unpaired. All automatically created actors passive; they only receive, but do no initiate any pairs.

As you move a unit around, you can see how the actor moves along with it, and how the cells it is currently in are being updated. Interactions can only occur if two actors have one or more overlapping cells.

The main work will be to create actors and their interaction functions, so that they can pair with other actors, including the ones automatically created by ALICE. For any complicated system with different moving parts that we want to build, ALICE is designed to take care of the entire control structure of our code. The task of writing the correct control structure then transforms into assigning the actors the correct properties on creation.

The way of writing code with ALICE might take some time to get used to, but it makes the coding faster and easier once you understand the concepts.

Do you need the ALICE API?

One of the primary strengths of ALICE is that every system can easily be customized and expanded. To do that, you need to have knowledge of the ALICE API. But, you can also just work with the pre-packaged CAT libraries. By doing so, you have a lot less reading to do. However, a rudimentary understanding of the fields you're setting in your tables and why you're setting them might still be advantageous.

The documentation of the CATs is included in their script files.

What is an Actor?

An actor is a class that attaches itself to any type of object and acts as the mediator for interactions between that object and any other object with an actor attached. Actors carry identifiers and function interfaces called interaction tables. When two actors come into interaction range, both actors check whether the identifiers of the other are contained within their interaction table, and if so, they form a pair. For example, an actor attached to a missile might pair with an actor attached to a unit to check for collision.

Once a pair has been created, the two actors will continue to interact until one of them gets destroyed, they leave their interaction range, or the pair is manually disabled. An interaction is a periodic event between two actors, represented by calling an interaction function that you can specify and fully customize. It is up to you to set the time intervals between the interactions. This is done via the return value of the interaction function. Two arguments are passed into the interaction function which specify the objects for which you created the actors, also called hosts. As mentioned earlier, hosts can be any type of objects. However, many features only support widgets and Lua tables, and there is little reason to attach an actor to a player, for example.

Some actors may be passive and only receive pairs, some only initiate pairs, and some actors may do both, all dependent on the properties you assign to them. However, in a pair, only one actor may initiate. Two-way pairs are not possible.

For executing code that only affects one object, such as moving or expiring objects, ALICE offers the convenient self-interaction feature.


How to create an Actor

To create an actor, do ALICE_Create(host, identifier, interactions, flags).

host
Any type of object which the actor is supposed to represent and be attached to. 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"}. It is recommended to always use camelCase formatting for identifiers.

interactions
A table that specifies the functions that are called when this object interacts with other objects. You can specify different interaction functions for different types of objects. For example:
Lua:
interactions = {milk = Drink, apple = Eat}

This will use the Drink function for all actors with the "milk" identifier and the Eat function for all actors with the "apple" identifier. A key can itself be a table, listing all the identifiers an object must have for a pairing to occur with that function. Example:
Lua:
interactions = {
    milk = Drink,
    apple = Eat,
    [{"steak", "grilled"}] = Eat,
    [{"steak", "raw"}] = Grill
}
You can overwrite interaction functions when an additional identifier is present, such as {steak = Eat, [{"steak", "raw"}] = Grill}. You have to make sure that the interaction function is always unambiguous or ALICE will throw an error.

flags
Flags are an optional fourth input argument discussed under flags.


Creating an actor for a table

When creating an actor for a table, you have the option to omit all input arguments except the first. If you do, the identifier, interactions table, and all flags will be retrieved directly from the table fields.

When using table hosts, there are special, hard-coded fields used by ALICE. For the coordinates of the object, you must use "x", "y", and "z". The z-coordinates are optional. "visual" is the visual representation of the object (most likely a special effect, but can also be a dummy unit or image). "owner" is the owner, which can either be a player or a converted player id. Example:
Lua:
local obelisk = {
    x = GetSpellTargetX(),
    y = GetSpellTargetY(),
    owner = GetOwningPlayer(GetSpellAbilityUnit()),
    visual = AddSpecialEffect(modelPath, GetSpellTargetX(), GetSpellTargetY()),
    identifier = "obelisk",
    interactions = {unit = HealingAura}
}

ALICE_Create(obelisk)

Note: All fields must be set before ALICE_Create is called. Changing fields afterwards has no effect. Instead, you should use the appropriate API function to change an actor's properties after creation.

Creating an Interaction Function

An interaction function has two input arguments of any type, specifying the interacting objects, and an optional return value of type number, specifying the time until the next interaction (in seconds). The object that carries the interaction function in its interactions table (the male actor) will always be passed as the first argument.

Within the interaction function, you have access to the ALICE_Pair functions. Example:
Lua:
function StepOnMine(mine, unit)
    local dist = ALICE_PairGetDistance2D()
    if dist < 100 then
        ALICE_Kill(mine)
        ALICE_Kill(unit)
    end
    return (dist - 100)/522
end

If you want the interactions to be executed every step, you can omit the return value. This reduces the overhead required and improves performance. Note, however, that an interaction function must either always return a value or never (return 0 instead of nil).

Self-Interaction Functions

Self-interaction is a convenient way to execute code that affects the object itself, such as moving a missile. You can use the "self" keyword in an interactions table to declare a self-interaction function. Example:
Lua:
interactions = {missile = DestroyMissile, self = Move}

You can pass a table to declare multiple self-interaction functions, which is not possible with regular interaction functions. Example:
Lua:
interactions = {
    missile = DestroyMissile,
    self = {
        Move,
        Accelerate
    }
}
In a self-interaction function, the actor's host is passed as both arguments. This can be useful if you're creating an interaction that both affects the object itself and those around it (an aura for example). But you can discard the second input argument most of the time.

Automatic Actor Creation

Very likely, all the actors you create will be attached to widgets or Lua tables. For your custom objects to interact with widgets, those widgets must have actors attached to them, or ALICE will not recognize them. To this end, ALICE will automatically create actors for units, destructables, and items using hooks and triggers. This automatic actor creation can be customized in the config.

Automatically created actors are passive and do not initiate pairs themselves, but can receive pairs from other actors to create interactions.

Units
Unit actors get the "unit" identifier, the fourCC code, as well as any classifications added to the ALICE_Config.UNIT_ADDED_CLASSIFICATIONS table. Upon death, the actor is not destroyed, but its "unit" identifier is swapped with "corpse". Because of this, if you want an actor to only pair with alive units, always include the "unit" keyword.

Destructables
Destructable actors get the "destructable" identifier and the fourCC code.

Items
Item actors get the "item" identifier and the fourCC code.

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.

Example:
Lua:
ALICE_Create(host, identifier, interactions, {isStationary = true, radius = 200})

radius
Overwrite the default object radius.

To increase performance, ALICE divides the map into a grid of cells (spatial hashing). The cells track where the different actors are on the map. Each actor is considered to be in a cell if its box intersects with it. Two actors can only interact if they share at least one cell. They will start interacting as soon as one enters a cell the other is in already.

Imagine we want to create an aura that heals units in a radius of 500. In our interaction function, we do the distance check and apply the healing if it is less than 500. The function will not be called unless the two objects share a cell, which prevents the system from having to check the distance of two objects on opposite sides of the map. But, this also means we have to be mindful of the size of the aura's actor.

This is why the radius flag is important. The actor's radius in the example has to be at least 500, so that the two actors start interacting before they are within the healing range. Some additional leeway is recommended. Note that the range check done by ALICE is inaccurate and you still have to check the exact distance within the interaction function.

anchor
Overwrites the host as the source of the actor coordinates with the object provided in anchor. This flag is incredibly useful if you want to pass as the host a data table attached to a unit or other type of object. Then you can anchor the actor to the unit directly and omit coordinate fields in the table.

Setting an anchor also prevents actors anchored to the same object from pairing.


Advanced Flags

These flags are used for optimization or to solve very specific issues and are therefore more niche.

selfInteractions
An alternative way to add selfInteractions to an object. Use either this flag or the self key in the interactions table.

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

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

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

zOffset
Moves the z-position of the object relative to its host or anchor. This modifies the behavior of ALICE_PairGetDistance3D() and similar functions.

cellCheckInterval
The cells an object is in is not updated instantly. Instead, ALICE checks periodically if the object has left a cell and/or has entered a cell. A fast-moving object must be checked more often than a slow-moving object, so with cellCheckInterval you can overwrite the default value that is set in the config.

persistOnDeath
Do not destroy the actor automatically when the unit that is its host or anchor dies. This is useful for actors attached to units that should be reactivated when that unit is revived. Only has an effect if UNITS_LEAVE_BEHIND_CORPSES is enabled.

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.

hasInfiniteRange
If this flag is set to true, the actor will interact over any distance like a global actor, except it still uses coordinates. More performant than setting the radius to a very high value.

width / height
These flags are similar to the radius flag, but can be used to create a non-square actor.

isUnselectable
Actor cannot be selected in debug mode.


Adding Interactions to Widgets

Managing Pair Data

Disabling Interactions

Accessing Actors

Asynchronous Code


Adding Interactions to Widgets

When you want to add interactions to widgets, there are multiple approaches. We can create a new actor on top of the already existing one. For example, here is an actor that represents a fire aura that damages all units in 500 range and is bound to a buff on the unit:
Lua:
function FireAuraDamage(source, target)
    local dist = ALICE_PairGetDistance2D()
    if dist < 500  then
        UnitDamageTarget(source, target, DAMAGE_AMOUNT, DAMAGE_TYPE_MAGIC, ATTACK_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
    end
    return 0.5
end

function OnFireAuraBuff()
    local identifier = "fireAura"
    local interactions = {unit = FireAuraDamage}
    local flags = {radius = 500, bindToBuff = "Bfir"}
    ALICE_Create(GetSpellAbilityUnit(), identifier, interactions, flags)
end

If we want the aura to damage the unit itself, we can modify the interactions table by adding it as a self-interaction:
Lua:
local interactions = {unit = FireAuraDamage, self = FireAuraDamage}

Because the host is passed as both arguments into the interactionFunc in a self-interaction, the unit will damage itself.

As soon as we go to more complicated interactions, where we want to read additional data or store data for the unit, there is a more convenient method. This method involves creating an additional, intermediate table as the host and anchoring it to the unit:
Lua:
function OnFireAuraBuff()
    local fireAura = {
        identifier = "fireAura",
        interactions = {unit = FireAuraDamage, self = FireAuraDamage},
        anchor = GetSpellAbilityUnit(),
        radius = 500,
        bindToBuff = "Bfir"
    }
    ALICE_Create(fireAura)
end

The problem now is that the target in the self-interaction points towards the table, not the host unit. But we can fix this problem easily by modifying the interactionFunc:
Lua:
function FireAuraDamage(fireAura, target)
    local dist = ALICE_PairGetDistance2D()
    if dist < 500  then
        UnitDamageTarget(ALICE_GetAnchor(fireAura), ALICE_GetAnchor(target), DAMAGE_AMOUNT, DAMAGE_TYPE_MAGIC, ATTACK_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
    end
    return 0.5
end

The ALICE_GetAnchor function returns the unit itself if target is a unit and the host unit if it is the fireAura table, so you're never trying to damage a table with the UnitDamageTarget function.

Managing Pair Data

ALICE offers various functions to store data for pairs and to execute code based on the current state of the pair.

Imagine we want to create an interaction where a lightning chain is created between two units whenever they are 500 or closer. When the units are in range, if it's the first time they are in range, we want to create the lightning, otherwise we want to move it. If they are not in range, we want to destroy the lightning if it exists.

For the creation of the lightning, we can use the ALICE_PairIsFirstContact function. We can store the data in a table that we get with ALICE_PairLoadData.
Lua:
function LightningChain(unitA, unitB)
    local data = ALICE_PairLoadData()
    if ALICE_PairGetDistance2D < 500 then
        local x1, y1, x2, y2 = ALICE_PairGetCoordinates2D()
        if ALICE_PairIsFirstContact() then
            data.lightning = AddLightning("CLPB", false, x1, y1, x2, y2)
        else
            MoveLightning(data.lightning, x1, y1, x2, y2)
        end
    elseif data.lightning then
        DestroyLightning(data.lightning)
        data.lightning = nil
    end
end

The problem with this implementation is that, when one of the units dies, or the units leave ALICE's interaction range, the function stops being executed and the DestroyLightning line is never reached, therefore leaking the lightning. To solve this issue, you can declare onDestroy functions. onDestroy functions are assigned to interaction functions.

ALICE_FuncSetOnDestroy is executed when the pair is destroyed. ALICE_FuncSetOnBreak is less strict than OnDestroy, as it is also executed when the pair leaves the interaction range. It would be better suited for this situation.
Lua:
function LightningChainOnBreak(__, __, data)
    if data.lightning then
        DestroyLightning(data.lightning)
        data.lightning = nil
    end
end

function LightningChain(unitA, unitB)
    local data = ALICE_PairLoadData()
    if ALICE_PairGetDistance2D < 500 then
        local x1, y1, x2, y2 = ALICE_PairGetCoordinates2D()
        if ALICE_PairIsFirstContact() then
            data.lightning = AddLightning("CLPB", false, x1, y1, x2, y2)
        else
            MoveLightning(data.lightning, x1, y1, x2, y2)
        end
    elseif data.lightning then
        DestroyLightning(data.lightning)
        data.lightning = nil
    end
end

ALICE_FuncSetOnBreak(LightningChain, LightningChainOnBreak)

The above implementation works better, but it still has a problem: The lightning is created only the first time the units come into range, but not the second time, because the ALICE_PairIsFirstContact function will return false. To solve this issue, we use the ALICE_PairReset function and replace ALICE_FuncSetOnBreak with ALICE_FuncSetOnReset. The ALICE_PairReset function resets ALICE_PairIsFirstContact and calls the OnReset function only if it was reset. Using the reset feature also simplifies our code.
Lua:
function LightningChainOnReset(__, __, data)
    DestroyLightning(data.lightning)
end

function LightningChain(unitA, unitB)
    local data = ALICE_PairLoadData()
    if ALICE_PairGetDistance2D < 500 then
        local x1, y1, x2, y2 = ALICE_PairGetCoordinates2D()
        if ALICE_PairIsFirstContact() then
            data.lightning = AddLightning("CLPB", false, x1, y1, x2, y2)
        else
            MoveLightning(data.lightning, x1, y1, x2, y2)
        end
    else
        ALICE_PairReset()
    end
end

ALICE_FuncSetOnReset(LightningChain, LightningChainOnReset)

This code structure can be used for almost all problems that involve creating effects/lightnings etc.

In a situation where we want to set up data tables or triggers etc. on creation of a pair, we do not use ALICE_PairIsFirstContact. Instead, we use the feature to add initializers to functions with ALICE_FuncSetInit. We can use ALICE_FuncSetOnDestroy to clean-up the data if necessary, but ALICE will take care of the data table clean-up in most situations.

Disabling Interactions

There are several ways to disable interactions between objects, each with their advantages and disadvantages.

Early Returns
The easiest method to disable an interaction is to put an early return into the interactionFunc based on some condition. All other methods are optimizations that are only relevant when performance is a major issue.

Here is a fire that deals damage only to enemy units:
Lua:
function FireDamage(fire, unit)
    if not ALICE_PairIsEnemy() then
        return 0.5
    end
    if ALICE_PairGetDistance2D() < 250 then
        UnitDamageTarget(fire.source, unit, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
    end
    return 0.5
end

The advantage of this approach is that, if a unit is mind-controlled, the fire will start dealing damage, because the pair still exists.

Disabling Pairs
If there's no possibility for a unit to change owners or for players to change alliances, we can disable the pair completely with ALICE_PairDisable:
Lua:
function FireDamage(fire, unit)
    if not ALICE_PairIsEnemy() then
        ALICE_PairDisable()
        return 0.5
    end
    if ALICE_PairGetDistance2D() < 250 then
        UnitDamageTarget(fire.source, unit, 25, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
    end
    return 0.5
end

A further optimization could be to disable the pair on creation:
Lua:
ALICE_FuncSetInit(FireDamage, function()
    if not ALICE_PairIsEnemy() then
        ALICE_PairDisable()
    end
end)

A disabled pair can be restored with the Pair Access function ALICE_Enable, but this function is difficult to use, because you have to provide the objects of each pair that needs to be restored.

Changing Identifiers
You can disable interactions by removing identifiers that are required for the interaction. For example, let's say the interaction table for the fire is:
Lua:
interactions = {
    [{"unit", "flammable"}] = FireDamage,
    [{"destructable", "flammable"}] = FireDamage
}
Then, interactions can be enabled and disabled with:
Lua:
ALICE_AddIdentifier(object, "flammable")
ALICE_RemoveIdentifier(object, "flammable")

Pausing Pairs
Like ALICE_PairDisable, ALICE_PairPause is called from within an interactionFunc and disables the pair until it is restored by another function. The differences are that pausing a pair preserves any data stored and unpausing is done on a per-function basis instead of a per-pair basis. Pausing should be viewed as a less permanent disable.

Let's say our fire has a strength that can change over time. It should only deal damage when it's strength is 10 or greater:
Lua:
function FireDamage(fire, unit)
    if fire.strength < 10 then
        ALICE_PairPause()
        return 0.5
    end
    if not ALICE_PairIsEnemy() then
        ALICE_PairDisable()
        return 0.5
    end
    if ALICE_PairGetDistance2D() < 250 then
        UnitDamageTarget(fire.source, unit, 5*(fire.strength - 10), false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
    end
    return 0.5
end

Within the function that controls the strength of the fire, we can unpause all paused pairs that use the FireDamage function when the fire's strength grows above the threshold:
Lua:
if oldStrength < 10 and newStrength >= 10 then
    ALICE_Unpause(fire, FireDamage)
end

Accessing Actors

Whenever an actor is created, a reference is stored on both the host and the anchor. Example:
Lua:
local myAura = {
    identifier = "aura",
    anchor = myUnit
}
ALICE_Create(myAura)

ALICE_HasActor(myUnit, "unit") --true
ALICE_HasActor(myUnit, "aura") --true
ALICE_HasActor(myAura, "aura") --true
ALICE_HasActor(myAura, "unit") --false

Every function that takes an object as an argument calls the same function to retrieve the correct actor. If the keyword parameter is omitted, the logic as to which actor is returned is as follows:
  • If the function is called from an interaction function or pair callback function, the actor that's part of the current pair is returned.
  • Otherwise, actors for which the object is the host, not the anchor, are prioritized.
  • The oldest actor is returned. For units, this might not always be the default unit actor, when there's a second actor created for it immediately after creation of the unit.

Unless you're calling an object function from an interaction function, omitting the keyword parameter should be discouraged, as it can easily lead to bugs as a system grows in complexity.

Asynchronous Code

ALICE can be used to run code asynchronously (in GetLocalPlayer() blocks), as long as the usual safety precautions are followed.

  • Do not create or destroy handles asynchronously.
  • Do not affect the game state asynchronously.
  • Do not use random functions asynchronously.

If these rules are followed, any function that is not a debug function is async-safe. For example, you can do
Lua:
if GetLocalPlayer() == whichPlayer then
    ALICE_CallDelayed(function() BlzFrameSetVisible(whichFrame, false) end, 2.5)
end
instead of
Lua:
ALICE_CallDelayed(function() 
    if GetLocalPlayer() == whichPlayer then
        BlzFrameSetVisible(whichFrame, false)
    end
end, 2.5)

Even actors can be created and destroyed asynchronously as long as they do not carry any interactions that require synchronicity.

There is one niche exception: If you create an actor anchored to an item or destructable and that widget has had no actors before, it will create a death trigger and therefore cause a desnc.


Debug Mode

Troubleshooting

Investigation


Debug Mode

You can enable debug mode with the ALICE_Debug() function. You can also enable debug mode ingame by typing "downtherabbithole". 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 with lightning effects. You can combine debug mode with Eikonium's IngameConsole to execute ALICE debug functions. Actor tooltips requires "CustomTooltip.toc" and "CustomTooltip.fdf". Set your interactionFuncs to global so that their names can get displayed!

In debug mode, you have access to these key shortcuts:

KeyDescription
CTRL + QCycle through the different actors anchored to the selected host.
CTRL + WLock selection so that clicking somewhere does not deselect the currently selected actor.
CTRL + THalt the cycle. Hold SHIFT to pause all units.
CTRL + RExecute the next step of the cycle.
CTRL + GToggle printing the function names of all invoked interaction functions for the selected actor.

Troubleshooting

The two most common bugs that you will encounter are thread crashes and actors not interacting.

Thread Crashes

If your custom code crashes, because ALICE uses a single thread, the entire cycle will crash. You can toggle HALT_ON_FIRST_CRASH in the config to set whether a repair of the cycle should be attempted.

The most common cause of a crash is that a variable required for an interaction function is nil. This may be because you forgot to set a table field, you misspelled the key, you flipped the input arguments of the interactionFunc, or the actor erroneously pairs with an actor whose host is not supposed to hold that field or even has the wrong type.

The ALICE_RecognizeFields and ALICE_RequireFields functions are useful in helping you with the first two cases. You can also enable the Ingame Console, enable debug mode, and type ALICE_TrackVariables("varName") to check whether the fields that are supposed to be present are.

To check whether an actor was paired with another actor it is not supposed to, investigate the actors listed in the crash message. Enable the ingame console and type ALICE_Select(N), where N is the unique number of the actors listed.

Not destroying an actor when its 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, this might also be the source of a crash. Actors attached to widgets will be cleaned up automatically, but for actors attached to tables, you have to do it manually. Always use ALICE_Kill to remove an object, so that you always destroy all actors attached to it. The object's .destroy method will be called by ALICE_Kill if it exists.

Actors not interacting

This can be due to actors not having the intended pairing behavior, or because of the radius of the actors being too small.

To check if the pairing behavior is correct, check the actor tooltip in debug mode.

If you select an actor, you will see the cells it is currently in. This will incidate its interaction range. 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.

To check the state of a pair, select the actors in debug mode, then do ALICE_GetPairState(N1, N2), where N1 and N2 are the unique numbers of the actors. The pair state can give you additional information about what might be the issue.

Other Errors

C-Stack Overflow
This error occurs when you nil the coordinate fields of a table host without destroying the actor.

Investigation

Debug Utils is incredibly useful when it comes to finding bugs that result in thread crashes. But, because Warcraft III does not offer any debug tools worth mentioning, other, less severe bugs are often much harder to find. This is especially the case when the bug only occurs in a few corner-case scenarios. Often, the only out is to spam the screen with print messages until the bug occurs and then scour through the messages to find the crucial line.

ALICE improves the debugging experience by allowing you to track variables in the actor tooltips. Consider that our interactionFunc has the following structure:
Lua:
function InteractionFunc(objectA, objectB)
    if not condition1 then
        return
    end
    if not condition2 then
        return
    end
    if not condition3 then
        return
    end

    DoStuff(objectA, objectB)
end

Sometimes, one of the conditions fails and the DoStuff function isn't called when it should be. Instead of bombarding the function with print statements, we store debug variables in the object tables.
Lua:
function InteractionFunc(objectA, objectB)
    objectA.failedCondition1 = nil
    objectA.failedCondition2 = nil
    objectA.failedCondition3 = nil
    if not condition1 then
        objectA.failedCondition1 = true
        return
    end
    if not condition2 then
        objectA.failedCondition2 = true
        return
    end
    if not condition3 then
        objectA.failedCondition3 = true
        return
    end

    DoStuff(objectA, objectB)
end

We can now make these table fields get displayed in the actor tooltips by using ALICE_TrackVariables, either through the IngameConsole or by calling it from the Lua root.
Lua:
ALICE_TrackVariables("failedCondition1", "failedCondition2", "failedCondition3")
As soon as the bug occurs, we halt the cycle by pressing Ctrl + T and select the actor that misbehaved. We can now go step-by-step by pressing Ctrl + R.

To make this method work for hosts that aren't tables, create global tables and store the value at the key of the object.
Lua:
FailedCondition1 = {}
FailedCondition2 = {}
FailedCondition3 = {}

function InteractionFunc(objectA, objectB)
    --[...]
    FailedCondition1[objectA] = true
    --[...]
end

ALICE_TrackVariables("FailedCondition1", "FailedCondition2", "FailedCondition3")



Change Log

Version 2.5.2
  • Updated the Cinematic Map to be compatible with the latest version.
  • Added the ALICE_FindIdentifier function, which returns the first entry in a list of identifiers for which an actor exists for a specified object.
  • selfInteractions is now a recognized flag that can be used as an alternative way to add self-interactions to an object.
  • Minor perfomance optimizations.
  • Fixed a typo that caused the ALICE_FindField function to not do anything (kekw).
  • Fixed a bug that caused the onCollision functions to sometimes crash when calculating the impact of a very fast object.
  • Fixed a bug that caused the CAT_AnimateShadow function to crash when the optional fields shadowX and shadowY were not set.
  • Fixed an issue that prevented the CAT_Knockback function form working on corpses.

Version 2.5
  • Unit actors are now available immediately after calling CreateUnit.
  • Revamped how units and items that are being hidden, loaded into transports, and/or picked up by units are handled. The actors are now preserved, but are suspended from all interactions until they reappear on the map.
  • Added the ALICE_FuncSetUnsuspendable function, which prevents an interaction from being disabled when an object is hidden, loaded into a transport, or picked up.
  • Creating a corpse with the CreateCorpse native will now create a corpse actor if UNITS_LEAVE_BEHIND_CORPSES is enabled.
  • The ALICE_SetAnchor, ALICE_SetRadius, and ALICE_SetCellCheckInterval functions have been removed and consolidated into the ALICE_SetFlag function, which can modify the values of most actor flags.
  • Added the ALICE_GetFlag function.
  • The persistOnDeath flag now only works on units.
  • Widget death event hooks are now executed before all actors are cleared.
  • Added an option to the config to overwrite ALICE's table recycling functions with external ones.
  • Updated the Cameras CAT, which used the removed ALICE_SetRadius function.
  • Fixed a bug that caused the destroy methods of the hosts of additional actors anchored to an object to not be executed upon the parent object's death.
  • Fixed a typo that caused some tables to prevent garbage collection.

Version 2.4.2
  • Fixed a bug that caused the periodic callback functions to not delay the first call by the correct amount if the specified delay exceeded the ALICE_Config.MAX_INTERVAL.

Version 2.4
  • Added MINACE, a stripped-down version of ALICE.
  • The fourCC codes of widgets are now added to the default actors' identifiers.
  • The ALICE_CallPeriodic function can now capture additional arguments and the interval is no longer limited by MAX_INTERVAL.
  • Added the ALICE_CallRepeated function, which works like ALICE_CallPeriodic, but executes the function a set number of times.
  • Added the ALICE_DisablePeriodic function, which should be used to disable periodic callbacks instead of ALICE_PairDisable.
  • Removed the ALICE_CallAt function.
  • Added the ALICE_GetPairState function for debug mode.
  • Removed the ALICE_SlowMotion function.
  • Removed the MAXIMUM_EVALUATION_TIME config parameter and the in-built freeze protection.
  • Changed the ALICE_Statistics function to print function names instead of actor identifiers.
  • The CAT_MoveArcedHoming function now recognizes the arcAngle field to allow missiles to not only arc vertically.
  • The CAT_UnitCollisionCheck function now recognizes the onlyTarget field, which excludes all units from the collision check other than the gizmo's target.
  • Instead of creating one death trigger for each actor, now at most one death trigger is created for each destructable and item.
  • Added the missing onDestructableEnter, onDestructableDestroy, onItemEnter, and onItemDestroy event hooks.
  • In debug mode, when clicking on a spot with multiple actors, you will now properly cycle through the different actors instead of repeatedly selecting the same one.
  • Fixed a bug that caused the CAT_MoveArced function to crash when the .arcAngle field was not set.
  • Fixed a bug that caused a single table passed as an additional argument into ALICE_CallDelayed to not be forwarded into the callback function.

Version 2.3.3
  • Fixed a bug that caused actors to not be destroyed properly when multiple actors are anchored to a widget (this time for real).

Version 2.3.2
  • Fixed a bug with the Forces CATs.
  • Added some missing annotations and fixed a find-and-replace mishap in the config annotations

Version 2.3
  • Asynchronous actors can now be paired with synchronous actors without risking a desync.
  • ALICE will now throw an error when the interaction function between two actors is ambiguous.
  • Shadowing interaction functions (such as {"unit" = DamageUnit, [{"unit", "hero"}] = DamageHero}) is now supported.
  • Improved config annotations. Config variables are not stored in the ALICE_Config table.
  • The destroyOnDeath flag has been replaced with persistOnDeath, which does the opposite. By default, all actors are now destroyed on a widget's death.
  • Added the width and height flags for actors, which can be used to create non-square actors.
  • The ALICE_Kill function can now also destroy unit- and image-based visuals.
  • Added the Rects CAT, which contains helper functions for creating actors from rects.
  • Added the onUnitChangeOwner option for ALICE_OnWidgetEvent.
  • Added the ALICE_SetInteraction function, which can modify the interaction table of an actor after creation.
  • Added the ALICE_FindField function, which returns the field in a table whose key matches an object's identifier.
  • Added the ALICE_GetAnchoredObject function, which returns objects anchored to the specified object.
  • Removed the ALICE_GetActor and ALICE_ForAllActorsDo functions.
  • ALICE_HasActor now has an optional flag to exclude actors only anchored to the object.
  • Added the ALICE_FuncSetName function so that interaction functions don't have to be globals in order for their names to be displayed in debug tooltips.
  • Pair Access functions can now access self-interactions by passing a function as the second argument.
  • The CAT_AnimateShadow function now uses images instead of special effects.
  • In the Collisions CAT, passing a table for onObjectDamage now works.
  • Fixed a bug that caused a destructable or item being removed or picked up to not destroy all actors if it has multiple.
  • Fixed a bug that caused ALICE_Kill to not destroy all actors on an object with multiple actors.
  • Fixed an issue that caused anchoring to an object that is itself anchored to another object to not work.

Version 2.2.3
  • Fixed a bug that caused item actors to have the wrong position when an item is dropped by a hero.
  • Fixed a bug that caused objects with the .hasInfiniteRange flag to be ignored by enum functions.
  • Fixed an issue that caused destructables to not receive an actor after being revived.
  • Removed AUTO_DEBUG_MODE and AUTO_SELECT config options. Instead, added ALICE_Debug function to enable debug mode.
  • Stationary flags are now always kept consistent among multiple actors attached to one object.

Version 2.2.2
  • Fixed a bug that caused the engine to crash when passing a non-widget handle as the host.
  • Fixed a bug that caused unpredictable behavior when changing the identifiers of global actors.
  • Classifications to unit actors can now be added individually.
  • Added the ALICE_OnWidgetEvent function. This function creates hooks on the widget event hooks (sup dawg) used by ALICE to create the default widget actors, allowing users to easily manage their own widget actors.
  • Added the ALICE_IsStationary function.
  • Added an optimization that caches the interaction function for two actor types, allowing it to be retrieved faster the next time two actors of the same types meet.

Version 2.2
  • Enabled asynchronous code execution. Pairs and actors can now be created asynchronously without risking a desync as long as certain rules are being followed.
  • Added the ALICE_EnumObjectsInLineSegment and ALICE_ForAllObjectsInLineSegmentDo functions.
  • Added the ALICE_GetClosestObject function.
  • Added the Cameras CAT, which creates an actor for each player's camera.
  • Various bug fixes.

Version 2.1.4
  • Accidently broke the Rockslide example in the test map in the last update. This has been fixed.
  • OnCreation hooks now always accept function arguments as advertised and now work properly in (hopefully) all situations.
  • ALICE_PairCooldown now returns the remaining cooldown instead of a boolean.
  • Added ALICE_FuncPauseOnStationary to declare a function that should always get paused when an object becomes stationary.
  • Removed the random delay option in ALICE_FuncSetDelay, as it is a possible cause of desync. Instead, added ALICE_FuncDistribute function to allow spreading out function calls over an interval.

Version 2.1.3
  • Fixed a bug that caused newly created objects to sometimes only start interacting when entering a new cell.
  • Fixed a bug that caused changing identifiers to sometimes crash the engine.

Version 2.1.2
  • Fixed the ALICE_PairCallDelayed function.
  • Added missing hooks for RemoveDestructable and RemoveItem that prevent actors from leaking.
  • All actors now get properly cleared on a widget death or removal, preventing possible leaks of additional actors.
  • ALICE_PairPause is now more reliable.
  • ALICE_PairCooldown now allows starting multiple cooldown.
  • The Matrix API has been removed and replaced with the Pair Access API, which is faster, more flexible, and less confusing.
  • Reduced memory requirement.

Version 2.1
  • The Units, Destructables, Items and Widgets CATs have been removed and incorporated into the main script.
  • Added unrecognized field warnings when using tables as hosts.
  • New hotkeys for debug mode: Halt, Resume, Next Step, Lock Selection, and Print Functions.
  • In debug mode, interaction visualization lightnings now last until at least the next step.
  • The interactionFunc table field has been renamed to interactions.
  • Functions are no longer permitted as the input for interactions. It now has to be a table. You can now list multiple identifiers in the key of an interactions table entry. The "other" keyword has been removed.
  • The pairsWith flag has been removed.
  • The cell size is now entered in the config, instead of the amount of cells.
  • Removed the ALICE_CreateFromClass function and incorporated its functionality into ALICE_Create.
  • Entities in the CATs have been renamed to gizmos.
  • Overhauled the pair data API. The ALICE_PairOnDestroy function has been removed and replaced by ALICE_FuncSetOnDestroy, ALICE_FuncSetOnBreak, and ALICE_FuncSetOnReset. ALICE_PairForget has been renamed to ALICE_PairReset.
  • Added the ALICE_TrackVariables function, which allows tracking of additional variables in actor tooltips.
  • Added the ALICE_PairPreciseInterval function to allow interaction intervals that are not integer multiples of the minimum interval.
  • Added the Callback API.
  • Added the ALICE_GetAnchor function.
  • Added the Modular API. It contains the OnCreation functions, which can inject properties into actors on creation and the ALICE_IncludeType and ALICE_ExcludeType functions, which overwrite the config for deciding which widgets receive actors.
  • In the Collisions CAT, the onWidgetDamage feature now works without declaring an onCollision function. onWidgetDamage can now be a function, not just a number. Added the option to disable friendly fire. The documentation has been improved.
  • Added the Mice CAT, which creates an actor for each player's mouse.

Version 2.0
  • Added ready-to-use libraries - Objects, Entities, Missiles, Collisions, Ballistics, and Forces.
  • Return values in interaction functions are now optional. If the return value is omitted, ALICE will automatically put pairs using that function into the optimized EveryStep cycle.
  • Added API functions to add or remove self-interactions.
  • Added the Matrix API. These functions can be used to write data tables and share data between object pairs.
  • ALICE now uses the WidgetType library to infer the correct type of objects passed into it.
  • To improve performance with widgets, Math API functions now use cached values for object coordinates that get reset each cycle.
  • Freeze protection is now based on evaluation time instead of number of pairs.
  • ALICE_Benchmark can now be used to view the evaluation time of each cycle.
  • Added the HALT_ON_FIRST_CRASH option in the config.
  • Reorganized API into Core and Advanced API.


To-Do List
  • Fix bugs related to ballistic movement around cliffs.
  • Add a better knockback system.

Credits

Cinematic Showcase Map
Fighting Spirit for Spellbringers
Edited by Vinz

Bullet/Muzzle Flash Models by Vinz
Arcane Explosion Model by Suselishe
Starsphere by ILH
Battlecruiser and Carrier Model Imports by Daratrix
Plasma Blast Model by nGy
Missile Model by Vinz

Hook and TotalInitialization by Bribe

Powered by Eikonium's Debug Utils.
Previews
Contents

A.L.I.C.E 2.5.2 Cinematic Map (Map)

ALICE 2.5.3 Test Map (Map)

ALICE API (Binary)

ALICE Main Script (Binary)

CATs (Binary)

CustomTooltip.fdf (Binary)

CustomTooltip.toc (Binary)

MINACE (Binary)

MINACE API (Binary)

Reviews
Wrda
Awarded Director's Cut due to how this resource expands beyond the concept of handling missiles: has an optimized algorithm for its own enumerations of units, destructibles, items, special effects and objects; has a great variety of API for one to add...
as a casual user with knowledge only to gui this looks like basic chinese to me
but there is so much potentail here! hope it gets approved :peasant-thumbs-up-cheers:
Thanks :plol: Yea, it's a bit too technical mumbo-jumbo right now, but I hope there can be a GUI-friendly version in the future, or something built with it that is GUI-friendly.
 
Saying this resource is extensive is a severe understatement. I am glad to see someone else using spatial hashing... The performance you can get from these algorithms are amazing. I actually have the problem where the effect models themselves are more expensive to compute than the collision/pathing logic. With that being said we can reduce the number of table lookups from table[x][y] to table[index]. You can do this by doing:
index = x + y * 1e7 (works for any map size)

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

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

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

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

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

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

Welcome to hive btw :plol:
 
Level 9
Joined
Sep 27, 2016
Messages
112
Haha, yea, you could build a sick RPG with it :psmile:. I'm actually looking into camera movement systems right now. Are you interested in that?

Thanks for the stars!

exactly, there no cure for that! lol

hmm.. like playing warcraft with 3d camera movement, but not always losing sight when walking under giant trees or the camera falling when walking through the hills?
 
exactly, there no cure for that! lol

hmm.. like playing warcraft with 3d camera movement, but not always losing sight when walking under giant trees or the camera falling when walking through the hills?
Hm, I'm not that far yet. Currently looking into ways to accurately track the mouse movement so you can move the camera with your mouse like in WoW. But you're talking about using the engine for the camera movement? If so, that's difficult, because camera positions are not synced and the engine needs to stay synced.

Do you have a map in mind that has that camera movement you're describing?

Thank you!
 
Level 9
Joined
Sep 27, 2016
Messages
112
Hm, I'm not that far yet.

bro, thats not too far.. you just managed to make the nearby trees faded and with physic/collision on progress..

Currently looking into ways to accurately track the mouse movement so you can move the camera with your mouse like in WoW.

that interesting.. cant wait for this.. really..

But you're talking about using the engine for the camera movement? If so, that's difficult, because camera positions are not synced and the engine needs to stay synced.

no, i meant not the camera thats moving, but my movement with 3rd camera.. sorry for my english.. maybe you can adjust the camera's distance of view when you near small trees / big trees / giant trees with accurate size of the trees or just simply hide the trees.. and adjust camera's height/offset when you walk on low/high ground with accurate height of the ground and the camera.. due to respect, you are the wizard of this matters.. im just a person who is always amazed by magic..

Do you have a map in mind that has that camera movement you're describing?

sorry, no longer have the maps, but Shadow of the Past and Daemonic's sword is not bad..
 
Last edited:
no, i meant not the camera thats moving, but my movement with 3rd camera.. sorry for my english.. maybe you can adjust the camera's distance of view when you near small trees / big trees / giant trees with accurate size of the trees or just simply hide the trees.. and adjust camera's height/offset when you walk on low/high ground with accurate height of the ground and the camera.. due to respect, you are the wizard of this matters.. im just a person who is always amazed by magic..
Ah, I see what you mean now. I totally forgot about the tree transparency thing in my video. :plol:

Yes, when you have a 3rd person camera like in WoW, it "collides" with objects and walls and goes around them. So, you'd need some kind of collision detection with the camera and the objects. Same thing if you want to make the objects transparent. The example in the video is really simple compared to a 3rd person camera in a 3D world. It's also based on the position of the hero and not on the position of the camera.

The collision detection could be done with ALICE. You would create an object that represents the camera that moves around the world. I would just need to upgrade the engine to allow interactions to be executed asynchronously if it's for a multiplayer map. One player has the camera over at location A and the camera is trying to go around tree B, the next player has the camera somewhere else and doesn't see the same collision detection being performed. This must not cause a desync.

That's my current thinking. Of course, there could be easier ways that I haven't considered yet.

sorry, no longer have the maps, but Shadow of the Past and Daemonic's sword is not bad..

They both just have regular game camera during gameplay, do they not?
 
Level 9
Joined
Sep 27, 2016
Messages
112
They both just have regular game camera during gameplay, do they not?

as i remember, they have 3rd person camera, but you know they are old maps, so i dont want to judge or anything, but sometimes i need to return the camera view back to normal view during gameplay :D ..

The collision detection could be done with ALICE. You would create an object that represents the camera that moves around the world. I would just need to upgrade the engine to allow interactions to be executed asynchronously if it's for a multiplayer map. One player has the camera over at location A and the camera is trying to go around tree B, the next player has the camera somewhere else and doesn't see the same collision detection being performed. This must not cause a desync.

that what i was talking about :D.. camera's distance get closer to the hero, if there a wall or trees behind the hero, but not change the view front the hero.. i mean it will change the field of view..
 
as i remember, they have 3rd person camera, but you know they are old maps, so i dont want to judge or anything, but sometimes i need to return the camera view back to normal view during gameplay :D ..
Well, hopefully a mouse-controlled camera doesn't turn out to be too jank, so RPG maps can use that :psmile:. With such a camera, you would also need WASD movement then. I can't get over the fact that none of these RPGs have their own movement system (at least drag-move) and it's just clicking everywhere while your hero is constantly spamming "A sound plan." "Of course." "For honor." "A sound plan." and so on.
 
Level 9
Joined
Sep 27, 2016
Messages
112
Well, hopefully a mouse-controlled camera doesn't turn out to be too jank, so RPG maps can use that :psmile:. With such a camera, you would also need WASD movement then. I can't get over the fact that none of these RPGs have their own movement system (at least drag-move) and it's just clicking everywhere while your hero is constantly spamming "A sound plan." "Of course." "For honor." "A sound plan." and so on.


check this out..
 
Last edited:
Level 2
Joined
Sep 3, 2023
Messages
7
Very nice update. I have a half-built particle system in JASS but may just give up finalizing it since this system is much more efficient.
 
The way the missiles just uncontrollably bounce off the force field is pure chef kiss.
Thank you! :psmile:

Very nice update. I have a half-built particle system in JASS but may just give up finalizing it since this system is much more efficient.
My system is Lua-only. Can you make the switch to Lua?


And here's the change log:

Version 2.0
  • Added ready-to-use libraries - Objects, Entities, Missiles, Collisions, Ballistics, and Forces.
  • Return values in interaction functions are now optional. If the return value is omitted, ALICE will automatically put pairs using that function into the optimized EveryStep cycle.
  • Added API functions to add or remove self-interactions.
  • Added the Matrix API. These functions can be used to write data tables and share data between object pairs.
  • ALICE now uses the WidgetType library to infer the correct type of objects passed into it.
  • To improve performance with widgets, Math API functions now use cached values for object coordinates that get reset each cycle.
  • Freeze protection is now based on evaluation time instead of number of pairs.
  • ALICE_Benchmark can now be used to view the evaluation time of each cycle.
  • Added the HALT_ON_FIRST_CRASH option in the config.
  • Reorganized API into Core and Advanced API.
 
Level 1
Joined
May 20, 2024
Messages
4
Any idea how to use your alice system to make custom auto attack projectiles? the native one cant be changed on a unit. I need ability to change unit projectile model.
 
Version 2.1 is out. This update focuses mostly on making ALICE easier to set up and work with. To this end, there are multiple changes that should simplify the API and make it more consistent. Thank you for the feedback from @Galileo.
  • The Units, Destructables, Items, and Widgets CATs have been removed and incorporated directly into the main script. There are new options in the config to customize ALICE's default actor creation.
  • When using tables as hosts, debugging can be a nightmare because you don't get a syntax check on a misspelled table field from your IDE. Therefore, I added an option in the config to warn about tables containing unrecognized fields. By default, only the fields used by ALICE are listed as being recognized, but more can be added with ALICE_RecognizeFields. ALICE_RequireFields is a similar feature. It warns you when a table field that is required for an interaction function is not set.
  • The debug mode has new hotkeys, such as halting and resuming the cycle and going to the next step. There is now an option to automatically enable debug mode on map initialization.
  • Simplified and streamlined how the pairing of actors is controlled. The ability to pass functions as the interactionFunc, the "other" keyword, and the pairsWith flag have all been removed. Actors that pair with everything are never a good idea and the pairsWith flag was unintuitive and clunky. In place of these features, you can now use tables as keys in the interactionFunc, for example interactionFunc = {[{"steak", "grilled"}] = Eat, [{"steak", "raw"}] = Grill}. An actor must possess all identifiers listed in the table for a pairing to occur.
  • The manuals have been moved to the resource page, since hiveworkshop is more pleasant to read than a code comment.
  • You now enter the cell size in the config instead of the amount, so it doesn't need to be updated as the map size changes.

Since the cost isn't too high yet, I renamed some of the functions and features:
  • The table containing all interaction functions is now expected to be called "interactions" instead of "interactionFunc", because it might be confusing to use that term for two different things.
  • Removed the ALICE_CreateFromClass function and incorporated its functionality into ALICE_Create.
  • We have doodads and widgets, but we do not have gizmos!? Therefore, I renamed "entities" (objects represented by a table with coordinates and a special effect) to gizmos, which is clearly a much more awesome term. I will from now on lobby that gizmo becomes the official term for that kind of object.
  • The gizmo fields "visual" and "owner" are now hard-coded and can no longer be customized.

Other changes and additions:


  • Added the Callback API, which allows creating delayed function calls that are performant and run on the main cycle. This makes them pauseable for debug purposes. Potentially, you could pause the entire game.
  • Added the Modular API, which contains functions to inject additional properties and functionality into actors on creation, including into the default actors created for widgets. This allows libraries to add functionality to actors that these libraries need to work correctly without modifying the ALICE script itself.
  • Overhauled how pair data is handled. There was an issue with the previous implementation, where an interaction could break without invoking the onDestroy function. Now, callback functions are declared on an per-function basis with ALICE_FuncSetOnDestroy, ALICE_FuncSetOnBreak, or ALICE_FuncSetOnReset, with all functions behaving slightly differently. There is now a tutorial on pair data in the Advanced Manual.
  • ALICE_PairPreciseInterval has been added to allow interaction intervals that are not integer multiples of the minimum interval.
  • You can now display additional variables in actor tooltips with ALICE_TrackVariables.
  • In the Collisions CAT, the onWidgetDamage feature now works without declaring an onCollision function. onWidgetDamage can now be a function, not just a number. Added the option to disable friendly fire. The documentation has been improved.
  • The WidgetType library has been updated with a better method and renamed to HandleType.
  • Added the Mice CAT, a snippet to create an actor for each player's mouse.


Upcoming:
  • Support for asynchronous code execution by moving all arrays to linked lists. This ensures that the order of execution remains consistent between all players when asynchronous pairs are thrown into the mix. This will enable several ideas for systems I have using visibility detection.
  • Fixing ballistic movement around cliffs.
  • Line-segment enumeration.
 
Small update to fix some bugs.

Version 2.1.2
  • Fixed the ALICE_PairCallDelayed function.
  • Added missing hooks for RemoveDestructable and RemoveItem that prevent actors from leaking.
  • All actors now get properly cleared on a widget death or removal, preventing possible leaks of additional actors.
  • ALICE_PairPause is now more reliable.
  • ALICE_PairCooldown now allows starting multiple cooldown.
  • The Matrix API has been removed and replaced with the Pair Access API, which is faster, more flexible, and less confusing.
 
I think its obviously for post reforged versions of the game strictly?
I'm not an expert on this, I haven't designed it with pre-Reforged in mind. There might be some frame functions that I'm using for the debug mode that aren't available pre-Reforged, but other than that, this is just a giant Lua table rearrangement machine, so most of it should work. If that's the case, it shouldn't be too much effort to make a pre-Reforged compatible version. I just have to know the functions I need to avoid.
 
Level 8
Joined
Aug 5, 2014
Messages
200
I'm not an expert on this, I haven't designed it with pre-Reforged in mind. There might be some frame functions that I'm using for the debug mode that aren't available pre-Reforged, but other than that, this is just a giant Lua table rearrangement machine, so most of it should work. If that's the case, it shouldn't be too much effort to make a pre-Reforged compatible version. I just have to know the functions I need to avoid.
I will try this system, and i hope most of it will work. Because i really like the functionality. Most of needed systems in one piece.
 
Last edited:
I will try this system, and i hope must of it will work. Becaus i really like the functionality. Most of needed systems in one piece.
Cool, hopefully you won't encounter too many bugs.

Could you give me an overview of the patch you're on? Lua was added in the last patch before Reforged, I assume you're on that one? Frame natives were also added in that patch, but not all of them if I'm not mistaken.
 
Level 8
Joined
Aug 5, 2014
Messages
200
Cool, hopefully you won't encounter too many bugs.

Could you give me an overview of the patch you're on? Lua was added in the last patch before Reforged, I assume you're on that one? Frame natives were also added in that patch, but not all of them if I'm not mistaken.

Of course im on 1.31. The last pre reforged patch to get as much of new functionality (and sadly bugs) as possible without need to always log in battle.net to test maps. If i was using later version, i didn't even think about a mere possibility to use this system.
 
More important bug fixes:

Version 2.1.3
  • Fixed a bug that caused newly created objects to sometimes only start interacting when entering a new cell.
  • Fixed a bug that caused changing identifiers to sometimes crash the engine.

Of course im on 1.31. The last pre reforged patch to get as much of new functionality (and sadly bugs) as possible without need to always log in battle.net to test maps. If i was using later version, i didn't even think about a mere possibility to use this system.
My knowledge of this is only from looking up the patch history just now, and from my vague memory, so I wouldn't know this. If you have the list of natives missing in 1.31 or know where I can find something like that, it would help me a lot.
 
Version 2.1.4
  • Accidently broke the Rockslide example in the test map in the last update. This has been fixed.
  • OnCreation hooks now always accept function arguments as advertised and now work properly in (hopefully) all situations.
  • ALICE_PairCooldown now returns the remaining cooldown instead of a boolean.
  • Added ALICE_FuncPauseOnStationary to declare a function that should always get paused when an object becomes stationary.
  • Removed the random delay option in ALICE_FuncSetDelay, as it is a possible cause of desync.
 
Last edited:
Version 2.2
  • Enabled asynchronous code execution. Pairs and actors can now be created asynchronously without risking a desync as long as certain rules are being followed.
  • Added the ALICE_EnumObjectsInLineSegment and ALICE_ForAllObjectsInLineSegmentDo functions.
  • Added the ALICE_GetClosestObject function.
  • Added the Cameras CAT, which creates an actor for each player's camera.
  • Various bug fixes.
 
Version 2.2.2
  • Fixed a bug that caused the engine to crash when passing a non-widget handle as the host 🫣.
  • Fixed a bug that caused unpredictable behavior when changing the identifiers of global actors.
  • Classifications to unit actors can now be added individually.
  • Added the ALICE_OnWidgetEvent function. This function creates hooks on the widget event hooks (sup dawg) used by ALICE to create the default widget actors, allowing users to easily manage their own widget actors.
  • Added the ALICE_IsStationary function.
  • Added an optimization that caches the interaction function for two actor types, allowing it to be retrieved faster the next time two actors of the same types meet.
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
123
been just a year away and all this goodness pops up. My god are you inspiring. Between this and perfect camera and mouse tracking, like every everything is possible now.

Also those rock animations are perfect, between standing still and then rolling at just the right speed to feel right
 
Last edited:
been just a year away and all this goodness pops up. My god are you inspiring. Between this and perfect camera and mouse tracking, like every everything is possible now.
Thank you so much! Yea, moddiemads' libraries are insane as well.
Here's to better modding days ahead than the post-Reforged days :peasant-cheers-back:.

Also those rock animations are perfect, between standing still and then rolling at just the right speed to feel right
Those took me some time to get right. The formulas I got online for the yaw/pitch/roll transformations didn't work, because yaw-pitch-roll is defined differently in Warcraft 3, so I just brute-force tried all possible permutations until one of them was correct :plol:.
 
Alright, hopefully the last bug fix update for now...

Version 2.2.3
  • Fixed a bug that caused item actors to have the wrong position when an item is dropped by a hero.
  • Fixed a bug that caused objects with the .hasInfiniteRange flag to be ignored by enum functions.
  • Fixed an issue that caused destructables to not receive an actor after being revived.
  • Removed AUTO_DEBUG_MODE and AUTO_SELECT config options. Instead, added ALICE_Debug function to enable debug mode.
  • Stationary flags are now always kept consistent among multiple actors attached to one object.

No wonder, it looked so complicated and amazing, I wont be able to make use of it, but I'm gonna say it still, it looks freaking amazing, good job.
A bummer, but you can always make the switch to Lua :plol:. It's worth it!
 
New version that fully enables asynchronous execution, adds a smorgasbord of new features, and fixes some of the remaining inconsistencies and bugs.

Version 2.3
  • Asynchronous actors can now be paired with synchronous actors without risking a desync.
  • ALICE will now throw an error when the interaction function between two actors is ambiguous.
  • Shadowing interaction functions (such as {"unit" = DamageUnit, [{"unit", "hero"}] = DamageHero}) is now supported.
  • Improved config annotations. Config variables are not stored in the ALICE_Config table.
  • The destroyOnDeath flag has been replaced with persistOnDeath, which does the opposite. By default, all actors are now destroyed on a widget's death.
  • Added the width and height flags for actors, which can be used to create non-square actors.
  • The ALICE_Kill function can now also destroy unit- and image-based visuals.
  • Added the Rects CAT, which contains helper functions for creating actors from rects.
  • Added the onUnitChangeOwner option for ALICE_OnWidgetEvent.
  • Added the ALICE_SetInteraction function, which can modify the interaction table of an actor after creation.
  • Added the ALICE_FindField function, which returns the field in a table whose key matches an object's identifier.
  • Added the ALICE_GetAnchoredObject function, which returns objects anchored to the specified object.
  • Removed the ALICE_GetActor and ALICE_ForAllActorsDo functions.
  • ALICE_HasActor now has an optional flag to exclude actors only anchored to the object.
  • Added the ALICE_FuncSetName function so that interaction functions don't have to be globals in order for their names to be displayed in debug tooltips.
  • Pair Access functions can now access self-interactions by passing a function as the second argument.
  • The CAT_AnimateShadow function now uses images instead of special effects.
  • In the Collisions CAT, passing a table for onObjectDamage now works.
  • Fixed a bug that caused a destructable or item being removed or picked up to not destroy all actors if it has multiple.
  • Fixed a bug that caused ALICE_Kill to not destroy all actors on an object with multiple actors.
  • Fixed an issue that caused anchoring to an object that is itself anchored to another object to not work.
 

Wrda

Spell Reviewer
Level 27
Joined
Nov 18, 2012
Messages
1,951
As I've discussed with Antares about several aspects of the resource elsewhere, I'll publish here my notes for reference.
Lua:
local function OnUnitChangeOwner()
        local u = GetTriggerUnit()
        local newOwner = GetOwningPlayer(u)
        unitOwnerFunc[newOwner] = unitOwnerFunc[newOwner] or function() return newOwner end
        if actorOf[u] then
            if actorOf[u].isActor then
                actorOf[u].getOwner = unitOwnerFunc[newOwner]
            else
                for __, actor in ipairs(actorOf[u]) do
                    actor.getOwner = unitOwnerFunc[newOwner]
                end
            end
        end
    end
    SetOwnerFuncs = function(self, source)
        if source then
            if IsHandle[source] then
                if HandleType[source] == "unit" then
                    local owner = GetOwningPlayer(source)
                    unitOwnerFunc[owner] = unitOwnerFunc[owner] or function() return owner end
                    self.getOwner = unitOwnerFunc[owner]
                else
                    self.getOwner = ReturnNil
                end
            elseif type(source) == "table" then
                if type(source.owner) == "number" then
                    self.getOwner = GetClassOwnerById
                elseif source.owner then
                    self.getOwner = GetClassOwner
                else
                    self.getOwner = ReturnNil
                end
            end
        end
        if self.getOwner == ReturnNil and source ~= self.anchor then
            SetOwnerFuncs(self, self.anchor)
        else
            self.sourceOfOwner = source
        end
    end
I wonder if you could rework this in a way to avoid the creation of anonymous functions.
sort(pairTypes, function(a, b) return a[4] > b[4] end)
This should be a straightforward one to rework.


Lua:
CreateOnDeathTrigger = function(self)
        if IsWidget[self.host] then
            self.destroyOnDeathTrigger = CreateTrigger()
            TriggerRegisterDeathEvent(self.destroyOnDeathTrigger, self.host)
            TriggerAddAction(self.destroyOnDeathTrigger, function() Destroy(self) end)
        elseif IsWidget[self.anchor] then
            self.destroyOnDeathTrigger = CreateTrigger()
            TriggerRegisterDeathEvent(self.destroyOnDeathTrigger, self.anchor)
            TriggerAddAction(self.destroyOnDeathTrigger, function() Destroy(self) end)
        else
            Warning("|cffff0000Warning:|r Cannot use automatic onDeath clean-up with non-widget hosts.")
        end
    end
Another potential rework.

local objects = ALICE_EnumObjectsInRange(mouseX, mouseY, 500, {MATCHING_TYPE_ALL}, nil)
It seems it would be better to reuse a table in the stack.



Lua:
---Executes the function onBreakFunc(objectA, objectB, pairData, wasDestroyed) when a pair using the specified function is destroyed, the actors leave interaction range. Only one callback per function.
    ---@param whichFunc function
    ---@param onBreakFunc function
    function ALICE_FuncSetOnBreak(whichFunc, onBreakFunc)
        functionOnBreak[whichFunc] = onBreakFunc
    end
Looking at ALICE_FuncSetOnReset you probably meant "or" here? Was leaving a bit of doubt.


Lua:
---Pause the entire cycle. Optional pauseGame parameter to pause all units on the map.
    ---@param pauseGame? boolean
    function ALICE_Halt(pauseGame)
        cycle.isHalted = true
        PauseTimer(MASTER_TIMER)
        if pauseGame then
            ALICE_ForAllObjectsDo(function(unit) PauseUnit(unit, true) end, "unit")
        end
        debug.gameIsPaused = pauseGame
    end

[CODE=lua]---Slow down the ALICE cycle by the specified factor.
    ---@param factor number
    function ALICE_SlowMotion(factor)
        cycle.isHalted = false
        TimerStart(MASTER_TIMER, ALICE_MIN_INTERVAL*factor, true, Main)
        if debug.gameIsPaused then
            ALICE_ForAllObjectsDo(function(unit) PauseUnit(unit, false) end, "unit")
            debug.gameIsPaused = false
        end
    end

    ---Resume the entire cycle.
    function ALICE_Resume()
        cycle.isHalted = false
        TimerStart(MASTER_TIMER, ALICE_MIN_INTERVAL, true, Main)
        if debug.gameIsPaused then
            ALICE_ForAllObjectsDo(function(unit) PauseUnit(unit, false) end, "unit")
            debug.gameIsPaused = false
        end
    end

Can easily rework to avoid using the anonymous functions.

Lua:
function ALICE_EnumObjectsInLineSegment(x1, y1, x2, y2, halfWidth, identifier, condition, ...)
        if x2 == x1 then
            return ALICE_EnumObjectsInRect(x1 - halfWidth, min(y1, y2), x1 + halfWidth, max(y1, y2), identifier, condition, ...)
        end


        ResetCoordinateLookupTables()


        local returnTable = {}
        local cells = GetTable()


        local angle = atan(y2 - y1, x2 - x1)
        local cosAngle = math.cos(angle)
        local sinAngle = math.sin(angle)
Why doesn't returnTable also use GetTable?

Lua:
---Compiles the identifiers of an object into a table. Optional keyword parameter to specify actor with the keyword in its identifier for an object with multiple actors.
    ---@param object any
    ---@param keyword? string
    function ALICE_GetIdentifier(object, keyword)
        local actor = GetActor(object,keyword)
        if actor == nil then
            return
        end
        local returnTable = {}
        for key, __ in pairs(actor.identifier) do
            insert(returnTable, key)
        end
        sort(returnTable)
        return returnTable
    end
Same here.
There's ALICE_GetClosestObject but no GetClosestNObjects? :D would be very nice to have that implemented.

objects script:
Lua:
if knockbackItem then
            SetItemPosition(knockbackItem, x, y)
            xi = GetItemX(knockbackItem)
            yi = GetItemY(knockbackItem)
            if not IsTerrainPathable(x , y , PATHING_TYPE_WALKABILITY) and x - xi < 1 and x - xi > -1 and y - yi < 1 and y - yi > -1 then
                SetUnitX(u, x)
                SetUnitY(u, y)
            else
                knockbackSpeed[u].x = 0
                knockbackSpeed[u].y = 0
                ALICE_PairPause()
                return
            end
            SetItemPosition(knockbackItem, WORLD_MIN_X, WORLD_MIN_Y)
        else
            SetUnitX(u, x)
            SetUnitY(u, y)
        end
Would be better to encapsulate the pathing check method into a function, which also solves the need of moving the item back to unaccessable spot, which right now is being skipped if the path isn't walkable.

Lua:
---@param u unit
    ---@param vx number
    ---@param vy number
    ---@param vz number
    function CAT_Knockback(u, vx, vy, vz)
        if not knockbackSpeed[u] then
            knockbackSpeed[u] = {}
            ALICE_AddSelfInteraction(u, CAT_ApplyKnockback, "CAT")
            knockbackSpeed[u].x = 0
            knockbackSpeed[u].y = 0
            knockbackSpeed[u].z = 0
        else
            ALICE_Unpause(u, nil, "unit")
        end


        knockbackSpeed[u].x = (knockbackSpeed[u].x) + vx
        knockbackSpeed[u].y = (knockbackSpeed[u].y) + vy
    end
Unused "vz" parameter.

gizmos script:

Lua:
CAT_GlobalReplace(widgetId, widgetType, constructorFunc)    A function that allows you to place widgets using the World Editor and replace them on map initialization with
                                                                gizmos. The constructorFunc is the function creating your gizmo. It will be called with the parameters
                                                                (x, y, z, mana, owner, facing, unit) if the replaced widget is a unit, (x, y, z, life, widget) if the widget is a
                                                                destructable, and (x, y, z, widget) if it is an item.
The comment is missing the life parameter if it's a unit or item. On the function itself, it's only missing the life parameter.

Lua:
local z_x1 = GetTerrainZ(x - 4, y)
        local z_x2 = GetTerrainZ(x + 4, y)
        local z_y1 = GetTerrainZ(x, y - 4)
        local z_y2 = GetTerrainZ(x, y + 4)
        local dz_x = (z_x2 - z_x1)/8
        local dz_y = (z_y2 - z_y1)/8
I would like to know why it's these magic numbers and not some other value :p

Lua:
if dist < 0 then
            if gizmo.onTerrainCollision then
                gizmo:onTerrainCollision(GetTerrainCollisionPoint(gizmo))
            else
                ALICE_Kill(gizmo)
            end
            gizmo.lastTerrainCollisionCheck = ALICE_TimeElapsed
            return INTERVAL
        elseif vHorizontal - gizmo.vz < 200 then
            gizmo.lastTerrainCollisionCheck = ALICE_TimeElapsed
            return dist/600
        else
            gizmo.lastTerrainCollisionCheck = ALICE_TimeElapsed
            return dist/(3*(vHorizontal - gizmo.vz))
        end
Another set of magic numbers.

Lua:
elseif widgetType == "destructable" then
            EnumDestructablesInRect(bj_mapInitialPlayableArea, nil, function()
                local d = GetEnumDestructable()
                if GetDestructableTypeId(d) == widgetId then
                    local x = GetDestructableX(d)
                    local y = GetDestructableY(d)
                    local z = GetTerrainZ(x, y)
                    local life = GetDestructableLife(d)
                    constructorFunc(x, y, z, life, d)
                    RemoveDestructable(d)
                end
            end)
        elseif widgetType == "item" then
            EnumItemsInRect(bj_mapInitialPlayableArea, nil, function()
                local i = GetEnumItem()
                if GetItemTypeId(i) == widgetId then
                    local x = GetItemX(i)
                    local y = GetItemY(i)
                    local z = GetTerrainZ(x, y)
                    constructorFunc(x, y, z, i)
                    RemoveItem(i)
                end
            end)
        end
The creation of anonymous functions are easily avoidable here.

Can always optimize parts such as
Lua:
local norm = missile.speed/sqrt(missile.vx^2 + missile.vy^2 + missile.vz^2)
and in other places, where you replace to the power of 2 with multiplication of the variable with itself.

effects script:
Would be nice for "CAT_Shadow.mdl" to be a configurable, allows users to create other types of shadows independently.

Lua:
---@param gizmo table
    local function InitAnimateShadow(gizmo)
        gizmo.shadow = AddSpecialEffect("CAT_Shadow.mdl", gizmo.x, gizmo.y)
        BlzSetSpecialEffectTimeScale(gizmo.shadow, 0)
        if gizmo.shadowHeight > gizmo.shadowWidth then
            BlzSetSpecialEffectTime(gizmo.shadow, 1 + 0.25*(gizmo.shadowHeight/gizmo.shadowWidth - 1))
            BlzSetSpecialEffectScale(gizmo.shadow, gizmo.shadowWidth/200)
            gizmo.shadowOffsetY = 0.15*gizmo.shadowWidth
            gizmo.shadowOffsetX = 0.25*gizmo.shadowWidth
            gizmo.shadowOffsetZ = 0.1*gizmo.shadowHeight
            gizmo.shadowAttenuation = 12/gizmo.shadowWidth
        else
            BlzSetSpecialEffectTime(gizmo.shadow,  1 - 0.25*(gizmo.shadowWidth/gizmo.shadowHeight - 1))
            BlzSetSpecialEffectScale(gizmo.shadow, gizmo.shadowHeight/200)
            gizmo.shadowOffsetY = 0.15*gizmo.shadowHeight
            gizmo.shadowOffsetX = 0.25*gizmo.shadowHeight
            gizmo.shadowOffsetZ = 0.1*gizmo.shadowWidth
            gizmo.shadowAttenuation = 12/gizmo.shadowHeight
        end
    end
Where do these numbers 0.25 0.15 0.1 12 come from?

Lua:
function CAT_Orient2D(gizmo)
        if gizmo.vx ~= 0 or gizmo.vy ~= 0 then
            BlzSetSpecialEffectYaw(gizmo.visual, atan2(gizmo.vy, gizmo.vx))
        end
        if gizmo.isResting then
            ALICE_PairPause()
        end
        return 0.1
    end


    function CAT_Orient3D(gizmo)
        if gizmo.vx ~= 0 or gizmo.vy ~= 0 or gizmo.vz ~= 0 then
            BlzSetSpecialEffectOrientation(gizmo.visual, atan2(gizmo.vy, gizmo.vx), atan2(-gizmo.vz, sqrt(gizmo.vx^2 + gizmo.vy^2)), 0)
        end
        if gizmo.isResting then
            ALICE_PairPause()
        end
        return 0.1
    end


    function CAT_OrientPropelled2D(gizmo)
        if gizmo.ax ~= 0 or gizmo.ay ~= 0 then
            BlzSetSpecialEffectYaw(gizmo.visual, atan2(gizmo.ay, gizmo.ax))
        end
        return 0.1
    end


    function CAT_OrientPropelled3D(gizmo)
        if gizmo.ax ~= 0 or gizmo.ay ~= 0 or gizmo.az ~= 0 then
            BlzSetSpecialEffectOrientation(gizmo.visual, atan2(gizmo.ay, gizmo.ax), atan2(-gizmo.az, sqrt(gizmo.ax^2 + gizmo.ay^2)), 0)
        end
        return 0.1
    end
Worth mentioning what the return value represents.


Lua:
local function WidgetCollisionMath2D(gizmo, widget)
        local data = ALICE_PairLoadData()
        local xa, ya, xb, yb = ALICE_PairGetCoordinates2D()
        local dx, dy = xa - xb, ya - yb
        local vxa, vya = CAT_GetObjectVelocity2D(gizmo)
        local vxb, vyb = CAT_GetObjectVelocity2D(widget)
        local dvx, dvy = vxa - vxb, vya - vyb
        local dist = sqrt(dx*dx + dy*dy)


        local nx, ny = dx/dist, dy/dist
        local collisionX, collisionY
        local overlap = data.radius + gizmo.collisionRadius - dist


        local normalSpeed = -(nx*dvx + ny*dvy)


        if normalSpeed < 0 then
            return false
        end


        collisionX = xb + nx*data.radius
        collisionY = yb + ny*data.radius


        local totalSpeed = sqrt(dvx*dvx + dvy*dvy)
        local tangentialSpeed = sqrt(totalSpeed^2 - normalSpeed^2)


        local massA = CAT_GetObjectMass(gizmo)
        local massB = CAT_GetObjectMass(widget)
        local massSum = massA + massB
        local centerOfMassVx, centerOfMassVy


        if massA == INF then
            if massB == INF then
                centerOfMassVx = (vxa*massA + vxb*massB)/massSum
                centerOfMassVy = (vya*massA + vyb*massB)/massSum
            else
                centerOfMassVx = vxa
                centerOfMassVy = vya
            end
        elseif massB == INF then
            centerOfMassVx = vxb
            centerOfMassVy = vyb
        else
            centerOfMassVx = (vxa*massA + vxb*massB)/massSum
            centerOfMassVy = (vya*massA + vyb*massB)/massSum
        end


        return true, xa, ya, xb, yb, dist,
        vxa, vya, vxb, vyb,
        dx, dy, dvx, dvy,
        nx, ny,
        collisionX, collisionY, overlap,
        massA, massB,
        normalSpeed, totalSpeed, tangentialSpeed,
        centerOfMassVx, centerOfMassVy
    end
Worth recycling the table when no longer needed.

Lua:
--------------------
    --Collision Checks
    --------------------


    local function InitDestructableCollisionCheck2D(gizmo, destructable)
        local data = ALICE_PairLoadData()
        local id = GetDestructableTypeId(destructable)
        data.radius = WIDGET_TYPE_COLLISION_RADIUS[id] or DEFAULT_DESTRUCTABLE_COLLISION_RADIUS
        data.collisionRange = data.radius + gizmo.collisionRadius
        gizmo.maxSpeed = gizmo.maxSpeed or DEFAULT_GIZMO_MAX_SPEED
    end


    function CAT_DestructableCollisionCheck2D(gizmo, destructable)
        local data = ALICE_PairLoadData()
        local dist = ALICE_PairGetDistance2D()


        if dist < data.collisionRange then
            local callback = FindCallback(gizmo, "onDestructableCollision")
            if callback then
                callback(gizmo, destructable)
            else
                ALICE_Kill(gizmo)
            end
        end


        if gizmo.isResting then
            ALICE_PairPause()
        end


        return (dist - data.collisionRange)/gizmo.maxSpeed
    end


   --===========================================================================================================================================================
    --Gizmo-Item Collisions
    --==========================================================================================================================================================


    --------------------
    --Collision Checks
    --------------------


    local function InitItemCollisionCheck2D(gizmo, item)
        local data = ALICE_PairLoadData()
        local id = GetItemTypeId(item)
        data.collisionRange = (WIDGET_TYPE_COLLISION_RADIUS[id] or DEFAULT_ITEM_COLLISION_RADIUS) + gizmo.collisionRadius
        gizmo.maxSpeed = gizmo.maxSpeed or DEFAULT_GIZMO_MAX_SPEED
    end


    function CAT_ItemCollisionCheck2D(gizmo, item)
        local data = ALICE_PairLoadData()
        local dist = ALICE_PairGetDistance2D()


        if dist < data.collisionRange then
            local callback = FindCallback(gizmo, "onItemCollision")
            if callback then
                callback(gizmo, item)
            else
                ALICE_Kill(gizmo)
            end
        end


        if gizmo.isResting then
            ALICE_PairPause()
        end


        return (dist - data.collisionRange)/gizmo.maxSpeed
    end
Same here.


Lua:
local function InitDestructableCollisionCheck3D(gizmo, destructable)
        local data = ALICE_PairLoadData()
        local id = GetDestructableTypeId(destructable)
        data.radius = WIDGET_TYPE_COLLISION_RADIUS[id] or DEFAULT_DESTRUCTABLE_COLLISION_RADIUS
        data.height = WIDGET_TYPE_HEIGHT[id] or DEFAULT_DESTRUCTABLE_HEIGHT
        data.collisionRange = data.radius + gizmo.collisionRadius
        data.maxdz = data.height/2 + gizmo.collisionRadius
        gizmo.maxSpeed = gizmo.maxSpeed or DEFAULT_GIZMO_MAX_SPEED
        data.xDest, data.yDest, data.zDest = ALICE_GetCoordinates3D(destructable)
    end


    function CAT_DestructableCollisionCheck3D(gizmo, destructable)
        local data = ALICE_PairLoadData()
        local dx, dy, dz
        if gizmo.x  then
            dx, dy, dz = data.xDest - gizmo.x, data.yDest - gizmo.y, data.zDest - gizmo.z
        else
            local x, y, z = ALICE_GetCoordinates3D(gizmo)
            dx, dy, dz = data.xDest - x, data.yDest - y, data.zDest - z
        end
        local horiDist = sqrt(dx*dx + dy*dy)
        local dist


        if horiDist < data.collisionRange and dz < data.maxdz and dz > -data.maxdz then
            local dtop = data.height/2 - dz
            if dtop < data.radius then
                dist = sqrt(horiDist*horiDist + (dtop - data.radius)^2)
            else
                dist = horiDist
            end
            if dist < data.collisionRange then
                local callback = FindCallback(gizmo, "onDestructableCollision")
                if callback then
                    callback(gizmo, destructable)
                else
                    if gizmo.onDestructableDamage then
                        DamageWidget(gizmo, destructable)
                    end
                    ALICE_Kill(gizmo)
                end
            end
        else
            dist = sqrt(horiDist*horiDist + dz*dz)
        end


        if gizmo.isResting then
            ALICE_PairPause()
        end


        return (horiDist - data.collisionRange)/gizmo.maxSpeed
    end
Same thing as the 2D counterpart.

ballistics script:
Lua:
local PI                                = bj_PI
You can store it with math.pi() :P


    ---@param xLaunch number
    ---@param yLaunch number
    ---@param zLaunch number
    ---@param xTarget number
    ---@param yTarget number
    ---@param zTarget number
    ---@param angle number
    ---@return number | nil, number | nil, number | nil
    function CAT_GetBallisticLaunchSpeedFromAngle(xLaunch, yLaunch, zLaunch, xTarget, yTarget, zTarget, angle)
        local dx = xTarget - xLaunch
        local dy = yTarget - yLaunch
        local dz = zTarget - zLaunch
        local dist = sqrt(dx^2 + dy^2)
        local vSquared = (GRAVITY*dist^2/(2*cos(angle)^2))/(dist*tan(angle) - dz)
        if vSquared < 0 then
            return nil, nil, nil
        end
        local velocity = sqrt(vSquared)
        return velocity*cos(angle)*dx/dist, velocity*sin(angle)*dy/dist, velocity*sin(angle)
    end
Potential space for optimization such as store cos(angle) and sin(angle).


Lua:
local function InitMoveablesCAT()
        Require "ALICE"
        Require "CAT_Objects"
        Require "PrecomputedHeightMap"
        INTERVAL = ALICE_MIN_INTERVAL
        ALICE_FuncSetInit(CAT_MoveBallistic, InitMoveBallistic)
        ALICE_RecognizeFields(
            "vx",
            "vy",
            "vz",
            "friction",
            "isResting",
            "collisionRadius"
        )
        ALICE_FuncRequireFields(CAT_MoveBallistic, true, false, "x", "y", "z", "vx", "vy", "collisionRadius")
    end
INTERVAL is a global and is very susceptible for name clashing.
Objects' script configuration is before you listing the possible functions to use, while on collisions 2D, forces 2D and possible others have the the list of functions before the configuration. So seems inconsistent at the moment.

This is the review of the last update:
Force2D script:
Lua:
local function ClearForce2D(source, target)
        local data = ALICE_PairLoadData(forceMatrix)
        local FxOld = data.Fx + data.Fxdt*(ALICE_TimeElapsed - data.lastUpdate)
        local FyOld = data.Fy + data.Fydt*(ALICE_TimeElapsed - data.lastUpdate)
        local FxdtOld = data.Fxdt
        local FydtOld = data.Fydt

        local forceTable
        if data.targetIsGizmo then
            forceTable = target
        else
            forceTable = unitForce[target]
        end
        

        if forceTable then
            forceTable.Fx = forceTable.Fx - FxOld
            forceTable.Fy = forceTable.Fy - FyOld
            forceTable.Fxdt = forceTable.Fxdt - FxdtOld
            forceTable.Fydt = forceTable.Fydt - FydtOld
        end
    end




    local function InitForce2D(source, target)
        local data = ALICE_PairLoadData(forceMatrix)
        data.lastUpdate = ALICE_TimeElapsed
        data.targetIsGizmo = type(target) == "table"
        if data.targetIsGizmo then
            data.typeMultiplier = 1
            if target.Fx == nil then
                ALICE_AddSelfInteraction(target, AccelerateGizmo2D)
                ALICE_AddSelfInteraction(target, ResumForceGizmo2D)
                target.Fx, target.Fy = 0, 0
                target.Fxdt, target.Fydt = 0, 0
            end
        elseif HandleType[target] == "unit" then
            data.typeMultiplier = FORCE_UNIT_MULTIPLIER
            if unitForce[target] == nil then
                unitForce[target] = {}
                local forceTable = unitForce[target]
                ALICE_AddSelfInteraction(target, AccelerateUnit2D)
                ALICE_AddSelfInteraction(target, ResumForceUnit2D)
                forceTable.Fx, forceTable.Fy = 0, 0
                forceTable.Fxdt, forceTable.Fydt = 0, 0
            end
        end
    end
source parameter isn't being used at all. Are you going to expand later, or it simply serves as a model for users to expand? Would be nice for unitForce[target] = {} to use the GetTable() functionality.

local dist = sqrt(dx^2 + dy^2)
Potential spot for optimization

Lua:
local function GravityForce2D(source, target, data, dist, dx, dy, dvx, dvy)
        if data.minDistance > dist then
            dist = data.minDistance
        end
        local factor = data.strength/dist^3
        return dx*factor, dy*factor
    end


    local function GravityInterval2D(source, target, data, dist, dx, dy, dvx, dvy)
        --Increased update frequency when objects are moving towards each other.
        local vdotd = -(dvx*dx + dvy*dy)
        if vdotd < 0 then
            vdotd = 0
        end
        return dist^2*data.intervalFactor/(1 + vdotd*data.intervalFactor)
    end

    local function PressureForce2D(source, target, data, dist, dx, dy, dvx, dvy)
        if dist > source.pressureMaxRange then
            return 0, 0
        end
        local factor = -source.pressureStrength*data.strengthFactor*(source.pressureMaxRange - dist)/(source.pressureMaxRange*dist)
        return dx*factor, dy*factor
    end

    local function PressureInterval2D(source, target, data, dist, dx, dy, dvx, dvy)
        --Increased update frequency when objects are moving towards each other.
        local vdivd = -(dvx*dx + dvy*dy)/dist^2
        if vdivd < 0 then
            vdivd = 0
        end
        local interval = (0.15 + 0.85*(dist/source.pressureMaxRange))*data.intervalFactor/source.pressureStrength
        return interval/(1 + vdivd*interval)
    end

    local function WindForce2D(source, target, data, dist, dx, dy, dvx, dvy)
        if dist > source.windFieldMaxRange then
            return 0, 0
        end
        local dvxWind = dvx + data.vx
        local dvyWind = dvy + data.vy
        local dvWindTotal = sqrt(dvxWind^2 + dvyWind^2)
        local factor = dvWindTotal*source.windFieldDensity*data.strengthFactor*(source.windFieldMaxRange - dist)/(source.windFieldMaxRange*dist)
        return dvxWind*factor, dvyWind*factor
    end

    local function WindFieldInterval2D(source, target, data, dist, dx, dy, dvx, dvy)
        --Increased update frequency when objects are moving towards each other.
        local vdivd = -(dvx*dx + dvy*dy)/dist^2
        if vdivd < 0 then
            vdivd = 0
        end
        local interval = (0.15 + 0.85*(dist/source.windFieldMaxRange))*data.intervalFactor/source.windFieldDensity
        return interval/(1 + vdivd*interval)
    end
Same question about the unused parameters, and other spots for optimizations (PressureForce2D is fine)

Lua:
local interval = (0.15 + 0.85*(dist/source.windFieldMaxRange))*data.intervalFactor/source.windFieldDensity
Where does 0.15 and 0.85 come from?

Lua:
function CAT_AddForce2D(class, interactsWith, initFunc, forceFunc, intervalFunc)
        class.initForceFunc = initFunc
        class.forceFunc = forceFunc
        class.intervalFunc = intervalFunc
        if class.interactions == nil then
            class.interactions = {}
        end
Spot to use a recycled table.

Force3D script:
Lua:
local function InitAccelerateUnit3D(unit)
        local data = ALICE_PairLoadData()
        data.multiplier = INTERVAL/CAT_GetObjectMass(unit)
        if data.multiplier == math.huge then
            error("Attempting to apply force to unit with no mass.")
        end
    end

    local function AccelerateUnit3D(unit)
        local data = ALICE_PairLoadData()
        local forceTable = unitForce[unit]
        if not forceTable or (forceTable.Fx == 0 and forceTable.Fy == 0 and forceTable.Fz == 0) then
            ALICE_PairPause()
            return
        end
        CAT_Knockback(unit, forceTable.Fx*data.multiplier, forceTable.Fy*data.multiplier, forceTable.Fz*data.multiplier)
        forceTable.Fx = forceTable.Fx + forceTable.Fxdt*INTERVAL
        forceTable.Fy = forceTable.Fy + forceTable.Fydt*INTERVAL
        forceTable.Fz = forceTable.Fz + forceTable.Fzdt*INTERVAL
    end

Force2D script uses forceMatrix on ALICE_PairLoadData, I'd assume you also wanted it here too.
Rest of Force3D is very similar to my comments on Force2D.

cameras script:
Lua:
local newCameraRadius = math.min(MAX_CAMERA_RADIUS, 0.5*GetCameraField(CAMERA_FIELD_TARGET_DISTANCE)/(camera.angleOfAttack + 5.88))

5.88, my favourite magic number ;)

This is now part of the Lua Bible. Extremely useful for Lua maps to go berserk with enumerations, spam units, effects, doodads, it won't disappoint you! Offers you templates of collisions, physics movement and a few more. Math is beyond my reach.

Splendid.


No more 10k lines to read please 😅
 
No more 10k lines to read please 😅
Actually currently creating an operating system inside Warcraft 3, but after that, I promise! :peasant-cheers-back:


Version 2.3.2
  • Fixed a bug with the Forces CATs.
  • Added some missing annotations and fixed a find-and-replace mishap in the config annotations
 
Version 2.4 adds improvements to the Callback API, as well as introduces Minimal ALICE, or MINACE, which is a version of ALICE that has been stripped of everything except for the Callback API and the few functions that would make sense to be used within the callbacks. The purpose of this version is mostly to allow me to use the Callback API as a requirement for other resources without demanding from the user that they install a 5k line library.

Version 2.4
  • Added MINACE, a stripped-down version of ALICE.
  • Added 1.31 support by replacing BlzFrameGetChild with BlzGetFrameByName.
  • The fourCC codes of widgets are now added to the default actors' identifiers.
  • The ALICE_CallPeriodic function can now capture additional arguments and the interval is no longer limited by MAX_INTERVAL.
  • Added the ALICE_CallRepeated function, which works like ALICE_CallPeriodic, but executes the function a set number of times.
  • Added the ALICE_DisablePeriodic function, which should be used to disable periodic callbacks instead of ALICE_PairDisable.
  • Removed the ALICE_CallAt function.
  • Added the ALICE_GetPairState function for debug mode.
  • Removed the ALICE_SlowMotion function.
  • Removed the MAXIMUM_EVALUATION_TIME config parameter and the in-built freeze protection.
  • Changed the ALICE_Statistics function to print function names instead of actor identifiers.
  • The CAT_MoveArcedHoming function now recognizes the arcAngle field to allow missiles to not only arc vertically.
  • The CAT_UnitCollisionCheck function now recognizes the onlyTarget field, which excludes all units from the collision check other than the gizmo's target.
  • Instead of creating one death trigger for each actor, now at most one death trigger is created for each destructable and item.
  • Added the missing onDestructableEnter, onDestructableDestroy, onItemEnter, and onItemDestroy event hooks.
  • In debug mode, when clicking on a spot with multiple actors, you will now properly cycle through the different actors instead of repeatedly selecting the same one.
  • Fixed a bug that caused the CAT_MoveArced function to crash when the .arcAngle field was not set.
  • Fixed a bug that caused a single table passed as an additional argument into ALICE_CallDelayed to not be forwarded into the callback function.

Unfortunately, didn't quite finish the operating system in time. Maybe next update :pwink:.
 
After downloading the RPGminimap, I thought I could use ALICE to improve my missile system since it’s now installed anyway. Best thing I could do, now I’m having tremendous fun applying ALICE capabilities everywhere!
Thank you Antares for such a powerful tool!
That is so great to hear! Can I quote your testimonial in the description? :plol:

Also, I'm wondering, did you use the in-built missile functions or integrate your own into ALICE?
 
Top