Log In  
:: Unfold ::

Here's a very weird bug I just discovered accidentally.

  • Visit a page with a comment that contains a bullet point list, like this (and possibly a page 1|2 section at the bottom, not sure).
  • Click and drag your mouse to highlight the text.
  • While still holding drag, move your mouse cursor to the right. A horizonal scroll bar will appear along the bottom, apparently equal to the length of the largest line of text in the bullet point list, if there wasn't any word wrapping. It seems very weird but related to the length of the text, so I'm just going to ramble on here for a bit to prove my point. Doesn't seem to work with the post preview though, so maybe this is comment-only.

More funny than anything, tbh. You can see this bug in the wild on this page: https://www.lexaloffle.com/bbs/?pid=46706#p

Bug occurs in Chrome on Windows 10.

P#116079 2022-08-21 02:28 ( Edited 2022-08-22 21:46)

:: Unfold ::

Especially in tweetcarts, using sub(s,i,i) has always felt like a waste of so many chars, and less intuitive than the standard s[i] in most other programming languages. Also, because of the massive amount of string parsing that happens in Pico-8 for data compression, and the fact that Lua does support this feature with a small amount of work, I'd really like this feature added. chr and ord were a step in the right direction (as previously, these needed two lookup tables generated at startup), but I think with the recent addition of sub(s,i,_), it's clear that a major string parsing use case is missing.

See this link, the implementation of __index that will still support t:func(a) is at the bottom of the page:

Why should this be added to the API though? Unfortunately, the main issue with people implementing this feature themselves seems to be that the basic Lua string library isn't present. getmetatable('') returns nil in Pico-8 code, so you can't assign functions to it. You could assign a custom metatable via setmetatable that re-implements all of the string functionality in every string created, but that would destroy your token count, as it would have to cover concatenation, construction, tostr, etc. And since the carts that usually need this are character limited (tweetcarts and maxed out carts), you don't have the space to do it yourself anyway.

I'm suggesting injecting a header in the Lua code that adds this functionality to the string type, for zero end-user token/char cost. See this discussion thread for an example: https://www.lexaloffle.com/bbs/?pid=110420#p . I'm guessing the header code doesn't look like this anymore, but effectively just putting the code in the same place as all() and add() and so on.

Since sub/chr/ord are already function calls, I feel like the metatable overhead of a pure-Lua implementation won't be an issue, but presumably there are more optimized ways to go about this in C as well. String concatenation might take a hit as well, and I'm curious if that's why this hasn't been added yet. I've been here long enough to remember when 0.1.2 let you do this via getmetatable('').__index = function(str,i) return sub(str,i,i) end (it was released with some kind of full API exposed), so I'm guessing it's a much easier fix in the internal systems than it would be elsewhere.

P#116078 2022-08-21 01:53 ( Edited 2022-08-21 01:54)

:: Unfold ::

Okay, so after realizing that c0-e5 was 65 total notes, I did a little digging into this. The sfx format specifies that pitch is a number from 0-63, so why does e5 work when you press [p] in the editor, while octave 4 is selected? The sound is higher than d#5, too.

First, let's try some p8scii. In the console, print("\a0szd#5e5") cstore(0x3200,0x3200,68) both plays a constant d#5d#5, and stores that in sfx 0. Then go to the sfx editor and select octave 4, and change it to d#5e5 via pressing [p], and press space to play back. Two different notes!

Now, go back to the console and do sfx(0). This gets the very weird behavior of d#5c1, almost like a default, but not the same default that \a has.

Then, go back to the editor and add a c1 directly below the e5, and press space for playback. Three notes. Then go back to the console and run sfx(0) again. This time you get a weird fading blend between the two c1 notes, almost like pico-8 sees them as two separate pitch values internally.

Finally, save the cart, reboot, and reload. The e5 is saved as a d#5 on the cart, which means playback changes again!

This could all be fixed by removing the ability to add e5 in the sfx editor, but there are some inconsistencies still with how it's handled in p8scii (since e5-b5 become d#5, but all other notes out of range (i.e. e6) become c5).

Lots to tweak here. It was an interesting glimpse into the internal systems that do the actual audio playback.

P#115121 2022-08-01 18:29 ( Edited 2022-08-29 02:57)

:: Unfold ::

I was going through some old code and discovered a change in behavior. Previously (version 0.2.3 and earlier?), if you did sub(str,1,nil), you would get the full string. Now, you only get the first letter of that string. I believe the recent API update for optimization is incorrectly evaluating nil args as 0, both for start and end.

