Log In  


Is there a way to display a PICO-8 number as a 32-bit unsigned integer?

I considered implementing a password system in a demake I'm making, but it requires me to interpret 4 bytes as an unsigned 32-bit int. I can't find a way to do that though.

Has anyone made a library to do this? If there's no native support, I assume you need to do some arithmetic that operates on an intermediate string representation of the number.

Note: I don't need to do 32-bit arithmetic. I just want to get the decimal representation (as a string is fine) of a number, as if it were an unsigned 32-bit int.



Would a hex printout be acceptable? I assume for a password system you'd want to display and read back the values and doing it in hex greatly simplifies both operations.

Here's what I came up with.

hex_digits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}

function u32hex(value)
  mask = 0x0.000F
  str = ""

  for i = 7, 0, -1 do
    str = str .. hex_digits[shl(band(lshr(value, i * 4), mask), 16) + 1]
  end

  return str
end

function _init()
  value = 0b1011011101101001.1010001101110111

  print(u32hex(value))
end

There's a tostr() you can use now, which converts numbers (among other things) to strings. If you add a ",true" argument, it converts a number to fixed-point hex, e.g.:

> print(tostr(12345.6789,true))
0x3039.adcc

So you can use that to quickly produce just the raw 32-bit hex:

function u32hex(value)
  local s=tostr(value,true)
  return sub(s,3,6)..sub(s,8)
end

> print(u32hex(12345.6789))
3039adcc

That's useful to know. I didn't realize tostr() could give an unsigned representation like that.


Yeah, I found that tostr() functionality, which is pretty interesting. Thanks for the 32-bit conversion!

However, hex isn't really acceptable since this is a demake; the passwords should ideally be compatible with the original game's passwords, which are in decimal.

Specifically, it takes a byte representation of the level, interprets it as a series of 32-bit unsigned integers (in little-endian), pads those integers and reverses all the digits.


Meh, the lack of unsigned math on PICO-8 makes the code a lot uglier, but I've done worse.

function u32dec(v)
	local s,c,i,d="",0,0
	if v<0 then
		v-=0x8000
		-- output digit offsets: 0x80000000 or 2147483647, backwards
		d={8,4,6,3,8,4,7,4,1,2}
	else
		-- no output digit offsets
		d={0,0,0,0,0,0,0,0,0,0}
	end
	repeat
		i+=1
		c+=d[i]+v%0x.000a/0x.0001
		s=(c%10)..s
		c=flr(c/10)
		v/=10
	until v==0
	return s
end

> print(u32dec(0))
0

> print(u32dec(1))
65536

> print(u32dec(0xffff.ffff))
4294967295

If this is the only thing you're using it for, then you could build the padding and reversing into the function:

function u32dec_pad_rev(v)
	local s,c,i,d="",0,0
	if v<0 then
		v-=0x8000
		d={8,4,6,3,8,4,7,4,1,2} -- 2147483648 backwards
	else
		d={0,0,0,0,0,0,0,0,0,0}
	end
	repeat
		i+=1
		c+=d[i]+v%0x.000a/0x.0001
		s=s..(c%10)
		c=flr(c/10)
		v/=10
	until i==10
	return s
end

> print(u32dec_pad_rev(0))
0000000000

> print(u32dec_pad_rev(1))
6355600000

> print(u32dec_pad_rev(0xffff.ffff))
5927694924

@Felice: Thanks a lot, this is great! Although, did you forget to put in the actual padding code in that second function there? I don't get any padding when I called that function.

I think I've done a logical error somewhere else when making the numbers to convert, though, since all the passwords I generate are invalid :P I'm pretty sure I'm messing up with the endianness somewhere, haha. But I'll probably figure it out.

