Giter Site home page Giter Site logo

thisismypassport / shrinko8 Goto Github PK

View Code? Open in Web Editor NEW
75.0 3.0 5.0 1.97 MB

Shrink (minify) Pico-8 carts, as well as other tools (e..g linting, format conversion)

License: MIT License

Python 38.96% Lua 56.42% HTML 1.52% JavaScript 3.11%
pico-8 minify

shrinko8's Introduction

Shrinko8

A set of Pico-8 cart tools, with a focus on shrinking code size.

You can download a recent Windows Executable here.

Otherwise, requires Python 3.7 or above to run.

Reading/Writing PNGs additionally requires the Pillow module (python -m pip install pillow to install)

Download the latest version of the source here.

The major supported features are:

  • Minification - Reduce the token count, character count and compression ratio of your cart.
  • Constants - Replace constant expressions with their value. Also removes 'if' branches with a constant condition.
  • Linting - Check for common code errors such as forgetting to declare a local.
  • Getting Cart Size - Count the amount of tokens, characters, and compressed bytes your cart uses.
  • Format Conversion - Convert between p8 files, pngs, and more. Achieves better code compression than Pico-8 when creating pngs.
  • Unminification - Add spaces and newlines to the code of a minified cart, to make it more readable
  • Custom Python Script - Run a custom python script to preprocess or postprocess your cart

Minification

Greatly reduces the character count of your cart, as well as greatly improves its compression ratio (so that its compressed size is smaller) and can reduce the number of tokens as well.

There are command line options to choose how aggressively to minify, as well as what metric (compressed size or character count) to focus on minifying.

It's recommended to combine minification with conversion to png (as seen in the examples below), as Shrinko8 is able to compress code better and can thus fit carts into pngs that Pico-8 cannot.

To minify your p8 cart:

You have several options, depending on how much minification you need:

The simplest approach, which gives good results and works on any cart:

python shrinko8.py path-to-input.p8 path-to-output.png --minify-safe-only

You can also add --focus-tokens, --focus-chars, or --focus-compressed to the command - depending on what you want Shrinko8 to focus on reducing.

The most aggressive approach, which gives the best results, but sometimes requires you to give additional information to shrinko8 to ensure it minifies your cart correctly:

python shrinko8.py path-to-input.p8 path-to-output.png --minify

If you want to minify, but also to keep your cart easily debuggable and reasonably readable by others, you can do:

python shrinko8.py path-to-input.p8 path-to-output.png --minify-safe-only --no-minify-rename --no-minify-lines

You can also minify to a p8 file (or a lua file), e.g:

python shrinko8.py path-to-input.p8 path-to-output.p8 --minify-safe-only

Debugging the minified cart

If the minified cart errors or misbehaves, here are some tips:

  • Try using --no-minify-lines to preserve linebreaks - the resulting cart will generate much more readable runtime errors, at only a very small character & byte cost.

  • If the errors or misbehaviours don't occur without minification, do try using --minify-safe-only, which always produces a cart that works identically to the original (if not, that's a bug - please report).

Minify options

You can specify what the minification should focus on reducing via additional command-line options:

  • --focus-tokens : Focus on reducing the amount of tokens, even if the compressed size or amount of characters grow. Can be combined with the below.
  • --focus-chars : Focus on reducing the amount of uncompressed characters, even if the compressed size grows.
  • --focus-compressed : Focus on reducing the compressed size of the code, even if the amount of characters grows.
  • By default, the minification is balanced for both metrics.

You can disable parts of the minification process via additional command-line options:

  • --no-minify-rename : Disable all renaming of identifiers
  • --no-minify-consts : Disable replacements of constant expressions with their value (see constants)
  • --no-minify-spaces : Disable removal of spaces (and line breaks)
  • --no-minify-lines : Disable removal of line breaks
  • --no-minify-comments : Disable removal of comments (requires --no-minify-spaces)
  • --no-minify-tokens : Disable removal and alteration of tokens (not including identifier renaming)
  • --no-minify-reoder : Disable reordering of statements

You can control how safe the minification is (see details about unsafe minifications):

  • --minify-safe-only : Do only safe minification. Equivalent to specifying all of the below.
  • --rename-safe-only : Do only safe renaming (equivalent to preserving all table keys, and - if _ENV is used in the cart - all globals)
  • --reorder-safe-only : Do only safe statement reordering.

Additional options:

  • --preserve : Equivalent to specifying --preserve: in the cart itself. Described here.
  • --rename-map <file> : Generate a file telling you how the identifiers were renamed. (This can be useful for debugging)

Operation details

  • All unnecessary comments, spaces and line breaks are removed.
  • Unnecessary tokens like parentheses and trailing commas are removed
  • Identifiers are renamed to be short and to use common characters for better compression
    • Under --focus-chars, identifiers are made as short as possible
  • Tokens are made more consistent, to reduce compression ratio
  • If/while statements may be converted to one-line shorthands, depending on the focus:
    • By default, they're converted to shorthand if deemed to have a positive impact on compression
    • Under --focus-chars, they're always converted to shorthand when possible
    • Under --focus-compressed, they're always converted to either all shorthands or all longhands
  • Multiple successive local declarations are merged into one when safe to do so, saving tokens.
    • under --focus-tokens, the same is done for multiple successive assignments. (Especially effective without --minify-safe-only)

Pitfalls of full minification

When using --minify without --minify-safe-only, Shrinko8 makes - by default - some assumptions about your cart:

  • Renaming assumptions: (--rename-safe-only disables these):

    • Your cart doesn't mix identifiers and strings when indexing tables or _ENV. (E.g. it doesn't access both some_table.x and some_table["x"]).
    • Your cart does not use _ENV (except for some simple cases)
  • Reordering assumption: (Only relevant under --focus-tokens; --reorder-safe-only disables it; complex to describe but hard to break):

    • Your cart does not access freshly-assigned variables or table members inside meta-methods or builtins overridden via _ENV. (See example here)

These assumptions allow Shrinko8 to - for example - freely rename identifiers used to index tables.

If these assumptions don't hold, the minified cart won't work properly, e.g:

local my_obj = {key=123} -- here, key is an identifier.
?my_obj.key -- OK. Here, key is an identifier again.
local my_key = "key" -- here, key is a string.
?my_obj[my_key] -- BUG! my_obj will not have a "key" member after minification!

In such cases, you have multiple ways to tell Shrinko8 precisely how your cart breaks these assumptions, allowing you to achieve better minification than would be possible with just --minify-safe-only:

Renaming specific strings

You can add a --[[member]] comment before a string to have the minifier rename it as if it were an identifier.

