====================================================================================================
Picotron User Manual
====================================================================================================
Picotron v0.1.1f
https://www.picotron.net
(c) Copyright 2022~25 Lexaloffle Games LLP
Author: Joseph White // [email protected]
Picotron is made with:
SDL2 http://www.libsdl.org
Lua 5.4 http://www.lua.org // see license.txt
lz4 by Yann Collet https://www.lz4.org // see license.txt
z8lua by Sam Hocevar https://github.com/samhocevar/z8lua
GIFLIB http://giflib.sourceforge.net/
libb64 by Chris Venter
Latest version of this manual: https://www.lexaloffle.com/picotron.php?page=resources
:: Contents
:: Welcome to Picotron
Picotron is a Fantasy Workstation for making pixelart games, animations, music, demos and other
curiosities. It can also be used to create tools that run inside Picotron itself, and to
customise things like live wallpapers and screensavers. It has a toy operating system designed
to be a cosy creative space, but runs on top of Windows, MacOS or Linux. Picotron apps can be
made with built-in tools, and shared with other users in a special 256k png cartridge format.
:: Specifications
Display: 480x270 / 240x135 64 definable colours
Graphics: Blend tables, tline3d, stencil clipping
Audio: 64-node synth and 8-channel tracker
Code: Lua 5.4 w/ PICO-8 compatability features
CPU: 8M Lua vm insts / second
Cart: .p64.png (256k) / .p64 ("unlimited")
====================================================================================================
Getting Started
====================================================================================================
----------------------------------------------------------------------------------------------------
Drive Storage
----------------------------------------------------------------------------------------------------
The first time Picotron is run, it will automatically create a configuration file that
specifies where to store files if one does not already exist:
Windows: C:/Users/Yourname/AppData/Roaming/Picotron/picotron_config.txt
OSX: /Users/Yourname/Library/Application Support/Picotron/picotron_config.txt
Linux: ~/.lexaloffle/Picotron/picotron_config.txt
The default configuration:
mount / C:/Users/Yourname/AppData/Roaming/Picotron/drive
From inside Picotron, any folder in the file navigator can be opened in a regular non-Picotron
file browser with "View in Host OS", or by typing "folder" from terminal. Picotron's filenav
gui is a work in progress, so you might want to do any complex file management from the host
OS for now!
----------------------------------------------------------------------------------------------------
Shortcuts
----------------------------------------------------------------------------------------------------
ALT+ENTER: Toggle Fullscreen
ALT+F4: Fast Quit (Windows)
CTRL-Q: Fast Quit (Mac, Linux) -- need to enable in settings
CTRL-R: Reload / Run / Restart cartridge
CTRL-S: Quick-Save working cartridge (or current file if not inside /ram/cart)
ALT+LEFT/RIGHT Change Workspace (use option on Mac)
ESCAPE: Toggle between desktop / terminal
ENTER: Pause menu (fullscreen apps)
CTRL-6: Capture a Screenshot, saved as {Your Desktop}/picotron_desktop
CTRL-7: Capture a Cartridge Label
CTRL-8: Start recording a GIF (SHIFT-CTRL-8 to select a region first)
CTRL-9: End recording GIF
----------------------------------------------------------------------------------------------------
Creating a Cartridge
----------------------------------------------------------------------------------------------------
Picotron is a cartridge-orientated workstation. A cartridge is like an application bundle, or a
project folder: it is a collection of Lua source code files, graphics, audio and any other data
files the cartridge needs to run. The "present working cartridge" is always in a RAM folder
named /ram/cart.
Click on the code editor workspace at top right, which looks like this: ()
Paste in a program (select here, CTRL-C, and then CTRL-V inside Picotron)
function _init()
bunny = unpod(
"b64:bHo0AEIAAABZAAAA-wpweHUAQyAQEAQgF1AXQDcwNzAHHxcHM"..
"AceBAAHc7cwFwFnAQcGAPAJZx8OJ0CXcF8dkFeQFy4HkBcuB5AXEBdA"
)
x = 232
y = 127
hflip = false
end
function _draw()
cls(3) -- clear the screen to colour 3 (green)
srand(0) -- reset the random number generator sequence
for i=1,50 do
print("\\|/",rnd(480),rnd(270),19) -- grass
print("*",rnd(480),rnd(270),rnd{8,10,14,23}) -- flower
end
ovalfill(x+2,y+15,x+12,y+18, 19) -- shadow
spr(bunny, x, y - (flr(hop)&2), hflip) -- bunny
end
function _update()
if (btn(0)) x -= 2 hflip = true
if (btn(1)) x += 2 hflip = false
if (btn(2)) y -= 2
if (btn(3)) y += 2
if (btn()>0) then
hop += .2 -- any button pressed -> hop
else
hop = 0
end
end
Now, press CTRL-R to run it. CTRL-R runs whatever is in /ram/cart, and the entry point in the
cartridge is always main.lua (the file you were editing).
After hopping around with the cursor keys, you can halt the program by pressing ESCAPE, and
then ESCAPE once more to get back to the code editor.
There is a lot going on here! The _init() function is always called once when the program
starts, and here it creates an image stored as text (a "pod"), and then sets the bunny's
initial x,y position.
_draw() is called whenever a frame is drawn (at 60fps or 30fps if there isn't enough available
cpu). _update() is always called at 60fps, so is a good place to put code that updates the
world at a consistent speed.
----------------------------------------------------------------------------------------------------
Adding Graphics
----------------------------------------------------------------------------------------------------
Normally graphics are stored in .gfx files included in the cartridge at gfx/*.gfx
Click on the second workspace to draw a sprite. By default, the sprite editor has
/ram/cart/gfx/0.gfx open which is a spritebank that is automatically loaded when a cartridge is
run (and can be modified with @set_spr).
Now, instead of using the "bunny" image, the index of the sprite can be used:
spr(1, x, y, hflip) -- hflip controls horizontal flipping
The map works the same way: map/0.map is automatically loaded, and can be drawn with:
map()
Map files can also be loaded directly, as a table of layers. Each layer has a @Userdata .bmp
that can be passed to map() as the first parameter:
layers = fetch("map/0.map")
map(layers[1].bmp)
To adjust the draw position to keep the player centered, try using camera() at the start of
_draw():
camera(x - 240, y - 135)
----------------------------------------------------------------------------------------------------
Adding Code Tabs
----------------------------------------------------------------------------------------------------
Multiple code tabs can be created by making a lua file for each one. Click on the [+] tab
button near the top and type in a name for the new file (a .lua extension will be added
automatically if needed), and then include them at the top of your main.lua program:
include "title.lua"
include "monster.lua"
include "math.lua"
The filename is relative to the present working directory, which starts as the directory a
program is run from (e.g. /ram/cart).
----------------------------------------------------------------------------------------------------
Saving a Cartridge
----------------------------------------------------------------------------------------------------
To save a cartridge to disk, open a terminal from the picotron menu (top left), and type:
save mycart.p64
(or just "save mycart" ~ the .p64 extension will be added automatically)
The save command simply copies the contents of /ram/cart to mycart.p64.
Note that you can not save the cart while you are inside /ram/cart (e.g. after you press escape
to halt a program). That would mean copying a folder somewhere inside itself! Also, saving to
anything inside /ram or /system will only save to memory which disappears the next time
Picotron is restarted.
Once a cartridge has been saved, the filename is set as the "present working cartridge", and
subsequent saves can be issued with the shortcut: CTRL-S. To get information about the current
cartridge, type "info" at the terminal prompt.
When editing code and graphics files inside a cartridge, those individual files are auto-saved
to /ram/cart so that CTRL-R will run the current version (there's no need to save before each
run).
When using an editor to edit a file that is outside /ram/cart, CTRL-S saves that individual
file.
----------------------------------------------------------------------------------------------------
Terminal Commands
----------------------------------------------------------------------------------------------------
Some handy commands: // "directory" means "folder"
ls list the current directory
cd foo change directory (e.g. cd /desktop)
mkdir foo create a directory
folder open the current directory in your Host OS
open . open the current directory in filenav
open fn open a file with an associated editor
rm filename remove a file or directory (be careful!)
cp f0 f1 copy file / directory f0 to f1
mv f0 f1 move file / directory f0 to f1
info information about the current cartridge
load cart load a cartridge into /ram/cart
save cart save a cartridge
To create your own commands, see: @Custom_Commands
----------------------------------------------------------------------------------------------------
Uploading a Cartridge to the BBS
----------------------------------------------------------------------------------------------------
Cartridges can be shared on the lexaloffle BBS:
https://www.lexaloffle.com/bbs/?cat=8
First, capture a label while your cart is running with CTRL-7. For windowed programs, the label
will include a screenshot of your desktop, so make sure you don't have anything personal lying
around!
You can give the cartridge some metadata (title, version, author, notes) using about:
> about /ram/cart
Hit CTRL-S to save the changes made to the label and metadata.
Then make a copy of your cartridge in .p64.png format just by copying it:
> cp mycart.p64 releaseable.p64.png
The label will be printed on the front along with the title, author and version metadata if it
exists. You can check the output by opening the folder you saved to, and then double clicking
on releaseable.p64.png (it is just a regular png)
> folder
Finally, go to https://www.lexaloffle.com/picotron.php?page=submit to upload the cartridge.
Cartridges are not publicly listed until a BBS post has been made including the cartridge.
----------------------------------------------------------------------------------------------------
Browsing BBS Cartridges
----------------------------------------------------------------------------------------------------
Cartridges can be browsed using the bbs:// protocol from within filenav. In the Picotron menu
(top left) there is an item "BBS Carts" that opens bbs:// in the root folder.
Cartridges can alternative be loaded directly from the BBS using the cartridge id:
> load #cart_id -- same as load bbs://cart_id.p64
They can also be run as if they are a local file:
> bbs://cart_id.p64
A specific version of the cart can be specified with the revision suffix:
> bbs://cart_id-0.p64
----------------------------------------------------------------------------------------------------
Exporting a Cartridge as HTML or PNG
----------------------------------------------------------------------------------------------------
Cartridges can be exported and shared as stand-alone html pages.
To export the currently loaded cartridge as a single file:
> export foo.html
View the page by opening the folder and double clicking on it:
> folder
To save a copy of the currently loaded cartridge in .p64.png format:
> export foo.p64.png
====================================================================================================
Customising your Machine
====================================================================================================
----------------------------------------------------------------------------------------------------
Desktop Customisation
----------------------------------------------------------------------------------------------------
Open the system settings via the Picotron menu (top left) or by typing "settings" at the prompt.
To create your own lists of themes, wallpapers and screensavers, create the following folders:
/appdata/system/themes
/appdata/system/wallpapers
/appdata/system/screensavers
Wallpapers and screensavers are regular .p64 cartridges -- you can copy anything in there that runs
in fullscreen.
Widgets are programs that run in the slide-out tooltray (pull the toolbar down from the top), and
are windowed programs that are not moveable and do not have a frame. To install a widget, first run
it as a windowed program, adjust the window size to your liking (if it resizeable), and then drag
and drop it into the tooltray. The widget will now be re-launched every time Picotron boots.
Running BBS apps can be installed directly in the tooltray -- there is no need to make a local
copy first.
Right-click a widget to pop in back out as a window, or to remove it.
Snap to grid mode for the desktop items can be enabled with the following command in terminal:
> store("/appdata/system/filenav.pod", {snap_to_grid=true})
----------------------------------------------------------------------------------------------------
Custom Commands
----------------------------------------------------------------------------------------------------
To create your own terminal commands, put .p64 or .lua files in /appdata/system/util.
When a command is used from commandline (e.g. "ls"), terminal first looks for it in
/system/util and /system/apps, before looking in /appdata/system/util and finally the current
directory for a matching .lua or .p64 file.
The present working directory when a program starts is the same directory as the program's
entry point (e.g. where main.lua is, or where the stand-alone lua file is). This is normally
not desireable for commandline programs, which can instead change to the directory the command
was issued from using env().path. For example:
cd(env().path)
print("args: "..pod(env().argv))
print("pwd: "..pwd())
Save it as /appdata/system/util/foo.lua, and then run it from anywhere by typing "foo".
----------------------------------------------------------------------------------------------------
Keyboard Layout
----------------------------------------------------------------------------------------------------
Text input (using @peektext() / @readtext()) defaults to the host OS keyboard layout / text
entry method.
Key states used for things like CTRL-key shortcuts (e.g. @key("ctrl") and @keyp("c")) are also
mapped to the host OS keyboard layout by default, but can be further configured by creating a
file called /appdata/system/keycodes.pod which assigns each keyname to a new scancode. The raw
names of keys (same as US layout) can alternatively be used on the RHS of each assignment, as
shown in this example that patches a US layout with AZERTY mappings:
store("/appdata/system/keycodes.pod", {a="q",z="w",q="a",w="z",m=";",[";"]=",",[","]="m"})
Note: you probably do not need need to do this! The default layout should work in most cases.
Raw scancodes themselves can also be remapped in a similar way using
/appdata/system/scancodes.pod, but is also normally not needed. The raw mapping is used in
situations where the physical location of the key matters, such as the piano-like keyboard
layout in the tracker. See /system/lib/events.lua for more details.
----------------------------------------------------------------------------------------------------
Defaults Apps
----------------------------------------------------------------------------------------------------
When opening a file via filenav or the open command, an application to open it with is selected
based on the extension. To change or add the default application for an extension, use the
default_app command. The following will associate files ending with ".sparkle" with the program
"/apps/tools/sparklepaint.p64":
default_app sparkle /apps/tools/sparklepaint.p64
The table of associations is stored in: /appdata/system/default_apps.pod
When a bbs:// cartridge is used to create or edit a file, the location of that cartridge is
stored in the file's metadata as metadata.prog. When a default app can not be found to open a
file, metadata.prog is used instead when it is available.
====================================================================================================
Anywhen
====================================================================================================
Anywhen is a tool for reverting to earlier versions of cartridges that are saved to disk from
inside Picotron. Every time a file is changed, Picotron records a delta between the last known
version and the current one, and is able to fetch any earlier version of a cartridge as long as
anywhen was active at that point in time. It can be turned off via settings.p64, but it is
recommended during early alpha (0.1.*) to leave it running as it might be helpful in recovering
lost data caused by bugs or lack of safety features (e.g. there is currently no confirmation
required when saving over files).
----------------------------------------------------------------------------------------------------
Temporal Loading
----------------------------------------------------------------------------------------------------
To load an earlier version of a cartridge even after it has been deleted or moved, use a
timestamp (written as local time) at the end of the load command:
load foo.p64@2024-04-10_12:00:00
An underscore is used between the date and time parts as spaces are not allowed in location
strings.
When an earlier version from the same (local) day is needed, the date and optionally seconds
parts can be omitted:
load foo.p64@12:00
Anywhen only stores changes made to files from within Picotron; it does not proactively look
for changes made in external editors except when generating the first log file per day. Also,
it only stores changes made to files saved to disk, and not to /ram.
----------------------------------------------------------------------------------------------------
Anywhen Storage
----------------------------------------------------------------------------------------------------
The anywhen data is stored in the same folder as the default picotron_config.txt location (type
"folder /" and then go up one folder in the host OS). The history is orgnised by month, and it
is safe to delete earlier month folders if they are no longer needed (but they normally
shouldn't take up too much space).
====================================================================================================
Code Editor
====================================================================================================
The code editor is open on boot in the first workspace, defaulting to /ram/cart/main.lua.
Like all of the standard tools, it runs in a tabbed workspace, where each tab is a separate
process editing a single file.
To open or create a file in the editor from terminal:
> code foo.lua
:: Keys
Hold shift Select (or click and drag with mouse)
CTRL-X,C,V Cut copy or paste selected
CTRL-Z,Y Undo, redo
CTRL-F Search for text in the current tab
CTRL-L Jump to a line number
CTRL-W,E Jump to start or end of current line
CTRL-D Duplicate current line
TAB Indent a selection (SHIFT-TAB to un-indent)
CTRL-B Comment / uncomment selected block
SHIFT-ENTER To automatically add block end and indent
CTRL-UP/DOWN Jump to start or end of file (same as CTRL-HOME/END)
CTRL-CURSORS Jump left or right by word
----------------------------------------------------------------------------------------------------
Embedding Images
----------------------------------------------------------------------------------------------------
The code editor can render gfx pod snippets (e.g. copied from the gfx editor) embedded in the
code. See /system/demos/carpet.p64 for an example of a diagram pasted into the source code.
Those snippets contain a header string using block comments "--[[pod_type=gfx]]", so can not
appear inside the same style of block comments and still parse and run. Use variations with
some matching number of ='s between the square brackets instead:
--[==[
a picture:
--[[pod_type="gfx"]]unpod("b64:bHo0AC4AAABGAAAA-wNweHUAQyAQEAQQLFAsIEwwTBAEAAUR3AIAUxwBfAEcBgCg3CBMEUxAPBE8IA==")
]==]
----------------------------------------------------------------------------------------------------
Using External Editors
----------------------------------------------------------------------------------------------------
The simplest way to use a text editor outside of Picotron is to store the files outside of a
cartidge and then @include() them. For example, the following snippet could be used at the top
of main.lua:
cd("/myproj") -- files stored in here will be accessible to the host OS text editor
include("src/draw.lua") -- /myproj/src/draw.lua
include("src/monster.lua") -- /myproj/src/monster.lua
Just remember to copy them to the cartridge (and comment out the "cd(...)") before releasing
it!
-- the -f switch copies over any existing directory
cp -f /myproj/src /ram/cart/src
As a general rule, released cartridges should be self-contained and not depend on anything
except for /system.
====================================================================================================
GFX Editor
====================================================================================================
The second workspace is a sprite and general-purpose image editor. Each .gfx file contains up to
256 sprites, and if the filename starts with a number, it is automatically loaded into that bank
slot and used from the map editor. See @map() and @spr() for notes about sprite indexing.
Don't forget to save your cartridge after drawing something -- the default filenames all point to
/ram/cart and isn't actually stored to disk until you use the save command (or CTRL-S to save the
current cartridge)
----------------------------------------------------------------------------------------------------
GFX Controls
----------------------------------------------------------------------------------------------------
SPACE Shortcut for the pan tool
MOUSEWHEEL To zoom in and out
S Shortcut for the select tool (hold down)
CTRL-A Select all
ENTER Select none
CURSORS Move selection
BACKSPACE Delete selection
CTRL-C Copy selection
CTRL-V Paste to current sprite
CTRL-B Paste big (2x2)
TAB Toggle RH pane
-,+ Navigate sprites
1,2 Navigate colours
RMB Pick up colour
F/V Flip selection horizontally or vertically
----------------------------------------------------------------------------------------------------
GFX Tools
----------------------------------------------------------------------------------------------------
The drawing tools can be selected using icons under the palette:
PENCIL Draw single pixels
BRUSH Draw using a fill pattern and brush shape
LINE Draw a line // SHIFT to snap closest axis, diagonal, or 2:1 slope
RECT Draw a rectange // SHIFT to snap to a square
ELLIPSE Draw an elipse // SHIFT to snap to a circle
BUCKET Fill an area
STAMP Stamp a copy of the clipboard at the cursor
SELECT Select a rectangular region; press Enter to remove
PAN Change the camera position
RECT and ELLIPSE tools can be drawn filled by holding CTRL
----------------------------------------------------------------------------------------------------
Multiple Sprite Selections
----------------------------------------------------------------------------------------------------
To select multiple sprites at once, hold shift and click and drag in the navigator. Resizing
and modifying sprite flags apply to all sprites in that region.
Each sprite has its own undo stack. Operations that modify more than one sprite at once (paste
multiple, batch resize) create a checkpoint in each individual undo stack, and can only be
undone once (ctrl-z) as a group immediately after the operation.
====================================================================================================
MAP Editor
====================================================================================================
The map editor uses similar shortcuts to the gfx editor, with a few changes in meaning.
The F, V and R flip and rotate selected tiles, but also set special bits on those tiles to indicate
that the tile itself should also be drawn flipped /rotated. The @map() command also observes those
bits.
To select a single tile (e.g. to flip it), use the picker tool (crosshair icon) or hold S for the
section tool and use right mouse button. When there is no selection, F, V, R can also be used to
set the bits on the curret item before it is placed.
Sprite 0 means "empty", and that tile is not drawn. The default sprite 0 is a small white x to
indicate that it is reserved with that special meaning. This can be disabled; see @map() for notes.
----------------------------------------------------------------------------------------------------
Map Layers
----------------------------------------------------------------------------------------------------
Each map file can contain multiple layers which are managed at the top right using the add layer
("+") button and the delete (skull icon) button. Currently only a single undo step is available
when deleting layers, so be careful!
Layers can be re-ordered using the up and down arrow icon buttons, named using the pencil icon
button, and hidden using the toggle button that looks like an eye.
Each layer can have its own size, and is drawn in the map editor centered.
----------------------------------------------------------------------------------------------------
Tile Sizes
----------------------------------------------------------------------------------------------------
A global tile size is used for all layers of a map file, taken from the size of sprite 0. The width
and height do not need to match.
Sprites that do not match the global tile size are still drawn, but stretched to fill the target
size using something equivalent to a @sspr() call.
====================================================================================================
SFX Editor
====================================================================================================
The SFX editor can be used to create instruments, sound effects (SFX), and music (SFX arranged into
"patterns").
Each of these has their own editing mode that can be switched between by pressing TAB, or by
clicking on the relevant navigator header on the left. Instruments can also be edited by
CTRL-clicking on them, and SFX and pattern items always jump to their editing modes when clicked.
In the SFX and pattern editing modes, press SPACE to play the current SFX or pattern, and SPACE
again to stop it.
Picotron has a specialised audio component called PFX6416 used to generate all sound. It takes a
block of RAM as input (containing the instrument, track and pattern definitions). The .sfx file
format is simply a 256k memory dump of that section of ram starting at 0x30000.
----------------------------------------------------------------------------------------------------
Instrument Editor
----------------------------------------------------------------------------------------------------
An instrument is a mini-synthesizer that generates sound each time a note is played. It is made
from a tree of up to 8 nodes, each of which either generates, modifies or mixes an audio
signal.
For example, a bass pluck instrument might have a white noise OSC node that fades out rapidly,
plus a saw wave OSC node that fades out more slowly.
The default instrument is a simple triangle wave. To adjust the waveform used, click and drag
the "WAVE" knob. In many cases this is all that is needed, but the instrument editor can
produce a variety of sounds given some experimentation. Alternatively, check the BBS for some
instruments that you can copy and paste to get started!
:: Structure
The root node at the top is used to control general attributes of the instrument. It has an
instrument name field (up to 16 chars), and toggle boxes RETRIG (reset every time it is
played), and WIDE (give child nodes separate stereo outputs).
To add a node, use one of the buttons on the top right of the parent:
+OSC: Add an oscillator (sound generator) to the parent.
+MOD: Modulate the parent signal using either frequency modulation or ring modulation.
+FX: Modify the parent signal with a FILTER, ECHO, or SHAPE effect.
An instrument that has two oscillators, each with their own FX applied to it before sending
to the mix might look like this:
ROOT
OSC
FX:FILTER
OSC
FX:ECHO
During playback, the tree is evaluated from leaves to root. In this case, first the FX
nodes are each applied to their parents, and then the two OSCs are mixed together to
produce the output signal for that instrument.
Sibling nodes (a group with the same parent) can be reordered using the up and down
triangle buttons. When a node is moved, it brings the whole sub-tree with it (e.g. if
there is a filter attached to it, it will remain attached). Likewise, deleting a node will
also delete all of its children.
:: Node Parameters
The parameters for each node (VOL, TUNE etc) can be adjusted by clicking and dragging the
corresponding knob. Each knob has two values that define a range used by @Envelopes; use
the left and right mouse button to adjust the upper and lower bounds, and the range
between them will light up as a pink arc inside the knob.
:: Parameter Operators
Parameters can be evaluated relative to their parents. For example, a node might use a
tuning one octave higher than its parent, in which case the TUNE will be "+ 12". The
operator can be changed by clicking to cycle through the available operators for that knob:
+ means add, * means multiply by parent.
:: Parameter Multipliers
Below the numeric value of each knob there is a hidden multiplier button. Click it to cycle
between *4, /4 and none. This can be used to alter the scale of that knobs's values. For
example, using *4 on the BEND knob will give a range of -/+ 1 tone instead of -/+ 1/2
semitone. There are more extreme multipliers available using CTRL-click (*16, *64), which
can produce extremely noisey results in some cases.
The default parameter space available in the instrument designer (without large multipliers)
shouldn't produce anything too harsh, but it is still possible to produce sounds that will
damage your eardrums especially over long periods of time. Please consider taking off your
headphones and/or turning down the volume when experimenting with volatile sounds!
:: Wide Instruments
By default, instruments are rendered to a mono buffer that is finally split and mixed to each
stereo channel based on panning position. To get stereo separation of voices within an
instrument, WIDE mode can be used. It is a toggle button in the root node at the top of the
instrument editor.
When WIDE mode is enabled, OSC nodes that are children of ROOT node have their own stereo
buffers and panning position. FX nodes that are attached to ROOT are also split into 2
separate nodes during playback: one to handle each channel. This can give a much richer sound
and movement between channels, at the cost of such FX nodes costing double towards the channel
maximum (8) and global maxmimum (64).
----------------------------------------------------------------------------------------------------
Instrument Nodes
----------------------------------------------------------------------------------------------------
:: OSC
There is only one type of oscillator (OSC), which reads data from a table of waveforms (a
"wavetable"), where each entry in the table stores a short looping waveform. Common
waveforms such as sine wave and square wave are all implemented in this way rather than
having special dedicated oscillator types.
VOL volume of a node's output
PAN panning position
TUNE pitch in semitones (48 is middle C)
BEND fine pitch control (-,+ 1/2 semitone)
WAVE position in wavetable. e.g. sin -> tri -> saw
PHASE offset of wave sample
:: Generating Noise
Noise is also implemented as a wavetable containing a single entry of a random sample of
noise. Every process starts with 64k of random numbers at 0xf78000 that is used to form
WT-1. Click the wavetable index (WT-0) in the oscilloscope to cycle through the 4
wavetables. WT-2 and WT-3 are unused by default.
At higher pitches, the fact that the noise is a repeating loop is audible. A cheap way to
add more variation is to set the BEND knob's range to -/+ maximum and then assign an
envelope to it. An @LFO (freq:~40) or @DATA envelope (LP1:16, LERP:ON, scribble some noisey
data points) both work well.
:: FM MOD
A frequency modulator can be added to any oscillator. This produces a signal in the same
way as a regular oscillator, but instead of sending the result to the mix, it is used to
rapidly alter the pitch of its parent OSC.
For example, a sine wave that is modulating its parent OSC at a low frequency will sound
like vibrato (quickly bending the pitch up and down by 1/4 of a semitone or so). The volume
of the FM MOD signal determines the maximum alteration of pitch in the parent.
As the modulating frequency (the TUNE of the FM:MOD) increases, the changes in pitch of the
parent OSC are too fast to hear and are instead perceived as changes in timbre, or the
"colour" of the sound.
:: RING MOD
Similar to FM, but instead of modulating frequency, RING MOD modulates amplitude: the
result of this oscillator is multiplied by its parent. At low frequencies, this is
perceived as fluctuation in the parent's volume and gives a temelo-like effect.
// The name "ring" comes from the original implementation in analogue circuits, which uses
a ring of diodes.
:: FILTER FX
The filter FX node can be used to filter low or high frequencies, or used in combination to
keep only mid-range frequencies. Both LOW and HIGH knobs do nothing at 0, and remove all
frequencies when set to maximum.
>
LOW Low pass filter
HIGH High pass filter
RES Resonance for the LPF
:: ECHO FX
Copy the signal back over itself from some time in the past, producing an echo effect. At
very short DELAY values this can also be used to modify the timbre, giving a string or
wind instrument feeling. At reasonably short delays (and layered with a second echo node)
it can be used to approximate reverb.
DELAY How far back to copy from; max is around 3/4 of a second
VOL The relative volume of the duplicated siginal. 255 means no decay at all (!)
A global maximum of 16 echo nodes can be active at any one time. Echo only applies while
the instrument is active; swtiching to a different instrument on a given channel resets the
echo buffer.
:: SHAPE FX
Modify the shape of the signal by running the amplitude through a gain function. This can
be used to control clipping, or to produce distortion when a low CUT (and high MIX) value
is used. CUT is an absolute value, so the response of the shape node is sensitive to the
volume of the input signal.
GAIN Multiply the amplitude
ELBOW Controls the gradient above CUT. 64 means hard clip. > 64 for foldback!
CUT The amplitude threshold above which shaping should take effect
MIX Level of output back into the mix (64 == 1.0)
----------------------------------------------------------------------------------------------------
Envelopes
----------------------------------------------------------------------------------------------------
Envelopes (on the right of the instrument designer) can be used to alter the value of a node
parameter over time. For example, an oscillator might start out producing a triangle wave and
then soften into a sine wave over 1 second. This is achieved by setting an upper and lower
value for the WAVE knob (see @Node_Parameters), and then assigning an evelope that moves the
parameter within that range over time.
To assign an envelope to a particular node parameter, drag the "ENV-n" label and drop it onto
the knob. Once an envelope has been assigned, it will show up as a blue number on the right of
the knob's numeric field. Click again remove it, or right click it to toggle "continue" mode
(three little dots) which means the envelope is not reset each time the instrument is
triggered.
When an envelope is evaluated, it takes the time in ticks from when the instrument started
playing (or when it was retriggered), and returns a value from 0 to 1.0 which is then mapped to
the knob's range of values.
Click on the type to cycle through the three types:
:: ADSR
ADSR (Attack Decay Sustain Release) envelopes are a common way to describe the change in volume
in response to a note being played, held and released.
When the note is played, the envelope ramps up from 0 to maximum and then falls back down to a
"sustain" level which is used until the note is released, at which point it falls back down to
0.
............................. 255 knob max
/\
/ \
/ \______ ....... S sustain level
/ \
/ \
../................\......... 0 knob min
|-----|--| |--|
A D R
Attack: How long to reach maximum. Larger values mean fade in slowly.
Decay: How long to fall back down to sustain level
Sustain: Stay on this value while the note is held
Release: How long to fall down to 0 from current value after release
For a linear fade in over 8 ticks, use: 8 0 255 0
For a linear fade out over 8 ticks: 0 8 0 0
The duration values are not linear. 0..8 maps to 0..8 ticks, but after that the actual
durations start jumping up faster. 128 means around 5.5 seconds and 255 means around 23
seconds.
:: LFO
Low frequency oscillator. Returns values from a sine wave with a given phase and frequency.
freq: duration to repeat // 0 == track speed
phase: phase offset
:: DATA
A custom envelope shape defined by 16 values. Indexes that are out of range return 0.
LERP: lerp smoothly between values instead of jumping
RND: choose a random starting point between 0 and T0 (tick 0 .. T0*SPD-1)
SPD: duration of each index // 0 == track speed
LP0: loop back to this index (0..)
LP1: loop back to LP0 just before reaching this index when note is held
T0: starting index (when RND is not checked)
These attributes that control playback of data envelopes are also available to ADSR and LFO,
accessible via the fold-out button that looks like three grey dots.
:: Random Values
This is not an envelope, but works in a similar way. Right clicking on an envelope button (to
the right of the knob's numeric field) when no envelope is assigned toggles random mode. When
this mode is active, a pink R is shown in that spot, and a random value within the knob's range
is used every time the instrument is triggered. This can be used to produce chaotic unexpected
sounds that change wildly on every playthrough, or subtle variation to things like drum hits
and plucks for a more natural sound.
----------------------------------------------------------------------------------------------------
Track Editor
----------------------------------------------------------------------------------------------------
A single track (or "SFX") is a sequence of up to 64 notes that can be played by the @sfx()
function.
SFX can be be played slowly as part of a musical pattern, or more quickly to function as a
sound effect. The SPD parameter determines how many ticks (~1/120ths of a second) to play each
row for.
Each row of a track has a pitch (C,C#,D..), instrument, volume, effect, and effect parameter.
Instrument and volume are written in hexadecimal (instrument "1f" means 31 in decimal). Volume
0x40 (64) means 100% volume, but larger values can be used.
The pitch, instrument and volume can each be set to "none" (internally: 0xff) by typing a dot
("."). This means that the channel state is not touched for that attribute, and the existing
value carries over.
An instrument's playback state is reset (or "retriggered") each time the instrument index is
set, and either the pitch or instrument changes. When RETRIG flag is set on the instrument
(node 0), only the instrument attribute index to be set for it to retrigger, even if the pitch
is the same as the previous row (e.g. for a hihat played on every row at the same pitch).
:: Pitch Entry
Notes can be entered using a workstation keyboard using a layout similar to a musical keyboard.
For a QWERTY keyboard, the 12 notes C..B can be played with the following keys (the top row are
the black keys):
2 3 5 6 7
Q W E R T Y U
An additional octave is also available lower down on the keyboard:
S D G H J
Z X C V B N M
Use these keys to preview an instrument, or to enter notes in the SFX or pattern editing modes.
Notes are played relative to the global octave (OCT) and volume (VOL) sliders at the top left.
Some instruments do not stop playing by themselves -- press SPACE in any editor mode to kill
any active sound generation.
:: Effects
Each effect command takes either a single 8-bit parameter or two 4-bit parameters.
PICO-8 effects 1..7 can be entered in the tracker using numbers, but are replaced with s, v, -,
<, >, a and b respectively. The behaviour for those effects matches PICO-8 when the parameter
is 0x00 (for example, a-00 uses pitches from the row's group of 4).
s slide to note (speed)
v vibrato (speed, depth)
- slide down from note (speed)
+ slide up from note (speed)
> fade out (end_%, speed)
a fast arp (pitch0, pitch1)
b slow arp (pitch0, pitch1)
t tremelo (speed, depth)
w wibble (speed, depth) // v + t
r retrigger (every n ticks)
d delayed trigger (after n ticks)
c cut (after n ticks)
p set channel panning offset
The meaning of "speed" varies, but higher is faster except for 0 which means "fit to track
speed".
Arpeggio pitches are in number of semitones above the channel pitch.
----------------------------------------------------------------------------------------------------
Pattern Editor
----------------------------------------------------------------------------------------------------
A pattern is a group of up to 8 tracks that can be played with the @music() function.
Click on the toggle button for each track to activate it, and drag the value to select which
SFX index to assign to it.
SFX items can also be dragged and dropped from the navigator on the left into the desired
channel.
The toggle buttons at the top right of each pattern control playback flow, which is also
observed by @music():
loop0 (right arrow): loop back to this pattern
loop1 (left arrow): loop back to loop0 after finishing this pattern
stop (square): stop playing after this pattern has completed
Tracks within the same pattern have different can lengths and play at different speeds. The
duration of the pattern is taken to be the duration (spd * length) of the left-most,
non-looping track.
====================================================================================================
Picotron Lua
====================================================================================================
Picotron uses a slightly extended version of Lua 5.4, and most of the standard Lua libraries
are available. For more details, or to find out about Lua, see www.lua.org.
The following is a primer for getting started with Lua syntax.
:: Comments
-- use two dashes like this to write a comment
--[[ multi-line
comments ]]
To create nested multi-line comments, add a matching number of ='s between the opening and
closing square brackets:
--[===[
--[[
this comment can appear inside another multi-line comment
]]
]===]
:: Types and assignment
Types in Lua are numbers, strings, booleans, tables, functions and nil:
num = 12/100
s = "this is a string"
b = false
t = {1,2,3}
f = function(a) print("a:"..a) end
n = nil
Numbers can be either doubles or 64-bit integers, and are converted automatically between the
two when needed.
:: Conditionals
if not b then
print("b is false")
else
print("b is not false")
end
-- with elseif
if x == 0 then
print("x is 0")
elseif x < 0 then
print("x is negative")
else
print("x is positive")
end
if (4 == 4) then print("equal") end
if (4 ~= 3) then print("not equal") end
if (4 <= 4) then print("less than or equal") end
if (4 > 3) then print("more than") end
:: Loops
Loop ranges are inclusive:
for x=1,5 do
print(x)
end
-- prints 1,2,3,4,5
x = 1
while(x <= 5) do
print(x)
x = x + 1
end
for x=1,10,3 do print(x) end -- 1,4,7,10
for x=5,1,-2 do print(x) end -- 5,3,1
:: Functions and Local Variables
Variables declared as local are scoped to their containing block of code (for example, inside a
function, for loop, or if then end statement).
y=0
function plusone(x)
local y = x+1
return y
end
print(plusone(2)) -- 3
print(y) -- still 0
:: Tables
In Lua, tables are a collection of key-value pairs where the key and value types can both be
mixed. They can be used as arrays by indexing them with integers.
a={} -- create an empty table
a[1] = "blah"
a[2] = 42
a["foo"] = {1,2,3}
Arrays use 1-based indexing by default:
> a = {11,12,13,14}
> print(a[2]) -- 12
But if you prefer 0-based arrays, just write something the zeroth slot (or use the @Userdata):
> a = {[0]=10,11,12,13,14}
Tables with 1-based integer indexes are special though. The length of such a table can be found
with the # operator, and Picotron uses such arrays to implement @add, @del, @deli, @all and
@foreach functions.
> print(#a) -- 4
> add(a, 15)
> print(#a) -- 5
Indexes that are strings can be written using dot notation
player = {}
player.x = 2 -- is equivalent to player["x"]
player.y = 3
See the @{Table_Functions} section for more details.
:: Picotron Shorthand
Picotron offers some shorthand forms following PICO-8's dialect of Lua, that are not standard
Lua.
:: Shorthand If / while statements
"if .. then .. end" statements, and "while .. then .. end" can be written on a single line:
if (not b) i=1 j=2
Is equivalent to:
if not b then i=1 j=2 end
Note that brackets around the short-hand condition are required, unlike the expanded version.
:: Shorthand Assignment Operators
Shorthand assignment operators can also be used if the whole statement is on one line. They can
be constructed by appending a '=' to any binary operator, including arithmetic (+=, -= ..),
bitwise (&=, |= ..) or the string concatenation operator (..=)
a += 2 -- equivalent to: a = a + 2
:: != operator
Not shorthand, but Picotron also accepts != instead of ~= for "not equal to"
print(1 != 2) -- true
print("foo" == "foo") -- true (string are interned)
----------------------------------------------------------------------------------------------------
Program Stucture
----------------------------------------------------------------------------------------------------
A Picotron program can optionally provide 3 functions:
function _init()
-- called once just before the main loop
end
function _update()
-- called 60 times per second
end
function _draw()
-- called each time the window manager asks for a frame
-- (normally 60, 30 or 20 times per second)
end
====================================================================================================
API Reference
====================================================================================================
----------------------------------------------------------------------------------------------------
Graphics
----------------------------------------------------------------------------------------------------
Graphics operations all respect the current @clip rectangle, @camera position, fill pattern
@fillp(), draw @color, @Colour_Tables and @Masks.
clip(x, y, w, h, [clip_previous])
sets the clipping rectangle in pixels. all drawing operations will be clipped to the
rectangle at x, y with a width and height of w,h.
clip() to reset.
when clip_previous is true, clip the new clipping region by the old one.
pset(x, y, [col])
sets the pixel at x, y to colour index col (0..63).
when col is not specified, the current draw colour is used.
for y=0,127 do
for x=0,127 do
pset(x, y, x*y/8)
end
end
pget(x, y)
returns the colour of a pixel on the screen at (x, y).
while (true) do
x, y = rnd(128), rnd(128)
dx, dy = rnd(4)-2, rnd(4)-2
pset(x, y, pget(dx+x, dy+y))
end
when x and y are out of bounds, pget returns 0.
sget(x, y)
sset(x, y, [col])
get or set the colour (col) of a sprite sheet pixel.
when x and y are out of bounds, sget returns 0.
fget(n, [f])
fset(n, [f], val)
get or set the value (val) of sprite n's flag f.
f is the flag index 0..7.
val is true or false.
the initial state of flags 0..7 are settable in the sprite editor, so can be used to create
custom sprite attributes. it is also possible to draw only a subset of map tiles by
providing a mask in @map().
when f is omitted, all flags are retrieved/set as a single bitfield.
fset(2, 1 | 2 | 8) -- sets bits 0,1 and 3
fset(2, 4, true) -- sets bit 4
print(fget(2)) -- 27 (1 | 2 | 8 | 16)
print(str, x, y, [col])
print(str, [col])
print a string str and optionally set the draw colour to col.
shortcut: written on a single line, ? can be used to call print without brackets:
?"hi"
when x, y are not specified, a newline is automatically appended. this can be omitted by
ending the string with an explicit termination control character:
?"the quick brown fox\0"
additionally, when x, y are not specified, printing text below 122 causes the console to
scroll. this can be disabled during runtime with poke(0x5f36,0x40).
print returns the right-most x position that occurred while printing. this can be used to
find out the width of some text by printing it off-screen:
w = print("hoge", 0, -20) -- returns 16
cursor(x, y, [col])
set the cursor position.
if col is specified, also set the current colour.
color([col])
set the current colour to be used by shape drawing functions (pset, circ, rect..), when one
is not given as the last argument.
if col is not specified, the current colour is set to 6.
cls([col])
clear the screen and reset the clipping rectangle.
col defaults to 0 (black)
camera([x, y])
set a screen offset of -x, -y for all drawing operations
camera() to reset
circ(x, y, r, [col])
circfill(x, y, r, [col])
draw a circle or filled circle at x,y with radius r
if r is negative, the circle is not drawn.
When bit 0x800000000 in col is set, circfill draws inverted (everything outside the circle
is drawn).
oval(x0, y0, x1, y1, [col])
ovalfill(x0, y0, x1, y1, [col])
draw an oval that is symmetrical in x and y (an ellipse), with the given bounding
rectangle.
When bit 0x800000000 in col is set, ovalfill is drawn inverted.
line(x0, y0, [x1, y1, [col]])
draw a line from (x0, y0) to (x1, y1)
if (x1, y1) are not given, the end of the last drawn line is used.
line() with no parameters means that the next call to line(x1, y1) will only set the end
points without drawing.
function _draw()
cls()
line()
for i=0,6 do
line(64+cos(t()+i/6)*20, 64+sin(t()+i/6)*20, 8+i)
end
end
rect(x0, y0, x1, y1, [col])
rectfill(x0, y0, x1, y1, [col])
draw a rectangle or filled rectangle with corners at (x0, y0), (x1, y1).
When bit 0x800000000 in col is set, rectfill draws inverted.
pal(c0, c1, [p])
pal() swaps colour c0 for c1 for one of three palette re-mappings (p defaults to 0):
0: draw palette
The draw palette re-maps colours when they are drawn. For example, an orange flower
sprite can be drawn as a red flower by setting the 9th palette value to 8:
pal(9,8) -- draw subsequent orange (colour 9) pixels as red (colour 8)
spr(1,70,60) -- any orange pixels in the sprite will be drawn with red instead
Changing the draw palette does not affect anything that was already drawn to the
screen.
1: display palette
The display palette re-maps the whole screen when it is displayed at the end of a
frame.
palt(c, is_transparent)
Set transparency for colour index c to is_transparent (boolean) transparency is observed by
@spr(), @sspr(), @map() and @tline3d()
palt(8, true) -- red pixels not drawn in subsequent sprite/tline draw calls
When c is the only parameter, it is treated as a bitfield used to set all 64 values. for
example: to set colours 0 and 1 as transparent:
-- set colours 0,1 and 4 as transparent
palt(0x13)
palt() resets to default: all colours opaque except colour 0. Same as palt(1)
spr(s, x, y, [flip_x], [flip_y])
Draw sprite s at position x,y
s can be either a userdata (type "u8" -- see @Userdata) or sprite index (0..255 for bank 0
(gfx/0.gfx), 256..511 for bank 1 (gfx/1.gfx) etc).
Colour 0 drawn as transparent by default (see @palt())
When flip_x is true, flip horizontally. When flip_y is true, flip vertically.
sspr(s, sx, sy, sw, sh, dx, dy, [dw, dh], [flip_x], [flip_y]]
Stretch a source rectangle of sprite s (sx, sy, sw, sh) to a destination rectangle on the
screen (dx, dy, dw, dh). In both cases, the x and y values are coordinates (in pixels) of
the rectangle's top left corner, with a width of w, h.
s can be either a userdata (type "u8") or the sprite index.
Colour 0 drawn as transparent by default (see @palt())
dw, dh defaults to sw, sh.
When flip_x is true, flip horizontally. When flip_y is true, flip vertically.
get_spr(index)
set_spr(index, ud)
Get or set the sprite (a 2d userdata object of type "u8") for a given index (0..8191).
When a cartridge is run, files in gfx/ that start with an integer (0..31) are automatically
loaded if they exist. Each file has 256 sprites indexes, so the sprites in gfx/0.gfx are
given indexes 0..255, the sprites in gfx/1.gfx are given indexes 256..511, and so on up to
gfx/31.gfx (7936..8191).
fillp(p)
Set a 4x4 fill pattern using PICO-8 style fill patterns. p is a bitfield in reading order
starting from the highest bit.
Observed by @circ() @circfill() @rect() @rectfill() @oval() @ovalfill() @pset() @line()
Fill patterns in Picotron are 64-bit specified 8 bytes from 0x5500, where each byte is a
row (top to bottom) and the low bit is on the left. To define an 8x8 with high bits on the
right (so that binary numbers visually match), fillp can be called with 8 arguments:
fillp(
0b10000000,
0b01011110,
0b00101110,
0b00010110,
0b00001010,
0b00000100,
0b00000010,
0b00000001
)
circfill(240,135,50,9)
Two different colours can be specified in the last parameter
circfill(320,135,50,0x1c08) -- draw with colour 28 (0x1c) and 8
To get transparency while drawing shapes, the shape target mask (see @Masks) should be set:
poke(0x550b,0x3f)
palt()
--> black pixels won't be drawn
:: Colour Tables
Colour tables are applied by all graphics operations when each pixel is drawn. Each one is
a 64x64 lookup table indexed by two colours:
1. the colour to be drawn (0..63)
2. the colour at the target pixel (0..63)
Each entry is then the colour that should be drawn. So for example, when drawing a black
(0) pixel on a red (8) pixel, the colour table entry for that combination might also be red
(in effect, making colour 0 transparent).
Additionally, one of four colour tables can be selected using the upper bits 0xc0 of either
the source or destination pixel. Using custom colour table data and selection bits allows
for a variety of effects including overlapping shadows, fog, tinting, additive blending,
and per-pixel clipping. Functions like @pal() and @palt() also modify colour tables to
implement transparency and colour switching.
Colour tables and masks are quite low level and normally can be ignored! For more details, see:
https://www.lexaloffle.com/dl/docs/picotron_gfx_pipeline.html
:: Masks
When each pixel is drawn, three masks are also used to determine the output colour. The
draw colour (or pixel colour in the case of a sprite) is first ANDed with the read mask.
The colour of the pixel that will be overwritten is then ANDed by the target mask. These
two values are then used as indexes into a colour table to get the output colour. Finally,
the write mask determines which bits in the draw target will actually be modified.
0x5508 read mask
0x5509 write mask
0x550a target mask for sprites
0x550b target_mask for shapes
The default values are: 0x3f, 0x3f, 0x3f and 0x00. 0x3f means that colour table selection
bits are ignored (always use colour table 0), and the 0x00 for shapes means that the target
pixel colour is ignored so that it is possible to draw a black rectangle with colour 0 even
though that colour index is transparent by default.
The following program uses only the write mask to control which bits of the draw target are
written. Each circle writes to 1 of the 5 bits: 0x1, 0x2, 0x4, 0x8 and 0x10. When they are
all overlapping, all 5 bits are set giving colour 31.
function _draw()
cls()
for i=0,4 do
-- draw to a single bit
poke(0x5509, 1 << i)
r=60+cos(t()/4)*40
x = 240+cos((t()+i)/5)*r
y = 135+sin((t()+i)/5)*r
circfill(x, y, 40, 1 << i)
end
end
----------------------------------------------------------------------------------------------------
Map
----------------------------------------------------------------------------------------------------
A map in Picotron is a 2d userdata of type i16. Each value refers to a single sprite in the
"sprite registry" (see @get_spr, @set_spr), which can hold up to 8192 sprites.
The default tile width and height are set to match sprite 0.
The bits in each cel value:
0x00ff the sprite number within a bank (0..255)
0x1f00 the bank number (0..31)
0x2000 flip the tile diagonally ** not supported by tline3d()
0x4000 flip the tile horizontally
0x8000 flip the tile vertically
All tile flipping bits are observed by the map editor and @map().
:: Setting a Current Working Map
Both @map() and @mget() can be used in PICO-8 form that assumes a single global map, which
defaults to the first layer of map/0.map if it exists, and can be set during runtime by
memory-mapping an int16 userdata to 0x100000:
mymap = fetch("forest.map")[2].bmp -- grab layer 2 from a map file
memmap(mymap, 0x100000)
map() -- same as map(mymap)
?mget(2,2) -- same as mymap:get(2,2)
map(tile_x, tile_y, [sx, sy], [tiles_x, tiles_y], [p8layers], [tile_w, tile_h])
map(src, tile_x, tile_y, [sx, sy], [tiles_x, tiles_y], [p8layers], [tile_w, tile_h])
Draw section of a map (starting from tile_x, tile_y) at screen position sx, sy (pixels),
from the userdata src, or from the current working map when src is not given. Note that
the src parameter can be omitted entirely to give a PICO-8 compatible form.
To grab a layer from a .map file:
layers = fetch("map/0.map") -- call once when e.g. loading a level
map(layers[2].bmp)
To draw a 4x2 blocks of tiles starting from 0,0 in the map, to the screen at 20,20:
map(0, 0, 20, 20, 4, 2)
tiles_x and tiles_y default to the entire map.
map() is often used in conjunction with camera(). To draw the map so that a player object
(drawn centered at pl.x in pl.y in pixels) is centered in fullscreen (480x270):
camera(pl.x - 240, pl.y - 135)
map()
p8layers is a bitfield. When given, only sprites with matching sprite flags are drawn. For
example, when p8layers is 0x5, only sprites with flag 0 and 2 are drawn. This has nothing
to do with the list of layers in the map editor -- it follows PICO-8's approach for getting
more than one "layer" out of a single map.
tile_w and tile_h specify the integer width and height in pixels that each tile should be
drawn. Bitmaps that do not match those dimensions are stretched to fit. The default values
for tile_w and tile_h are @0x550e, @0x550f (0 means 256), which are in turn initialised to
the dimensions of sprite 0 on run.
Sprite 0 is not drawn by default, so that sparse maps do not cost much cpu as only the
non-zero tiles are expensive. To draw every tile value including 0, set bit 0x8 at 0x5f36:
poke(0x5f36), peek(0x5f36) | 0x8
mget(x, y)
mset(x, y, val)
PICO-8 style getters & setters that operate on the current working map. These are
equivalent to using the userdata methods :get and :set directly:
mymap = userdata("i16", 32,32)
mymap:set(1,3,42)
?mymap:get(1,3) -- 42
memmap(mymap, 0x100000)
?mget(1,3) -- 42
tline3d(src_ud, x0, y0, x1, y1, u0, v0, u1, v1, w0, w1, [flags])
Draw a textured line from (x0,y0) to (x1,y1), sampling colour values from userdata src.
When src is type u8, it is considered to be a single texture image, and the coordinates
are in pixels. When src is type i16 it is considered to be a map, and coordinates are in
tiles. When the (src) is not given, the current map is used.
Both the dimensions of the map and the tile size must be powers of 2.
u0, v0, u1, v1 are coordinates to sample from, given in pixels for sprites, or tiles for
maps. Colour values are sampled from the sprite present at each map tile.
w0, w1 are used to control perspective and mean 1/z0 and 1/z1. Default values are 1,1
(gives a linear interpolation between uv0 and uv1).
Experimental flags useful for polygon rendering / rotated sprites: 0x100 to skip drawing
the last pixel, 0x200 to perform sub-pixel texture coordinate adjustment.
Unlike @map() or PICO-8's tline, @tline3d() does not support empty tiles: pixels from
sprite 0 are always drawn, and there is no p8layers bitfield parameter.
----------------------------------------------------------------------------------------------------
Audio
----------------------------------------------------------------------------------------------------
sfx(n, [channel], [offset], [length], [mix_volume])
Play sfx n (0..63) on channel (0..15) from note offset (0..63 in notes) for length notes.
Giving -1 as the sfx index stops playing any sfx on that channel. The existing channel
state is not altered: stopping an sfx that uses an instrument with a long echo will not cut
the echo short.
Giving -2 as the sfx index stops playing any sfx on that channel, and also clears the
channel state state: echos are cut short and the channel is immediately silent.
Giving nil or -1 as the channel automatically chooses a channel that is not being used.
Negative offsets can be used to delay before playing.
When the sfx is looping, length still means the number of (posisbly repeated) notes to
play.
When mix_volume is given, the channel is mixed at that value (0x40 means 1.0). Otherwise
the value at 0x553a is used (0x40 by default). In addition to the per-channel mix volume,
all channels are subject to a per-proess global volume specified at 0x5538 (default: 0x40
== 1.0).
When sfx/0.sfx is found on cartridge startup, it is loaded at 0x30000 which is the default
base address for tracks actived by sfx(). A custom base address can be assigned with
poke(0x553c, base_addr >> 16) before each call to sfx().
music(n, [fade_len], [channel_mask], [base_addr])
Play music starting from pattern n.
n -1 to stop music
fade_len is in ms (default: 0). so to fade pattern 0 in over 1 second:
music(0, 1000)
channel_mask is bitfield that specifies which channels to reserve for music only, low bits
first.
For example, to play only on the first three channels 0..2, the lowest three bits should be
set:
music(0, nil, 0x7) -- bits: 0x1 | 0x2 | 0x4
Reserved channels can still be used to play sound effects on, but only when that channel
index is explicitly requested by @sfx().
When base_addr is given, the channels used to play music are assigned that location in
memory to read data from. This can be used to load multiple .sfx files into memory and play
them at the same time. For example, to load some music at 0x80000 and play it without
interfering with sound effects stored at the default location of 0x30000:
fetch("sfx/title.sfx"):poke(0x80000) -- load 256k into 0x80000..0xbffff
music(0, nil, nil, 0x80000)
When music channels are mixed, they are subject to a global per-app volume specified at
0x5538 (default: 0x40 == 1.0), which is then multiplied by a global music volume at 0x5539
(default: 0x40 == 1.0).
note(pitch, inst, vol, effect, effect_p, channel, retrig, panning)
This provides low level control over the state of a channel. It is useful in more niche
situations, like audio authoring tools and size-coding.
Internally this is what is used to play each row of a sfx when one is active. Use 0xff to
indicate an attribute should not be altered.
Every parameter is optional:
pitch channel pitch (default 48 -- middle C)
inst instrument index (default 0)
vol channel volume (default 64)
effect channel effect (default 0)
effect_p effect parameter (default 0)
channel channel index (0..15 -- default 0)
retrig (boolean) force retrigger -- default to false
panning set channel panning (-128..127)
To kill all channels (including leftover echo and decay envelopes):
note() -- same as sfx(-2, -1)
:: Querying Mixer State
Global mixer state:
stat(464) -- bitfield indicating which channels are playing a track (sfx)
stat(465, addr) -- copy last mixer stereo output buffer output is written as
-- int16's to addr. returns number of samples written.
stat(466) -- which pattern is playing (-1 for no music)
stat(467) -- return the index of the left-most non-looping music channel
Per channel (c) state:
stat(400 + c, 0) -- note is held (0 false 1 true)
stat(400 + c, 1) -- channel instrument
stat(400 + c, 2) -- channel vol
stat(400 + c, 3) -- channel pan
stat(400 + c, 4) -- channel pitch
stat(400 + c, 5) -- channel bend
stat(400 + c, 6) -- channel effect
stat(400 + c, 7) -- channel effect_p
stat(400 + c, 8) -- channel tick len
stat(400 + c, 9) -- channel row
stat(400 + c, 10) -- channel row tick
stat(400 + c, 11) -- channel sfx tick
stat(400 + c, 12) -- channel sfx index (-1 if none finished)
stat(400 + c, 13) -- channel last played sfx index
stat(400 + c, 19, addr) -- fetch stereo output buffer (returns number of samples)
stat(400 + c, 20 + n, addr) -- fetch mono output buffer for a node n (0..7)
----------------------------------------------------------------------------------------------------
Input
----------------------------------------------------------------------------------------------------
btn([b], [pl])
Returns the state of button b for player index pl (default 0 -- means Player 1)
0 1 2 3 LEFT RIGHT UP DOWN
4 5 Buttons: O X
6 MENU
7 reserved
8 9 10 11 Secondary Stick L,R,U,D
12 13 Buttons (not named yet!)
14 15 SL SR
A secondary stick is not guaranteed on all platforms! It is preferable to offer an
alternative control scheme that does not require it, if possible.
The return value is false when the button is not pressed (or the stick is in the deadzone),
and a number between 1..255 otherwise. To get the X axis of the primary stick:
local dx = (btn(1) or 0) - (btn(0) or 0)
Stick values are processed by btn so that the return values are only physically possible
positions of a circular stick: the magnitude is clamped to 1.0 (right + down) even with
digital buttons gives values of 181 for btn(1) and btn(3), and it is impossible for e.g.
LEFT and RIGHT to be held at the same time. To get raw controller values, use peek(0x5400 +
player_index*16 + button_index).
Keyboard controls are currently hard-coded:
0~5 Cursors, Z/X
6 Enter -- disable with window{pauseable=false}
8~11 ADWS
12,13 F,G
14,15 Q,E
btnp(b, [pl])
btnp is short for "Button Pressed"; Instead of being true when a button is held down, btnp
returns true when a button is down and it was not down the last frame. It also repeats
after 30 frames, returning true every 8 frames after that. This can be used for things
like menu navigation or grid-wise player movement.
The state that btnp() reads is reset at the start of each call to @_update60, so it is
preferable to use btnp only from inside that call and not from _draw(), which might be
called less frequently.
Custom delays (in frames @ 60fps) can be set by poking the following memory addresses:
poke(0x5f5c, delay) -- set the initial delay before repeating. 255 means never repeat.
poke(0x5f5d, delay) -- set the repeating delay.
In both cases, 0 can be used for the default behaviour (delays 30 and 8)
key(k, [raw])
keyp(k, [raw])
returns the state of key k
function _draw()
cls(1)
-- draw when either shift key is held down
if (key("shift")) circfill(100,100,40,12)
end
The name of each k is the same as the character it produces on a US keyboard with some
exceptions: "space", "delete", "enter", "tab", "ctrl", "shift", "alt", "pageup",
"pagedown".
By default, key() uses the local keyboard layout; On an AZERTY keyboard, key("a") is true
when the key to the right of Tab is pressed. To get the raw layout, use true as the second
parameter to indicate that k should be the name of the raw scancode. For example, key("a",
true) will be true when the key to the right of capslock is held, regardless of local
keyboard layout.
if (key"ctrl" and keyp"a") printh("CTRL-A Pressed")
keyp() has the same behaviour key(), but true when the key is pressed or repeating.
peektext()
readtext([clear])
To read text from the keyboard via the host operating system's text entry system,
peektext() can be used to find out if there is some text waiting, and readtext() can be
used to consume the next piece of text:
while (peektext())
c = readtext()
printh("read text: "..c)
end
When "clear" is true, any remaining text in the queue is discarded.
mouse()
Returns mouse_x, mouse_y, mouse_b, wheel_x, wheel_y
mouse_b is a bitfield: 0x1 means left mouse button, 0x2 right mouse button
mouselock(lock, event_sensitivity, move_sensitivity)
when lock is true, Picotron makes a request to the host operating system's window manager
to capture the mouse, allowing it to control sensitivity and movement speed.
returns dx,dy: the relative position since the last frame
event_sensitivity in a number between 0..4 that determines how fast dx, dy change (1.0
means once per picotron pixel)
move_sensitivity in a number between 0..4: 1.0 means the cursor continues to move at the
same speed.
local size, col = 20, 16
function _draw()
cls()
circfill(240, 135, size*4, col)
local _,_,mb = mouse()
dx,dy = mouselock(mb > 0, 0.05, 0) -- dx,dy change slowly, stop mouse moving
size += dx -- left,right to control size
col += dy -- up,down to control colour
end
----------------------------------------------------------------------------------------------------
Strings
----------------------------------------------------------------------------------------------------
Strings in Lua are written either in single or double quotes or with matching [[ ]] brackets:
s = "the quick"
s = 'brown fox';
s = [[
jumps over
multiple lines
]]
The length of a string (number of characters) can be retrieved using the # operator:
>print(#s)
Strings can be joined using the .. operator. Joining numbers converts them to strings.
>print("three "..4) --> "three 4"
When used as part of an arithmetic expression, string values are converted to numbers:
>print(2+"3") --> 5
chr(val0, val1, ...)
Convert one or more ordinal character codes to a string.
chr(64) -- "@"
chr(104,101,108,108,111) -- "hello"
ord(str, [index], [num_results])
Convert one or more characters from string STR to their ordinal (0..255) character codes.
Use the index parameter to specify which character in the string to use. When index is out
of range or str is not a string, ord returns nil.
When num_results is given, ord returns multiple values starting from index.
ord("@") -- 64
ord("123",2) -- 50 (the second character: "2")
ord("123",2,3) -- 50,51,52
sub(str, pos0, [pos1])
grab a substring from string str, from pos0 up to and including pos1. when pos1 is not
specified, the remainder of the string is returned. when pos1 is specified, but not a
number, a single character at pos0 is returned.
s = "the quick brown fox"
print(sub(s,5,9)) --> "quick"
print(sub(s,5)) --> "quick brown fox"
print(sub(s,5,true)) --> "q"
split(str, [separator], [convert_numbers])
Split a string into a table of elements delimited by the given separator (defaults to ",").
When separator is a number n, the string is split into n-character groups. When
convert_numbers is true, numerical tokens are stored as numbers (defaults to true). Empty
elements are stored as empty strings.
split("1,2,3,a,b") -- {1,2,3,"a","b"}
split("one:two:3",":",false) -- {"one","two","3"}
split("1,,2,") -- {1,"",2,""}
type(val)
Returns the type of val as a string.
> print(type(3))
number
> print(type("3"))
string
To find out if a number is an integer or float, use math.type(num).
create_delta(str0, str1)
apply_delta(str0, delta)
create_delta returns a string encoding all of the information needed to get from str0 to
str1 ("delta"). The delta can then be used by apply_delta to reproduce str1 given only
str0.
For example, given the two strings:
str0 = the quick brown fox
str1 = the quick red fox
create_delta(str0, str1) will return a string that instructs apply_delta() to replace
"brown" with "red".
d = create_delta(str0, str1)
print(apply_delta("the quick brown fox", d)) --> the quick red fox
Note that the string given to apply_delta must be exactly the same as the one used to
create the delta; otherwise apply_delta returns nil.
deltas can be used together with pod() to encode the difference between two tables of
unstructured data:
a = {1,2,3}
b = {1, "banana", 2, 3}
d = create_delta(pod(a), pod(b))
-- reconstruct b using only a and the delta (d)
b2 = apply_delta(pod(a), d)
foreach(unpod(b2), print)
1
banana
2
3
This makes deltas useful for things like undo stacks and perhaps (later) changes in game
state to send across a network. The binary format of the delta includes a few safety
features like crc and length checks to ensure that the input and output strings are as
expected. The first 4 bytes of the delta string are always "dst\0".
The backend for delta encoding is also used internally by anywhen to log incremental
changes made to each file. There is a lot riding on its correctness ~ please let me know if
you discover any odd behaviour with deltas!
----------------------------------------------------------------------------------------------------
Tables
----------------------------------------------------------------------------------------------------
With the exception of pairs(), the following functions and the # operator apply only to tables
that are indexed starting from 1 and do not have NIL entries. All other forms of tables can be
considered as unordered hash maps, rather than arrays that have a length.
add(tbl, val, [index])
Add value val to the end of table tbl. Equivalent to:
tbl[#tbl + 1] = val
If index is given then the element is inserted at that position:
foo={} -- create empty table
add(foo, 11)
add(foo, 22)
print(foo[2]) -- 22
del(tbl, val)
Delete the first instance of value VAL in table TBL. The remaining entries are shifted left
one index to avoid holes.
Note that val is the value of the item to be deleted, not the index into the table. (To
remove an item at a particular index, use deli instead). del() returns the deleted item, or
returns no value when nothing was deleted.
a={1,10,2,11,3,12}
for item in all(a) do
if (item < 10) then del(a, item) end
end
foreach(a, print) -- 10,11,12
print(a[3]) -- 12
deli(tbl, [index])
Like @del(), but remove the item from table tbl at index. When index is not given, the last
element of the table is removed and returned.
count(tbl, [val])
Returns the length of table t (same as #tbl) When val is given, returns the number of
instances of VAL in that table.
all(tbl)
Used in for loops to iterate over all items in a table (that have a 1-based integer index),
in the order they were added.
t = {11,12,13}
add(t,14)
add(t,"hi")
for v in all(t) do print(v) end -- 11 12 13 14 hi
print(#t) -- 5
foreach(tbl, func)
For each item in table tbl, call function func with the item as a single parameter.
> foreach({1,2,3}, print)
pairs(tbl)
Used in for loops to iterate over table tbl, providing both the key and value for each
item. Unlike @all(), pairs() iterates over every item regardless of indexing scheme. Order
is not guaranteed.
t = {["hello"]=3, [10]="blah"}
t.blue = 5;
for k,v in pairs(t) do
print("k: "..k.." v:"..v)
end
Output:
k: 10 v:blah
k: hello v:3
k: blue v:5
----------------------------------------------------------------------------------------------------
PODs
----------------------------------------------------------------------------------------------------
A POD ("Picotron Object Data") is a string that encodes Lua values: tables, userdata, strings,
numbers booleans, and nested tables containing those types.
PODs form the basis of all data transfer and storage in Picotron. Every file is a single POD on
disk, the contents of the clipboard is a POD, images embedded in documents are PODs, and
messages sent between processes are PODs.
pod(val, [flags], [metadata])
Returns a binary string encoding val.
flags determine the encoding format (default: 0x0)
metadata is an optional value that is encoded into the string and stores additional
information about the pod.
?pod({a=1,b=2})
{a=1,b=2}
pod() returns nil when the input value contains functions, circular references, or other
values that can not be encoded.
flags:
0x1 pxu: encode userdata in a compressed (RLE-style) form
0x2 lz4: binary compression pass (dictionary matching)
0x4 base64 text encoding (convert back into a text-friendly format)
Plaintext PODs can get quite large if they contain images or map data. A compressed
binary encoding can be generated using flags 0x1 and 0x2, which are normally used
together as the pxu format aims to produce output that can be further compressed by
lz4. store() uses this format by default.
The resulting string contains non-printable characters and starts with the header
"lz4\0", so only the first 3 characters are printed here:
?pod({a=1,b=2}, 0x3)
lz4
unpod(str)
returns the decoded value, and the decoded metadata as a second result:
str = pod({4,5,6}, 0, {desc = "an uninteresting sequence"})
c,m = unpod(str) -- returns content and metadata
?m.desc -- an uninteresting sequence
?c[1] -- 4
----------------------------------------------------------------------------------------------------
Files
----------------------------------------------------------------------------------------------------
A file in picotron is a single POD (see the previous section), and uses the metadata part of
the POD as a metadata fork. As such, files are stored and fetched atomically; there is no
concept of a partial read, write or append.
store(filename, obj, [metadata])
store a lua object (tables, strings, userdata, booleans and numbers are allowed) as a file.
filenames can contain alphanumeric characters, "_", "-" and "."
When metadata is given, each field is added to the file's metadata without clobbering any
existing fields.
store("foo.pod", {x=3,y=5})
a = fetch("foo.pod")
?a.x -- 3
When a cartridge needs to persist data (settings, high scores etc), it can use store() to
write to /appdata:
store("/appdata/mygame_highscores.pod", highscore_tbl)
If the cartridge needs to store more than one or two files, a folder can be used:
mkdir("/appdata/mygamename")
store("/appdata/mygamename/highscores.pod", highscore_tbl)
Either method is fine. In most cases, cartridges are run directly from the BBS and thus
sandboxed so that writes to /appdata/ are mapped to /appdata/bbs/cart_id/. This means that
BBS carts can not read or clobber data written by other bbs carts, except for data written
to a special shared folder: /appdata/shared.
When running under web, /appdata (and only /appdata) is persisted using Indexed DB
storage. This applies to both html exports and carts running on the BBS.
fetch(filename)
Return a lua object stored in a given file. Returns the object and metadata.
store_metadata(filename, metadata)
fetch_metadata(filename)
Store and fetch just the metadata fork of a file or directory. This can be faster in some
cases.
mkdir(name)
Create a directory
ls([path])
list files and folders in given path relative to the current directory.
cp(src, dest)
Copy a file from src to dest. Folders are copied recursively, and dest is overwritten.
mv(src, dest)
Move a file from src to dest. Folders are moved recursively, and dest is overwritten.
rm(filename)
Delete a file or folder (recursive).
Mount points are also deleted, but the contents of their origin folder are not deleted
unless explicitly given as a parameter to rm.
pwd()
Return the present working directory. Relative filenames (that do not start with "/") all
resolve relative to this path.
cd()
Change directory.
fullpath(filename)
Resolve a filename to its canonical path based on the present working directory (pwd()).
fstat(filename)
returns 3 attributes of given filename (if it exists):
string: "file" or "folder"
number: size of file
string: origin of path
include(filename)
Load and run a lua file.
The filename is relative to the present working directory, not the directory that the file
was included from.
Note that include() is quite different from PICO-8's #include, although it is used in a
similar way. The difference is that include() is a regular function that is called at
runtime, rather than PICO-8's #include which inserts the raw contents of the included file
at the preprocessing stage.
include(filename) is roughly equivalent to:
load(fetch(filename))()
:: File Sandboxing
A sandboxed process only has limited access to the filesystem. This allows untrusted
cartridges to be run without risk of messing up other parts of the system (e.g. a
malicious bbs cart might try to rm("/desktop")). All BBS carts (e.g. bbs://foo.p64) are
run sandboxed; they are only allowed to write to /appdata (which is mapped to
/appdata/bbs/{bbs_id}), and /appdata/shared. They can also only read from themselves,
/system and /ram/shared.
When a cartridge is copied from the BBS to local filesystem (e.g. desktop), it is given
some metadata so that it continues to run sandboxed in the same way: .sandbox = "bbs" and
.bbs_id (the cart id). It can be un-sandboxed using the about tool and unchecking
"sandbox", or by using "load -u #foo"
To sandbox a cartridge during development to see how it will behave on the BBS, type
"about" from the commandline and check the sandbox field to get a dummy bbs id starting
with an underscore that can be used for testing.
Files opened via the open command (/system/util/open.lua) are additionally accessible from
sandboxed processes no matter where they are on disk, as are files drag-and-dropeed into
the app window, and files chosen via the file open dialogue. In short: access to arbitrary
locations is given to sandboxed apps when the user performs an action that shows clear
intent to allow it.
For more details, see:
https://www.lexaloffle.com/dl/docs/picotron_filesystem.html#Sandboxing
----------------------------------------------------------------------------------------------------
System
----------------------------------------------------------------------------------------------------
printh(str)
print a string to the host operating system's console for debugging.
env()
Returns a table of environment variables given to the process at the time of creation.
?pod(env()) -- view contents of env()
stop([message])
stop the cart and optionally print a message
assert(condition, [message])
if condition is false, stop the program and print message if it is given. this can be
useful for debugging cartridges, by assert()'ing that things that you expect to be true are
indeed true.
assert(actor) -- actor should exist and be a table
actor.x += 1 -- definitely won't get a "referencing nil" error
time()
t()
Returns the number of seconds elapsed since the cartridge was run.
This is not the real-world time, but is calculated by counting the number of times
_update60 is called. multiple calls of time() from the same frame return the same result.
date(format, t, delta)
Returns the current day and time formatted using Lua's standard date strings.
format: specifies the output string format, and defaults to "!%Y-%m-%d %H:%M:%S" (UTC) when
not given. Picotron timestamps stored in file metadata are stored in this format.
t: specifies the moment in time to be encoded as a string, and can be either an integer
(epoch timestamp) or a string indicating UTC in the format: "!%Y-%m-%d %H:%M:%S". When t is
not given, the current time is used.
delta: number of seconds to add to t.
-- show the current UTC time (use this for timestamps)
?date()
-- show the current local time
?date("%Y-%m-%d %H:%M:%S")
-- convert a UTC date to local time
?date("%Y-%m-%d %H:%M:%S", "2024-03-14 03:14:00")
-- local time 1 hour ago
?date("%Y-%m-%d %H:%M:%S", nil, -60*60)
get_clipboard()
set_clipboard(text)
Read and write the contents of the clipboard. The value is always a single string; to copy
structured objects to the clipboard, use @pod() and @unpod().
For security reasons, get_clipboard() only has access to the host clipboard after ctrl-v is
pressed while Picotron is active. Until ctrl-v is pressed, changes to the host clipboard
have no effect on the return value of get_clipboard(). The same is true for sandboxed
applications (e.g. bbs carts): they are only able to access clipboard contents from other
processes once ctrl-v is pressed while that app has keyboard focus.
out = "[output]\n"
function _update()
if key"ctrl" and keyp"c" then
local test_str = "test"..flr(rnd(10000))
set_clipboard(test_str)
out ..= "ctrl-c copied: "..test_str.."\n"
end
if key"ctrl" and keyp"v" then
out ..= "ctrl-v pasted: "..get_clipboard().."\n"
end
-- this will only work for clipboard contents that is copied from within Picotron
-- (or within the same app when sandboxed), unless pasted with ctrl-v first.
if key"ctrl" and keyp"b" then
out ..= "ctrl-b pasted: "..get_clipboard().."\n"
end
end
function _draw()
cls()
print(out, 2,2,7)
end
----------------------------------------------------------------------------------------------------
Memory
----------------------------------------------------------------------------------------------------
Each process in Picotron has a limit of 32MB RAM, which includes both allocations for Lua
objects, and data stored directly in RAM using memory functions like poke() and memcpy(). In
the latter case, 4k pages are allocated when a page is written, and can not be deallocated
during the process lifetime.
Only 16MB of ram is addressable: 0x000000..0xffffff. Memory addresses below 0x80000 and above
0xf00000 are mostly reserved for system use, but anything in the 0x80000..0xefffff range can
be safely used for arbitrary purposes.
:: Memory Layout
0x000000 ~ 0x003fff Legacy PICO-8 range, but probably safe to use!
0x004000 ~ 0x0047ff Primary P8SCII Font (2k)
0x005000 ~ 0x0053ff ARGB display palettes (1k)
0x005400 ~ 0x005477 Per-scanline rgb display palette selection (120 bytes)
0x005480 ~ 0x0054bf Indexed display palette (64 bytes)
0x0054c0 ~ 0x00553f Misc draw state (128 bytes)
0x005580 ~ 0x0055ff Raw controller state (128 bytes)
0x005600 ~ 0x005dff Secondary P8SCII font (2k)
0X005e00 ~ 0x005eff Reserved: P8 persistent state (256 bytes)
0x005f00 ~ 0x005f7f P8 draw State (some used by Picotron)
0x005f80 ~ 0x007fff Reserved: legacy P8 gpio, video memory
0x008000 ~ 0x00bfff Colour tables (16k)
0x00c000 ~ 0x00ffff Reserved (16k)
0x010000 ~ 0x02ffff Display / Draw Target (128k)
0x030000 ~ 0x07ffff Default audio data range
0x080000 ~ 0xefffff Available for arbitrary use
0xf00000 ~ 0xffffff Wavetable data
peek(addr, [n])
read a byte from an address in ram. if n is specified, peek() returns that number of
results (max: 65536). for example, to read the first 2 bytes of video memory:
a, b = peek(0x10000, 2)
poke(addr, val1, val2, ...)
write one or more bytes to an address in base ram. if more than one parameter is provided,
they are written sequentially (max: 65536).
peek2(addr)
poke2(addr, val)
peek4(addr)
poke4(addr, val)
peek8(addr)
poke8(addr, val)
i16,i32 and i64 versions.
memcpy(dest_addr, source_addr, len)
copy len bytes of base ram from source to dest. sections can be overlapping (but is slower)
memset(dest_addr, val, len)
write the 8-bit value val into memory starting at dest_addr, for len bytes.
for example, to fill half of video memory with 0xc8:
> memset(0x10000, 0xc8, 0x10000)
----------------------------------------------------------------------------------------------------
Windows
----------------------------------------------------------------------------------------------------
Each process in Picotron has a single window, and a single display that always matches the size
of the window. The display is a u8 userdata that can be manipulated using the regular userdata
methods, or using the gfx api while the display is also the draw target.
When a program has a _draw function but a window does not exist by the end of _init(), a
fullscreen display and workspace is created automatically. To explicitly create a fullscreen
display before then, window() with no parameters can be used.
Although switching between fullscreen and windowed modes is possible, the window manager does
not yet support that and will produce unexpected results (a window in a fullscreen workspace,
or a fullscreen window covering the desktop).
get_display()
Returns the current display as a u8, 2d userdata. There is no way to set the display
userdata directly; it can be resized using the window() function.
set_draw_target(ud)
get_draw_target()
Set the draw target to ud, which must be a u8, 2d userdata. When ud is not given,
set_draw_target() defaults to the current display.
window(attribs)
window(width, height)
Create a window and/or set the window's attributes. attribs is table of desired attributes
for the window:
window{
width = 80,
height = 160,
resizeable = false,
title = "Palette"
}
function _draw()
cls(7)
for y=0,7 do
for x=0,3 do
circfill(10 + x * 20, 10 + y * 20, 7, x+y*4)
end
end
end
width -- width in pixels (not including the frame)
height -- height in pixels
title -- set a title displayed on the window's titlebar
pauseable -- false to turn off the app menu that normally comes up with ENTER
tabbed -- true to open in a tabbed workspace (like the code editor)
has_frame -- default: true
moveable -- default: true
resizeable -- default: true
wallpaper -- act as a wallpaper (z defaults to -1000 in that case)
autoclose -- close window when is no longer in focus or when press escape
z -- windows with higher z are drawn on top. Defaults to 0
cursor -- 0 for no cursor, 1 for default, or a userdata for a custom cursor
squashable -- window resizes itself to stay within the desktop region
System cursors are named, and can be requested using a string:
pointer hand with a finger that presses down while mouse button is pressed
grab open hand that changes into grabbing pose while mouse button is pressed
dial hand in a dial-twirling pose that disappears while mouse button is held down
crosshair
vid(video_mode)
Set a fullscreen video mode. Currently supported modes:
vid(0) -- 480x270
vid(3) -- 240x135
vid(4) -- 160x90
====================================================================================================
Userdata
====================================================================================================
Userdata in Picotron is a fixed-size allocation of memory that can be manipulated as a 1d or 2d
array of typed data. It is used to repesent many things in Picotron: vectors, matrices, to
store sprites, maps and the contents of display. Therefore, all of these things can be
manipulated with the userdata API. It is also possible to expose the raw binary contents of a
userdata to RAM (by giving it an address with @memmap), in which case userdata API can be used
to directly manipulate the contents of RAM.
:: Userdata Access
u = userdata("i16", 4, 8) -- a 4x8 array of 16-bit signed integers
u:set(2,1,42) -- set the elements at x=2, y=1 to 42
?#u -- the total number of elements (32)
Userdata can be indexed as a 1d array using square brackets, and the first 7 elements of a
userdata can be accessed using special names: x y z u v w t.
The following assignments and references are equivalent for a 2d userdata of width 4:
u:set(2,1,42)
u[6] = 42
u.t = 42
?u:get(2,1)
?u[6]
?u.t
userdata(data_type, width, height, [data])
Create a userdata with a data type: "u8", "i16", "i32", "i64", or "f64". The first 4 are
integers which are unsigned (u) or signed(i), and with a given number of bits. The last one
is for 64-bit floats, and can be used to implement vectors and matrices.
data is a string of hexadecimal values encoding the initial values for integer values, or a
list of f64s separated by commas.
A 2d 8-bit userdata can also be created by passing a PICO-8 [gfx] snippet as a string (copy
and paste from PICO-8's sprite editor):
s = userdata("[gfx]08080400004000444440044ffff094f1ff1944fff9f4044769700047770000a00a00[/gfx]")
spr(s, 200, 100)
vec(...)
A convenience function for constructing 1d vectors of f64s.
v = vec(1.0,2.0,3.5)
-- equivalent to:
v = userdata("f64", 3)
v:set(0, 1.0,2.0,3.5)
userdata:width()
userdata:height()
returns the width, height of a userdata
height() returns nil for a 1d userdata.
?userdata(get_display():width()) -- width of current window
userdata:attribs()
returns the width, height, type and dimensionality of a userdata. Unlike :height(),
:attribs() returns 1 as the height for 1d userdata.
userdata:get(x, n)
userdata:get(x, y, n)
Return n values starting at x (or x, y for 2d userdata), or 0 if out of range.
?get_display():get(20, 10) -- same as ?pget(20, 10)
userdata:set(x, val0, val1, ..)
userdata:set(x, y, val0, val1, ..)
Set one or more value starting at x (or x, y for 2d userdata).
Values set at locations out of range are clipped and have no effect.
get and set are also available as global functions: set(u,0,0,3) is the same as
u:set(0,0,3). When the global set() is passed a nil userdata, no error or action is
performed.
userdata:row(i)
userdata:column(i)
Return a row or column of a 2d userdata (0 is the first row or column), or nil when out of
range.
userdata:blit(dest, src_x, src_y, dest_x, dest_y, width, height)
blit(src, dest, src_x, src_y, dest_x, dest_y, width, height)
Copy a region of one userdata to another. The following copies a 8x7 pixel region from
sprite 0 to the draw target at 100, 50:
blit(get_spr(0), get_draw_target(), 0, 0, 100, 50, 8, 7)
Both src and dest must be the same type.
When dest is the draw target, the current clipping state is applied. Otherwise no clipping
is performed (except to discard writes outside the destination userdata). In either case,
no other aspects of the draw state are observed, and it is much faster than an equivalent
sspr call.
All arguments are optional: width and height default to the src width and height, and the
two userdata parameters default to the current draw target.
userdata:mutate(data_type, [width], [height])
Change the type or size of a userdata. When changing data type, only integer types can be
used.
The binary contents of the userdata are unchanged, but subsequent operations will treat
that data using the new format:
> ud = userdata("i32", 2, 2)
> ud:set(0,0, 1,2,3,-1)
> ?pod{ud:get()}
{1,2,3,-1}
> ud:mutate("u8", 8,2)
> ?pod{ud:get()}
{1,0,0,0,2,0,0,0,3,0,0,0,255,255,255}
The total data size given by the new data type and dimensions must be the same as or
smaller than the old one. In the above example, the userdata starts with 2x2 i32's (16
bytes) and is changed to 8x2 u8's (also 16 bytes).
When the width and height is not given, the existing width is used multiplied by the ratio
of old data type size to new one, and the existing height is used as-is. Note that this can
result in a loss of total data size when the width is not evenly divisible.
> ud = userdata("u8", 200, 50)
> ud:mutate("i64")
> ?{ud:attribs()}
{25,50,"i64",2}
userdata:lerp(offset, len, el_stride, num_lerps, lerp_stride)
linearly interpolate between two elements of a userdata
offset is the flat index to start from (default: 0)
len is the length (x1-x0) of the lerp, including the end element but not the start element.
el_stride is the distance between elements (default: 1)
Multiple lerps can be performed at once using num_lerps, and lerp_stride. lerp_stride is
added to offset after each lerp.
> v = vec(2,0,0,0,10):lerp()
?pod{v:get()} -- 2,4,6,8,10
> v = vec(0,2,0,4,0):lerp(1,2)
?pod{v:get()} -- 0,2,3,4,0
> v = vec(2,0,0,0,10):lerp(0,2,2)
?pod{v:get()} -- 2,0,6,0,10
> v = vec(1,0,3,0,10,0,30):lerp(0,2,1, 2,4)
?pod{v:get()} -- 1,2,3, 0, 10,20,30
userdata:convert(data_type, [dest])
Return a copy of userdata cast as a different type. When converting to ints, f64 values are
flr()'ed and out of range values overflow.
v = vec(5.3, 5.7, 257)
?pod{v:convert("u8"):get()} -- {5,5,1}
userdata:sort(index, descending)
Sort a 2d userdata of any type by the value found in the index column (0 by default).
When descending is true, sort from largest to smallest.
scores = userdata("i32", 2, 3)
scores:set(0,0, 3,2000, 4,4000, 7,3000)
scores:sort(1, true) -- sort by second column descending
?pod{scores:get()} -- {3,2000, 7,3000, 4,4000)
----------------------------------------------------------------------------------------------------
UserData Operations
----------------------------------------------------------------------------------------------------
Userdata can be used with arithmetic operators, in which case the operator is applied per
element:
v = vec(1,2,3) + vec(10,20,30)
?v -- (11.0,22.0,33.0)
When one of the terms is a scalar, that value is applied per element:
v = vec(1,2,3) + 10
?v -- (11.0, 12.0, 13.0)
v = vec(1,2,3) / 2
?v -- (0.5,1.0,1.5)
Supported operators for any userdata type: + - * / % ^
Bitwise operators for integer userdata types: & | ^^
Each operator has a corresponding userdata metamethod that can take additional parameters (see
@userdata:op):
:add :sub :mul :div :mod :pow :band :bor :bxor
Additional operation metamethods that do not have a corresponding operator:
:max -- return the largest of each element / scalar
:min -- return the smallest of each element / scalar
Additional unary operation metamethods that ignore the src parameter:
:copy -- equivalent to :add(0, ...) :abs -- abs(x) for each element (except: int_min ->
int_min, not int_max) :sgn -- returns -1 for negative values and 1 for positive values and
zero :sgn0 -- returns -1 for negative values and 1 for positive values, and 0 for zero
userdata:op(src, dest, src_offset, dest_offset, len, src_stride, dest_stride, spans)
Applies operator op (add, mult etc) to each element and written to a new userdata. All
parameters are optional.
For each element, the LHS is taken from the calling userdata, and the RHS is taken from the
"src" userdata:
dest_val = ud_val {op} src_val
For example, the following divides each value in a userdata by a value from src:
?vec(1,2,3):div(vec(2,2,10)) -- (0.5, 1.0, 0.3)
?vec(1,2,3) / vec(2,2,10) -- same thing
ud or src can be a number in which case that number is used as the LHS / RHS operand for each
element:
v = vec(1,2,3)
v = v:add(10) -- add 10 to each element -- same as v += 10
dest is an optional output userdata, which can be the boolean value true to mean "write to
self". This can be used to avoid the cpu overhead of creating new userdata objects.
dest must be the same shape as the calling userdata, otherwise nil is returned. This is because
c = a:div(b) should give the same result as a:div(b,c) for the modified elements.
v:add(10, v) -- add 10 to each element of v, written in-place
v:add(10, true) -- same thing
:: Partial Operations
Flat offsets into src and dest can be given, as well as a number of elements to process (len).
When operations are applied to a partial subset of elements, the remaining elements are not
modified. This means that any existing values in the calling userdata (or in dest when dest is
given) can carry over.
For example, in the following call to :mul, only the 2 elements are modified starting from
offset 1:
out = vec(0,1,2,3,4):mul(100, nil, 0, 1, 2)
?out -- (0,100,200,3,4)
When dest is given, the same rule applies.
out = vec(5,6,7,8,9)
vec(0,1,2,3,4):mul(100, out, 0, 1, 2)
?out -- (5,100,200,8,9)
:: Stride
The last 3 parameters (src_stride, dest_stride and spans) can be used together to apply the
operation to multiple, non-contiguous spans of length len. src_stride, and dest_stride specify
the number of elements between the start of each span for src and dest respectively. Both are
expressed as flat indices (i.e. for 2d userdata the element at x,y has a flat index of x + y *
width).
This is easier to see with a visual example:
foo = userdata("u8", 4, "01020304")
function _draw()
cls()
circfill(240 + t()*10,135,100,7)
get_display():add(foo, true, 0, 0, 4, 0, 16, 10000)
end
This is an in-place operation -- reading and writing from the display bitmap (which is a 2d
userdata).
It modifies the first 4 pixels in each group of 16, 10000 times (so 40000 pixels are modified).
First, 1 is added to the first pixel, 2 to the second, up to the 4th pixel. The second span
starts at the 16th pixel, and reading again from the start of foo (because the stride for src
is 0), which means the same 4 values are added for every span.
Note that this example is a pure userdata operation -- no graphical clipping is performed
except to stay within the linear range of each input userdata.
:: Overlapping Evaluations
Userdata operations that have overlapping regions are allowed, and are always calculated left
to right. This means that when the src and dest userdata are the same, some elements may be
read after they have already been modified at some point earlier in the operation, and that
new value is then used for another calculation.
In the following example, the destination offset is 1, which means that the first calculation
is a[1]= a[1]+a[0], and then a[2]=a[2]+a[1] and so on. The result is that each element is
evaluated to the sum of itself plus all of the elements before it:
> a = vec(1,2,5,200)
> a:add(a,true,0,1)
> ?pod{unpack(a)}
{1,3,8,208} -- 1, 2+1, 5+2+1, 200+5+2+1
:: CPU Costs
Operations on userdata cost 1 cycle for every 24 operations, except for mult (16 ops), div/mod
(4 ops), pow (2 ops), and operations that do a compare (4), plus any overhead for the function
call itself.
:: Copy with Lookup Table
userdata:copy(idx, dest, idx_offset, dest_offset, len, idx_stride, dest_stride, spans)
** this form will be deprecated in 0.1.2 -- use :take instead with the same parameters.
When :copy is given a table as the first argument (after self), it is taken to be a lookup
table into that userdata for the start of each span.
userdata:take(idx, dest, idx_offset, dest_offset, span_len, idx_stride, dest_stride, spans)
Take values from the userdata at locations specified by idx.
idx is a i32 userdata, where each value is a flat index into the userdata. When dest is not
specified, the userdata returned by :take is the same shape as idx.
src = vec(0,10,20,30,40)
idx = userdata("i32",4,2)
idx:set(0,0, 0,2,4,0,2,4,1,3) -- flat indexes into src
dest = src:take(idx) -- grab 8 values
?pod{dest:get()} -- 0,20,40,0,20,40,10,30
When dest (a userdata of the same type) is given, values are written starting at dest_offset,
also a flat index.
Multiple spans can be specified the same way as other userdata operations. Each value in idx
specifies the start index of a span, and span_len elements are copied from that position in
the calling userdata.
The default span_len is 1, in which case the default shape of the output is the same as the
shape of idx.
idx_stride is applied between each index, and defaults to 1.
dest_stride is applied after writing each span. It defaults to span_len.
To take 3 spans from src, each of length 4:
src = vec(0,1,2,3,4,5,6,7)
idx = userdata("i32",3)
idx:set(0, 3,1,4)
dest = src:take(idx,nil, 0,0, 4)
When the length of each span is > 1, the default shape of the output is a row for each span. In
this case, there are 3 spans starting at positions 3,1 and 4 -- each each span is 4 values. So
the resulting userdata is 4x3:
3 4 5 6
1 2 3 4
4 5 6 7
----------------------------------------------------------------------------------------------------
Matrices and Vectors
----------------------------------------------------------------------------------------------------
Matrices and vectors can be represented as 2d and 1d userdata of type f64:
mat = userdata("f64", 4, 4)
set(mat, 0, 0,
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1)
pos = vec(3,4,5)
?pos.x -- 3
pos += vec(10,10,10) -> 13.0, 14.0, 15.0
pos *= 2 -> 26.0, 28.0, 30.0
?v
matmul(m0, m1, [m_out])
Multiply two matrixes together. matmul is part of the userdata metatable, so it can also be
called using the equivalent form: m0:matmul(m1).
When m_out is given, the output is written to that userdata. Otherwise a new userdata is
created of width m1:width() and height m0:height().
As per standard matrix multiplication rules, the width of m0 and the height of m1 must match --
otherwise no result is returned.
mat = mat:matmul(mat)
v2 = vec(0.7,0.5,0.5,1):matmul(mat) -- vector width matches matrix height
matmul3d(m0, m1, [m_out])
For 3d 4x4 transformation matrices, matmul3d can be used.
matmul3d implements a common optimisation in computer graphics: it assumes that the 4th column
of the matrix is (0,0,0,1), and that the last component of LHS vector (the mysterious "w") is
1. Making these assumptions still allows for common tranformations (rotate, scale, translate),
but reduces the number of multiplies needed, and so uses less cpu.
matmul3d can be used on any size vector as only the first 3 components are observed, and
anything larger than a 3x4 userdata for the RHS matrix; again, excess values are ignored.
So apart from the cpu and space savings, matmul3d can be useful for storing extra information
within the same userdata (such as vertex colour or uv), as it will be ignored by matmul3d().
matmul() is less flexible in this way, as it requires unambiguous matrix sizes.
See /system/demos/carpet.p64 for an example.
:: Vector methods
:magnitude()
:distance(v)
:dot(v)
:cross(v, [v_out])
:: Matrix methods
:matmul(m, [m_out])
:matmul2d(m, [m_out])
:matmul3d(m, [m_out])
:transpose([m_out])
Like the per-component operation methods, v_out or m_out can be "true" to write to self.
Matrix methods always return a 2d userdata, even when the result is a single row. They can only
be used with f64 userdata (with the exception of :transpose, that can handle any type).
----------------------------------------------------------------------------------------------------
Userdata Memory Functions
----------------------------------------------------------------------------------------------------
The contents of an integer-typed userdata can be mapped to ram and accessed using regular
memory functions like peek and memcpy. This can be useful for things like swapping colour
tables in and out efficiently.
memmap(ud, addr)
Map the contents of an integer-type userdata to ram.
addr is the starting memory address, which must be in 4k increments (i.e. end in 000 in
hex).
Userdata does not need to be sized to fit 4k boundaries, with one exception: addresses
below 0x10000 must always be fully mapped, and memmap calls that break that rule return
with no effect.
unmap(ud, [addr])
Unmap userdata from ram. When an address is given, only the mapping at that address is
removed. This is relevant only when there are multiple mappings of the same userdata to
different parts of memory.
unmap(ud) is needed in order for a userdata to be garbage collected, as mapping it to ram
counts as an object reference. Overwriting mappings with @memmap() is not sufficient to
release the reference to the original userdata.
userdata:peek(addr, offset, elements)
userdata:poke(addr, offset, elements)
read or write from ram into an integer-typed userdata.
addr is the address to peek / poke
offset is the userdata element to start from (flattened 1d index), and len is the number of
elements to peek/poke.
For example, to poke a font (which is a pod containing a single u8 userdata) into memory:
fetch("/system/fonts/p8.font"):poke(0x4000)
Or to load only the first 4 instruments of a .sfx file:
fetch("foo.sfx"):poke(0x40000, 0x10000, 0x200 * 4)
----------------------------------------------------------------------------------------------------
Batch GFX Operations
----------------------------------------------------------------------------------------------------
:: Batch GFX Operations
A userdata can be used to represent lists of arguments to be passed to gfx functions, so that
multiple draws can be made with only the overhead of a single function call. This is supported
by @pset, @circfill, @rectfill, @tline3d and @spr.
The following draws 3 circles:
args = userdata("f64", 4, 3)
args:set(0,0,
100,150,5,12, -- blue circle
200,150,5,8, -- red cricle
300,150,5,9) -- orange circle
circfill(args)
gfx_func(p, offset, num, num_params, stride)
p is the f64 userdata -- normally 2d with a row for each call
offset is the flat offset into the userdata for the first call. Default: 0
num is the number of gfx calls to make. Default: p:height()
params is the number of parameters to pass to the gfx function. Default: p:width()
stride is the number of elements to jump after each call. Default: p:width()
--------------------------------------------------------------------------------------------
PICOTRON VERSION HISTORY
--------------------------------------------------------------------------------------------
0.1.1f:
Added: export foo.p64.png to save a copy of working cartridge (without switching from that working cart)
Changed: only foreground process can read controller buttons
Changed: ctrl-p opens picotron menu (useless now but reserved for future)
Changed: linux uses dynamically loaded libcurl (wget no longer required), mac uses static libcurl
Fixed: load #foo sometimes fails to fetch cartridge (when no version number specified)
Fixed: batch cart downloads don't happen in parallel on mac, web (-> slow to open bbs://new/0 etc)
Fixed: not caching cart fetches under web (now cached per session, but not persisted in IndexedDB)
Fixed: system level crash when a process include()s itself (temporary hack: can't include file > 256 times)
Fixed: exported html shell does not block all keypresses when picotron app has focus (e.g. ctrl-s, ctrl-o)
Fixed: sandboxed dev cartridge can store() to self -- only /appdata and /appdata/shared should be writeable
Fixed: (regression in 0.1.1e) some gui events are not dispatched when there are multiple active guis
Fixed: hiding a button on click throws an error // https://www.lexaloffle.com/bbs/?pid=160440#p
0.1.1e:
Added: /system/widgets/owl.p64
Added: widgets can be installed by dragging any window into the tooltray -- stored in /appdata/system/widgets.pod
Added: bbs:// protocol // try "BBS Carts" from the Picotron menu
Added: automatic sandboxing for all bbs carts. /appdata maps to /appdata/bbs/cart_id + restrictions on send_message()
Added: files created by bbs:// carts are associated (metadata.prog) as a fallback default app to open it.
Added: open(location) // uses /system/util/open.lua -- can be used by sandboxed cartridges w/ a rate limit
Added: pitch ratios (TUNE *) can now compound and observe envelopes, RAND, multipliers, be set in ROOT.
Added: mousewheel to adjust tracker number fields / mb + wheel for inst knobs & fields. hold ctrl for +8,-8
Added: play music from a separate .sfx file: fetch("music.sfx"):poke(0x80000) music(0,0,nil,0x80000)
Added: Audio mix volume (0x5538), music mix volume (0x5539) and per-channel volume (0x553a, used by sfx()).
Added: ud:convert(type) // can convert between any type. f64 values are floored to convert to ints.
Added: ud:sort(column, desc) // to sort by a given column index and desc==true to sort largest to smallest
Added: ud:pow(), ud:sgn(), ud:sgn0(), ud:abs()
Added: ud:transpose() works on any data type
Added: diagonal flip bit + r to rotate selection in map + gfx editor. Supported by map(), but not tline3d() [yet?]
Added: gui attributes: squash_to_clip, confine_to_clip, squash_to_parent, confine_to_parent
Added: squashable windows: window{width=200,height=100,squashable=true} -- /system/demos/squashable.p64
Added: menuitem() label can be a function that returns the string
Added: screen / gif captures are named after the active window
Added: /system/screensavers/xyzine.p64 (run directly to use it interactively)
Added: desktop support for web in exports (allows exports with tabbed interfaces, gif & png captures, persist /desktop)
Changed: Per-process RAM limit is 32MB, with 16MB addressable (was 16MB, 16MB)
Changed: Userdata lookups have a separate function: ud:take(idx,...); *** ud:copy(idx,...) will be removed in 0.1.2!
Changed: Reduced number of sprite banks from 64 -> 32 (in order to make diagonal flip bit standard)
Changed: userdata:op() with no args is now a NOP for all ops except :copy; used to use self as RHS which is confusing
Changed: include() returns the results from loaded function (used to return true) -> can use module loading pattern
Changed: tweaked default palette for pairings & separation: 16,18,21,24,25,26,31
Changed: cartridge label stored in ram in qoi format (was png) for faster encoding and .p64.png mounting
Changed: default sandbox profile for bbs carts relaxed; can read /desktop, can R/W /ram/cart, can launch filenav.p64
Changed: env().title removed, env().prog_name moved to env().argv[0]
Changed: get_clipboard() can only read the host clipboard after ctrl-v is pressed inside picotron (security)
Changed: all gui callbacks always get mx,my,mb (was missing in :update)
Changed: ?vec(1/3,2,3.1E+234) prints with more precision, and integers without the fractional part (same as ?pod{...})
Changed: map editor: f,v (and now r) when nothing is selected alters the current brush; not the whole map. cursors moves camera.
Changed: pal() only charges cpu for colour tables that have changed since last call (~ 2x as fast)
Changed: escape while running a fullscreen cartridge brings up pause menu. (use alt+L/R to switch to desktop instead)
Fixed: terminal launched from another terminal sends print() output of launched programs to the parent terminal instead of self
Fixed: .p64 files still sometimes only partially stored (!) // race condition; introduced atomic disk operations for safety
Fixed: pending disk changes are not flushed when closing window immediately (<100ms) after copying / saving a cartridge
Fixed: crash when > 256 carts are mounted in one session due to unneeded cart mounts not being swept
Fixed: a:take(b2d) returns the correct shape when b is 2d, but only takes the first row of items.
Fixed: a:copy(nil, dest, ...) does not observe offsets/strides
Fixed: userdata ops with a scalar and an output e.g. a:add(3, b) -> reads from b instead of a
Fixed: mutate() does not alter dimensionality
Fixed: context menu cut / copy callbacks are broken
Fixed: carts that are folders on host (folders named foo.64) are sorted out of order by ls()
Fixed: unpod"{foo[hoge]}" crashes
Fixed: srand() only gives a different seed every second in web player (see rain in /bbs/?tid=142370)
Fixed: gui element draw() needs to exist for an element's children to be clipped and hidden
Fixed: scrollbar content is clickable outside of parents even when clip_to_parent is true
Fixed: abs(), sgn(), min(), max(), mid() are slow (using placeholder lua implementations)
Fixed: coroutine.resume() doesn't handle nested coroutines -- now just an alias for coresume()
Fixed: userdata :div(0), :idiv(0) throw an error; should behave same as regular operators (return -min,+max for that type)
Fixed: pressing space while playing an instrument (in instrument editor) triggers sfx instead of stopping instrument
Fixed: app menu sometimes appears partially outside visible desktop area (now uses confine_to_clip)
Fixed: printing strings to terminal ending in \0 does not surpress "newline" // e.g. print("abc\0")print("def")
Fixed: unpod() always returning floats for numbers stored as integers // ?unpod(pod(3)) -> 3.0, should be 3
Fixed: tline3d flags in batch operation are ignored
Fixed: The main instrument tune knob in the sfx editor does not work as expected when set to multiply by just ratios
Fixed: Sandboxed carts grant read access to the folder they are inside
Fixed: File dropped from host produces drop_items message without mx, my set (now always center of active window)
Fixed: Wallpaper process sometimes set as active window (should be uninteractive except for mouse x,y,wheel_* events)
Fixed: New Cart produces a completely empty cart folder; should match the default cart with main.lua, gfx/, sfx/, map/
Fixed: Fill tool cursor is broken in gfx editor
Fixed: ctrl-r, ctrl-m causes key("r"), key("m") to be true in active window
Fixed: escape halts currently running pwc (run with ctrl-r) when not in that workspace
Fixed: program corun in terminal can clobber env(), causing subsequent terminal commands to crash (ref: #picovania)
Fixed: shift-ctrl-r broken // runs the file in the code editor and jumps back to output without restarting program
Fixed: note() is sometimes delayed or has no audible effect on a channel that was previously used by sfx()
Fixed: instrument playback logic is broken; should be able to hold, but also to stop long-tail instruments & tracks/music
Fixed: pause menu doesn't close after selected item and callback returns falsey value
Fixed: button state persists after closing pause menu (now: each button is ignored until released)
Fixed: ord() with a negative number of items freezes
Fixed: circ(x,y,rad) with no colour parameter is a NOP (should draw with current draw colour)
0.1.1d:
Added: batch gfx operations for pset,circfill,rectfill,tline3d,spr // many draws with a single function call
Added: /system/demos/pixeldust.p64 // demos batch draw operations
Added: userdata:lerp(offset, len, el_stride, num_lerps, lerp_stride)
Added: userdata:copy(idx, ...) to perform a copy using idx as a lookup table
Added: desktop file items close together form stacks (use mousewheel to flip through)
Added: new context menu structure and items: create, load carts / cut,copy,paste / open cartridge contents
Added: mouse(new_x, new_y) to warp mouse position // not supported under web [yet]
Added: filenav: shift-click in list or grid mode for range selection
Added: filenav list mode: show non-cart folders first with icons
Changed: some vm inst cost only 1 cycle instead of 2 (same as PICO-8: add, sub, bitwise ops, load*, move, unm)
Changed: ls() returns non-cart folders first, is case-insensitive, and sorts by numeric values first
Changed: mousewheel events are passed to the window under the cursor (used to be the active window)
Fixed: memcpy() freezes when len < 4
Fixed: i64 userdata indexed writes have no effect // a = userdata("i64",64) a[0] = 3
Fixed: drawing filled shapes completely outside of clip rectangle sometimes crashes under web (causes illegal read)
Fixed: undercharging cpu for shapes partially clipped horizontally
Fixed: overcharging cpu when expensive operation falls near the end of a process slice
Fixed: host screensaver is blocked while Picotron is running // now using SDL_HINT_VIDEO_ALLOW_SCREENSAVER
Fixed: fetch("http://..") fails // regression in 0.1.1c -- was handling only https
Fixed: gif capture: frame delta is wrong when scanline palette (0x5400) changes
Fixed: music fade in/out speed is slower than requested (was updating once per mix instead of once per tick)
Fixed: filenav list mode: context menu not updated after right-clicking file
Fixed: filenav list mode: file modified date missing (now loads from pod metadata when available)
Fixed: filenav grid mode: when showing many files in grid mode, start to get visual junk
Fixed: unnecessarily large mix buffer size (regression in 0.1.1c -- made sound triggering / tracker feel laggy)
Fixed: matmul functions fail when output is same as input (because clobbering input data as generate output)
Fixed: map editor sprite navigator showing sprites from wrong bank
Fixed: /desktop/readme.txt stored in binary format by 0.1.1b (0.1.1d now re-saves in host-readable txt format)
Fixed: ctrl-r in html exports drops to terminal (is meant to reset cartridge)
Fixed: .p64.png inside an .p64 is listed twice by ls() (and so filenav, ls command etc)
Fixed: panning position is not reset when audio channel is killed
Fixed: default instrument 0 data not initialised for new process (relies on loading a default .sfx before using note())
Fixed: when opening a file in host and in fullscreen, result of the action is not visible (now minimizes self)
Fixed: pulldown menu: parent.onclose is not called (causes e.g. extra click needed for focus after using context menu)
Fixed: renaming a file on desktop causes it to jump around
Fixed: renaming a file via menu drops the original extension when not specified
0.1.1c
Added: inverted drawing for rectfill,circfill,ovalfill // set bit 0x800000000 in col parameter
Added: ud:mutate(type, width, height) to change the type / size of a userdata object
Added: cut/copy/paste/delete selected files in filenav
Added: desktop snap to grid POC -- turn on with: store("/appdata/system/filenav.pod", {snap_to_grid=true})
Added: /system/demos/birds.p64 // demos /ram/shared/windows.pod subscription pattern + transparent window
Added: integer divide for f64 userdata // ?vec(-3.1,3.1,5.9) \ 1 --> (-4.0,3.0,5.0)
Added: sfx(-2, channel_index) to hard kill a channel (leftover state like echos / decay are cut short)
Added: shift-delete, ctrl-insert, shift-insert shortcuts in code editor to cut, copy and paste
Added: userdata :max :min // returns the largest / smallest of each element
Optimised: faster path for shape hspans where span_w >= 16, (window_w&0x7)==0, target_mask==0 (~6M pixels / frame @60fps)
Optimised: filenav (uses cached render at rest), squishy windows, process blitting used by wm
Changed: /ram/shared/windows.pod is published by wm every frame
Changed: mouse cursor position is clamped to display region in host fullscreen (can hide at bottom right pixel)
Changed: stat(400+chan_index, 12) returns -1 when sfx is not playing.
Changed: sfx() offset can be negative to create a delay of n rows before notes are issued
Changed: sfx() channel selection prefers channel 8~15 to reduce accidental clobbering when music starts
Changed: Debug is not available to sandboxed programs
Fixed: event handling and menus breaks when using a custom mainloop; "vid(0)::_::flip()goto _" now works
Fixed: menuitem{id="rename"} does not remove item from menu
Fixed: menuitem() calls from non-active windows clobbering app menu (was affecting filenav)
Fixed: icons missing on drive.loc and readme.txt after fresh install
Fixed: capture.p64 doesn't respect video mode
Fixed: p8scii control character \^c missing -- \^d, \a is still missing, but probably won't support in Picotron
Fixed: filenav menu shows file ops for a single file when multiple files are selected (confusing)
Fixed: nil + vec(1) hard crashes // nil values should be treated as 0 for userdata ops
Fixed: dormant music channels that become active are played up to one mix buffer out of sync (erf!)
Fixed: text input buffer spills into textfield when it becomes active ~ should clear on gaining focus
Fixed: tracker playback following is broken when first track is empty, or when gui play button is used
Fixed: pressing enter in tracker should clear selection or insert a row otherwise, but not both
Fixed: ctrl-c to copy patterns in tracker cuts them
Fixed: off by 1 when blitting process displays to negative positions // causes red pixel in corner of squishy windows
Fixed: "picotron -home foo" host crashes when foo doesn't exist
Fixed: wrangle.lua crashes when opening legacy pods that do not have .revision in metadata
Fixed: gif encoder leaves inter-frame junk for colour 63 when using scanline display palette 3
Fixed: read mask not applied for non-aligned spans while drawing shapes with target mask == 0
Fixed: tonum(bool) doesn't return 0 or 1 (PICO-8 behaviour)
Fixed: mousewheel message is propagated to parent of scrollbox (should consume)
Fixed: Unable to send small number between processes // was happening for values serialised with scientific notation
Fixed: default tab width doesn't line up with monospace font ._.
0.1.1b
Fixed: wm crash when trying to set workspace to "tooltray" when show_in_workspace == nil
Fixed: _rm not defined in fs.lua (causing "load #foo" to fail)
Fixed: gif recording initialisation sometimes fails silently and produces .gif of size 0
0.1.1
Added: html exporter: export foo.html // single self-contained html file that can run locally
Added: web support (bbs/exports): /appdata storage (IDBFS), mouselock(), extended gamepad (twin-stick + SL,SR)
Added: gif capture // ctrl+8 to start, ctrl+9 to end -- max 16 seconds
Added: capture.p64 tool - can be opened with with shift+ctrl+6 or shift+ctrl+8 to select a region from anywhere
Added: FX:FILTER:RES knob can be multiplied by FX:FILTER:LOW for better resonant peak behaviour
Added: FX:SHAPE:MIX knob can be multiplied by the instrument's root node volume
Added: store("foo.bin", "some binary string\0\1\2\3", {metadata_format="none"}) to generate raw host file output
Added: adaptive battery saver: drop down to 30fps when idle for 500ms, and not running fullscreen app or /ram/cart
Added: sandboxing // WIP -- used by web bbs player to prevent carts from being able to clobber each other's data
Added: semi-transparent notification bar
Added: show selected colour index in sprite editor
Added: headless script execution: picotron -x foo.lua (experimental! foo.lua must be inside picotron's drive)
Added: picotron -home foo // to specify a home folder where config.txt / default drive is stored
Changed: show_in_workspace taken defaults to true when creating a window
Fixed: filenav is slow with many / large files // improved fstat(), fetch_metadata()
Fixed: segfault when too many ord() results
Fixed: pod("abc\0def", 0x0) only encodes "abc"
Fixed: reading a cartridge as a file (fetch"foo.p64") immediately after its content changes returns the older version
Fixed: screenshots do not observe scaneline palette
Fixed: time() is wrong when battery saver is active
Fixed: removed EXIT from pause menu when running in BBS player
Fixed: stale audio ram mixed after poking to unmapped pages (is supposed to mark as dirty and send to pfx6416)
Fixed: slide effects use same keys as SFX navigation (-, +) --> should block navigation when cursor is in fx channel!
Fixed: sound is not paused when pause menu is active
Fixed: arpeggios a-00, b-00 producing high-pitched when group of 4 contains empty rows
Fixed: stereo mixing broken under web
0.1.0h
Added: PFX6416 effects: tremelo, vibrato, wibble, slide, fade, arps, retrigger, cut, delay, pan
Added: PFX6416 stereo mixing, + wide instruments (allows nodes to have separate panning position)
Added: tracker interface: thumbnails, channel output scopes, cursor/playback following, re-order instrument nodes
Added: tracker can now handle up to 64 instruments, 384 SFXs and 128 patterns
Added: text editor shortcuts: ctrl+e (end), ctrl+w (staWt), ctrl+up (same as ctrl-home), ctrl+down
Added: text editor operations: ctrl+b (block comment) ctrl+d (duplicate) shift+enter (add "end" and indent), ctrl+l
Added: monospace toggle button in code editor's app menu
Added: file modification events // on_event("modified:/foo.txt", function(msg) end)
Added: select/copy/paste multiple items (shift+drag/click): sprites, instruments, SFXs, patterns
Added: gfx editor: batch resize and flag modification, shift to snap shape tools, 1,2 to switch colours
Added: run command // similar to ctrl-r, but can pass commandline arguments
Added: "Fullscreen:Window" in settings to be multi-monitor friendly (uses a borderless window)
Added: memory accounting: max 16MB per process (stat(0)) // total Lua allocations + 4k ram pages allocated on write
Added: fget / fset supports reading/writing a single bit // fget(n, b), fset(n, b, val)
Added: unmap(ud, addr) to unmap only one userdata that may still be mapped elsewhere
Added: userdata :peek(addr) :poke(addr) // e.g. can fetch("/system/fonts/p8.font"):poke(0x4000)
Added: map() can take tile_w, tile_h as the last two parameters (integers); defaults to 0x550e, 0x550f
Added: userdata:row(), userdata:column()
Added: ord("abc",1,3) multiple return values
Changed: resonance knob in filter FX tweaked to have a more even distribution of values
Changed: ctrl+left/right in text editor skips whitespace
Changed: memmap(ud, addr) (was memmap(addr, ud) -- that legacy form is still supported)
Changed: maximum values that can be poked / peeked in a single call: 65536
Changed: map() does not draw (or charge for) spr 0 by default // for old behaviour: poke(0x5f36, 0x8)
Changed: foldback uses a more usual triangle function instead of cosine (sounds similar, but less harmonic junk)
Changed: many cpu accounting adjustments to get closer to real-world cost
Optimised: transparent window blits (filenav.p64 on desktop), and pal() calls use less host cpu
Fixed: crash when blit()ing to a region outside the target, but partially inside source userdata (0.1.0g fix was incomplete)
Fixed: thread contention slowing down tracker while playing music in pattern mode on low end machines
Fixed: text editor: doesn't handle trailing newline in selection
Fixed: text editor: cursor x position is lost when moving across short lines
Fixed: text editor: shift-tab on selection does nothing when line starts with spaces instead of tabs
Fixed: can't use file wrangler from /ram/cart; e.g. file open -> filenav launches terminal instead of /ram/cart
Fixed: when saving a cart with ctrl+S, files deleted in /ram/cart are not also removed from the rewritten .p64
Fixed: window{pauseable=false} doesn't disable pausing on Enter for fullscreen apps
Fixed: changing draw target or display size does not release the old userdata for garbage collection
Fixed: palt() ignores single integer parameter (is means to act as a 64-bit bitfield)
Fixed: tline3d crash on tiles that use flip bits // now supports tile flipping
Fixed: map editor slow when zoomed out and/or there are more than a few layers
Fixed: can not assign integer userdata element to a real number using flat indexing // get_draw_target()[0] = 9.1
Fixed: matmul cpu cost is wrong for large matrices (was charging for only w*h multiplies instead of w*h*h)
Fixed: scalar-userdata operations with scalar on LHS treated as if RHS ((2/vec(1))[0] == 0.5 instead of 2.0
Fixed: userdata:tranpose() broken for non-square matrices
Fixed: f64 userdata loses its data type on copy (as reported by :attribs())
Fixed: resetting a cartridge from the pause menu sometimes kills it
Fixed: doubletap / doubleclick message firing even when two clicks are far apart
Fixed: global mute setting not observed
Fixed: (web) multiple webaudio mixer on reset / open multiple carts in one bbs thread (causes speedup / glitches)
0.1.0g
Added: pause menu for fullscreen programs (ENTER -> continue, toggle sound, reset cartridge, exit)
Added: file extension associations. To specify a default app: default_app vgfx /apps/tools/veditor.p64
Added: shortcuts: ctrl-home, ctrl-end in code/text editor; ctrl-a (home) and ctrl-d (delete) in terminal
Changed: .sfx format and resource loader now stores 256k by default (space for 398 SFX; is backwards compatible)
Changed: Open File and New File (via app menu) always open in the program that requested it (ref: VisiTrack, VGFX)
Changed: Can not rename a file over an existing filename in filenav
Fixed: sometimes boot into an invalid workspace and need to switch back and forth to mend
Fixed: web player: audio, key(), touch controls (but only P8 buttons for now)
Fixed: the first file change logged by anywhen each day is stored as the newly written version instead of the old version
Fixed: stray globals in terminal: k, res, cproj_draw, cproj_update
Fixed: node output missing in instrument designer
Fixed: crash when blit()ing from outside of source bitmap // was happening when using magnifying glass at bottom right
Fixed: magnifying glass drawing black as transparent
Fixed: btn(), btnp() when no _update() callback exists only works via ctrl-r and not when run directly from desktop / terminal
Fixed: several types of crackles / discontinuities in audio mixer, mostly relating to echo nodes
Fixed: SFXs 64 and above are not initialised to "empty" (instead, zeroed data that shows up as C0's)
Fixed: some ctrl- combinations produce a textinput event. e.g. ctrl-1 // explicitly blocked in events.lua
Fixed: (Mac) icon is slightly too big
Fixed: (Mac) Option key not mapped to "alt"
Fixed: cursor in terminal doesn't wrap with command string
Fixed: Locked mouse speed is different along X & Y (partial fix)
Fixed: Moving a folder inside itself causes folder to be deleted
Fixed: crash if call clip() in _init (before a display is created)
0.1.0f
Added: reboot
Added: logged disk writes for backups / versioning ("anywhen" in settings)
Added: load cart.p64@2024-04-06_14:00:00 to load a cart from the past (or just @14:00 for short if same day -- local time)
Added: date() can take a time to convert (string or epoch time) and a delta: date(nil, "2024-02-01_15:00:00", delta_secs)
Added: stat(87) for timezone delta in seconds (add to local time to get UTC)
Added: drag and drop host files into picotron (copied to /ram/drop -- not mounted) -> generates a "drop_items" message
Added: drop a png into gfx editor to load it (colour fits to current display palette)
Added: filenav: open files or folder on host via app menu ("View in Host OS")
Added: can fetch .p64 host files directly as binary strings // same semantics as fetching "https://...foo.p64"
Added: PICO-8 style string indexing; ?("abcde")[4] --> "d" ?("abcde")[02] --> nil
Added: btnp() repeat rates (follows PICO-8: initial repeat delay: @5f5c, subsequent delays at @5f5d specified at 30fps)
Added: >>, << operators for integer userdata
Added: Row markers in tracker pattern view +
Added: mouselock(true, event_sensitivity, movement_sensitivity) -- mouselock(false) to disable; dx,dy = mouselock()
Added: tline3d dev flags (last param): 0x100 skip last pixel; 0x200 apply sub-pixel adjustment // see: /bbs/?tid=141647
Added: music() fade in and out
Changed: increased userdata ops per cycle (2x for mul,div,mod; 4x for others)
Changed: ls() sorts results with (non-cart) folders first by default
Changed: renamed create_diff / apply_diff -> create_delta / apply_delta
Changed: timestamps shown by about.p64 and default tooltray clock show local times
Changed: when pixel_perfect is off, blitter prescales to 960x540 (still quite blurry though)
Changed: screenshots stored to /desktop/host -- a folder automatically mounted on host at {Desktop}/picotron_desktop
Changed: cp, mv take -f flags (required if copying over an existing folder / cartridge)
Fixed: trailing slash for folders on tab complete in filenav, terminal
Fixed: delete key + AltGr not working in terminal
Fixed: tracker note keys using mapped key names -- should be raw scancode layout
Fixed: tracker knobs hard to use when close to edge of the screen (now using mouselock and finer sensitivity)
Fixed: using keyboard controls while a game controller is plugged in causes ghost button presses
Fixed: ceil(1.0) returns 2 ._.
Fixed: crash on multiplying userdata by scalar on LHS // ?(tostring (1 * vec(1, 1, 1)))
Fixed: redundant cartridge file flushing (writes that happen within 300ms are now batched)
Fixed: search for an empty spot on desktop to create dropped files
Fixed: key() / keyp() returns false when received keyup and keydown messages in same frame (should be true for 1 frame)
Fixed: keyboard btn() responds to keypress one frame late
Fixed: memmap(0x8000) crashes // update: can't unmap pages like this, can only unmap by userdata: unmap(ud)
Fixed: high dpi display modes blurry [not sure if will be fixed in 0.1.0f]
Fixed: fetching https:// forces url to be lowercase
Fixed: LFO phase knob ignored
0.1.0e
Added: sfx tracker: undo / selections / copy + paste single instruments / track data / patterns
Added: gfx bank selection in map editor
Added: pixel_perfect x stretch: each axis separately uses largest integer multiple that fits
Added: hold down ctrl x 2 to boot into terminal (useful for recovering from borked configurations)
Added: create new tab flow: guess file extension from other files in the target folder when none is given
Added: home/end/ctrl+e in terminal to control cursor position
Changed: palt(1) sets colour 0 as transparent, not 63 (departure from P8 style)
Fixed: double listings of .p64 files in /
Fixed: host folders called foo.p64 collapse back into .p64 files on save (but still happens on mv / cp)
Fixed: flip bits not observed when drawing with map()
Fixed: map editor draws an out of bounds row and column at bottom and right
Fixed: workspace icons lose transparency after using magnifying glass
Fixed: \n\r pasted from windows creates overlapping lines in code editor (now filtered out)
Fixed: host keypresses getting through to Picotron (alt+tab, ctrl+alt+left/right)
Fixed: filenav crashes when actioning intention with a filename that can not be resolved
Fixed: can not use AltGR during text entry
Fixed: default key mappings: command keys & delete were missing in 0.1.0d
Fixed: bad keycodes.pod / scancodes.pod format causes wm crash on boot
0.1.0d
Added: default keyboard mapping for key()/keyp() uses host OS layout by default
Added: can map multiple physical keys to a single virtual key
Added: sfx len (becomes loop0 when loop1 > len)
Added: warning on startup when the /system version does not match the build version
Changed: about.p64 now shows/edits the metadata of /ram/cart by default (i.e. just type: about)
Changed: rename triplane.p64 to biplane.p64 (need to re-select it again from wallpapers)
Fixed: /system rom in 0.1.0c was the wrong version! (caused map drawing and other things to break)
Fixed: (Windows) rm does not delete host folders
Fixed: (Mac) crashes after ~13.5 minutes
Fixed: host system user data paths are clipped at non-ascii characters
0.1.0c
Added: custom map tile sizes (taken from sprite 0)
Added: layer naming and ordering (the first layer in the list is now drawn on top)
Added: mget(), mset(), ceil()
Added: async remote fetch (put it in a coroutine)
Added: /system/util: shutdown pwd info
Added: right click on desktop to create new file / get file info
Added: /appdata/system/keycodes.pod to map virtual key (to a raw name or directly to scancode)
// store("/appdata/system/keycodes.pod", {a="q",z="w",q="a",w="z",m=51})
Added: future version checking; a separate runtime version number is stored with each cart.
Added: delete file menu item in filenav (moves a single file to /ram/compost)
Added: send_message(pid, {msg=..., _delay = 2}) to send a delayed message (_delay is in seconds)
Changed: filenames can contain hyphens
Changed: terminal searches for commands in current path /after/ standard paths (/system/util, ..)
Changed: added more undo checkpoints to the text editor
Changed: gui elements must explicitly :set_keyboard_focus(true) on click to consume textinput events
Changed: screenshots and untitled cart filenames are given an integer suffix to reduce collisions
Changed: when saving a file with no extension, wrangler automatically adds the default filename extension
Changed: track (sfx) length can be specified pico-8 style by increasing loop0 (memory layout doesn't change)
Fixed: load #bbs_id twice over the same local file -> fails to unmount the first cartridge
Fixed: audio lock causing random crashes on Mac (tentative ~ not sure if that was the cause or the only cause)
Fixed: cp allows copying to inside self (-> crash; e.g. when save cart to /ram/cart)
Fixed: reset() does not reset scanline palette selection bits at 0x5400
Fixed: (Mac) vertical red line junk on letterboxed area in fullscreen mode
Fixed: (Windows) printh doesn't send anything to terminal
Fixed: drop file into a folder exactly when it opens --> hard freeze (wm crashes)
Fixed: when dragging files and move mouse quickly, offset from mouse doesn't match original position
Fixed: flr("garbage") causes runtime error (should return 0 to match PICO-8 behaviour)
Fixed: text editor operations (undo, indent, double click select) stop working after using search
Fixed: width/height fields dump earlier keypress junk + no way to delete characters
Fixed: msg.has_pointer not always set when it should be (--> cursor not changing on window title)
Fixed: msg.mx, msg.my absolute values for draw callbacks; should be relative to gui element
Fixed: no printh output under Windows (switched to using SDL_Log)
Fixed: ctrl+6 screenshot while in video mode 3 or 4 is not scaled to cover the 480x270 output
Fixed: flashing when windowed cartridge runs at < 60fps with a custom display palette (e.g. inst editor)
Fixed: flashing when video mode 3 or 4 running at < 60fps
Fixed: filenav selects .loc files (drive.loc) as target to save over instead of opening it like a folder
Fixed: corrupted /desktop/drive.loc due to aforementioned bug -- now automatically mended on startup
Fixed: run bells.p64 and then enter tracker -> audio is mixed from left over junk state
Fixed: note entry sometimes does not play in pattern editing mode
Fixed: can edit track that is not currently visible
Fixed: ASDR release is calculated incorrectly (is way too long) when played in track view
Fixed: clipping: tline3d (w is wrong), spr() when flipped
0.1.0b
Added: system event logging in log.txt (same folder as picotron_config.txt)
Added: /appdata/system/scancodes.pod to remap physical key scancode
// e.g. store("/appdata/system/scancodes.pod", {lctrl=57})
Changed: apple option / windows menu keys are mapped to "ctrl"
Fixed: Default mapping of lctrl is wrong
Fixed: Windows file saving generating corrupt data (opened in text instead of binary mode)
Fixed: Crash when reading corrupted lz4 pods -- now returns a nil object
// (& deals with existing corrupt settings.pod)
Fixed: Windows BSOD on boot
Fixed: Button mappings wrong for controller index 1 and above
0.1.0 First release of binaries
Comments