A list of helpful libraries and code

This little snippet saved my sanity. It means you don't have to manage the dimensions and initialisation when using tables as multi-dimensional arrays in Lua.

from: https://stackoverflow.com/a/21287623/28290

function newAutotable(dim)
    local MT = {};
    for i=1, dim do
        MT[i] = {__index = function(t, k)
            if i < dim then
                t[k] = setmetatable({}, MT[i+1])
                return t[k];
            end
        end}
    end

    return setmetatable({}, MT[1]);
end

-- Usage
local at = newAutotable(3);
print(at[0]) -- returns table
print(at[0][1]) -- returns table
print(at[0][1][2]) -- returns nil
at[0][1][2] = 2;
print(at[0][1][2]) -- returns value
print(at[0][1][3][3]) -- error, because only 3 dimensions set
7 Likes

I just discovered the strict mode script in lua that check if the variables you use have been properly initialized. It is useful to prevent making typos in your variable that can go easily unnoticed and create bugs.

http://www.lua.org/extras/5.2/strict.lua

In my code I just added a check so that it would be used only in the simulator

-- strict.lua
-- checks uses of undeclared global variables
-- All global variables must be 'declared' through a regular assignment
-- (even assigning nil will do) in a main chunk before being used
-- anywhere or assigned to inside a function.
-- distributed under the Lua license: http://www.lua.org/license.html

if not playdate.isSimulator then
  return
end

local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget

local mt = getmetatable(_G)
if mt == nil then
  mt = {}
  setmetatable(_G, mt)
end

mt.__declared = {}

local function what ()
  local d = getinfo(3, "S")
  return d and d.what or "C"
end

mt.__newindex = function (t, n, v)
  if not mt.__declared[n] then
    local w = what()
    if w ~= "main" and w ~= "C" then
      error("assign to undeclared variable '"..n.."'", 2)
    end
    mt.__declared[n] = true
  end
  rawset(t, n, v)
end
  
mt.__index = function (t, n)
  if not mt.__declared[n] and what() ~= "C" then
    error("variable '"..n.."' is not declared", 2)
  end
  return rawget(t, n)
end

Update: It seems this piece of code is actually already in the SDK you just have to import 'CoreLibs/strict'

5 Likes

This is random but I just wrote this function to generate an array of midi notes. Maybe someone some day will find it useful besides me.

