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)
5



[Please log in to post a comment]