====================================================================================================
    Picotron User Manual
====================================================================================================

Picotron v0.1.0g
https://www.picotron.net
(c) Copyright 2024 Lexaloffle Games LLP
Author: Joseph White // hey@lexaloffle.com

Picotron is built 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

Latest version of this manual (as html, txt):

https://www.lexaloffle.com/picotron.php?page=resources

:: 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 compat features
    CPU:      8M 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, you can open any non-ram folder in the file navgiator with "Open Host OS 
    Folder", 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
    ALT+LEFT/RIGHT   Change Workspace (use option on Mac)
    ESCAPE:          Toggle between desktop / terminal
    ENTER:           Pause menu (fullscreen apps)

----------------------------------------------------------------------------------------------------
 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. On the host operating system cartridges are a single .p64 
    file, but inside picotron they are treated like folders. The current 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.

    Now, instead of using the "bunny" image, the index of the sprite can be used:

    spr(0, 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()

    To add a camera that follow the player around, try this at the start of _draw():

    camera(x - 240, y - 135)

----------------------------------------------------------------------------------------------------
 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 here 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.

    Picotron is still in alpha -- please keep plenty of backups and save often!	If an editor 
    crashes but the window manager is still running, there is a good chance you can still save to 
    disk with CTRL+S.

----------------------------------------------------------------------------------------------------
 Terminal Commands
----------------------------------------------------------------------------------------------------

    Some handy commands:

    ls           list the current directory (folder)
    cd foo       change directory (e.g. cd /desktop)
    mkdir foo    create a folder 
    folder       open the current folder in your Host OS
    open .       open the current folder in filenav
    open fn      open a file with an associated editor
    rm filename  remove a file or folder (be careful!)
    info         information about the current cartridge
    load cart    load a cartridge into /ram/cart
    save cart    save a cartridge 

    To create your own commands, put .p64 or .lua files in /appdata/system/util.

----------------------------------------------------------------------------------------------------
 Using External Editors
----------------------------------------------------------------------------------------------------

    The simplest way to use a text editor is to store the files outside of a cartidge and then 
    include() them:

    cd("/myproj/src")
    include("src/draw.lua")
    include("src/monster.lua")

    Just remember to copy them back inside the cartridge (and comment out the "cd(...)") before 
    releasing it!

    cp /myproj/src /ram/cart/src

    As a general rule, cartridges should be self-contained and not depend on anything except for 
    /system.

----------------------------------------------------------------------------------------------------
 Creating Windowed Programs 
----------------------------------------------------------------------------------------------------

    Each process can have a single window. To set the window:

    window(320,200)

    Additional attributes can be passed in using a table:

    window{
        width  = 320,
        height = 200,
        resizeable = false,
        title = "Bunny Example"
    }

    To disable the pause menu coming up in fullscreen apps when Enter is pressed:

    window{
        pauseable = false
    }

----------------------------------------------------------------------------------------------------
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

    Then save a copy of your cartridge in .p64.png 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, choose the 'Submit' on the BBS webpage to upload the cartridge. Cartridges are not 
    publically visible until a BBS post has been made including the cartridge (unless someone can 
    guess the cartridge id that you give it). Cartridges can be loaded directly from the BBS using:

    > load #bbs_cart_id

    Or a specific version of the cart with the revision suffix:

    > load #bbs_cart_id-0

====================================================================================================
    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. See /system/startup.lua for 
examples of how to launch widgets. Additional widgets and other programs can be launched at startup 
by creating /appdata/system/startup.lua (which is run after /system/startup.lua).

----------------------------------------------------------------------------------------------------
 Custom Commands
----------------------------------------------------------------------------------------------------

    To create your own 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.

----------------------------------------------------------------------------------------------------
 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 for 0.1.0d: you probably don't 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 normally not needed. The raw mapping is used in 
    situations where the physical location of the key mappers, such as the piano-like keyboard 
    layout in the tracker. See /system/lib/events.lua for more details.

    Data persistence is not supported under web yet.

----------------------------------------------------------------------------------------------------
 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

====================================================================================================
    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 activated 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).

    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.

    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).

    Anywhen and Picotron itself are both still in early alpha; please also back up your work  using 
    traditional methods!

====================================================================================================
    Editors
====================================================================================================

----------------------------------------------------------------------------------------------------
Code Editor
----------------------------------------------------------------------------------------------------

    Hold shift to select (or click and drag with mouse)
    CTRL-X, C, V to cut copy or paste selected
    CTRL-Z, Y to undo, redo
    CTRL-F to search for text in the current tab
    CTRL-LEFT, RIGHT to jump by word
    TAB to indent a selection (shift to un-indent)
    Double click to select a word

----------------------------------------------------------------------------------------------------
GFX Editor
----------------------------------------------------------------------------------------------------

The second workspace is a sprite editor. Each .gfx file contains up to 256 sprites, and if the 
filename starts with a number, it can be indexed by the map editor.

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)

    S          shortcut for the select tool (hold down)
    SPACE      shortcut for the pan tool 
    CTRL-A     select all
    ENTER      deselect
    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
    CTRL       modify some draw tools (fill)
    RMB        pick up colour
    F/V        flip selection horizontally or vertically

Draw sprites from your program with spr(index, x, y). 0.gfx is loaded automatically.

----------------------------------------------------------------------------------------------------
Map Editor
----------------------------------------------------------------------------------------------------

Changes made to /ram/cart/gfx/0.gfx automatically show up in the map editor.

The map editor uses similar shortcuts, with a few changes in meaning. For example, F and V also 
flip selected tiles, but also set special bits on those tiles to indicate that the tile itself 
should also be drawn flipped. The map() command also observes those bits.