E.g:

local my_key = --[[member]]"key" -- here, key is a string but is renamed as if it were an identifier
local my_obj = {key=123} -- here, key is an identifier
?my_obj[my_key] -- success, this prints 123 even after minification

You can also use this with multiple keys split by comma (or any other characters):

local my_keys = split --[[member]]"key1,key2,key3" -- here, each of key1, key2 and key3 is renamed

And you can similarly use --[[global]] for globals:

local my_key = --[[global]]"glob"
glob = 123
?_ENV[my_key] -- 123

Advanced: if you have string literals in some special format that you're parsing into a table (like "key=val,key2=val2"), you can use this custom python script - or a variant thereof - to allow only the keys within the string to be renamed.

Preserving identifiers across the entire cart

You can instruct the minifier to preserve certain identifiers across the entire cart by adding a --preserve: comment anywhere in the code:

--preserve: my_global_1, my_global_2, update_*, *.my_member, my_env.*
  • my_global_1 and my_global_2 will not be renamed when used as globals
  • globals whose names start with update_ will not be renamed
  • my_member will not be renamed when used to index a table
  • table members will not be renamed when accessed through my_env

If you prefer, you can instead pass this information in the command line, e,g:

python shrinko8.py path-to-input.p8 path-to-output.png --minify --preserve "my_global_1, my_global_2, update_*, *.my_member, my_env.*"

You can combine wildcards and negation (!) to preserve everything except some identifiers:

--preserve: *.*, !*.my_*
  • Only identifiers starting with my_ will be renamed when used to index a table

Renaming table keys the same way as globals

You can instruct the minifier to rename table keys the same way as globals (allowing you to freely mix _ENV and other tables), by adding the following comment in the code:

--preserve: *=*.*

If you prefer, you can instead pass --preserve "*=*.*" to the command line.

Controlling renaming of all keys of a table

You can use --[[preserve-keys]], --[[global-keys]] and --[[member-keys]] to affect how all keys of a table are renamed.

This can be applied on either table constructors (aka {...}) or on variables. When applying on variables, the hint affects all members accessed through that variable, as well as any table constructors directly assigned to it.

local --[[preserve-keys]]my_table = {preserved1=1, preserved2=2}
my_table.preserved1 += 1 -- all member accesses through my_table are preserved
?my_table["preserved1"]

-- here, {preserved3=3} is not directly assigned to my_table and so needs its own hint
my_table = setmetatable(--[[preserve-keys]]{preserved3=3}, my_meta)
?my_table["preserved3"]

-- while assigning directly to _ENV no longer requires a hint, indirect assignment like below does:
local env = --[[global-keys]]{assert=assert, add=add}
do
  local _ENV = env
  assert(add({}, 1) == 1)
end

This can be also be useful when assigning regular tables to _ENV:

-- hints on an _ENV local affects all globals in its scope
for --[[member-keys]]_ENV in all({{x=1,y=5}, {x=2,y=6}}) do
  x += y + y*x
end

Advanced - Controlling renaming of specific identifier occurrences

The --[[global]], --[[member]] and --[[preserve]] hints can also be used on a specific occurrence of an identifier to change the way it's renamed.

Usually, there are easier ways to control renaming (such as by preserving identifiers across the entire cart or controlling renaming of all keys in a table), but this option is here for cases where you need precise control over how to rename each occurence.

do
  -- NOTE: can be more easily achieved via --[[global-keys]]
  local _ENV = {--[[global]]assert=assert}
  assert(true)
end
-- NOTE: can be more easily achieved via --[[member-keys]]
for _ENV in all({{x=1}, {x=2}}) do
  --[[member]]x += 1
end

Advanced - Renaming Built-in Pico-8 functions

For cases like tweet-carts, when you use a builtin function multiple times throughout your cart, you often want to assign it to a shorter name at the beginning of the cart. With shrinko8, you can keep using the full name of the builtin, but tell the minifer to only preserve the builtin when it's first accessed, as follows:

--preserve: !circfill, !rectfill
circfill, rectfill = --[[preserve]]circfill, --[[preserve]]rectfill
circfill(10,10,20); circfill(90,90,30)
rectfill(0,0,100,100); rectfill(20,20,40,40)

Above, all uses of circfill and rectfill are renamed except for the ones preceded by --[[preserve]]

Be aware that doing this won't reduce the compressed size of the cart, and will increases the token count (due to the assignment), so it's only for when you care about character count above all else.

Advanced - Explicit renaming

While Shrinko8 has good heuristics for choosing identifier names, it's still possible to improve upon them when hand-minifying carts (useful especially when trying to fit small carts under some chosen limit).

In order to still be able to use Shrinko8 in such cases, a hint is provided to instruct Shrinko8 how to rename specific variables:

function --[[rename::f]]func(--[[rename::a]]arg)
    local --[[rename::b]]val = arg
end

A rename hint affects all instances of the marked variable.

Prevent merging of specific statements

You can insert --[[no-merge]] between two statements to ensure they're not merged, e.g:

-- note: this example requires --focus-tokens to see the effect
local weird_table = setmetatable({add_me=0}, {
    __newindex=function(tbl, key, val) rawset(tbl, key, val + t.add_me) end
})
-- the following statements do not do the same thing if combined into one
-- aka: weird_table.add_me, weird_table.new_key = 3, 4
-- so we can add --[[no-merge]] between them to ensure they're not merged.
weird_table.add_me = 3
--[[no-merge]]
weird_table.new_key = 4

Keeping comments

You can keep specific comments in the output via:

--keep: This is a comment to keep
-- But this comment is gone after minify

Constants

During minification, Shrinko8 will automatically replace most constant expressions with their value:

func(60*60)
-- becomes:
func(3600)

func('the answer is: '..1+3*2)
-- becomes:
func('the answer is: 7')

In addition, variables that are declared with the --[[const]] hint are treated as constants:

--[[const]] k_hero_spr = 4
spr(k_hero_spr, x, y)
-- becomes:
spr(4, x, y)

--[[const]] version = 'v1.2'
?'version: '..version
-- becomes:
?'version: v1.2'

-- the --[[const]] hint can apply to either individual variables or entire local statements
--[[const]] local k_rock,k_box,k_wall = 4,5,6
objs={k_rock,k_wall,k_wall,k_box}
-- becomes:
objs={4,6,6,5}

-- some builtin functions can be used inside const declarations
--[[const]] k_value = 2.5
--[[const]] derived = flr(mid(k_value, 1, 5))
?derived
-- becomes:
?2