function generateMidiNotes(octave, offset, note_count, reverse_order)
	local base <const> = {24, 26, 28, 29, 31, 33, 35}
	local notes = table.create(note_count)
	
	local index_start <const> = reverse_order and (note_count-1) or 0
	local index_end <const> = reverse_order and 0 or (note_count-1)
	local index_inc <const> = reverse_order and -1 or 1
	local note_i = 0
	
	for i = index_start, index_end, index_inc do
		notes[note_i + 1] = base[((offset + i) % #base) + 1] + (12 * (math.floor((offset + i) / #base) + octave))
		note_i += 1
	end
	
	return notes
end
4 Likes

I'll happily be your first customer on this one, Dustin : D
I'm having a hard time understanding how to generate good random musical scales for my game, it's all very new to me.

1 Like

Here a very simple parser to get paths from a svg file.

It returns a table of paths which are tables or coordinate { x1, y1, x2, y2, x3, y3, ... }

function getSvgPaths( svg_filepath )
	local file, file_error = playdate.file.open( svg_filepath, playdate.file.kFileRead )
	assert(file, "getSvgPaths(), Cannot open file", svg_filepath," (",file_error,")")

	local push = table.insert
	local commandArgCount = { M=2, L=2, T=2, H=1, V=1, C=6, S=6, A=7, Z=0}

	-- read the whole file
	local fileContent = ""
	repeat
		local line = file:readline()
		if line then
			fileContent = fileContent..line
		end
	until not line

	local result = table.create( 8 )
	for path in fileContent:gmatch("<path.-/>") do
		local previousX, previousY = 0, 0
		local newPath = table.create( 8 )

		local name = path:match("id=\"(.-)\"")
		if not name then name = #result + 1 end
		result[name] = newPath

		local d_content = path:match(" d=\"(.-)\"")
		for command in d_content:gmatch("%a[%-%d%., ]*") do
			local first_character = command:sub(1,1)
			local command_letter = first_character:upper()
			local absolute_coordinates = command_letter==first_character

			local args = table.create( 6 )
			for number in command:gmatch("[-%d%.]+") do
				push(args, tonumber(number))
			end
			local argCount = commandArgCount[ command_letter ]

			local argIndex = 0
			while argIndex+argCount<=#args do
				local relativeX, relativeY = 0, 0
				if not absolute_coordinates then
					relativeX, relativeY = previousX, previousY
				end

				if command_letter=="M" or command_letter=="L" or command_letter=="T" then
					push( newPath, args[argIndex+1] + relativeX)
					push( newPath, args[argIndex+2] + relativeY)
				elseif command_letter=="H" then
					push( newPath, args[argIndex+1] + relativeX)
					push( newPath, previousY)
				elseif command_letter=="V" then
					push( newPath, previousX)
					push( newPath, args[argIndex+1] + relativeY)
				elseif command_letter=="C" then
					push( newPath, args[argIndex+5] + relativeX)
					push( newPath, args[argIndex+6] + relativeY)
				elseif command_letter=="S" then
					push( newPath, args[argIndex+3] + relativeX)
					push( newPath, args[argIndex+4] + relativeY)
				elseif command_letter=="A" then
					push( newPath, args[argIndex+6] + relativeX)
					push( newPath, args[argIndex+7] + relativeY)
				elseif command_letter=="Z" then
					push( newPath, newPath[1])
					push( newPath, newPath[2])
				end

				previousX = newPath[#newPath-1]
				previousY = newPath[#newPath]

				argIndex = argIndex + math.max(argCount, 1)
			end
		end
	end

	for rect in fileContent:gmatch("<rect.-/>") do
		local width = tonumber( rect:match("width=\"([-%d%.]+)\"") )
		local height = tonumber( rect:match("height=\"([-%d%.]+)\"") )
		local x = tonumber( rect:match("x=\"([-%d%.]+)\"") )
		local y = tonumber( rect:match("y=\"([-%d%.]+)\"") )

		local name = rect:match("id=\"(.-)\"")
		if not name then name = #result + 1 end
		result[name] = {
			x, y,
			x+width, y,
			x+width, y+height,
			x, y+height,
			x, y,
		}
	end

	return result
end

Update

  • Fix various bugs
  • return a hashmap with the names of the path
  • Increase compatibility
  • added support for rectangle primitive
5 Likes

So when I was working on the LDtk level loader, something that was bothering me was that parsing a level was not fast enough. To solve this issue, after reading a level, I was exporting the result in a lua file that can be used instead of the json file.

I wanted to share a simpler version of this that could be used more generally. But the general idea:

  1. When running in the simulator, after parsing a file we write the result as a lua file in the save folder (In the SDK folder)
  2. The lua file is copied in the project folder
  3. When running on the console if the lua file is present, load it instead of parsing the file
  4. Save the frame!

The first piece of the puzzle is the following function that export a table as a lua file

function writeLua( filepath, table_to_export )
	assert( filepath, "writeLua, filepath required")
	assert( table_to_export, "writeLua, table_to_export required")

	local file, file_error = playdate.file.open( filepath, playdate.file.kFileWrite)
	if not file then
		print("writeLua, Cannot open file ", filepath," (", file_error, ")")
		return
	end

	local _isArray = function( t )
		if type(t[1])=="nil" then return false end

		local pairs_count = 0
		for key in pairs(t) do
			pairs_count = pairs_count + 1
			if type(key)~="number" then
				return false
			end
		end

		return pairs_count==#t
	end

	local _write_entry
	_write_entry = function( entry, name )
		local entry_type = type(entry)

		if entry_type=="table" then
			file:write("{")
			if _isArray( entry ) then
				for key, value in ipairs(entry) do
					_write_entry(value, key)
					file:write(",")
				end
			else
				for key, value in pairs(entry) do
					file:write("[\""..tostring(key).."\"]=")
					_write_entry(value, key)
					file:write(",")
				end
			end
			file:write("}")
		elseif entry_type=="string" then
			file:write("\""..tostring(entry).."\"")
		elseif entry_type=="boolean" or entry_type=="number" then
			file:write(tostring(entry))
		else
			file:write("nil")
		end
	end

	file:write("return ")
	_write_entry( table_to_export )

	file:close()
end

To actually parse or load the lua file directly I have the following code

-- set _enable to false to always load the original file
local _enable = true

-- folder in the project folder where the pre parsed file will be
local _folder = "preParsed/"

function parseFile( parser_fn, filename, ...)
	local pdzFilename = _folder..filename..".pdz"

	if _enable then
		if playdate.file.exists( pdzFilename ) then
			return playdate.file.run( pdzFilename )
		else
			print( "parseFile(): The following file is not pre-parsed", filename)
		end
	end

	return parser_fn( filename, ...)
end

if playdate.isSimulator then
	parseFile = function( parser_fn, filename, ...)
		local result = parser_fn( filename, ...)

		-- save result in lua file
		local luaFilename = _folder..filename..".lua"
		playdate.file.mkdir( luaFilename:match("^(.-)[^/]*$") )
		writeLua( luaFilename, result)

		return result
	end
end

To use it I simply replace a call to a parsing function with it

So for example instead of
level = json.decodeFile( "level_1-1.json" )

I would call it this way
level = parseFile( json.decodeFile, "level_1-1.json" )

Using "level_1-1.json" from the SDK example as a comparaison, loading the pre-parsed lua file is 5 times faster on the playdate than parsing the json file normally. For the svg parser I posted earlier the advantage is even more pronounced since this is 10 times faster.

Big caveats
The biggest drawback of this technique is that you need to be aware of the cached files otherwise you might have edit the original file and the game will still load the previous pre-parsed version. It get even trickier since at the moment files are not deleted when uploading a game to the playdate (so you might delete all the pre-parsed in your project but on playdate it will still load some file you don't even know are still there)

Tips
I created a symbolic link in my save folder to the "preParsed/" folder in my project so that I don't have to manually copy the files.
I also wrote a function in my project to pre-parsed all files in one go. Right now I call it when the game start since there is not that much files but later I might simply call it from the simulator console when I need to create a build.

2 Likes

Really useful, Nic. Thanks!

In my build process I manage something similar by copying the timestamp from the original file to the processed file. Then I compare file timestamps to see if I need to reprocess the file.

But I don't think it's currently possible to do this on the device using standard SDK/Lua functions. SDK can at least get modified timestamp but can't set it.

Might make a good feature request.

This is something I experimented with. The best solution I came up with was to have a symbolic link to the whole project folder in the save folder to be able to check the file modification time for the original file, not the one in the pdx.
But requiring proper setup of the save folder for the feature to work, I thought is was getting over-complicated so I preferred a simpler solution.

I would actually prefer to have full read/write access to my project folder to be honest, that would work way better (similar to playdate.simulator.writeToFile(image, path)). But when I thought about it, would I want all game running in the simulator to have full access to my files? mmmh not really.

Put together a tiny Playdate utility to experiment with the built-in easing functions. It's a single lua file, easy to build and run on device. Not a bad way to tune animations and experiment with additional ease settings like softness for back and amplitude/period for elastic. Also nice to get a sense of how an animation may feel on device.

main.lua.zip (2.6 KB)

up/down change selected setting.
right/left change selected value.
hold A while pressing left/right to change selected value by smaller increments.
press A to run animation again.

playdate-animation-tester-3

18 Likes

Just used this for the first time and it worked great! I had an issue trying to unsubscribe when I was using an anonymous function.

A simple solution I found while looking at similar libs is to return the function that was passed to the subscribed function and then use that for unsubscribing.

function Signal:subscribe(key, bind, fn)
	local t = self.listeners[key]
	local v = {fn = fn, bind = bind}
	if not t then
		self.listeners[key] = {v}
	else
		t[#t + 1] = v
	end
	return fn
end

And then

function Init()
	self.fn = signal:subscribe(
		"eye-colected", self, function(ref, event, x, y, eyesQuota, index)
			...
		end
	)
end

function Remove()
	signal:unsubscribe("eye-colected", self.fn)
	EyeCounter.super.remove(self)
end
1 Like

Thanks for this. I'll think about this a bit!

Here is a set of helpers I found useful while working in C, you need to initialize asap and not use until you have initialized or you will get crashes. Doing it first thing in the event handler should be fine.

pd_memory_tools.h

#ifndef PD_MEMORY_TOOLS_H
#define PD_MEMORY_TOOLS_H

#include "pd_api.h"

void initializeMemoryTools(PlaydateAPI *playdate);

static void* pd_malloc(size_t size);
static void* pd_calloc(size_t count, size_t size);
static void* pd_realloc(void* ptr, size_t size);
static void  pd_free(void* ptr);

#endif

pd_memory_tools.c

#include "pd_memory_tools.h"

static void* pd_realloc(void* ptr, size_t size) {
    return pd->system->realloc(ptr, size);
}

static void* pd_malloc(size_t size) {
    return pd_realloc(NULL, size);
}

static void* pd_calloc(size_t count, size_t size) {
    return memset(pd_malloc(count * size), 0, count * size);
}

static void pd_free(void* ptr) {
    pd_realloc(ptr, 0);
}

static PlaydateAPI *pd = NULL;
void initializeMemoryTools(PlaydateAPI *playdate) {
    pd = playdate;
}
4 Likes

Online Json to lua table converter: JSON Lua table converter - MAGEDDO

I use this for static level data. I put the lua files in my source folder and load the resulting pdz that pdc automatically creates with file.run

3 Likes

Here's a lightweight drop-in replacement for playdate.graphics.image that adds support for animated images: Lightweight AnimatedImage Library

Based on a conversation in the Playdate Squad Discord earlier I came up with some functions for drawing bezier curves in the Playdate Lua SDK by using line segments. Both quadratic beziers (3-point) and cubic beziers (4-point, the kind you'd find in most vector drawing apps) are supported!

This site has a nice interactive demo to compare both kinds of curves and the step parameter: A Primer on Bézier Curves

9 Likes

I'll add this here too in case the announcement thread falls by the way side. I'm working on a little Amiga mod player, blast from the past but could be very useful given the memory constraints if we can get it to run fast enough on real hardware.

2 Likes

You may want to check for overflow when implementing calloc. It's one of the reason it's best to use calloc versus malloc with a total size to avoid buffer overruns (although on PlayDate it probably will not be an issue given the lack of user input on these calls).

Something like this:

void* pd_calloc(size_t nb_of_items, size_t item_size)
{
    if (item_size && (nb_of_items > (SIZE_MAX / item_size))) {
        return NULL;
    }

    size_t size = nb_of_items * item_size;
    void* memory = pd_malloc(size);
    if(memory != NULL) {
        memset(memory, 0, size);
    }
    
    return memory;
}
2 Likes

I built a dependency/package manager for Playdate: GitHub - jm/toybox: A dependency management system for Lua on the Playdate. Just got the binary releases up (accessible at Release First release! · jm/toybox · GitHub) so I hope folks can give it a spin. I'm working on getting it published in package managers etc so it's easier to upgrade.

I also published a repo of examples for things that don't currently have single file examples so that they have focused example scripts to learn from: GitHub - jm/playdate_examples: Examples for the Playdate SDK I'll be adding to this as I experiment with more of the APIs for sure. :slight_smile:

8 Likes

Amazing! I'm working on a little eurorack modular synth app for the Playdate, but I love trackers. My Polyend Tracker is my fav piece of gear!

10 Likes

Mentioned in a discussion thread I created, but wanted to drop this here in case it's useful to anyone checking this thread in the future. I started work on a tool to more easily view and choose dither patterns directly on device:

Thanks!

2 Likes