[Lua][vJass] New Table

@Bribe, could this line in the destroy method
JASS:
call Table(t).remove(this)
supposed to be
JASS:
call Table(t).remove(0)
?

Thanks for that, it was actually supposed to be tracker.remove(this)

I am actually now working on 5.0 and am abandoning my earlier preview idea of 4.2 in favor of a separate HashTableEx struct so that users can have both tracked and non-tracked hashtables in the same map.

BribeFromTheHive/NewTable
 
Level 7
Joined
Feb 9, 2021
Messages
301
JASS:
local TableArray damageImmune = TableArray[0x2000]

set damageImmune[this].integer[type] = 0
set damageImmune[this].boolean[type] = true

Would this work?
 
This is mostly a proof-of-concept, but I feel the design will help people to avoid having to change too much within their scripts in order to make them Lua-compatible.

The majority of this library is just building backwards-compatibility with the old Table types. This whole thing performs slightly worse than just regular Lua Tables, but should perform better than even a direct hashtable call in JASS.

TableArray and HashTable are exactly the same thing in this resource. However, thanks to Lua, both options offer a proper "flush" method.

Lua:
do Table = {}
--[[
    Made by Bribe, special thanks to Vexorian & Nestharus
   
    One map, no hashtables. Welcome to Lua NewTable version 1.0.1.0
   
    API
   
    ------------
    Table.create() returns a new Table. The difference between this and a regular table is only that it will ignore keyword indices such as "integer, boolean, handle, etc."
    | tab.destroy()
    |     --flushes the table.
    |   
    | tab:flush()
    |     --same as destroy.
    |   
    | tab[some_index] = value
    |    store a value of any type in the table at some index.
    |   
    | tab[some_index]
    |     load a value from a table at some index but ignore indices that were aligned to types like "integer/boolean/real/etc"
    |
    | tab:has(some_index)
    |     whether anything is stored at that index. Same as tab[some_index] ~= nil
    |
    | tab:remove(some_index)
    |     remove the value at that index. Same as tab[some_index] = nil
    |
    ----------------
    TableArray[array_size] returns a new TableArray.
    | array:destroy()
    |     does nothing
    |
    | array:flush()
    |     flushes the array as well as any tables within it.
    |
    | array.size
    |     returns the size of the array specified at the time it was created.
    |
    | array[some_index]
    |     returns an existing Table or creates a new one at that index.
]]
Table.create = function() return setmetatable({}, Table) end

function Table:has(key) return self[key] ~= nil end
function Table:remove(key) self[key] = nil end
function Table:flush() for key,_ in pairs(self) do self[key] = nil end end
Table.destroy = Table.flush

local dump = { --Create a table just to store the types that will ignored
    handle = true,
    agent = true,
    real = true,
    boolean = true,
    string = true,
    integer = true,
    player = true,
    widget = true,
    destructable = true,
    item = true,
    unit = true,
    ability = true,
    timer = true,
    trigger = true,
    triggercondition = true,
    triggeraction = true,
    event = true,
    force = true,
    group = true,
    location = true,
    rect = true,
    boolexpr = true,
    sound = true,
    effect = true,
    unitpool = true,
    itempool = true,
    quest = true,
    questitem = true,
    defeatcondition = true,
    timerdialog = true,
    leaderboard = true,
    multiboard = true,
    multiboarditem = true,
    trackable = true,
    dialog = true,
    button = true,
    texttag = true,
    lightning = true,
    image = true,
    ubersplat = true,
    region = true,
    fogstate = true,
    fogmodifier = true,
    hashtable = true
}
Table.__index = function(self, key)
    local get = dump[key] and self or rawget(Table, key)
    if get then
        rawset(self, key, get) --for performance on further calls at the expense of a little more memory usage.
        return get
    end
end
   
local repeater

TableArray = setmetatable(
{
    destroy = DoNothing,
    has = function(self, key) return rawget(self, key) ~= nil end,
    flush = function(self)
        for key, val in pairs(self) do
            if type(val) == "table" then val:flush() end
            self[key] = nil
        end
    end
},
{
    __index = function(self, size)
        if not repeater then
            repeater = {
                __index = function(self, key)
                    local new = rawget(TableArray, key)
                    if new then return new end
                    new = Table.create()
                    rawset(self, key, new)
                    return new
                end
            }
        end
        return setmetatable({size = size}, repeater)
    end
})
HashTable = TableArray
HashTable.create = function(val) return HashTable[val] end

end--LIBRARY
 
Last edited:
Also more of a proof-of-concept, but building backwards-compatibiliy with Vexorian's Table in Lua is absolutely gorgeous in functionality:


