Coder Social home page Coder Social logo

picotool's Introduction

picotool: Tools and Python libraries for manipulating PICO-8 game files

PICO-8 is a fantasy game console by Lexaloffle Games. The PICO-8 runtime environment runs cartridges (or carts): game files containing code, graphics, sound, and music data. The console includes a built-in editor for writing games. Game cartridge files can be played in a browser, and can be posted to the Lexaloffle bulletin board or exported to any website.

picotool is a suite of tools and libraries for building and manipulating PICO-8 game cartridge files. The suite is implemented in, and requires, Python 3. The tools can examine and transform cartridges in various ways, and you can implement your own tools to access and modify cartridge data with the Python libraries.

Useful tools include:

  • p8tool build: assembles cartridges from multiple sources, as part of a game development workflow
  • p8tool stats: reports statistics on one or many cartridge files
  • p8tool listlua: prints the Lua code of a cartridge
  • p8tool luafind: searches the Lua code of a collection of cartridges
  • p8tool luafmt: formats the Lua code of a cartridge to make it easier to read

There are additional tools that are mostly useful for demonstrating and troubleshooting the library: writep8, listtokens, printast, luamin. A separate demo, p8upsidedown, uses picotool to transform the code and data of a game to turn it upsidedown.

picotool supports reading and writing both of PICO-8's cartridge file formats: the text-based format .p8, and the PNG-based binary format .p8.png.

How do most people use picotool?

Most people use picotool for the luamin command, which condenses the Lua code of a cart to use as few characters as possible. This is useful if you have a large game whose code is above PICO-8's character limit but below the token limit. In this rare case, luamin helps you keep your developer version easy to read and modify while you polish the game for publication. In most cases, your cart will exceed the token limit first, and minification doesn't help with unusual cases such as long string data.

I originally created picotool for general purpose build workflows, especially the build tool. Since then, PICO-8 has added features such as #include support, making some of the build features unnecessary. (#include doesn't quite work like require() but it's good enough for most things!) I'm still interested in the potential for token optimization features such as dead code elimination, but these are not yet implemented.

I hope picotool can be the basis for your custom workflows and cart manipulation experiments, so you don't have to write your own cart read/write routines. If there's a feature you'd like to see in the libraries or the tools, please let me know!

Installing picotool

To install the picotool tools and libraries:

  1. Install Python 3 version 3.4 or later, if necessary. (picotool has not been tested with Python 2.)

  2. Download and unpack the zip archive, or use Git to clone the Github repository.

    • Unpacking the zip archive creates a root directory named picotool-master.
    • When cloning the repo, this is just picotool, or whatever you named it when you cloned it.
  3. Change to the unpacked archive or clone directory from the above step.

  4. Install the software. The dot (.) tells pip install to use the current working directory, which should be picotool-master (from the .zip file) or picotool (from git clone).

    pip install .

You should now have a p8tool command in your path.

Using picotool

To use a tool, you run the p8tool command with the appropriate arguments. Without arguments, it prints a help message. The first argument is the name of the tool to run (such as stats), followed by the arguments expected by that tool.

For example, to print statistics about a cart named helloworld.p8.png:

p8tool stats helloworld.p8.png

p8tool build

The build tool creates or updates a cartridge file using other files as sources. It is intended as a part of a game development workflow, producing the final output cartridge.

The tool takes the filename of the output cartridge, with additional arguments describing the build. If the output cartridge does not exist, the build starts with an empty cartridge. Otherwise, it uses the existing cartridge as the default, and overwrites sections of it based on the arguments.

For example, you can create a cartridge in PICO-8, use PICO-8's built-in graphics and sound editors, then use p8tool build to replace the Lua code with the contents of a .lua file:

p8tool build mygame.p8.png --lua mygame.lua

As another example, to create a new cartridge using the spritesheet (gfx) from one cartridge file, music (sfx, music) from another, and Lua code from a .lua file:

p8tool build mygame.p8.png --gfx mygamegfx.p8 --sfx radsnds.p8.png --music radsnds.p8.png --lua mygame.lua

You can also erase a section of an existing cart with an argument such as --empty-map.

The available arguments are as follows:

  • --lua LUA: use Lua code from the given cart or .lua file
  • --gfx GFX: use spritesheet from the given cart
  • --gff GFF: use sprite flags from the given cart
  • --map MAP: use map from the given cart
  • --sfx SFX: use sound effects from the given cart
  • --music MUSIC: use music patterns from the given cart
  • --empty-lua: use an empty Lua code section
  • --empty-gfx: use an empty spritesheet
  • --empty-gff: use empty sprite flags
  • --empty-map: use an empty map
  • --empty-sfx: use empty sound effects
  • --empty-music: use empty music patterns

If the output cart filename ends with .p8.png, the result is a cartridge with a label image. If the file already exists, the cartridge label is reused. If the file does not exist, an empty cartridge label is used. To use a non-empty label, you must open the cart in PICO-8, take a screenshot (press F6 while running), set the title and byline in the first two lines of code (as Lua comments), then save the .p8.png file from PICO-8. Future runs of p8tool build will reuse the label.

Packages and the require() function

p8tool build supports a special feature for organizing your Lua code, called packages. When loading Lua code from a file with the --lua mygame.lua argument, your program can call a function named require() to load Lua code from another file. This is similar to the require() function available in some other Lua environments, with some subtle differences due to how picotool does this at build time instead of at run time.

Consider the following simple example. Say you have a function you like to use in several games in a file called mylib.lua:

function handyfunc(x, y)
  return x + y
end

handynumber = 3.14

Your main game code is in a file named mygame.lua. To use the handyfunc() function within mygame.lua, call require() to load it:

require("mylib")

result = handyfunc(2, 3)
print(result)

r = 5
area = handynumber * r * r

All globals defined in the required file are set as globals in your program when require() is called. While this is easy enough to understand, this has the disadvantage of polluting the main program's global namespace.

A more typical way to write a Lua package is to put everything intended to be used by other programs in a table:

HandyPackage = {
  handyfunc = function(x, y)
    return x + y
  end,
  handynumber = 3.14,
}

Then in mygame.lua:

require("mylib")

result = HandyPackage.handyfunc(2, 3)

This is cleaner, but still has the disadvantage that the package must be known by the global name HandyPackage wihtin mygame.lua. To fix this, Lua packages can return a value with the return statement. This becomes the return value for the require() call. Furthermore, Lua packages can declare local variables that are not accessible to the main program. You can use these features to hide explicit names and return the table for the package:

local HandyPackage = {
  handyfunc = function(x, y)
    return x + y
  end,
  handynumber = 3.14,
}

return HandyPackage

The main program uses the return value of require() to access the package:

HandyPackage = require("mylib")

result = HandyPackage.handyfunc(2, 3)

The require() function only evaluates the package's code once. Subsequent calls to require() with the same string name do not reevaluate the code. They just return the package's return value. Packages can safely require other packages, and only the first encountered require() call evaluates the package's code.

