• 🏆 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] vJass2Lua Runtime Plugin

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
vJass, meet Lua. This resource will allow you to use syntax very similar to vJass, including create/allocate, destroy/deallocate, extending/delegating structs, modules, stub methods and much more.

This resource is featured in my tutorial [Lua] How to switch from vJass to Lua (structs, methods, modules and more)

Since its inception, this has grown substantially in size and purpose. It is now the lynchpin behind enabling vJass2Lua.

Support for "onInit" and "initializer FuncName" were also introduced in version 1.3, provided that you also have GlobalInitialization in the map.

Support for method operators has been introduced in version 1.4 (includes all varieties except for the comparing operator, which I doubt anyone had ever used).

2.0 changelog:

Support for textmacros with parameters, Hook 5.0, vJass 2D arrays has been added.

Struct.module/module.implement have been changed to vJass.module/vJass.implement. Parameters are now reworked.

Version 2.0 changes the name from vJass-style-struct to vJass2Lua Runtime Plugin

Lua:
do vJass, Struct = {}, {} --vJass2Lua runtime plugin, version 2.0.1.0 by Bribe
    
    --Requires optional Global Initialization: https://www.hiveworkshop.com/threads/global-initialization.317099/
    
    --In case vJass2Lua misses any string concatenation, this metatable hook will pick it up and correct it.
    getmetatable("").__add = function(obj, obj2) return obj .. obj2 end
    ---@class vJass : table
    ---@field interface function
    ---@field module function
    ---@field implement function
    ---@field textmacro function
    ---@field runtextmacro function
    ---@field struct Struct
    ---Constructor: vJass.struct([optional_parent_struct]) or just Struct()
    ---@class Struct : table
    ---@field allocate fun(from_struct)
    ---@field deallocate fun(struct_instance)
    ---@field create fun(from_struct)
    ---@field destroy fun(struct_instance)
    ---@field onDestroy fun(struct_instance)
    ---@field onCreate fun(struct_instance)
    ---@field extends fun(other_struct_or_table)
    ---@field private inception fun(child_struct)
    ---@field super Struct
    ---@field isType fun(struct_to_compare, other_struct)
    ---@field environment fun(struct_to_iterate)
    ---@field _operatorset fun(your_struct, var_name, your_func)
    ---@field _operatorget fun(your_struct, var_name, your_func)
    ---@field _getindex fun(your_struct, index)
    ---@field _setindex fun(your_struct, index, value)
    
    local rawget = rawget
    local macros = {}
    ---@param macroName string
    ---@param args? table string[] --a table containing any number of strings
    ---@param macro string|function --the code that is to be "inserted"
    function vJass.textmacro(macroName, args, macro, ...)
        macros[macroName] = {macro, args}
    end --index this macro as a table and store its name and arguments.
    ---@param macroName string
    ---@param ... string --any number of strings
    function vJass.runtextmacro(macroName, ...)
        local storedMacro = macros[macroName]
        if storedMacro then
            local macro = storedMacro[1] --get the macro string
            if type(macro) == "function" then
                macro(...)
                return
            end
            local macroargs = storedMacro[2] --get the args of textmacro (pointers)
            
            if macroargs then
                local args = {...} --get the args of runtextmacro (values)
                local n = #macroargs
                -- the below string pattern checks for $word_with_or_without_underscores$.
                -- Need to use double percentages in World Editor, but a single percentage on each should be used when testing code outside of World Editor.
                load(macro:gsub("%%$([%%w_]+)%%$", function(arg)
                    for i = 1, n do -- search all of the textmacro's registered arguments to find out which of the pointers match
                        if arg == macroargs[i] then
                            return args[i] --substitute the runtextmacro arg string at the matching pointer position
                        end
                    end
                end))()
            else
                load(macro)()
            end
        end
    end
    ---Create a new module that can be implemented by any struct.
    local modules = {}
    local moduleQueue = {}
    
    ---@param moduleName string
    ---@param moduleFunc fun(struct : Struct)
    function vJass.module(moduleName, privacy, scope, moduleFunc)
        local module, init = {}, {}
        if privacy ~= "" then
            modules[scope..moduleName] = module
        else
            modules[moduleName] = module
        end
        moduleQueue[#moduleQueue+1] = module
        module.implement = function(struct)
            if not init[struct] then
                init[struct] = true
                moduleFunc(struct)
            end
        end
    end
    
    ---Implement a module by name
    ---@param moduleName string
    ---@param struct Struct
    function vJass.implement(moduleName, scope, struct)
        local module = modules[scope..moduleName]
        if not module then
            module = modules[moduleName]
        end
        if module then
            module.implement(struct)
        end
    end
    
    local interface, defaults
    
    --Does its best to automatically handle the vJass "interface" concept.
    ---@param default any
    ---@return table
    function vJass.interface(default)
        interface = interface or {
            __index = function(tab, key)
                local dflt = rawget(Struct, key)
                if not dflt then
                    return defaults[tab]
                end
                return dflt
            end
        }
        local new = setmetatable({}, interface)
        defaults = defaults or {}
        defaults[new] = default and function() return default end or DoNothing
        return new
    end
    do
        local mt
        vJass.array2D = function(w, h)
            if not mt then
                mt = {
                    __index = function(self, key)
                        local new = {}
                        rawset(self, key, new)
                        return new
                    end
                }
            end
            return setmetatable({width = w, height = h, size=w*h}, mt)
        end
    end
    vJass.hook = function(funcName, userFunc)
        local old
        if AddHook then
            old = AddHook(funcName, function(...)
                userFunc(...)
                old(...)
            end)
        else
            old = _G[funcName]
            _G[funcName] = function(...)
                userFunc(...)
                old(...)
            end
        end
    end
    vJass.struct = Struct --just for naming consistency with the above vJass-prefixed methods.
    local mt = {
        __index = function(self, key)
            local get = rawget(self, "_getindex") --declaring a _getindex function in your struct enables it to act as method operator []
            return get and get(self, key) or rawget(Struct, key) --however, it doesn't extend to child structs.
        end
        ,
        __newindex = function(self, key, val)
            local set = rawget(self, "_setindex") --declaring a _setindex function in your struct enables it to act as method operator []=
            if set then set(self, key, val) else rawset(self, key, val) end
        end
    }
    setmetatable(Struct, mt)
    
    --Loop function to iterate all of the structs from the child to the parent.
    function Struct:inception()
        local skip = true
        return function()
            if skip then
                skip = nil
            else
                self = self.super
            end
            return self
        end
    end
    
    ---Allocate, call the stub method onCreate and return a new struct instance via myStruct:create().
    ---@return Struct new_instance
    function Struct:allocate()
        local newInstance = {}
        setmetatable(newInstance, {__index = self})
        for struct in self:inception() do
            struct.onCreate(newInstance)
        end
        return newInstance
    end
    
    Struct.destroyed = {__mode = "v"}
    
    ---Deallocate and call the stub method onDestroy via myStructInstance:destroy().
    function Struct.deallocate(self)
        for struct in self:inception() do
            struct.onDestroy(self)
        end
        setmetatable(self, Struct.destroyed)
    end
    
    local structQueue = {}
    ---Acquire another struct's keys via myStruct:extends(otherStruct)
    ---@param parent Struct
    function Struct:extends(parent)
        for key, val in pairs(parent) do
            if self[key] == nil then
                self[key] = val
            end
        end
    end
     
    ---Create a new "vJass"-style struct with myStruct = Struct([parentStruct]).
    ---@param parent? Struct
    ---@return Struct new_struct
    function mt:__call(parent)
        if self == Struct then
            local struct
            if parent then
                struct = setmetatable({super = parent, allocate = parent.create, deallocate = parent.destroy}, {__index = parent})
            else
                struct = setmetatable({}, {__index = self})
            end
            structQueue[#structQueue+1] = struct
            return struct
        end
        return self --vJass typecasting... shouldn't be used in Lua, but there could be cases where this will work without the user having to change anything.
    end
    
    --stub methods:
    Struct.create = Struct.allocate
    Struct.destroy = Struct.deallocate
    Struct.onCreate = DoNothing
    Struct.onDestroy = DoNothing
    
    ---Check if a child struct belongs to a particular parent struct.
    ---@param parent Struct
    ---@return boolean
    function Struct:isType(parent)
        for struct in parent:inception() do
            if struct == self then return true end
        end
    end
    mt = {}
    local environment = setmetatable({}, mt)
    environment.struct = environment
    local getter = function(key)
        local super = rawget(environment.struct, "super")
        while super do
            local get = rawget(super, key)
            if get ~= nil then
                return get
            else
                super = rawget(super, "super")
            end
        end
    end
    mt.__index = function(_, key)
        --first check the initial struct, then check extended structs (if any), then check the main Struct library, or finally check if it's a global
        return rawget(environment.struct, key) or getter(key) or rawget(Struct, key) or rawget(_G, key)
    end
    mt.__newindex = environment.struct
    
    ---Complicated, but allows invisible encapsulation via:
    ---do local _ENV = myStruct:environment()
    ---    x = 10
    ---    y = 100 --assigns myStruct.x to 10 and myStruct.y to 100.
    ---end
    ---@param self Struct
    ---@return table
    function Struct:environment()
        environment.struct = self
        return environment
    end
    
    local function InitOperators(struct)
        if not struct.__getterFuncs then
            local mt = getmetatable(struct)
            if not mt then mt = {} ; setmetatable(struct, mt) end
            struct.__getterFuncs = {}
            struct.__setterFuncs = {}
            mt.__index = function(self, var)
                local call = self.__getterFuncs[var]
                if call then return call(self)
                elseif not self.__setterFuncs[var] then
                    return rawget(rawget(self, "parent") or Struct, var)
                end
            end
            mt.__newindex = function(self, var, val)
                local call = rawget(self, "__setterFuncs")
                call = call[var]
                if call then call(self, val)
                elseif not self.__getterFuncs[var] then
                    rawset(self, var, val)
                end
            end
        end
    end
    
    ---Create a new method operator with myStruct:_operatorset("x", function(val) SetUnitX(u, val) end)
    ---@param var string
    ---@param func fun(val)
    function Struct:_operatorset(var, func) --treat the var-string as a "write-only" variable
        InitOperators(self)
        self.__setterFuncs[var] = func
    end
    ---Create a new method operator with myStruct:_operatorget("x", function() return GetUnitX(u) end)
    ---@param var string
    ---@param func fun()->any
    function Struct:_operatorget(var, func) --treat the var-string as a "read-only" variable
        InitOperators(self)
        self.__getterFuncs[var] = func
    end
    
    if OnGlobalInit then
        OnGlobalInit(function()
            for module in ipairs(moduleQueue) do
                if module.onInit then pcall(module.onInit) end
            end
            for struct in ipairs(structQueue) do
                if struct.onInit then pcall(struct.onInit) end
            end
            moduleQueue, structQueue = nil, nil
        end)
    end
end --end of Struct library
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Version 1.4 now supports vJass-style method operators:

JASS:
method operator x takes nothing returns real
endmethod

method operator x= takes real r returns nothing
endmethod

static method operator [] takes integer index returns integer
endmethod

method operator []= takes integer index, integer val returns nothing
endmethod
->
Lua:
thistype:_operatorget("x", function(self)
end)

thistype:_operatorset("x", function(self, r)
end)

function thistype:_getindex(index)
end

function thistype:_setindex(index, val)
end
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Version 1.5 is here, and enables vJass2Lua even further by reducing the complexity of _ENV overrides into simple "local _ENV = myStruct:environment()" rather than requiring for loops.

Removed ugly Library and Scope structs and replaced them with do/end blocks. The presentation is therefore much cleaner.

Minor "typecasting" will now run correctly, provided no one is trying to compare a table to zero.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Hello, I think you could also add the "classes" keyword and thistype to be according with the sumneko extension.
Hi, could you please elaborate? You are asking that I should declare "thistype" as a field of the "Struct" class? Or do you mean something else (e.g. that I make a change to vJass2Lua)?
 
Level 24
Joined
Jun 26, 2020
Messages
1,852
Hi, could you please elaborate? You are asking that I should declare "thistype" as a field of the "Struct" class? Or do you mean something else (e.g. that I make a change to vJass2Lua)?
I mean, thistype refers to the actual class e.g. struct so with adding it here would be enough, because the people who will use the vJass2Lua should have this first, and having thistype as a "subclass" of Struct in theory would make don't have problem with the sumneko extension.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I mean, thistype refers to the actual class e.g. struct so with adding it here would be enough, because the people who will use the vJass2Lua should have this first, and having thistype as a "subclass" of Struct in theory would make don't have problem with the sumneko extension.
Do you mean the problem with the "undefined global" message? That's because I overload _ENV as a failsafe to make sure "integer i" in a struct is treated as "thistype.i" in Lua even though vJass2Lua has no idea that "i" is a member of "thistype".

This is because vJass didn't force struct members to be prefixed with "this." or "thistype." so to avoid the guessing game I have to overload _ENV and ensure the invisible inferences are picked up by the struct and not by the global table "_G".

I've already submitted a feature request to Sumneko on GitHub to make that message go away under the right circumstances. For now, the error can be ignored.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I have finally cracked textmacro processing.

If anyone wants to play around with this, you can use Online Lua Compiler and paste in both snippets below.

Lua:
do
    local macros = {}

    ---@param macroName string
    ---@param macroStr string
    ---@param ... string[]
    function Textmacro(macroName, macroStr, ...)
        macros[macroName] = {macroStr, table.pack(...)}
    end --index this macro as a table and store its name and arguments.
    
    --checks for $word_with_or_without_underscores$
    local findArg = "%$([%w_]+)%$"
    
    ---@param macroName string
    ---@param ... string[]
    function Runtextmacro(macroName, ...)
        local macrotable = macros[macroName]
        if not macrotable then return end
       
        local macro = macrotable[1] --get the macro string
        local macroargs = macrotable[2] --get the args of the textmacro (pointers)
       
        local args = table.pack(...) --get the args of runtextmacro (values)
        if args.n > 0 and macroargs.n > 0 then
            while true do
                local _,_,arg = macro:find(findArg)
                if not arg then break end --stop scanning once there are no more $keywords$
                for i = 1, macroargs.n do --check all of the args to see if the pointers are a match
                    if arg == macroargs[i] then
                        macro = macro:gsub(findArg, args[i], 1) --substitute the runtextmacro arg string at the matching pointer position
                    end
                end
            end
        end
        print("return function()\n"..macro.."\nend")
    end
end
Example usage:
Lua:
        Textmacro("Increase", [[
            function IncreaseStored$TYPEWORD$(g, m, l)
                Store$TYPEWORD$(g, m, l, GetStored$TYPEWORD$(g, m, l) + 1)
            end
        ]], "TYPEWORD")
       
        Runtextmacro("Increase", "Integer")
        Runtextmacro("Increase", "Real")
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Lua:
do vJass, Struct = {}, {} --vJass2Lua support script, version 2.0.0.0 by Bribe
  
    --Requires optional Global Initialization: https://www.hiveworkshop.com/threads/global-initialization.317099/
  
    ---Constructor: Struct([optional_parent_struct])
    ---@class Struct : table
    ---@field allocate fun(from_struct)
    ---@field deallocate fun(struct_instance)
    ---@field create fun(from_struct)
    ---@field destroy fun(struct_instance)
    ---@field onDestroy fun(struct_instance)
    ---@field onCreate fun(struct_instance)
    ---@field extends fun(other_struct_or_table)
    ---@field private inception fun(child_struct)
    ---@field super Struct
    ---@field isType fun(struct_to_compare, other_struct)
    ---@field environment fun(struct_to_iterate)
    ---@field interface
    ---@field _operatorset fun(your_struct, var_name, your_func)
    ---@field _operatorget fun(your_struct, var_name, your_func)
    ---@field _getindex fun(your_struct, index)
    ---@field _setindex fun(your_struct, index, value)
 
    ---@class vJass : table
    ---@field interface function
    ---@field module function
    ---@field implement function
    ---@field textmacro function
    ---@field runtextmacro function
  
    local rawget = rawget
    local macros = {}
    ---@param macroName string
    ---@param args? table string[] --a table containing any number of strings
    ---@param macroStr string --the code that is to be "inserted"
    function vJass.textmacro(macroName, args, macroStr, ...)
        macros[macroName] = {macroStr, args}
    end --index this macro as a table and store its name and arguments.
    ---@param storedMacro string
    ---@param ... string --any number of strings
    local function parseMacro(storedMacro, ...)
        local macro = storedMacro[1] --get the macro string
        local macroargs = storedMacro[2] --get the args of textmacro (pointers)
      
        if macroargs then
            local args = {...} --get the args of runtextmacro (values)
          
            -- the below string pattern checks for $word_with_or_without_underscores$.
            -- Need to use double percentages in World Editor, but a single percentage on each should be used when testing code outside of World Editor.
            return macro:gsub("%%$([%%w_]+)%%$", function(arg)
                for i = 1, #macroargs do -- search all of the textmacro's registered arguments to find out which of the pointers match
                    if arg == macroargs[i] then
                        return args[i] --substitute the runtextmacro arg string at the matching pointer position
                    end
                end
            end)
        end
        return macro --no args; just generate the code where the user wants it to be found in.
    end
    ---@param macroName string
    ---@param ... string --any number of strings
    function vJass.runtextmacro(macroName, ...)
        if macros[macroName] then
            load("return function()\n"..parseMacro(macros[macroName], ...).."\nend", nil, "t", _G)()
        end
    end
    ---Create a new module that can be implemented by any struct.
    local modules = {}
    local moduleQueue = {}
  
    ---@param moduleName string
    ---@param moduleFunc fun(struct : Struct)
    function vJass.module(moduleName, moduleFunc)
        local module, init = {}, {}
        modules[moduleName] = module
        moduleQueue[#moduleQueue+1] = module
        module.implement = function(struct)
            if not init[struct] then
                init[struct] = true
                moduleFunc(struct)
            end
        end
    end
  
    ---Implement a module by name
    ---@param moduleName string
    ---@param struct Struct
    function vJass.implement(moduleName, struct)
        local module = modules[moduleName]
        if module then
            module.implement(struct)
        end
    end
  
    local interface, defaults
  
    --Does its best to automatically handle the vJass "interface" concept.
    ---@param default any
    ---@return table
    function vJass.interface(default)
        interface = interface or {
            __index = function(tab, key)
                local dflt = rawget(Struct, key)
                if not dflt then
                    return defaults[tab]
                end
                return dflt
            end
        }
        local new = setmetatable({}, interface)
        defaults = defaults or {}
        defaults[new] = default and function() return default end or DoNothing
        return new
    end
  
    local mt = {
        __index = function(self, key)
            local get = rawget(self, "_getindex") --declaring a _getindex function in your struct enables it to act as method operator []
            return get and get(self, key) or rawget(Struct, key) --however, it doesn't extend to child structs.
        end
        ,
        __newindex = function(self, key, val)
            local set = rawget(self, "_setindex") --declaring a _setindex function in your struct enables it to act as method operator []=
            if set then set(self, key, val) else rawset(self, key, val) end
        end
    }
    setmetatable(Struct, mt)
  
    --Loop function to iterate all of the structs from the child to the parent.
    function Struct:inception()
        local skip = true
        return function()
            if skip then
                skip = nil
            else
                self = self.super
            end
            return self
        end
    end
  
    ---Allocate, call the stub method onCreate and return a new struct instance via myStruct:create().
    ---@return Struct new_instance
    function Struct:allocate()
        local newInstance = {}
        setmetatable(newInstance, {__index = self})
        for struct in self:inception() do
            struct.onCreate(newInstance)
        end
        return newInstance
    end
  
    Struct.destroyed = {__mode = "v"}
  
    ---Deallocate and call the stub method onDestroy via myStructInstance:destroy().
    function Struct.deallocate(self)
        for struct in self:inception() do
            struct.onDestroy(self)
        end
        setmetatable(self, Struct.destroyed)
    end
  
    local structQueue = {}
    ---Acquire another struct's keys via myStruct:extends(otherStruct)
    ---@param parent Struct
    function Struct:extends(parent)
        for key, val in pairs(parent) do
            if self[key] == nil then
                self[key] = val
            end
        end
    end
   
    ---Create a new "vJass"-style struct with myStruct = Struct([parentStruct]).
    ---@param parent? Struct
    ---@return Struct new_struct
    function mt:__call(parent)
        if self == Struct then
            local struct
            if parent then
                struct = setmetatable({super = parent, allocate = parent.create, deallocate = parent.destroy}, {__index = parent})
            else
                struct = setmetatable({}, {__index = self})
            end
            structQueue[#structQueue+1] = struct
            return struct
        end
        return self --vJass typecasting... shouldn't be used in Lua, but there could be cases where this will work without the user having to change anything.
    end
  
    --stub methods:
    Struct.create = Struct.allocate
    Struct.destroy = Struct.deallocate
    Struct.onCreate = DoNothing
    Struct.onDestroy = DoNothing
  
    ---Check if a child struct belongs to a particular parent struct.
    ---@param parent Struct
    ---@return boolean
    function Struct:isType(parent)
        for struct in parent:inception() do
            if struct == self then return true end
        end
    end
    mt = {}
    local environment = setmetatable({}, mt)
    environment.struct = environment
    local getter = function(key)
        local super = rawget(environment.struct, "super")
        while super do
            local get = rawget(super, key)
            if get ~= nil then
                return get
            else
                super = rawget(super, "super")
            end
        end
    end
    mt.__index = function(_, key)
        --first check the initial struct, then check extended structs (if any), then check the main Struct library, or finally check if it's a global
        return rawget(environment.struct, key) or getter(key) or rawget(Struct, key) or rawget(_G, key)
    end
    mt.__newindex = environment.struct
  
    ---Complicated, but allows invisible encapsulation via:
    ---do local _ENV = myStruct:environment()
    ---    x = 10
    ---    y = 100 --assigns myStruct.x to 10 and myStruct.y to 100.
    ---end
    ---@param self Struct
    ---@return table
    function Struct:environment()
        environment.struct = self
        return environment
    end
  
    local function InitOperators(struct)
        if not struct.__getterFuncs then
            local mt = getmetatable(struct)
            if not mt then mt = {} ; setmetatable(struct, mt) end
            struct.__getterFuncs = {}
            struct.__setterFuncs = {}
            mt.__index = function(self, var)
                local call = self.__getterFuncs[var]
                if call then return call(self)
                elseif not self.__setterFuncs[var] then
                    return rawget(rawget(self, "parent") or Struct, var)
                end
            end
            mt.__newindex = function(self, var, val)
                local call = rawget(self, "__setterFuncs")
                call = call[var]
                if call then call(self, val)
                elseif not self.__getterFuncs[var] then
                    rawset(self, var, val)
                end
            end
        end
    end
  
    ---Create a new method operator with myStruct:_operatorset("x", function(val) SetUnitX(u, val) end)
    ---@param var string
    ---@param func fun(val)
    function Struct:_operatorset(var, func) --treat the var-string as a "write-only" variable
        InitOperators(self)
        self.__setterFuncs[var] = func
    end
    ---Create a new method operator with myStruct:_operatorget("x", function() return GetUnitX(u) end)
    ---@param var string
    ---@param func fun()->any
    function Struct:_operatorget(var, func) --treat the var-string as a "read-only" variable
        InitOperators(self)
        self.__getterFuncs[var] = func
    end
  
    if OnGlobalInit then
        OnGlobalInit(function()
            for module in ipairs(moduleQueue) do
                if module.onInit then pcall(module.onInit) end
            end
            for struct in ipairs(structQueue) do
                if struct.onInit then pcall(struct.onInit) end
            end
            moduleQueue, structQueue = nil, nil
        end)
    end
end --end of Struct library
 
Last edited:
Top