• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Set/Group Data Structure

Level 20
Joined
Jul 10, 2009
Messages
477

Overview

API & Code (variant 1)

API & Code (variant 2)

Changelog

Set Data Structure

A data container like unitgroups that is not limited to units, but can contain any object.

Description:
This submission contains two libraries:
  • The Set library offers methods to create and use Sets. Sets are container objects, which can hold any other object and which are guaranteed to contain no object more than once. Exactly like unit groups, but not limited to units, and with much more convenient usage.
    Sets get automatically garbage collected by the Lua-VM, so (in contrast to unitgroups and forces) they don't produce any memory leaks.
  • The SetUtils library offers equivalents to pick-all-units-matching-condition-natives (players/destructables/items), but return a Set instead of a group or force.
Variant 1 vs. Variant 2:
I submitted two variants of the code. Differences only concern the SetUtils functionality, i.e. how the Wc3 pick natives are mimicked.
I recommend the first variant, if you are mainly interested in the Set-library and rarely use pick-functions. In this scenario, it offers better performance.
I recommend the second variant, if you regularly need pick-functions. In this scenario, it offers more convenience (like the option to also pick units with locust) and even fixes some bugs that the original Wc3 natives have.

For those further interested, differences are as follows:
  • The first variant is using the existing Warcraft pick-natives (GroupEnumUnitsInRect, etc.) to create a group/force and converts the result to a Set. This variant is guaranteed to give the same results as if you were using the group/force natives directly, but also suffers from the known bugs these natives currently have, like units sporadically being ignored by GroupEnumUnitsInRect and memory leaks created from using closures in conditionfuncs, see here. In addition, this variant shows the same quirks that the original natives had, like locust units being ignored, except by GroupEnumUnitsOfPlayer.
  • The second variant manually saves all units entering your map into a global Set and implements the pick natives by checking all of them for meeting the desired condition (such as being owned by a particular player). To maintain the global Set, this variant alters all CreateUnit-natives (makes them add the created unit to the global Set) and creates a trigger to add all units that enter the map by being trained, constructed or summoned. Credits to @Beckx for suggesting this method.
    Specifics for variant 2 are:
    • Maintaining the global Set obviously adds a bit of performance overhead
    • It does not suffer from the known bugs mentioned for variant 1, i.e. doesn't forget picking units and doesn't produce Condition leaks.
    • You can specify in all pick functions, whether locust units should be picked or not.
    • For the conditional pick functions, you input regular Lua functions that take a unit and return a boolean instead of a Wc3 conditionfunc. I.e. you don't need the Condition() or Filter() wrap and you don't need GetFilterUnit(). This is much more convenient and doesn't produce memory leaks.
    • Also in the conditional pick funcions, you can specify as many conditions as you want.
    • If you use variant2-pick-functions as event response to being trained, summoned or constructed, the resulting Set is not guaranteed to contain the trained/summoned/constructed unit.
      To solve this, either use a 0-timer before using the pick-function or just use the "Enters map" event instead (which doesn't have this problem, i.e. pick functions are always able to pick the entering unit).

Optional Dependencies:
  • Global Initialization
    Simplifies the installation process of this resource (see Installation section below). I absolutely recommend getting and using that library in your own project. Honestly though, don't download it just for the sake of Set/SetUtils, because that process takes longer than just installing this resource as described :D.
  • SyncedTable
    The SyncedTable-library allows you to iterate over Lua-tables in multiplayer-maps without risking a desync (in contrast, iterating over normal tables with the pairs-function can cause desyncs). I recommend having it in every Lua-map that is designed for multiplayer.
    Likewise, certain Set-functionalities like the addAll-method are not compatible with normal Lua-tables (as they might desync), but are compatible with SyncedTables.

Installation:

Required:
Create a new Script document in your trigger editor and paste the code from the "API & Code (variant1)" tab into it.

Optional:
Do this, if you need the SetUtils.subscribeSetForAutoUnitRemoval-feature (see "Further Notes" below):
  1. Either have the Global Initialization library in your map (must be located above this resource!)
    OR create a GUI trigger with the "MapInitialization" event and add the following Action:
    • Init SetUtils
      • Events
        • Map initialization
      • Conditions
      • Actions
        • Custom script: SetUtils.createTriggers()
  2. Make a copy of the "Defend" ability in the object editor (the Human ability used by Footmen).
    Scroll to the line CUSTOM_DEFEND_ABILITY = nil in the code of this resource (just below the documentation) and replace nil by the ability code of the copied Defend ability (in FourCC).
    Example: if your copy of "Defend" has abi code 'A000', you would set CUSTOM_DEFEND_ABILITY = FourCC('A000').
Required:
  1. Create a new Script document in your trigger editor and paste the code from the "API & Code (variant2)" tab into it.
  2. Either have the Global Initialization library in your map (must be located above this resource!)
    OR create a GUI trigger with the "MapInitialization" event and add the following Action:
    • Init SetUtils
      • Events
        • Map initialization
      • Conditions
      • Actions
        • Custom script: SetUtils.createTriggers()
Optional:
Do this, if you need the SetUtils.subscribeSetForAutoUnitRemoval-feature (see "Further Notes" below):
  • Make a copy of the "Defend" ability in the object editor (the Human ability used by Footmen).
    Scroll to the line CUSTOM_DEFEND_ABILITY = nil in the code of this resource (just below the documentation) and replace nil by the ability code of the copied Defend ability (in FourCC).
    Example: if your copy of "Defend" has abi code 'A000', you would set CUSTOM_DEFEND_ABILITY = FourCC('A000').

Example Code:
Please refer to the API, which includes a few lines of example code. If you need more than that, feel free to ask me in this thread ;)