----------------------------------------------------------------------------------------------------
SFX Editor
----------------------------------------------------------------------------------------------------

[coming soon]

WIP specs: https://www.lexaloffle.com/dl/docs/picotron_synth.html

====================================================================================================
    API Reference
====================================================================================================

----------------------------------------------------------------------------------------------------
    File System
----------------------------------------------------------------------------------------------------


    store(filename, obj, [metadata])

        store a lua object (tables, strings, userdata, booleans and numbers are allowed) as a file.

        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 ot two files, a folder can be created:

        mkdir("/appdata/mygamename")
        store("/appdata/mygamename/highscores.pod", highscore_tbl)

        Either method is fine -- in future versions, cartridges running directly from BBS will be 
        sandboxed, in which case these folders will automatically be mapped to something like 
        /appdata/bbs/cart_id/ (and not be able to clobber data written by other bbs carts).


    fetch(filename)

        Return a lua object stored in a given file. Returns the object and metadata as a table.


    fetch_metadata(filename)

    store_metadata(filename, metadata)

        Fetch and store just the metadata fork of a file. 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(file)

        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.


    fullpath(file)

        Resolve a filename to its canonical path based on the present working directory (pwd()).

----------------------------------------------------------------------------------------------------
    System
----------------------------------------------------------------------------------------------------


    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


    printh(str)

        print a string to the host operating system's console for debugging.


    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 time (UTC)
        ?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)

----------------------------------------------------------------------------------------------------
    Graphics
----------------------------------------------------------------------------------------------------

    // https://www.lexaloffle.com/dl/docs/picotron_gfx_pipeline.html


    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..15).

        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. a custom return value can be specified 
        with:

        poke(0x5f36, 0x10)
        poke(0x5f5b, newval)


    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. a custom value can be specified with:

        poke(0x5f36, 0x10)
        poke(0x5f59, newval)


    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 drawing functions.

        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.


    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.


    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).


    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") or sprite index (0..255 for bank 0, 256..511 for 
        bank 1 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 sprite index or the sprite userdata.

        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.


    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 parameters:

        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,0x1208) -- draw with colour 18 and 8

        To get transparency while drawing shapes, the shape target mask should be set:

        poke(0x550b,0x3f)
        palt()
        --> black pixels won't be drawn

----------------------------------------------------------------------------------------------------
    Table Functions
----------------------------------------------------------------------------------------------------

    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 hash maps or sets, 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

----------------------------------------------------------------------------------------------------
    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
        5 6         Buttons: O X
        7           MENU
        8           reserved
        9 10 11 12  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


    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

        mouselock is not implemented under web yet

----------------------------------------------------------------------------------------------------
    Map
----------------------------------------------------------------------------------------------------


    mget(x, y)

    mset(x, y, val)

        Get or set map value (val) at x,y for the current map (loaded from the first layer of 
        map/0.map by default).

        To set the current map that mget, mset operate on:

        memmap(0x100000, my_map)


    map(tile_x, tile_y, [sx, sy], [tile_w, tile_h], [layers])

        draw section of map (starting from tile_x, tile_y) at screen position sx, sy (pixels).

        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) 

        tile_w and tile_h default to the entire map.

        map() is often used in conjunction with camera(). to draw the map so that a player object 
        (at pl.x in pl.y in pixels) is centered in fullscreen (480x270):

        camera(pl.x - 240, pl.y - 135)
        map()

        layer is a bitfield. When given, only sprites with matching sprite flags are drawn. For 
        example, when layers is 0x5, only sprites with flag 0 and 2 are drawn.


    tline3d(src, 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.

        The width and height of src 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.

----------------------------------------------------------------------------------------------------
    Audio
----------------------------------------------------------------------------------------------------


    sfx(n, [channel], [offset], [length])

        Play sfx (track) n (0..63) on channel (0..15) from note offset (0..63 in notes) for length 
        notes.

        Giving -1 as the channel automatically chooses a channel that is not being used.

        Giving -1 as the track index stops playing sound on that channel.


    music(n, [fade_len], [channel_mask])

        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 specifies which channels to reserve for music only. for example, to play only 
        on channels 0..2:

        music(0, nil, 7) -- 1 | 2 | 4

        Reserved channels can still be used to play sound effects on, but only when that channel 
        index is explicitly requested by @sfx().

----------------------------------------------------------------------------------------------------
    Userdata
----------------------------------------------------------------------------------------------------

    Userdata in Picotron can be thought of as a fixed-size, 1d or 2d array of typed data. Sprites 
    and maps are represented using u8 (unsigned 8-bit) and i16 (signed 16-bit) values respectively, 
    and so can be manipulated with the following functions.


    userdata(data_type, width, height, [data])

    Create a userdata with a data type: "u8", "i16", "i32", "i64", or "f64".

    data is a string of hexvalues encoding the initial values.

    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 = userdata("[gfx]08080400004000444440044ffff094f1ff1944fff9f4044769700047770000a00a00[/gfx]")
    spr(s, 200, 100)


    ud:width()

    ud:height()

    returns the width, height of a userdata


    ud:attribs()

    returns the width, height, type and dimensionality of a userdata.


    ud:get(x, n)

    ud:get(x, y, n)

    Return n values starting at x (or x, y for 2d userdata), or 0 if out of range.


    ud:set(x, val)

    ud:set(x, y, val)

    Set a value at x (or x, y for 2d userdata). When out of range, has no effect.



--------------------------------------------------------------------------------------------
	PICOTRON VERSION HISTORY
--------------------------------------------------------------------------------------------

	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 happan 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