Furthermore, constant if and elseif branches are removed appropriately, allowing you to easily keep debug code in your source files, enabling it by simply changing the value of a variable:

--[[const]] TRACE = false
--[[const]] DEBUG = true

if (TRACE) ?"something happened!"
if DEBUG then
  spr(debug_spr, 10, 10)
end

-- becomes:
spr(debug_spr,10,10)

Some details to keep in mind:

  • Local variables that aren't declared as --[[const]] may still be treated as constants in cases where it's safe & advantageous to do so.
  • Local variables that are declared as --[[const]] still follow the usual lua scoping rules. They cannot be reassigned but new locals with the same name can be defined.
  • Global variables that aren't declared as --[[const]] are currently never treated as constants.
  • Global variables that are declared as --[[const]] are assumed to always have their constant value. They cannot be reassigned and can only be used below their declaration.

Passing constants via command line

You can even declare constants in the command line, if you prefer:

python shrinko8.py path-to-input.p8 path-to-output.p8 --minify-safe-only --const DEBUG true --const SPEED 2.5 --str-const VERSION v1.2

--[[CONST]] SPEED = 0.5 -- default value
if DEBUG then
  ?'debug version ' .. (VERSION or '???')
end
hero = 0
function _update()
  hero += SPEED
end

Becomes: (disregarding other minifications)

?"debug version v1.2"
hero = 0
function _update()
  hero += 2.5
end

Limitations

Keep in mind that in some cases, Shrinko8 will play it safe and avoid a computation whose result is questionable or has a high potential to change between pico8 versions. If this prevents a --[[const]] variable from being assigned a constant, Shrinko8 will warn about this:

-- here, abs overflows (due to receiving -0x8000), and shrinko8 chooses not to rely on the overflow behaviour
--[[const]] x = abs(0x7fff+1)-1
?x

-- warning:
--tmp.lua:1:13: Local 'x' is marked as const but its value cannot be determined due to 'abs(0x7fff+1)'

-- Becomes only:
x=abs(32768)-1
?x

If you find such limitations that you'd like to see lifted, feel free to open an issue.

Finally, note that:

  • You can turn off all constant replacement via --no-minify-consts.
  • You can prevent treating specific variables as constants by declaring them with a --[[non-const]] hint. (though normally, there is no reason to do this)

Linting

Linting finds common code issues in your cart, like forgetting to use a 'local' statement

To lint your p8 cart:

python shrinko8.py path-to-input.p8 --lint

You can combine linting with other operations:

python shrinko8.py path-to-input.p8 path-to-output.p8 --lint --count --minify

Linting options

You can disable certain lints globally via additional command-line options:

  • --no-lint-unused : Disable lint on unused variables
  • --no-lint-duplicate : Disable lint on duplicate variable names
  • --no-lint-undefined : Disable lint on undefined variables

Normally, a lint failure prevents cart creation, but --no-lint-fail overrides that.

Normally, lint errors are displayed in a format useful for external editors, showing the line number in the whole .p8 file. However, you can use --error-format tabbed to show the pico8 tab number and line number inside that tab instead.

Misc. options:

  • --lint-global : Equivalent to specifying --lint: inside the cart itself

Undefined variable lints

In Pico-8 (and lua in general), variables that aren't explicitly declared as local (via a local statement) are implicitly global. This can cause all sorts of bugs and headaches if you typo the name of a local or forget to declare a local.

This lint alerts you when you're accessing a variable that wasn't declared as local and isn't a known global variable, e.g:

function f()
    x, y = 10, 20 -- lint warning: you probably meant to use 'local' here instead of assigning to global variables.
    while x < y do stuff(x, y) end
end

Defining global variables

The linter normally allows you to define global variables in the global scope or in the _init function. If you don't, your options are either:

Tell the linter about the globals it didn't see you define via the --lint: hint:

--lint: global_1, global_2
function f()
    dostuff(global_1, global_2)
end

Tell the linter to allow you to define globals (by assigning to them) in a specific function via the --lint: func::_init hint:

--lint: func::_init
function my_init()
    global_1, global_2 = 1, 2 -- these globals can be used anywhere since they're assigned here
end

Re-assigning built-in globals

Similarly, to protect against accidental use of built-in globals like run or t, the linter only allows you to assign to built-in globals in the global scope or in an _init function:

function f()
    t = func() -- lint warning: you probably meant to use 'local' here, even though t is a built-in global
end

If you do want to reassign some built-in global anywhere, you can use --lint:

--lint: print
function f()
    local old_print = print
    print = function() end
    call_something()
    print = old_print
end

Unused variable lints

This lint alerts you when you've declared a local but never used it, which is usually a mistake.

It also tells you when the last parameter of a function is unused, as that's either a mistake or a waste of a token.

To tell the linter that some specific local is OK to be unused, named it beginning with underscore (e.g. _ or _some_name). E.g:

do
  local _, _, x, y = get_stuff() -- lint warning about y (but not about _) - you probably meant to pass it to do_stuff
  do_stuff(x, x)
end

Duplicate variable lints

This lint alerts you when you declare a local with the same name as a local in a parent scope (even across functions).

This can cause confusion and bugs since you can accidentally use the wrong local. E.g:

function f()
  for i=1,10 do
    do_stuff(i)
    for i=1,5 do -- lint warning about i
      do_more(i)
    end
  end
end

The linter allows duplicate variables if they're all named _:

local _, _, x, y, _, z = stuff()

Getting Cart Size

You can enable printing the number of tokens, characters, and compressed bytes used by the code in the cart (including percentages):

python shrinko8.py path-to-input.p8 --count

E.g may print:

tokens: 8053 98%
chars: 30320 46%
compressed: 12176 77%

Note that the compressed size is how this tool would compress the code, which is better than how Pico-8 would.

You can combine counting with other operations, in which case the counts are of the output cart, not the input cart:

python shrinko8.py path-to-input.p8 path-to-output.p8 --lint --count --minify

In such cases, you can also use --input-count to count the number of tokens, characters, and compressed bytes (if applicable) of the input cart.

If you're not interested in the number of tokens or in the compressed size, you can use --no-count-tokenize or --no-count-compress to avoid tokenizing or compressing the cart just to get the count. (You will still see the count if the tokenize/compress had to be done anyway, though)

Format Conversion

