==================================================================================================== Picotron User Manual ==================================================================================================== Picotron v0.1.1f https://www.picotron.net (c) Copyright 2022~25 Lexaloffle Games LLP Author: Joseph White // hey@lexaloffle.com 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