Further Notes:

  • An important note specifically for Sets that contain units:
    The pick-functions from the SetUtils-library will never pick units that have previously been removed from the game, but it can easily happen that a unit leaves the game after it was added to a Set.
    Per default and in contrast to native unitgroups, those units willl not be automatically removed from all Sets containing them, so they stay in the Set as dead references, potentially causing bugs (especially when looping over the Set).
    The SetUtils-library however provides two solutions:
    1. If you conducted the necessary steps in the installation process, you can use the SetUtils.subscribeSetForAutoUnitRemoval(Set) function to let the SetUtils-library automatically remove all units from the specified Set that leave the game. You have to use the function on every Set that you want to subscribe to this feature, but having too many subscribed Sets at once will decrease map performance (don't bother about up to 100 subscribed Sets, but make sure you only subscribe Sets that you want to keep for longer. There is no need to subscribe one-time-use-Sets anyway). I.e. subscribing a unit-Set prevents bugs and lets them behave as you would except from a unitgroup.
    2. As an alternative to 1. and without any requirements, you could also use SetUtils.clearInvalidUnitRefs(Set) on a unit-Set to manually remove invalid unit references, but you have to do it every time before looping over it. This can be more performant, if you have a lot of Sets, but rarely loop over each individual one. And well, this is the only option, if you haven't conducted the optional steps during the installation process.
  • The submission also contains a DEBUG_MODE setting, which per default is set to true (see the code directly below the documentation).
    If enabled, it will automatically check for if you accidently confused dot-notation and colon-notation upon using any of the Set-methods (i.e. it prints an error on screen upon using a method with dot notation, which was supposed to be used with colon notation and vice versa). I have implemented this mainly for myself, because I had a hard time getting used to colon notation, when I started with Lua and had a lot of bugs just coming from accidently using dots at wrong places. Be sure to set the constant to false, before you release your map.

Lua:
if Debug and Debug.beginFile then Debug.beginFile("Set/Group") end
--[[

------------------------------------
-- | API Set & Set Utils v1.3.3 | --
------------------------------------

 by Eikonium

 --> https://www.hiveworkshop.com/threads/lua-set-group-datastructure.331886/

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Sets are data containers that can contain every element only once. Much like unitgroups, but not limited to units.                                                    |
| The Set-API offers functions to create, alter and loop through Sets.                                                                                                  |
| The SetUtils library offers equivalents to pick-all-units-matching-condition-natives (players/destructables/items) that return a Set instead of a group or force.     |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Optional Dependencies:                                                                                                                                                |
|                                                                                                                                                                       |
| Total Initialization by Bribe:                                                                                                                                        |
|       https://www.hiveworkshop.com/threads/lua-global-initialization.317099/                                                                                          |
|       Having Total Init in your map script frees you of creating a GUI trigger that runs SetUtils.createTriggers() on Map Init.                                       |
|       Please make sure that you copy Global Initilization to a script file in your map that is located above(!) the Set library.                                      |
| SyncedTable                                                                                                                                                           |
|       https://www.hiveworkshop.com/threads/lua-syncedtable.332894/                                                                                                    |
|       Many Set-functionalities like union, intersection, except, fromTable and addAllKeys do support arrays and SyncedTables, but not normal tables (because it could |
|       lead to desyncs).                                                                                                                                               |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* API Functions:
*
* The API uses standard Lua object-oriented syntax, i.e. you have to pay attention to whether the function you want to use requires dot- or colon-notation (it's clearly visible in the documentation).
* The library provides a debug-mode that allows you to get notified about when you used dots instead of colons and vice versa.
*
* -------------
* | Set class |
* -------------
*        - The class itself mainly offers ways to create new Sets.
*    Set.create(...) -> Set
*        - Creates a new Set and adds all specified arguments as elements.
*        - Example:
*           local a = Set.create("bla", 50, Player(0)) --creates a Set with 3 elements.
*    Set.union(...) -> Set
*        - Creates a new Set as the union of all specified arguments.
*        - All arguments must be either Sets, SyncedTables, arrays, forces or groups. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
*    Set.intersection(...) -> Set
*        - Creates a new Set as the intersection of all specified arguments.
*        - All arguments must be either Sets, SyncedTables, arrays, forces or groups. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
*    Set.except(containerA, containerB) -> Set
*        - Creates a new Set having all elements from specified containerA except the elements from specified containerB.
*        - All arguments must be either Sets, SyncedTables, arrays, forces or groups. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
*    Set.fromForce(force) -> Set
*        - Creates a new Set containing all players from the specified force.
*    Set.fromGroup(group) -> Set
*        - Creates a new Set containing all units fromn the specified group.
*    Set.fromTable(array|SyncedTable) -> Set
*        - Creates a new Set containing all elements from the specified table (elements refer to the values of the table, not the keys).
*        - Table must be either a SyncedTable or an array. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
* ---------------
* | Set objects |
* ---------------
*        - The following methods are available for any existing Set object.
*    --------------------------------
*    | ADDING AND REMOVING ELEMENTS |
*    --------------------------------
*    <Set>:add(...)
*        - adds any number of specified arguments as elements to <Set>
*        - ignores all arguments that are already contained in <Set>
*        - returns <Set> to allow for chaining methods
*        - if you highly care for performance and only want to add one single element, you can use <Set>:addSingle(element) instead.
*    <Set>:remove(...)
*        - removes any number of specified arguments from <Set>
*        - ignores all arguments that are not contained in <Set>
*        - returns <Set> to allow for chaining methods
*        - if you highly care for performance and only want to remove one single element, you can use <Set>:removeSingle(element) instead.
*    <Set>:addAll(container)
*        - adds all elements from the specified container as elements to <Set> (and ignores everything that is already present in <Set>)
*        - the container must be a Set, array, force or group (in case of an array, "elements" refers to its values)
*        - returns <Set> to allow for chaining methods
*    <Set>:addAllKeys(SyncedTable) [or any table with a multiplayer-synched pairs function]
*        - adds all keys from the specified table as elements to <Set>
*        - this method is only multiplayer-compatible, if you input a SyncedTable or if you have overwritten the pairs-function to make it synchronous on the input table. Otherwise, using this method can lead to a desync.
*        - returns <Set> to allow for chaining methods
*    <Set>:removeAll(container)
*        - removes all elements present in the specified container from <Set> (and ignores everything that is not contained in <Set>)
*        - the container must be a Set, array, force or group.
*        - returns <Set> to allow for chaining methods
*    <Set>:retainAll(container)
*        - only keeps elements in <Set> that are also contained in the specified container and removes the rest.
*        - the container must be a Set, array, force or group.
*        - returns <Set> to allow for chaining methods
*    <Set>:clear()
*        - removes all elements from <Set>
*        - returns <Set> (which is empty now) to allow for chaining methods
*    ---------------------
*    | LOOPING MECHANISM |
*    ---------------------
*    <Set>:elements()
*        - iterator function for the generic for-loop over <Set>
*        - Example:
*             local exampleSet = Set.create(Player(0), Player(1), "bla", 5)
*             for dings in exampleSet:elements() do
*                 print(dings)
*             end
*    -----------
*    | UTILITY |
*    -----------
*    <Set>:contains(element) -> boolean
*        - returns true, if the element is contained in <Set> and false otherwise.
*    <Set>:size() -> integer
*        - returns the number of elements of the set.
*    <Set>:isEmpty() -> boolean
*        - returns true, if <Set> contains no elements, and false otherwise.
*    <Set>:toString() -> string
*        - returns a comma separated list of all elements of <Set>, engulfed in {}-brackets.
*    <Set>:print()
*        - prints <Set>:toString() on screen.
*    <Set>:random() -> any
*        - returns a random element from <Set>
*    <Set>:toArray() -> any[]
*        - returns a normal array (table) containing ell elements from <Set>.
*        - not really useful in most cases, as arrays are not better than sets most of the time. There are exceptions however, e.g. when you want to sort the elements (Sets don't have an order, but arrays have).
*    <Set>:intersects(otherSet) -> boolean
*        - Returns true, if <Set> has at least one common element with the specified argument.
*        - Argument currently only supports other Sets - not arrays, forces or groups.
*    <Set>:copy() -> Set
*        - Returns a new Set containing the same elements as <Set>.
* -------------------
* | Set Utils class |
* -------------------
*        - this class offers set equivalents for the Wc3 natives that pick and return a group of units, players, destructables and items.
*    ------------------
*    | SIMPLE GETTERS |
*    ------------------
*        | -> Units |
*        ------------
*        SetUtils.getUnitsInRect(rect)
*            - returns a new Set with all units located in the specified rect
*            - like in the Warcraft native, the rect is considered an half-open rectangle (closed to west and south, open to east and north?). That means that entering units are not guaranteed to be picked.
*            - does not pick units having the locust ability
*        SetUtils.getUnitsInRange(float x, float y, float radius)
*            - returns a new Set with all units within the specified radius of the specified coordinates.
*            - does not pick units having the locust ability
*        SetUtils.getUnitsOfPlayer(player)
*            - returns a new Set with all units owned by the specified player.
*        SetUtils.getUnitsOfTypeId(integer typeId)
*            - returns a new Set with all units on the map having the specified type.
*            - the typeId parameter has to be created out of the FourCC-function, e.g. SetUtils.getUnitsOfTypeId(FourCC('hfoo'))
*        SetUtils.getUnitsOfPlayerAndTypeId(player, integer typeId)
*            - returns a new Set with all units owned by the specified player and having the specified type.
*        SetUtils.getUnitsSelected(player)
*            - returns a new Set with all units currently being selected by the specified player.
*            - returns the empty Set, when the player doesn't have any units selected.
*            - this function needs to synchronize local selections between players, so it might not be as instant as usual (needs investigation).
*        ------------------------------------
*        | -> Players, Destructables, Items |
*        ------------------------------------
*        SetUtils.getPlayersAll()
*            - returns a new Set containing all players that were present during map init, i.e. a copy of InitialPlayersPlaying.
*        SetUtils.getDestructablesInRect(rect)
*            - returns a Set with all Destructables located in the specified rect.
*        SetUtils.getItemsInRect(rect)
*            - returns a Set with all Items located in the specified rect.
*    -----------------------
*    | CONDITIONAL GETTERS |
*    -----------------------
*        - Below functions are variants to the above functions that take a condition function as an additional parameter.
*          Only elements passing the condition will join the set.
*        - The condition must be a either a Wc3 native conditionfunc or a Lua-function that takes nothing and returns a boolean.
*          If you choose to provide a Lua-function, SetUtils will automatically convert it to a conditionfunc by applying the Condition()-native.
*          If you use the same condition function over and over, it is recommended that you apply Condition() yourself, save it in a variable and use the same thing instead of creating a new one every time.
*        - Use GetFilterUnit() inside of the condition function to refer to the unit being checked.
*        ------------
*        | -> Units |
*        ------------
*        SetUtils.getUnitsInRectMatching(rect, condition)
*            - returns a new Set with all units located in the specified rect and matching the specified condition.
*            - like the Warcraft native, the rect is considered an half-open rectangle (closed to west and south, open to east and north?). That means that entering units are not guaranteed to be picked.
*            - Example:
*               local r = <someRect>
*               local condition = Condition(function() return GetOwningPlayer(GetFilterUnit()) == Player(0) end)
*               local exampleSet = SetUtils.getUnitsInRectMatching(r, condition) --will contain all units in the rect owned by Player 1.
*        SetUtils.getUnitsInRangeMatching(float x, float y, float radius, condition)
*            - returns a new Set with all units within the specified radius of the specified coordinates that match the specified condition.
*        SetUtils.getUnitsOfPlayerMatching(player, condition)
*            - returns a new Set with all units owned by the specified player and matching the specified condition.
*        SetUtils.getUnitsOfTypeIdMatching(integer typeId, condition)
*            - returns a new Set with all units on the map having the specified type and matching the specified condition.
*            - the typeId parameter has to be created out of the FourCC-function, e.g. SetUtils.getUnitsOfTypeId(FourCC('hfoo'))
*        SetUtils.getUnitsSelectedMatching(player, condition)
*            - returns a new Set with all units currently being selected by the specified player and matching the specified condition.
*            - returns the empty Set, when the player doesn't have any units selected.
*            - this function needs to synchronize local selections between players, so it might not be as instant as usual (needs investigation).
*        ------------------------------------
*        | -> Players, Destructables, Items |
*        ------------------------------------
*        SetUtils.getPlayersMatching()
*            - returns a new Set containing all players that were present during map init and who match the specified condition.
*        SetUtils.getDestructablesInRectMatching(rect)
*            - returns a Set with all Destructables located in the specified rect and matching the specified condition.
*        SetUtils.getItemsInRectMatching(rect)
*            - returns a Set with all Items located in the specified rect and matching the specified condition.
*    -----------
*    | UTILITY |
*    -----------
*    SetUtils.clearInvalidUnitRefs(Set [, boolean checkIfUnit])
*        - Removes all invalid unit references from the specified Set, i.e. units that have already been removed from the game.
*        - Only useful for Sets that actually contain units.
*        - If your Set contains non-unit elements, you must set the second parameter to true to avoid crashes.
*        - Good to use as safety mechanism before looping over unit Sets, because Sets (in contrast to Wc3 native unitgroups) don't automatically remove units that leave the game after Set creation.
*        - As an alternative, you can use SetUtils.subscribeSetToAutoUnitRemoval(Set) (see below) on the Set to automatically remove invalid unit references for the rest of the game.
*    SetUtils.subscribeSetToAutoUnitRemoval(Set [, boolean subscribe_yn])   [requires you to declare CUSTOM_DEFEND_ABICODE, see options below]
*        - Subscribes the specified set to automatic removal of invalid unit references, i.e. for the rest of the game, units that are removed from the game will also be removed from the specified set.
*        - Only useful for Sets that actually contain units. Avoids bugs, when looping over it. As an alternative, you can just call SetUtils.clearInvalidUnitReferences (see above) on the Set before each loop.
*        - Set the second parameter to false (default true) to unsubscribe the specified Set from the automatic unit cleaning.
*        - As long as a Set is subscribed, it will not be garbage collected.
*    SetUtils.triggerRegisterAnyUnitRemoveEvent(trigger)     [requires you to declare CUSTOM_DEFEND_ABICODE, see options below]
*        - Adds the event "any unit is removed from the game" to the specified trigger. Not compatible with other events on the same trigger!
*        - Use "GetTriggerUnit()" to refer to the unit being removed.
*        - Will trigger twice, if the remove unit also had the original Defend ability (or just another copy), so don't use this, if you also plan to use the Defend ability in your map.
*        - This functionality is not really Set-specific, but the system does use the event internally, so there is no reason to not offer it to you guys.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]
do
    -----------------
    -- | OPTIONS | --
    -----------------

    -- Debug-Mode will notify you per ingame error-message, if you confused dot-notation and colon-notation. The check doesn't recognize 100% of confusions, but works for most of the cases (and most important, it has no false positives).
    -- Set this to false before the release of your map.
    local DEBUG_MODE = true             ---@type boolean

    -- To be able to use SetUtils.subscribeSetToAutoUnitRemoval, you must provide the ability code of your custom Defend ability below.
    local CUSTOM_DEFEND_ABICODE = nil   ---@type integer -- (Option 1) Enter the abi code of your custom defend ability here (as FourCC('xxxx')). If you do, this resource will use the ability to detect units leaving the game.

    ---Start of code. No need to read further.---

    ---@diagnostic disable: param-type-mismatch, cast-local-type

    ---------------------
    -- | Set Library | --
    ---------------------

    ---@class Set
    Set = {
        data = {}               ---@type table data structure saving the actual elements
        ,   orderedKeys = {}    ---@type any[] to keep iteration order synchronized in multiplayer
        ,   n = 0               ---@type number number of elements in the Set
    }
    Set.__index = Set
    Set.__tostring = function(x)
        setmetatable(x,nil) --detach metatable from object to allow using normal tostring (below) without recursive call of Set.__tostring
        local result = tostring(x)
        result = 'Set' .. string.sub(result, string.find(result, ':', nil, true), -1)
        setmetatable(x,Set)
        return result
    end

    ---Prints an error message on screen, applying red color and the ERROR-prefix.
    ---@param message string
    local throwError = function(message) print("|cffff5555Error: " .. message .. "|r") end

    ---To be used as a first-line-check in methods that are required to be called with colon-notation.
    ---Prints an error-message on screen, if the method was instead used with dot-notation.
    ---Doesn't catch all false uses, but most of them. Most importantly, it never brings up false positives.
    ---@param methodName string name of the method, will be printed as part of the error message
    ---@param pseudoSelf any using colon-notation does always pass the object itself as first argument, so we can check it for if it is really a Set (ok) or not (error)
    local checkColonNotation = function(methodName, pseudoSelf)
        if getmetatable(pseudoSelf) ~= Set then
            throwError("Method " .. methodName .. " used with .-notation instead of :-notation.")
        end
    end

    ---To be used as a first-line-check in functions that are required to be called with dot-notation.
    ---Prints an error-message on screen, if the method was instead used with colon-notation.
    ---Doesn't catch all false uses, but most of them. Most importantly, it never brings up false positives.
    ---@param methodName string name of the method, will be printed as part of the error message
    ---@param firstArgumentOfMethod any using colon-notation does always pass the object itself as first argument, so in case of a wrong Set:method() call, the first passed argument would be the Set class itself (error).
    local checkDotNotation = function(methodName, firstArgumentOfMethod)
        if firstArgumentOfMethod == Set then
            throwError("Method " .. methodName .. " used with :-notation instead of .-notation.")
        end
    end

    ---Returns the wc3-type of any object, i.e. 'unit', if the input is a unit. Returns the Lua-type in case the input is not a Warcraft-type.
    ---@param input any the object to be checked
    ---@return string wc3Type
    local wc3Type = function (input)
        local typeString = type(input)
        if typeString == 'userdata' then
            typeString = tostring(input) --toString returns the warcraft type plus a colon and some hashstuff.
            return string.sub(typeString, 1, (string.find(typeString, ":", nil, true) or 0) -1) --string.find returns nil, if the argument is not found, which would break string.sub. So we need or as coalesce.
        else
            return typeString
        end
    end

    --- Set constructor. Creates a Set containing all specified arguments as elements. Not specifying any arguments will create an empty Set.
    ---@param ... any
    ---@return Set
    function Set.create(...)
        if DEBUG_MODE then checkDotNotation("Set.create(...)", ...) end
        local new = {}
        new.data = {} --place to save the actual elements of the set. Elements can't be saved in self, because they might conflict with function names of the class (adding the element "add" would prevent future access to the add-method).
        new.orderedKeys = {}
        setmetatable(new, Set)
        new:add(...)
        return new
    end

    --- Returns true, if the input parameter is a Set and false otherwise.
    ---@param anything any
    ---@return boolean
    function Set.isSet(anything)
        if DEBUG_MODE then checkDotNotation("Set.isSet(anything)", anything) end
        return getmetatable(anything) == Set
    end

    ---Adds a single given element to the set. Already existing Elements are a valid input, but won't be added again.
    ---@param element any
    ---@return Set self
    function Set:addSingle(element)
        if DEBUG_MODE then checkColonNotation("Set:addSingle(element)", self) end
        if element ~=nil and not self.data[element] then
            self.n = self.n + 1
            self.data[element] = self.n
            self.orderedKeys[self.n] = element
        end
        return self
    end

    ---Adds all specified arguments to the Set. Already existing elements are a valid input, but won't be added again.
    ---E.g. add(2, {2}) would add two elements, the number 2 and Set containing the number 2.
    ---@param ... any
    ---@return Set self
    function Set:add(...)
        if DEBUG_MODE then checkColonNotation("Set:add(...)", self) end
        for i = 1, select('#', ...) do
            self:addSingle(select(i, ...))
        end
        return self
    end

    ---Removes the specified element from set, if existent. Non-existent elements are a valid input, but won't change the Set.
    ---@param element any
    ---@return Set self
    function Set:removeSingle(element)
        if DEBUG_MODE then checkColonNotation("Set:removeSingle(element)", self) end
        if self.data[element] then
            local i,n = self.data[element], self.n
            self.data[self.orderedKeys[n]] = i --last element takes iteration slot of removed element
            self.orderedKeys[i] = self.orderedKeys[n]
            self.orderedKeys[n] = nil
            self.data[element] = nil
            self.n = self.n - 1
        end
        return self
    end

    ---Removes all specified arguments from the Set, if existent. Non-existent elements are a valid input, but won't change the Set.
    ---@param ... any
    ---@return Set self
    function Set:remove(...)
        if DEBUG_MODE then checkColonNotation("Set:remove(...)", self) end
        for i = 1, select('#', ...) do
            self:removeSingle(select(i, ...))
        end
        return self
    end

    ---returns true, if set contains given element, false otherwise
    ---@param element any element to check for
    ---@return boolean
    function Set:contains(element)
        if DEBUG_MODE then checkColonNotation("Set:contains(element)", self) end
        return self.data[element] ~= nil
    end

    ---Keeps all Elements in the set that are also present in another Set/SyncedTable/array/Force/Group. Removes all elements that are not.
    ---For SyncedTables and Arrays, elements refer to the values, not the keys. If the specified container is an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param container Set | SyncedTable | any[] | force | group
    ---@return Set self
    function Set:retainAll(container)
        if DEBUG_MODE then checkColonNotation("Set:retainAll(container)", self) end
        local typeString = wc3Type(container)
        --first add all elements to a Set, if the input container is not already. This allows to intersect more easily.
        local containerAsSet = Set.create()
        if typeString == 'group' then --Case 1: container is group
            ForGroup(container, function () containerAsSet:addSingle(GetEnumUnit()) end)
        elseif typeString == 'force' then --Case 2: container is force
            ForForce(container, function () containerAsSet:addSingle(GetEnumPlayer()) end)
        elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is Set
            containerAsSet = container
        elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
            for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
                containerAsSet:addSingle(element)
            end
        elseif(typeString == 'table') then --Case 5: container is a Table. We then assume, it's an array.
            for _, element in ipairs(container) do
                containerAsSet:addSingle(element)
            end
        else --Case 6: invalid input.
            throwError("retainAll is only compatible with a Set, SyncedTable, array, force or group")
            ---@diagnostic disable-next-line: missing-return-value
            return
        end

        -- do intersection
        for element in self:elements() do
            if not containerAsSet:contains(element) then self:removeSingle(element) end
        end
        return self
    end

    ---Removes all elements from the set that are present in another Set/SyncedTable/array/Force/Group. For SyncedTables and arrays, elements means values, not keys.
    ---If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key will be ignored.
    ---@param container Set | SyncedTable | any[] | force | group
    ---@return Set self
    function Set:removeAll(container)
        if DEBUG_MODE then checkColonNotation("Set:removeAll(container)", self) end
        local typeString = wc3Type(container)
        if typeString == 'group' then --Case 1: container is a group
            ForGroup(container, function () self:removeSingle(GetEnumUnit()) end)
        elseif typeString == 'force' then --Case 2: container is a force
            ForForce(container, function () self:removeSingle(GetEnumPlayer()) end)
        elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is a Set
            for element in container:elements() do
                self:removeSingle(element)
            end
        elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
            for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
                self:removeSingle(element)
            end
        elseif(type(container) == 'table') then --Case 5: container is a table. We then assume, it's a sequence.
            for _, element in ipairs(container) do
                self:removeSingle(element)
            end
        else --Case 6: invalid input.
            throwError("removeAll is only compatible with a Set, SyncedTable, array, force or group")
        end
        return self
    end

    ---Adds all Elements of the given Container to the Set.
    ---Specifying an array or SyncedTable will add all values of that table to the set.
    ---If you specify an array, it must be a sequence. Otherwise, all values beyond the first nil key will not be added.
    ---@param container Set | any[] | SyncedTable | force | group
    ---@return Set self
    function Set:addAll(container)
        if DEBUG_MODE then checkColonNotation("Set:addAll(container)", self) end
        local typeString = wc3Type(container)
        if typeString == 'group' then --Case 1: container is Group
            ForGroup(container, function () self:addSingle(GetEnumUnit()) end)
        elseif typeString == 'force' then --Case 2: container is Force
            ForForce(container, function () self:addSingle(GetEnumPlayer()) end)
        elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is Set
            for element in container:elements() do
                self:addSingle(element)
            end
        elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
            for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
                self:addSingle(element)
            end
        elseif typeString == 'table' then --Case 5: container is table (and we then assume it's an array)
            for _, element in ipairs(container) do
                self:addSingle(element)
            end
        else --Case 6: invalid input.
            throwError("addAll is only compatible with a Set, SyncedTable, array, force or group")
        end
        return self
    end

    ---Adds all keys of a given table as elements to the set. This method is only multiplayer-compatible, if you use a SyncedTable as input OR if you are have overwritten the pairs function to make it multiplayer-synchronous. Otherwise it might lead to desyncs.
    ---@param whichTable SyncedTable Adds all keys of given table to the set
    ---@return Set self
    function Set:addAllKeys(whichTable)
        if DEBUG_MODE then checkColonNotation("Set:addAllKeys(container)", self) end
        if(type(whichTable) == 'table') then
            for key, _ in pairs(whichTable) do --pairs-function is multiplayer synced for SyncedTables.
                self:add(key)
            end
        else
            throwError("AddAllKeys only compatible with SyncedTables")
        end
        return self
    end

    ---returns an iterator for a standard for loop
    ---usage: for element in set:elements() do ... end
    ---You can both remove and add elements during the loop. Added elements will also be contained in the loop.
    ---@return function iterator
    function Set:elements()
        if DEBUG_MODE then checkColonNotation("Set:elements()", self) end
        local i = 0
        local lastKey
        return function()
            if lastKey == self.orderedKeys[i] then
                i = i+1 --only increase i, if the last key in loop is still in place. If not, it means that the element has been removed and we need to stay at i.
            end
            lastKey = self.orderedKeys[i]
            return lastKey
        end
    end

    ---returns the number of elements in this set.
    ---@return integer
    function Set:size()
        if DEBUG_MODE then checkColonNotation("Set:size()", self) end
        return self.n
    end

    ---returns true, when the set is empty and false otherwise
    ---@return boolean
    function Set:isEmpty()
        if DEBUG_MODE then checkColonNotation("Set:isEmpty()", self) end
        return self:size() == 0
    end

    ---Returns a random element from the Set.
    function Set:random()
        if DEBUG_MODE then checkColonNotation("Set:random()", self) end
        return self.orderedKeys[math.random(self.n)]
    end

    ---removes all Elements from the set
    ---@return Set self
    function Set:clear()
        if DEBUG_MODE then checkColonNotation("Set:clear()", self) end
        self.data = {}
        self.orderedKeys = {}
        self.n = 0
        return self
    end

    ---Returns an array with exactly the elements of the Set. Only do this, when another function input needs an array, because why should you use a Set, when you convert it to an array anyway?
    ---@return any[] array
    function Set:toArray()
        if DEBUG_MODE then checkColonNotation("Set:toArray()", self) end
        local i,result = 1,{}
        for element in self:elements() do
            result[i] = element
            i = i+1
        end
        return result
    end

    ---Returns a comma separated list of all elements of <Set>, engulfed in {}-brackets.
    ---@return string
    function Set:toString()
        if DEBUG_MODE then checkColonNotation("Set:toString()", self) end
        local elementsToString = {}
        for i = 1, self.n do
            elementsToString[i] = tostring(self.orderedKeys[i]) --must be translated to strings, else table.concat wouldn't work.
        end
        return '{' .. table.concat(elementsToString, ', ', 1, self.n) .. '}'
    end

    ---prints all elements of the Set on Screen (space separated)
    function Set:print()
        if DEBUG_MODE then checkColonNotation("Set:print()", self) end
        print(self:toString())
    end

    ---Returns true, if this Set has at least one common element with another Set, i.e. the intersection is not empty.
    ---@param otherSet Set
    ---@return boolean haveCommonElement
    function Set:intersects(otherSet)
        if DEBUG_MODE then checkColonNotation("Set:intersects(otherSet)", self) end
        for element in self:elements() do
            if otherSet.data[element] then
                return true
            end
        end
        return false
    end

    ---Returns a copy of an existing Set.
    ---@return Set copy
    function Set:copy()
        if DEBUG_MODE then checkColonNotation("Set:copy()", self) end
        return Set.create():addAll(self)
    end

    ---Returns a new Set, which is the union of all specified parameters.
    ---You can specify any number of arguments of types Set, SyncedTable, array, force and group.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param ... Set | SyncedTable | any[] | force | group
    ---@return Set union
    function Set.union(...)
        if DEBUG_MODE then checkDotNotation("Set.union(...)", ...) end
        local resultSet = Set.create()
        for i = 1, select('#',...) do
            resultSet:addAll(select(i, ...))
        end
        return resultSet
    end

    ---Returns a new Set, which is the intersection of all specified parameters.
    ---You can specify any number of arguments of types Set, SyncedTable, array, force and group.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param ... Set | SyncedTable | any[] | force | group
    ---@return Set intersection
    function Set.intersection(...)
        if DEBUG_MODE then checkDotNotation("Set.intersection(...)", ...) end
        local n = select('#',...)
        local resultSet = Set.create()
        if n > 0 then resultSet:addAll(...) end --actually only adds the first container (addAll only supports one param)
        for i = 2, n do
            resultSet:retainAll(select(i,...))
        end
        return resultSet
    end

    ---Returns a new Set, which equals setA exluding the elements of setB.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param containerA Set | SyncedTable | any[] | force | group
    ---@param containerB Set | SyncedTable | any[] | force | group
    ---@return Set setDifference
    function Set.except(containerA, containerB)
        if DEBUG_MODE then checkDotNotation("Set.except(A,B)", containerA) end
        return Set.create():addAll(containerA):removeAll(containerB)
    end

    ---Returns the Set of all units from the specified unitgroup.
    ---@param unitgroup group
    ---@return Set
    function Set.fromGroup(unitgroup)
        if DEBUG_MODE then checkDotNotation("Set.fromGroup(unitgroup)", unitgroup) end
        local unitSet = Set.create()
        ForGroup(unitgroup, function () unitSet:addSingle(GetEnumUnit()) end)
        return unitSet
    end

    ---Returns the Set of all players from the specified playergroup.
    ---@param playergroup force
    ---@return Set
    function Set.fromForce(playergroup)
        if DEBUG_MODE then checkDotNotation("Set.fromForce(playergroup)", playergroup) end
        local playerSet = Set.create()
        ForForce(playergroup, function () playerSet:addSingle(GetEnumPlayer()) end)
        return playerSet
    end

    ---Returns a new Set with all elements from the specified table (i.e. all values, not keys). The table must be either a SyncedTable or array.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param whichTable SyncedTable | any[]
    ---@return Set
    function Set.fromTable(whichTable)
        if DEBUG_MODE then checkDotNotation("Set.fromTable(whichTable)", whichTable) end
        return Set.create():addAll(whichTable)
    end

    --------------------------
    -- | SetUtils Library | --
    --------------------------

    --Mimick existing pick natives, but create Sets instead of groups, forces, destructable
    SetUtils = {}

    local autoUnitRemoveSubscriptions = Set.create() --Set of all Sets that are subscribed to automatic unit removal. Only in use, when a custom defend ability was provided.

    local getUnitTypeId, unitAddAbility, unitMakeAbilityPermanent = GetUnitTypeId, UnitAddAbility, UnitMakeAbilityPermanent --localize natives for quicker access

    ---Removes the specified unit from all sets subscribed to auto-removal of invalid unit references.
    ---Called upon any unit leaving the game.
    ---@param unitToRemove unit
    local function removeUnitFromSubscribedSets(unitToRemove)
        for set in autoUnitRemoveSubscriptions:elements() do
            set:removeSingle(unitToRemove)
        end
    end

    local checkDotNotation = function(firstArgumentOfMethod)
        if firstArgumentOfMethod == SetUtils then
            throwError("SetUtils method used with :-notation instead of .-notation.")
        end
    end

    --------UnitGroups---------

    ---Executes the specified enumFunc to create a unitgroup and converts the output to a set.
    ---@param enumFunc function
    ---@param destroyCondition? boolexpr pass a boolean expr if you want it to be destroyed after the enumeration.
    ---@param ... any the parameters to pass to enumFunc
    ---@return Set
    local function enumGroupToSet(enumFunc, destroyCondition, ...)
        if DEBUG_MODE then checkDotNotation(...) end
        local unitgroup = CreateGroup()
        enumFunc(unitgroup, ...)
        local unitSet = Set.fromGroup(unitgroup)
        DestroyGroup(unitgroup)
        if destroyCondition then
            DestroyBoolExpr(destroyCondition)
        end
        return unitSet
    end

    ---Returns Condition(func), if the input a is a function. Returns the input otherwise.
    ---@param func? function | boolexpr
    ---@return boolexpr condition, boolean converted_yn
    local function conditionIfNecessary(func)
        local converted_yn = type(func) == 'function' ---@diagnostic disable-next-line: return-type-mismatch
        return (converted_yn and Condition(func)) or func, converted_yn --real boolexpr have type userdata
    end

    ---Returns the Set of all units in a specified rect that match a specified condition.
    ---Use GetFilterUnit() to refer to the unit being checked by the condition.
    ---@param whichRect rect
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getUnitsInRectMatching(whichRect, condition)
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        return enumGroupToSet(GroupEnumUnitsInRect, converted_yn and convertedCondition or nil, whichRect, convertedCondition)
    end

    ---Returns the Set of all units in a specified rect.
    ---@param whichRect rect
    ---@return Set
    function SetUtils.getUnitsInRect(whichRect)
        return SetUtils.getUnitsInRectMatching(whichRect)
    end

    ---Returns the Set of all units within a specified radius of the specified coordinates matching the specified condition.
    ---Use GetFilterUnit() to refer to the unit being checked by the condition.
    ---@param x real
    ---@param y real
    ---@param radius real
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getUnitsInRangeMatching(x, y, radius, condition)
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        return enumGroupToSet(GroupEnumUnitsInRange, converted_yn and convertedCondition or nil, x, y, radius, convertedCondition)
    end

    ---Returns the Set of all units within a specified radius of the specified coordinates.
    ---@param x real
    ---@param y real
    ---@param radius real
    ---@return Set
    function SetUtils.getUnitsInRange(x, y, radius)
        return SetUtils.getUnitsInRangeMatching(x, y, radius)
    end

    ---Returns the Set of all units owned by the specified player and matching the specified condition.
    ---Use GetFilterUnit() to refer to the unit being checked by the condition.
    ---@param whichPlayer player
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getUnitsOfPlayerMatching(whichPlayer, condition)
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        return enumGroupToSet(GroupEnumUnitsOfPlayer, converted_yn and convertedCondition or nil, whichPlayer, convertedCondition)
    end

    ---Returns the Set of all units owned by the specified player.
    ---@param whichPlayer player
    ---@return Set
    function SetUtils.getUnitsOfPlayer(whichPlayer)
        return SetUtils.getUnitsOfPlayerMatching(whichPlayer)
    end

    --Permanently stores conditions for retreiving typeIds and the worldBounds rect. Both are created on demand. Prevents continous re-creation of conditionfuncs from anonymous functions.
    --TypeIds are static and limited in number, so there's not much chance of creating unused objects with this system.
    local storage = setmetatable({}, {})
    getmetatable(storage).__index = function(t,k)
        if k == 'worldBounds' then
            t[k] = GetWorldBounds()
        elseif k == 'returnTrue' then
            t[k] = Condition(function() return true end)
        else
            t[k] = Condition(function() return GetUnitTypeId(GetFilterUnit()) == k end)
        end
        return rawget(t,k)
    end

    ---Returns the Set of all units having the specified unitType and matching the specified condition.
    ---Use GetFilterUnit() to refer to the unit being checked by the condition.
    ---@param typeId integer
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getUnitsOfTypeIdMatching(typeId, condition)
        condition = condition or storage['returnTrue']
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        local logicalAnd = And(storage[typeId], convertedCondition)
        local returnSet = enumGroupToSet(GroupEnumUnitsInRect, logicalAnd, storage['worldBounds'], logicalAnd)
        if converted_yn then DestroyBoolExpr(convertedCondition) end
        return returnSet
    end

    ---Returns the Set of all units owned by the specified player and having the specified unitType.
    ---Use GetFilterUnit() to refer to the unit being checked by the condition.
    ---@param whichPlayer player
    ---@param typeId integer
    ---@return Set
    function SetUtils.getUnitsOfPlayerAndTypeId(whichPlayer, typeId)
        return enumGroupToSet(GroupEnumUnitsOfPlayer, nil, whichPlayer, storage[typeId])
    end

    ---Returns the Set of all units having the specified unitType.
    ---@param typeId integer
    ---@return Set
    function SetUtils.getUnitsOfTypeId(typeId)
        return SetUtils.getUnitsOfTypeIdMatching(typeId)
    end

    ---Returns the Set of all units being currently selected by a player and matching the specified condition.
    ---@param whichPlayer player
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getUnitsSelected(whichPlayer, condition)
        SyncSelections() --important to prevent desyncs, as selections are saved locally.
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        return enumGroupToSet(GroupEnumUnitsSelected, converted_yn and convertedCondition or nil, whichPlayer, convertedCondition)
    end

    SetUtils.getUnitsSelectedMatching = SetUtils.getUnitsSelected

    --------PlayerGroups---------

    ---Returns the Set of all players.
    ---Contains players that were present during game start, including computer players.
    ---@return Set
    function SetUtils.getPlayersAll()
        return Set.fromForce(GetPlayersAll()) --global wc3 var, doesn't produce memory leaks
    end

    ---Returns the Set of all active players (including computer players) matching the specified condition.
    ---Only contains players that were present during game start, including computer players.
    ---Use GetFilterPlayer() to refer to the player being checked in the condition.
    ---@param condition function | boolexpr
    ---@return Set
    function SetUtils.getPlayersMatching(condition)
        local playergroup = CreateForce()
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        ForceEnumPlayers(playergroup, convertedCondition)
        local playerSet = Set.fromForce(playergroup)
        DestroyForce(playergroup)
        if converted_yn then
            DestroyBoolExpr(convertedCondition)
        end
        return playerSet
    end

    --------DestructableGroups---------

    ---Returns the Set of all destructables in the specified rect matching the specified condition.
    ---Use GetFilterDestructable() to refer to the destructable being checked in the condition.
    ---@param whichRect rect
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getDestructablesInRectMatching(whichRect, condition)
        local destructableSet = Set.create()
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        EnumDestructablesInRect(whichRect, convertedCondition, function() destructableSet:add(GetEnumDestructable()) end)
        if converted_yn then
            DestroyBoolExpr(convertedCondition)
        end
        return destructableSet
    end

    ---Returns the Set of all destructables in the specified rect.
    ---@param whichRect rect
    ---@return Set
    function SetUtils.getDestructablesInRect(whichRect)
        return SetUtils.getDestructablesInRectMatching(whichRect)
    end

    --------ItemGroups---------

    ---Returns the Set of all items in the specified rect matching the specified condition.
    ---Use GetFilterItem() to refer to the item being checked in the condition.
    ---@param whichRect rect
    ---@param condition function | boolexpr
    ---@return Set
    function SetUtils.getItemsInRectMatching(whichRect, condition)
        local itemSet = Set.create()
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        EnumItemsInRect(whichRect, convertedCondition, function() itemSet:add(GetEnumItem()) end)
        if converted_yn then
            DestroyBoolExpr(convertedCondition)
        end
        return itemSet
    end

    ---Returns the Set of all items in the specified rect.
    ---@param whichRect rect
    ---@return Set
    function SetUtils.getItemsInRect(whichRect)
       return SetUtils.getItemsInRectMatching(whichRect)
    end

    --------Utility---------

    ---Removes all invalid unit references from the specified Set, i.e. units that have already been removed from the game.
    ---If your Set contains non-unit elements, you must set the second parameter to true to avoid crashes.
    ---@param whichSet Set the Set that might contain references to removed units
    ---@param checkIfUnit? boolean default: false. Set to true to avoid crashes, if the Set contains non-unit elements.
    function SetUtils.clearInvalidUnitRefs(whichSet, checkIfUnit)
        for element in whichSet:elements() do
            if not checkIfUnit or wc3Type(element) == 'unit' then
                if getUnitTypeId(element) == 0 then
                    whichSet:removeSingle(element)
                end
            end
        end
    end

    ---Subscribes the specified set to automatic removal of invalid unit references, i.e. for the rest of the game, units that are removed from the game will also be removed from the specified set.
    ---Set the second parameter to false (default true) to unsubscribe the specified Set from the automatic unit cleaning.
    ---As long as a Set is subscribed, it will not be garbage collected.
    ---This function requires CUSTOM_DEFEND_ABICODE to be set.
    ---@param whichSet Set
    ---@param subscribe_yn? boolean default: true. true to subscribe. false to unsubscribe.
    function SetUtils.subscribeSetToAutoUnitRemoval(whichSet, subscribe_yn)
        if CUSTOM_DEFEND_ABICODE then
            if subscribe_yn or subscribe_yn == nil then
                SetUtils.clearInvalidUnitRefs(whichSet, true)
                autoUnitRemoveSubscriptions:addSingle(whichSet)
            else
                autoUnitRemoveSubscriptions:removeSingle(whichSet)
            end
        else
            throwError("You can't use SetUtils.subscribeToAutoUnitRemoval, until you have provided a custom defend ability.")
        end
    end

    local hasUnitBeenRemovedCondition ---@type conditionfunc initialized in SetUtils.createTriggers() below.

    ---Adds the event "unit is removed from the game" to the specified trigger.
    ---This event is not compatible with other events, so don't use it on triggers with multiple events (other events will simply be invalidated).
    ---Requires CUSTOM_DEFEND_ABICODE to be set.
    ---@param whichTrigger trigger
    function SetUtils.triggerRegisterAnyUnitRemoveEvent(whichTrigger)
        if CUSTOM_DEFEND_ABICODE then
            TriggerRegisterAnyUnitEventBJ(whichTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
            TriggerAddCondition(whichTrigger, hasUnitBeenRemovedCondition)
        else
            throwError("You can't use SetUtils.triggerRegisterAnyUnitRemoveEvent, until you have provided a custom defend ability.")
        end
    end

    --------Triggers for unit auto removal---------

    function SetUtils.createTriggers()
        --Init unit reference cleanup methods
        --Add the custom defend ability to all units entering the map, if it was provided.
        if CUSTOM_DEFEND_ABICODE then
            for i = 0, GetBJMaxPlayers() - 1 do
                SetPlayerAbilityAvailable(Player(i), CUSTOM_DEFEND_ABICODE, false)
            end
            local enterTrigger = CreateTrigger()
            TriggerRegisterEnterRectSimple( enterTrigger, bj_mapInitialPlayableArea )
            local function prepareNewUnit()
                local u = GetTriggerUnit()
                unitAddAbility(u, CUSTOM_DEFEND_ABICODE)
                unitMakeAbilityPermanent(u, true, CUSTOM_DEFEND_ABICODE)
            end
            TriggerAddAction(enterTrigger, prepareNewUnit)

            --Initialize upvalue. We don't set hasUnitBeenRemovedCondition earlier to prevent using Wc3 natives in the Lua root.
            hasUnitBeenRemovedCondition = Condition(function() return (GetIssuedOrderId() == 852056) and GetUnitAbilityLevel(GetTriggerUnit(), CUSTOM_DEFEND_ABICODE) == 0 end) --undefend order. This one is issued upon units leaving the game, but also under other circumstances. Ability-Level == 0 proves the removed from the game event.

            local removeTrigger = CreateTrigger()
            SetUtils.triggerRegisterAnyUnitRemoveEvent(removeTrigger)
            TriggerAddAction(removeTrigger, function() removeUnitFromSubscribedSets(GetTriggerUnit()) end)
        end
    end

    ---@diagnostic disable-next-line: undefined-global
    if OnInit and OnInit.trig then OnInit.trig(SetUtils.createTriggers) end --use TotalInit library, if available.
end
if Debug and Debug.endFile then Debug.endFile() end
Lua:
if Debug and Debug.beginFile then Debug.beginFile("Set/Group") end
--[[

------------------------------------
-- | API Set & Set Utils v1.3.3 | --
------------------------------------

 by Eikonium

 --> https://www.hiveworkshop.com/threads/lua-set-group-datastructure.331886/

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Sets are data containers that can contain every element only once. Much like unitgroups, but not limited to units.                                                    |
| The Set-API offers functions to create, alter and loop through Sets.                                                                                                  |
| The SetUtils library offers equivalents to pick-all-units-matching-condition-natives (players/destructables/items) that return a Set instead of a group or force.     |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Optional Dependencies:                                                                                                                                                |
|                                                                                                                                                                       |
| Total Initialization by Bribe:                                                                                                                                        |
|       https://www.hiveworkshop.com/threads/lua-global-initialization.317099/                                                                                          |
|       Having Total Init in your map script frees you of creating a GUI trigger that runs SetUtils.createTriggers() on Map Init.                                       |
|       Please make sure that you copy Global Initilization to a script file in your map that is located above(!) the Set library.                                      |
| SyncedTable                                                                                                                                                           |
|       https://www.hiveworkshop.com/threads/lua-syncedtable.332894/                                                                                                    |
|       Many Set-functionalities like union, intersection, except, fromTable and addAllKeys do support arrays and SyncedTables, but not normal tables (because it could |
|       lead to desyncs).                                                                                                                                               |
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* API Functions:
*
* The API uses standard Lua object-oriented syntax, i.e. you need to pay attention to whether the function you want to use requires dot- or colon-notation (it's clearly visible in the documentation).
* The library provides a debug-mode that allows you to get notified about when you used dots instead of colons and vice versa.
*
* -------------
* | Set class |
* -------------
*        - The class itself mainly offers ways to create new Sets.
*    Set.create(...) -> Set
*        - Creates a new Set and adds all specified arguments as elements.
*        - Example:
*           local a = Set.create("bla", 50, Player(0)) --creates a Set with 3 elements.
*    Set.union(...) -> Set
*        - Creates a new Set as the union of all specified arguments.
*        - All arguments must be either Sets, SyncedTables, arrays, forces or groups. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
*    Set.intersection(...) -> Set
*        - Creates a new Set as the intersection of all specified arguments.
*        - All arguments must be either Sets, SyncedTables, arrays, forces or groups. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
*    Set.except(containerA, containerB) -> Set
*        - Creates a new Set having all elements from specified containerA except the elements from specified containerB.
*        - All arguments must be either Sets, SyncedTables, arrays, forces or groups. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
*    Set.fromForce(force) -> Set
*        - Creates a new Set containing all players from the specified force.
*    Set.fromGroup(group) -> Set
*        - Creates a new Set containing all units fromn the specified group.
*    Set.fromTable(array|SyncedTable) -> Set
*        - Creates a new Set containing all elements from the specified table (elements refer to the values of the table, not the keys).
*        - Table must be either a SyncedTable or an array. If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
* ---------------
* | Set objects |
* ---------------
*        - The following methods are available for any existing Set object.
*    --------------------------------
*    | ADDING AND REMOVING ELEMENTS |
*    --------------------------------
*    <Set>:add(...)
*        - adds any number of specified arguments as elements to <Set>
*        - ignores all arguments that are already contained in <Set>
*        - returns <Set> to allow for chaining methods
*        - if you highly care for performance and only want to add one single element, you can use <Set>:addSingle(element) instead.
*    <Set>:remove(...)
*        - removes any number of specified arguments from <Set>
*        - ignores all arguments that are not contained in <Set>
*        - returns <Set> to allow for chaining methods
*        - if you highly care for performance and only want to remove one single element, you can use <Set>:removeSingle(element) instead.
*    <Set>:addAll(container)
*        - adds all elements from the specified container as elements to <Set> (and ignores everything that is already present in <Set>)
*        - the container must be a Set, array, force or group (in case of an array, "elements" refers to its values)
*        - returns <Set> to allow for chaining methods
*    <Set>:addAllKeys(SyncedTable) [or any table with a multiplayer-synched pairs function]
*        - adds all keys from the specified table as elements to <Set>
*        - this method is only multiplayer-compatible, if you input a SyncedTable or if you have overwritten the pairs-function to make it synchronous on the input table. Otherwise, using this method can lead to a desync.
*        - returns <Set> to allow for chaining methods
*    <Set>:removeAll(container)
*        - removes all elements present in the specified container from <Set> (and ignores everything that is not contained in <Set>)
*        - the container must be a Set, array, force or group.
*        - returns <Set> to allow for chaining methods
*    <Set>:retainAll(container)
*        - only keeps elements in <Set> that are also contained in the specified container and removes the rest.
*        - the container must be a Set, array, force or group.
*        - returns <Set> to allow for chaining methods
*    <Set>:clear()
*        - removes all elements from <Set>
*        - returns <Set> (which is empty now) to allow for chaining methods
*    ---------------------
*    | LOOPING MECHANISM |
*    ---------------------
*    <Set>:elements()
*        - iterator function for the generic for-loop over <Set>
*        - Example:
*             local exampleSet = Set.create(Player(0), Player(1), "bla", 5)
*             for dings in exampleSet:elements() do
*                 print(dings)
*             end
*    -----------
*    | UTILITY |
*    -----------
*    <Set>:contains(element) -> boolean
*        - returns true, if the element is contained in <Set> and false otherwise.
*    <Set>:size() -> integer
*        - returns the number of elements of the set.
*    <Set>:isEmpty() -> boolean
*        - returns true, if <Set> contains no elements, and false otherwise.
*    <Set>:toString() -> string
*        - returns a comma separated list of all elements of <Set>, engulfed in {}-brackets.
*    <Set>:print()
*        - prints <Set>:toString() on screen.
*    <Set>:random() -> any
*        - returns a random element from <Set>
*    <Set>:toArray() -> any[]
*        - returns a normal array (table) containing ell elements from <Set>.
*        - not really useful in most cases, as arrays are not better than sets most of the time. There are exceptions however, e.g. when you want to sort the elements (Sets don't have an order, but arrays have).
*    <Set>:intersects(otherSet) -> boolean
*        - Returns true, if <Set> has at least one common element with the specified argument.
*        - Argument currently only supports other Sets - not arrays, forces or groups.
*    <Set>:copy() -> Set
*        - Returns a new Set containing the same elements as <Set>.
* -------------------
* | Set Utils class |
* -------------------
*        - this class offers set equivalents for the Wc3 natives that pick and return a group of units, players, destructables and items.
*    ------------------
*    | SIMPLE GETTERS |
*    ------------------
*        | -> Units |
*        ------------
*        - in contrast to the wc3 natives, all SetUtils unit getters have the option to include locust units.
*        SetUtils.getUnitsInRect(rect [, boolean includeLocust])
*            - returns a new Set with all units located in the specified rect, optionally including units with locust (default: false).
*            - like the Warcraft native, the rect is considered an half-open rectangle (closed to west and south, open to east and north?). That means that entering units are not guaranteed to be picked.
*        SetUtils.getUnitsInRange(float x, float y, float radius [, boolean includeLocust])
*            - returns a new Set with all units within the specified radius of the specified coordinates, optionally including units with locust (default: false).
*        SetUtils.getUnitsOfPlayer(player [, boolean includeLocust])
*            - returns a new Set with all units owned by the specified player, optionally including units with locust (default: false).
*        SetUtils.getUnitsOfTypeId(integer typeId [, boolean includeLocust])
*            - returns a new Set with all units on the map having the specified type, optionally including units with locust (default: false).
*            - the typeId parameter has to be created out of the FourCC-function, e.g. SetUtils.getUnitsOfTypeId(FourCC('hfoo'))
*        SetUtils.getUnitsOfPlayerAndTypeId(player, integer typeId [, boolean includeLocust])
*            - returns a new Set with all units owned by the specified player and having the specified type, optionally including units with locust (default: false).
*        SetUtils.getUnitsSelected(player)
*            - returns a new Set with all units currently being selected by the specified player.
*            - returns the empty Set, when the player doesn't have any units selected.
*            - this function needs to synchronize local selections between players, so it might not be as instant as usual (needs investigation).
*        ------------------------------------
*        | -> Players, Destructables, Items |
*        ------------------------------------
*        SetUtils.getPlayersAll()
*            - returns a new Set containing all players that were present during map init.
*        SetUtils.getDestructablesInRect(rect)
*            - returns a Set with all Destructables located in the specified rect.
*        SetUtils.getItemsInRect(rect)
*            - returns a Set with all Items located in the specified rect.
*    -----------------------
*    | CONDITIONAL GETTERS |
*    -----------------------
*        - Below functions are variants of the above pick functions that can additionally take any number of conditions.
*          Only elements passing all conditions will join the set.
*        ------------
*        | -> Units |
*        ------------
*        - All condition functions in the unit conditional getters API must be either be functions taking a unit and returning a boolean, or functions taking nothing and returning a boolean.
*           If choosing the latter method, use GetFilterUnit() to refer to the unit being checked.
*        - Anonymous lua functions are a suitable way to pass conditions.
*        SetUtils.getUnitsInRectMatching(rect [, boolean includeLocust] [, function(unit):boolean condition1] [, function(unit):boolean condition2] [, ...])
*            - returns a new Set with all units located in the specified rect and matching all specified conditions, optionally including units with locust (default: false).
*            - like the Warcraft native, the rect is considered an half-open rectangle (closed to west and south, open to east and north?). That means that entering units are not guaranteed to be picked.
*            - Example:
*               local r = <someRect>
*               local exampleSet = SetUtils.getUnitsInRectMatching(r, nil, function(u) return GetOwningPlayer(u) == Player(0) end) --will contain all units in the rect owned by Player 1.
*        SetUtils.getUnitsInRangeMatching(float x, float y, float radius [, boolean includeLocust] [, function(unit):boolean condition1] [, function(unit):boolean condition2] [, ...])
*            - returns a new Set with all units within the specified radius of the specified coordinates that match all specified conditions, optionally including units with locust (default: false).
*        SetUtils.getUnitsOfPlayerMatching(player [, boolean includeLocust] [, function(unit):boolean condition1] [, function(unit):boolean condition2] [, ...])
*            - returns a new Set with all units owned by the specified player and matching all specified conditions, optionally including units with locust (default: false).
*        SetUtils.getUnitsOfTypeIdMatching(integer typeId [, boolean includeLocust] [, function(unit):boolean condition1] [, function(unit):boolean condition2] [, ...])
*            - returns a new Set with all units on the map having the specified type and matching all specified conditions, optionally including units with locust (default: false).
*            - the typeId parameter has to be created out of the FourCC-function, e.g. SetUtils.getUnitsOfTypeId(FourCC('hfoo'))
*        SetUtils.getUnitsSelectedMatching(player [, function(unit):boolean condition1] [, function(unit):boolean condition2] [, ...])
*            - returns a new Set with all units currently being selected by the specified player and matching all specified conditions.
*            - returns the empty Set, when the player doesn't have any units selected.
*            - this function needs to synchronize local selections between players, so it might not be as instant as usual (needs investigation).
*        ------------------------------------
*        | -> Players, Destructables, Items |
*        ------------------------------------
*        - All condition functions in this part of the API must be warcraft conditionfuncs (i.e. functions taking nothing and returning a boolean engulfed by Condition()).
*           You can however directly pass a Lua-function and SetUtils will do the Condition() stuff for you.
*        SetUtils.getPlayersMatching(function conditionfunc)
*            - returns a new Set containing all players that were present during map init and who match the specified condition.
*        SetUtils.getDestructablesInRectMatching(rect, function conditionfunc)
*            - returns a Set with all Destructables located in the specified rect and matching the specified condition.
*        SetUtils.getItemsInRectMatching(rect, function conditionfunc)
*            - returns a Set with all Items located in the specified rect and matching the specified condition.
*    -----------
*    | UTILITY |
*    -----------
*    SetUtils.clearInvalidUnitRefs(Set [, boolean checkIfUnit])
*        - Removes all invalid unit references from the specified Set, i.e. units that have already been removed from the game.
*        - Only useful for Sets that actually contain units.
*        - If your Set contains non-unit elements, you must set the second parameter to true to avoid crashes.
*        - Good to use as safety mechanism before looping over unit Sets, because Sets (in contrast to Wc3 native unitgroups) don't automatically remove units that leave the game after Set creation.
*        - As an alternative, you can use SetUtils.subscribeSetToAutoUnitRemoval(Set) (see below) on the Set to automatically remove invalid unit references for the rest of the game.
*    SetUtils.subscribeSetToAutoUnitRemoval(Set [, boolean subscribe_yn])   [requires you to declare CUSTOM_DEFEND_ABICODE, see options below]
*        - Subscribes the specified set to automatic removal of invalid unit references, i.e. for the rest of the game, units that are removed from the game will also be removed from the specified set.
*        - Only useful for Sets that actually contain units. Avoids bugs, when looping over it. As an alternative, you can just call SetUtils.clearInvalidUnitRefs (see above) on the Set before each loop.
*        - Set the second parameter to false (default true) to unsubscribe the specified Set from the automatic unit cleaning.
*        - As long as a Set is subscribed, it will not be garbage collected.
*    SetUtils.triggerRegisterAnyUnitRemoveEvent(trigger)     [requires you to declare CUSTOM_DEFEND_ABICODE, see options below]
*        - Adds the event "any unit is removed from the game" to the specified trigger. Not compatible with other events on the same trigger!
*        - Use "GetTriggerUnit()" to refer to the unit being removed.
*        - Will trigger twice, if the remove unit also had the original Defend ability (or just another copy), so don't use this, if you also plan to use the Defend ability in your map.
*        - This functionality is not really Set-specific, but the system does use the event internally, so there is no reason to not offer it to you guys.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]
do
    -----------------
    -- | OPTIONS | --
    -----------------

    -- Debug-Mode will notify you per ingame error-message, if you confused dot-notation and colon-notation. The check doesn't recognize 100% of confusions, but works for most of the cases (and most important, it has no false positives).
    -- Set this to false before the release of your map.
    local DEBUG_MODE = true             ---@type boolean

    -- The SetUtils library internally maintains a Set of all units in the game, but needs to exclude units that are removed from the game. Otherwise, the pick functions will include invalid unit references, which could cause several bugs.
    -- Per default, every pick function will check every unit for if it has been removed from the game or not, but you can additionally choose one of the following options to gain additional performance.
    -- Option 1: Resource will exclude units immediately upon being removed from the game (trigger-based).
        -- Usage: Create a custom copy of the "Defend"-ability in object editor and enter its ability code to CUSTOM_DEFEND_ABICODE below.
        -- Recommended if: you cycle a lot of units and plan to use a lot of pick-functions from the SetUtils-library.
    -- Option 2: Resource will periodically check for invalid unit references.
        -- Usage: Set CLEAN_INTERVAL below to a sensible value, e.g. 300 (-> every 5 minutes).
        -- Recommended if: you cycle a lot of units, but rarely use a SetUtils pick-function.
    -- Option 3: Only check for invalid unit references upon using a pick-function.
        -- Usage: This behaviour is always active, so you don't need to change any constant below.
        -- Recommended if: you don't use SetUtils pick functions at all.
    local CUSTOM_DEFEND_ABICODE = nil   ---@type integer -- (Option 1) Enter the abi code of your custom defend ability here (as FourCC('xxxx')). If you do, this resource will use the ability to detect units leaving the game.
    local CLEAN_INTERVAL = nil          ---@type number    -- (Option 2) Interval in seconds to check for invalid unit references. Once every few minutes should be sufficient.

    ---Start of code. No need to read further.---

    ---------------------
    -- | Set Library | --
    ---------------------

    ---@class Set
    Set = {
        data = {}               ---@type table data structure saving the actual elements
        ,   orderedKeys = {}    ---@type any[] to keep iteration order synchronized in multiplayer
        ,   n = 0               ---@type number number of elements in the Set
    }
    Set.__index = Set
    Set.__tostring = function(x)
        setmetatable(x,nil) --detach metatable from object to allow using normal tostring (below) without recursive call of Set.__tostring
        local result = tostring(x)
        result = 'Set' .. string.sub(result, string.find(result, ':', nil, true), -1)
        setmetatable(x,Set)
        return result
    end

    ---Prints an error message on screen, applying red color and the ERROR-prefix.
    ---@param message string
    local printError = function(message) print("|cffff5555Error: " .. message .. "|r") end

    ---To be used as a first-line-check in methods that are required to be called with colon-notation.
    ---Prints an error-message on screen, if the method was instead used with dot-notation.
    ---Doesn't catch all false uses, but most of them. Most importantly, it never brings up false positives.
    ---@param methodName string name of the method, will be printed as part of the error message
    ---@param pseudoSelf any using colon-notation does always pass the object itself as first argument, so we can check it for if it is really a Set (ok) or not (error)
    local checkColonNotation = function(methodName, pseudoSelf)
        if getmetatable(pseudoSelf) ~= Set then
            printError("Method " .. methodName .. " used with .-notation instead of :-notation.")
        end
    end

    ---To be used as a first-line-check in functions that are required to be called with dot-notation.
    ---Prints an error-message on screen, if the method was instead used with colon-notation.
    ---Doesn't catch all false uses, but most of them. Most importantly, it never brings up false positives.
    ---@param methodName string name of the method, will be printed as part of the error message
    ---@param firstArgumentOfMethod any using colon-notation does always pass the object itself as first argument, so in case of a wrong Set:method() call, the first passed argument would be the Set class itself (error).
    local checkDotNotation = function(methodName, firstArgumentOfMethod)
        if firstArgumentOfMethod == Set then
            printError("Method " .. methodName .. " used with :-notation instead of .-notation.")
        end
    end

    ---Returns the wc3-type of any object, i.e. 'unit', if the input is a unit. Returns the Lua-type in case the input is not a Warcraft-type.
    ---@param input any the object to be checked
    ---@return string wc3Type
    local wc3Type = function (input)
        local typeString = type(input)
        if typeString == 'userdata' then
            typeString = tostring(input) --toString returns the warcraft type plus a colon and some hashstuff.
            return string.sub(typeString, 1, (string.find(typeString, ":", nil, true) or 0) -1) --string.find returns nil, if the argument is not found, which would break string.sub. So we need or as coalesce.
        else
            return typeString
        end
    end

    --- Set constructor. Creates a Set containing all specified arguments as elements. Not specifying any arguments will create an empty Set.
    ---@param ... any
    ---@return Set
    function Set.create(...)
        if DEBUG_MODE then checkDotNotation("Set.create(...)", ...) end
        local new = {}
        new.data = {} --place to save the actual elements of the set. Elements can't be saved in self, because they might conflict with function names of the class (adding the element "add" would prevent future access to the add-method).
        new.orderedKeys = {}
        setmetatable(new, Set)
        new:add(...)
        return new
    end

    --- Returns true, if the input parameter is a Set and false otherwise.
    ---@param anything any
    ---@return boolean
    function Set.isSet(anything)
        if DEBUG_MODE then checkDotNotation("Set.isSet(anything)", anything) end
        return getmetatable(anything) == Set
    end

    ---Adds a single given element to the set. Already existing Elements are a valid input, but won't be added again.
    ---@param element any
    ---@return Set self
    function Set:addSingle(element)
        if DEBUG_MODE then checkColonNotation("Set:addSingle(element)", self) end
        if element ~=nil and not self.data[element] then
            self.n = self.n + 1
            self.data[element] = self.n
            self.orderedKeys[self.n] = element
        end
        return self
    end

    ---Adds all specified arguments to the Set. Already existing elements are a valid input, but won't be added again.
    ---E.g. add(2, {2}) would add two elements, the number 2 and Set containing the number 2.
    ---@param ... any
    ---@return Set self
    function Set:add(...)
        if DEBUG_MODE then checkColonNotation("Set:add(...)", self) end
        for i = 1, select('#',...) do
            self:addSingle(select(i, ...))
        end
        return self
    end

    ---Removes the specified element from set, if existent. Non-existent elements are a valid input, but won't change the Set.
    ---@param element any
    ---@return Set self
    function Set:removeSingle(element)
        if DEBUG_MODE then checkColonNotation("Set:removeSingle(element)", self) end
        if self.data[element] then
            local i,n = self.data[element], self.n
            self.data[self.orderedKeys[n]] = i --last element takes iteration slot of removed element
            self.orderedKeys[i] = self.orderedKeys[n]
            self.orderedKeys[n] = nil
            self.data[element] = nil
            self.n = self.n - 1
        end
        return self
    end

    ---Removes all specified arguments from the Set, if existent. Non-existent elements are a valid input, but won't change the Set.
    ---@param ... any
    ---@return Set self
    function Set:remove(...)
        if DEBUG_MODE then checkColonNotation("Set:remove(...)", self) end
        for i = 1, select('#', ...) do
            self:removeSingle(select(i, ...))
        end
        return self
    end

    ---returns true, if set contains given element, false otherwise
    ---@param element any element to check for
    ---@return boolean
    function Set:contains(element)
        if DEBUG_MODE then checkColonNotation("Set:contains(element)", self) end
        return self.data[element] ~= nil
    end

    ---@diagnostic disable: param-type-mismatch

    ---Keeps all Elements in the set that are also present in another Set/SyncedTable/array/Force/Group. Removes all elements that are not.
    ---For SyncedTables and Arrays, elements refer to the values, not the keys. If the specified container is an array, it must form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param container Set | SyncedTable | any[] | force | group
    ---@return Set self
    function Set:retainAll(container)
        if DEBUG_MODE then checkColonNotation("Set:retainAll(container)", self) end
        local typeString = wc3Type(container)
        --first add all elements to a Set, if the input container is not already. This allows to intersect more easily.
        local containerAsSet = Set.create()
        if typeString == 'group' then --Case 1: container is group
            ForGroup(container, function () containerAsSet:addSingle(GetEnumUnit()) end)
        elseif typeString == 'force' then --Case 2: container is force
            ForForce(container, function () containerAsSet:addSingle(GetEnumPlayer()) end)
        elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is Set
            ---@diagnostic disable-next-line: cast-local-type
            containerAsSet = container
        elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
            for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
                containerAsSet:addSingle(element)
            end
        elseif(typeString == 'table') then --Case 5: container is a Table. We then assume, it's an array.
            for _, element in ipairs(container) do
                containerAsSet:addSingle(element)
            end
        else --Case 6: invalid input.
            printError("retainAll is only compatible with a Set, SyncedTable, array, force or group")
        end

        -- do intersection
        for element in self:elements() do
            if not containerAsSet:contains(element) then self:removeSingle(element) end
        end
        return self
    end

    ---Removes all elements from the set that are present in another Set/SyncedTable/array/Force/Group. For SyncedTables and arrays, elements means values, not keys.
    ---If specifying an array, it must form a sequence. Otherwise, values beyond the first nil-key will be ignored.
    ---@param container Set | SyncedTable | any[] | force | group
    ---@return Set self
    function Set:removeAll(container)
        if DEBUG_MODE then checkColonNotation("Set:removeAll(container)", self) end
        local typeString = wc3Type(container)
        if typeString == 'group' then --Case 1: container is a group
            ForGroup(container, function () self:removeSingle(GetEnumUnit()) end)
        elseif typeString == 'force' then --Case 2: container is a force
            ForForce(container, function () self:removeSingle(GetEnumPlayer()) end)
        elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is a Set
            for element in container:elements() do
                self:removeSingle(element)
            end
        elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
            for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
                self:removeSingle(element)
            end
        elseif(type(container) == 'table') then --Case 5: container is a table. We then assume, it's a sequence.
            for _, element in ipairs(container) do
                self:removeSingle(element)
            end
        else --Case 6: invalid input.
            printError("removeAll is only compatible with a Set, SyncedTable, array, force or group")
        end
        return self
    end

    ---Adds all Elements of the given Container to the Set.
    ---Specifying an array or SyncedTable will add all values of that table to the set.
    ---If you specify an array, it must be a sequence. Otherwise, all values beyond the first nil key will not be added.
    ---@param container Set | any[] | SyncedTable | force | group
    ---@return Set self
    function Set:addAll(container)
        if DEBUG_MODE then checkColonNotation("Set:addAll(container)", self) end
        local typeString = wc3Type(container)
        if typeString == 'group' then --Case 1: container is Group
            ForGroup(container, function () self:addSingle(GetEnumUnit()) end)
        elseif typeString == 'force' then --Case 2: container is Force
            ForForce(container, function () self:addSingle(GetEnumPlayer()) end)
        elseif (getmetatable(container) == getmetatable(self)) then --Case 3: container is Set
            for element in container:elements() do
                self:addSingle(element)
            end
        elseif SyncedTable and SyncedTable.isSyncedTable(container) then --Case 4: container is SyncedTable
            for _, element in pairs(container) do --pairs-function is multiplayer synced for SyncedTables.
                self:addSingle(element)
            end
        elseif typeString == 'table' then --Case 5: container is table (and we then assume it's an array)
            for _, element in ipairs(container) do
                self:addSingle(element)
            end
        else --Case 6: invalid input.
            printError("addAll is only compatible with a Set, SyncedTable, array, force or group")
        end
        return self
    end

    ---@diagnostic enable: param-type-mismatch

    ---Adds all keys of a given table as elements to the set. This method is only multiplayer-compatible, if you use a SyncedTable as input OR if you are have overwritten the pairs function to make it multiplayer-synchronous. Otherwise it might lead to desyncs.
    ---@param whichTable SyncedTable Adds all keys of given table to the set
    ---@return Set self
    function Set:addAllKeys(whichTable)
        if DEBUG_MODE then checkColonNotation("Set:addAllKeys(container)", self) end
        if(type(whichTable) == 'table') then
            for key, _ in pairs(whichTable) do --pairs-function is multiplayer synced for SyncedTables.
                self:add(key)
            end
        else
            printError("AddAllKeys only compatible with SyncedTables")
        end
        return self
    end

    ---returns an iterator for a standard for loop
    ---usage: for element in set:elements() do ... end
    ---You can both remove and add elements during the loop. Added elements will also be contained in the loop.
    ---@return function iterator
    function Set:elements()
        if DEBUG_MODE then checkColonNotation("Set:elements()", self) end
        local i = 0
        local lastKey
        return function()
                if lastKey == self.orderedKeys[i] then
                    i = i+1 --only increase i, if the last key in loop is still in place. If not, it means that the element has been removed and we need to stay at i.
                end
                lastKey = self.orderedKeys[i]
                return lastKey
            end
    end

    ---returns the number of elements in this set.
    ---@return integer
    function Set:size()
        if DEBUG_MODE then checkColonNotation("Set:size()", self) end
        return self.n
    end

    ---returns true, when the set is empty and false otherwise
    ---@return boolean
    function Set:isEmpty()
        if DEBUG_MODE then checkColonNotation("Set:isEmpty()", self) end
        return self:size() == 0
    end

    ---Returns a random element from the Set.
    function Set:random()
        if DEBUG_MODE then checkColonNotation("Set:random()", self) end
        return self.orderedKeys[math.random(self.n)]
    end

    ---removes all Elements from the set
    ---@return Set self
    function Set:clear()
        if DEBUG_MODE then checkColonNotation("Set:clear()", self) end
        self.data = {}
        self.orderedKeys = {}
        self.n = 0
        return self
    end

    ---Returns an array with exactly the elements of the Set. Only do this, when another function input needs an array, because why should you use a Set, when you convert it to an array anyway?
    ---@return any[] array
    function Set:toArray()
        if DEBUG_MODE then checkColonNotation("Set:toArray()", self) end
        local i,result = 1,{}
        for element in self:elements() do
            result[i] = element
            i = i+1
        end
        return result
    end

    ---Returns a comma separated list of all elements of <Set>, engulfed in {}-brackets.
    ---@return string
    function Set:toString()
        if DEBUG_MODE then checkColonNotation("Set:toString()", self) end
        local elementsToString = {}
        for i = 1, self.n do
            elementsToString[i] = tostring(self.orderedKeys[i]) --must be translated to strings, else table.concat wouldn't work.
        end
        return '{' .. table.concat(elementsToString, ', ', 1, self.n) .. '}'
    end

    ---prints all elements of the Set on Screen (space separated)
    function Set:print()
        if DEBUG_MODE then checkColonNotation("Set:print()", self) end
        print(self:toString())
    end

    ---Returns true, if this Set has at least one common element with another Set, i.e. the intersection is not empty.
    ---@param otherSet Set
    ---@return boolean haveCommonElement
    function Set:intersects(otherSet)
        if DEBUG_MODE then checkColonNotation("Set:intersects(otherSet)", self) end
        for element in self:elements() do
            if otherSet.data[element] then
                return true
            end
        end
        return false
    end

    ---Returns a copy of an existing Set.
    ---@return Set copy
    function Set:copy()
        if DEBUG_MODE then checkColonNotation("Set:copy()", self) end
        return Set.create():addAll(self)
    end

    ---Returns a new Set, which is the union of all specified parameters.
    ---You can specify any number of arguments of types Set, SyncedTable, array, force and group.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param ... Set | SyncedTable | any[] | force | group
    ---@return Set union
    function Set.union(...)
        if DEBUG_MODE then checkDotNotation("Set.union(...)", ...) end
        local resultSet = Set.create()
        for i = 1, select('#', ...) do
            resultSet:addAll(select(i,...))
        end
        return resultSet
    end

    ---Returns a new Set, which is the intersection of all specified parameters.
    ---You can specify any number of arguments of types Set, SyncedTable, array, force and group.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param ... Set | SyncedTable | any[] | force | group
    ---@return Set intersection
    function Set.intersection(...)
        if DEBUG_MODE then checkDotNotation("Set.intersection(...)", ...) end
        local n = select('#',...)
        local resultSet = Set.create()
        if n > 0 then resultSet:addAll(...) end --actually only adds the first container (addAll only supports one param)
        for i = 2, n do
            resultSet:retainAll(select(i,...))
        end
        return resultSet
    end

    ---Returns a new Set, which equals setA exluding the elements of setB.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param containerA Set | SyncedTable | any[] | force | group
    ---@param containerB Set | SyncedTable | any[] | force | group
    ---@return Set setDifference
    function Set.except(containerA, containerB)
        if DEBUG_MODE then checkDotNotation("Set.except(A,B)", containerA) end
        return Set.create():addAll(containerA):removeAll(containerB)
    end

    ---Returns the Set of all units from the specified unitgroup.
    ---@param unitgroup group
    ---@return Set
    function Set.fromGroup(unitgroup)
        if DEBUG_MODE then checkDotNotation("Set.fromGroup(unitgroup)", unitgroup) end
        local unitSet = Set.create()
        ForGroup(unitgroup, function () unitSet:addSingle(GetEnumUnit()) end)
        return unitSet
    end

    ---Returns the Set of all players from the specified playergroup.
    ---@param playergroup force
    ---@return Set
    function Set.fromForce(playergroup)
        if DEBUG_MODE then checkDotNotation("Set.fromForce(playergroup)", playergroup) end
        local playerSet = Set.create()
        ForForce(playergroup, function () playerSet:addSingle(GetEnumPlayer()) end)
        return playerSet
    end

    ---Returns a new Set with all elements from the specified table (i.e. all values, not keys). The table must be either a SyncedTable or array.
    ---Arrays are required to form a sequence. Otherwise, values beyond the first nil-key are ignored.
    ---@param whichTable SyncedTable | any[]
    ---@return Set
    function Set.fromTable(whichTable)
        if DEBUG_MODE then checkDotNotation("Set.fromTable(whichTable)", whichTable) end
        return Set.create():addAll(whichTable)
    end

    --------------------------
    -- | SetUtils Library | --
    --------------------------

    --Mimick existing pick natives, but create Sets instead of groups, forces, destructable
    SetUtils = {}

    --Preparation: Use a Set to save all units, overwrite existing CreateUnit functions.
    local allUnits = Set.create() --Set of all units currently present on the map. This set is used as a base for all pick-functions in SetUtils.
    local autoUnitRemoveSubscriptions = Set.create(allUnits) --Set of all Sets that are subscribed to automatic unit removal. Only in use, when a custom defend ability was provided.

    local removeTimer ---@type timer Periodic Timer to check for invalid unit references in allUnits.
    local getUnitTypeId, unitAddAbility, unitMakeAbilityPermanent = GetUnitTypeId, UnitAddAbility, UnitMakeAbilityPermanent --localize natives for quicker access

    ---Adds new units to the allUnits Set and adds the custom defend ability, if provided by the user.
    ---@param u unit
    local function registerNewUnit(u)
        allUnits:addSingle(u)
        if CUSTOM_DEFEND_ABICODE then
            unitAddAbility(u, CUSTOM_DEFEND_ABICODE)
            unitMakeAbilityPermanent(u, true, CUSTOM_DEFEND_ABICODE)
        end
    end

    ---Check-function for invalid unit references, i.e. units that have been removed from the game (via RemoveUnit or complete decay).
    ---To be used by the periodic cleanup option.
    local function checkForDeadReferences()
        SetUtils.clearInvalidUnitRefs(allUnits)
    end

    ---Removes the specified unit from all sets subscribed to auto-removal of invalid unit references.
    ---Called upon any unit leaving the game.
    ---@param unitToRemove unit
    local function removeUnitFromSubscribedSets(unitToRemove)
        for set in autoUnitRemoveSubscriptions:elements() do
            set:removeSingle(unitToRemove)
        end
    end

    --------Overwrite Natives---------

    --The native pick functions are able to pick new units immediately after creation.
    --That holds for both creating and picking a new unit in immediate order within a function as well as for event responses like enters map, gets constructed, gets summoned.
    --If we just registered new units to the allUnits-Set as an enters-map-event-response, using SetUtils-pick-functions on the same event would not be guaranteed to pick the new unit.
    --So instead of using enters-map-event, we directly register the unit in the CreateUnit-native (by overwriting the native) and all similar natives.
    --We also register trained, summoned and constructed units upon the respective events instead of enters-map.
    --Tests show that this method enables the SetUtils-pick-functions to properly mimick the original behaviour, i.e. pick new units upon the enter-map-event.
    --If you use a pick function upon one of the "gets trained", "gets summoned" and "gets constructed" events, the new unit is still NOT guaranteed to be picked.

    local oldCreateUnit = CreateUnit

    ---@param owningPlayer player
    ---@param unitid integer
    ---@param x number
    ---@param y number
    ---@param face number
    ---@return unit
    function CreateUnit(owningPlayer, unitid, x, y, face)
        local u = oldCreateUnit(owningPlayer, unitid, x, y, face)
        registerNewUnit(u)
        return u
    end

    local oldCreateUnitByName = CreateUnitByName

    ---@param owningPlayer player
    ---@param unitname string
    ---@param x number
    ---@param y number
    ---@param face number
    ---@return unit
    function CreateUnitByName(owningPlayer, unitname, x, y, face)
        local u = oldCreateUnitByName(owningPlayer, unitname, x, y, face)
        registerNewUnit(u)
        return u
    end

    local oldCreateUnitAtLoc = CreateUnitAtLoc

    ---@param owningPlayer player
    ---@param unitid integer
    ---@param whichLocation location
    ---@param face number
    ---@return unit
    function CreateUnitAtLoc(owningPlayer, unitid, whichLocation, face)
        local u = oldCreateUnitAtLoc(owningPlayer, unitid, whichLocation, face)
        registerNewUnit(u)
        return u
    end

    local oldCreateUnitAtLocByName = CreateUnitAtLocByName

    ---@param owningPlayer player
    ---@param unitname string
    ---@param whichLocation location
    ---@param face number
    ---@return unit
    function CreateUnitAtLocByName(owningPlayer, unitname, whichLocation, face)
        local u = oldCreateUnitAtLocByName(owningPlayer, unitname, whichLocation, face)
        registerNewUnit(u)
        return u
    end

    local oldBlzCreateUnitWithSkin = BlzCreateUnitWithSkin

    ---@param owningPlayer player
    ---@param unitid integer
    ---@param x number
    ---@param y number
    ---@param face number
    ---@param skinId integer
    ---@return unit
    function BlzCreateUnitWithSkin(owningPlayer, unitid, x, y, face, skinId)
        local u = oldBlzCreateUnitWithSkin(owningPlayer, unitid, x, y, face, skinId)
        registerNewUnit(u)
        return u
    end

    local oldRemoveUnit = RemoveUnit

    ---@param whichUnit unit
    function RemoveUnit(whichUnit)
        allUnits:removeSingle(whichUnit)
        oldRemoveUnit(whichUnit)
    end

    ---Only used for the player, item and destructable enums.
    ---Returns Condition(func), if the input a is a function. Returns the input otherwise.
    ---@param func? function | boolexpr
    ---@return boolexpr condition, boolean converted_yn
    local function conditionIfNecessary(func)
        local converted_yn = type(func) == 'function' ---@diagnostic disable-next-line: return-type-mismatch
        return (converted_yn and Condition(func)) or func, converted_yn --real boolexpr have type userdata
    end

    --------UnitGroups---------

    ---Returns the AND-concatenation of all functions
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return function
    local function matchAllConditions(...)
        local tableOfConditions = table.pack(...)
        return function(...)
            for i = 1, tableOfConditions.n do
                if tableOfConditions[i] and not tableOfConditions[i](...) then
                    return false
                end
            end
            return true
        end
    end

    local oldGetFilterUnit = GetFilterUnit

    ---Returns the Set of all living units that match all specified conditions.
    ---All params must be functions that takes either a unit or nothing and return a boolean (true, if the unit is supposed to be in the result set). If using a function taking nothing, use GetFilterUnit() to access the unit being checked.
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return Set
    function SetUtils.getUnitsMatching(...)
        local lastUsedLoopUnit
        GetFilterUnit = function() return lastUsedLoopUnit end --overwrites GetFilterUnit for the duration of the pick loop to allow usage of GetFilterUnit() in conditionfuncs.
        local conditionfunc = matchAllConditions(...)
        local result = Set.create()
        for loopUnit in allUnits:elements() do
            lastUsedLoopUnit = loopUnit
            if getUnitTypeId(loopUnit) == 0 then --Remove all dead unit references from the data structure. This condition provides extra safety, even if the undefend method to remove dead references is used.
                allUnits:removeSingle(loopUnit)
            elseif conditionfunc(loopUnit) then
                result:addSingle(loopUnit)
            end
        end
        if CLEAN_INTERVAL then --cleanup was conducted during the pick loop above, so we can delay the next cleanup.
            TimerStart(removeTimer, CLEAN_INTERVAL, true, checkForDeadReferences)
        end
        GetFilterUnit = oldGetFilterUnit
        return result
    end

    local Aloc = FourCC('Aloc')
    local function hasUnitNoLocust(u)
        return GetUnitAbilityLevel(u,Aloc) == 0
    end

    ---Returns the Set of all units in a specified rect that match all specified conditions. You can specify to include units with locust (the Wc3 native would not do that).
    ---All ... params must be functions that takes either a unit or nothing and return a boolean (true, if the unit is supposed to be in the result set). If using a function taking nothing, use GetFilterUnit() to access the unit being checked.
    ---@param whichRect rect
    ---@param includeLocust? boolean defines, if units having locust should be picked or not. default: false
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return Set
    function SetUtils.getUnitsInRectMatching(whichRect, includeLocust, ...)
        return SetUtils.getUnitsMatching(function(u) return RectContainsUnit(whichRect,u) end, (not includeLocust and hasUnitNoLocust) or nil, ...)
    end

    ---Returns the Set of all units in the specified rect. You can specify to include units with locust (the Wc3 native would not do that).
    ---@param whichRect rect
    ---@param includeLocust? boolean defines, if units having locust should be picked or not. default: false
    ---@return Set
    function SetUtils.getUnitsInRect(whichRect, includeLocust)
        return SetUtils.getUnitsInRectMatching(whichRect, includeLocust)
    end

    ---Returns the Set of all units within a specified radius of the specified coordinates matching all specified conditions. You can specify to include units with locust (the Wc3 native would not do that).
    ---All ... params must be functions that takes either a unit or nothing and return a boolean (true, if the unit is supposed to be in the result set). If using a function taking nothing, use GetFilterUnit() to access the unit being checked.
    ---@param x number
    ---@param y number
    ---@param radius number
    ---@param includeLocust? boolean default:false
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return Set
    function SetUtils.getUnitsInRangeMatching(x, y, radius, includeLocust, ...)
        return SetUtils.getUnitsMatching(function(u) return IsUnitInRangeXY(u, x, y, radius) end, (not includeLocust and hasUnitNoLocust) or nil, ...)
    end

    ---Returns the Set of all units within a specified radius of the specified coordinates. You can specify to include units with locust (the Wc3 native would not do that).
    ---@param x number
    ---@param y number
    ---@param radius number
    ---@param includeLocust? boolean default:false
    ---@return Set
    function SetUtils.getUnitsInRange(x, y, radius, includeLocust)
        return SetUtils.getUnitsInRangeMatching(x, y, radius, includeLocust)
    end

    ---Returns the Set of all units owned by the specified player and matching all specified conditions. You can specify to include units with locust (the Wc3 native would do that in contrast to all other pick functions).
    ---All ... params must be functions that takes either a unit or nothing and return a boolean (true, if the unit is supposed to be in the result set). If using a function taking nothing, use GetFilterUnit() to access the unit being checked.
    ---@param whichPlayer player
    ---@param includeLocust? boolean default:false
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return Set
    function SetUtils.getUnitsOfPlayerMatching(whichPlayer, includeLocust, ...)
        return SetUtils.getUnitsMatching(function(u) return GetOwningPlayer(u) == whichPlayer end, (not includeLocust and hasUnitNoLocust) or nil, ...)
    end

    ---Returns the Set of all units owned by the specified player. You can specify to include units with locust (the Wc3 native would do that in contrast to all other pick functions).
    ---@param whichPlayer player
    ---@param includeLocust? boolean default:false
    ---@return Set
    function SetUtils.getUnitsOfPlayer(whichPlayer, includeLocust)
        return SetUtils.getUnitsOfPlayerMatching(whichPlayer, includeLocust)
    end

    ---Returns the Set of all units having the specified unitType and matching all specified conditions. You can specify to include units with locust (the Wc3 native would not do that).
    ---All ... params must be functions that takes either a unit or nothing and return a boolean (true, if the unit is supposed to be in the result set). If using a function taking nothing, use GetFilterUnit() to access the unit being checked.
    ---@param typeId integer
    ---@param includeLocust? boolean default:false
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return Set
    function SetUtils.getUnitsOfTypeIdMatching(typeId, includeLocust, ...)
        return SetUtils.getUnitsMatching(function(u) return GetUnitTypeId(u) == typeId end, (not includeLocust and hasUnitNoLocust) or nil, ...)
    end

    ---Returns the Set of all units owned by the specified player and having the specified unitType. You can specify to include units with locust.
    ---@param whichPlayer player
    ---@param typeId integer
    ---@param includeLocust? boolean default:false
    ---@return Set
    function SetUtils.getUnitsOfPlayerAndTypeId(whichPlayer, typeId, includeLocust)
        return SetUtils.getUnitsMatching(function(u) return GetUnitTypeId(u) == typeId and GetOwningPlayer(u) == whichPlayer end, (not includeLocust and hasUnitNoLocust) or nil)
    end

    ---Returns the Set of all units having the specified unitType. You can specify to include units with locust.
    ---@param typeId integer
    ---@param includeLocust? boolean default:false
    ---@return Set
    function SetUtils.getUnitsOfTypeId(typeId, includeLocust)
        return SetUtils.getUnitsOfTypeIdMatching(typeId, includeLocust)
    end

    ---Returns the Set of all units being currently selected by a player and matching all specified conditions.
    ---All ... params must be functions that takes either a unit or nothing and return a boolean (true, if the unit is supposed to be in the result set). If using a function taking nothing, use GetFilterUnit() to access the unit being checked.
    ---@param whichPlayer player
    ---@param ... fun(unitToCheck:unit):boolean
    ---@return Set
    function SetUtils.getUnitsSelected(whichPlayer, ...)
        SyncSelections() --important to prevent desyncs, as selections are saved locally.
        return SetUtils.getUnitsMatching(function(u) return IsUnitSelected(u,whichPlayer) end, ...)
    end

    SetUtils.getUnitsSelectedMatching = SetUtils.getUnitsSelected

    --------PlayerGroups---------

    ---Returns the Set of all players.
    ---Only contains players that were present during game start, including computer players.
    ---@return Set
    function SetUtils.getPlayersAll()
        return Set.fromForce(GetPlayersAll()) --global wc3 var, doesn't produce memory leaks
    end

    ---Returns the Set of all active players (including computer players) matching the specified condition.
    ---Only contains players that were present during game start, including computer players.
    ---Use GetFilterPlayer() to refer to the player being checked in the condition.
    ---@param condition function | boolexpr
    ---@return Set
    function SetUtils.getPlayersMatching(condition)
        local playergroup = CreateForce()
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        ForceEnumPlayers(playergroup, convertedCondition)
        local playerSet = Set.fromForce(playergroup)
        DestroyForce(playergroup)
        if converted_yn then
            DestroyBoolExpr(convertedCondition)
        end
        return playerSet
    end

    --------DestructableGroups---------

    ---Returns the Set of all destructables in the specified rect matching the specified condition.
    ---Use GetFilterDestructable() to refer to the destructable being checked in the condition.
    ---@param whichRect rect
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getDestructablesInRectMatching(whichRect, condition)
        local destructableSet = Set.create()
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        EnumDestructablesInRect(whichRect, convertedCondition, function() destructableSet:add(GetEnumDestructable()) end)
        if converted_yn then
            DestroyBoolExpr(convertedCondition)
        end
        return destructableSet
    end

    ---Returns the Set of all destructables in the specified rect.
    ---@param whichRect rect
    ---@return Set
    function SetUtils.getDestructablesInRect(whichRect)
        return SetUtils.getDestructablesInRectMatching(whichRect)
    end

    --------ItemGroups---------

    ---Returns the Set of all items in the specified rect matching the specified condition.
    ---Use GetFilterItem() to refer to the item being checked in the condition.
    ---@param whichRect rect
    ---@param condition? function | boolexpr
    ---@return Set
    function SetUtils.getItemsInRectMatching(whichRect, condition)
        local itemSet = Set.create()
        local convertedCondition, converted_yn = conditionIfNecessary(condition)
        EnumItemsInRect(whichRect, convertedCondition, function() itemSet:add(GetEnumItem()) end)
        if converted_yn then
            DestroyBoolExpr(convertedCondition)
        end
        return itemSet
    end

    ---Returns the Set of all items in the specified rect.
    ---@param whichRect rect
    ---@return Set
    function SetUtils.getItemsInRect(whichRect)
       return SetUtils.getItemsInRectMatching(whichRect)
    end

    --------Utility---------

    ---Removes all invalid unit references from the specified Set, i.e. units that have already been removed from the game.
    ---If your Set contains non-unit elements, you must set the second parameter to true to avoid crashes.
    ---@param whichSet Set the Set that might contain references to removed units
    ---@param checkIfUnit? boolean default: false. Set to true to avoid crashes, if the Set contains non-unit elements.
    function SetUtils.clearInvalidUnitRefs(whichSet, checkIfUnit)
        for element in whichSet:elements() do
            if not checkIfUnit or wc3Type(element) == 'unit' then
                if getUnitTypeId(element) == 0 then
                    whichSet:removeSingle(element)
                end
            end
        end
    end

    ---Subscribes the specified set to automatic removal of invalid unit references, i.e. for the rest of the game, units that are removed from the game will also be removed from the specified set.
    ---Set the second parameter to false (default true) to unsubscribe the specified Set from the automatic unit cleaning.
    ---As long as a Set is subscribed, it will not be garbage collected.
    ---This function requires CUSTOM_DEFEND_ABICODE to be set.
    ---@param whichSet Set
    ---@param subscribe_yn? boolean default: true. true to subscribe. false to unsubscribe.
    function SetUtils.subscribeSetToAutoUnitRemoval(whichSet, subscribe_yn)
        if CUSTOM_DEFEND_ABICODE then
            if subscribe_yn or subscribe_yn == nil then
                SetUtils.clearInvalidUnitRefs(whichSet, true)
                autoUnitRemoveSubscriptions:addSingle(whichSet)
            else
                autoUnitRemoveSubscriptions:removeSingle(whichSet)
            end
        else
            printError("You can't use SetUtils.subscribeToAutoUnitRemoval, until you have provided a custom defend ability.")
        end
    end

    local hasUnitBeenRemovedCondition ---@type conditionfunc initialized in SetUtils.createTriggers() below.

    ---Adds the event "unit is removed from the game" to the specified trigger.
    ---This event is not compatible with other events, so don't use it on triggers with multiple events (other events will simply be invalidated).
    ---Requires CUSTOM_DEFEND_ABICODE to be set.
    ---@param whichTrigger trigger
    function SetUtils.triggerRegisterAnyUnitRemoveEvent(whichTrigger)
        if CUSTOM_DEFEND_ABICODE then
            TriggerRegisterAnyUnitEventBJ(whichTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
            TriggerAddCondition(whichTrigger, hasUnitBeenRemovedCondition)
        else
            printError("You can't use SetUtils.triggerRegisterAnyUnitRemoveEvent, until you have provided a custom defend ability.")
        end
    end

    --------Triggers to maintain the unit getters---------

    function SetUtils.createTriggers()
        --We use separate Train, Summon and Construction events instead of one enters-map-event to ensure that new units can be picked immediately as enters-map-event response.
        local addTrigger1 =  CreateTrigger()
        TriggerRegisterAnyUnitEventBJ( addTrigger1, EVENT_PLAYER_UNIT_TRAIN_FINISH ) --Units entering the map by being trained
        TriggerAddAction(addTrigger1, function() registerNewUnit(GetTrainedUnit()) end)
        local addTrigger2 = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ( addTrigger2, EVENT_PLAYER_UNIT_SUMMON ) --Units entering the map by being summoned
        TriggerAddAction(addTrigger2, function() registerNewUnit(GetSummonedUnit()) end)
        local addTrigger3 = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ( addTrigger3, EVENT_PLAYER_UNIT_CONSTRUCT_START ) --Units entering the map by being constructed
        TriggerAddAction(addTrigger3, function() registerNewUnit(GetConstructingStructure()) end)

        --Init unit reference cleanup methods
        --Method 1: Custom defend ability, catch undefend order.
        if CUSTOM_DEFEND_ABICODE then
            for i = 0, GetBJMaxPlayers() - 1 do
                SetPlayerAbilityAvailable(Player(i), CUSTOM_DEFEND_ABICODE, false)
            end

            --Initialize upvalue. We don't set hasUnitBeenRemovedCondition earlier to prevent using Wc3 natives in the Lua root.
            hasUnitBeenRemovedCondition = Condition(function() return (GetIssuedOrderId() == 852056) and GetUnitAbilityLevel(GetTriggerUnit(), CUSTOM_DEFEND_ABICODE) == 0 end) --undefend order. This one is issued upon units leaving the game, but also under other circumstances. Ability-Level == 0 proves the removed from the game event.

            local removeTrigger = CreateTrigger()
            SetUtils.triggerRegisterAnyUnitRemoveEvent(removeTrigger)
            TriggerAddAction(removeTrigger, function() removeUnitFromSubscribedSets(GetTriggerUnit()) end)
        end
        --Method 2: Periodically remove invalid references.
        if CLEAN_INTERVAL then
            removeTimer = CreateTimer()
            TimerStart(removeTimer, CLEAN_INTERVAL, true, checkForDeadReferences)
        end
    end

    ---@diagnostic disable-next-line: undefined-global
    if OnInit and OnInit.trig then OnInit.trig(SetUtils.createTriggers) end --use GlobalInit library, if available.
end
if Debug and Debug.endFile then Debug.endFile() end

  • 10.4.2021: v1.1 - added missing checkDotNotation in SetUtils version1. Fixed <set>:size() in both versions.
  • 30.5.2021: v1.2 - added multiplayer-stable iteration order. Using Set-library doesn't lead to desyncs anymore.
  • 21.6.2021: v1.2.1 - fixed two bugs with <set>:toString and <set>:remove that were introduced with v1.2
  • 23.1.2022: v1.3- big thanks to @Wrda for PM'ing me his suggestions and brainstorming about solutions.
    • Added an option to automatically (or manually) exclude units from Sets that have been removed from the game.
      See new functions SetUtils.clearInvalidUnitRefs and SetUtils.subscribeSetToAutoUnitRemoval.
    • Added DEBUG_MODE to enable users to deactivate dot- and colon-notation checks before map release.
    • Added optional dependencies for Global Initialization and SyncedTable.
    • Vastly improved documentation
    • Variant 2 of the code no longer excludes dead units from the pick functions and now offers several methods to ensure that units having left the game are never picked.
    • Using tostring() on Sets will now yield "Set: <...>" instead of "table: <...>".
    • Fixed a bug, where the methods addAll, removeAll and retainAll would crash due to a missing local declaration of the wc3Type function
  • 03.10.2022: v1.3.1 - Adapted to new function names of the Global Initialization library
  • 27.11.2022: v1.3.2
    • Fixed a potential (although rare) source of memory leak in variant 1 of the code. Thanks @WaterKnight for ponting me towards it.
    • Switched vararg-functions (Set:add, Set:remove, Set:union, Set:intersection) from table.pack to select-based enumeration to improve performance and avoid the need of garbage collection.
    • Again adapted to new function names of the Global Init library.
    • Added Debug.beginFile-support for the upcoming version of DebugUtils.
  • 16.04.2023: v1.3.3
    • Fixed a bug introduced in version 1.3.2 with variant 2 of SetUtils, where picking units with locust wouldn't work as intended.
 
Last edited:

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
I agree this is useful. Even though Lua tables are designed to be VERY flexible, that's also the same reason I find myself writing repetitive extra code logics or wrappers on top of it for different usecases. So this is a great time saver.
As a related case, just yesterday I was searching luarocks for a generic and robust linkedlist library and haven't found any, so I need to write my own.
 
Level 6
Joined
Dec 29, 2019
Messages
82
I am just wondring, is it really necessary to use boolexpir via Condition() at all ? Anonymous functions can leak like hell... especially when they are passed into BoolExpr, maybe safer approach would be to get rid of Condition() boolexpr and setting function type params to nil in the end of execution ? Idk that would need to be tested ... if you've tested it and it didn't leak then i guess its all fine.
 
Level 20
Joined
Jul 10, 2009
Messages
477
I am just wondring, is it really necessary to use boolexpir via Condition() at all ? Anonymous functions can leak like hell... especially when they are passed into BoolExpr, maybe safer approach would be to get rid of Condition() boolexpr and setting function type params to nil in the end of execution ? Idk that would need to be tested ... if you've tested it and it didn't leak then i guess its all fine.
Warcraft natives that take a conditionfunc unfortunately don't accept regular lua functions, e.g. GroupEnumUnitsInRect(g,r,function() return true end) would just yield an error.
So in version 1 of above resource, you can not skip on the Condition() wraps, so you will produce memory leaks according to ScrewTheTree's post upon using conditions. Which isn't a problem as long as you don't need any condition, like GroupEnumUnitsInRect(g,r,nil) or in terms of the SetUtils library, SetUtils.getUnitsInRect(r).
If you do need conditions, you can still get around most leaks easily: One option would be, if you are needing the same condition over and over - just save your conditionfunc in a variable and use the same thing every time -> no leaking.
Another option is to always skip the condition in the SetUtils function and remove all unwanted elements from the resulting Set afterwards:
Lua:
exampleSet = SetUtils.getUnitsInRect(r)
for u in exampleSet:elements() do
    if not condition(u) then
        exampleSet:remove(u)
    end
end
(or if you want to only loop through the result Set once, just check for the condition during that loop).
Note that the SetUtils conditional pickers will only apply Condition(), if you haven't already, i.e. if you are inputting a function (lua type 'function') instead of a boolexpr (lua type 'userdata'). So you can have complete control about the leaks, if you want, by just inputting boolexpr.

Version 2 doesn't use boolexpr for the unit pickers at all and thus doesn't produce any memory leaks, so the answer to your question is No, its not necessary :p
The code still contains some Condition() stuff, which is only used for mimicking the player, item and destructable pickers. Those have simply not been changed from version 1, i.e. they still require a boolexpr to work and my code will apply Condition(), if you haven't done so. I just simply haven't seen a use case for converting them to version 2 logic, as players can be easily checked manually anyway and I didn't want to produce constant overhead for maintaining a global destructable and item array, as most mappers wouldn't need it, myself included.
If someone needs it, I can add it, though (well, I'd have to find out, how to catch all destructables and items entering the map :D).

Fortunately, the whole leak question becomes irrelevant as long as you don't use conditions. SetUtils.getUnitsInRect(r) is always fine, no matter which version you are using (apart from natives known bugs in version 1, we have talked about that).
 
Level 20
Joined
Jul 10, 2009
Messages
477
Update v1.2:
I rewrote a big part of the Set library to make it multiplayer-safe. Iterating over Sets now has the same guaranteed iteration order for all players and thus will no longer desync.

Update v1.2.1:
Fixed two bugs introduced with v1.2:
  • Set iteration on sets that are completely empty after <set>:remove(lastElement) will no longer include lastElement.
  • <set>:toString() will now also work, when the Set contains non-string elements.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
Update v1.3:
  • Added an option to automatically (or manually) exclude units from Sets that have been removed from the game.
    See new functions SetUtils.clearInvalidUnitRefs and SetUtils.subscribeSetToAutoUnitRemoval.
  • Added DEBUG_MODE to enable users to deactivate dot- and colon-notation checks before map release.
  • Added optional dependencies for Global Initialization and SyncedTable.
  • Vastly improved documentation
  • Variant 2 of the code no longer excludes dead units from the pick functions and now offers several methods to ensure that units having left the game are never picked.
  • Using tostring() on Sets will now yield "Set: <...>" instead of "table: <...>".
  • Fixed a bug, where the methods addAll, removeAll and retainAll would crash due to a missing local declaration of the wc3Type function
Big thanks to @Wrda for PM'ing me his suggestions and brainstorming about solutions.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Overall, looks good. I was surprised to see you using UnitIndexer-type methods in order to accomplish some of the ghost-removal methods. I think it's a bit superfluous to incorporate all that functionality in this resource yourself instead of adding a dependency on one that already exists (or one that you build that could be outsourced).

Unless I am mistaken, you need to use the boolexpr of EVENT_PLAYER_UNIT_ISSUED_ORDER rather than a triggercondition, otherwise it could fail in circumstances such as a paused unit being removed from a transport. Someone can correct me if I'm wrong, because this knowledge is based on how the game worked 10-12 years ago.
 
Level 20
Joined
Jul 10, 2009
Messages
477
Thank you for reviewing this resource.

I was surprised to see you using UnitIndexer-type methods in order to accomplish some of the ghost-removal methods. I think it's a bit superfluous to incorporate all that functionality in this resource yourself instead of adding a dependency on one that already exists (or one that you build that could be outsourced).
From my perspective, the only non-Set-related functionality I'm using is the custom Unit-removed-from-game event.
Would you really add a dependency just to outsource that single thing?

I'm not even sure, what the purpose of that outsourced resource would be. Unit-indexers are not necessary to use in Lua and I'm not sure what other library that custom event could be incorporated in.

Honestly, I'd currently prefer to leave this as is. But I'm happy to hear you elaborating further.

Unless I am mistaken, you need to use the boolexpr of EVENT_PLAYER_UNIT_ISSUED_ORDER rather than a triggercondition, otherwise it could fail in circumstances such as a unit being removed from a transport.
I've just tested the case you described. Unit being removed from a transport did not trigger my custom "Unit-removed-from-game"-event. Reason is the part of the trigger condition that asks for GetUnitAbilityLevel(GetTriggerUnit(), CUSTOM_DEFEND_ABICODE) == 0. From what I know, this part is true if and only if the triggering unit got removed from the game.
But please let me know, when you find any case going wrong.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Thank you for reviewing this resource.


From my perspective, the only non-Set-related functionality I'm using is the custom Unit-removed-from-game event.
Would you really add a dependency just to outsource that single thing?

I'm not even sure, what the purpose of that outsourced resource would be. Unit-indexers are not necessary to use in Lua and I'm not sure what other library that custom event could be incorporated in.

Honestly, I'd currently prefer to leave this as is. But I'm happy to hear you elaborating further.


I've just tested the case you described. Unit being removed from a transport did not trigger my custom "Unit-removed-from-game"-event. Reason is the part of the trigger condition that asks for GetUnitAbilityLevel(GetTriggerUnit(), CUSTOM_DEFEND_ABICODE) == 0. From what I know, this part is true if and only if the triggering unit got removed from the game.
But please let me know, when you find any case going wrong.
The specific circumstance that I've referred to is actually when a paused unit (via PauseUnit) is removed from the entire game (via RemoveUnit) from within a transport. It's extremely rare, of course.

I recommend, just for sanity's sake, that your users just use the classic "GroupRefresh" technique:

Lua:
---@param g group
function GroupRefresh(g)
    local f = true
    ForGroup(g, function()
        if f then
            GroupClear(g)
            f = nil
        end
        GroupAddUnit(g, GetEnumUnit())
    end)
    if f then
        GroupClear(g)
    end
end

Nevertheless, since all this is totally optional and that it stands alone from resources that it doesn't need to depend on but has support for, I will approve this.
 
Level 20
Joined
Jul 10, 2009
Messages
477
Update v1.3.1
Adapted to new function names of the Global Initialization library (capital O).
Also corrected a type with the library function (OnTrigInit, not OnTriggerInit). I seem to have changed that name within Global Init in my own setup in the past and forgot about it, oh boy.



@Bribe I just came back reading your last comment. Can you maybe explain again, why users should use your GroupRefresh method? Was it just to prevent issues with removing paused units from within transport?
-> What is your GroupRefresh actually doing?
-> It seems to be a manual method for "cleaning" unitgroups? How is it applicable to Sets? My resource provides SetUtils.clearInvalidUnitRefs for manual ghost removal, isn't that sufficient?
-> Your code looks like you want to call GroupClear twice, at start and end, but the first is only called, if the group is not empty?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Update v1.3.1
Adapted to new function names of the Global Initialization library (capital O).
Also corrected a type with the library function (OnTrigInit, not OnTriggerInit). I seem to have changed that name within Global Init in my own setup in the past and forgot about it, oh boy.

You're not going crazy! I changed the API from onTriggerInit to OnTrigInit. Sorry for the trouble. I really do hate changing API, so I tend to avoid it as much as possible.


@Bribe I just came back reading your last comment. Can you maybe explain again, why users should use your GroupRefresh method? Was it just to prevent issues with removing paused units from within transport?
-> What is your GroupRefresh actually doing?
-> It seems to be a manual method for "cleaning" unitgroups? How is it applicable to Sets? My resource provides SetUtils.clearInvalidUnitRefs for manual ghost removal, isn't that sufficient?
-> Your code looks like you want to call GroupClear twice, at start and end, but the first is only called, if the group is not empty?

So what happens is that ForGroup does not iterate over removed units, but you can call GroupClear inside of the same ForGroup callback and it will keep iterating over the list of active units and re-adding them, with a completely fresh slate.

The next GroupClear is a fallback, in case there are zero active units in the group, but possibly more than zero removed units remaining in the group.

Regardless, manually removing them is better. GroupRefresh was never very popular, but it was quite creative.

Additionally, ForGroup no longer works like this if one uses the new [Lua] - Lua-Infused GUI + Automatic Group, Location, Rect and Force leak prevention . Instead, units will be removed in the same way you remove them here (provided you have UnitEvent in the map and set the configuration accordingly).
 
Level 20
Joined
Jul 10, 2009
Messages
477
Update to v1.3.2
  • Fixed a potential (although rare) source of memory leak in variant 1 of the code. Thanks @WaterKnight for ponting me towards it.
  • Switched vararg-functions (Set:add, Set:remove, Set:union, Set:intersection) from table.pack to select-based enumeration to improve performance and avoid the need of garbage collection.
  • Again adapted to new function names of the Global Init library (Calm down @Bribe :p)
  • Added Debug.beginFile-support for the upcoming version of DebugUtils.
 
If you ever decide to recycle tables here is a stateless elements function that doesn't create a function per loop:

Lua:
    local function setIterator(state)
        local i = state.i
        if state.lastKey == state.orderedKeys[i] then
            i = i + 1
            state.i = i
        end
        state.lastKey = state.orderedKeys[i]
        if not state.lastKey then
            state.orderedKeys = nil
            state.i = nil
            ReleaseTable(state)
        end
        return state.lastKey
    end

    function Set:elements()
        local state = NewTable()
        state.i = 0
        state.orderedKeys = self.orderedKeys
        return setIterator, state
    end
 
Top