It's interesting that PICO-8 as a fantasy console has the restriction that all numbers are 16:16 fixed point, and that it's hard to do unsigned int arithmetic. It's the opposite of the restriction I'm used to from actual retro consoles like Game Boy, where all numbers are 8-bit and anything else is really hard to do, haha. (There's some very basic 16-bit arithmetic built in there though, since memory addresses are 16-bit.)


Oops! My bad, a last-second edit broke the loop condition in the padded version. It should have been "repeat ... until i==10". I fixed it.


Actually, it occurs to me that I'm wasting cycles constructing those digit offset tables on every call when they're effectively constants. This is better:

_offset_80000000={8,4,6,3,8,4,7,4,1,2}
_offset_00000000={0,0,0,0,0,0,0,0,0,0}

function u32dec(v)
	local s,c,i,d="",0,0
	if v<0 then
		v-=0x8000
		d=_offset_80000000
	else
		d=_offset_00000000
	end
	repeat
		i+=1
		c+=d[i]+v%0x.000a/0x.0001
		s=(c%10)..s
		c=flr(c/10)
		v/=10
	until v==0
	return s
end

function u32dec_pad_rev(v)
	local s,c,i,d="",0,0
	if v<0 then
		v-=0x8000
		d=_offset_80000000
	else
		d=_offset_00000000
	end
	repeat
		i+=1
		c+=d[i]+v%0x.000a/0x.0001
		s=s..(c%10)
		c=flr(c/10)
		v/=10
	until i==10
	return s
end

And, honestly, you could do the full functionality with one configurable routine:

_offset_80000000={8,4,6,3,8,4,7,4,1,2}
_offset_00000000={0,0,0,0,0,0,0,0,0,0}

function u32dec(v,pad,rev)
	local s,c,i,d="",0,0
	if v<0 then
		v-=0x8000
		d=_offset_80000000
	else
		d=_offset_00000000
	end
	repeat
		i+=1
		c+=d[i]+v%0x.000a/0x.0001
		s=rev and s..(c%10) or (c%10)..s
		c=flr(c/10)
		v/=10
	until i==10 or not pad and v==0
	return s
end

> ?u32dec(1)
65536

> ?u32dec(1,true)
0000065536

> ?u32dec(1,false,true)
63556

> ?u32dec(1,true,true)
6355600000

:)


Thanks for posting your solution Felice. I hadn't been able to figure out how to correctly handle negative numbers so your code was very helpful.

I took a try at shortening it a bit based on the observation that after the first division by 10 the number would be small enough to fit in the lower 31 bits.

I got it to match yours in most cases, but then I realized that on some of the mismatches, mine is wrong, and in some, yours is wrong. (>>> comes from a sanity check in python)

INFO: mismatch 0x917a.48dd
INFO: theirs 440710365
INFO: mine 2440710365
>>> 0x917a48dd
2440710365

INFO: mismatch 0xc06c.71a0
INFO: theirs 3228332448
INFO: mine 3228332458
>>> 0xc06c71a0
3228332448

I haven't figured out the bugs, but I thought I'd post it before I went to bed in case someone wanted to take a look.

function my_u32dec(v)
  local s,c,n="",0,v<0
  v=band(v,0x7FFF.FFFF)
  repeat
    c+=v%0x.000a/0x.0001+(n and 8 or 0)
    s=(c%10)..s
    c=flr(c/10)
    v/=10
    if n then
      v+=0xccc.cccc + (c and 0x.0001 or 0)
      c=0
      n=false
    end
  until v==0
  return s
end

_offset_80000000={8,4,6,3,8,4,7,4,1,2}
_offset_00000000={0,0,0,0,0,0,0,0,0,0}

function u32dec(v,pad,rev)
  local s,c,i,d="",0,0
  if v<0 then
    v-=0x8000
    d=_offset_80000000
  else
    d=_offset_00000000
  end
  repeat
    i+=1
    c+=d[i]+v%0x.000a/0x.0001
    s=rev and s..(c%10) or (c%10)..s
    c=flr(c/10)
    v/=10
  until i==10 or not pad and v==0
  return s
end

function _init()
  local tests = 100
  for i = 1, tests do
    local v = rnd(0x7FFF)
    if rnd() < 0.5 then
      v *= -1
    end

    local theirs = u32dec(v)
    local mine = my_u32dec(v)
    if theirs ~= mine then
      printh("mismatch "..tostr(v, true))
      printh("theirs "..theirs)
      printh("mine "..mine)
    end
  end
end

Of course I've kept looking at it after I said I would go to bed. Mine was wrong because I forgot 0 is true in Lua, and yours was wrong only when padding was off and the last digit was 2 because v would become 0 and terminate the loop before the 2 got added on. I copied over your padding and reversing code and now the two versions always agree.

function my_u32dec(v,pad,rev)
  local s,c,n="",0,v<0
  v=band(v,0x7FFF.FFFF)
  repeat
    c+=v%0x.000a/0x.0001+(n and 8 or 0)
    s=rev and s..c%10 or c%10 ..s
    c=flr(c/10)
    v/=10
    if n then
      v+=0xccc.cccc + (c>0 and 0x.0001 or 0)
      c=0
      n=false
    end
  until #s==10 or not pad and v==0
  return s