Lua:
do --Table BC methods for Lua, version 1.1

    Table.exists = Table.has
    Table.reset = Table.flush
    function Table:flush(key)       --no longer in conflict with NewTable. It only took 11 years?
        if key then self[key] = nil
        else Table.reset() end
    end
    
    local indexer = {}
    function Table.flush2D(index)
        indexer[index] = nil
    end
  
    local oldIndex = rawget(Table, __index)
    rawset(Table, __index, function(self, index)
        if self == Table then               --need to check if it's a static method operator and not a regular instance.
            local t = indexer[index]
            if not t then
                t = Table.create()
                rawset(indexer, index, t)
            end
            return t
        end
        return oldIndex(self, index)
    end)
    setmetatable(Table, Table)
 
    HandleTable = Table
    StringTable = Table --Jesus Christ, Lua. Your tables are so overpowered.
end
 
Last edited:
I'm a was a confused on details of HashTable until I tested it.
It isn't really clear that you can do the following and get the correct result, but you can! Awesome!
(your example were more in-line with WC3 vanilla Hashtable than this)
JASS:
        set heroAbility = HashTable.create()
        set heroAbility[1].integer[2] = 47
        set heroAbility[1][2].real[0] = 3.14
        set heroAbility[1][2].integer[0] = 17
        set heroAbility[1][2][0].integer[0] = 42
        set heroAbility[1][2][0][123].integer[0] = 1337
        call BJDebugMsg(I2S(heroAbility[1].integer[2]))
        call BJDebugMsg(R2S(heroAbility[1][2].real[0]))
        call BJDebugMsg(I2S(heroAbility[1][2].integer[0]))
        call BJDebugMsg(I2S(heroAbility[1][2][0].integer[0]))
        call BJDebugMsg(I2S(heroAbility[1][2][0][123].integer[0]))

Edit: Note that this is not 100% safe!
 
Last edited:
did I program HashTable to return another HashTable at the end of the second array sequence? If so, then yes it would work. But I thought the 2nd array index returned the "Table" type, which would break the chain you're describing here.

I just checked the code and the 2nd index access is returning a "Table" object. The way to make it go infinite would be to wrap subsequent calls with HashTable(yourHash[index1][index2])[index3][index4].

The reason why your syntax works is because "Table" indices return "Table" rather than "integer", because I allowed it to do that for similar reasons to what you are doing there. However, when you access "0" at the root index, this area is hazardous because this is the "null data zone". If someone mis-loads what they think is a table and access it, the number would be zero and hence overwrite data you've stored to Table(0).

When you access Table[0][123] you are crossing over the restricted indices provided by Table and are messing with whatever Table is represented in memory with the value 123.

However, if you do set yourHash[index1][index2] = HashTable.create() then you don't have to use the wrapper on subsequent "get" calls to that index point.
 
Last edited:
This would explain some weird behavior caused by my

JASS:
heroAbility[unit-index][ability-event-index][extra-triggered-effect-index].integer[effect-identifier]
heroAbility[unit-index][ability-event-index][extra-triggered-effect-index].real[effect-data-index]
Where both ability-event-index and extra-triggered-effect-index were zero-indexed, even when the above example printed expected values. My example wasn't as good as I expected, cool. Thanks.

I guess that the set yourHash[index1][index2] = HashTable.create() will occupy the "integer" value for index2. So I cannot keep an integer value at yourHash[index1].integer[index2] while doing this, which makes sense.
 
Last edited:
Level 17
Joined
Apr 13, 2008
Messages
1,603
Hello sir @Bribe ,

I'm new to your table and fairly ignorant about Warcraft III performance / resource management.

My question:

I'm designing an inventory system with frames and want to use Table as a database for items.
Does table eat significant resources for "empty fields"? For example if my system refers to items in this way, do I risk stability or performance?

JASS:
itemDB[playerId][playerId*10000]

If, I allow 24 players, and each player to have 10 heroes with unique inventories storing up to 1000 items each, that is 240 000 indexes storing 10-20 values each.
 
Hello sir @Bribe ,

I'm new to your table and fairly ignorant about Warcraft III performance / resource management.

My question:

I'm designing an inventory system with frames and want to use Table as a database for items.
Does table eat significant resources for "empty fields"? For example if my system refers to items in this way, do I risk stability or performance?

JASS:
itemDB[playerId][playerId*10000]

If, I allow 24 players, and each player to have 10 heroes with unique inventories storing up to 1000 items each, that is 240 000 indexes storing 10-20 values each.
I've not seen cases where hashtables values actually hit each other and start to override, so I would consider it safe. You can "allocate" as much in that 2D context as you want (or even make a TableArray instead) but unless you fill some data in the in-between parts it's just empty space. Hashtables are unlike JASS arrays in that they don't "fill in" or "allocate" empty areas.
 