Shrinko8 supports multiple cart formats, and allows converting between them:

  • p8 - Pico-8 cart source
  • png - Pico-8 cart exported into a png
  • rom - Pico-8 cart exported into a rom
  • tiny-rom - Pico-8 cart exported into a tiny rom (code only)
  • lua - raw lua code, with no headers
  • clip - Pico-8 clipboard format (i.e. [cart]...[/cart])
  • url - Pico-8 education version url (code & gfx only)
  • js, pod - Exported formats, see section on how to read or write them.
  • label - A 128x128 image of a cart's label (label only)
  • spritesheet - A 128x128 image of a cart's spritesheet (gfx only)
  • auto - try to determine automatically from content

E.g:

python shrinko8.py path-to-input.p8 path-to-output.png
python shrinko8.py path-to-input.png path-to-output.rom
python shrinko8.py path-to-input.rom path-to-output.lua
python shrinko8.py path-to-export/windows/data.pod path-to-output.p8

By default, the format is determined by the file extension, but you can specify it explicitly via:

  • --input-format <format> for the input format.
  • --format <format> for the output format (Where <format> is one of the formats listed above)

You can combine conversion with other operations:

python shrinko8.py path-to-input.p8 path-to-output.rom --count --lint --minify

Specifying the format is also useful when using the standard input/output (via -), e.g.:

python shrinko8.py path-to-input.p8 - --minify --format lua (This prints minified lua to stdout)

You can convert a cart to multiple formats at once using --extra-output path [format]:

python shrinko8.py path-to-input.p8 path-to-output.png --extra-output path-to-output.p8 --extra-output path-to-output.rom

You can additionally export the cart's spritesheet and label:

python shrinko8.py path-to-input.p8 path-to-output.png --extra-output path-to-spritesheet.png spritesheet --extra-output path-to-label.png label

Specifying custom labels & titles

Normally, shrinko8 will take the label and title (if any) from the input cart, same as pico8 does.

However, it is also possible to override the label from a custom 128x128 screenshot via --label <path> and the title via --title "some title"

Merging multiple carts into one

You can tell Shrinko8 to merge specific sections from other carts into the input cart using --merge path sections [format].

The following example takes the label from label-cart.p8 and sfx & music from sounds-cart.p8:

python shrinko8.py path-to-input.p8 path-to-output.png --merge label-cart.p8 label --merge sounds-cart.p8 sfx,music

The following example imports the spritesheet from a 128x128 image at spritesheet.png

python shrinko8.py path-to-input.p8 path-to-output.png --merge spritesheet.png gfx spritesheet

Reading and writing exported formats

Shrinko8 supports reading and writing exported formats. Creating exports through Shrinko8 can be useful in cases when Pico8's compression algorithm isn't able to fit your cart into the export, while Shrinko8's can.

Creating an export requires you to have a copy of Pico8 and provide the pico8.dat file that comes with it as an argument to Shrinko8, as seen below.

Reading exports

Shrinko8 can read the following exports:

  • js - Pico-8 carts exported to html+js - supply the .js file to shrinko8.
  • pod - Pico-8 carts exported as (any) executables - supply the .pod file to shrinko8.

When you pass an export as the input parameter to Shrinko8, it will - by default - read the main cart inside.

If the export contains more than one cart, you can use:

  • --list to list the names of the carts in the export (the first cart listed is the main cart)
  • --dump <folder> to dump all the carts in the export into the given folder
  • --cart <name> to select which cart to read from the export, instead of the main cart

Creating exports

Shrinko8 can create the following exports:

  • bin - A directory containing all exports (both binary and web). Recommended.
  • js - Just the .js file for an html+js export.
  • pod - Just the .pod file for any binary export.

When you pass an export as the output parameter to Shrinko8, it will - by default - try to create a new export containing a single cart.

However, for that to work, you need to also supply --pico8-dat <path to pico8.dat file inside your pico8 directory> to Shrinko8, e.g:

python shrinko8.py path-to-input.p8 path-to-output.bin --pico8-dat c:/pico8/pico8.dat

You can create a multi-cart export by supplying additional input carts:

python shrinko8.py path-to-main-cart.p8 extra-cart-1.p8 extra-cart-2.p8 path-to-output.bin --pico8-dat c:/pico8/pico8.dat

If you need to explicitly specify the type of each additional input cart, you can instead use --extra-input

Also, if both the input and output are exports, all carts from the input get placed in the output, unless --cart is explicitly specified.

Unminification

You can undo some of the effects of minification, or just reformat the cart's code in a consistent manner:

python shrinko8.py path-to-input.p8 path-to-output.p8 --unminify

Of course, renaming cannot be undone, so the resulting code may still not be readable.

Options:

  • --unminify-indent : Specify the size of the indentation to use (default: 2)

Custom Python Script

For advanced usecases, you can create a python script that will be called to preprocess or postprocess the cart before/after the other steps.

This can be used for:

  • Merging in code and data (from other carts, or data files, etc.)
  • Saving minor variations of the cart.
  • Likely much more.

To run, use --script <path>, here shown together with other tools:

python shrinko8.py path-to-input.p8 path-to-output.png --count --lint --minify --script path-to-script.py

You can also pass arguments to your script by putting them after --script-args:

python shrinko8.py path-to-input.p8 path-to-output.png --count --lint --minify --script path-to-script.py --script-args my-script-arg --my-script-opt 123

Example python script showing the API and various capabilities:

# this is called after your cart is read but before any processing is done on it:
def preprocess_main(cart, args, **_):
    print("hello from preprocess_main!")

    # 'cart' contains 'code' and 'rom' attributes that can be used to read or modify it
    # 'cart.code' is a pico8 string where each char is between '\0' and '\xff'
    #             use to/from_p8str in pico_defs.py to convert a pico8 string from/to a unicode string
    #             use decode/encode_p8str in pico_defs.py to convert a pico8 string from/to raw bytes
    # 'cart.rom' is a bytearray with some extra APIs like get16/set32/etc (see Memory in pico_defs.py)

    # copy the spritesheet from another cart
    from pico_cart import read_cart
    other_cart = read_cart("test.p8") # can be used to read p8 or png carts
    cart.rom[0x0000:0x2000] = other_cart.rom[0x0000:0x2000]

    # encode binary data into a string in our cart
    # our cart's code should contain a string like so: "$$DATA$$"
    from pico_utils import bytes_to_string_contents
    with open("binary.dat", "rb") as f:
        cart.code = cart.code.replace("$$DATA$$", bytes_to_string_contents(f.read()))

    # args.script_args contains any arguments sent to this script
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("arg", help="first arg sent to script", nargs="?")
    parser.add_argument("--my-script-opt", type=int, help="option sent to script")
    opts = parser.parse_args(args.script_args)
    print("Received args:", opts.arg, opts.my_script_opt)