This behavior was useful if you wanted to have a table of string lengths for tokenized text display, as in ?sub(dialog,1,dialog_token[time]). Once the time var had reached above the size of the dialog_token table, the default table nils had the same behavior as showing the full string. I can fix with a min(#dialog_token,...), so not crucial, but the old behavior feels more intuitive to me.

P#114753 2022-07-24 15:51

:: Unfold ::

Okay this is going to be a hard bug to resolve, because it's tough to pin down. I'm on Win 10 64bit, and for the past few versions of Pico-8, games have been having this input dropping issue every so often. It's definitely in v0.2.4 (original, no b c d), and may be in v0.2.3 or earlier, but the recent stat(28) raw input addition makes me think it started happening around that time.

What happens: you're playing a game normally, then you lose all control for a few seconds. The game is still updating and drawing, but mashing keyboard keys does nothing. I could record the next time it happens, but it's random AND rare, and all you'd see is the game running with the player standing still. After a few seconds, input starts working again.

Why this sucks to debug: not only do you have to be paying active attention to the game, you also don't know when it's going to happen. Sometimes you just think you messed up and pressed the wrong buttons, since it's hard to be sure, and everything is normal after a few seconds. But this is a critical btn/btnp bug that's severely reducing the quality of gameplay, so it needs to be fixed. The best I can do to help is say that I'm playing in windowed mode on the left half of the screen, there are no alerts or Windows messages that might be grabbing focus (as far as I know), and the issues occur long into a gameplay session (sometimes a half-hour or more between drops?).

Could be related to integer overflow, though this feels more random than that. The Lua code of a few games I checked are just btn/btnp'ing every frame, so it's not user error. Could also be the window losing focus, but I run this computer lean and don't have any kind of discord/antivirus/other popups ever show up in the notification bar, and pico-8 was focused in the taskbar. I'll try adding read_controllers_in_background=1 to see if that fixes things, but it may take a while for me to encounter the bug again, and it was happening before v0.2.4c was ever posted. I'm honestly not sure what else it could be, no other program on my computer has this input dropping issue.

P#111477 2022-05-07 20:36 ( Edited 2022-05-07 20:37)

:: Unfold ::

On a fresh boot of pico-8, this glitchy grid effect happens exactly once, the first time that secret colors are used in a screen palette. Then never again.

I discovered this in my latest cart, and was wondering if this is an intentional indicator of "hardware mode change", or a bad sign that something is going seriously wrong. The starting palette of this cart uses only normal 0-15 colors, as does the "raw" palette you see when you press up. However, every other palette will show this glitch once for about 0.5 seconds, before the screen returns to normal. Re-running the cart does not show it ever again, until you reboot pico-8 completely.

Using if(pi==3) poke(0x5f2e,1) stop() poke(0x5f2e,0) at the end of the _draw function means that the effect will happen for the same amount of time without any game code running at all, which makes me think that this visual glitch is intentional. Note also that without the poke to make the screen palette persist in the editors, there is no glitch effect, likely because it doesn't flip() with secret colors active. Any idea why this is happening, @zep?

P#104519 2022-01-08 02:51

:: Unfold ::

by shy
Cart #aaconv-4 | 2022-01-08 | Code ▽ | Embed ▽ | No License

I was reading up on Bit Arrays to learn more secret bitwise knowledge, and stumbled onto a very clever way to count bits, via Hamming weights and a magic multiplication number. Short story is that this led to efficient fullscreen Anti-Aliasing for 1-bit screen data (colors 0 and 1 only).

Other convolution shaders are easily doable with similar techniques. This one takes every pixel and adds weighted amounts of the neighbors in this way:


You might be able to do gaussian blurs with a larger sampling neighborhood, or things like Conway's Game of Life. I'm really surprised how efficient some of these techniques are, even on pico-8. Counting every single 1 in the entire screen data is only ~6% CPU @ 30fps.


I cleaned up the interface, added color palettes, and optimized a bit, so this should be finished now. Managed to get a super-optimized version of the AA working via loop unrolling, but also added @Felice 's adjustment to the version with fewer tokens. There's a secret demo-like effect to play around with now too, discovered via offset bugs. Calling this experiment done!



Wanted to test the limits of optimization, at the cost of tokens. Original AA function is 101 tokens, faster version is 434, fastest version is 1397, but officially runs twice as fast! (9% CPU at 30fps) I don't think I can push this idea any further, and there are diminishing returns, so the middle speed is probably the best choice in most cases.


Very slight changes based on @freds72's optimization suggestion. Times and token counts are down across the board!


3%CPU@60fps improvement to the middle function for +30 tokens (or a 1% improvement for -24 tokens, commented out in the code). The max speed function becomes less relevant!

P#104468 2022-01-07 14:14 ( Edited 2022-01-08 18:38)

:: Unfold ::

Since the introduction of large maps, I think there's one feature missing that would help many pico-8 games: the ability to flip and rotate individual tiles in the map. Games often use up a lot of their sprite space just for the map data, duplicating tiles for the different boundaries needed, or using manual spr() calls to get the necessary x_flip and y_flip needed. Since there's no visible editor to worry about anymore, this could be a quick patch (via a poke value to an alternate map() drawing mode), and would make map() much more useful.

Flip map mode, option one: 0x8000+ is used for map data (same format used now), 0x2000 is the flip data for that map data, using 2 bits per tile (x_flip, y_flip).

Flip map mode, option two: the 1 byte of map data stored anywhere now references a cell # in the first 16 sprites (n 0-15, 4 bits), x flip (1 bit), y flip (1 bit), and 90 degree clockwise rotations (2 bits). Like so:

map data: 1 byte per tile (same as now)
nnnn = sprite number, 0 to 15
x = flip horizontally?
y = flip vertically?
rr = clockwise rotation, in 90 degree increments (00: no rotation, 01: 90deg, 10: 180deg, 11: 270deg)

This limits you to 16 tiles for map() calls, but those tiles are also much more useful now, with hundreds of drawable combinations.

Either of these two options would be very helpful for reducing the sprite cost and token cost of nice looking maps. It would be great to have an option for easy rotations that doesn't involve tline calls, too.

P#103189 2021-12-20 18:04

:: Unfold ::

During some glitchy sandstorm effect testing for a tweetcart, I managed to hard crash the Pico-8 executable completely. This minimal code reproduction does it consistently on my machine after about 10 seconds of moving around. It seems to be related to the interaction between sin, rnd, and line, because removing those line()s fixes things. Likely a specific hex value in line() blows up somewhere, and rnd hits it occasionally with this setup?

Warning: I have no idea what this code will do on the BBS. Run at your own risk, as it consistently crashes the PICO-8 exe completely in just a few seconds!

Cart #crashlinernd-0 | 2021-11-27 | Code ▽ | Embed ▽ | No License

P#100879 2021-11-27 06:00 ( Edited 2021-11-28 19:06)

:: Unfold ::

Psst, hey kid, you wanna make a megaman? I'm not supposed to tell anyone this, but check this out.

Two questions I often see asked by Pico-8 beginners are how to animate your player sprite, and how to make them shoot bullets. When setting up your game objects for tasks like this, tables can scare a lot of new Lua coders away, because it can feel easier to just use a bunch of variables. Don't fall into this trap though! It'll make the code a lot harder to maintain as your project grows, and doesn't work for things like bullets at all.

Tables are an easy way to organize objects that get created and die, update, draw to the screen at the same time, etc. Getting more confident with all of the features of Lua is the first step to improving as a programmer (check out the Lua manual for more info), but for now I'll just go through a simple and efficient table structure that will work for many game designs. Before we get into game related stuff though, let's talk about the basics of tables.

How to turn tables

Tables can do a huge variety of things in Lua, and they're very easy to set up. Just use curly braces to group a set of values together, and then those variables can be accessed and updated more easily as a group. The default table constructor stores values in a list that you access via table_name[number], like so:

tbl = {1,2,3,4,5}   --five number values are stored in the variable called tbl, a table
print(tbl[1])       --print the first number (1)
print(#tbl)         --print the length of the table (5, because there are 5 total values)

Tables are 1-indexed in Lua, which makes a lot of programmers (me) complain, because they're used to other programming languages where lists start at index zero. But for beginner programmers, this just means that the first default entry in a table is stored at [1]. The last value is therefore stored at the same index as the length of the table, so this works:

print(tbl[#tbl])    --will print the last number in the table (5)
tbl[#tbl+1] = 6     --adds a new value (6) to the end of the list. same as add(tbl,6)

The length operator (#) only counts the list-style table variables I used above, but this is not the only way that you can make good use of tables. Sometimes you don't want a list, you want an object.

obj = {x=5,y=16,w=4,h=4} --create an object with a position, width, and height
obj.x += 1               --add one to the object's x value, and update it (+= is add & set)
obj.jump = true          --create a new value in the table, and set it to something (true)
print(#obj)              --be careful, this will print 0! named variables aren't counted.

These tables are like objects, with named variables of their own that you access via the dot (.) operator. You can even put tables inside of other tables and chain dot accesses, which is a great way to organize things and re-use code. The names make it easier to remember what your variables are, but they mean the table isn't a list-type table anymore. It's an object.

However, obj.x and obj[1] can exist in the same table, because obj.x is actually secretly obj["x"] behind the scenes. Both are just values stored at a named position, but the key to access x is a string of text this time, instead of a number. Every time you use the square brackets, you are asking Lua if something currently exists at that location in the table. You either get a value back, or you get nil if nothing is there.

print(obj.x)                       --after the code above, this will print 6
print(obj["x"])                    --this will also print 6, it's just another way to look at x
if(obj.jump) print(obj.randomtypo) --this will print nil, since nothing exists at obj["randomtypo"]

There is nothing wrong with that last line of code, and Lua will happily run it without complaining at all. This is critical to understand. If something doesn't exist in a table, the value you get back is just nil, a special value that means "nothing". If you see errors mentioning nil, it probably means you made a typo on that line, or you accidentally deleted something in a table.

obj.x = nil           --uh oh, x doesn't exist anymore. you just deleted it
tbl[3] = nil          --very dangerous! the length of tbl might be 6 or 2 now...
deli(tbl,3)           --much safer, (#tbl) will always be 5 now

This is nice and all, but then how do you use tables for games? Let's go through a few practical examples for Pico-8, and a couple questions that get asked here about once a week.

Making bullets

Bullets are a simple kind of object with some special needs. They move every frame, and disappear after a while. You need to add new objects when the player shoots a new bullet, and remove them when they hit something or go off screen. Using tables, all of this is possible and easy:

objs = {}                    --a list of all the objects in the game (starts empty)
function objdraw(o)          --a basic function for drawing objects,
 spr(o.spr,o.x,o.y)            --as long as those objects have spr, x, and y values inside
function bulletupdate(b)     --a function for moving bullets a little bit at a time
 b.x += b.dx                 --x moves by the change in x every frame (dx)
 b.y += b.dy                 --y moves by the change in y every frame (dy)
 b.time -= 1                 --if bullets have existed for too long, erase them
 return b.time > 0           --returns true if still alive, false if it needs to be removed
function newbullet(x,y,w,h,dx,dy)--bullets have position x,y, width, height, and move dx,dy each frame
 local b = {                 --only use the b table inside this function, it's "local" to it
  x=x,y=y,dx=dx,dy=dy,       --the x=x means let b.x = the value stored in newbullet()'s x variable
  w=w,h=h,                   --b.w and b.h are also set to the function's w and h args
  time=60,                   --this is how long a bullet will last before disappearing
  update=bulletupdate,       --you can put functions in tables just like any other value
  spr=0,draw=objdraw         --bullets don't have special drawing code, so re-use a basic object draw
 add(objs,b)                 --now we can manage all bullets in a list
 return b                    --and if some are special, we can adjust them a bit outside of this function

function _draw()             --the game's draw function, only called 30 times/second when there's no lag
 for o in all(objs) do o:draw() end --o:draw() is the same as o.draw(o). this is useful here!
function _update()           --the game's update function, always called 30 times/second
 if(btnp(4)) newbullet(0,64,4,4,2,0)
 local i,j=1,1               --to properly support objects being deleted, can't use del() or deli()
 while(objs[i]) do           --if we used a for loop, adding new objects in object updates would break
  if objs[i]:update() then
   if(i!=j) objs[j]=objs[i] objs[i]=nil --shift objects if necessary
  else objs[i]=nil end       --remove objects that have died or timed out
  i+=1                       --go to the next object (including just added objects)

Copy and paste this code into Pico-8, and press Z/C to shoot!

This demonstrates two of the harder parts of creating bullets that work. For one thing, you need to have a list of bullets that changes as they get created and destroyed. For another, you can't use del()/deli() to remove them from the object table while you are updating the list of objects, because the position of all the table entries afterwards changes when you do that. You have to write custom while loop code that supports deleting objects correctly and efficiently.

This may seem complicated, and that's okay! One of the things that's important to understand about Pico-8 is that the code editor is not simple, and that gives you a lot of power to do interesting things. Lua is a fully functional programming language, it is not a reduced set of commands specifically for game design tasks. It is sometimes a lot harder to do simple things because of this, and you will have to learn Lua programming well to make anything except very basic games, but that is the audience that Pico-8 was designed for. Simple design constraints, but very complex and flexible code.

Anyway, we are using both kinds of tables in this example. Each bullet object is the second kind of table, with a group of named values (like b.x, b.y, etc). But maybe you also noticed that objs is the first kind of table, a sequence of values stored at numbered positions (objs[1], objs[2], etc). In general, this is the difference between an object-type table and a list-type table, so a list of objects would clearly need to use both.

Animating sprites

Let's tackle a trickier problem now, which is animating your object art. Since an animation is just a list of sprites displayed one at a time, and sprites are numbers in the spritesheet, we'll need a list-type table full of numbers for that. But some animations repeat, or play other animations when they are finished, which means they need more data than just a list of images to show. If only there was a special kind of object that could do both of these things at the same time...

This is what makes Lua really special. Since everything is a table, we can have a list-type table and an object-type table be the same thing, and solve both of these problems at the same time! Here's a chunk of Pico-8 code that supports framerates, looping animations, and automatic transitions between anims, and works with the object management from the previous code snippet too:

anims={                      --all character animations, described by name
 idle={fr=15,1,2},           --idle has a frame rate of 15, and loops between sprite #1 and #2 forever
 punch={fr=5,next="idle",17,18,19}, --a 3-image punch animation that returns to idle when done
 walkright={fr=5,4,3,4,5},   --you can reuse frames of art to save space. Walking 4,3,4,5,4,3,4,5,...
 --etc etc
function _init()
 player={ x=64,y=64,
  update=playerupdate,       --custom update function for the player object, described below _init
  spr=1,draw=objdraw }
 player.play="idle"          --start a new animation. this is all you need for the animate() function
 add(objs,player)            --bullets and the player are both updated and drawn together!
function playerupdate(p)
 if(p.state!="punch" and btnp(5)) p.play="punch" --you can only punch when not already punching
 animate(p)                  --you can animate any object just by setting play to something
 return true                 --still alive, so return true
function animate(p)
 if p.state != p.play then   --start a new animation
  p.state = p.play
  p.animindex = 1            --start with the first frame in the animation table
  p.time = 0                 --reset the timer
 elseif #anims[p.state] > 1 then --continue playing an animation with multiple frames
  p.time += 1
  if p.time >= anims[p.state].fr then --the current frame has been on screen for long enough
   p.time = 0
   p.animindex = (p.animindex % #anims[p.state]) + 1 --go to the next frame
   --this loops animations. "punch" becomes (current index % 3) + 1, so 1,2,3,1,2,3,1...
   if p.animindex == 1 and anims[p.state].next then --at the moment the animation restarts,
    p.play = anims[p.state].next                    --play something else instead
    p.state = p.play
 p.spr = anims[p.state][p.animindex] --lastly, update the current sprite number drawn to screen

Copy this code and paste it under the other snippet for bullets. Then press X to punch! (You'll have to draw some art though, or use my cartridge below.)

While this is a very simplified demonstration, hopefully you can see how powerful object management with tables is. You can play a new animation with one simple line of code anywhere, and add as many animations as you want to the anims table at the top. Want your enemies or bullets to animate too? Just add their animations to the same table, with slightly different names.

If you want bigger sprites, try making a new playerdraw() function that supports sprite width and sprite height, by adding new table variables to your player object. Something I've also done to save art for upwards walking animations is flipped the same image horizontally back and forth, and if you use the drawing function below instead, it's very easy. Just use negative numbers in the anims table now, they will draw the same image as the positive number, but flipped horizontally:

anims.walkup={fr=5,6,7,-6,-7} --you can add/change animations later if you want
function playerdraw(o)
 spr(abs(o.spr),o.x,o.y,o.sw,o.sh,o.spr<0) --abs() means the index drawn is always >= 0

It might make more sense to store your player's facing direction in your player object, too. Then you could flip horizontally-flipped animations via o.facing_left!=(o.spr<0), but that's a bit complicated to explain. Here's a cart with basic player movement and shooting, see if you can read through the code and understand the adjustments I made here.

Cart #shyguide-0 | 2021-09-20 | Code ▽ | Embed ▽ | No License

And one last tip for more advanced coders: you can unpack()/split() the animation frame numbers if you want to save on tokens. As long as unpack() doesn't have a comma after it, it will work just like a list of numbers would, for 4 total tokens no matter how long the animation is. (split"string" takes advantage of some weird Lua function calling semantics, but it still works. Read that Lua manual to find more tricks like this!)


Final words

This should get you started with the basics, but now you have to experiment! You can write new drawing or updating functions to add special animations or behaviors. You could check if a bullet hit something (with a rectangle check) and return false, to kill it before it times out. What if a bullet didn't move at all (dx=0, dy=0), and it just made a solid area of damage when it was created, like a fighting game hitbox? What if you made a bullet that created extra bullets while it animates? What if you wanted some objects to always be drawn on top of other objects, in a different draw layer?

These are all pretty simple adjustments to this basic object management system, which is why it's so great. Hopefully this guide helps some people organize their logic better, and makes complicated things like fancy animated bullets very easy to do.

But uh, don't tell anyone I told you this, kid. It's a secret that the old programmers have been hiding since 19XX. Definitely don't share this information with anybody else, or work together to make it better.

P#97572 2021-09-20 05:20 ( Edited 2021-10-02 18:54)

:: Unfold ::

Cart #catch128-0 | 2021-06-28 | Code ▽ | Embed ▽ | No License

X to restart, arrows to move. Catch only one of each!

Welcome to the Glyphmon cave, Nop. Professor Treename here, explaining how to find and catch all 128 unique creatures in this tiny game.

Explore the different areas and walk up to new Glyphmon to add them to your Character Set. But I'm not made of money Nop, you only have 128 Glyphballs to collect them all. If you catch any duplicates, you fail!

Your Glyph scope at the bottom shows you which Glyphmon you've already collected in the room, and any you haven't caught yet. Pay attention to color, and don't get tricked by colors that look similar! You can also re-enter rooms if you've scared any Glyphmon off the first time, to try again.

Don't be afraid to call it quits with a high score, it's difficult to find and collect them all. But if you're a perfectionist, here are some strategies to keep in mind:

  • Each room will always have the same Glyphmon, moving in the same direction. If you enter the room from different places, it can make certain Glyphmon easier to collect.

  • Be vigilant when you enter a new room, because sometimes you'll have to dodge a Glyphmon right away. Remember that you can always run away if you have to.

  • Some Glyphmon are rarer than others. The last few could be very hard to find!

This tiny game is a #tweetcart made for Tweet Tweet Jam 6. The entire source code fits in 560 characters / 310 tokens. Click the CODE tab underneath the cartridge to view the horrible nonsense code. :P

P#94187 2021-06-28 23:30

:: Unfold ::

Cart #borecode-0 | 2021-06-23 | Code ▽ | Embed ▽ | No License

Red Alert! The master hacker known only as The Stick has infiltrated the most secure server in the world. To prove yourself, avoid detection from the security field for as long as possible.

Your hacking skills allow you to dig through nearby firewalls to escape the lasers. Some say there are even secret digging strategies that help you further, like FAST TOGGLING and RISKY PUNCHTHROUGH. How long can you last?

This is a #tweetcart made for Tweet Tweet Jam 6. The entire source code (including gameplay, art, and sound effects!) is 557 letters of text. Click the CODE tab underneath the cartridge to view the horrible nonsense code. :P

Why? Because it's fun!

P#93910 2021-06-23 20:33 ( Edited 2021-06-23 22:46)

:: Unfold ::

The 0x5600+ area doesn't get reset properly when a new cartridge is loaded, or the current one is run again. Pretty easy to replicate, but here's a demo:


function _update60()
 if btnp(5) then
function _draw()
 for i=0,14,2 do
 for i=0,239 do
  print(chr(i+16),(i<112 and 2 or 0)+8*(i%16),10+8*(i\16),6)

Run once, and toggle between fonts with X. If you completely remove the first line of code, this still works. If you reload the cart and completely remove the first line of code before running it even once, this still works.

P#90266 2021-04-09 14:16 ( Edited 2021-04-09 14:32)

:: Unfold ::

A small cart I made a while back and just remembered to upload here, testing spirals and palette cycling. It's 241 tokens, and runs at 0.71CPU @ 60fps. Surprisingly cheap, considering the fastest render method was to just draw a bunch of circfills and memcpy half the screen. Could be great for a menu background!

by shy
Cart #hypfx-0 | 2021-02-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

I put a lot of tech into hiding the drawing and palette limitations of pico 8, so feel free to browse the code. Some of the exploration is also commented out in the cart, for anyone that's interested about alt method efficiency. :)

P#87590 2021-02-13 17:10

:: Unfold ::

Per this discussion, I wanted to mockup a potential new idea for the Pico8 editor, to better incorporate some of the undocumented features that are being used more and more often by people. The 32 color extended palette is the major one, which I still think does not play nice with the existing editors, for three reasons:

  • Repeatedly tweaking the colors in your art requires you to look up arbitrary numbers and re-type them into a line of code, which introduces a huge delay that makes doing this in Pico8 very unpleasant. Some tools have been coded by other people to help you with this in-game, but I'm talking about the editor itself. The current art tabs don't work with the alt colors at all, and color is important enough that this is kind of making them unusable in their current state. I feel like every game I've played in the past year here has tweaked at least one color from the starting 16 into an alternate, so it's a huge use case.
  • When you poke(0x5f2e,1) to preserve the palette in editor screens, it really messes up the code/sfx/music editor tabs, where colors are very important for communicating information. If I'm tweaking art and code at the same time, in that wonderful Pico8 way, I have to keep uncommenting and running a for loop that resets the palette and stops() at the top of my code tab, or just accept the weird colors in the code tab now. Duplicate colors can even make the text in the console completely disappear!
  • Running special code to get the editors to look right is really hacky, and may not even be possible if you've maxed out your tokens on the game code. Shouldn't this palette feature be something that is saved with the cart ROM permanently, and only affect specific areas of specific tabs? Shouldn't a fresh startup change the palette on load, and be easy to tweak even if your code is completely broken, and doesn't run at all?

This is my suggestion for a quick fix: a hardware settings tab (see the thunderbolt icon in the top bar). To avoid scaring newcomers, this tab might be hidden until you triple-click the sprite tab, or gets enabled with some other quick, documented shortcut. It enables access to a lot of the values in the hardware state that are commonly used in a "set it and forget it" kind of way. I think these settings should be saved in cart ROM, and booted up when a cart is loaded or directly run.

The idea is to use the scanline mode for two distinct palettes: ART and GUI. The top and bottom red bars would always use the GUI palette, as would the code/sfx/music/console tabs. ART would be used on scanlines in the sprite and map tabs only, like so:

(notice the red UI bars and gray tools, but correct palettes in the art scanlines)

This plan is not perfect (the art cursors still go weird, and the colors not being selectable on the sprite or map tab directly makes them less easily tweaked), but it goes a long way to making a 16-color palette from the 32 total colors a lot more usable in a limited display, as well as exposing some useful features that 99% of people don't know about right now. Without something like this, I have personally resorted to just not making art in Pico8 anymore, because importing a spritesheet is easier than coding with mostly arbitrary numbers every time I want to tweak the art palette.

Also, a good half of the time when I'm giving tech advice, it involves linking people to the memory page from the fan site. These undocumented features are very useful in a variety of ways, and I think mid-tier experienced people would appreciate more direct access to them than a code command. Especially since the hardware settings that get poked at the very beginning of carts (like mouse+keyboard access or screen mode) rarely ever get un-set afterwards, so it makes sense to save those settings in the cartridge.

Anyway, happy to hear comments and alternate ideas. I just think the second palette has grown much bigger than expected, and Pico8's editors aren't totally set up for it right now.

P#86042 2021-01-01 23:57

:: Unfold ::

This is a bug in 0.2.1b, and possibly earlier, causing crashes in BBS carts like Pieces of Cake. After the new add() functionality was added, complex or token-optimized code causes a hard crash with this error:

Note that having a space between these statements, as in "add(...) end", does not solve the parsing problem, but putting a newline between them sometimes does (no idea why). It's also possible that nested parentheses inside the add() call are messing up a regex parser, since this also consistently solves the crash:

local f=fn(sub(str,i,i),i)

As per @Felice, the bug is from an implied nil return from a function, which is apparently different than an explicit "return nil". add(t,nil) works, as should this (since it gets converted to a true nil through assignment):

function fn() end
local f=fn()

This fails:

P#79749 2020-07-22 05:10 ( Edited 2020-07-24 00:34)

:: Unfold ::

Posted this in the discord, but decided it was worth a repost here. If you're new to binary math, this is a decent introduction of the concepts, and how you can use them in Pico-8.

Introduction to binary numbers

Numbers in pico-8 are stored in binary as 16.16 (32 bits, half above the decimal point, half below). A good way to visualize these numbers is like this:
... 128 64 32 16 8 4 2 1 . 1/2 1/4 1/8 1/16 ...

Thus, 1001.01 is how the number 9.25 actually looks in binary. To convert whole numbers, use your fingers and count powers of two until you hit the last one less than your number, then subtract that much from the number and repeat the process (so 47 is 32+8+4+2+1 == 101111).
pico-8 lets you type numbers as binary (0b101111) or hexadecimal (0x2f), but they're all the same as 47. Use whichever conversion method makes it easiest for you.

Negative numbers are a whole other thing involving two's complement (so -1 is 0b1111111111111111 in Pico-8, because then adding 1 to it makes it zero). Probably not worth mentioning if you're just starting out, but something to keep in mind.
Now, with that out of the way, let's talk binary ops...

Binary operations

The important part about dealing with binary ops is that you have to remember that everything is powers of two. 1 is 0001, 2 is 0010, 4 is 0100, 8 is 1000, so these numbers will become very important for checking if a single bit is on or off. Your basic operations for changing bits are these:

(number & 0b0010)!=0 --is bit 2 on?
number = number | 0b0100 --turn bit 3 on
number = number & 0b1111111111111011.1111111111111111 --turn bit 3 off (but save the rest!)
number = number ^^ 0b1000 --toggle bit 4 on/off (make it the opposite of what it was, 0 or 1)

Shifting operators are great, because they're basically like multiplying or dividing by powers of two, but faster for the computer to use. They let you add bits together in the same number, by moving them to the same "slots". 0001<<3 == 1000, and 1000>>2 == 0010. Remember also that the way these numbers are stored means that <<3 is the same as multiplying by 2^3, or 8. >>2 is the same as dividing by 2^2, or 4.

The difference between a "logical right shift" and an "arithmetic right shift" has to do with what gets shifted into the empty slots. Remember that two's complement weirdness where -1 was stored as 1111111111111111? Well, >> preserves the top bit, so it still divides negative numbers properly by powers of two. >>> just puts 0s into the empty slots.

If you're working with bits and not math, you usually want the >>> operator instead.

Rotation is only really useful in rare cases, in my experience. Some random number generators use it because it doesn't discard anything or replace stuff with zeros, which are obviously less-random ways to adjust your data.

How to use all this

The most common case of bitwise ops is packing multiple numbers into the same number. If you only need numbers between 0 and 15, they can be stored in 4 bits, so there's 28 bits just sitting around doing nothing in each pico-8 number! You can therefore use shifting and bitwise ORs to make a number hold more than one piece of information inside it, which saves space. And that's actually how the screen pixels are stored in memory, since they can only have 16 color values per pixel. If you peek or poke into the screen memory at 0x6000 and above, you'll need to understand how to do some bit shifting to put the colors in the right places.

Just one last thing for bitwise, and then I'll end my TED talk. If you need binary flags (on or off values), you can obviously store that in just one bit. Things like pico-8's sprite flags are stored in this way, so checking bits by &'ing with powers of two will tell you if those individual bits are on or off directly. With the set-on or set-off examples I put above, you can adjust those true/false flags as well.

fget(spr,3) is basically the same as (flags[spr]&(0b00000001<<3))!=0.
That might seem more complicated in expression syntax, but if you go through it this should make sense.

Also note that I wrote a lot of binary syntax with leading zeros for clarity, but you don't have to do that. I much prefer hex myself, and when you get comfortable with it that becomes much easier to use. The conversion between hexadecimal and binary is very easy, because every four 0000's just become a number from 0-f, similar to how the numbers 0-9 are repeated to store bigger numbers like 10 or 100. It's probably outside of the scope of an introduction to these concepts, but once you learn this you have another tool you can use.

If you're interested in experimenting with any of this, I strongly recommend that you check out the Memory section of the Pico-8 manual. There are lots of cool functions to use there!

P#77830 2020-06-09 00:18

:: Unfold ::

So this is probably going to be a long, weird post, and is inspired by this post by @Mot a few weeks ago. I kind of dislike the way that Pico-8's cartridge RAM and Lua RAM exist in different worlds, and how everyone eventually uses huge strings to store data on the Lua side, when they run out of token space. It's fine to do that, and in fact I love that the option exists, but is it really a good default way for people to bypass the Pico-8 limitations? I have an idea I want to float to the community about this.

My proposal is to add two functions to the API, that convert between a Lua table and a cartridge bitstream. Using them to get your token count down would be a pre-publishing task, but it could be automated so that you only have to comment out the table to publish your game:

table = { true, x=15.5, [0]="yes", {"why?"}={"because","lua","supports","this","too"} }
len = luastore(0x0,table[,max_length]) --stores at 0x0 in cartridge RAM (not ROM), returns length of stored data
--cstore(0x0,0x0,len) --ROM is much slower and limits what these two functions can do, so keep it optional
table = luaextract(0x0[,len]) --use length if provided, otherwise it's at the start of the bitstream

In addition to giving people a standard way to store things without wasting a large number of tokens decoding the bitstream, this also does a lot to connect the Lua and cartridge worlds, and makes things like saving games trivial:

save_data_table = luaextract(0x5e00) --length is explicitly encoded at the start of the bitstream

But in addition to those advantages, it also gives people an opportunity to store data in a compressed bitstream that is standardized across cartridges, and gives a real use case for the 0x4300-0x5dff user data section of memory as temporary Lua storage! Writing your own encoders and decoders can be fun, but a fast, internal system that doesn't use your limited token space would open up this kind of development for a lot more newcomers. I think it caters to the idea of a cozy development environment, since it makes hitting that token limit barrier easily fixable for many creators, and less demoralizing.

Demo implementation

The rest of this post is a sample implementation of how Lua data could be serialized quickly, but with a decent amount of compression so that it works for the intended purpose. I did this more for fun than anything, and I'm sure there are people here that could do a better job of this, but hopefully this explains what I was thinking:

compressed bitstream storage format, for types: NUMBER, STRING, BOOLEAN, TABLE

All bitstreams start with an implicit 16-bit header, representing the total number of bytes stored. This is more a safety measure than anything else, and can be bypassed with a clever destination offset of 2.

All other LENGTHs are stored as packed ints: [2 bits: coded length in nibbles][4-16 bits: value]

STRINGs are stored as: [LENGTH][x bytes: one per character]

DATA is stored as: [4 bits: number flags][2 bits: type code, if non-number][type-specific data...]
Number flags prioritize compressing numerical data first, since it is assumed most common.

NUMBER DATA is stored as: [non-zero in 0xff00][non-zero in 0xff][non-zero in 0x.ff][non-zero in 0x.00ff][1-4 bytes: value]
0000 in number flags = non-number type follows

Two-bit type codes define less common types (6 total bits to declare these data types):
00 = FALSE DATA (end of entry)
01 = TRUE DATA (end of entry)
10 = STRING DATA (STRING storage follows)
11 = TABLE DATA (TABLE storage follows)

TABLEs are stored as: [2 bits: key flags][string keys+data][number keys+data][non-number/string keys+data]
Key flags optimize what follows in the table:
00 = empty table (end of entry)
10 = string keys only
01 = number keys only
11 = all keys, including BOOLEAN and TABLE keys (for full Lua support, more than for usefulness)

If STRING key flag is active, that section is stored as: [LENGTH of string keys][STRING][DATA][STRING][DATA]...

If NUMBER key flag is active, that section is stored as: [LENGTH of number keys][NUMBER][DATA][NUMBER][DATA]...
Since all keys in this section are numbers, [0000] in number flags is shorthand meaning "previous key + 1". If [0000] is the number flags of the first entry, it is assumed to be the first default key in a table, 1.

If both key flags are active, the final section is stored as: [[2 bits: type code][type DATA...][DATA]]...10
Since all keys in this section are not numbers, 4-bit number flags of DATA type are always omitted for keys.
Since type code 10 (STRING) is impossible in this section, those two bits signify the end of the table structure.
Therefore, in the false-positive case of only NUMBER and STRING keys, this section is only two bits: 10

demonstration encoding:
luastore(0x5e00,{1,[0]=0,str=5.25,[true]=false,{"what"}="why?"}) becomes this bitstream:
[0000|11|11]: [00|0001]:[00|0011][24 bits:"str"]=[0110|00000101|01000000];
              [11|01]:{[00|0001][0000]=[000010][00|0100][32 bits:"what"]}=
                      [000010][00|0100][32 bits:"why?"]
              [10] - end of typed key table, and end of table overall
[table w/all]:[1 str key]:[3 chars:"str"]=[num type:5.5];
              [2 num keys]:[default key]=[num:1],[num:0]=[num:0];
              [bool:true]=[bool type:false],
              [table w/num]:{[1 num key]:[default key]=[str type|4 chars:"what"]}=
                      [str type|4 chars:"why?"]
              ; = 27.75 bytes, store as 30 total bytes in 2-byte integer at bitstream start

I'm interested to hear if people like this idea, and if @zep thinks it would add something to the PICO-8 API. I know it just hit Beta and this is unlikely to be implemented, but I do think it would add a lot of value for many people in the community, myself included.

P#77321 2020-05-28 16:22 ( Edited 2020-05-28 18:28)

:: Unfold ::

I've been looking into building a portable PICO-8 recently, so I'm going through display component websites for a square or nearly-square screen. This website (link) has a 60fps, 4" square touch display, so I was wondering if anyone has any experience with the company, Pimoroni.

They mention on the store page that it "works really well for Pico-8 games", which I think is pretty great, if it's made specifically for what I want to build. I thought @zep mentioned somewhere recently that he was looking for a square screen like this, too, so if the company is good then this may be a starting point for an official PICO-8 portable?

Here's a short video clip of the display in use that I found on another website. Pimoroni seems to be a reseller, but they've added code to get it working on Raspberry Pi, and I can't find anything online about "HyperPixel" that doesn't link back to them. I'm new to builds like this, so if anyone has experience with these kinds of projects, I could use some advice.

P#75955 2020-05-06 12:50

Follow Lexaloffle:          
Generated 2023-09-22 12:24:02 | 0.131s | Q:57