Where packages are located

The first argument to require() is a string name. picotool finds the file that goes with the string name using a library lookup path. This is a semicolon-delimited (;) list of filesystem path patterns, where each pattern uses a question mark (?) where the string name would go.

The default lookup path is ?;?.lua. With this path, require("mylib") would check for a file named mylib, then for a file named mylib.lua, each in the same directory as the file containing the require() call. The lookup path can also use absolute filesystem paths (such as /usr/share/pico8/lib/?.lua). You can customize the lookup path either by passing the --lua-path=... argument on the command line, or by setting the PICO8_LUA_PATH environment variable.

For example, with this environment variable set:

PICO8_LUA_PATH=?;?.lua;/home/dan/p8libs/?/?.p8

The require("3dlib") statement would look for these files, in this order, with paths relative to the file containing the require() statement:

3dlib
3dlib.lua
/home/dan/p8libs/3dlib/3dlib.p8

To prevent malicious code from accessing arbitrary files on your hard drive (unlikely but it's nice to prevent it), the require() string cannot refer to files in parent directories with ../. It can refer to child directories, such as require("math/linear").

As with Lua, packages are remembered by the string name used with require(). This means it is possible to have two copies of the same package, each known by a different name, if it can be reached two ways with the lookup path. For example, if the file is named foo.lua and the lookup path is ?;?.lua, require("foo") and require("foo.lua") treat the same file as two different packages.

Packages and game loop functions

When you write a library of routines for PICO-8, you probably want to write test code for those routines. picotool assumes that this test code would be executed in a PICO-8 game loop, such that the library can be in its own test cart. For this purpose, you can write your library file with _init(), _update() or _update60(), and _draw() functions that test the library. By default, require() will strip the game loop functions from the library when including it in your game code so they don't cause conflicts or consume tokens.

For example:

local HandyPackage = {
  handyfunc = function(x, y)
    return x + y
  end,
  handynumber = 3.14,
}

function _update()
  test1 = HandyPackage.handyfunc(2, 3)
end
function _draw()
  cls()
  print('test1 = '..test1)
end

return HandyPackage

If you want to keep the game loop functions present in a package, you can request them with a second argument to require(), like so:

require("mylib", {use_game_loop=true})

How require() actually works

Of course, PICO-8 does not actually load packages from disk when it runs the cartridge. Instead, picotool inserts each package into the cartridge code in a special way that replicates the behavior of the Lua require() feature.

When you run p8tool build with the --lua=... argument, picotool scans the code for calls to the require() function. If it sees any, it loads and parses the file associated with the string name, and does so again if the required file also has require() calls.

Each required library is stored once as a function object in a table inserted at the top of the final cartridge's code. A definition of the require() function is also inserted that finds and evaluates the package code in the table as needed.

To match Lua's behavior, require() maintains a table named package.loaded that maps string names to return values. As with Lua, you can reset this value to nil to force a require() to reevaluate a package.

This feature incurs a small amount of overhead in terms of tokens. Each library uses tokens for its own code, plus a few additional tokens for storing it in the table. The definition for require() is another 40 tokens or so. Naturally, the inserted code also consumes characters.

Formatting or minifying Lua in a built cart

You can tell p8tool build to format or minify the code in the built output using the --lua-format or --lua-minify command line arguments, respectively.

p8tool build mycart.p8.png --lua=mygame.lua --lua-format

This is equivalent to building the cart then running p8tool luafmt or p8tool luamin on the result.

The build command supports the options --keep-names-from-file=<filename> and --keep-all-names. See luamin, below, for more information.

p8tool stats

The stats tool prints statistics about one or more carts. Given one or more cart filenames, it analyzes each cart, then prints information about it.

% p8tool stats helloworld.p8.png
hello world (helloworld.p8.png)
by zep
version: 0  lines: 48  chars: 419  tokens: 134

This command accepts an optional --csv argument. If provided, the command prints the statistics in a CSV format suitable for importing into a spreadsheet. This is useful when tallying statistics about multiple carts for comparative analysis.

p8tool --csv stats mycarts/*.p8* >cartstats.csv

p8tool listlua

The listlua tool extracts the Lua code from a cart, then prints it exactly as it appears in the cart.

% p8tool listlua helloworld.p8.png
-- hello world
-- by zep

t = 0

music(0)

function _update()
 t += 1
end

function _draw()
 cls()

...

p8tool luafmt

The luafmt tool rewrites the Lua region of a cart to make it easier to read, using regular indentation and spacing. This does not change the token count, but it may increase the character count, depending on the initial state of the code.

The command takes one or more cart filenames as arguments. For each cart with a name like xxx.p8.png, it writes a new cart with a name like xxx_fmt.p8.

% p8tool luafmt helloworld.p8.png
% cat helloworld_fmt.p8
pico-8 cartridge // http://www.pico-8.com
version 5
__lua__
-- hello world
-- by zep

t = 0

music(0)

function _update()
  t += 1
end

function _draw()
  cls()

  for i=1,11 do
    for j0=0,7 do
      j = 7-j0
      col = 7+j
...

By default, the indentation width is 2 spaces. You can change the desired indentation width by specifying the --indentwidth=... argument:

% p8tool luafmt --indentwidth=4 helloworld.p8.png
% cat helloworld_fmt.p8
...
function _update()
    t += 1
end

function _draw()
    cls()

    for i=1,11 do
        for j0=0,7 do
            j = 7-j0
            col = 7+j
...

The current version of luafmt is simple and mostly just adjusts indentation. It does not adjust spaces between tokens on a line, align elements to brackets, or wrap long lines.

p8tool luafind

The luafind tool searches for a string or pattern in the code of one or more carts. The pattern can be a simple string or a regular expression that matches a single line of code.

Unlike common tools like grep, luafind can search code in .p8.png carts as well as .p8 carts. This tool is otherwise not particularly smart: it's slow (it runs every file through the parser), and doesn't support fancier grep-like features.

% p8tool luafind 'boards\[.*\]' *.p8*
test_gol.p8.png:11:  boards[1][y] = {}
test_gol.p8.png:12:  boards[2][y] = {}
test_gol.p8.png:14:    boards[1][y][x] = 0
test_gol.p8.png:15:    boards[2][y][x] = 0
test_gol.p8.png:20:boards[1][60][64] = 1
test_gol.p8.png:21:boards[1][60][65] = 1
test_gol.p8.png:22:boards[1][61][63] = 1
test_gol.p8.png:23:boards[1][61][64] = 1
test_gol.p8.png:24:boards[1][62][64] = 1
test_gol.p8.png:30:  return boards[bi][y][x]
test_gol.p8.png:36:      pset(x-1,y-1,boards[board_i][y][x] * alive_color)
test_gol.p8.png:54:          ((boards[board_i][y][x] == 1) and neighbors == 2)) then
test_gol.p8.png:55:        boards[other_i][y][x] = 1
test_gol.p8.png:57:        boards[other_i][y][x] = 0

You can tell luafind to just list the names of files containing the pattern without printing the lines using the --listfiles argument. Here's an example that looks for carts that contain examples of Lua OO programming:

% p8tool luafind --listfiles 'self,' *.p8*
11243.p8.png
12029.p8.png
12997.p8.png
13350.p8.png
13375.p8.png
13739.p8.png
15216.p8.png
...

p8tool writep8

The writep8 tool writes a game's data to a .p8 file. This is mostly useful for converting a .p8.png file to a .p8 file. If the input is a .p8 already, then this just makes a copy of the file. (This can be used to validate that the picotool library can output its input.)

The command takes one or more cart filenames as arguments. For each cart with a name like xxx.p8.png, it writes a new cart with a name like xxx_fmt.p8.

% p8tool writep8 helloworld.p8.png
% cat helloworld_fmt.p8
pico-8 cartridge // http://www.pico-8.com
version 5
__lua__
-- hello world
-- by zep

t = 0

music(0)

function _update()
 t += 1
end

function _draw()
 cls()

...

p8tool luamin

The luamin tool rewrites the Lua region of a cart to use as few characters as possible. It does this by discarding comments and extraneous space characters, and renaming variables and functions. This does not change the token count.

The command takes one or more cart filenames as arguments. For each cart with a name like xxx.p8.png, it writes a new cart with a name like xxx_fmt.p8.

By default, luamin renames variables, attributes, and labels to use as few characters as possible. You can disable this behavior with a command line flag: --keep-all-names

You can disable renaming just for specific names by providing a text file listing names, with this command line option: --keep-names-from-file=<filename> This file supports the following features:

  • Each name appears on its own line.
  • Blank lines are ignored.
  • Lines that begin with a # are ignored, so you can have comments in this file.

Disabling renaming is useful in cases where code refers to object attributes using both dot syntax (foo.bar) and indexing syntax (foo["bar"]). In this case, luamin renames the dot syntax but can't rename the indexing syntax (because "bar" can be any expression). You can add bar to the list of names to keep to prevent this, while still allowing renaming for other symbols (including foo in this example).

luamin always preserves the first two comments that appear before any other code. PICO-8 uses these comments as the title and author metadata for the label when exporting a .p8.png.

Statistically, you will run out of tokens before you run out of characters, and minifying is unlikely to affect the compressed character count. Carts are more useful to the PICO-8 community if the code in a published cart is readable and well-commented. That said, some authors have found luamin useful in the late stages of writing a large cart that includes blob data strings, where you might hit the character limit first and you need to squeeze in a few more characters to finish the game.

% p8tool luamin helloworld.p8.png
% cat helloworld_fmt.p8
pico-8 cartridge // http://www.pico-8.com
version 5
__lua__
a = 0
music(0)
function _update()
a += 1
end
function _draw()
cls()
...

p8tool listtokens

The listtokens tool is similar to listlua, but it identifies which characters picotool recognizes as a single token.

% p8tool listtokens ./picotool-master/tests/testdata/helloworld.p8.png
<-- hello world>
<-- by zep>

<0:t>< ><1:=>< ><2:0>

<3:music><4:(><5:0><6:)>

<7:function>< ><8:_update><9:(><10:)>
< ><11:t>< ><12:+=>< ><13:1>
<14:end>

<15:function>< ><16:_draw><17:(><18:)>
< ><19:cls><20:(><21:)>

...

When picotool parses Lua code, it separates out comments, newlines, and spaces, as well as proper Lua tokens. The Lua tokens appear with an ascending number, illustrating how picotool counts the tokens. Non-token elements appear with similar angle brackets but no number. Newlines are rendered as is, without brackets, to make them easy to read.

Note: picotool does not currently count tokens the same way PICO-8 does. One purpose of listtokens is to help troubleshoot and fix this discrepancy. See "Known issues."

p8tool printast

The printast tool prints a visualization of the abstract syntax tree (AST) determined by the parser. This is a representation of the structure of the Lua code. This is useful for understanding the AST structure when writing a new tool based on the picotool library.

% p8tool printast ./picotool-master/tests/testdata/helloworld.p8.png

Chunk
  * stats: [list:]
    - StatAssignment
      * varlist: VarList
        * vars: [list:]
          - VarName
            * name: TokName<'t', line 3 char 0>
      * explist: ExpList
        * exps: [list:]
          - ExpValue
            * value: 0
    - StatFunctionCall
      * functioncall: FunctionCall
        * exp_prefix: VarName
          * name: TokName<'music', line 5 char 0>
        * args: FunctionArgs
          * explist: ExpList
            * exps: [list:]
              - ExpValue
                * value: 0
    - StatFunction
      * funcname: FunctionName
        * namepath: [list:]
          - TokName<'_update', line 7 char 9>
        * methodname: None
      * funcbody: FunctionBody
        * parlist: None
        * dots: None
        * block: Chunk

...

Building new tools

picotool provides a general purpose library for accessing and manipulating PICO-8 cart data. You can add the picotool directory to your PYTHONPATH environment variable (or append sys.path in code), or just copy the pico8 module to the directory that contains your code.

The easiest way to load a cart from a file is with the from_file() function, in the pico8.game.file module:

#!/usr/bin/env python3

from pico8.game import file

g = file.from_file('mycart.p8.png')
print('Tokens: {}'.format(g.lua.get_token_count()))

Aspects of the game are accessible as attributes of the Game object:

  • lua
  • gfx
  • gff
  • map
  • sfx
  • music

Lua code is treated as bytestrings throughout the API. This is because PICO-8 uses a custom text encoding equivalent to lower ASCII plus arbitrary high characters for the glyph set. Take care to use b'bytestring literals' when creating or comparing values.

API under construction

While the library in its current state is featureful enough for building simple tools, it is not yet ready to promise backwards compatibility in future releases. Feel free to mess with it, but please be patient if I change things.

Important: The parser in particular is kinda jank and I may completely redo the API for the AST before v1.0.

Developing picotool

To start developing picotool, clone the repo, set up a Python virtual environment, and install p8tool in editable mode:

git clone [email protected]:dansanderson/picotool.git
cd picotool
python3 -m venv pyenv
source pyenv/bin/activate

# Install picotool in the virtual environment, with symlinks to source:
pip install -e .

# Install development tools:
pip install --upgrade pip
pip install -r pydevtools.txt

With everything installed and the virtual environment active, you can run some useful commands:

# To run the development version of p8tool:
p8tool

# To open an ipython REPL with picotool on the load path:
ipython
# To enable module auto-reloading:
#   %autoreload 2
# To import a pico8 module:
#   from pico8.lua import lua
# Autocompletion works with the tab key:
#   lua.unicode<TAB>

# To run all tests:
pytest

# To run specific tests:
pytest tests/pico8/lua/lua_tests.py

# To run the tests and calculate a coverage report:
coverage run pytest

# To display the coverage report on the console:
coverage report -m

# To produce an interactive HTML document based on the coverage report, as ./htmlcov/index.html:
coverage html

See pytest documentation, coverage documentation.

Visual Studio Code set-up

I use Visual Studio Code for editing with its powerful Python extension. When you open the picotool root folder in VSCode, it finds the active Python virtual environment and use ./pyenv/bin/python as its Python interpreter. If it doesn't, click "Python" in the left of the bottom bar, then select this path for the interpreter.

When you open a new Terminal within VSCode (press Ctrl + backtick), it automatically activates the Python environment. If it doesn't, or if you prefer a standalone terminal app, remember to activate the environment at the beginning of each session:

source picotool/pyenv/bin/activate

Building Sphinx documentation

The picotool documentation lives in docs/ as a Sphinx project. To build the documentation as web pages:

cd docs
make html

The generated HTML pages are in docs/_build/html/. Open the index.html file in your browser to use them.

You are welcome and encouraged to contribute pull requests to the Sphinx documentation. Once accepted, I'll take care of publishing the changes to the documentation site.

(For my own reference: The documentation site uses the gh-pages branch of the Github repo as its source. There is a crude make publish target that builds the HTML and pushes the built files to this branch.)

Known issues

For a complete list of known issues, see the issues list in the Github project:

[https://github.com/dansanderson/picotool/issues]

picotool is a hobby project and I have to ration the time I spend on issues. I welcome pull requests, and preemptively apologize if I don't review them in a timely manner.

I try to triage issues such that picotool's main features work with all cartridges made with the '''latest knwon version''' of PICO-8. During the PICO-8 beta, picotool has lagged behind on new Lua syntax shortcuts and built-in keywords, though this should settle down as PICO-8 reaches version 1.0. So far, PICO-8 has been largely backwards compatible, but I cannot promise continued support for carts made with old beta versions (0.x) of PICO-8.

picotool and PICO-8 count tokens in slightly different ways, resulting in different counts. More refinement is needed so that picotool matches PICO-8. As far as I can tell, with picotool making some concessions to match PICO-8 in known cases, PICO-8's counts are consistently higher. In most cases, the difference is only by a few tokens, even for large carts. If you find specific cases of token count mismatches, please file them as issues with reproducable examples.

picotool's People

Contributors

benwiley4000 avatar dansanderson avatar rhysu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

picotool's Issues

Label generation; convert between .p8 __label__ and .p8.png actual labels

picotool can read and save "label" data from and to .p8 files. When a .p8 cart with label data is saved as a .p8.png, the label is discarded in favor of either the empty label or a label borrowed from a different .p8.png file.

Ideally, picotool would support full label generation when converting a .p8 to a .p8.png, including using a label image (if any) and generating the title and byline in the Pico-8 font on the label from the code. I'm not sure the title and byline features are worth the effort. A label-to-png routine could also be used for import/export of a PNG file as gfx data, so that might be worth figuring out.

include()/import() "macro"

It would be nice with a include() or import() function which (unlike require()) worked exactly like a simple macro, and spliced the contents of the file argument into the target file.

This would not incur any token overhead, just make it easier to spread development over several files (for easier git blame, cleaner code, etc.).

See also #44.

luamin warning that the character limit is 32k, not 64k?

I'm testing out luamin on a cart I'm working on and getting some output I don't understand:

tvl@rocinante:~/src/picotool $ python3 p8tool stats ~/src/lasertank-pico8/lasertank.p8
lasertank (lasertank.p8)
by tobiasvl
- version: 16
- lines: 978
- chars: 58496
- tokens: 4369
- compressed chars: 21826

tvl@rocinante:~/src/picotool $ python3 p8tool luamin ~/src/lasertank-pico8/lasertank.p8
/home/tvl/src/lasertank-pico8/lasertank.p8 -> /home/tvl/src/lasertank-pico8/lasertank_fmt.p8
warning: character count 52175 exceeds the Pico-8 limit of 32768
tvl@rocinante:~/src/picotool $ python3 p8tool stats ~/src/lasertank-pico8/lasertank_fmt.p8
lasertank_fmt.p8
- version: 8
- lines: 934
- chars: 52175
- tokens: 4369
- compressed chars: 20666

But surely the Pico-8 character limit is 65535, not 32768?

I suspect this has something to do with the fact that my cartridge's compressed size is 142% (compared to 134% in the minified cart), but it's not immediately clear what limit I'm hitting from the output.

(Note that the fact that my cart is much too big is OK and unsurprising, since I'm storing level data as strings in the code "ROM" area, and that's fine, but I'm trying to find out how many levels I can cram in there.)

Support for the Type() function in PICO-8?

Hi,

It seems that luamin does not recognise the Type() function in PICO-8?

I use this function quite a lot and in the minified output, it seems to leave renamed function calls (e.g. ab() ), but there's never a definition of what ab is?

Let me know if you need any further information (or can't replicate!)

Thanks

Code tab management?

Pico-8 0.1.11 code tabs are implemented as comment lines that look like scissors:

code in tab 0
-->8
code in tab 1
-->8
code in tab 2

For any cart processing that cares about code tabs, this is easy to manage with just strings. It's possible for lua.py to model tabs and manage this on behalf of the client. I mention it here in case anyone wants to upvote or comment on this idea, but it's probably a low priority.

Picotool macro preprocessor

While the fancy require() behavior is nice, and useful as sort of fake-Lua, it would also be nice to have a real macro preprocessor at hand, similar to cpp or m4. And while it's possible to invoke off-the-shelf preprocessors separately, it's not an easy out-of-the-box experience. Sometimes you just want a #include, or even a #define, and not have to think about the toolchain.

If cpp or m4 could be wrangled to work well with Lua source files, maybe p8tool build could offer built-in integration as options. If not, maybe it'd make sense to implement a simple proprietary macro preprocessor similar to cpp directly in p8tool build.

Add a "watch" mode for builds

p8tool build does a build from multiple files a single time. A p8tool watch (or maybe p8tool build --watch) could do the same thing, but continue running and do it again any time a source file changes.

Have a remove comments option

Have a flag that will only strip out comments and that's it. Leave everything else the way it is.

Removing comments is an easy way to get some compression headroom. I'm very generous with my comments and hate having to remove many of them as I'll forget everything when I go back to work on things much later in the future.

(Using luamin with my game still results in odd in-game behavior. Can't really isolate it yet but when I do, I'll put in another ticket.)

Support multiline strings: [[ ...\n... ]]

Support for multiline comments is coming soon, but we also need support for multiline strings: x = [[ ...\n... ]]

Only a few carts use it (e.g. 16085, 16569) but it is legit Lua and is supported by Pico-8 in recent versions.

Support if (cond) do

Entirely by accident, Pico-8 supports an illegal variant of if (cond) then\n ... end that looks like if (cond) do\n ... end. This is a result of Pico-8's "short-if" feature accidentally accepting the multiline do block as if it were on a single line. If it's supported, people will use it (intentionally or otherwise), e.g. 16314, 16350, 16992, 16999, 17135, 17238, 17406, 17550.

Pico-8 is unlikely to remove support for this accident, so picotool should support it. Whether it needs to preserve it, I don't know. It's possible authors of old carts used this trick to reduce character count, though hopefully the relatively new emphasis on token count makes this obsolete. To preserve it, we'd need to extend the grammar. It might be easier to convert "do" to "then" and risk pushing carts over the limit, or it might not.

Glyph support

Picotool currently uses the 'ascii' encoding when reading and writing .p8 files. I believe this would fail when code contains glyphs.

The glyph representation in .p8 files has changed as of 0.1.11e to use a UTF-8 encoding, so it's likely that the correct solution is to switch to that encoding, then carefully make sure these sequences are converted to binary correctly. It might be worth accepting only the narrowest range of supported code points and not all UTF-8 values. IIRC previous behavior with high chars in Lua source was undefined in Pico-8, usually accepted silently or possibly converted invisibly.

Also, glyphs can now be used in identifiers, so this likely requires an update to the lexer.

Support ? print

Pico-8 treats ?() as an alias for print(). This is primarily for use at the command prompt, but I've seen it used in a couple of carts. Maybe picotool should support it as a keyword.

luamin should always start short-ifs on new lines

Short-ifs only work if they're the only thing on the line. There are some cases where luamin is collapsing the initial whitespace to just a space on the previous line, e.g.:

if(a) then
 if(b) c=1
end

(IIRC there's already special handling to make sure short-if ends with a newline, so it shouldn't be difficult to make it start with one as well.)

Support `__label__` region in .p8 files

Pico-8 0.10 added a new __label__ region. picotool rejects this when it encounters it. picotool should support __label__ fully, including preserving and using the label when converting between .p8 and .p8.png. As a partial measure, it should ignore the __label__ region instead of rejecting it.

Index out of range when I use require?

This could be user error, but I'm having a hell of a time trying to get requires working.

Trying to build a file (main.lua) with a require is returning this:

Traceback (most recent call last): File "/Applications/picotool/p8tool", line 8, in <module> sys.exit(tool.main(sys.argv[1:])) File "/Applications/picotool/pico8/tool.py", line 592, in main return args.func(args) File "/Applications/picotool/pico8/build/build.py", line 258, in do_build lua_path=getattr(args, 'lua_path', None)) File "/Applications/picotool/pico8/build/build.py", line 155, in _evaluate_require reqd_lua.reparse(writer_cls=lua.LuaASTEchoWriter) File "/Applications/picotool/pico8/lua/lua.py", line 177, in reparse version=self.version) File "/Applications/picotool/pico8/lua/lua.py", line 131, in from_lines result.update_from_lines(lines) File "/Applications/picotool/pico8/lua/lua.py", line 140, in update_from_lines self._lexer.process_lines(lines) File "/Applications/picotool/pico8/lua/lexer.py", line 462, in process_lines for line in lines: File "/Applications/picotool/pico8/lua/lua.py", line 158, in to_lines for line in writer.to_lines(): File "/Applications/picotool/pico8/lua/lua.py", line 811, in to_lines for chunk in self.walk(): File "/Applications/picotool/pico8/lua/lua.py", line 246, in walk for t in self._walk(self._root): File "/Applications/picotool/pico8/lua/lua.py", line 798, in _walk for t in super()._walk(node): File "/Applications/picotool/pico8/lua/lua.py", line 227, in _walk for t in result: File "/Applications/picotool/pico8/lua/lua.py", line 403, in _walk_Chunk yield self._get_semis(node) File "/Applications/picotool/pico8/lua/lua.py", line 390, in _get_semis if self._tokens[self._pos].matches(lexer.TokSymbol(b';')): IndexError: list index out of range

Any help would be appreciated!

luamin option to support preserving property names in dot syntax when also accessed via value indexing

swordfish1_fmt_000

I suspect its something to do with the renaming of table indexes but im not sure!

The error in question is here, but it appears in other sectons if i comment this out.

original:

function draw_puffer(e)
 local sinfo=e.spr_info[e.state] --load state sprites
 local sprites = sinfo.sprites -- all states have one sprite set for puffer
 local flipx = false -- is it flipped horixontally?
 local flipy = false --is it flipped vertically?
 local sprw=1 --how many sprites wide is it?
 local sprh=1 --how many sprites tall is it?
 local xoff=-4 --how much is the draw offset on x
 local yoff=-4 --how much is the draw offset on y
 --if player is to the right, flip
 if player.x > e.x then flipx = true end
 --if puffed, change offest
 if e.state=="puffed" then xoff = -8 yoff = -8 sprw=2 sprh=2 end
 --set the current sprite accoring to animt and dt
 local spri = sprites[flr(e.animt/sinfo.dt)%#sprites+1]
 drawoutline(spri,e.x+xoff,e.y+yoff,sprw,sprh,flipx,flipy,0,true)
end

minified:

function gx(gh)
local ft=gh.eo[gh.cc]
local fy=ft.fy
local fu=false
local fv=false
local fw=1
local fx=1
local ct=-4
local cu=-4
if s.t>gh.t then fu=true end
if gh.cc=="puffed"then ct=-8 cu=-8 fw=2 fx=2 end
local fz=fy[flr(gh.ej/ft.eu)%#fy+1]
gc(fz,gh.t+ct,gh.u+cu,fw,fx,fu,fv,0,true)
end

i'm happy to send you the full code if that helps find the error?

Thanks

.p8.png code compression is inefficient, does not match Pico-8

The current compress_code routine creates a compressed code blob that is compatible with Pico-8's decompressor (and picotool's own decompressor). However it is far less efficient than Pico-8's compressor and the results are very large, often much larger than the uncompressed original. So something is wrong.

Note that Pico-8's info command doesn't reflect this because it loads the cart successfully then runs its own compressor to determine the compressed size.

See the commented-out lines of testCompressCodeHelloExample to test the failure.

Game loop stripping doesn't make sense with module-style require()

The docs currently suggest that a library can take the form of a cart with a game loop that runs tests on the library, and the require() statement ignores the game loop when the library is included. This doesn't make sense with a module-style Lua file, which has a return statement at the end that cannot appear that way in a cart directly.

At the very least, the docs should be cleaned up to not recommend using these features together. It's possible that this feature isn't useful at all and should be removed. It might be a better recommendation to have a test cart that require()'s the library under test in a more traditional way.

Unable to import modules from sister folders

If i have two folders at the root of my directory, I'm unable to import modules from one folder to the other.


└── root/
    └── folderA/
        ├── module1
        └── module2
    └── folderB/
        ├── module3
        └── module4

When I require module3 in module1 by calling require(folderB/module3), I get an error.

I'm able to fix this error by commenting out the "require() relative filenames" check, and instead calling require('../folderB/module3'), but this is explicitly disallowed as documented.

Multiple lua files

Is assembling a cart from multiple lua files supported?

My use case is to separate my reusable code into multiple .lua files, then bring the library code for my different systems into the cart file.

Support new truncated .p8 format

As of Pico-8 0.1.11e, the .p8 file format has changed (version "15") such that tailing rows of each section are truncated if they match the default state. If entire sections match the default state, they are elided from the file. An empty cart (saved after reboot) is the two-line header, the __lua__ section header, and a blank line.

Game.from_p8_file() needs updating to expect entire sections to be missing from the raw data. Each section type's from_lines() class method needs testing and possible updating, though at first glance it looks like it coincidentally does the right thing for truncated sections in most cases.

Nice to have would be to update each section's to_lines() to write this format, as well as game.to_p8_file() (with an updated "version" header).

Support unofficial // line comments

Apparently Pico-8 supports // (double slash) as a line comment indicator equivalent to -- (double dash). This is not valid Lua and is undocumented for Pico-8, but it appears to work, and some old carts use it. Maybe picotool should support it.

Allow config for only renaming vars and stripping comments

I'm not exactly sure what all the "luamin" options does entirely, but I'm really needing it just rename variables and strip out comments.

When I run the tool and it compresses, I get a lot of one-off-ish errors when I try to run, like a problem with a equal sign or something. Opening the cart, it looks like it might be something with the whitespace compression moving things next to each other...when I add whitespace it sometimes works. Sorry I can't provide more detail then that.

But just having the ability to tell p8tool to rename variables and leaving everything else the same would do wonders on saving compression size.

Thanks for your work thus far. the p8tool works great in most cases...just came across one cart I'm making where the output gives pico-8 some grief.

Encoding error on Pico-8 glyphs (or possibly: AttributeError: 'bytes' object has no attribute 'hex')

Wrote this at bbs, but not sure if you are looking there anymore. (http://www.lexaloffle.com/bbs/?pid=34133#p34133)

Dear dddaaannn.

I am trying to use your picotool to minify my game. Currently I have:
65302/65536 char count and 8136/8192 token count.
When I am trying to use luamin on my cart I get:

C:\Python34>python p8tool luamin saveyourself.p8
saveyourself.p8 -> saveyourself_fmt.p8
warning: token count 29022 exceeds the Pico-8 limit of 32768Traceback (most rece
nt call last):
  File "p8tool", line 8, in <module>
    sys.exit(tool.main(sys.argv[1:]))
  File "C:\Python34\pico8\tool.py", line 512, in main
    return args.func(args)
  File "C:\Python34\pico8\tool.py", line 349, in do_luamin
    return process_game_files(args.filename, luamin, args=args)
  File "C:\Python34\pico8\tool.py", line 210, in process_game_files
    procfunc(g, out_fname, args=args)
  File "C:\Python34\pico8\tool.py", line 242, in luamin
    lua_writer_cls=lua.LuaMinifyTokenWriter)
  File "C:\Python34\pico8\game\game.py", line 746, in to_file
    self.to_p8_file(outfh, *args, **kwargs)
  File "C:\Python34\pico8\game\game.py", line 659, in to_p8_file
    for l in self.gfx.to_lines():
  File "C:\Python34\pico8\gfx\gfx.py", line 95, in to_lines
    yield bytes(newdata).hex() + '\n'
AttributeError: 'bytes' object has no attribute 'hex'

I have ton of text packed into loooong strings if that helps.
Please help me, as I can't even export my game to html at this point.

Thanks in advance!

Plugin support for tools

The Python libraries are useful for building new tools, but there's currently no good way to extend the behavior of an existing tool without copy-pasting code. There might be opportunities for plugin-like extensions. For example, luafmt/luamin could be given the path to a LuaWriter class that is run in place of the usual writer or in addition as a post-processor.

Feedback welcome on what kinds of plugin APIs would be useful.

Edge case for luamin and short-ifs

luamin collapses space between non-word characters and keywords. This is mostly correct except it breaks edge cases involving Pico-8's short-if. This feature is invisibly implemented by Pico-8 as a preprocessor replacement, and does not match the "if" if it has a non-word non-space character to its left.

x=true
y=(0)
if (x) y=1
print(y)

minifies to

x=true y=(0)if(x) y=1
print(y)

which would be correct if the "if" were expanded to its long form, but Pico-8 fails to expand it.

This works:

x=true y=(0)if(x) then y=1 end
print(y)

The workaround is to either avoid short-if, or to make sure that the previous line to a short-if ends in a word char.

luamin should either recognize short-if and preserve the leading space or just always preserve the leading space of the "if" keyword.

Support/preserve escaped char strings (such as "lowercase" letters)

Hi,

I am using the below helper function to display text using the secret(?) "lowercase" font in PICO-8.

However, it appears that luamin output converts the escaped sequence of characters to their UPPERCASE equivalents (and some other changes as well)?

Orig Code:

function smallcaps(s)
	local d=""
	local l,c,t=false,false
	for i=1,#s do
		local a=sub(s,i,i)
		if a=="^" then
			if(c) then d=d..a end
				c=not c
			elseif a=="~" then
				if(t) then d=d..a end
				t,l=not t,not l
			else 
				if c==l and a>="a" and a<="z" then
				for j=1,26 do
					if a==sub("abcdefghijklmnopqrstuvwxyz",j,j) then
						a=sub("\65\66\67\68\69\70\71\72\73\74\75\76\77\78\79\80\81\82\83\84\85\86\87\88\89\90\91\92",j,j)
					break
					end
				end
			end
			d=d..a
			c,t=false,false
		end
	end
	return d
end

Minified Code:

function ia(hh) local e=""local id,kf,kg=false,false for jo=1,#hh do local kh=sub(hh,jo,jo) if kh=="^"then
if(kf) then e=e..kh end
kf=not kf elseif kh=="~"then if(kg) then e=e..kh end
kg,id=not kg,not id else if kf==id and kh>="a"and kh<="z"then
for ki=1,26 do if kh==sub("abcdefghijklmnopqrstuvwxyz",ki,ki) then
kh=sub("ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\",ki,ki) break end end end e=e..kh kf,kg=false,false end end return e end

You can see the output has converted one of the code strings to:
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\

It's not a major issue, as I can just keep copy+pasting the orig string back over the minified output. But it is a bit of a pain 😉

Thanks

Update sfx for custom instruments

Pico-8 0.1.11 added custom instruments, the ability to use sfx 0-7 as waveforms in other sfx (practically speaking for music patterns). This is implemented in .p8 using the high bit of the waveform nibble (8-F == instruments 0-7) and in memory and .p8.png using the unused high bit of the second byte of a note.

sfx.py's comment needs updating, and this unused bit needs to be parsed, written, and given an accessor. Thankfully, this is backwards compatible with older carts, which always sets this bit to 0, so I don't think we need a format version number check.

Support shell expansion of --lua-path

Currently, --lua-path entries must be either a path pattern with an absolute path prefix (/usr/local/share/pico8/?.lua) or a path pattern forward-relative to the file calling require() (?/?.lua). This is sufficient for users that always put their libs in a specific place or projects where all source files are in the same dir. But this puts a burden on library authors to educate users on setting the --lua-path, and doesn't make it easy to provide a portable command invocation in library documentation.

My own intuition would have been to document an invocation like this:

p8tool build --lua-path "?;?.lua;./?.lua" --lua demos/mydemo.lua mydemo.p8

where mydemo.lua might contain require('somelib/foo') and somelib/foo.lua is in the library's top-level alongside demos/. The ./?.lua does not work as expected. If I replace it with $PWD/?.lua it works as intended, but this is more shell-specific and less portable.

I think the right solution is to shell-expand Lua path entries only when given with --lua-path. /x/y/z/? and a/b/? behave as before (absolute FS path and require-relative, respectively). ./a/b/? would expand to an absolute FS path relative to the current working directory (equivalent to $PWD/a/b/? as expanded by the shell itself). I don't see a reason not to support ../a/b/? or ~/a/b/? (user home dir expansion) in this way when provided directly on the command line.

It's worth experimenting with the official Lua interpreter to see what it supports, and try to match.

Binary numbers don't work

print(0b0011)
A .p8 file with the above code in returns the following error
binarytest.p8: Expected b')' at line 1 char 7 binarytest.p8: could not load cart

It seems to be to do with p8s new binary number representation.

Match Pico-8's nested multiline bracket behavior

Lua supports multiline comments that start with --[[ and end with the closest ]]. Lua does not allow nested pairs of brackets, even if the inner start pair does not look like a multiline comment but instead looks like a multiline string:

--[[
 foo [[bar]] baz
]]

is a syntax error in Lua, and in picotool. Pico-8 allows this. Apparently it allows nested pairs: breaking the inner opening brackets in this example result in both (correct) broken syntax coloring and a syntax error when running the cart.

I found a cart (36936) that begins with a large multiline comment documenting how to use an API, and uses multiline string brackets in code samples in this documentation. Pico-8 allows this but this breaks in picotool. I'm tentatively inclined to leave this as is (it's rare), but I'm documenting the issue here in case it becomes a larger problem later.

"Alias"-ing of built-in PICO-8 functions? [Suggestion]

Hiya,

Just a suggestion: rather than excluding built-in PICO-8 functions from being "minified", could luamin instead create a shorter "alias" to them? Such as...
pr=printh pr("hello world")
This is a technique I normally use from time to time to reduce the overall character count.

Now, I guess this would only be beneficial if a particular function is called more than a couple of times (to warrant the overhead of the alias), but just thought that, if possible, this might be a way to further reduce the final output's footprint.

At the very least, it would be nice if luamin wouldn't "break" the code when I setup a function alias (which seems to happen currently).

Anyways, just a thought. 😉

Parse binary number literals

0b010101 binary literals are a recent addition and need picotool support. Don't forget fractional support: 0b010101.10101

Appreciation

I absolutely love the new require() support in picotool, having tried to write an implementation myself (https://github.com/nucleartide/PACK-8). I'll be using picotool's require going forward, and contributing back (issues, examples, additions) wherever possible.

Thanks for creating such an awesome toolset!

non 0 status on error

p8tool should exit with non-zero status when error occurs while processing a cart.

Example:
10521 p8
p8tool stats returns

/home/teddy/.lexaloffle/pico-8/bbs/1/10521.p8.png: Expected end at line 474 char 31
/home/teddy/.lexaloffle/pico-8/bbs/1/10521.p8.png: could not load cart

on this cart but exit status is 0
This makes using p8tool in shell scripts quiet hard.

Support foo"bar" and foo{bar} function calling syntaxes

Lua supports a special syntax for one-argument function calls whose values are strings or tables where you can omit the parentheses. Pico-8 counts this syntax as one token less than the equivalent with parens.

print"hi"    -- 2 tokens
print("hi")  -- 3 tokens

require() argument to generate global aliases for "imported" symbols

The Lua module idiom recommended with require() has several kinds of token overhead that impact Pico-8 developers: a one-time cost of +49, a per-library cost of +9, and possibly +1 per exported symbol to arrange a library in table form. Another potential costly area of overhead is dereferencing the exported symbols from the table, with a cost of about +2 per reference.

This last one could be mitigated by aliasing the exported symbol into a global, effectively "importing" the symbol into the global namespace with a cost of +4 tokens per import:

MyLib = require('mylib.lua')
someMethod = MyLib.someMethod

someMethod()
someMethod()
someMethod()

Today, devs can set up these aliases automatically. I'm wondering if it might be handy for this to be a build-time-only argument to require() to generate these, a sort of "import" statement. The following would produce the same code as above:

MyLib = require('mylib.lua', import={'someMethod'})

someMethod()
someMethod()
someMethod()

This is only a modest convenience if the imports have to be listed. A "import all" feature would be useful, but unfortunately it would be impractical to thoroughly determine the list of symbols to alias with just static code analysis. (Maybe we could use a Lua-based helper to evaluate the module code and iterate over the table it returns, though that would require installing Lua. Or implementing a Lua interpreter. :) )

Build is producing gibberish for my cart, not sure why.

Hiya,

I have an otherwise-perfectly-fine .p8 file which is over the compressed size limit, so I'm trying to minify it. I tried this:

./picotool/p8tool build dragondell.p8.png --lua dragondell.p8 --gfx dragondell.p8 --lua-minify

and it produced no error, but when I run stats on the resulting .p8.png, I get:

dragondell.p8.png: Syntax error (remaining:b'\x15\x1a\x1c! \x1f\x01\x18\x1b\x0f \x18\x02\x17\x11%;\x18\x11\x12 9<:\x1e\x15\x13\x14<[!\x1c=C\x10\x1b#\x1a=M\x1b>D$\x023\x02\x039\x02\x049\x02\x059\x02\x069\x02\x079\x02\x08A\xaf\x0f\x1b\x18\x1b\x1e<\x16\x10\x11Bd=2;\x0e\x18 \x0f\x179<]\x10\x17\x0e\x18!\x11<\x8e\x1e\x11\x10=\x8b\x13\x1e\x11\x11C\x14?W\x1eD1@D\x1b\x18\x15"?v\x13\x1e %A`#\x14\x15 @\x7f@\x9d\x1b\x1e \x1a\x13Bx%\x11\x18\x18\x1b#@\x80B\xa7D\xbfA\xab\x1c\x15\x1aGw\x1c\x11 \x0f\x14L\xf0\x02\x089\x02\t9\x02\n9\x02\x0b9\x02\x0cM\x1bM/N\x10\x04N\x11\x04N\x12\x04N\x13\x04\x08SQ\x1e\x1b% \x18;N5\x1fP\x1a,G\xf4E\xf8\x11\x02-W?\x19 \x1c\x02 \x15\x18Qw<,;\x1e\x1bQ%<;\x0e!\x1f\x14<[ P\x12<\x9b \x1bX\x1d>?# \x18\x18<\x9b;=\x1aX\x1f\x04G\x1e\x05Y\x11\x07H\x10\x06Y\x16\x08Y*\tB\xa7#  \x11\x1e@t<\x1c;\x1fBa<_Q\x17=\x9e\x1a=\xce\x1a#_\x15\x07M\x1b\x06^\x1e\x06N\x1f\x08`\x12\x08\x04H\xa9\x0f\x1b\x15b\x18<{\x1f<l\x14\x11\x1fe\x1dJ<f\x1c>b]EL2\x1c\x18!P)\x08S\x1cS)\x05T\x1a\x07e\x1a\x07f!\x03N\xab_\x10\x1fAg\x10 \x15\x1f@m !\x18\x15N`\x19P\x14\x1e\x1b\x1b\x19j\x14\x06j\x15\x06M%Y\x18\x07L\xbd\x0f \x1f S\x1b\x1aKc=`Sc>PMs=`\x11A&^\x16\x06^\x17\x08o\x1a\x08\x07X\xaa\x1c\x1b\x1e ]\x1aV\x8c=Q\x0e\x1b  EAa\x1b\x08`g[=\x14\x11 \x1eMm\x11$\x0f\x18 \x19  \x15\x1bPg\x1f\x19\x1b\x17C#cOf\x13\x05\x01\x01\x01\x12!\x1a\x0f>$\x02!\x1a\x1cu\x10*{\x19\x12\x1e\x1b\x19`\x13\x1b+\x01\x02\x02\x02<>z\x12<7x\x18\x04=5 \x1b{\x15<\x15y\x19( >7\x15\x12>J1=, \x14\x11\x1a\x02\x1e\x11 !\x1e\x1a\x02\x11\x1a\x10@8<_ .A//9C\xd02\x7f\x1fC">.E\x84\x0f \x1e"\x11;E *$9\x02y\x1d\x19 &\x11EG\x86N\x10\x15oU\x87\xf7\x87\xf7\x87\x17o\x18I"\x8d\x17\x1f\x14!\x12\x12\x18\x11F)I\x15\x10\x11\x1eJK\x86\x18\x15\x88\x1e(A\'\x892\n') at line 1 char 1
dragondell.p8.png: could not load cart

I can load it into Pico-8, and the graphics look fine, but the code section is garbage.

Things I tried:

  • Skipping --lua-minify has the same result (no error, graphics correct, code garbage).
  • Skipping --gfx correctly minifies the code, but of course doesn't include the spritesheet.
    • Doing this and then using build again to add the graphics back in creates an unloadable cart like above.
    • Doing this then using listlua to output just the code and hand-copying that over the code in a copy of the working cart creates a cart which loads and runs, but has new bugs in it. :/

I see in the readme that picotool sometimes "rejects a cart with a syntax error but Pico-8 accepts it," which I suppose could be the case, but I'm not sure how I'd determine that, nor what the numbers in parentheses afterwards refer to (since they don't seem to be either issue numbers or cart numbers).

Any ideas what might be up, or anything else I can try? Apparently Github won't let me attach a .p8 file, but I'd be happy to send it to you another way if that's helpful. (It's a slightly newer and apparently slightly larger version of this one: https://www.lexaloffle.com/bbs/?tid=30999)

Thanks!

Emoji support

Seems PicoTool is not accepting the emoji-like symbols Pico-8 files uses:

C:\Users\ricko\AppData\Roaming\pico-8\carts>python XXX\p8tool luafmt test.p8
test.p8: Syntax error (remaining:b'\xf0\x9f\x85\xbe\xef\xb8\x8f,0)) then\n') at line 636 char 30

Support "?" short-print statements

Pico-8 has another preprocessor macro that treats

? x,y,z

as

print(x,y,z)

As with short-if, this requires that the statement be on its own line. Short-print is mostly intended for use at the command prompt, but some carts use it in cart code because it's one token shorter than print with parens, and fewer chars.

For completeness, maybe Picotool should attempt to support this. I probably wouldn't want to do this unless I remove short-if from the grammar and more properly treat both macros as actual preprocessing steps--taking extra care to flag the AST so it dumps the original short-form statement.

.p8.png compiler?

It would be nice to be able to write Pico-8 programs with my laptop editor, and compile them to .p8.png's

This is great - thanks!

IndexError: list index out of range when parsing required empty .lua file

If the main or a required lua script is empty (ignoring comments), the parsing step will fail with IndexError:

Traceback (most recent call last):
  File "/usr/local/bin/p8tool", line 8, in <module>
    sys.exit(tool.main(sys.argv[1:]))
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/tool.py", line 592, in main
    return args.func(args)
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/build/build.py", line 258, in do_build
    lua_path=getattr(args, 'lua_path', None))
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/build/build.py", line 155, in _evaluate_require
    reqd_lua.reparse(writer_cls=lua.LuaASTEchoWriter)
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 182, in reparse
    version=self.version)
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 133, in from_lines
    result.update_from_lines(lines)
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 145, in update_from_lines
    self._lexer.process_lines(lines)
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lexer.py", line 470, in process_lines
    for line in lines:
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 163, in to_lines
    for line in writer.to_lines():
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 817, in to_lines
    for chunk in self.walk():
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 251, in walk
    for t in self._walk(self._root):
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 804, in _walk
    for t in super()._walk(node):
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 232, in _walk
    for t in result:
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 409, in _walk_Chunk
    yield self._get_semis(node)
  File "/home/hsandt/Developer/PICO-8/picotool-repo/pico8/lua/lua.py", line 396, in _get_semis
    if self._tokens[self._pos].matches(lexer.TokSymbol(b';')):
IndexError: list index out of range

This is due to a do-while pattern (while True-break in Python) in LuaASTEchoWriter._get_semis:

    if self._args.get('ignore_tokens') or not self._tokens:
        return b' '

    spaces_and_semis = []
    while True:
        spaces = self._get_code_for_spaces(node)
        if self._tokens[self._pos].matches(lexer.TokSymbol(b';')):
            self._pos += 1
            spaces_and_semis.append(spaces + b';')
        else:
            spaces_and_semis.append(spaces)
            break

A possible fix is:

    while True:
        # early exit
        if not self._tokens:
            break  # empty string will be returned
        spaces = self._get_code_for_spaces(node)
        # ...

or at the beginning:

    if self._args.get('ignore_tokens') or not self._tokens:
        return b' '  # slightly different, will return a space instead of empty string

etc.

The function is low-level and may be called on chunks that are not files, so I guess using the same behavior as ignore_tokens, i.e. returning a whitespace, is safer.

p8tool build option to take Lua from stdin

More than once now I've recommended build solutions that involve using another tool to produce a .lua file, then p8tool build --lua to add it to a cart. Often, the other tool can actually write the Lua to stdout. It'd be a nice to have an option that would accept this from stdin so these tools can be piped without an intermediate file.

Incorrect minification of goto label

Labels are not properly replaced by the corresponding minified goto name.
Original:
::draw::
cls(0)
goto draw

Minified:
::draw::
cls(0)
goto a

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.