# this is called before your cart is written, after it was fully processed
def postprocess_main(cart, **_):
    print("hello from postprocess_main!")

    # dump the code of the cart to be written
    from pico_defs import from_p8str
    with open("out.txt", "w", encoding="utf8") as f:
        f.write(from_p8str(cart.code)) # from_p8str converts the code to unicode

    # write an extra cart based on the current cart, but with a zeroed spritesheet, in both p8 and png formats
    from pico_cart import write_cart, CartFormat
    new_cart = cart.copy()
    new_cart.rom[0x0000:0x2000] = bytearray(0x2000) # zero it out
    write_cart("new_cart.p8", new_cart, CartFormat.p8)
    write_cart("new_cart.p8.png", new_cart, CartFormat.png)

    # write a new cart with the same rom but custom code, in rom format
    from pico_cart import Cart, CartFormat, write_cart
    from pico_defs import to_p8str
    new_cart = Cart(code=to_p8str("-- rom-only cart 🐱"), rom=cart.rom)
    write_cart("new_cart2.rom", new_cart, CartFormat.rom)

Advanced - custom sub-language

For really advanced usecases, if you're embedding a custom language inside the strings of your pico-8 code, you can let Shrinko8 know how to lint & minify it.

E.g. this allows renaming identifiers shared by both the pico-8 code and the custom language.

Mark the language with --[[language::<name>]] in the code:

eval(--[[language::evally]][[
    circfill 50 50 20 7
    my_global_var <- pack
    rawset my_global_var .some_member 123
    rawset my_global_var .another_member 456
]])

In the python script, provide a class that handles the language via sublanguage_main:

(This is a complete example of what sublanguages can do, you can find a simpler example below

from pico_process import SubLanguageBase, is_identifier
from collections import Counter

class MySubLanguage(SubLanguageBase):
    # NOTE: all members are optional.

    # called to parse the sub-language from a string
    # (strings consist of raw pico-8 chars ('\0' to '\xff') - not real unicode)
    def __init__(self, str, on_error, **_):
        # our trivial language consists of space-separated tokens in newline-separated statements
        self.stmts = [stmt.split() for stmt in str.splitlines()]
        # we can report parsing errors:
        #on_error("Example")

    # these are utility functions for our own use:

    def is_global(self, token):
        # is the token a global in our language? e.g. sin / rectfill / g_my_global
        return is_identifier(token)

    def is_member(self, token):
        # is the token a member in our language? e.g. .my_member / .x
        return token.startswith(".") and self.is_global(token[1:])
        
    # for --lint:

    # called to get globals defined within the sub-language's code
    def get_defined_globals(self, **_):
        for stmt in self.stmts:
            if len(stmt) > 1 and stmt[1] == "<-": # our lang's assignment token
                yield stmt[0]

    # called to lint the sub-language's code
    def lint(self, builtins, globals, on_error, **_):
        for stmt in self.stmts:
            for token in stmt:
                if self.is_global(token) and token not in builtins and token not in globals:
                    on_error("Identifier '%s' not found" % token)
        # could do custom lints too

    # for --minify:

    # called to get all characters that won't get removed or renamed by the minifier
    # (aka, all characters other than whitespace and identifiers)
    # this is optional and doesn't affect correctness, but can slightly improve compressed size
    def get_unminified_chars(self, **_):
        for stmt in self.stmts:
            for token in stmt:
                if not self.is_global(token) and not self.is_member(token):
                    yield from token

    # called to get all uses of globals in the language's code
    def get_global_usages(self, **_):
        usages = Counter()
        for stmt in self.stmts:
            for token in stmt:
                if self.is_global(token):
                    usages[token] += 1
        return usages
        
    # called to get all uses of members (table keys) in the language's code
    def get_member_usages(self, **_):
        usages = Counter()
        for stmt in self.stmts:
            for token in stmt:
                if self.is_member(token):
                    usages[token[1:]] += 1
        return usages

    # for very advanced languages only, see test_input/sublang.py for details
    # def get_local_usages(self, **_):

    # called to rename all uses of globals/members/etc
    def rename(self, globals, members, **_):
        for stmt in self.stmts:
            for i, token in enumerate(stmt):
                if self.is_global(token) and token in globals:
                    stmt[i] = globals[token]
                elif self.is_member(token) and token[1:] in members:
                    stmt[i] = members[token[1:]]

    # called (after rename) to return a minified string
    def minify(self, **_):
        return "\n".join(" ".join(stmt) for stmt in self.stmts)

# this is called to get a sub-language class by name
def sublanguage_main(lang, **_):
    if lang == "evally":
        return MySubLanguage

Example - simple sub-language for table parsing

Often it's useful in pico-8 to define a simple sub-language to parse something like this:

"key1=val1,key2=val2,val3,val4"

To:

{key1="val1",key2="val2","val3","val4"}

Here, to minify properly, the keys (key1/key2) should be renamed as members, while the values should be left alone.

The custom python script:

from pico_process import SubLanguageBase
from collections import Counter

class SplitKeysSubLang(SubLanguageBase):
    # parses the string
    def __init__(self, str, **_):
        self.data = [item.split("=") for item in str.split(",")]

    # counts usage of keys
    # (returned keys are ignored if they're not identifiers)
    def get_member_usages(self, **_):
        return Counter(item[0] for item in self.data if len(item) > 1)

    # renames the keys
    def rename(self, members, **_):
        for item in self.data:
            if len(item) > 1:
                item[0] = members.get(item[0], item[0])

    # formats back to string
    def minify(self, **_):
        return ",".join("=".join(item) for item in self.data)

def sublanguage_main(lang, **_):
    if lang == "splitkeys":
        return SplitKeysSubLang

In the code:

-- (implementation of splitkeys omitted)
local table = splitkeys(--[[language::splitkeys]]"key1=val1,key2=val2,val3,val4")
?table.key1 -- "val1"
?table[1] -- "val3"

To run, use --script <path> as before.

shrinko8's People

Contributors

jsibbiso avatar pancelor avatar thisismypassport 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

Watchers

 avatar  avatar  avatar

shrinko8's Issues

Support nested comments?

Pico-8, unlike standard lua, supports nested comments like "--[[ bla [[ bla ]] bla ]]".
It seems like the sort of thing that's part of the language rather than a mistake, so I should probably support it.
(Noticed by Heracleum on discord)

Feature Proposal: Find most token (or character) intense parts of code.

That was a little of a convoluted title...

Explanation:
I would find it helpful to be able to see which parts of the code have the most tokens / characters after minification.
In other words, which parts can be minified the least / most.

If this already exists, I would love to know.

Thanks!

meta tag causes stdout (warning?) codo_free fail 21 0

My apologies if this is not a bug, I have no idea how the __meta:*__ tag works or what it does, and can't find any documentation on it other than it being briefly mentioned in the 0.2.4 changelog.

Shrinko8 appends this line to the end of the cart when minified:

__meta:title__
>8

Those lines' presence in the cart causes Pico-8 to print this message to stdout in a Linux shell (not stdout, though I'm not sure if Pico-8 would differentiate between the two).