Level 17
Joined
Apr 13, 2008
Messages
1,603
Does Table not work for you? I've used this on all the patches since I think 1.28 and never had any issues.
It works.
But initially I tried to do a initialize the table in the globals like this:

globals
Table InventoryDB = InventoryDB.create()
endglobals

The world editor does not throw an error, but it does not work when you initialize the table in the globals.
 
It works.
But initially I tried to do a initialize the table in the globals like this:

globals
Table InventoryDB = InventoryDB.create()
endglobals

The world editor does not throw an error, but it does not work when you initialize the table in the globals.
Yeah this is mentioned somewhere earlier in the thread. I create Tables from index 8192 specifically so that you can use vJass "key" type to cast those to Table. You can also cast StructName.typeid into Table and it will work the same way (because struct type IDs share the same key stack as "key" itself).
 
Table has just been officially updated to version 5.1. You can now build N-Dimensional tables, just pick your syntax:

Table2D (formerly HashTable, which still works as an API, but I would recommend switching to Table2D)
Table2DT (formerly HashTableEx, which also still works). This syntax is preferred if you want automatic cleanup of the interrim keys that are generated.
Table3/4/5D/T - choose what depth you want. If you want more than 5 dimensions, just create a new 'runtextmacro' statement and ensure it follows the established pattern.

I've added a 'toString/print' function to Table2DT, which was useful for me during the debugging process to ensure that the 'remove' and 'destroy' methods were recursively destroying what they were supposed to.

I've also fixed the DEBUG_MODE/debug as these were always disabled in a recent WC3 patch, even with the Debug Mode option checked. I've changed the debugging inside of Table to use informational logging (DEEP_TEST) and TEST which will warn users about problems using it. I've left "TEST" on by default, so please set it to 'false' if you know what you're doing and want better performance.

This is the most recent unit test setup I've been using:
JASS:
function Trig_Test_Actions takes nothing returns nothing
    // create/destroy test 1
    local Table4DT myTable = Table4DT.create()

    set myTable[1][1][1][1] = 10
    set myTable[1][2][2][2] = 20
    set myTable[2][1][1][1] = 30
    set myTable[2][2][2][2] = 40
    set myTable[3][3][3][3] = myTable

    call Table2DT(myTable).print()

    call myTable.destroy()

    // create/destroy test 2
    set myTable = Table4DT.create()

    set myTable[1][2][3][4] = 100
    set myTable[5][6][7][8] = 200
    set myTable[9][10][11][12] = 300
    set myTable[13][14][15][16] = 400
    set myTable[17][18][19][20] = myTable

    call Table2DT(myTable).print()

    call myTable.destroy()

    set myTable = Table4DT.create()

    set myTable[1][1][1][1] = 10
    set myTable[1][2][2][2] = 20
    call BJDebugMsg("Should load 10: " + I2S(myTable[1][1][1][1]))
    call BJDebugMsg("Should load 20: " + I2S(myTable[1][2][2][2]))
    call myTable[1].remove(2)
    call BJDebugMsg("Should no longer have the *[2][2][2] branch:")
    call Table2DT(myTable).print()

    // remove Test 3: Destroy after remove
    call myTable.destroy()
    call BJDebugMsg("Should not print anything")
    call Table2DT(myTable).print()

    // remove Test 4: Self-reference and remove
    set myTable = Table4DT.create()
    set myTable[3][3][myTable][myTable] = myTable
    call BJDebugMsg("Should contain self-reference at [3][3][*][*]")
    call Table2DT(myTable).print()
    call myTable[3][3].remove(myTable)
    call BJDebugMsg("Should now only show [3][3] as indexed")
    call Table2DT(myTable).print()
    call myTable.destroy()
endfunction

//===========================================================================
function InitTrig_Test takes nothing returns nothing
    call TimerStart(CreateTimer(), 0.01, false, function Trig_Test_Actions )
endfunction
 
Table Version 6 has just dropped, and it is the single largest undertaking I've worked on with this project since creating the original API years ago.

