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

[Lua] Global Variable Remapper [The Future of GUI]

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
Global Variable Remapper creates a new frontier for GUI interaction by providing a way to override normal "udg_" variables to instead run code when referenced or set.


Example 1 - Override "udg_RemovePoint" to remove any location that is assigned to it (actually, this pre-dates Lua-Infused GUI, which is - by far - the best memory leak solution ever created for GUI):

Lua:
GlobalRemap("udg_RemovePoint", nil, function(loc) RemoveLocation(loc) end)
  • SomeTrig
    • Events
    • Conditions
    • Actions
      • Set TempPoint = (Point(0.00, 0.00))
      • Set RemovePoint = TempPoint -------- Invokes the Lua code and calls RemoveLocation on "TempPoint"
Example 2 - Allow a variable to be referenced, but prevent it from being set:

Lua:
do
    local internalVar = "static"
    GlobalRemap("udg_ReadonlyVar", function() return internalVar end, nil)
end

  • SomeTrig
    • Events
    • Conditions
    • Actions
      • Game - Display to (All players) the text: (ReadonlyVar)
      • -------- The above will display "static" --------
      • Set Readonly = "I have the power"
      • Game - Display to (All players) the text: (ReadonlyVar)
      • -------- The above will STILL display "static" --------


Lua:
if Debug then Debug.beginFile 'GlobalRemap' end
--[[
Global Variable Remapper v1.4 by Bribe

Turns normal GUI variables into function calls that integrate seamlessly with Lua.
--]]
local oldInit = InitGlobals
function InitGlobals()
    oldInit()

    local printError = Debug and Debug.throwError or print

    ---@param name string
    ---@param error string
    local function softErrorHandler(name, error)
        printError('!!!GlobalRemap Error!!! "' .. name .. '" ' .. error)
    end

    if not Hook then --https://github.com/BribeFromTheHive/Lua/blob/master/Hook.lua
        softErrorHandler('Hook', 'is required but not found.')
        return
    end

    local default = DoNothing
    local getters = {}
    local setters = {}

    ---@param hook Hook.property
    ---@param _G table
    ---@param key string
    ---@return unknown
    local function __index(hook, _G, key)
        if getters[key]~=nil then
            return getters[key]()
        end
        return hook.next(_G, key)
    end

    ---@param hook Hook.property
    ---@param _G table
    ---@param key string
    ---@param val unknown
    local function __newindex(hook, _G, key, val)
        if setters[key]~=nil then
            setters[key](val)
        else
            hook.next(_G, key, val)
        end
    end

    Hook.add('__index',    __index,    0, _G, rawget)
    Hook.add('__newindex', __newindex, 0, _G, rawset)

    ---Remap a global variable (non-array) to call getFunc when referenced or setFunc when assigned.
    ---This serves as a permanent hook and cannot be removed.
    --
    ---@param variableStr string            # a string such as "udg_MyVariable"
    ---@param getFunc? fun(): unknown        # a function that takes nothing but returns the expected value when `udg_MyVariable` is referenced.
    ---@param setFunc? fun(value: unknown)   # 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, so it even allows read-only GUI variables.
    function GlobalRemap(variableStr, getFunc, setFunc)
        if getters[variableStr] then
            softErrorHandler(variableStr, 'has been remapped twice. There can only be one remap per variable.')
            return
        end
        _G[variableStr] = nil                     --Delete the variable from the global table.
        getters[variableStr] = getFunc or default --Assign a function that returns what should be returned when this variable is referenced.
        setters[variableStr] = setFunc or default --Assign a function that captures the value the variable is attempting to be set to.
    end

    ---This function allows you to override the behavior of a global array variable.
    ---
    ---You can provide custom getter and setter functions that will be called whenever the variable is accessed or modified.
    ---
    ---If 'preserveState' is 'true', the original array is preserved and passed to the getter and setter functions as an extra parameter.
    ---This is particularly useful when multiple resources want to remap the same array. As long as the resource that called it
    ---most recently handles the previous state correctly, multiple remappings can coexist without conflict.
    ---
    ---Like GlobalRemap, this hook cannot be reversed.
    ---
    ---@param variableStr string                                         # The name of the global array variable you want to remap, such as "udg_MyVariableArray".
    ---@param getFunc fun(index: unknown, state?: table): unknown        # A function that takes the index of the array and a table representing the current state of the variable.
    ---@param setFunc? fun(index: unknown, value: unknown, state?: table) # A function that takes the index of the array, the value that is being assigned to the variable, and a table representing the current state of the variable.
    ---@param preserveState? true                                        # If not provided, the state passed to the callback functions will simply be 'nil'
    function GlobalRemapArray(variableStr, getFunc, setFunc, preserveState)
        getFunc = getFunc or default
        setFunc = setFunc or default

        local state = _G[variableStr]
        if type(state) ~= 'table' then
            softErrorHandler(variableStr, 'is an invalid array to remap. Its type must be "table" but is instead "' .. type(state) .. '".')
            return
        end
        state = preserveState and state

        _G[variableStr] = setmetatable({}, {
            __index = function(_, index)
                return getFunc(index, state)
            end,
            __newindex = function(_, index, val)
                setFunc(index, val, state)
            end
        })
    end
end

if Debug then Debug.endFile() end

Changes from previous version

Unit Tests (can be run in ZeroBrane Studio)
 
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
That's interesting. This basically allows you to have Globals as properties.

Could you do automatic leak cleaning with this?
Instead of removing udg_RemovePoint when it is assigned, could you remove the previous value?
Something like this:
Lua:
_TempPoint = nil
GlobalRemap("udg_TempPoint", function() return _TempPoint end, function(loc)
    if _TempPoint then
        RemoveLocation(_TempPoint)
    end
    _TempPoint = loc
end)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,456
That's interesting. This basically allows you to have Globals as properties.

Could you do automatic leak cleaning with this?
Instead of removing udg_RemovePoint when it is assigned, could you remove the previous value?
Something like this:
Lua:
_TempPoint = nil
GlobalRemap("udg_TempPoint", function() return _TempPoint end, function(loc)
    if _TempPoint then
        RemoveLocation(_TempPoint)
    end
    _TempPoint = loc
end)

Wow, yes, that would absolutely work perfectly in fact, and is a much more practical use than my example was. Nice work!
 
Level 38
Joined
Feb 27, 2007
Messages
4,951
This thread having only 1 actual reply blows my mind. This Global Variable Remapper is an immensely valuable resource that should change how people use GUI completely! Holy moly. I'm only aware of this because you specifically mentioned it in another post, Bribe. People need to see this. Tutorials should be made??

T H E _ F U T U R E _ I S _ H E R E !

The only downside to this is that it requires the map code to be in Lua but the WE still defaults to JASS (unless that's only in the non-Reforged editor I have), so any users with existing projects would have to convert or leave behind any JASS code within their triggers. Probably a pain in the ass.
 
Last edited:
Top