codo_free fail 21 0

It seems like some kind of warning message?

Anyway, thanks for making this great new tool!

Reported compressed size in shrinko8 does not match pico8

Shrinko8 brings the uncompressed code of DAGGER WULF down to 1009 bytes using Balanced - Aggressive Minification 😲

However, pasting the compressed code into Pico8 0.2.5g reports 1021 bytes

Is shrinko8 (at least the web version) miscounting UTF-8 characters ?

If that is of any help, PXAVIZ does report 1021 bytes


Uncompressed code of DAGGER WULF

for y=0,62do
for x=0,79do
-- draw sprites
--   1 = grass
--   2 = palm tree
--   3 = rocks
-- 4-7 = amulet pieces
--   8 = spider?
--   9 = player
--  10 = hut
sset(x+8,y%7,"0x"..(
  "0000000003bbbb00dd505d50000000000000000000a0aaaaaaaa0a0000288820000ccd0000a9a900000000003bbb3bb0dd50dd5000000000aaaa000000a0a0a0aaaa0a00028888200ccccd000aaa9a9000000000b32bb3b0555055500000aaaaa00aa00000a0000aaaaa0a000288822000a4acc0aaa999a900000100b042b3b000000550000aa0000a00a00000a00aaa0000aa0088822220000aa0000a4004a0001001000022b0b05dd50000000a00a00aa0a00000aa0000aaaaa0002828208000a88a00a400004a0001000004422000dd550d50000a00aaaaa0a000000aaaaaa000000088808080000880000400004000000000044420005555055000aa00a0a0a00a000000000000000000020080000044040004000040"
)[x+1+y%7*80])
-- pick room tile out of
--  9 prefab rooms
i=
("222022222002222200002000000020000022020002222022200002222000002222020200002000022202202000222202222220222220002220000020020200200020020222222000022222022220000022000002000000020000022000002222022200000222200222022000000000200000020022222200002222000000222220200000000220220000000022200220000022000002202220020200000000220002002000222202000000222202222000002202220200200002022202200000222202222220222200020220222000020000202022220000022220000")
[(x\7+y\7*2)%9*49+x%7+y%7*7+1]
-- wall the whole map
if(x*y<1or x==62or y==62)i=2
-- draw map
if(x<=62)mset(x,y,i+rnd(2))
end
end
-- health
ph=0
-- beasts table
beast = {}
-- move entity if possible
function clear(d)
yy=z.y+sin(d/4)
xx=z.x+cos(d/4)
if(mget(xx,yy)\2!=1)z.x,z.y,z.m=xx,yy,true
end
::_::
-- graphic mode 3 - 64x64
poke(24364,3)
-- wave dagger
b=btnp(❎)
-- wait frame and cls
?"⁶1⁶c"
-- when no health
--  intro screen
if(ph<1)then
-- reset count of amulet pieces
-- reset hit
pp=0hit=0
srand()
-- reset amulet pieces
mset(3,3,4)
mset(62-3,3,5)
mset(62-3,62-3,7)
mset(3,62-3,6)
-- reset hut
mset(31,31,10)
-- draw grass and reset beasts
for x=0,62do
p={x=32;y=32}
beast[x]={x=rnd(60)\1;y=rnd(60)\1,h=1}
spr(1,x,rnd(124))spr(4,32-8,32,2,1)spr(6,32-8,32+8-1,2,1)end
-- draw title and co ♪
?"⁶w⁶td𝘢𝘨𝘨𝘦𝘳\n`w𝘶𝘭𝘧",8,8,8
?"@𝘱01 #𝘱𝘪𝘤𝘰1𝘬",8,58,5
?"❎ to start⁷szdagerwf",10,50,t()*62
-- if dagger -> start game
if(b❎)ph=5
else
-- if some health
--  game loop
-- reduce hit intensity
hit*=.9
-- draw the move
map(0,0,4-p.x\7*56-hit\1*sin(rnd()),4-p.y\7*56,64,64)
z=p
z.m=b-- play dagger sounds ♪
if(b❎)?"⁷ce"
-- play hit sound ♪
if(hit>1)?"⁷i6c"
-- move the player
if(btnp(➡️))clear(0)
if(btnp(⬆️))clear(1)
if(btnp(⬅️))clear(2)
if(btnp(⬇️))clear(3)
-- draw the player
spr(9,4-p.x\7*56+z.x*8,4-p.y\7*56+z.y*8,1,1,t()%2>1)
i=mget(z.x,z.y)
-- pick up a piece of amulet? ♪
if(i\4==1)pp+=1mset(z.x,z.y,0)?"⁷szc-g"
-- reached the hut with
--  full amulet! ⌂♪
if(i*pp==40)?"well done⁷szdagerwf",14,19,10
-- beast loop
for x=0,62do
 z=beast[x]
 -- if it's alive
  -- draw it
  -- if the player moved
  --  move randomly
  --  if it touches the player
  --   die if the player used the dagger
  --   else hit the player 
    if(z.h>0) spr(8,4-p.x\7*56+z.x*8,4-p.y\7*56+z.y*8,1,1,(x+t())%2>1) if(p.m) clear(rnd(4)\1) if(p.x==z.x and p.y==z.y)  if(b❎) z.h=0 else hit=2 ph-=1
end
-- draw health and
--  number of pieces of amulet
?ph.."웃 ◆"..pp,1,2,0
?ph.."웃 ◆"..pp,0,1,0
?ph.."웃 ◆"..pp,2,1,0
?ph.."웃 ◆"..pp,1,1,t()*62
end
goto _

Feature proposal: annotate source with token/character counts for functions

I want to add per-function token and character counts, pre and post minification, to https://github.com/sparr/pico8lib. My initial idea was to use a script to parse the functions out of a source file, then feed them separately to shrinko8 -c, parse the output, prepend it as a comment to the function, then reassemble the source file. However, it occurs to me that a lot of the necessary functionality to do this better already exists in this project.

My proposal is a mode for shrinko8 that does all of this directly. Parse a cart, identify all the functions, count their tokens and characters, minify them, count those tokens and characters, insert a comment into the original cart above the function (possibly before existing comments, and overwriting a previous comment from this process) and emit the annotated original cart.

