Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

TIC-80

Fantasy computer for making, playing and sharing tiny games. · By Nesbox

A way to save 8-character strings to persistent memory!

A topic by chronosv2 created Aug 25, 2017 Views: 930 Replies: 4
Viewing posts 1 to 3
(6 edits)

Hello! I've been doing the beginnings of tinkering around with TIC-80 after having used PICO-8 for about a month, and while I was looking through the documentation I realized I had run into quite a problem -- PICO-8 gives you 63 values to store user data to, and TIC-80 only gives you 7. This lead to an interesting problem, because one of the things I've been tinkering around with fantasy consoles for is a mini-RPG, complete with name entry.

In PICO-8 it was simple -- since you could only use capital letters I saved each letter as a number 1 to 26, then reconstructed the name with those values.

With TIC-80, I was scratching my head... until an idea hit me: You get 32-bit integers, which give you 2147483647 possible positive values (if the variables are signed, which I assume they are). And since TIC-80 supports lowercase letters, you can range 1 to 52, 53 if you allow for spaces. I decided to store the letters, grouped together, into integers. Since splitting 2147483647 into groups of 2 digits gives 21 47 48 36 47, I can't use the 21 (or 42 if unsigned) at the start. So I store 2 sets of 4 letters in integers ranging from 0 (nothing) to 53,535,353.

Here's the code to do this if you want to replicate the functionality -- you'll just need to implement these into save/load functions.
678 chars excluding comments.

ltrs="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "
local flr=math.floor
--Convert 2 32-bit integers into one 8-character string
function num2str(v1,v2)
 local n={0,0,0,0,0,0,0,0}
 local v2=v2 or 0
 local o=""
 for i=8,5,-1 do
  n[i]=v2%100
  v2 = flr(v2/100)
 end
 for i=4,1,-1 do
  n[i]=v1%100
  v1 = flr(v1/100)
 end
 for i=1,8 do
  local l=n[i]
  if l>0 then
   o=o..ltrs:sub(l,l)
  end
 end
 return o
end
--Take a string (str) up to 8 chars and turn it into 2 integers
function str2num(str)
 if #str>8 then error"String too long for str2num." end
 local s={}
 for i=1,#str do
  s[i]=ltrs:find(str:sub(i,i))
 end
 local o={}
 local t=""
 local j=1
 for i=1,#s do
  t=t..s[i]
  if i==4 then o[j]=flr(t*1) j=j+1 t="" end
 end
 if #s>4 then o[j]=flr(t*1) end
 o[2]=o[2] or 0
 return o
end

I'm sure there's a more efficient way to do this, but I'm used to the PICO-8 syntax. Also apologies for the variable shortening but that was a concession for the 64kb limit -- even though it would be best to do that after I was finished, for some reason I got into that mindset immediately and that's how I wrote it..

Variable 'o' is always "output", table 'n' are the numbers once split to turn into a string, table 's' is the opposite -- the numbers converted from their positions in the 'ltrs' string.

Hopefully people find this useful!

Edit Log:

There was an error being generated when you passed a 4-character string to str2num. 

    o[j]=flr(t*1)

became

    if #s>4 then o[j]=flr(t*1) end

The above line was intended to write the value of 't' to the first number if the length was 1, 2, 3 or 4 characters and write to the second number in any other case. Thanks to not writing the logic properly 't' was trying to be converted from an empty string to a number and caused an error. This edit fixes that problem.

There are 28 bytes of persistent memory. To save a name, use 5 bits per letter. That means 8 characters take 40bits = 5 bytes.

That certainly would be ideal, but unfortunately I'm not currently familiar or comfortable enough with binary operations to make it so. Even in its original PICO-8 form it could be taken from 128 bits down to the same 40. I know it's a terrible waste of space as it is now but I never pretended I really knew what I was doing with TIC-80 (or PICO-8 for that matter). Maybe one day it'll be that efficient but even if it's a jumping-off point for someone more skilled then it's done its job.

Would this help?

http://graphics.stanford.edu/~seander/bithacks.html

So it's taken some time, but after using the brick-wall method (banging my head against it until it works) I've finally managed to get a working save-safe (mostly) efficient storage metod that can store up to 10 characters. 11 or more characters will cause an overflow, so I've taken the liberty of putting a sanity check in for that.

I wanted to stick to my original rules (1-53, 1 is capital 'A', 27 is lowercase 'a' and 53 is space), meaning I needed 6 bits per character; after doing a little tinkering with binary (specifically trying to come up with a way to handle bitflags for saves) I came up with a method that seems to be working, but has some overhead (2-4 bits) caused by  endianness that I'm not sure how to fix.

Anyway, here's the code (Size: 710)

nums={3,34,44,41,40,41,45,48,9,9}
local fmt=string.format
--Size counting starts on next line
function combine(tab)
 local out=0
    if #tab > 10 then trace"Error: Input table too big."end
 for i=1,#tab do
        if tab[i] then
            if tab[i]>53 then tab[i]=53 end
            out=out+tab[i]
            out=out<<6
     end
    end
 return out
end
function bytify(num)
 local out={}
    local i=1
    while num>0 do
        out[i]=num & 0xFF
     num=(num >> 8) // 1
        i=i+1
    end
    return out
end
function rebuild(tab)
 local out=0
    for i=#tab,1,-1 do
        if tab[i] then
            out = (out << 8)
         out = out + (tab[i]&0xFF)
        end
    end
    return out
end
function retable(num)
 local out={}
 local i=0
    while num>0 do
     if num>0 then
         out[i]=num&0x3F
      num=(num>>6)//1
        end
        i=i+1
    end
    local n={}
    for i=1,#out do
     n[#out-(i-1)]=out[i]
    end
 return n
end
--Size counting stops at end of previous line
--Debug Stuff but shows how to use the code
trace("Starting name:\n "..table.concat(nums,','))
val1 = combine(nums)
trace("Integer out:\n "..fmt("%0X",val1))
val2 = bytify(val1)
trace("Bytes out:\n "..table.concat(val2,","))
val3 = rebuild(val2)
trace("Integer confirm:\n "..fmt("%0X",val3))
val4 = retable(val3)
trace("Confirm:\n "..table.concat(val4,','))

And a little proof that it's working (as best as I can make it work with my current skills):


This works in tandem with some of my above code (specifically this:

ltrs="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "
function str2num(str)
 local s={}
 for i=1,#str do
  s[i]=ltrs:find(str:sub(i,i))
 end
    return s
end

to convert the name string into a table. I haven't done the opposite conversion yet, but it wouldn't be that difficult, and you could even forgo the string step altogether if you operated strictly on a table and used a "name()" function or equivalent to convert the table into a string for use in your game.