Log In  

I think table serialization is a pretty well-known token-saving technique but I hadn't seen anyone post one like this that just operates via peek and poke rather than using strings, so I thought I'd share my table serializer and deserializer here (originally written for PIZZA PANDA) in case this is useful to anyone.

The serializer writes binary data directly to memory so you can use the output however you want, e.g. you can use the "storing binary data as strings" technique to store it as a string, but you could also just store it anywhere in the cart's data.

The deserializer (the part you need to include in your final cart) is 170 tokens and it similarly reads bytes directly from memory.

The serialized format is pretty efficient and uses data types which are more specific than Lua itself, in order to save storage space; each one of the following is a separate "type":

  • 8-bit integer
  • 16-bit integer
  • full "number" (32 bits)
  • boolean true
  • boolean false
  • string
  • empty table
  • array
  • table

This way if your table has a bunch of little 8-bit integers in it, you're not storing a bunch of whole 32-bit numbers for no reason.

The serialized format has the following limitations in order to keep the deserializer small:

  • max 255 properties in a table
  • max 255 characters in a string
  • no "function" type support

Serialize Table

function serialize_table(addr, t, isarray)
    poke(addr, count_props(t))
    addr += 1

    if isarray then
        for v in all(t) do
            addr = serialize_value(addr, v)
        end
    else
        for k, v in pairs(t) do
            addr = serialize_value(addr, k)
            addr = serialize_value(addr, v)
        end
    end

    return addr
end

function serialize_value(addr, v)
    if type(v) == "number" then
        if v & 0x00ff == v then
            poke(addr, 0)
            addr += 1
            poke(addr, v)
            return addr + 1
        elseif v & 0xffff == v then
            poke(addr, 1)
            addr += 1
            poke2(addr, v)
            return addr + 2
        else
            poke(addr, 2)
            addr += 1
            poke4(addr, v)
            return addr + 4
        end
    elseif type(v) == "boolean" and v == true then
        poke(addr, 3)
        return addr + 1
    elseif type(v) == "boolean" and v == false then
        poke(addr, 4)
        return addr + 1
    elseif type(v) == "string" then
        poke(addr, 5)
        addr += 1

        local len = #v
        assert(len <= 255, "string must be <= 255 chrs")
        poke(addr, len)
        addr += 1

        for i = 1, len do
            poke(addr, ord(v[i]))
            addr += 1
        end

        return addr
    elseif type(v) == "table" then
        if is_empty(v) then
            poke(addr, 6)
            return addr + 1
        elseif is_array(v) then
            poke(addr, 7)
            return serialize_table(addr + 1, v, true)
        else
            poke(addr, 8)
            return serialize_table(addr + 1, v)
        end
    end
end

function count_props(t)
    local propcount = 0

    for k, v in pairs(t) do
        propcount += 1
    end

    return propcount
end

function is_array(t)
    return #t == count_props(t)
end

function is_empty(t)
    for k, v in pairs(t) do
        return false
    end

    return true
end

Deserialize Table

-- 170 tokens
-- limitations:
-- * max 255 properties in a table
-- * max 255 characters in a string
-- * no "function" type support

function deserialize_table(addr, isarray)
    local t, propcount, k, v = {}, @addr
    addr += 1

    for i = 1, propcount do
        if isarray then
            k = i
        else
            k, addr = deserialize_value(addr)
        end
        v, addr = deserialize_value(addr)
        t[k] = v
    end

    return t, addr
end

function deserialize_value(addr)
    local vtype, v = @addr
    addr += 1

    if vtype == 0 then     -- 8-bit integer
        return @addr, addr + 1
    elseif vtype == 1 then -- 16-bit integer
        return %addr, addr + 2
    elseif vtype == 2 then -- number
        return $addr, addr + 4
    elseif vtype == 3 then -- boolean true
        return true, addr
    elseif vtype == 4 then -- boolean false
        return false, addr
    elseif vtype == 5 then -- string
        local len = @addr
        addr += 1

        v = ""
        for i = 1, len do
            v ..= chr(@addr)
            addr += 1
        end

        return v, addr
    elseif vtype == 6 then -- empty table
        return {}, addr
    elseif vtype == 7 then -- array
        return deserialize_table(addr, true)
    elseif vtype == 8 then -- table
        return deserialize_table(addr)
    end
end

btw if you want to see more context for how I used this in an actual project, here is the source code of that project: https://github.com/andmatand/pizza-panda

also here is a quick example:

-- step 1. do something like this in your build cart
t1 = {
  -- put lots of stuff in here
}

-- this returns the address right after the end of what
-- was stored so you can store multiple tables in a row
addr = serialize_table(0x2000, t1)

t2 = {
  -- another table
}
addr = serialize_table(addr, t2)

-- etc.

-- step 2. do something like this in your final published cart
t1, addr = deserialize_table(0x2000)
t2 = deserialize_table(addr)
P#137585 2023-11-17 23:25 ( Edited 2023-11-18 00:22)


[Please log in to post a comment]