I'm willing to do most of the work to make this happen, but I find the code here a bit daunting. I've made it to process_code where it calls tokenize then parse, and I can look through Nodes under root, but I am not sure how to get back from a Node to tokens that count_tokens wants, or how to turn a Node back into a cart after I've modified some of its grandchildren. Generally moving back and forth between the source, tokens, and nodes, in a way that keeps them mapped to each other for modifying one based on the other, is a mystery to me so far. I would appreciate any pointers you can share before I get started on this. Even if this isn't a welcome feature for the main repo, I'd still like to pursue it as a fork or patch so I can use it myself.

tiny rom support? (export -t out.p8.rom)

Is there a way to create a "tiny" rom with this tool? the compressor works better than pico8's default, but if I can't get that output as a tiny rom (like you can get in pico8 by typing export -t out.p8.rom) then, for example, the better-compressed result can't be entered into sizecoding contests.

The only way I know how to export ROMs from shrinko8 is with shrinko8 game.p8 out.rom, but out.rom is always a 32K file.

for reference, see the "PICO1K Tools" section of the 0.2.5 release notes or the "tiny cartridges" section of the 0.2.4 release notes. (I don't imagine they're too helpful to you but maybe export -t @clip would be useful while debugging)

String minification with extra symbols unexpected

This command:

echo 'print(--[[member]]"apple,banana,~coconut")' | python3 shrinko8.py - - --minify --format lua

Produces this output:

print"a,p,~coconut"

But I expect this output:

print"a,p,~c"

Is there a way to add more delimiters here, besides just commas?

Minification can create erroneous binary literals

I tried minifying an already-somewhat-hand-compressed cart to see what I missed. shrinko8 seems to have deleted spaces between a series of assignments in such a way that =0b= appeared as a substring.

image

Before (w/o _min) / after (w/ _min) .p8 files:

examples.zip

choosing varnames in output

