This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
KingofGamesYami's profile picture

Autosaving Tables!

Started by KingofGamesYami, 06 October 2014 - 12:24 AM
KingofGamesYami #1
Posted 06 October 2014 - 02:24 AM
If you have ever tried to write a script that saves a lot of things, say a bank server, you've had to repeatedly save tables whenever they are modified. I have the solution! After a day or two of tinkering around with metatables & serializing, I've created a method that makes a table save every time it is edited. I got the idea from locked tables, where people where using __newindex to detect when it was being modified, and prevent it. Here I've modified that method to additionally save the table after modification.

pastebin get h0eW3iEA

code

local function serializeImpl( t, tTracking, sIndent )
    local sType = type(t)
    if sType == "table" then
    	t = getmetatable( t ).origin
        if tTracking[t] ~= nil then
            error( "Cannot serialize table with recursive entries", 0 )
        end
        tTracking[t] = true

        if next(t) == nil then
            -- Empty tables are simple
            return "{}"
        else
            -- Other tables take more work
            local sResult = "{\n"
            local sSubIndent = sIndent .. "  "
            local tSeen = {}
            for k,v in ipairs(t) do
                tSeen[k] = true
                sResult = sResult .. sSubIndent .. serializeImpl( v, tTracking, sSubIndent ) .. ",\n"
            end
            for k,v in pairs(t) do
                if not tSeen[k] then
                    local sEntry
                    if type(k) == "string" and string.match( k, "^[%a_][%a%d_]*$" ) then
                        sEntry = k .. " = " .. serializeImpl( v, tTracking, sSubIndent ) .. ",\n"
                    else
                        sEntry = "[ " .. serializeImpl( k, tTracking, sSubIndent ) .. " ] = " .. serializeImpl( v, tTracking, sSubIndent ) .. ",\n"
                    end
                    sResult = sResult .. sSubIndent .. sEntry
                end
            end
            sResult = sResult .. sIndent .. "}"
            return sResult
        end

    elseif sType == "string" then
        return string.format( "%q", t )

    elseif sType == "number" or sType == "boolean" or sType == "nil" then
        return tostring(t)

    else
        error( "Cannot serialize type "..sType, 0 )

    end
end

local function serialize( t )
    local tTracking = {}
    return serializeImpl( t, tTracking, "" )
end

local function makeInternalSaving( tbl, save )
	for k, v in pairs( tbl ) do
		if type( v ) == "table" then
			tbl[ k ] = makeInternalSaving( v )
		end
	end
	return setmetatable( {}, {
		__index = tbl,
		__newindex = function( self, key, value )
			if type( value ) == "table" then
				tbl[ key ] = makeInternalSaving( value, save )
			else
				tbl[ key ] = value
			end
			save()
		end,
		__len = function() return #tbl end,
		__metatable = { origin = tbl },
		} )
end

function makeAutoSaving( tbl, file )
	for k, v in pairs( tbl ) do
		if type( v ) == "table" then
			tbl[ k ] = makeInternalSaving( v )
		end
	end
	local savable = {}
	local function save()
		local f = fs.open( file, "w" )
		f.write( serialize( savable ) )
		f.close()
	end
	return setmetatable( savable, {
		__index = tbl,
		__newindex = function( self, key, value )
			if type( value ) == "table" then
				tbl[ key ] = makeInternalSaving( value, save )
			else
				tbl[ key ] = value
			end
			save()
		end,
		__len = function( ... ) return #tbl end,
		__metatable = { origin = tbl },
		__call = function() return tbl end,
	} )
end
How to useSimple: Paste the code into the top of your program and write this:

makeAutoSaving( YOUR_TABLE, FILE_NAME )

Alternatively, you can use it as a seperate file and load it as an api, in which case you can use this

api.makeAutoSaving( YOUR_TABLE, FILE_NAME )

Also, this method DOES prevent the use of pairs and ipairs, and the # operator is broken. I tried adding __len, but it doesn't want to work (although, that could be the emulator). Because of this, I modify the metatable in such a way that doing

getmetatable( t ).origin
will get you the actual table, however setting variables to this table bypasses my code entirely. It is mainly there to get the length of, although you can iterate through it if necessary. Here's an example of good iteration:

for k, v in pairs( getmetatable( t ).origin ) do
  if v == "some random thingy" then
   t[ k ] = "r" --#modify the variable holding the modified table
  end
end

As always, constructive criticism is appreciated.

Edit: Fixed bug with restoring previous tables!
Edited on 06 October 2014 - 12:12 PM
Mr. Bateman #2
Posted 06 October 2014 - 04:06 AM
Does this modify serializeImpl at all? It might be useful for RAMDisk, but i'm not sure if I like this approach or a time-based one.
KingofGamesYami #3
Posted 06 October 2014 - 01:47 PM
Does this modify serializeImpl at all? It might be useful for RAMDisk, but i'm not sure if I like this approach or a time-based one.
Yes, it modifies one line

        if sType == "table" then
                t = getmetatable( t ).origin --#have to add this or it won't find anything!!