Name | Type | is_array | initial_value |
Action_duration | real | No | |
Action_effect | effect | Yes | |
Action_forDuration | force | No | |
Action_group | group | Yes | |
Action_hash | hashtable | No | |
Action_index | integer | No | |
Action_integer | integer | Yes | |
Action_loop | force | Yes | |
Action_point | location | Yes | |
Action_real | real | Yes | |
Action_unit | unit | Yes | |
Action_wait | real | No | |
BasicEvent1 | real | No | |
BasicEvent2 | real | No | |
BasicEvent3 | real | No | |
DetectRemoveAbility | abilcode | No | |
DetectTransformAbility | abilcode | No | |
EffectArray | effect | Yes | |
EventIndex | integer | No | |
GroupInRange_allow_allies | boolean | No | |
GroupInRange_allow_dead | boolean | No | |
GroupInRange_allow_enemies | boolean | No | |
GroupInRange_allow_flying | boolean | No | |
GroupInRange_allow_heroes | boolean | No | |
GroupInRange_allow_living | boolean | No | |
GroupInRange_allow_magicImmune | boolean | No | |
GroupInRange_allow_mechanical | boolean | No | |
GroupInRange_allow_nonHeroes | boolean | No | |
GroupInRange_allow_self | boolean | No | |
GroupInRange_allow_structures | boolean | No | |
GroupInRange_count | integer | No | |
GroupInRange_filter | force | No | |
GroupInRange_max | integer | No | |
GroupInRange_point | location | No | |
GroupInRange_radius | real | No | |
GroupInRange_source | unit | No | |
GroupInRange_units | group | No | |
INFINITE_LOOP | integer | No | |
IntegerArray | integer | Yes | |
MyInteger | integer | No | |
OnSpellCast | real | No | |
OnSpellChannel | real | No | |
OnSpellEffect | real | No | |
OnSpellFinish | real | No | |
OnUnitActive | real | No | |
OnUnitCreation | real | No | |
OnUnitDeath | real | No | |
OnUnitIndexed | real | No | |
OnUnitLoaded | real | No | |
OnUnitPassive | real | No | |
OnUnitReincarnating | real | No | |
OnUnitRemoval | real | No | |
OnUnitRevival | real | No | |
OnUnitTransform | real | No | |
OnUnitUnloaded | real | No | |
Spell__abilcode | string | No | |
Spell__Ability | abilcode | No | |
Spell__Caster | unit | No | |
Spell__CasterOwner | player | No | |
Spell__CastPoint | location | No | |
Spell__Channeling | boolean | No | |
Spell__Completed | boolean | No | |
Spell__Index | integer | No | |
Spell__Level | integer | No | |
Spell__LevelMultiplier | real | No | |
Spell__Target | unit | No | |
Spell__TargetPoint | location | No | |
Spell__wait | real | No | |
Spell__whileChannel | force | No | |
UnitEvent_cargo | group | Yes | |
UnitEvent_getKey | integer | No | |
UnitEvent_index | integer | No | |
UnitEvent_preplaced | boolean | Yes | |
UnitEvent_reincarnating | boolean | Yes | |
UnitEvent_setKey | unit | No | |
UnitEvent_summoner | unit | Yes | |
UnitEvent_transporter | unit | Yes | |
UnitEvent_unit | unit | No | |
UnitEvent_unitType | unitcode | Yes | |
WaitForEvent | real | No | |
WaitIndex | integer | No |
do
local _,skipThisLine1,skipThisLine2,caller
local map = {
w = "war3map",
b = "blizzard.j",
--c = "common.j",
--j = "jdoodle"
}
local function checkCaller()
caller = rawget(_G, "OnGameStart") or pcall --"print" does not work in the Lua root. If Global Initialization is in the map, delay the print message until the game has loaded.
end
local function traceError(errorMsg)
local level, n, traceback, msg = 3, 0, {}, nil
repeat
_,msg = pcall(error, "", level) --traceback skips function calls used by the "try" function.
if msg ~= "" and msg ~= skipThisLine1 and msg ~= skipThisLine2 then
n = n + 1
traceback[n] = msg
end
level = level + 1
until level == 16
local i, results, prev = 0, {}, nil
repeat
msg = traceback[n - i]
local current = msg:sub(1,1)
i = i + 1
if current ~= prev then
prev = current
results[i] = "\n-> " .. map[current] .. ".lua: " .. msg
else
results[i] = "-> " .. msg
end
until i == n
caller(function() print("|cffff5555ERROR: " .. errorMsg .. "\nStack Traceback: " .. table.concat(results):gsub("[^\x25s]+.lua:(\x25d+):", "\x251") .. "<- error" .. "|r") end )
end
---@param userFunc function the function to call in protected mode
---@param ... any params for the user's function
function try(userFunc, ...)
checkCaller()
if not skipThisLine1 then _,skipThisLine1 = pcall(error, "", 2) end return select(2, xpcall(userFunc, traceError, ...))
end
function ThrowError(msg)
checkCaller()
if not skipThisLine2 then _,skipThisLine2 = pcall(error, "", 2) end traceError(msg)
end
end
--[[Example output for a caught error:
ERROR: war3map.lua:55: attempt to index a nil value (local 'i')
Stack Traceback:
-> war3map.lua: 50 -> 52 -> 53 -> 55 <- error
--]]
do
local innerLoop
innerLoop = function(object, constTable, depth)
local objType = type(object)
if objType == "string" then
return '"'..object..'"' --wrap the string in quotes.
elseif objType == 'table' then
if not constTable[object] then
constTable.count = constTable.count + 1
constTable[object] = "table"..constTable.count
if next(object)==nil then
return constTable[object]..": {}"
elseif not constTable.maxDepth or depth <= constTable.maxDepth then
local mappedKV = {}
local function deepParse(val)
return innerLoop(val, constTable, depth + 1)
end
local indent = (" "):rep(depth - 1)
for k,v in pairs(object) do
table.insert(mappedKV, '\n '..indent ..'[' .. deepParse(k) .. '] = ' .. deepParse(v))
end
return constTable[object]..': {'.. table.concat(mappedKV, ',') .. '\n'..indent..'}'
end
end
end
return constTable[object] or tostring(object)
end
function table.tostring(whichTable, maxDepth)
return innerLoop(whichTable, {maxDepth=maxDepth, count=0}, 1)
end
function table.print(whichTable, maxDepth)
print(table.tostring(whichTable, maxDepth))
end
end
--[==[
Global Initialization 4.3.1 by Bribe
Special thanks:
Eikonium, Tasyen, HerlySQR, Forsakn and Troll-Brain
What this does:
Allows you to postpone the initialization of your script until a specific point in the loading sequence.
Additionally, all callback functions in this resource are safely called via xpcall or pcall, giving you highly valuable debugging information.
Also provides the ability to Require another resource. This functions similarly to Lua's "require" method, which was disabled in the WarCraft 3 environment.
Why not just use do...end blocks in the Lua root?
Creating WarCraft 3 objects in the Lua root has been reported to cause desyncs. Even still, the Lua root is a weird place to initialize
(e.g. doesn't allow "print", which makes debugging extremely difficult). do...end blocks force you to organize your triggers from top to bottom based on their requirements.
What is the sequence of events?
1) On...Init functions that require nothing - or - already have their requirements fulfilled in the Lua root.
2) On...Init functions that have their requirements fulfilled based on other On...Init declarations.
3) Repeat step 2 until all executables are loaded and all subsequent initializers have run.
4) OnGameStart is the final initializer.
5) Display an error message to reveal any missing requirements.
Basic API for initializer functions:
OnGlobalInit(function()
print "All udg_ variables have been initialized"
end)
OnTrigInit(function()
print "All InitTrig_ functions have been called"
end)
OnMapInit(function()
print "All Map Initialization events have run"
end)
OnGameStart(function()
print "The game has now started"
end)
Note: You can optionally include a string as an argument to give your initializer a name. This is useful in two scenarios:
1) If you don't add anything to the global API but want it to be useful as a requirment.
2) If you want it to be accurately defined for initializers that optionally require it.
API for Requirements:
local someLibrary = Require "SomeLibrary"
> Imitates Lua's built-in (but disabled in WarCraft 3) "require" method, provided that you use it from an On...Init or OnGameStart callback function.
local optionalRequirement = Require "OptionalRequirement"
> Similar to the Require method, but will only wait if the optional requirement was declared in an On...Init string parameter.
--------------
CONFIGURABLES: ]==]
do
--change this assignment to false or nil if you don't want to print any caught errors at the start of the game.
--You can otherwise change the color code to a different hex code if you want.
local _ERROR = "ff5555"
local _USE_COROUTINES = true --Change this to false if you don't use the "Require" API.
local _USE_LIBRARY_API = true --Change this to false if you don't use the "library" API.
--END CONFIGURABLES
-------------------
local _G = _G
local rawget = rawget
local insert = table.insert
local function doesVariableExist(name) --added for readability.
return rawget(_G, name)~=nil
end
local printError = DoNothing
local throwError, errorQueue
if _ERROR then
errorQueue = {}
printError = function(errorMsg)
insert(errorQueue, "|cff".._ERROR..errorMsg.."|r")
end
end
throwError = rawget(_G, "ThrowError") or printError
local library
if _USE_LIBRARY_API then
library = {
declarations = {},
initQueue = {},
loaded = {},
initiallyMissingRequirements = _ERROR and {},
initialize = function(forceOptional)
if library.initQueue[1] then
local continue, tempInitQueue
repeat
continue=false
library.initQueue, tempInitQueue = {}, library.initQueue
for _,func in ipairs(tempInitQueue) do
if func(forceOptional) then
--Something was initialized; therefore further systems might be able to initialize.
continue=true
else
--If the queued initializer returns false, that means it did not run, so we re-add it.
insert(library.initQueue, func)
end
end
until not continue or not library.initQueue[1]
end
end
}
end
local runInitializer = {}
local oldInitBlizzard = InitBlizzard
InitBlizzard = function()
runInitializer["OnMainInit"](true) --now we are safely outside of the Lua root and can start initializing.
oldInitBlizzard()
--Try to hook, if the variable doesn't exist, run the initializer immediately. Once either have executed, call the continue function.
local function tryHook(whichHook, whichInit, continue)
local hookedFunction = rawget(_G, whichHook)
if hookedFunction then
_G[whichHook] = function()
hookedFunction()
runInitializer[whichInit]()
continue()
end
else
runInitializer[whichInit]()
continue()
end
end
tryHook("InitGlobals", "OnGlobalInit", function()
tryHook("InitCustomTriggers", "OnTrigInit", function()
tryHook("RunInitializationTriggers", "OnMapInit", function()
local function runOnGameStart()
runInitializer["OnGameStart"]()
runInitializer=nil
if _ERROR then
if _USE_LIBRARY_API and library.initQueue[1] then
for _,ini in ipairs(library.initiallyMissingRequirements) do
if not doesVariableExist(ini) and not library.loaded[ini] then
printError("OnLibraryInit missing requirement: "..ini)
end
end
end
for _,msg in ipairs(errorQueue) do
print(msg) --now that the game has started, call the queued error messages.
end
errorQueue=nil
end
library=nil
end
--Use a timer to mark when the game has actually started.
TimerStart(CreateTimer(), 0, false, function()
DestroyTimer(GetExpiredTimer())
runOnGameStart()
end)
end)
end)
end)
end
local function callUserInitFunction(initFunc, name, initDeclaresItsName)
local function initFuncWrapper()
local function funcWrapper()
initFunc()
if _USE_LIBRARY_API and initDeclaresItsName then
library.loaded[initDeclaresItsName]=true
end
end
if _ERROR then
if try then try(funcWrapper) --https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/post-3552846
else
xpcall(funcWrapper, function(msg)
xpcall(error, printError, "\nGlobal Initialization Error with "..name..":\n"..msg, 4)
end)
end
else
pcall(funcWrapper)
end
end
if _USE_COROUTINES then
coroutine.resume(coroutine.create(initFuncWrapper))
else
initFuncWrapper()
end
end
---Handle logic for initialization functions that wait for certain initialization points during the map's loading sequence.
---@param name string
---@return fun(libName?:string, userFunc:fun()) OnInit --Calls userFunc during the defined initialization stage.
local function createInitAPI(name)
local userInitFunctionList = {}
--Create a handler function to run all initializers pertaining to this particular sequence.
runInitializer[name]=function(skipLibrary)
local function initialize()
for _,f in ipairs(userInitFunctionList) do
callUserInitFunction(f, name, _USE_LIBRARY_API and library.declarations[f])
end
userInitFunctionList=nil
_G[name] = nil
end
if _USE_LIBRARY_API and not skipLibrary then
library.initialize()
initialize()
library.initialize()
library.initialize(true) --force libraries with optional requirements to run.
else
initialize()
end
end
---Calls userFunc during the map loading process.
---@param libName? string
---@param userFunc fun()
return function(libName, userFunc)
if not userFunc or type(userFunc)=="string" then
libName,userFunc=userFunc,libName
end
if type(userFunc) == "function" then
insert(userInitFunctionList, userFunc)
if _USE_LIBRARY_API and libName then
library.declarations[userFunc] = libName
library.loaded[libName]=library.loaded[libName] or false
end
else
throwError("bad argument to '" .. name.."' (function expected, got "..type(userFunc)..")")
end
end
end
OnMainInit = createInitAPI("OnMainInit") -- Runs "before" InitBlizzard is called. Might not actually have any benefit over OnGlobalInit and might be deprecated at some future point.
OnGlobalInit = createInitAPI("OnGlobalInit") -- Runs once all GUI variables are instantiated.
OnTrigInit = createInitAPI("OnTrigInit") -- Runs once all InitTrig_ are called.
OnMapInit = createInitAPI("OnMapInit") -- Runs once all Map Initialization triggers are run.
OnGameStart = createInitAPI("OnGameStart") -- Runs once the game has actually started.
if _USE_LIBRARY_API then
---Might be deprecated at some future point in favor of Require, Require.optional and On...Init functions which declare a name string.
---@param whichInit string|table
---@param userFunc fun()
function OnLibraryInit(whichInit, userFunc)
local nameOfInit
local typeOfInit = type(whichInit)
if typeOfInit=="string" then whichInit = {whichInit}
elseif typeOfInit~="table" then
throwError("bad argument #1 to 'OnLibraryInit' (table expected, got "..typeOfInit..")")
return
else
nameOfInit = whichInit.name
if nameOfInit then
library.loaded[nameOfInit]=false
end
end
if not userFunc or type(userFunc)~="function" then
throwError("bad argument #2 to 'OnLibraryInit' (function expected, got "..type(userFunc)..")")
end
if _ERROR then
for _,initName in ipairs(whichInit) do
if not doesVariableExist(initName) then
insert(library.initiallyMissingRequirements, initName)
end
end
end
insert(library.initQueue, function(forceOptional)
if whichInit then
for _,initName in ipairs(whichInit) do
--check all strings in the table and make sure they exist in _G or were already initialized by OnLibraryInit with a non-global name.
if not doesVariableExist(initName) and not library.loaded[initName] then return end
end
if not forceOptional and whichInit.optional then
for _,initName in ipairs(whichInit.optional) do
--If the item isn't yet initialized, but is queued to initialize, then we postpone the initialization.
--Declarations would be made in the Lua root, so if optional dependencies are not found by the time
--OnLibraryInit runs its triggers, we can assume that it doesn't exist in the first place.
if not doesVariableExist(initName) and library.loaded[initName]==false then return end
end
end
whichInit = nil --flag as nil to prevent recursive calls.
--run the initializer if all requirements either exist in _G or have been fully declared.
callUserInitFunction(userFunc, "OnLibraryInit", nameOfInit)
return true
end
end)
end
end
if _USE_LIBRARY_API and _USE_COROUTINES then
local function addReq(optional, ...)
if not doesVariableExist(...) and not optional or library.loaded[...]==false then
local co = coroutine.running()
OnLibraryInit(optional and {optional={...}} or {...}, function() coroutine.resume(co) end)
coroutine.yield(co)
end
return rawget(_G, ...) or library.loaded[...]
end
---@class Require
---@field __call fun(requirement: string):any --local requirement = Require "SomeRequirement" --Patiently waits for the requirement and returns it
---@field optional fun(requirement: string):any --local optReq = Require.optional "SomethingElse" --Impatiently waits for the requirement and returns it
Require = {
__call = function(_, ...) return addReq(false, ...) end,
optional = function(...) return addReq(true, ...) end
}
setmetatable(Require, Require)
end
end
--[[--------------------------------------------------------------------------------------
Timed Call and Echo v2.0.0.0 by Bribe, special thanks to Eikonium and Jesus4Lyf
Allows automatic timer tracking for the most common use cases (one-shot timers, and
repeating timers that merge together for optimization).
----------------------------------------------------------------------------------------]]
do
local _DEFAULT_ECHO_TIMEOUT = 0.03125
local _EXIT_WHEN_FACTOR = 0.5 --Will potentially stop the echo before it has fully run
--its course (via rounding). Set to 0 to disable. Can also override this from the Timed.echo 5th parameter.
local zeroList, _ZERO_TIMER
local timerLists = {}
local insert = table.insert
Timed = {
--[[--------------------------------------------------------------------------------------
Name: Timed.call
Args: [delay, ]userFunc
Desc: After "delay" seconds, call "userFunc". Delay defaults to 0 seconds.
----------------------------------------------------------------------------------------]]
---@param delay? number
---@param userFunc function
---@return fun(doNotDestroy:boolean):number removalFunc -> only gets returned if the delay is > 0.
call=function(delay, userFunc)
if type(delay)=="function" then
userFunc,delay=delay,userFunc
end
if not delay or delay <= 0 then
if zeroList then
insert(zeroList, userFunc)
else
zeroList = {userFunc}
_ZERO_TIMER = _ZERO_TIMER or CreateTimer()
TimerStart(_ZERO_TIMER, 0, false, function()
local tempList = zeroList
zeroList = nil
for _, func in ipairs(tempList) do func() end
end)
end
else
local t = CreateTimer()
TimerStart(t, delay, false, function()
DestroyTimer(t)
t=nil
userFunc()
end)
return function(doNotDestroy)
local result = 0
if t then
result = TimerGetRemaining(t)
if not doNotDestroy then
PauseTimer(t)
DestroyTimer(t)
t=nil
end
end
return result
end
end
end,
--[[--------------------------------------------------------------------------------------
Name: Timed.echo
Args: [timeout, duration,] userFunc
Desc: Calls userFunc every "timeout" seconds until userFunc returns true.
-> will also stop calling userFunc if the duration is reached.
-> Returns a function you can call to manually stop echoing the userFunc.
--------------------------------------------------------------------------------------
Note: This merges all matching timeouts together, so it is advisable only to use this
for smaller numbers (e.g. <.3 seconds) where the difference is less noticeable.
----------------------------------------------------------------------------------------]]
---@param timeout? number
---@param duration? number
---@param userFunc fun():boolean -- if true, echo will stop
---@param onExpire? function -- If the duration is specified and expiration occurs naturally, call this function.
---@param tolerance? number -- Ranges from 0-1. If the duration is specified, the tolerance helps to measure the accuracy of the final tick.
---@return function remove_func
echo=function(timeout, duration, userFunc, onExpire, tolerance)
if type(timeout) == "function" then
--parames align to original API of (function[,timeout])
userFunc,timeout,duration=timeout,duration,userFunc
elseif not userFunc then
--params were (timeout,userFunc)
userFunc,duration=duration,nil
--else params were exactly as defined.
end
local wrapper = function()
return not userFunc or userFunc() --this wrapper function allows manual removal to be understood and processed accordingly.
end
timeout = timeout or _DEFAULT_ECHO_TIMEOUT
if duration then
local old=wrapper
local exitwhen = timeout*(tolerance or _EXIT_WHEN_FACTOR)
wrapper=function() --this wrapper function enables automatic removal once the duration is reached.
if not old() then
duration = duration - timeout
if duration >= exitwhen then
return
elseif onExpire then
print(duration, exitwhen)
onExpire()
end
end
return true
end
else
duration=0
end
local timerList = timerLists[timeout]
if timerList then
local remaining = TimerGetRemaining(timerList.timer)
if remaining >= timeout * 0.50 then
duration = duration + timeout --The delay is large enough to execute on the next tick, therefore increase the duration to avoid double-deducting.
insert(timerList, wrapper)
elseif timerList.queue then
insert(timerList.queue, wrapper)
else
timerList.queue = {wrapper}
end
duration = duration - remaining --decrease the duration to compensate for the extra remaining time before the next tick.
else
timerList = {wrapper}
timerLists[timeout] = timerList
timerList.timer = CreateTimer()
TimerStart(timerList.timer, timeout, true, function()
local top=#timerList
for i=top,1,-1 do
if timerList[i]() then --The userFunc is to be removed:
if i~=top then
timerList[i]=timerList[top]
end
timerList[top]=nil
top=top-1
end
end
if timerList.queue then --Now we can add the queued items to the main list
for i,func in ipairs(timerList.queue) do
timerList[top+i]=func
end
timerList.queue = nil
elseif top == 0 then --list is empty; clear its data.
timerLists[timeout] = nil
PauseTimer(timerList.timer)
DestroyTimer(timerList.timer)
end
end)
end
return function(doNotDestroy)
if not doNotDestroy then
userFunc=nil
end
return duration
end
end
}
end
--[[
--------------------------------------------------------------------
Hook 5.0.2.0
--------------------------------------------------------------------
Provides a single function as its API: AddHook.
AddHook returns two functions: 1) old function* and 2) function to call to remove the hook.**
*The old function will point to either the originally-hooked function, or it will point to the next-lower priority
"AddHook" function a user requested. Not calling "oldFunc" means that any lower-priority callbacks will not
execute. It is therefore key to make sure to prioritize correctly: higher numbers are called before lower ones.
"Classic" way of hooking in Lua is shown below, but doesn't allow other hooks to take a higher priority, nor can it be removed safely.
local oldFunc = BJDebugMsg
BJDebugMsg = print
The below allows other hooks to coexist with it, based on a certain priority, and can be removed:
local oldFunc, removeFunc = AddHook("BJDebugMsg", print)
A small showcase of what you can do with the API:
local oldFunc, removeFunc --remember to declare locals before any function that uses them
oldFunc, removeFunc = AddHook("BJDebugMsg", function(s)
if want_to_display_to_all_players then
oldFunc(s) --this either calls the native function, or allows lower-priority hooks to run.
elseif want_to_remove_hook then
removeFunc() --removes just this hook from BJDebugMsg
elseif want_to_remove_all_hooks then
removeFunc(true) --removes all hooks on BJDebugMsg
else
print(s) --not calling the native function means that lower-priority hooks will be skipped.
end
print("returning nothing") --this function should return whatever the original function returns. BJDebugMsg returns nothing, but CreateUnit returns a unit.
end)
]]------------------------------------------------------------------
---@param oldFunc string The name of the old function you wish to hook
---@param userFunc function The function you want to run when oldFunc is called. The args and return values would normally mimic the function that is hooked.
---@param priority? number Defaults to 0. Hooks are called in order of highest priority down to lowest priority.
---@param parentTable? table Defaults to _G, which is the table that stores all global variables.
---@param defaultOldFunc? function If the oldFunc does not exist in the parent table, use this default function instead.
---@param storeIntoMetatable? boolean Defaults to true if the "default" parameter is given.
---@return fun(args_should_match_the_original_function?):any old_function_or_lower_priority_hooks
---@return fun(remove_all_hooks?:boolean) call_this_to_remove_hook
function AddHook(oldFunc, userFunc, priority, parentTable, defaultOldFunc, storeIntoMetatable)
parentTable = parentTable or _G
priority = priority or 0
local hookStr = "__hookHandler_"..oldFunc --You can change the prefix if you want (in case it conflicts with any other prefixes you use in the parentTable).
if defaultOldFunc and storeIntoMetatable == nil or storeIntoMetatable then
--Index the hook to the metatable instead of the user's given table.
local mt = getmetatable(parentTable)
if not mt then
--Create a new metatable in case none existed.
mt = {}
setmetatable(parentTable, mt)
end
parentTable = mt
end
local index = 2 --The index defaults to 2 (index 1 is reserved for the original function that you're trying to hook)
local hooks = rawget(parentTable, hookStr)
if hooks then
local fin = #hooks
repeat
--Search manually for an index based on the priority of all other hooks.
if hooks[index][2] > priority then break end
index = index + 1
until index > fin
else
--create a table that stores all hooks that can be added to this function.
--[1] either points to the native function, to the default function (if none existed).
--[2] is a function that is a function called to update hook indices when a new hook is added or an old one is removed.
hooks = {
--hooks[1] serves as the root hook table.
{
--index[1]
--Falls back to a junk function if no oldFunc nor default was provided.
--Will throw an error in that case if you un-comment the block quote below.
rawget(parentTable, oldFunc) or defaultOldFunc --[[or print("failed to hook "..oldFunc)]] or function() end
,
--index[2]
---@param where integer
---@param instance? table [1]:function userFunc, [2]:integer priority, [3]:fun(index:integer)
function(where, instance)
local n = #hooks
if where > n then
--this only occurs when this is the first hook.
hooks[where] = instance
elseif where == n and not instance then
--the top hook is being removed.
hooks[where] = nil
--assign the next function to the parent table
rawset(parentTable, oldFunc, hooks[where-1][1])
else
if instance then
--if an instance is provided, we add it
table.insert(hooks, where, instance)
where = where + 1
n = n + 1
else
--if no instance is provided, we remove the existing index.
table.remove(hooks, where)
n = n - 1
end
--when an index is added or removed in the middle of the list, re-map the subsequent indices:
for i = where, n do
hooks[i][3](i)
end
end
end
}
}
rawset(parentTable, hookStr, hooks) --this would store the hook-table holding BJDebugMsg as _G["__hookHandler_BJDebugMsg"]
end
--call the stored function at root-hook-table[2] to assign indices.
hooks[1][2](index, { --this table belongs specifically to this newly-added Hook instance.
--index[1] is the function that needs to be called in place of the original function.
userFunc
,
--index[2] is the priority specified by the user, so it can be compared with future added hooks.
priority
,
--index[3] is the function that is used to inject an instruction to realign the instance's local index
--Almost everything is processed in local scope via containers/closures.
--This keeps the "oldFunc" recursive callbacks extremely performant.
function(i) index = i end
})
if index == #hooks then
--this is the highest-priority hook and should be called first.
--Therefore, insert the user's function as the actual function that gets natively called.
rawset(parentTable, oldFunc, userFunc)
end
--This is the first returned function (old_function_or_lower_priority_hooks):
return function(...)
return hooks[index-1][1](...)
end,
---This is the second returned function (call_this_to_remove_hook):
function(removeAll)
if removeAll or #hooks == 2 then
--Remove all hooks, clear memory and restore the original function to the parent table.
rawset(parentTable, hookStr, nil)
rawset(parentTable, oldFunc, hooks[1][1])
else
hooks[1][2](index) --remove just the single hook instance
end
end
end
OnGlobalInit("GlobalRemap", function()
Require "AddHook" --https://www.hiveworkshop.com/threads/hook.339153
--[[
--------------------------------------------------------------------------------------
Global Variable Remapper v1.3 by Bribe
- Intended to empower the GUI user-base and those who design systems for them.
API:
GlobalRemap(variableStr[, getterFunc, setterFunc])
@variableStr is a string such as "udg_MyVariable"
@getterFunc is a function that takes nothing but returns the expected value when
"udg_MyVariable" is referenced.
@setterFunc is a function that takes a single argument (the value that is being
assigned) and allows you to do what you want when someone uses "Set MyVariable = SomeValue".
The function doesn't need to do anything nor return anything. Enables read-only
GUI variables for the first time in WarCraft 3 history.
GlobalRemapArray(variableStr[, getterFunc, setterFunc])
@variableStr is a string such as "udg_MyVariableArray"
@getterFunc is a function that takes the index of the array and returns the
expected value when "MyVariableArray" is referenced.
@setterFunc is a function that takes two arguments: the index of the array and the
value the user is trying to assign. The function doesn't return anything.
----------------------------------------------------------------------------------------]]
local getters, setters, skip
---Remap a non-array global variable
---@param var string
---@param getFunc? fun():any
---@param setFunc? fun(value:any)
function GlobalRemap(var, getFunc, setFunc)
if not skip then
getters, setters, skip = {}, {}, DoNothing
local oldGet, oldSet
oldGet = AddHook("__index",
function(tab, index)
local func = getters[index]
if func then
return func()
else
print("Trying to read undeclared global: "..tostring(index))
return oldGet(tab, index)
end
end, nil, _G,
function(a, b)
return rawget(a, b)
end, true)
oldSet = AddHook("__newindex",
function(tab, index, val)
local func = setters[index]
if func then
func(val)
else
oldSet(tab, index, val)
end
end, nil, _G,
function(a, b, c)
rawset(a, b, c)
end, true)
end
_G[var] = nil --Delete the variable from the global table.
getters[var] = getFunc or skip --Assign a function that returns what should be returned when this variable is referenced.
setters[var] = setFunc or skip --Assign a function that captures the value the variable is attempting to be set to.
end
---Remap a global variable array
---@param var string
---@param getFunc? fun(index : any) -> any
---@param setFunc? fun(index : any, val : any)
function GlobalRemapArray(var, getFunc, setFunc) --will get inserted into GlobalRemap after testing.
local tab = {}
_G[var] = tab
getFunc = getFunc or DoNothing
setFunc = setFunc or DoNothing
setmetatable(tab, {
__index = function(_, index)
return getFunc(index)
end,
__newindex = function(_, index, val)
--if index==nil then
-- print("Attempt to index a nil value: "..GetStackTrace())
-- return
--end
setFunc(index, val)
end
})
end
end)
--[[
Lua-Infused GUI with automatic memory leak resolution: Modernizing the experience for a better future for users of the Trigger Editor.
Credits:
Bribe, Tasyen, Dr Super Good, HerlySQR
Transforming rects, locations, groups, forces and BJ hashtable wrappers into Lua tables, which are automatically garbage collected.
Provides RegisterAnyPlayerUnitEvent to cut down on handle count and simplify syntax for Lua users while benefitting GUI.
Provides GUI.enumUnitsInRect/InRange/Selected/etc. which replaces the first parameter with a function (which takes a unit), for immediate action without needing a separate group variable.
Provides GUI.loopArray for safe iteration over a __jarray
Updated: 20 Oct 2022
--]]
GUI = {}
do
--Configurables
local _USE_GLOBAL_REMAP = false --Found on https://www.hiveworkshop.com/threads/global-variable-remapper-the-future-of-gui.339308/
local _USE_UNIT_EVENT = false --set to true if you have UnitEvent in your map and want to automatically remove units from their unit groups.
--Define common variables to be utilized throughout the script.
local _G = _G
local unpack = table.unpack
do
--[[-----------------------------------------------------------------------------------------
__jarray expander by Bribe
This snippet will ensure that objects used as indices in udg_ arrays will be automatically
cleaned up when the garbage collector runs, and tries to re-use metatables whenever possible.
-------------------------------------------------------------------------------------------]]
local mts = {}
local weakKeys = {__mode="k"} --ensures tables with non-nilled objects as keys will be garbage collected.
---Re-define __jarray.
---@param default? any
---@param tab? table
---@return table
__jarray=function(default, tab)
local mt
if default then
mts[default]=mts[default] or {
__index=function()
return default
end,
__mode="k"
}
mt=mts[default]
else
mt=weakKeys
end
return setmetatable(tab or {}, mt)
end
--have to do a wide search for all arrays in the variable editor. The WarCraft 3 _G table is HUGE,
--and without editing the war3map.lua file manually, it is not possible to rewrite it in advance.
for k,v in pairs(_G) do
if type(v) == "table" and string.sub(k, 1, 4)=="udg_" then
__jarray(v[0], v)
end
end
---Add this safe iterator function for jarrays.
---@param whichTable table
---@param func fun(index:integer, value:any)
function GUI.loopArray(whichTable, func)
for i=rawget(whichTable, 0)~=nil and 0 or 1, #whichTable do
func(i, rawget(whichTable, i))
end
end
end
--[=============[
• HASHTABLES •
--]=============]
do --[[
GUI hashtable converter by Tasyen and Bribe
Converts GUI hashtables API into Lua Tables, overwrites StringHashBJ and GetHandleIdBJ to permit
typecasting, bypasses the 256 hashtable limit by avoiding hashtables, provides the variable
"HashTableArray", which automatically creates hashtables for you as needed (so you don't have to
initialize them each time).
]]
function StringHashBJ(s) return s end
function GetHandleIdBJ(id) return id end
local function load(whichHashTable,parentKey)
local index = whichHashTable[parentKey]
if not index then
index=__jarray()
whichHashTable[parentKey]=index
end
return index
end
if _USE_GLOBAL_REMAP then
OnGlobalInit(function()
local remap = Require "GlobalRemapArray"
local hashes = __jarray()
remap("udg_HashTableArray", function(index)
return load(hashes, index)
end)
end)
end
local last
GetLastCreatedHashtableBJ=function() return last end
function InitHashtableBJ() last=__jarray() ; return last end
local function saveInto(value, childKey, parentKey, whichHashTable)
if childKey and parentKey and whichHashTable then
load(whichHashTable, parentKey)[childKey] = value
end
end
local function loadFrom(childKey, parentKey, whichHashTable, default)
if childKey and parentKey and whichHashTable then
local val = load(whichHashTable, parentKey)[childKey]
return val ~= nil and val or default
end
end
SaveIntegerBJ = saveInto
SaveRealBJ = saveInto
SaveBooleanBJ = saveInto
SaveStringBJ = saveInto
local function createDefault(default)
return function(childKey, parentKey, whichHashTable)
return loadFrom(childKey, parentKey, whichHashTable, default)
end
end
local loadNumber = createDefault(0)
LoadIntegerBJ = loadNumber
LoadRealBJ = loadNumber
LoadBooleanBJ = createDefault(false)
LoadStringBJ = createDefault("")
do
local sub = string.sub
for key in pairs(_G) do
if sub(key, -8)=="HandleBJ" then
local str=sub(key, 1,4)
if str=="Save" then _G[key] = saveInto
elseif str=="Load" then _G[key] = loadFrom end
end
end
end
function HaveSavedValue(childKey, _, parentKey, whichHashTable)
return load(whichHashTable, parentKey)[childKey] ~= nil
end
FlushParentHashtableBJ = function(whichHashTable)
for key in pairs(whichHashTable) do
whichHashTable[key]=nil
end
end
function FlushChildHashtableBJ(whichHashTable, parentKey)
whichHashTable[parentKey]=nil
end
end
--[===========================[
• LOCATIONS (POINTS IN GUI) •
--]===========================]
do
local oldLocation, location = Location
do
local oldRemove = RemoveLocation
local oldGetX = GetLocationX
local oldGetY = GetLocationY
local oldRally = GetUnitRallyPoint
function GetUnitRallyPoint(unit)
local removeThis = oldRally(unit) --Actually needs to create a location for a brief moment, as there is no GetUnitRallyX/Y
local loc = {oldGetX(removeThis), oldGetY(removeThis)}
oldRemove(removeThis)
return loc
end
end
RemoveLocation = DoNothing
function Location(x,y)
return {x,y}
end
do
local oldMoveLoc = MoveLocation
local oldGetZ=GetLocationZ
function GUI.getCoordZ(x,y)
GUI.getCoordZ = function(x,y)
oldMoveLoc(location, x, y)
return oldGetZ(location)
end
location = oldLocation(x,y)
return oldGetZ(location)
end
end
function GetLocationX(loc) return loc[1] end
function GetLocationY(loc) return loc[2] end
function GetLocationZ(loc)
return GUI.getCoordZ(loc[1], loc[2])
end
function MoveLocation(loc, x, y)
loc[1]=x
loc[2]=y
end
local function fakeCreate(varName, suffix)
local getX=_G[varName.."X"]
local getY=_G[varName.."Y"]
_G[varName..(suffix or "Loc")]=function(obj) return {getX(obj), getY(obj)} end
end
fakeCreate("GetUnit")
fakeCreate("GetOrderPoint")
fakeCreate("GetSpellTarget")
fakeCreate("CameraSetupGetDestPosition")
fakeCreate("GetCameraTargetPosition")
fakeCreate("GetCameraEyePosition")
fakeCreate("BlzGetTriggerPlayerMouse", "Position")
fakeCreate("GetStartLocation")
BlzSetSpecialEffectPositionLoc = function(effect, loc)
local x,y=loc[1],loc[2]
BlzSetSpecialEffectPosition(effect, x, y, GUI.getCoordZ(x,y))
end
---@param oldVarName string
---@param newVarName string
---@param index integer needed to determine which of the parameters calls for a location.
local function hook(oldVarName, newVarName, index)
local new = _G[newVarName]
local func
if index==1 then
func=function(loc, ...)
return new(loc[1], loc[2], ...)
end
elseif index==2 then
func=function(a, loc, ...)
return new(a, loc[1], loc[2], ...)
end
else--index==3
func=function(a, b, loc, ...)
return new(a, b, loc[1], loc[2], ...)
end
end
_G[oldVarName] = func
end
hook("IsLocationInRegion", "IsPointInRegion", 2)
hook("IsUnitInRangeLoc", "IsUnitInRangeXY", 2)
hook("IssuePointOrderLoc", "IssuePointOrder", 3)
IssuePointOrderLocBJ =IssuePointOrderLoc
hook("IssuePointOrderByIdLoc", "IssuePointOrderById", 3)
hook("IsLocationVisibleToPlayer", "IsVisibleToPlayer", 1)
hook("IsLocationFoggedToPlayer", "IsFoggedToPlayer", 1)
hook("IsLocationMaskedToPlayer", "IsMaskedToPlayer", 1)
hook("CreateFogModifierRadiusLoc", "CreateFogModifierRadius", 3)
hook("AddSpecialEffectLoc", "AddSpecialEffect", 2)
hook("AddSpellEffectLoc", "AddSpellEffect", 3)
hook("AddSpellEffectByIdLoc", "AddSpellEffectById", 3)
hook("SetBlightLoc", "SetBlight", 2)
hook("DefineStartLocationLoc", "DefineStartLocation", 2)
hook("GroupEnumUnitsInRangeOfLoc", "GroupEnumUnitsInRange", 2)
hook("GroupEnumUnitsInRangeOfLocCounted", "GroupEnumUnitsInRangeCounted", 2)
hook("GroupPointOrderLoc", "GroupPointOrder", 3)
GroupPointOrderLocBJ =GroupPointOrderLoc
hook("GroupPointOrderByIdLoc", "GroupPointOrderById", 3)
hook("MoveRectToLoc", "MoveRectTo", 2)
hook("RegionAddCellAtLoc", "RegionAddCell", 2)
hook("RegionClearCellAtLoc", "RegionClearCell", 2)
hook("CreateUnitAtLoc", "CreateUnit", 3)
hook("CreateUnitAtLocByName", "CreateUnitByName", 3)
hook("SetUnitPositionLoc", "SetUnitPosition", 2)
hook("ReviveHeroLoc", "ReviveHero", 2)
hook("SetFogStateRadiusLoc", "SetFogStateRadius", 3)
---@param min table location
---@param max table location
---@return rect newRect
RectFromLoc = function(min, max)
return Rect(min[1], min[2], max[1], max[2])
end
---@param whichRect rect
---@param min table location
---@param max table location
SetRectFromLoc = function(whichRect, min, max)
SetRect(whichRect, min[1], min[2], max[1], max[2])
end
end
--[=============================[
• GROUPS (UNIT GROUPS IN GUI) •
--]=============================]
do
local mainGroup = bj_lastCreatedGroup
DestroyGroup(bj_suspendDecayFleshGroup)
DestroyGroup(bj_suspendDecayBoneGroup)
DestroyGroup=DoNothing
CreateGroup=function() return {indexOf={}} end
bj_lastCreatedGroup=CreateGroup()
bj_suspendDecayFleshGroup=CreateGroup()
bj_suspendDecayBoneGroup=CreateGroup()
local groups
if _USE_UNIT_EVENT then
groups = {}
function GroupClear(group)
if group then
local u
for i=1, #group do
u=group[i]
groups[u]=nil
group.indexOf[u]=nil
group[i]=nil
end
end
end
else
function GroupClear(group)
if group then
for i=1, #group do
group.indexOf[group[i]]=nil
group[i]=nil
end
end
end
end
function GroupAddUnit(group, unit)
if group and unit and not group.indexOf[unit] then
local pos = #group+1
group.indexOf[unit]=pos
group[pos]=unit
if groups then
groups[unit] = groups[unit] or __jarray()
groups[unit][group]=true
end
end
end
function GroupRemoveUnit(group, unit)
local indexOf = group and unit and group.indexOf
if indexOf then
local pos = indexOf[unit]
if pos then
local size = #group
if pos ~= size then
indexOf[group[size]] = pos
end
group[size]=nil
indexOf[unit]=nil
if groups then
groups[unit][group]=nil
end
end
end
end
function IsUnitInGroup(unit, group)
return unit and group and group.indexOf[unit]
end
function FirstOfGroup(group)
return group and group[1]
end
local enumUnit
GetEnumUnit=function() return enumUnit end
function GUI.forGroup(group, code)
for i=1, #group do
code(group[i])
end
end
ForGroup = function(group, code)
if group and code then
local old = enumUnit
GUI.forGroup(group, function(unit)
enumUnit=unit
code()
end)
enumUnit=old
end
end
do
local oldUnitAt=BlzGroupUnitAt
function BlzGroupUnitAt(group, index)
return group and group[index+1]
end
local oldGetSize=BlzGroupGetSize
local function groupAction(code)
for i=0, oldGetSize(mainGroup)-1 do
code(oldUnitAt(mainGroup, i))
end
end
for _,name in ipairs({
"OfType",
"OfPlayer",
"OfTypeCounted",
"InRect",
"InRectCounted",
"InRange",
"InRangeOfLoc",
"InRangeCounted",
"InRangeOfLocCounted",
"Selected"
}) do
local varStr = "GroupEnumUnits"..name
local old=_G[varStr]
_G[varStr]=function(group, ...)
if group then
old(mainGroup, ...)
GroupClear(group)
groupAction(function(unit)
GroupAddUnit(group, unit)
end)
end
end
--Provide API for Lua users who just want to efficiently run code, without caring about the group itself.
GUI["enumUnits"..name]=function(code, ...)
old(mainGroup, ...)
groupAction(code)
end
end
end
for _,name in ipairs {
"ImmediateOrder",
"ImmediateOrderById",
"PointOrder",
"PointOrderById",
"TargetOrder",
"TargetOrderById"
} do
local new = _G["Issue"..name]
_G["Group"..name]=function(group, ...)
for i=1, #group do
new(group[i], ...)
end
end
end
GroupTrainOrderByIdBJ = GroupImmediateOrderById
BlzGroupGetSize=function(group) return group and #group or 0 end
function GroupAddGroup(group, add)
if not group or not add then return end
GUI.forGroup(add, function(unit)
GroupAddUnit(group, unit)
end)
end
function GroupRemoveGroup(group, remove)
if not group or not remove then return end
GUI.forGroup(remove, function(unit)
GroupRemoveUnit(group, unit)
end)
end
GroupPickRandomUnit=function(group)
return group and group[1] and group[GetRandomInt(1,#group)] or 0
end
IsUnitGroupEmptyBJ=function(group)
return not group or not group[1]
end
ForGroupBJ=ForGroup
CountUnitsInGroup=BlzGroupGetSize
BlzGroupAddGroupFast=GroupAddGroup
BlzGroupRemoveGroupFast=GroupRemoveGroup
GroupPickRandomUnitEnum=nil
CountUnitsInGroupEnum=nil
GroupAddGroupEnum=nil
GroupRemoveGroupEnum=nil
if groups then
OnGlobalInit(function()
Require "UnitEvent"
UnitEvent.onRemoval(function(data)
local u = data.unit
local g = groups[u]
if g then
for _,group in pairs(g) do
GroupRemoveUnit(group,u)
end
end
end)
end)
end
end
--[========================[
• RECTS (REGIONS IN GUI) •
--]========================]
do
local oldRect, rect = Rect
function Rect(...) return {...} end
local oldSetRect = SetRect
function SetRect(r, mix, miy, max, may)
r[1]=mix
r[2]=miy
r[3]=max
r[4]=may
end
do
local oldWorld = GetWorldBounds
local getMinX=GetRectMinX
local getMinY=GetRectMinY
local getMaxX=GetRectMaxX
local getMaxY=GetRectMaxY
local remover = RemoveRect
RemoveRect=DoNothing
local newWorld
function GetWorldBounds()
if not newWorld then
local w = oldWorld()
newWorld = {getMinX(w),getMinY(w),getMaxX(w),getMaxY(w)}
remover(w)
end
return {unpack(newWorld)}
end
GetEntireMapRect = GetWorldBounds
end
function GetRectMinX(r) return r[1] end
function GetRectMinY(r) return r[2] end
function GetRectMaxX(r) return r[3] end
function GetRectMaxY(r) return r[4] end
function GetRectCenterX(r) return (r[1] + r[3])/2 end
function GetRectCenterY(r) return (r[2] + r[4])/2 end
function MoveRectTo(r, x, y)
x = x - GetRectCenterX(r)
y = y - GetRectCenterY(r)
SetRect(r, r[1]+x, r[2]+y, r[3]+x, r[4]+y)
end
---@param varName string
---@param index integer needed to determine which of the parameters calls for a rect.
local function hook(varName, index)
local old = _G[varName]
local func
if index==1 then
func=function(rct, ...)
oldSetRect(rect, unpack(rct))
return old(rect, ...)
end
elseif index==2 then
func=function(a, rct, ...)
oldSetRect(rect, unpack(rct))
return old(a, rect, ...)
end
else--index==3
func=function(a, b, rct, ...)
oldSetRect(rect, unpack(rct))
return old(a, b, rect, ...)
end
end
_G[varName] = function(...)
if not rect then rect = oldRect(0,0,32,32) end
_G[varName] = func
return func(...)
end
end
hook("EnumDestructablesInRect", 1)
hook("EnumItemsInRect", 1)
hook("AddWeatherEffect", 1)
hook("SetDoodadAnimationRect", 1)
hook("GroupEnumUnitsInRect", 2)
hook("GroupEnumUnitsInRectCounted", 2)
hook("RegionAddRect", 2)
hook("RegionClearRect", 2)
hook("SetBlightRect", 2)
hook("SetFogStateRect", 3)
hook("CreateFogModifierRect", 3)
end
--[===============================[
• FORCES (PLAYER GROUPS IN GUI) •
--]===============================]
do
local oldForce, mainForce, initForce = CreateForce
initForce = function()
initForce = DoNothing
mainForce = oldForce()
end
CreateForce=function() return {indexOf={}} end
DestroyForce=DoNothing
local oldClear=ForceClear
function ForceClear(force)
if force then
for i,val in ipairs(force) do
force.indexOf[val]=nil
force[i]=nil
end
end
end
do
local oldCripple = CripplePlayer
local oldAdd=ForceAddPlayer
CripplePlayer = function(player,force,flag)
if player and force then
initForce()
for _,val in ipairs(force) do
oldAdd(mainForce, val)
end
oldCripple(player, mainForce, flag)
oldClear(mainForce)
end
end
end
function ForceAddPlayer(force, player)
if force and player and not force.indexOf[player] then
local pos = #force+1
force.indexOf[player]=pos
force[pos]=player
end
end
function ForceRemovePlayer(force, player)
local pos = force and player and force.indexOf[player]
if pos then
force.indexOf[player]=nil
local top = #force
if pos ~= top then
force[pos] = force[top]
force.indexOf[force[top]] = pos
end
force[top] = nil
end
end
function BlzForceHasPlayer(force, player)
return force and player and force.indexOf[player]
end
function IsPlayerInForce(player, force)
return player and force and force.indexOf[player]
end
function IsUnitInForce(unit, force)
return unit and force and force.indexOf[GetOwningPlayer(unit)]
end
local enumPlayer
local oldForForce = ForForce
local oldEnumPlayer = GetEnumPlayer
GetEnumPlayer=function() return enumPlayer end
ForForce = function(force, code)
local old = enumPlayer
for _,player in ipairs(force) do
enumPlayer=player
code()
end
enumPlayer=old
end
local function funnelEnum(force)
ForceClear(force)
initForce()
oldForForce(mainForce, function()
ForceAddPlayer(force, oldEnumPlayer())
end)
oldClear(mainForce)
end
local function hookEnum(varStr)
local old=_G[varStr]
_G[varStr]=function(force, ...)
initForce()
old(mainForce, ...)
funnelEnum(force)
end
end
hookEnum("ForceEnumPlayers")
hookEnum("ForceEnumPlayersCounted")
hookEnum("ForceEnumAllies")
hookEnum("ForceEnumEnemies")
CountPlayersInForceBJ=function(force) return #force end
CountPlayersInForceEnum=nil
GetForceOfPlayer=function(player)
--No longer leaks. There was no reason to dynamically create forces to begin with.
return bj_FORCE_PLAYER[GetPlayerId(player)]
end
end
--Blizzard forgot to add this, but still enabled it for GUI. Therefore, I've extracted and simplified the code from DebugIdInteger2IdString
function BlzFourCC2S(value)
local result = ""
for _=1,4 do
result = string.char(value % 256) .. result
value = value // 256
end
return result
end
function TriggerRegisterDestDeathInRegionEvent(trig, r)
--Removes the limit on the number of destructables that can be registered.
EnumDestructablesInRect(r, nil, function() TriggerRegisterDeathEvent(trig, GetEnumDestructable()) end)
end
IsUnitAliveBJ=UnitAlive --use the reliable native instead of the life checks
function IsUnitDeadBJ(u) return not UnitAlive(u) end
function SetUnitPropWindowBJ(whichUnit, propWindow)
--Allows the Prop Window to be set to zero to allow unit movement to be suspended.
SetUnitPropWindow(whichUnit, math.rad(propWindow))
end
if _USE_GLOBAL_REMAP then OnGlobalInit(function()
Require "GlobalRemap"
GlobalRemap("udg_INFINITE_LOOP", function() return -1 end) --a readonly variable for infinite looping in GUI.
end) end
do
local cache=__jarray()
function GUI.wrapTrigger(whichTrig)
local func=cache[whichTrig]
if not func then
func=function()if IsTriggerEnabled(whichTrig)and TriggerEvaluate(whichTrig)then TriggerExecute(whichTrig)end end
cache[whichTrig]=func
end
return func
end
end
do
--[[---------------------------------------------------------------------------------------------
RegisterAnyPlayerUnitEvent by Bribe
RegisterAnyPlayerUnitEvent cuts down on handle count for alread-registered events, plus has
the benefit for Lua users to just use function calls.
Adds a third parameter to the RegisterAnyPlayerUnitEvent function: "skip". If true, disables
the specified event, while allowing a single function to run discretely. It also allows (if
Global Variable Remapper is included) GUI to un-register a playerunitevent by setting
udg_RemoveAnyUnitEvent to the trigger they wish to remove.
The "return" value of RegisterAnyPlayerUnitEvent calls the "remove" method. The API, therefore,
has been reduced to just this one function (in addition to the bj override).
-----------------------------------------------------------------------------------------------]]
local fStack,tStack,oldBJ = {},{},TriggerRegisterAnyUnitEventBJ
function RegisterAnyPlayerUnitEvent(event, userFunc, skip)
if skip then
local t = tStack[event]
if t and IsTriggerEnabled(t) then
DisableTrigger(t)
userFunc()
EnableTrigger(t)
else
userFunc()
end
else
local funcs,insertAt=fStack[event],1
if funcs then
insertAt=#funcs+1
if insertAt==1 then EnableTrigger(tStack[event]) end
else
local t=CreateTrigger()
oldBJ(t, event)
tStack[event],funcs = t,{}
fStack[event]=funcs
TriggerAddCondition(t, Filter(function()
for _,func in ipairs(funcs)do func()end
end))
end
funcs[insertAt]=userFunc
return function()
local total=#funcs
for i=1,total do
if funcs[i]==userFunc then
if total==1 then DisableTrigger(tStack[event]) --no more events are registered, disable the event (for now).
elseif total> i then funcs[i]=funcs[total] end --pop just the top index down to this vacant slot so we don't have to down-shift the entire stack.
funcs[total]=nil --remove the top entry.
return true
end
end
end
end
end
local trigFuncs
function TriggerRegisterAnyUnitEventBJ(trig, event)
local removeFunc=RegisterAnyPlayerUnitEvent(event, GUI.wrapTrigger(trig))
if _USE_GLOBAL_REMAP then
if not trigFuncs then
trigFuncs=__jarray()
GlobalRemap("udg_RemoveAnyUnitEvent", nil, function(t)
if trigFuncs[t] then
trigFuncs[t]()
trigFuncs[t]=nil
end
end)
end
trigFuncs[trig]=removeFunc
end
return removeFunc
end
end
---Modify to allow requests for negative hero stats, as per request from Tasyen.
---@param whichHero unit
---@param whichStat integer
---@param value integer
function SetHeroStat(whichHero, whichStat, value)
(whichStat==bj_HEROSTAT_STR and SetHeroStr or whichStat==bj_HEROSTAT_AGI and SetHeroAgi or SetHeroInt)(whichHero, value, true)
end
--The next part of the code is purely optional, as it is intended to optimize rather than add new functionality
CommentString = nil
RegisterDestDeathInRegionEnum = nil
--This next list comes from HerlySQR, and its purpose is to eliminate useless wrapper functions (only where the parameters aligned):
StringIdentity = GetLocalizedString
TriggerRegisterTimerExpireEventBJ = TriggerRegisterTimerExpireEvent
TriggerRegisterDialogEventBJ = TriggerRegisterDialogEvent
TriggerRegisterUpgradeCommandEventBJ = TriggerRegisterUpgradeCommandEvent
RemoveWeatherEffectBJ = RemoveWeatherEffect
DestroyLightningBJ = DestroyLightning
GetLightningColorABJ = GetLightningColorA
GetLightningColorRBJ = GetLightningColorR
GetLightningColorGBJ = GetLightningColorG
GetLightningColorBBJ = GetLightningColorB
SetLightningColorBJ = SetLightningColor
GetAbilityEffectBJ = GetAbilityEffectById
GetAbilitySoundBJ = GetAbilitySoundById
ResetTerrainFogBJ = ResetTerrainFog
SetSoundDistanceCutoffBJ = SetSoundDistanceCutoff
SetSoundPitchBJ = SetSoundPitch
AttachSoundToUnitBJ = AttachSoundToUnit
KillSoundWhenDoneBJ = KillSoundWhenDone
PlayThematicMusicBJ = PlayThematicMusic
EndThematicMusicBJ = EndThematicMusic
StopMusicBJ = StopMusic
ResumeMusicBJ = ResumeMusic
VolumeGroupResetImmediateBJ = VolumeGroupReset
WaitForSoundBJ = TriggerWaitForSound
ClearMapMusicBJ = ClearMapMusic
DestroyEffectBJ = DestroyEffect
GetItemLifeBJ = GetWidgetLife -- This was just to type casting
SetItemLifeBJ = SetWidgetLife -- This was just to type casting
UnitRemoveBuffBJ = UnitRemoveAbility -- The buffs are abilities
GetLearnedSkillBJ = GetLearnedSkill
UnitDropItemPointBJ = UnitDropItemPoint
UnitDropItemTargetBJ = UnitDropItemTarget
UnitUseItemDestructable = UnitUseItemTarget -- This was just to type casting
UnitInventorySizeBJ = UnitInventorySize
SetItemInvulnerableBJ = SetItemInvulnerable
SetItemDropOnDeathBJ = SetItemDropOnDeath
SetItemDroppableBJ = SetItemDroppable
SetItemPlayerBJ = SetItemPlayer
ChooseRandomItemBJ = ChooseRandomItem
ChooseRandomNPBuildingBJ = ChooseRandomNPBuilding
ChooseRandomCreepBJ = ChooseRandomCreep
String2UnitIdBJ = UnitId -- I think they just wanted a better name
GetIssuedOrderIdBJ = GetIssuedOrderId
GetKillingUnitBJ = GetKillingUnit
IsUnitHiddenBJ = IsUnitHidden
IssueTrainOrderByIdBJ = IssueImmediateOrderById -- I think they just wanted a better name
IssueUpgradeOrderByIdBJ = IssueImmediateOrderById -- I think they just wanted a better name
GetAttackedUnitBJ = GetTriggerUnit -- I think they just wanted a better name
SetUnitFlyHeightBJ = SetUnitFlyHeight
SetUnitTurnSpeedBJ = SetUnitTurnSpeed
GetUnitDefaultPropWindowBJ = GetUnitDefaultPropWindow
SetUnitBlendTimeBJ = SetUnitBlendTime
SetUnitAcquireRangeBJ = SetUnitAcquireRange
UnitSetCanSleepBJ = UnitAddSleep
UnitCanSleepBJ = UnitCanSleep
UnitWakeUpBJ = UnitWakeUp
UnitIsSleepingBJ = UnitIsSleeping
IsUnitPausedBJ = IsUnitPaused
SetUnitExplodedBJ = SetUnitExploded
GetTransportUnitBJ = GetTransportUnit
GetLoadedUnitBJ = GetLoadedUnit
IsUnitInTransportBJ = IsUnitInTransport
IsUnitLoadedBJ = IsUnitLoaded
IsUnitIllusionBJ = IsUnitIllusion
SetDestructableInvulnerableBJ = SetDestructableInvulnerable
IsDestructableInvulnerableBJ = IsDestructableInvulnerable
SetDestructableMaxLifeBJ = SetDestructableMaxLife
WaygateIsActiveBJ = WaygateIsActive
QueueUnitAnimationBJ = QueueUnitAnimation
SetDestructableAnimationBJ = SetDestructableAnimation
QueueDestructableAnimationBJ = QueueDestructableAnimation
DialogSetMessageBJ = DialogSetMessage
DialogClearBJ = DialogClear
GetClickedButtonBJ = GetClickedButton
GetClickedDialogBJ = GetClickedDialog
DestroyQuestBJ = DestroyQuest
QuestSetTitleBJ = QuestSetTitle
QuestSetDescriptionBJ = QuestSetDescription
QuestSetCompletedBJ = QuestSetCompleted
QuestSetFailedBJ = QuestSetFailed
QuestSetDiscoveredBJ = QuestSetDiscovered
QuestItemSetDescriptionBJ = QuestItemSetDescription
QuestItemSetCompletedBJ = QuestItemSetCompleted
DestroyDefeatConditionBJ = DestroyDefeatCondition
DefeatConditionSetDescriptionBJ = DefeatConditionSetDescription
FlashQuestDialogButtonBJ = FlashQuestDialogButton
DestroyTimerBJ = DestroyTimer
DestroyTimerDialogBJ = DestroyTimerDialog
TimerDialogSetTitleBJ = TimerDialogSetTitle
TimerDialogSetSpeedBJ = TimerDialogSetSpeed
TimerDialogDisplayBJ = TimerDialogDisplay
LeaderboardSetStyleBJ = LeaderboardSetStyle
LeaderboardGetItemCountBJ = LeaderboardGetItemCount
LeaderboardHasPlayerItemBJ = LeaderboardHasPlayerItem
DestroyLeaderboardBJ = DestroyLeaderboard
LeaderboardDisplayBJ = LeaderboardDisplay
LeaderboardSortItemsByPlayerBJ = LeaderboardSortItemsByPlayer
LeaderboardSortItemsByLabelBJ = LeaderboardSortItemsByLabel
PlayerGetLeaderboardBJ = PlayerGetLeaderboard
DestroyMultiboardBJ = DestroyMultiboard
SetTextTagPosUnitBJ = SetTextTagPosUnit
SetTextTagSuspendedBJ = SetTextTagSuspended
SetTextTagPermanentBJ = SetTextTagPermanent
SetTextTagAgeBJ = SetTextTagAge
SetTextTagLifespanBJ = SetTextTagLifespan
SetTextTagFadepointBJ = SetTextTagFadepoint
DestroyTextTagBJ = DestroyTextTag
ForceCinematicSubtitlesBJ = ForceCinematicSubtitles
DisplayCineFilterBJ = DisplayCineFilter
SaveGameCacheBJ = SaveGameCache
FlushGameCacheBJ = FlushGameCache
SaveGameCheckPointBJ = SaveGameCheckpoint
LoadGameBJ = LoadGame
RenameSaveDirectoryBJ = RenameSaveDirectory
RemoveSaveDirectoryBJ = RemoveSaveDirectory
CopySaveGameBJ = CopySaveGame
IssueTargetOrderBJ = IssueTargetOrder
IssueTargetDestructableOrder = IssueTargetOrder -- This was just to type casting
IssueTargetItemOrder = IssueTargetOrder -- This was just to type casting
IssueImmediateOrderBJ = IssueImmediateOrder
GroupTargetOrderBJ = GroupTargetOrder
GroupImmediateOrderBJ = GroupImmediateOrder
GroupTargetDestructableOrder = GroupTargetOrder -- This was just to type casting
GroupTargetItemOrder = GroupTargetOrder -- This was just to type casting
GetDyingDestructable = GetTriggerDestructable -- I think they just wanted a better name
GetAbilityName = GetObjectName -- I think they just wanted a better name
end
OnGlobalInit(function()
Require "AddHook" --https://www.hiveworkshop.com/threads/hook.339153/
Require.optional "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper.339308
--[[
CreateEvent v1.3
CreateEvent is built for GUI support, event linking via coroutines, simple events
(e.g. Heal Event), binary events (like Unit Indexer) or complex event systems such as
Spell Event, Damage Engine and Unit Event.
There are three main functions to keep in mind for the global API:
CreateEvent
-----------
The absolute core, and is really just far too complicated to be able to outline what it
does in this description box. Reading through the code in the comments below, or looking
at the example code, will help to try to get your head around what's going on.
WaitForEvent
------------
Useful only when linking events together, and is used to suspend your running function
until the chosen event is called.
ControlEventRecursion
---------------------
Useful only if wanting recursion for a specific function to be able to exceed a certain
point, but also useful for debugging (as it returns the current depth and max depth).
Due to optional parameters/return values, the API supports a very simple or very
complex environment, depending on how you choose to use it. It is also easily
extensible, so it can be used to support API for other event libraries.
]]
local events={}
local globalRemapCalled, cachedTrigFuncs, globalEventIndex, createEventIndex, recycleEventIndex, globalFuncRef, runEvent
local weakTable={__mode="k"}
local userFuncList=setmetatable({}, weakTable)
---@param eventStr? string - Reserved only for GUI trigger registration
---@param isLinkable? boolean - If true, will use event linking to bind events when possible.
---@param maxEventDepth? integer - Defaults to 1. If 0, events are forbidden to branch off of each other.
---@param customRegister? fun(userFunc:function, continue?:fun(editedFunc:fun(), noHook:boolean), firstReg?:boolean, trigRef?:trigger, opCode?:limitop, priority?:number):function,function
---@return fun(userFunc:function, priority?:number, manualControl?:boolean):nextEvent:function, removeOrPause:fun(pause:boolean) registerEvent
---@return fun(...) runEvent - Run all functions registered to this event. The first parameter should be a unique index, and if the second parameter is a function, it will be executed as a "one time event", without being registered.
---@return function removeEvent - Call this to remove the event completely.
---@return integer eventIndex - If Lua users want to use "WaitForEvent", they need this value in order to know what to wait for.
function CreateEvent(eventStr, isLinkable, maxEventDepth, customRegister)
local thisEvent = createEventIndex() -- each event needs a unique numerical index.
events[thisEvent] = DoNothing -- use a dummy function to enable Hook without having to use it as a default function.
maxEventDepth = maxEventDepth or 1 -- apply the default recusion depth allowance for when none is specified.
--Declare top-level variables needed for use within this particular event.
local removeEventHook, unpauseList, trigRef, opCode
local registerEvent=function(userFunc, priority, disablePausing, manualControl)
local evaluator, wrapper, nextEvent, firstReg
--all data is indexed by the user's function. So, for linked events, the user should pass the
--same function to each of them, to ensure that they can be connected via one harmonious coroutine:
local funcRef=userFuncList[userFunc]
if isLinkable then
local resumer=function(co, ...)
globalFuncRef = funcRef
coroutine.resume(co, ...)
if not manualControl and coroutine.status(co)=="suspended" then
nextEvent(...)
return true
end
end
if funcRef then
wrapper=function(eventIndex, ...)
--completely change the user's function to load and resume a coroutine.
local thread=funcRef[eventIndex]
if thread and thread.waitFor==thisEvent then
return resumer(thread.co, eventIndex, ...)
end
end
else
--It is imperitive that the eventIndex is a unique value for the event, in order to ensure that
--coroutines can persist even with overlapping events that call the same function. A good example
--is a Unit Index Event which runs when a unit is created, and once more when it is removed. The
--Event Index can be a unit, and the "on index" coroutine can simply yield until the "on deindex"
--runs for that exact unit. Another example is simply using a dynamic table for the eventIndex,
--which is what systems like Damage Engine use.
wrapper=function(eventIndex, ...)
--transform calling the user's function into a coroutine
return resumer(coroutine.create(userFunc), eventIndex, ...)
end
firstReg=true
end
else
wrapper=userFunc
if maxEventDepth > 0 then
evaluator = function() globalFuncRef = funcRef end
end
end
if not funcRef then
funcRef=setmetatable({userFunc=userFunc, maxDepth=maxEventDepth}, weakTable)
userFuncList[userFunc]=funcRef
end
local isPaused
if not disablePausing then
local old = evaluator or DoNothing
evaluator = function() return isPaused or old() end
end
evaluator = evaluator or DoNothing
if manualControl then
local old = wrapper
wrapper = function(...)
if not evaluator() then
old(...)
end
end
else
local old = wrapper
wrapper=function(...)
if not evaluator() then
if (not old(...) or not isLinkable) then
nextEvent(...)
end
end
end
end
---Call this function from the custom register to actually register the event.
---This also allows you access to the return values, because your custom register
---needs to return those two functions (wrapped, nilled or unspoiled).
---@param editedFunc? function
---@param noHook? boolean
---@return fun(args_should_match_the_original_function?: any):any nextEvent
---@return fun(pause:boolean, autoUnpause:boolean) removeOrPause
local continue=function(editedFunc, noHook)
local removeUserHook
if noHook then
nextEvent,removeUserHook=DoNothing,DoNothing
else
nextEvent,removeUserHook=AddHook(thisEvent, editedFunc, priority, events) --Hook the processed function to the event.
removeEventHook=removeUserHook --Really only needs to be done once.
end
local enablerFunc
enablerFunc=function(pause, autoUnpause)
if pause==nil then
removeUserHook()
if firstReg and isLinkable then
userFuncList[userFunc]=nil --should the function ever be re-registered, this is key.
end
else
isPaused=pause
if autoUnpause then
--this is the same as the pause/unpause mechanism of Damage Engine 5.
unpauseList=unpauseList or {}
table.insert(unpauseList, enablerFunc)
end
end
end
funcRef.enabler=enablerFunc
return nextEvent, enablerFunc --return the user's 2 control functions.
end
if customRegister then
local a,b,clearRef=customRegister(wrapper, continue, firstReg, trigRef, opCode, priority)
if clearRef then
userFuncList[userFunc]=nil
end
return a,b
else
return continue(wrapper)
end
end
local removeTRVE
if eventStr then
if isLinkable then
--Align the "global.X" syntax accordingly, so that it works properly with Set WaitForEvent = SomeEvent.
pcall(function() globals[eventStr]=thisEvent end) --Have to pcall this, as there is no safe "getter" function to check if the real is indexed to the global to begin with.
if GlobalRemap and not globalRemapCalled then
globalRemapCalled=true
GlobalRemap("udg_WaitForEvent", nil, WaitForEvent)
GlobalRemap("udg_EventIndex", function() return globalEventIndex end)
GlobalRemap("udg_EventRecusion", nil, function(maxDepth)
if globalFuncRef then
globalFuncRef.maxDepth=maxDepth
end
end)
end
end
local oldTRVE
oldTRVE,removeTRVE=AddHook("TriggerRegisterVariableEvent",
function(userTrig, userStr, userOp, userVal) --This hook runs whenever TriggerRegisterVariableEvent is called:
if eventStr==userStr then
local cachedTrigFunc
if cachedTrigFuncs then
cachedTrigFunc=cachedTrigFuncs[userTrig]
else
cachedTrigFuncs=setmetatable({},weakTable)--will only be called once per game.
end
if not cachedTrigFunc then
cachedTrigFunc=function()
if IsTriggerEnabled(userTrig) and TriggerEvaluate(userTrig) then
TriggerExecute(userTrig)
end
end
cachedTrigFuncs[userTrig]=cachedTrigFunc
end
trigRef,opCode=userTrig,userOp
registerEvent(cachedTrigFunc, userVal)
trigRef,opCode=nil,nil
else
return oldTRVE(userTrig, userStr, userOp, userVal)
end
end)
end
local running
return registerEvent,
--Second return value is a function that runs the event when called. Any number of paramters can be specified, but the first should be unique to the event instance you're running.
function(eventIndex, specialExec, ...)
if running then
--rather than going truly recursive, queue the event to be ran after the first event, and wrap up any events queued before this.
local max = globalFuncRef.maxDepth or maxEventDepth
if max>0 then
if running==true then running={} end
table.insert(running, table.pack(eventIndex, specialExec, ...))
local depth = (globalFuncRef.depth or 0) + 1
if depth >= max then
--max recursion has been reached for this function. Pause it and let it be automatically unpaused at the end of the sequence.
globalFuncRef.depth = 0
globalFuncRef.enabler(true, true)
else
globalFuncRef.depth = depth
end
end
else
local oldIndex,oldList=globalEventIndex,globalFuncRef--cache so that different overlapping events don't have a conflict.
running=true
runEvent(events[thisEvent], eventIndex, specialExec, ...)
while running~=true do
--This is the same recursion processing introduced in Damage Engine 5.
local runner=running; running=true
for _,args in ipairs(runner) do
runEvent(events[thisEvent], table.unpack(args, 1, args.n))
end
end
if unpauseList then
--unpause users' functions that were set to be automatically unpaused.
for func in ipairs(unpauseList) do func(false) end
unpauseList=nil
end
running=nil
globalEventIndex,globalFuncRef=oldIndex,oldList--retrieve so that overlapping events don't break.
end
end,
--Third return value is used to remove the event. Usually unused, as most events would be persistent.
function()
if thisEvent~=0 then
if removeEventHook and events[thisEvent]~=DoNothing then
removeEventHook(true)
end
if removeTRVE then
removeTRVE()
end
recycleEventIndex(thisEvent)
events[thisEvent] = nil
thisEvent = 0
end
end,
thisEvent --lastly, return the event ID for Lua users who want to be able to wait for events.
end
do
local eventN=0
local eventR={}
--works indentically to vJass struct index handling
---@return integer
createEventIndex=function()
if #eventR>0 then
return table.remove(eventR, #eventR)
else
eventN=eventN+1
return eventN
end
end
---@param index integer
recycleEventIndex=function(index)
table.insert(eventR, index)
events[index]=nil
end
runEvent = function(eventFunc, eventIndex, specialFunc, ...)
globalEventIndex=eventIndex
eventFunc(eventIndex, ...)
if specialFunc and type(specialFunc)=="function" then
--A special execution was necessary to enable SpellEvent's O(1) function execution based on ability ID.
--Without it, all events would need to run, regardless of if the conditions matches.
specialFunc(eventIndex, ...)
end
end
---@param whichEvent integer
function WaitForEvent(whichEvent)
globalFuncRef[globalEventIndex]={co=coroutine.running(), waitFor=whichEvent}
coroutine.yield()
end
---Raises or lowers the recursion tolerance for the running function.
---The return values are intended for debugging. I expect the automatic recursion handling to be sufficient for most cases.
---@param maxDepth? integer
---@return integer currentDepth
---@return integer maximumDepth
function ControlEventRecursion(maxDepth)
if globalFuncRef then
if maxDepth then
globalFuncRef.maxDepth = maxDepth
end
return globalFuncRef.depth or 0, globalFuncRef.maxDepth or 0
end
return 0, 0
end
end
end)
OnGlobalInit("PreciseWait", function()
local hook = Require.optional "AddHook" --https://www.hiveworkshop.com/threads/hook.339153
local remap = Require.optional "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper-the-future-of-gui.339308/
--Precise Wait v1.4.1.0
--This changes the default functionality of TriggerAddAction, PolledWait
--and (because they don't work with manual coroutines) TriggerSleepAction and SyncSelections.
local _ACTION_PRIORITY = 1 --Specify the hook priority for hooking TriggerAddAction (higher numbers run earlier in the sequence).
local _WAIT_PRIORITY = -2 --The hook priority for TriggerSleepAction/PolledWait
local function wait(duration)
local thread = coroutine.running()
if thread then
local t = CreateTimer()
TimerStart(t, duration, false, function()
DestroyTimer(t)
coroutine.resume(thread)
end)
coroutine.yield(thread)
end
end
if remap then
--This enables GUI to access WaitIndex as a "local" index for their arrays, which allows
--the simplest fully-instanciable data attachment in WarCraft 3's GUI history. However,
--using it as an array index will cause memory leaks over time, unless you also install
--Lua-Infused GUI: https://www.hiveworkshop.com/threads/lua-infused-gui-automatic-group-location-rect-and-force-leak-prevention.317084/
remap("udg_WaitIndex", coroutine.running)
end
if not hook then
hook = function(varName, userFunc)
local old = rawget(_G, varName)
rawset(_G, varName, userFunc)
return old
end
end
hook("PolledWait", wait, _WAIT_PRIORITY)
hook("TriggerSleepAction", wait, _WAIT_PRIORITY)
local oldSync
oldSync = hook("SyncSelections",
function()
local thread = coroutine.running()
if thread then
function SyncSelectionsHelper() --this function gets re-declared each time, so calling it via ExecuteFunc will still reference the correct thread.
oldSync()
coroutine.resume(thread)
end
ExecuteFunc("SyncSelectionsHelper")
coroutine.yield(thread)
end
end)
local oldAdd
oldAdd = hook("TriggerAddAction", function(trig, func)
--Return a function that will actually be added as the triggeraction, which itself wraps the actual function in a coroutine.
return oldAdd(trig, function() coroutine.resume(coroutine.create(func)) end)
end, _ACTION_PRIORITY)
end)
OnGlobalInit(function()
Require "Timed" --https://www.hiveworkshop.com/threads/timed-call-and-echo.339222/
Require "AddHook" --https://www.hiveworkshop.com/threads/hook.339153
Require "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper.339308
Require "RegisterAnyPlayerUnitEvent" --https://www.hiveworkshop.com/threads/collection-gui-repair-kit.317084/
Require "CreateEvent" --https://www.hiveworkshop.com/threads/event-gui-friendly.339451/
Require "PreciseWait" --https://www.hiveworkshop.com/threads/precise-wait-gui-friendly.316960/
--[[
Lua Unit Event 1.2.0.2
In addition to the existing benefits enjoyed over the past years, this Lua version supports linked events that allow
your trigger to Wait until another event runs (meaning you can do attachment and cleanup from one trigger).
Variable names have been completely changed from all prior Unit Event incarnations.
> All real variable event names are now prefixed with OnUnit...
> All array references (unit properties) are now prefixed with UnitEvent_
> Lua users can access a unit's properties via UnitEvent[unit].property (e.g. reincarnating/cargo)
> Lua users can easily add a readonly GUI property to a unit via UnitEvent.addProperty("propertyName")
>>> GUI accesses it via UnitEvent_propertyName, the second is readable and writable within Lua via UnitEvent[unit].propertyName
> UnitUserData (custom value of unit) has been completely removed. This is the first unit event/indexer to not use UnitUserData nor hashtables.
>>> UnitEvent_unit is the subject unit of the event.
>>> UnitEvent_index is an integer in GUI, but points to a the unit.
>>> UnitEvent_setKey lets you assign a unit to the key.
>>> UnitEvent_getKey is an integer in GUI, but points to the unit you assigned as the key.
>>>>> Lua doesn't care about array max sizes, nor the type of information used as an index in that array (because it uses tables and not arrays).
>>>>> GUI is over 20 years old and can easily be fooled. As long as the variable is defined with the correct type, it doesn't care what happens to that variable behind the scenes.
--]]
UnitEvent={}
local _REMOVE_ABIL = FourCC('A001')
local _TRANSFORM_ABIL = FourCC('A002') --be sure to assign these to their respective abilities if you prefer not to initialize via GUI
--Un-comment this next line if you want to use the custom value of units functionality (for backwards compatibility):
--GetUnitUserData = function(unit) return unit end
--[[
Full list of GUI variables:
real udg_OnUnitIndexed
real udg_OnUnitCreation
real udg_OnUnitRemoval
real udg_OnUnitReincarnating
real udg_OnUnitRevival
real udg_OnUnitLoaded
real udg_OnUnitUnloaded
real udg_OnUnitTransform
real udg_OnUnitDeath
real udg_OnUnitActive
real udg_OnUnitPassive
ability udg_DetectRemoveAbility
ability udg_DetectTransformAbility
unit udg_UnitEvent_unit
integer udg_UnitEvent_index
unit udg_UnitEvent_setKey
integer udg_UnitEvent_getKey
boolean array udg_UnitEvent_preplaced
unit array udg_UnitEvent_summoner
unittype array udg_UnitEvent_unitType
boolean array udg_UnitEvent_reincarnating
unit array udg_UnitEvent_transporter
unitgroup array udg_UnitEvent_cargo
--]]
local eventList={}
local udg_prefix="udg_OnUnit"
local makeAPI = function(name)
UnitEvent["on"..name],
eventList["on"..name] = CreateEvent(udg_prefix..name, true)
end
local unitIndices={} ---@type UnitEventTable[]
--onIndexed and onCreation occur at roughly the same time, but the unit's creation should be used instead as it will have more data.
--Combined, they are the counterparts to onRemoval.
makeAPI("Indexed")
makeAPI("Creation")
makeAPI("Removal")
--counterparts (though revival doesn't only come from reincarnation):
makeAPI("Reincarnating")
makeAPI("Revival")
--perfect counterparts:
makeAPI("Loaded")
makeAPI("Unloaded")
--stand-alone events:
makeAPI("Transform")
makeAPI("Death")
--perfect counterparts that generalize all but the "transform" event:
makeAPI("Active")
makeAPI("Passive")
--Used to get the UnitEvent table from the unit to detect UnitEvent-specific properties.
UnitEvent.__index = function(_, unit) return unitIndices[unit] end
---@param name string
UnitEvent.addProperty = function(name)
GlobalRemapArray("udg_UnitEvent_"..name, function(unit) return unitIndices[unit][name] end)
end
---@class UnitEventTable : table
---@field unit unit
---@field preplaced boolean
---@field summoner unit
---@field transporter unit
---@field cargo group
---@field reincarnating boolean
---@field private new boolean
---@field private alive boolean
---@field private unloading boolean
--The below two variables are intended for GUI typecasting, because you can't use a unit as an array index.
--What it does is bend the rules of GUI (which is still bound by strict JASS types) by transforming those
--variables with Global Variable Remapper (which isn't restricted by any types).
--"setKey" is write-only (assigns the key to a unit)
--"getKey" is read-only (retrieves the key and tells GUI that it's an integer, allowing it to be used as an array index)
local lastUnit
GlobalRemap("udg_UnitEvent_setKey", nil, function(unit)lastUnit=unit end) --assign to a unit to unlock the getKey variable.
GlobalRemap("udg_UnitEvent_getKey", function() return lastUnit end) --type is "integer" in GUI but remains a unit in Lua.
local runEvent
do
local eventUnit
local getEventUnit = function() return eventUnit end
runEvent = function(event, unitTable)
local cached = eventUnit
eventUnit = unitTable.unit
eventList[event](unitTable)
eventUnit = cached
end
GlobalRemap("udg_UnitEvent_unit", getEventUnit) --the subject unit for the event.
GlobalRemap("udg_UnitEvent_index", getEventUnit) --fools GUI into thinking unit is an integer
end
--add a bunch of read-only arrays to access GUI data. I've removed the "IsUnitAlive" array as the GUI living checks are fixed with the GUI Enhancer Colleciton.
UnitEvent.addProperty("preplaced")
UnitEvent.addProperty("unitType")
UnitEvent.addProperty("reincarnating")
UnitEvent.addProperty("transporter")
UnitEvent.addProperty("summoner")
if rawget(_G, "udg_UnitEvent_cargo") then
UnitEvent.addProperty("cargo")
end
--Flag a unit as being able to move or attack on its own:
local function setActive(unitTable)
if unitTable and not unitTable.active and UnitAlive(unitTable.unit) then --be sure not to run the event when corpses are created/unloaded.
unitTable.active = true
runEvent("onActive", unitTable)
end
end
---Flag a unit as NOT being able to move or attack on its own:
local function setPassive(unitTable)
if unitTable and unitTable.active then
unitTable.active = nil
runEvent("onPassive", unitTable)
end
end
local function getFunc(active)
return function(unitTable) (active and setActive or setPassive)(unitTable) end
end
UnitEvent.onCreation(getFunc(true), 2, true)
UnitEvent.onUnloaded(getFunc(true), 2, true)
UnitEvent.onRevival(getFunc(true), 2, true)
UnitEvent.onLoaded(getFunc(), 2, true)
UnitEvent.onReincarnating(getFunc(), 2, true)
UnitEvent.onDeath(getFunc(), 2, true)
UnitEvent.onRemoval(getFunc(), 2, true)
--UnitEvent.onIndex(function(unitTable) print(tostring(unitTable.unit).."/"..GetUnitName(unitTable.unit).." has been indexed.") end)
setmetatable(UnitEvent, UnitEvent)
--Wait until GUI triggers and events have been initialized.
OnTrigInit(function()
if rawget(_G, "Trig_Unit_Event_Config_Actions") then
Trig_Unit_Event_Config_Actions()
_REMOVE_ABIL = udg_DetectRemoveAbility or _REMOVE_ABIL
_TRANSFORM_ABIL = udg_DetectTransformAbility or _TRANSFORM_ABIL
end
local function checkAfter(unitTable)
if not unitTable.checking then
unitTable.checking = true
Timed.call(0, function()
unitTable.checking = nil
if unitTable.new then
unitTable.new = nil
runEvent("onCreation", unitTable) --thanks to Spellbound for the idea
elseif unitTable.transforming then
local unit = unitTable.unit
runEvent("onTransform", unitTable)
unitTable.unitType = GetUnitTypeId(unit) --Set this afterward to give the user extra reference
--Reset the transforming flags so that subsequent transformations can be detected.
unitTable.transforming = nil
UnitAddAbility(unit, _TRANSFORM_ABIL)
elseif unitTable.alive then
unitTable.reincarnating = true
unitTable.alive = false
runEvent("onReincarnating", unitTable)
elseif UnitAlive(unitTable.unit) then
unitTable.alive = true
runEvent("onRevival", unitTable)
unitTable.reincarnating = false
end
end)
end
end
local re = CreateRegion()
local r = GetWorldBounds()
local maxX, maxY = GetRectMaxX(r), GetRectMaxY(r)
RegionAddRect(re, r); RemoveRect(r)
local function unloadUnit(unitTable)
local unit, transport = unitTable.unit, unitTable.transporter
GroupRemoveUnit(unitIndices[transport].cargo, unit)
unitTable.unloading = true
runEvent("onUnloaded", unitTable)
unitTable.unloading = nil
if not IsUnitLoaded(unit) or not UnitAlive(transport) or GetUnitTypeId(transport) == 0 then
unitTable.transporter = nil
end
end
local preplaced = true
local onEnter = Filter(
function()
local unit = GetFilterUnit()
local unitTable = unitIndices[unit]
if not unitTable then
unitTable = {
unit = unit,
new = true,
alive = true,
unitType= GetUnitTypeId(unit)
}
UnitAddAbility(unit, _REMOVE_ABIL)
UnitMakeAbilityPermanent(unit, true, _REMOVE_ABIL)
UnitAddAbility(unit, _TRANSFORM_ABIL)
unitIndices[unit] = unitTable
unitTable.preplaced = preplaced
runEvent("onIndexed", unitTable)
checkAfter(unitTable)
elseif unitTable.transporter and not IsUnitLoaded(unit) then
--the unit was dead, but has re-entered the map (e.g. unloaded from meat wagon)
unloadUnit(unitTable)
end
end)
TriggerRegisterEnterRegion(CreateTrigger(), re, onEnter)
RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_LOADED,
function()
local unit = GetTriggerUnit()
local unitTable = unitIndices[unit]
if unitTable then
if unitTable.transporter then
unloadUnit(unitTable)
end
--Loaded corpses do not issue an order when unloaded, therefore must
--use the enter-region event method taken from Jesus4Lyf's Transport: https://www.thehelper.net/threads/transport-enter-leave-detection.126051/
if not unitTable.alive then
SetUnitX(unit, maxX)
SetUnitY(unit, maxY)
end
local transporter = GetTransportUnit()
unitTable.transporter = transporter
local g = unitIndices[transporter].cargo
if not g then
g=CreateGroup()
unitIndices[transporter].cargo = g
end
GroupAddUnit(g, unit)
runEvent("onLoaded", unitTable)
end
end)
RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_DEATH,
function()
local unitTable = unitIndices[GetTriggerUnit()]
if unitTable then
unitTable.alive = false
runEvent("onDeath", unitTable)
if unitTable.transporter then
unloadUnit(unitTable)
end
end
end)
RegisterAnyPlayerUnitEvent(EVENT_PLAYER_UNIT_SUMMON,
function()
local unitTable = unitIndices[GetTriggerUnit()]
if unitTable.new then
unitTable.summoner = GetSummoningUnit()
end
end)
local orderB = Filter(
function()
local unit = GetFilterUnit()
local unitTable = unitIndices[unit]
if unitTable then
if GetUnitAbilityLevel(unit, _REMOVE_ABIL) == 0 then
runEvent("onRemoval", unitTable)
unitIndices[unit] = nil
unitTable.cargo = nil
elseif not unitTable.alive then
if UnitAlive(unit) then
checkAfter(unitTable)
end
elseif not UnitAlive(unit) then
if unitTable.new then
--This unit was created as a corpse.
unitTable.alive = false
runEvent("onDeath", unitTable)
elseif not unitTable.transporter or not IsUnitType(unit, UNIT_TYPE_HERO) then
--The unit may have just started reincarnating.
checkAfter(unitTable)
end
elseif GetUnitAbilityLevel(unit, _TRANSFORM_ABIL) == 0 and not unitTable.transforming then
unitTable.transforming = true
checkAfter(unitTable)
end
if unitTable.transporter and not unitTable.unloading and not (IsUnitLoaded(unit) and UnitAlive(unit)) then
unloadUnit(unitTable)
end
end
end)
local p
local order = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
p = Player(i)
GroupEnumUnitsOfPlayer(bj_lastCreatedGroup, p, onEnter)
SetPlayerAbilityAvailable(p, _REMOVE_ABIL, false)
SetPlayerAbilityAvailable(p, _TRANSFORM_ABIL, false)
TriggerRegisterPlayerUnitEvent(order, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, orderB)
end
preplaced = false
end)
end)
OnMapInit(function()
local deadItemStack = {}
local checkForDeadItems = function()
local item = GetEnumItem()
if GetWidgetLife(item) == 0 then
table.insert(deadItemStack, item)
end
end
TimerStart(CreateTimer(), 15, true, function()
for i=#deadItemStack, 1, -1 do
SetWidgetLife(deadItemStack[i], 1)
RemoveItem(deadItemStack[i])
deadItemStack[i] = nil
end
EnumItemsInRect(bj_mapInitialPlayableArea, nil, checkForDeadItems)
end)
end)
--[[
Action v1.3.0.0 by Bribe
What it does:
1) Allows GUI to declare its own functions to be passed to a Lua system.
2) Automatic variable localization and cleanup for effects.
3) Automatic variable localization for units, reals, integers, groups and locations.
4) Recursive local tracking (as long as array indices are not shadowed).
Why it can benefit:
1) Allows you to have all of your functions in one GUI trigger
2) Each trigger and each sub-action within the trigger has access to each others' data.
3) No need to create the same variables for each new trigger if you use the variables provided here.
How it works:
1) In some cases replaces ForForce, allowing you to manipulate the callback function instead.
2) Attaches data via coroutines, allowing all locals powered by this system to be local to the running instance of the trigger, regardless of how many times it waits.
3) To destroy (for example) a unit group: Set DestroyGroup = TempGroup1
--]]
OnGlobalInit(function()
Require "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper.339308
Require "PreciseWait" --https://www.hiveworkshop.com/threads/precise-wait-gui-friendly.316960/
Action={}
local systemPrefix="udg_Action_"
local actions={}
local cleanTracker
local topIndex=__jarray()
local oldForForce,lastIndex
oldForForce=AddHook("ForForce", function(whichForce, whichFunc)
if actions[whichForce] then
local index=lastIndex
lastIndex=nil
actions[whichForce](whichFunc, index)
else
oldForForce(whichForce, whichFunc)
end
end)
---@param whichVar string --The name of the user-defined global. It will add the udg_ prefix if you don't feel like adding it yourself.
---@param onForForce fun(function) --Takes the GUI function passed to ForForce and allows you to do whatever you want with it instead.
---@param isArray? boolean --If true, will pass the array index to the onForForce callback.
function Action.create(whichVar, onForForce, isArray)
if whichVar:sub(1,4)~="udg_" then
whichVar="udg_"..whichVar
end
local force = _G[whichVar]
if isArray then
if force then
GlobalRemapArray(whichVar, function(index)
lastIndex=index
return whichVar
end)
end
else
if force then
GlobalRemap(whichVar, function()
lastIndex=nil
return whichVar
end)
end
end
actions[whichVar]=onForForce
end
local durations=__jarray()
local getCoroutine=coroutine.running
Action.create(systemPrefix.."forDuration", function(func)
local co = getCoroutine()
if durations[co] then
while durations[co] > 0 do
func()
end
end
end)
GlobalRemap(systemPrefix.."duration", function() return durations[getCoroutine()] end, function(val) durations[getCoroutine()] = val end)
--Look at this: Every time a trigger runs, it can have its own fully-fledged, perfectly MUI hashtable, without having to initialize or destroy it manually.
local hash = __jarray()
GlobalRemap(systemPrefix.."hash", function()
local parent = topIndex[getCoroutine()]
hash[parent] = hash[parent] or __jarray()
return hash[parent]
end)
---Nearly the same as Precise PolledWait, with the exception that it tracks the duration.
---@param duration real
function Action.wait(duration)
local co = getCoroutine()
if co then
local t = CreateTimer()
TimerStart(t, duration, false, function()
DestroyTimer(t)
if durations[co] then
durations[co] = durations[co] - duration
end
coroutine.resume(co)
end)
coroutine.yield(co)
end
end
GlobalRemap(systemPrefix.."wait", nil, Action.wait)
local function cleanup(co)
if cleanTracker[co] then
for _,obj in ipairs(cleanTracker[co]) do
DestroyEffect(obj)
end
end
end
---Allows the function to be run as a coroutine a certain number of times (determined by the array index)
---Most importantly, the calling function does not wait for these coroutines to complete (just like in GUI when you execute a trigger from another trigger).
---@param func function
---@param count number
Action.create(systemPrefix.."loop", function(func, count)
local top = getCoroutine()
for _=1,count do
local co
co = coroutine.create(function()
func()
cleanup(co)
end)
topIndex[co]=top
coroutine.resume(co)
end
end, true)
local oldAction
oldAction = AddHook("TriggerAddAction", function(trig, func)
return oldAction(trig, function()
local co = getCoroutine()
topIndex[co]=co
func()
cleanup(co)
end)
end, 2)
GlobalRemap(systemPrefix.."index", function() return topIndex[getCoroutine()] end)
for _,name in ipairs {
"point",
"group",
"effect",
"integer",
"real",
"unit"
} do
local tracker=__jarray()
local varName=systemPrefix..name
if name=="effect" then
cleanTracker=tracker
end
GlobalRemapArray(varName, function(index)
local co = getCoroutine()
::start::
local result = rawget(tracker[co], index) --check if the value is already assigned at the level of the current coroutine
if not result and topIndex[co]~=co then
--If not, hunt around and try to find if any calling coroutines have the index assigned.
co = topIndex[co]
goto start
end
return result
end,
function(index, val)
local tb = tracker[getCoroutine()]
if not tb then
tb={}
tracker[getCoroutine()]=tb
elseif name=="effect" and tb[index] then
DestroyEffect(tb[index])
end
tb[index]=val
end)
end
end)
--[[
GroupInRange v1.2 by Bribe
What it does:
1) Collects all units in range of a point and adds them to a leakless group (GroupInRange_units)
2) Automate exclusion of common unit classifications (e.g. spell immune/allies)
3) Allows custom filters to be built in real-time.
4) Provides GroupInRange to Lua users, which allows persistent filters without having to rebuild them from scratch each time.
Why it can benefit:
1) You no longer have to think about whether your group will leak, as this does not.
2) Shorter triggers and easier configuration of filters.
How it works:
1) Using Action, it allows you to specify custom filter definitions within the Actions block of GroupInRange_filter.
2) Uses the same mechanics that were introduced in GUI Spell System, but built them universally for the sake of modularity (e.g. can be used with damage events).
--]]
OnGlobalInit(function()
Require "Action" --https://www.hiveworkshop.com/threads/action-the-future-of-gui-actions.341866/
--[[
GUI Variables:
player group udg_GroupInRange_filter
--MANDATORY variables to set:
real udg_GroupInRange_radius
point udg_GroupInRange_point
optional boolean filters default:
udg_GroupInRange_allow_self false
udg_GroupInRange_allow_allies false
udg_GroupInRange_allow_enemies true > self, allies and enmies can only be determined if a source unit is specified.
udg_GroupInRange_allow_heroes true
udg_GroupInRange_allow_nonHeroes true > By default, all heroes and non-heroes are included.
udg_GroupInRange_allow_living true
udg_GroupInRange_allow_dead false > By default, only living units are included.
udg_GroupInRange_allow_flying false
udg_GroupInRange_allow_magicImmune false
udg_GroupInRange_allow_mechanical false
udg_GroupInRange_allow_structures false > By default, these are excluded.
--Optional setonly variables for extra filtering:
integer udg_GroupInRange_max > Do not pick any more units than this number (excludes units at random).
unit udg_GroupInRange_source > if specified, enables filtering for enemies and allies.
unit group udg_GroupInRange_exclude > Do not add any units from this group.
--Readonly
unit group udg_GroupInRange_units > The group of units who passed the filters.
intger udg_GroupInRange_count > Number of units in that group
If you want to save the units for a longer duration, assign a group variable to (Last created unit group) as this will create a copy for you to use.
--]]
local defaultFilter = {
allies = false,
enemies = true,
heroes = true,
nonHeroes = true,
living = true,
dead = false,
flying = false,
magicImmune = false,
mechanical = false,
structures = false,
self = false,
}
local enumGroup=CreateGroup()
local getCompare,udgFilter
local variable=function(name, getter, setter)
GlobalRemap("udg_GroupInRange_"..name, getter, setter)
end
for name in pairs(defaultFilter) do
variable("allow_"..name, nil, function(val) udgFilter[name]=val end)
end
for _,name in ipairs{"exclude","point","radius","max","source"} do
variable(name, nil, function(val) udgFilter[name]=val end)
end
variable("units", function() return enumGroup end)
variable("count", function() return udgFilter.count end)
---@param is boolean
---@param yes string
---@param no string
---@return boolean|nil
local compare=function(is, yes, no)
return ( is and getCompare(yes) --or print("GroupInRange Failed at Yes: "..yes.." boolean is: "..tostring(is))
) or (not is and getCompare(no) --or print("GroupInRange Failed at No: " ..no.. " boolean is: "..tostring(is))
)
end
function GroupInRange(args, filter)
local x,y
if args.point then
x,y=args.point[1],args.point[2]
elseif args.x then
x,y=args.x,args.y
else
print"GroupInRange Error: No location or coordinates were provided!"
return
end
filter=filter or defaultFilter
getCompare=function(name)
if filter[name]==nil then
--print("GroupInRange default filter used for: "..name)
return defaultFilter[name]
end
--print("GroupInRange filter is set for: "..name.." to: "..tostring(filter[name]))
return filter[name]
end
local source = filter.source
local owner = source and GetOwningPlayer(source)
GUI.enumUnitsInRange(function(unit)
if IsUnitInRangeXY(unit, x, y, args.radius) --and incDebug() --index 1
and (not filter.exclude or not IsUnitInGroup(unit, filter.exclude)) --and incDebug() --index 2
and ((filter.heroes~=false
and filter.nonHeroes~=false)
or compare(IsUnitType(unit, UNIT_TYPE_HERO), "heroes", "nonHeroes")) --and incDebug() --index 3
and (not owner or compare(IsUnitEnemy(unit, owner), "enemies","allies")) --and incDebug() --index 4
and compare(UnitAlive(unit), "living", "dead") --and incDebug() --index 5
and (filter.flying or not IsUnitType(unit, UNIT_TYPE_FLYING)) --and incDebug() --index 6
and (filter.structures or not IsUnitType(unit, UNIT_TYPE_STRUCTURE)) --and incDebug() --index 7
and (filter.mechanical or not IsUnitType(unit, UNIT_TYPE_MECHANICAL)) --and incDebug() --index 8
and (filter.magicImmune or not IsUnitType(unit, UNIT_TYPE_MAGIC_IMMUNE)) --and incDebug() --index 9
then
GroupAddUnit(enumGroup, unit)
end
end, x, y, args.radius + (filter.structures and 197 or 64), nil)
if source then
if not filter.self and IsUnitInGroup(source, enumGroup) then
GroupRemoveUnit(enumGroup, source)
end
owner=GetOwningPlayer(source)
end
local inGroup=#enumGroup
if filter.max then
while inGroup > filter.max do
GroupRemoveUnit(enumGroup, GroupPickRandomUnit(enumGroup))
inGroup=inGroup-1
end
end
--print("inGroup:"..inGroup)
filter.count=inGroup
end
Action.create("udg_GroupInRange_filter", function(func)
udgFilter={}
func()
GroupInRange(udgFilter,udgFilter)
--try(GroupInRange,udgFilter,udgFilter)
end)
end)
--Lua Spell Event v1.0 beta
OnGlobalInit(function()
Require "GlobalRemap" --https://www.hiveworkshop.com/threads/global-variable-remapper.339308
Require "RegisterAnyPlayerUnitEvent" --https://www.hiveworkshop.com/threads/collection-gui-repair-kit.317084/
Require "CreateEvent" --https://www.hiveworkshop.com/threads/event-gui-friendly.339451/
Require "Action" --https://www.hiveworkshop.com/threads/action-the-future-of-gui-actions.341866/
Require "PreciseWait" --https://www.hiveworkshop.com/threads/precise-wait-gui-friendly.316960/
SpellEvent={}
local _AUTO_ORDER = "spellsteal" --If TriggerRegisterCommandEvent is called and this order is specified,
--ignore the actual request and instead allow it to be treated as an aiblity to be registered by Spell System.
--In GUI, this event looks like: Game - Button for ability Animate Dead and order Human Spellbreaker - Spell Steal pressed.
--If you actually WANT to use this order with this event, you could assign this to a different order (e.g. "battleroar").
--If you want to be using ALL of these, then I recommend waiting until OnGameStart to call your own TriggerRegisterCommandEvent.
--[=========================================================================================[
Required GUI:
Events work differently, and it's now allowed to create a spell without being forced into using a separate Config trigger.
real udg_OnSpellChannel
real udg_OnSpellCast
real udg_OnSpellEffect
real udg_OnSpellFinish
The below preserve the API from the vJass version:
unit udg_Spell__Caster -> When set, will assign Spell__Index to whatever the last spell this unit cast was.
player udg_Spell__CasterOwner
location udg_Spell__CastPoint
location udg_Spell__TargetPoint
unit udg_Spell__Target
integer udg_Spell__Index -> Now is a Lua table behind the scenes.
integer udg_Spell__Level
real udg_Spell__LevelMultiplier
abilcode udg_Spell__Ability
boolean udg_Spell__Completed
boolean udg_Spell__Channeling
real udg_Spell__Duration
Thanks to Lua, the above variables are read-only, as intended.
New to Lua:
real udg_Spell__wait -> Replaces Spell__Time. Use this instead of a regular wait to preserve event data after the wait.
integer udg_Spell__whileChannel -> The loop will continue up until the point where the caster stops channeling the spell.
integer udg_Spell__forDuration -> Set Spell__Duration before entering the loop, then the loop will continue for the duration.
string udg_Spell__abilcode -> Useful for debugging purposes.
All of the other variables will be deprecated; possibly at some future point split into separate systems.
--]=========================================================================================]
local eventSpells,trigAbilMap = {},{}
local oldCommand, remove
SpellEvent.__index = function(_,unit) return eventSpells[unit] end
local eventSpell
SpellEvent.addProperty = function(name, getter, setter)
getter = getter or function() return eventSpell[name] end
GlobalRemap("udg_Spell__"..name, getter, setter)
end
SpellEvent.addProperty("Index", function() return eventSpell end)
SpellEvent.addProperty("Caster", nil, function(unit) eventSpell = eventSpells[unit] end)
SpellEvent.addProperty("Ability")
SpellEvent.addProperty("Target")
SpellEvent.addProperty("CasterOwner")
SpellEvent.addProperty("Completed")
SpellEvent.addProperty("Channeling")
do
local getLevel = function() return eventSpell.Level end
SpellEvent.addProperty("Level", getLevel)
SpellEvent.addProperty("LevelMultiplier", getLevel)
end
do
local castPt, targPt = {},{}
SpellEvent.addProperty("CastPoint", function()
castPt[1]=GetUnitX(eventSpell.Caster)
castPt[2]=GetUnitY(eventSpell.Caster)
return castPt
end)
SpellEvent.addProperty("TargetPoint", function()
if eventSpell.Target then
targPt[1]=GetUnitX(eventSpell.Target)
targPt[2]=GetUnitY(eventSpell.Target)
else
targPt[1]=eventSpell.x
targPt[2]=eventSpell.y
end
return targPt
end)
end
GlobalRemap("udg_Spell__wait", nil, function(duration)
local spell = eventSpell
Action.wait(duration)
eventSpell = spell --it's really this simple! No more crazy data rerieval with loads of GUI variables to reset.
end)
Action.create("udg_Spell__whileChannel", function(func)
while eventSpell.Channeling do
func()
end
end)
GlobalRemap("udg_Spell__abilcode", function()
if eventSpell then
return BlzFourCC2S(eventSpell.Ability)
end
return "nil"
end)
local eventList = {}
local coreFunc = function()
local caster = GetTriggerUnit()
local ability = GetSpellAbilityId()
local whichEvent = GetTriggerEventId()
local cache = eventSpell
eventSpell = eventSpells[caster]
if not eventSpell or not eventSpell.Channeling then
eventSpell = {
Caster = caster,
Ability = ability,
Level = GetUnitAbilityLevel(caster, ability),
CasterOwner = GetTriggerPlayer(),
Target = GetSpellTargetUnit(),
x = GetSpellTargetX(),
y = GetSpellTargetY(),
Channeling = true
}
eventSpells[caster] = eventSpell
if whichEvent == EVENT_PLAYER_UNIT_SPELL_CHANNEL then
eventList.onChannel(eventSpell)
else --whichEvent == EVENT_PLAYER_UNIT_SPELL_EFFECT
eventSpell.Channeling = false
eventList.onEffect(eventSpell) --In the case of Charge Gold and Lumber, only an OnEffect event will run.
end
elseif whichEvent == EVENT_PLAYER_UNIT_SPELL_CAST then
eventList.onCast(eventSpell)
elseif whichEvent == EVENT_PLAYER_UNIT_SPELL_EFFECT then
eventList.onEffect(eventSpell)
elseif whichEvent == EVENT_PLAYER_UNIT_SPELL_FINISH then
eventSpell.Completed = true
else --whichEvent == EVENT_PLAYER_UNIT_SPELL_ENDCAST
eventSpell.Channeling = false
eventList.onFinish(eventSpell)
end
eventSpell=cache
end
--Fairly complicated, but if a user registers multiple abilities to one event AND any of them were
--already registered by another trigger, we have to do this.
local valid
local getCompare = function(func, abil)
valid = valid or {}
local check = valid[func]
if not check then
check = {}
valid[func] = check
local old = func
func = function(spell)
if check[spell.Ability] then
old(spell)
end
end
end
check[abil] = true
return func
end
local makeAPI = function(name)
local reg, run, cachedAbil
local abils = {}
reg, run = CreateEvent("udg_OnSpell"..name, true, 1, function(func, continue, firstReg, trigRef)
local exit
if firstReg then
if trigRef then
for i=#trigAbilMap, 1, -1 do
local map = trigAbilMap[i]
if map[1]==trigRef then
local abil = map[2]
if abils[abil] then
--Multiple functions want access to the same spell. Shouldn't happen very often, but if it does,
--any subsequent registration will simply check the spell ID as a condition.
func = getCompare(func, abil)
else
exit=true
abils[abil]=func
end
else
trigAbilMap={}
break
end
end
elseif cachedAbil then
if abils[cachedAbil] then
func = getCompare(func, cachedAbil)
else
abils[cachedAbil] = func
exit = true
end
cachedAbil=nil
end
end
return continue(func, exit)
end)
SpellEvent["on"..name] = function(abil, ...)
cachedAbil=abil
reg(...)
cachedAbil=nil
end
eventList["on"..name] = function(spell)
--print("running "..name)
run(spell, abils[spell.Ability])
end
end
local spellStrs = {"Channel","Cast","Effect","Finish","Endcast"}
for _,name in ipairs(spellStrs) do
if name~="Endcast" then
makeAPI(name)
end
RegisterAnyPlayerUnitEvent(_G["EVENT_PLAYER_UNIT_SPELL_"..string.upper(name)], coreFunc)
end
oldCommand, remove = AddHook("TriggerRegisterCommandEvent", function(whichTrig, whichAbil, whichOrder)
if whichOrder==_AUTO_ORDER then
if trigAbilMap[1] and whichTrig ~= trigAbilMap[1][1] then
trigAbilMap={}
end
table.insert(trigAbilMap, {whichTrig, whichAbil})
else
oldCommand(whichTrig, whichAbil, whichOrder) --normal use of this event has been requested.
end
end)
OnMapInit(remove) --remove the hook once the map initialization triggers have run.
setmetatable(SpellEvent, SpellEvent)
end)
try(function()
function bob()
local function dude()
local i
i[2] = "bob"
end
dude()
end
--bob()
error("generic error message")
do
local function reallyHiddenFailingFunction()
-- this will throw an error in 1.32.10 Lua
-- however if you call ("str", nil) then it'll fail silently
QuestSetEnabledBJ("ignored", "causesErr")
end
function failingFunction()
reallyFailingFunction()
end
function reallyFailingFunction()
reallyHiddenFailingFunction()
end
end
--failingFunction()
end)
--OnLibraryInit tests
OnGlobalInit("Yuna", function()
Require "Tidus"
Require.optional "Kimahri"
print "Yuna has arrived, thanks to Tidus, but without Kimahri."
end)
OnGlobalInit("Tidus", function()
Require "Jecht"
print "Tidus has arrived, thanks to Jecht."
end)
OnGlobalInit(function()
Require "Spira"
print "Spira does not exist, so this library will never run."
end)
OnGlobalInit("Jecht", function()
Require.optional "Sin"
print "Jecht has arrived, without Sin."
end)
OnGlobalInit("Wakka", function()
Require.optional "Lulu"
print "This will be called after all other OnGlobalInits, because Lulu optionally requires Wakka."
end)
OnGlobalInit("Lulu", function()
Require.optional "Wakka"
print "This will be called after all other OnGlobalInits, because Wakka optionally requires Lulu."
end)
OnGlobalInit(function()
print "This is a basic initializer with no requirements."
end)
OnGlobalInit(function()
Require.optional "Zanarkand"
print "This initializer optionally requires an a nonexistent requirement. It will not wait for Zanarkand."
end)
OnTrigInit(function()
print "This runs last, because its initializer runs later in the sequence."
end)
OnGlobalInit(function()
Require "CreateEvent"
local register,run,remove,id=CreateEvent(nil, 0)
local register2,run2,remove2,id2=CreateEvent(nil, id)
local func1,func2
func1=function()
print "first function"
WaitForEvent(id2)
print "first function pt 2"
end
func2=function()
print "second function"
WaitForEvent(id2)
print "second function pt 2"
end
register(func1)
register(func2)
register2(func1)
register2(func2)
run("main seq")
print("====first event ran====")
run2("main seq")
print("====both events ran====")
run("second seq")
--Prints:
--[[
second function
first function
====first event ran====
second function pt 2
first function pt 2
====both events ran====
second function
first function
]]
end)
OnGlobalInit(function()
Require "CreateEvent"
local event1={CreateEvent("udg_BasicEvent1", 0)} --BasicEvent1 is the first in a chain of events
local event2={CreateEvent("udg_BasicEvent2", "udg_BasicEvent1")} --assign BasicEvent2 as the next event in the sequence starting with BasicEvent1
local event3={CreateEvent("udg_BasicEvent3", "udg_BasicEvent2")} --assign BasicEvent3 as the next event in the sequence following BasicEvent2
OnGameStart(function()
local eventId1={}
local eventId2={}
local eventId3={}
event1[2](eventId1)
event1[2](eventId2)
event1[2](eventId3)
event2[2](eventId2)
event2[2](eventId1)
event2[2](eventId3)
event3[2](eventId3)
event3[2](eventId2)
event3[2](eventId1)
end)
end)