(We've talked about this a bit before, but I've found a nice clear use-case for this, and figured I'd make an issue to hold discussion.)

This cart is a simplified version of my pico1k jam entry:

-- gitignored/test.lua
x,lvl=8,1
::_::
menuitem(1,"level "..lvl,function(b)
 lvl+=b\2%2-b%2
 menuitem(nil_,"level "..lvl)
end)
b=btnp()
x+=b\2%2-b%2
flip()cls()
spr(0,x*8,64)
goto _

Notice that b\2%2-b%2 is repeated. It would be great for it to be repeated in the output as well, but unfortunately it uses different variable names (e\2%2-e%2 and n\2%2-n%2).

> shrinko8 gitignored/test.lua out.lua --minify --focus-compressed --no-minify-lines --count
tokens: 60 1%
chars: 145 0%
compressed: 124 1%
-- out.lua
e,l=8,1
::e::
menuitem(1,"level "..l,function(e)
l+=e\2%2-e%2
menuitem(o,"level "..l)
end)
n=btnp()
e+=n\2%2-n%2
flip()cls()
spr(0,e*8,64)
goto e

If I manually change the varnames, I can get it down to 120 compressed, but I want the renaming to happen automatically every time I build my game. I don't want shrinko8 to be smarter and do it automatically -- I just want more control over this somehow.

How can I get the out.lua to use the same variable names in the e\2%2-e%2/n\2%2-n%2 lines?

bad output for shorthand print with `--focus-compressed --no-minify-spaces`

Here's a bug that seems to require the bizarre combination of flags --focus-compressed and --no-minify-spaces. I ran across it while debugging -- that's why I had --no-minify-spaces

if(x==1)?1
if x==0 then
  ?2
  ?3
end

shrinko8 test.lua - -f lua --minify --focus-compressed --no-minify-spaces

if i==1then?1 end if i==0 then
  ?2
  ?3
end

(the output is invalid pico8 code; ?1 end if needs a newline in there somewhere)

-1 => 65535

I just ran across this oddity:

$ echo 'a=-1' | shrinko8 - - --minify --count  #good
tokens: 3 0%
chars: 4 0%
compressed: 13 0%
pico-8 cartridge // http://www.pico-8.com
version 41
__lua__
a=-1

$ echo 'a=-1' | shrinko8 - - --minify --no-minify-tokens --count  #what?
tokens: 3 0%
chars: 7 0%
compressed: 16 0%
pico-8 cartridge // http://www.pico-8.com
version 41
__lua__
a=65535

support `--label` for p8 output

When 1.p8 has no __label__ section, 2.p8 will not have one either:
shrinko8 1.p8 2.p8 --label label.png

Workaround: shrinko8 1.p8 temp.p8.png && shrinko8 temp.p8.png 2.p8

very slow compression (10s)

Hey! I don't know if this is really an "issue" per se, it's more of a curiosity that I thought you might be interested in. I found a large cart and idly wondered how well shrinko8 would compress it, but it seemed to get caught in an infinite loop! but when I added some debugging prints, it wasn't actually stuck, it was just very slow:

> time shrinko8 '#amiquest-4' --bbs --count --no-count-tokenize
chars: 58092 89%
compressed: 15450 98.94%

I timed this a few times and got 10 or 11 seconds each time:

10.77s user 0.04s system 98% cpu 11.002 total
11.24s user 0.04s system 97% cpu 11.563 total
10.94s user 0.05s system 98% cpu 11.203 total

In comparison, a cart of mine only takes 2 seconds to compress; it also is very full of stringified sprite/map data.

> time shrinko8 '#thetower-0' --bbs --count --no-count-tokenize
chars: 27851 42%
compressed: 12498 80%

I timed it a few times:

1.15s user 0.03s system 81% cpu 1.439 total
1.11s user 0.04s system 89% cpu 1.290 total
1.17s user 0.03s system 89% cpu 1.347 total


Maybe this is just a case of "yep, that's expected with the compression algorithm". (I tried amiquest with --fast-compression and it dropped down to 2 seconds as well) But I did think shrinko8 was stuck in an infinite loop, possibly a progress bar of some kind might be nice?

Preserve annotation preserves names of global functions but renames invocations

This cart, when minified, will crash:

function --[[preserve]]obj_new()
 return {}
end

function _init()
 obj=obj_new()
end

The minified code (with line breaks) is:

function obj_new()
 return{}
end

function _init()
 n=u()
end

Strangely, if I use either the global or member annotations instead it works fine. It also works fine with no annotation.

Is this maybe the wrong way to preserve the names of these functions?

bug: preserve for-loop / function parameter

This cart runs fine in pico8; it prints 9 numbers with some colors:

-- test2.lua
function _draw()
 cls()
 for --[[preserve]] n=1,3 do
  for i=1,3 do
   print(i+1,n+1)
  end
 end
end

(btw be careful with that annotation -- the spaces are required)

But minifying breaks things -- the minified cart is upset because o+1 is nil arithmetic:
shrinko8 --minify -f lua --no-minify-lines --no-minify-spaces test2.lua out.lua

-- out.lua
function _draw()
 cls()
 for  n=1,3 do
  for n=1,3 do
   print(n+1,o+1)
  end
 end
end

another similarly bugged example:

> echo 'function foo(--[[preserve]] x) return x+1 end' | shrinko8 --minify -f lua - -
function n(x)return n+1end

(I would expect function n(x)return x+1end instead)


I initially tried to preserve these vars from the commandline (--preserve 'n') but it didn't work. However, local vars declared like local n=3 were correctly preserved. Maybe this is related?

My shrinko8 is the commit tagged v1.1.2 ("avoid bxBX...")

unhandled exception

I am getting this error.

input chars: 462515 705.75%
input tokens: 100740 1229.74%
tokens: 100740 1229.74%
chars: 462515 705.75%
compressed: 124640 798.16%
Traceback (most recent call last):
  File "shrinko8.py", line 210, in <module>
  File "shrinko8.py", line 195, in main
  File "pico_compress.py", line 25, in write_compressed_size
  File "pico_compress.py", line 463, in compress_code
  File "utils.py", line 983, in u16
struct.error: ushort format requires 0 <= number <= 0xffff
[4184] Failed to execute script 'shrinko8' due to unhandled exception!

As you can see there's a quite big input here. I moved from PICO8 to PICOLOVE and use shrinko8 to compile all the Lua/p8 scripts into one, minified .p8 file. If possible, I would like to get some more info on this error: on what's going on, if it is related to my source.

And yes, a great tool, thx for creating it.

Feature Proposal: Constant variables.

This code:

x = 5 -- constant

function draw_stuff()
    print("constant variable: " .. x)
    rectfill(x, x, x + 10, x + 10, 12)
end

would get replaced with:

function draw_stuff()
    print("constant variable: " .. 5)
    rectfill(5, 5, 5 + 10, 5 + 10, 12)
end

or even:

function draw_stuff()
    print("constant varaible: 5")
    rectfill(5, 5, 15, 15, 12)
end

The above example could be a really nice feature for big games (which I guess shrinko8 is made for).

README preserve example doesnt work

Am I misunderstanding the readme? I can't find a combination of flags that makes something like a,b = circfill, rectfill appear in the output:

--preserve: !circfill, !rectfill
circfill, rectfill = --[[preserve]]circfill, --[[preserve]]rectfill
circfill(10,10,20); circfill(90,90,30)
rectfill(0,0,100,100); rectfill(20,20,40,40)
--preserve: !rnd
rnd = --[[preserve]]rnd
rnd(12)

shrinko8 --minify test.lua - -f lua --no-minify-spaces

circfill, rectfill = circfill, rectfill
circfill(10,10,20) circfill(90,90,30)
rectfill(0,0,100,100) rectfill(20,20,40,40)
a = rnd
a(12)

This doesn't actually affect me, its just a bug(?) I noticed recently.

Notice that it does work for rnd, likely because rnd is (correctly) missing from this list?

argument parser acting strange (not trimming spaces?)

not-working script:

#!/usr/bin/env bash

# usage:
#   ./compress.sh    # build + minify
#   ./compress.sh 1  # build only

MINIFY="--minify"
if [[ -n "$1" ]]; then
  echo "skipping minification..."
  MINIFY=""
fi

/w/shrinko8/shrinko8.py game.p8 tower.p8.png --lint --count "$MINIFY" --script compress_combine.py

when I ./compress.sh, shrinko8 runs normally. when I ./compress.sh 1, it shows the usage string and says shrinko8.py: error: unrecognized arguments:, which is unexpected (I expect it to run normally)

I assume the problem is because the command ends up having two spaces in it, which confuses the argument parser, but I haven't looked into it.

My shrinko8 commit hash is 8a4f6e0


workaround:

#!/usr/bin/env bash

# usage:
#   ./compress.sh    # build + minify
#   ./compress.sh 1  # build only

MINIFY="--minify"
if [[ -n "$1" ]]; then
  echo "skipping minification..."
  MINIFY="--count" # hacky
fi

/w/shrinko8/shrinko8.py game.p8 tower.p8.png --lint --count "$MINIFY" --script compress_combine.py

Compress fails on binary string

Compress fails with:

ValueError: '' is not in list

or:

---> 11 write_cart("carts/new_cart.p8.rom", new_cart, CartFormat.rom)

File [c:\Users\Frederic\Source\Repos\hexen\.venv\lib\site-packages\pico_cart.py:711](file:///C:/Users/Frederic/Source/Repos/hexen/.venv/lib/site-packages/pico_cart.py:711), in write_cart(path, cart, format, **opts)
    709     file_write(path, write_cart_to_image(cart, **opts))
    710 elif format == CartFormat.rom:
--> 711     file_write(path, write_cart_to_rom(cart, **opts))
    712 elif format == CartFormat.tiny_rom:
    713     file_write(path, write_cart_to_tiny_rom(cart, **opts))

File [c:\Users\Frederic\Source\Repos\hexen\.venv\lib\site-packages\pico_cart.py:101](file:///C:/Users/Frederic/Source/Repos/hexen/.venv/lib/site-packages/pico_cart.py:101), in write_cart_to_rom(cart, with_trailer, keep_compression, **opts)
     99     w.bytes(cart.code_rom)
    100 else:
--> 101     compress_code(w, cart.code, **opts)
    103 if with_trailer:
    104     w.setpos(k_cart_size)
...
    475         w.u16(size & 0xffff) # only throw under fail_on_error above
    477 else:
--> 478     w.bytes(bytes(ord(c) for c in code))

ValueError: bytes must be in range(0, 256)

Repro code:

from pico_cart import write_cart, CartFormat, Cart
new_cart = Cart()
new_cart.rom[0x0000:0x2000] = bytearray(0x2000) # zero it out
new_cart.set_code("""-- my game
-- @freds72
local title_gfx={bytes="\0\0\0\0\0\0\0\0\0¹\0\0\0■\0\0■▮\0■■■■■■■■"}
function _init()
end
""")

write_cart("carts/new_cart.p8.rom", new_cart, CartFormat.rom)

note: the binary string has been copied from Pico (eg. valid)

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.