What's so special about version 6?
  • Every TableArray/HashTable method has been hoisted up to the Table struct itself.
  • Tables can spawn new dimensions or forks (formerly TableArrays) on-demand, rather than needing to be created with their depth in mind.
  • All values inside of a Table can be tracked, as long as you choose to use the new syntax of save/store/write instead of just []=.
  • Very powerful debugging of Table values when in TEST mode, which will help to isolate data issues very quickly.
  • HandleTable/StringTable syntax is integrated directly into the Table API. Instead of save/[]/remove/has/link, you can use:
    Handle-based keys: store/get/forget/stores/bind
    String-based keys: write/read/delete/written/join
  • table.getKeys() returns a read-only Table listing all keys within contained in the Table. Index 0 contains the total number of keys inside of the table (N) and indices 1-N contain all of the keys in your table. So you can loop through all of your keys and do what you need or want with them, just like with a real programming language. Compares well with pairs in Lua or Object.keys in JavaScipt.
I've preserved backwards-compatibility with prior versions of Table as cleanly as possible. The deprecated TableArray and HashTable/TableXD syntax has been moved over to a separate resource, and all existing scripts will work fine as long as the map includes the Table5BC library.

Here are the new unit tests I've used:
JASS:
function Trig_Test_Actions takes nothing returns nothing
    // create/destroy test 1
    local Table myTable = Table.create()

    call myTable.link(1).link(1).link(1).save(1, 10)
    call myTable.link(1).link(2).link(2).real.save(2, 20)
    call myTable.link(2).link(1).link(1).boolean.save(1, false)
    call myTable.link(2).link(2).link(2).string.save(2, "Hello, world")
    call myTable.link(3).link(3).link(3).save(3, myTable)
    call myTable.link(3).link(3).forkLink(69, 4).unit.save(0, null)

    call myTable.print()

    call myTable.destroy()

    // create/destroy test 2
    set myTable = Table.create()

    call myTable.link(1).link(2).link(3).save(4, 100)
    call myTable.link(5).link(6).link(7).save(8, 200)
    call myTable.link(9).link(10).link(11).save(12, 300)
    call myTable.link(13).link(14).link(15).save(16, 400)
    call myTable.link(17).link(18).link(19).save(20, myTable)

    call myTable.print()

    call myTable.destroy()

    set myTable = Table.create()

    call myTable.link(1).link(1).link(1).save(1, 10)
    call myTable.link(1).link(2).link(2).save(2, 20)
    call BJDebugMsg("Should load 10: " + I2S(myTable[1][1][1][1]))
    call BJDebugMsg("Should load 20: " + I2S(myTable[1][2][2][2]))
    call myTable[1].remove(2)
    call BJDebugMsg("Should no longer have the *[2][2][2] branch:")
    call myTable.print()

    // remove Test 3: Destroy after remove
    call myTable.destroy()
    call BJDebugMsg("Should print the Table as destroyed")
    call myTable.print()

    // remove Test 4: Self-reference and remove
    set myTable = Table.create()
    call myTable.link(3).link(3).link(myTable).save(myTable, myTable)
    call BJDebugMsg("Should contain self-reference at [3][3][*][*]")
    call myTable.print()
    call myTable[3][3].remove(myTable)
    call BJDebugMsg("Should now only show [3][3] as indexed")
    call myTable.print()
    call myTable.destroy()
endfunction

//===========================================================================
function InitTrig_Test takes nothing returns nothing
    call TimerStart(CreateTimer(), 0.01, false, function Trig_Test_Actions )
endfunction
 
Table Version 6 has just dropped, and it is the single largest undertaking I've worked on with this project since creating the original API years ago.
Thank you so much for this!
This is huge!
So you can loop through all of your keys and do what you need or want with them, just like with a real programming language
Indeed!

I will upgrade next time I sit with my map!
 
So, if I understand correctly it's now possible to do this:

JASS:
//global Table heroAbility
local Table subtable = heroAbility.link(uix).link(abilityIndex).link(abilityEffects + 1) //next effect-index for this ability for this unit

to get a subtable instead of having to do something along these lines:

JASS:
//global HashTable heroAbility
local Table subtable
if abilityEffects == 0 then
    set heroAbility[uix][abilityIndex] = Table.create()
endif
set subtable = Table(heroAbility[uix][abilityIndex])[abilityEffects + 1] //next effect-index for this ability for this unit

This is great!
Although I have some old systems to adjust and hopefully they'll work afterwards :p
 
Correct, link will always check if a key exists first, and create a new Table there if not.

However, if you already know for a fact that the key exists (you already called link), you can use the normal [], get or read. So the extra effort is only needed on the first link/bind/join, and can be relaxed on follow-up references.
 
I glossed over the introduction of the syntax for neato burrito multiple-assignment-upon-creation, a feature enjoyed for years with real programming languages.

Lua:
Lua:
local animalSounds = {
    dog = "woof",
    cat = "meow",
    duck = "quack"
}

JavaScript
JavaScript:
const animalSounds = {
    dog: "woof",
    cat: "meow",
    duck: "quack"
};