end

_offset_80000000={8,4,6,3,8,4,7,4,1,2}
_offset_00000000={0,0,0,0,0,0,0,0,0,0}

function u32dec(v,pad,rev)
  local s,c,i,d="",0,0
  if v<0 then
    v-=0x8000
    d=_offset_80000000
  else
    d=_offset_00000000
  end
  repeat
    i+=1
    c+=d[i]+v%0x.000a/0x.0001
    s=rev and s..(c%10) or (c%10)..s
    c=flr(c/10)
    v/=10
  until i==10 or not pad and v==0 and d~=_offset_80000000
  return s
end

function _init()
  -- printh(my_u32dec(0xc06c.71a0))
  local tests = 100
  local mismatches = 0
  for i = 1, tests do
    local v = rnd(0x7FFF)
    if rnd() < 0.5 then
      v *= -1
    end

    local theirs = u32dec(v)
    local mine = my_u32dec(v)
    if theirs ~= mine then
      mismatches += 1
      printh("mismatch "..tostr(v, true))
      printh("theirs "..theirs)
      printh("mine "..mine)
    end
  end
  printh(tostr(mismatches).." mismatches")
end

NEW! IMPROVED!

I noticed I could do an unsigned /10 with a logical shift right and a /5, so that cured the signed division problem, and your note about everything being okay after the first divide made me realize that I could solve the signed modulo problem by offsetting the initial carry value properly when negative.

Also, the issue you noted above is better-solved by checking not just the value for zero, but also the carry.

function u32dec(v,pad,rev)
	local s,i,c="",0,(v>=0 or v==0x8000) and 0 or v%0x.000a<0x.0004 and 6 or -4
	repeat
		i+=1
		c+=v%0x.000a/0x.0001
		s=rev and s..(c%10) or (c%10)..s
		c=flr(c/10)
		v=lshr(v,1)/5 -- unsigned /10
	until i==10 or not pad and v==0 and c==0
	return s
end

Tested at length against a reference version, with both specific and random numbers. Seems good.

Please confirm my results, though. :)


Side note: Interesting discovery: PICO-8 treats 0x8000 as -32768 for division, but +32768 for modulo.


Nice. Those are clever improvements, and the code looks very nice now.

Tobiasvl, it's very cool that your password system will be compatible with the original game. I didn't realize that was one of the requirements. I hope you get it working :)


I did get it to work!! The last version from @mrjorts worked, I got it up and running just a couple of hours ago - I had to seek out and squash some bugs in my own code first where I generated the actual bytes to convert with your algorithms, where I'd used the wrong endianness. If you're interested, this is the algorithm: https://jroatch.xyz/2011/blog/polarium-password-encoding (and the game is Picolarium, which has been posted here on the BBS already).

I'll put in the new version from @Felice, and then I'll also have to implement a nicer UI for the level editor, hehe. But the level editor works and exports levels correctly! I'll still have to import levels based on the 32-bit numbers, but I'll probably manage that myself now. Thanks a bunch, both of you!


Always happy to be of service. :)


If anyone's just itching for more challenges like this one, how about the reverse algorithm?

In other words, given 10 digits (either as a zero-padded string of length 10 or, for simplicity's sake, just a sequence of ints that we can assume are positive numbers <= 9), convert it to an unsigned 32-bit integer.

Stuff like this apparently isn't my strongest suit...


That one is easier, thanks to builtin str->int conversion:

function u32(s)
  s=sub("0000000000"..s,-10)
  return sub(s,1,4)*0xf.424+sub(s,5,7)*0x.03e8+sub(s,8)*0x.0001
end

Basically, hack it up into pico8-integer-safe values for ones, thousands, and millions, then multiply by 1>>16, 1000>>16, and 1000000>>16 and add the results.

Note this solution assumes the incoming string contains only a valid unsigned 32-bit decimal integer between 0 and 4294967295 (0xffffffff). Bad input will, of course, produce bad output (GIGO).

Note that, with the neat trick where you can leave off parens on a function call if the only argument is a string literal, you can type this:

hi_scores =
{
  { "fel", u32"1000000000" },
  { "hrp", u32"6245396" },
  { "drp", u32"5400823" },
  { "foo", u32"4141237" },
  { "nub", u32"10" }
}

or

> print( u32"16384" )
0.25

Perfect, thank you so much. That was pretty simple.



[Please log in to post a comment]