Blog
Atom feed is here.
Today marks my first day working full-time on my own projects. I'm focusing on building a game. Toward learning how everything works end-to-end from first principles, I'm building a game engine. I will likely ship with SDL3, though.
My game will feature tilemaps built from pixel art tiles. I love using Aseprite for my art. At the time of this writing, Aseprite supports building tilemaps but only supports exporting them using an out-of-tool script. This script exports tilemaps and supporting data as JSON. I don't want to write or depend on a JSON parser, though, so I chose to fork and extend the script to output binary.
Aseprite scripts are written in Lua, so I needed to learn Lua. I half-jokingly searched for "learn lua in 10 minutes" and turned up Learn Lua in 15 Minutes. Using this, the official documentation, and asking an LLM questions here and there, I picked up Lua pretty quickly.
Some interesting things I learned are that Lua has a way to allow one Lua file
to import another (require
) which caches anything that the imported file runs
on import so that when additional imports of the same file happen, nothing
top-level runs again. There is another mode (dofile
) which imports without
caching, always running anything exposed at the top-level scope in the imported
file. Another thing is that Lua only has a table data structure which doubles
as an array. Array indices start from 1
, not 0
. You can use negative
indicies when accessing the command-line arguments to find arguments passed to
Lua itself, not to your script.
Anyway, without showing the full glue code, I introduced this binary.lua
to
the exporter script, added command-line argument parsing to the main script,
and allowed the caller to either export JSON or binary. I'll stick with binary
and write a simple parser in my game.
When calling a non-graphical script from Aseprite, you do so from the command line. You pass args before specifying the script. This is for at least Aseprite v1.3.14.3.
aseprite -b foo.aseprite --script-param bar=baz --script my_script.lua
The content of binary.lua
in its initial form is below.
-- Export an Aseprite tilemap and supporting data to a binary file.
--
-- Output Schema
-- -------------
-- schema_major_ver: uint8_t
-- schema_minor_ver: uint8_t
-- schema_patch_ver: uint8_t
-- canvas_width_px : uint16_t
-- canvas_height_px: uint16_t
-- tilesets : array<tileset>
-- layers : array<layer>
--
-- array
-- -----
-- An array `array<T>` is a single `uint64_t` indicating the number of instances
-- of `T` immediately after. The instances of `T` are contiguous in memory.
-- For example, a 2-element array of type `array<uint8_t>` is laid out as
-- follows in memory:
-- uint64_t (num elements)
-- uint8_t (first element)
-- uint8_t (second element)
--
-- string
-- ------
-- A string is a single `uint64_t` indicating the number of characters
-- immediately following and then said number of characters. The characters are
-- 8-bit bytes. Each byte is a single ASCII character.
--
-- tileset
-- -------
-- image_pathname: string
-- tile_width_px : uint16_t
-- tile_height_px: uint16_t
--
-- layer
-- -----
-- name : string
-- tileset_id : uint16_t
-- width_tiles : uint16_t
-- height_tiles: uint16_t
-- tiles : array<index_into_tileset>
--
-- index_into_tileset
-- ------------------
-- index: uint16_t
--
-- Notes:
-- - Strings do not support Unicode.
-- - All numbers are encoded as little-endian.
-- - The current schema only supports a single cel per layer.
local binary = { _version_major = 0, _version_minor = 0, _version_patch = 1 }
-- Write select keys out of table `t` to the open file `f` as binary. See
-- the output schema in the file documentation for what will be written out to
-- `f`.
function binary.encode(f, t)
-- Schema version.
f:write(string.pack("<I1", binary._version_major))
f:write(string.pack("<I1", binary._version_minor))
f:write(string.pack("<I1", binary._version_patch))
-- Canvas width, height.
f:write(string.pack("<I2", t.width))
f:write(string.pack("<I2", t.height))
-- Tilesets.
f:write(string.pack("<I8", #t.tilesets))
for i = 1, #t.tilesets do
local ts = t.tilesets[i]
f:write(string.pack("<I8", #ts.image))
f:write(ts.image)
f:write(string.pack("<I2", ts.grid.tileSize.width))
f:write(string.pack("<I2", ts.grid.tileSize.height))
end
-- Layers.
f:write(string.pack("<I8", #t.layers))
for i = 1, #t.layers do
local l = t.layers[i]
f:write(string.pack("<I8", #l.name))
f:write(l.name)
f:write(string.pack("<I2", l.tileset))
if #l.cels > 1 then
error("Layer " .. i .. " has more than 1 cel")
end
local cel = l.cels[1]
f:write(string.pack("<I2", cel.tilemap.width))
f:write(string.pack("<I2", cel.tilemap.height))
f:write(string.pack("<I8", #cel.tilemap.tiles))
for j = 1, #cel.tilemap.tiles do
f:write(string.pack("<I2", cel.tilemap.tiles[j]))
end
end
end
return binary
1:14 to 1:28.
A friend shared this article with me recently, talking about how worrying about looking stupid often just stunts your growth. I agree! People should see how intentionally stupid ML models are while they're "growing," until they're suddenly smarter than Sapiens.
My friend sent me this article recently about people spiraling into delusions aided by conversations with ChatGPT.
For some reason, the phenomenon of declining birthrates in first-world countries quickly came to mind. There are many explanations for declining birthrates, but that it is mostly only happening in first-world countries seems like a well-veiled form of wireheading. We used to need kids to guarantee we have hands to work the farms and sustain the empire. Then we had kids to carry on the family lineage. Then we had kids because, who is going to take care of me when I'm older? Then we had kids just because that's what grown-ups do, right? Now having kids is increasingly relegated to reacting to biological urges and personally-held images of family. The more well-off a country becomes, the less any one individual needs to be responsible for anything critical on the community level, and things that were once critical to survival now largely come for "free," and we feel some sort of existential emptiness from not having a larger-than-self, meaningful goal to grind away towards. We often fill this emptiness with both readily-available pleasures and ultimately-meaningless, artificially-difficult quests. We're just wireheading ourselves.
The models of ChatGPT alluded to in the article presumably did not have an explicit goal to radicalize or delude anyone. The models' ability to assume arbitrary human-like personas had an un(?)intended side-effect of finding the holes in the swiss cheese of the constructs that help keep one comfortably integrated into society, the constructs that sit in front of the users' psyches. Side-stepping the question of whether ChatGPT is "good" or "bad" in this regard, it seems to me that the delusional in the article are liking the whole experience.