Table:
JASS:
local Table animalSounds = Table.create()/*
*/.string.write("dog", "woof")/*
*/.string.write("cat", "meow")/*
*/.string.write("duck", "quack")
 
I'm a bit unsure when stuff written to same "index" claches and when it's ok to reuse the same index. Do you know?
Is it best to never use for example 1 as index for both a real and a string? Does it work with different handle-types?
I've started to always use different indexes because I'm unsure but it'd be good to know :)

FYI: My map now uses table 6 and it's great! Thanks yet again :)
 
No clash in hashtables:

integer
real
string
boolean
handle

Any type of handle will clash with each other.

Keep in mind that with Lua, types are irrelevant, so keep the keys unique with Lua tables. For best practice, you should do the same with hashtables. I only use overlapping keys for shadow tables, because I had no choice.
 
Level 10
Joined
Dec 16, 2017
Messages
402
Table Version 6 has just dropped, and it is the single largest undertaking I've worked on with this project since creating the original API years ago.

What's so special about version 6?
  • Every TableArray/HashTable method has been hoisted up to the Table struct itself.
  • Tables can spawn new dimensions or forks (formerly TableArrays) on-demand, rather than needing to be created with their depth in mind.
  • All values inside of a Table can be tracked, as long as you choose to use the new syntax of save/store/write instead of just []=.
  • Very powerful debugging of Table values when in TEST mode, which will help to isolate data issues very quickly.
  • HandleTable/StringTable syntax is integrated directly into the Table API. Instead of save/[]/remove/has/link, you can use:
    Handle-based keys: store/get/forget/stores/bind
    String-based keys: write/read/delete/written/join
  • table.getKeys() returns a read-only Table listing all keys within contained in the Table. Index 0 contains the total number of keys inside of the table (N) and indices 1-N contain all of the keys in your table. So you can loop through all of your keys and do what you need or want with them, just like with a real programming language. Compares well with pairs in Lua or Object.keys in JavaScipt.
I've preserved backwards-compatibility with prior versions of Table as cleanly as possible. The deprecated TableArray and HashTable/TableXD syntax has been moved over to a separate resource, and all existing scripts will work fine as long as the map includes the Table5BC library.

Here are the new unit tests I've used:
JASS:
function Trig_Test_Actions takes nothing returns nothing
    // create/destroy test 1
    local Table myTable = Table.create()

    call myTable.link(1).link(1).link(1).save(1, 10)
    call myTable.link(1).link(2).link(2).real.save(2, 20)
    call myTable.link(2).link(1).link(1).boolean.save(1, false)
    call myTable.link(2).link(2).link(2).string.save(2, "Hello, world")
    call myTable.link(3).link(3).link(3).save(3, myTable)
    call myTable.link(3).link(3).forkLink(69, 4).unit.save(0, null)

    call myTable.print()

    call myTable.destroy()

    // create/destroy test 2
    set myTable = Table.create()

    call myTable.link(1).link(2).link(3).save(4, 100)
    call myTable.link(5).link(6).link(7).save(8, 200)
    call myTable.link(9).link(10).link(11).save(12, 300)
    call myTable.link(13).link(14).link(15).save(16, 400)
    call myTable.link(17).link(18).link(19).save(20, myTable)

    call myTable.print()

    call myTable.destroy()

    set myTable = Table.create()

    call myTable.link(1).link(1).link(1).save(1, 10)
    call myTable.link(1).link(2).link(2).save(2, 20)
    call BJDebugMsg("Should load 10: " + I2S(myTable[1][1][1][1]))
    call BJDebugMsg("Should load 20: " + I2S(myTable[1][2][2][2]))
    call myTable[1].remove(2)
    call BJDebugMsg("Should no longer have the *[2][2][2] branch:")
    call myTable.print()

    // remove Test 3: Destroy after remove
    call myTable.destroy()
    call BJDebugMsg("Should print the Table as destroyed")
    call myTable.print()

    // remove Test 4: Self-reference and remove
    set myTable = Table.create()
    call myTable.link(3).link(3).link(myTable).save(myTable, myTable)
    call BJDebugMsg("Should contain self-reference at [3][3][*][*]")
    call myTable.print()
    call myTable[3][3].remove(myTable)
    call BJDebugMsg("Should now only show [3][3] as indexed")
    call myTable.print()
    call myTable.destroy()
endfunction

//===========================================================================
function InitTrig_Test takes nothing returns nothing
    call TimerStart(CreateTimer(), 0.01, false, function Trig_Test_Actions )
endfunction
Hi, is this live, can it be downloaded somewhere?
 
Top