2014-11-09 23:03:20 -07:00
local _ENV = mkmodule ( ' persist-table ' )
2014-11-14 16:49:40 -07:00
--[[
persist - table.lua
author expwnent
2014-11-09 23:03:20 -07:00
2014-11-14 16:49:40 -07:00
This module is intended to help facilitate persistent table lookups .
It is a wrapper over dfhack.persistent calls .
It supports tables of arbitrary dimension and shape .
It stores information about each table and subtable ' s size and children.
For convenience , all stored information is itself persistent .
It would be more efficient to cache stored information in global variables and update it on save / load but this is not yet implemented .
Ask expwnent to try this if performance is bad .
2014-11-16 18:41:11 -07:00
Usage :
local persistTable = require ' persist-table '
persistTable.GlobalTable . doomitude = ' doom! ' -- will be stored persistently
print ( persistTable.GlobalTable . doomitude ) --doom!
persistTable.GlobalTable . doomitude = nil --delete the persistent record
print ( persistTable.GlobalTable . doomitude ) --nil
persistTable.GlobalTable . mana = { } --allocate a subtable for mana
local mana = persistTable.GlobalTable . mana --setting elements in this table will be persistent too
mana [ ' 1 ' ] = ' 3 ' --slightly faster than persistTable.GlobalTable.mana['1'] = '3'
--be aware that if you don't change the local variable mana when the game exits and reloads a different save you will run into mysterious problems so don't do that
mana [ ' 2 ' ] = ' 100 '
mana.maximum = ' 1000 ' --tables can be any arbitrary shape
local globalTable = persistTable.GlobalTable --this is safe too
globalTable.mana = nil --this is safe: it will deallocate all subtables cleanly
globalTable.revengeDesire = { }
--globalTable.revengeDseire.foo = {} -- error: tables must be allocated first with parentTable.subtableName = {}
--you can check if it exists with globalTable.tableName != nil
local revengeTable = globalTable.revengeDesire
revengeTable [ ' 2 ' ] = revengeTable [ ' 2 ' ] or { } --this is fine too
revengeTable [ ' 2 ' ] [ ' 3 ' ] = ' totally a lot, man '
revengeTable [ ' 3 ' ] = { }
revengeTable [ ' 3 ' ] [ ' 2 ' ] = ' maybe a little bit '
revengeTable [ ' 2 ' ] = nil
-------------
And so on .
Be careful not to name your tables in a way that will conflict with other scripts ! The easiest way is to just put all your tables in one giant table named based on your script .
All stored values MUST be strings . Numbers can be supported later but are not yet supported . Keep in mind there is a significant overhead for each table element so don ' t go totally crazy storing massive amounts of information or the game will run out of RAM.
--table._children returns a list of child keys
for _ , childKey in ipairs ( table._children ) do
local child = table [ childKey ]
--blah
2014-11-09 23:03:20 -07:00
end
2014-11-16 15:06:59 -07:00
--]]
2014-11-16 18:41:11 -07:00
local prefix = ' persist-table '
2014-11-16 15:06:59 -07:00
2014-11-16 18:41:11 -07:00
local function ensure ( name )
2014-11-16 15:06:59 -07:00
return dfhack.persistent . save ( { key = name } )
end
2014-11-16 18:41:11 -07:00
local function gensym ( )
2014-11-16 15:06:59 -07:00
local availables = dfhack.persistent . get_all ( prefix .. ' $available ' ) or { }
local available = nil
local smallest = nil
for _ , candidate in pairs ( availables ) do
--TODO: it would be great if we could iterate over these in order but we can't
local score = tonumber ( candidate.value )
2014-11-16 18:41:11 -07:00
--print('gensym', candidate, score, available, smallest)
2014-11-16 15:06:59 -07:00
if ( score and ( not available or score < smallest ) ) then
smallest = score
available = candidate
end
end
if available then
local value = available.value
available : delete ( )
2014-11-16 18:41:11 -07:00
--print('gensym: allocate ' .. value)
2014-11-16 15:06:59 -07:00
return value
end
--none explicitly available, so smallest unused is the next available number
local smallestUnused = ensure ( prefix .. ' $smallest_unused ' )
if smallestUnused.value == ' ' then
smallestUnused.value = ' 0 '
end
local result = smallestUnused.value
smallestUnused.value = tostring ( 1 + tonumber ( result ) )
smallestUnused : save ( )
2014-11-16 18:41:11 -07:00
--print('gensym: allocate ' .. result)
2014-11-16 15:06:59 -07:00
return result
end
2014-11-16 18:41:11 -07:00
local function releasesym ( symb )
2014-11-16 15:06:59 -07:00
local availables = dfhack.persistent . get_all ( prefix .. ' $available ' ) or { }
for _ , available in pairs ( availables ) do
2014-11-16 18:41:11 -07:00
--print('releasesym: ', symb, available.value, available)
2014-11-16 15:06:59 -07:00
if available.value == symb then
print ( ' error: persist-table.releasesym( ' .. symb .. ' ): available.value = ' .. available.value )
return
end
end
dfhack.persistent . save ( { key = prefix .. ' $available ' , value = symb } , true )
2014-11-16 18:41:11 -07:00
--print('releasesym: unallocate ' .. symb)
2014-11-16 15:06:59 -07:00
end
local intCount = 7
local existIndex = intCount - 0
local existValue = 1
local pointerIndex = intCount - 1
local pointerValue = 1
local defaultValue = - 1
local function isEmpty ( table )
return next ( table ) == nil
end
local function deletePersistent ( name )
if name == ' ' then
return
end
local children = dfhack.persistent . get_all ( prefix .. name ) or { }
for _ , childKey in ipairs ( children ) do
local childEntry = ensure ( prefix .. name .. ' $$ ' .. childKey.value )
if childEntry.ints [ existIndex ] == existValue and childEntry.ints [ pointerIndex ] == pointerValue then
deletePersistent ( childEntry.value )
end
childEntry : delete ( )
childKey : delete ( )
end
releasesym ( name )
end
2014-11-16 18:41:11 -07:00
GlobalTable = GlobalTable or { key = ' mastertable ' }
GlobalTable.mt = GlobalTable.mt or { }
GlobalTable.mt . __index = GlobalTable.mt . __index or function ( theTable , key )
if key == ' _children ' then
return rawget ( theTable , key )
end
--print(rawget(theTable,'key') .. '[' .. key .. ']')
local entry = ensure ( prefix .. rawget ( theTable , ' key ' ) .. ' $$ ' .. key )
2014-11-16 15:06:59 -07:00
if entry.ints [ existIndex ] == existValue and entry.ints [ pointerIndex ] == defaultValue then
2014-11-16 18:41:11 -07:00
--print('string: ' .. entry.value)
2014-11-16 15:06:59 -07:00
return entry.value
end
if entry.ints [ pointerIndex ] == pointerValue then
--pre-existing pointer
local result = { key = entry.value }
result.mt = rawget ( GlobalTable , ' mt ' )
2014-11-16 18:41:11 -07:00
local childArgs = dfhack.persistent . get_all ( prefix .. entry.value )
result._children = { }
for _ , childArg in ipairs ( childArgs or { } ) do
--print(result._children, childArg.value)
table.insert ( result._children , childArg.value )
end
2014-11-16 15:06:59 -07:00
setmetatable ( result , rawget ( GlobalTable , ' mt ' ) )
2014-11-16 18:41:11 -07:00
--print('theTable: ' .. entry.value)
2014-11-16 15:06:59 -07:00
return result
end
entry : delete ( )
2014-11-16 18:41:11 -07:00
--print 'theTable[key] does not exist.'
2014-11-16 15:06:59 -07:00
return nil
end
2014-11-16 18:41:11 -07:00
GlobalTable.mt . __newindex = GlobalTable.mt . __newindex or function ( theTable , key , value )
--print(rawget(theTable,'key') .. '[' .. key .. '] = ' .. tostring(value))
local entry = ensure ( prefix .. rawget ( theTable , ' key ' ) .. ' $$ ' .. key )
2014-11-16 15:06:59 -07:00
local old = entry.value
local isNew = entry.ints [ existIndex ] == defaultValue
if entry.ints [ existIndex ] == existValue and entry.ints [ pointerIndex ] == pointerValue then
if type ( value ) == ' table ' and rawget ( value , ' mt ' ) == rawget ( GlobalTable , mt ) and entry.value == rawget ( value , ' key ' ) then
--if setting it to the same table it already is, then don't do anything
return
end
deletePersistent ( entry.value )
end
if not value then
2014-11-16 18:41:11 -07:00
--print('__newindesx: delete')
2014-11-16 15:06:59 -07:00
--delete
2014-11-16 18:41:11 -07:00
for i , child in ipairs ( dfhack.persistent . get_all ( prefix .. rawget ( theTable , ' key ' ) ) or { } ) do
2014-11-16 15:06:59 -07:00
if child.value == key then
child : delete ( )
end
end
entry : delete ( )
return
elseif type ( value ) == ' string ' then
2014-11-16 18:41:11 -07:00
--print('__newindesx: string')
2014-11-16 15:06:59 -07:00
entry.value = value
entry.ints [ pointerIndex ] = defaultValue
entry.ints [ existIndex ] = existValue
entry : save ( )
if isNew then
2014-11-16 18:41:11 -07:00
--print('new child!')
dfhack.persistent . save ( { key = prefix .. rawget ( theTable , ' key ' ) , value = key } , true )
2014-11-16 15:06:59 -07:00
end
return
elseif type ( value ) == ' table ' then
2014-11-16 18:41:11 -07:00
--print('__newindesx: table')
2014-11-16 15:06:59 -07:00
if rawget ( value , ' mt ' ) ~= rawget ( GlobalTable , ' mt ' ) then
if not isEmpty ( value ) then
error ( ' setting value to an invalid table ' )
end
2014-11-16 18:41:11 -07:00
--print('__newindesx: empty table')
2014-11-16 15:06:59 -07:00
--empty table: allocate a thing
entry.ints [ pointerIndex ] = pointerValue
entry.ints [ existIndex ] = existValue
entry.value = gensym ( )
entry : save ( )
if isNew then
2014-11-16 18:41:11 -07:00
--print('new child!')
dfhack.persistent . save ( { key = prefix .. rawget ( theTable , ' key ' ) , value = key } , true )
2014-11-16 15:06:59 -07:00
end
return
end
2014-11-16 18:41:11 -07:00
--print('__newindesx: table assignment')
2014-11-16 15:06:59 -07:00
entry.value = rawget ( value , ' key ' )
entry.ints [ pointerIndex ] = pointerValue
entry.ints [ existIndex ] = existValue
entry : save ( )
if isNew then
2014-11-16 18:41:11 -07:00
--print('new child!')
dfhack.persistent . save ( { key = prefix .. rawget ( theTable , ' key ' ) , value = key } , true )
2014-11-16 15:06:59 -07:00
end
return
else
error ( ' type(value) = ' .. type ( value ) )
end
end
setmetatable ( GlobalTable , GlobalTable.mt )
2014-11-09 23:03:20 -07:00
return _ENV