Assets, packs and mods
A game is more than code. It has textures, sounds, models and fonts, and those files have to be found while you work, stay together when you ship, and — if you want it — be replaceable by your players.
Natiny handles all three with one idea: a stack of folders. In this guide we will load a file from a folder, pack that folder into a few files for release, and then let a mod replace part of it without changing a line of the game.
A stack of folders
Natiny does not know that your assets live in data/. It does not know the
word data at all. What it knows is a list of folders you gave it, and one
rule for reading from that list.
You add a folder with mount:
natiny.resource.mount("data")
From that moment, the folder's contents are named from inside it. The file
data/textures/hero.png is loaded as "textures/hero.png". The mount path is
not part of the name.
That is the part worth remembering, because everything else follows from it. If
two folders both contain textures/hero.png, both can answer the same request.
Natiny takes the answer from the folder mounted last:
mods/hd-textures ← mounted last, asked first
data ← mounted first, the fallback
A file the top folder does not have falls through to the one below it. Nothing in the game has to know which folder answered.
Load your first file
If you are writing Lua, the wrapper has already mounted data/ from beside the
executable, and it ran your main.lua out of it. So you can load straight away:
local natiny = require("natiny")
local res = natiny.resource.load("textures/hero.png")
local hero = natiny.texture.create(res)
natiny.resource.destroy(res)
Note that there is no data/ in that path. The folder was mounted, so its
contents are named from inside it.
The same applies to require. A module at data/game/state.lua is:
local state = require("game.state")
Run the game. If the texture appears, the mount is working, and it will keep working unchanged through everything below.
Pack the folder for release
Shipping a folder of several thousand loose files is slow to copy, slow to install, and an invitation to anyone who wants your art. Natiny can glue the whole folder into a handful of files instead. The executable does it from the command line:
natiny.exe pack data
That names the folder to pack. The packs go back into the same folder, which is what a release build wants. You now have:
data/
├── index.pack the list of what is inside
├── 0000.pack
├── 0001.pack
├── main.lua ← still there, still yours
└── textures/
Run the game again. It behaves exactly as before, because a mounted folder
holding an index.pack answers the same names it answered as loose files.
To ship, delete the loose files and keep the packs:
my_game/
├── natiny.exe
└── data/
├── index.pack
├── 0000.pack
└── 0001.pack
That folder is the whole game. Not one line of code changed.
Packing is a build step, not a startup step
The command runs on your own machine while you prepare a release, and it fits straight into a build script: it reads relative paths from the directory you typed it in, prints what it packed, and returns a non-zero exit code when something went wrong.
natiny.exe pack data build/shipped --part-size=4M
A second folder sends the packs somewhere else instead of into the folder being
packed, which is useful while you are still checking the result.
natiny.exe pack --help lists the options.
The same thing is available from Lua as
natiny.resource.pack, for a tool of your
own. On phones and in the browser there is no folder of loose files to gather,
so packing there does nothing.
Packing the same folder twice gives you the same bytes, and the packer steps
over its own index.pack and 0000.pack, so you can run it again after every
change without it eating the previous result.
What packing gives you
Three things, and none of them need any work from you:
- Fewer, smaller files. The contents are compressed together, which helps a great deal when a hundred Lua files share most of their text.
- Faster loading. Files that sit next to each other in the pack are read together, so the second one usually costs nothing.
- Assets that are not simply sitting there - if you ask for it. Packing
writes in the clear by default;
natiny pack data --key="..."encrypts, and whatever mounts the result names the same passphrase:natiny.resource.mount("data", "...")in Lua, or the second argument ofnatiny_resource_mount()in C. The wrapper's owndatafolder is the one case it handles for you - build it with-DNATINY_BUNDLE_KEY="..."and it packs and mounts that folder under your key without a line of script.
On that last point, be clear-eyed: this stops a player opening your game with an archive tool and taking the art. It does not stop a determined person with a debugger, because the key travels inside the program that reads it. It is a lock on a door, not a safe.
Let a mod replace part of the game
A mod is a folder mounted above the game. There is no mod system to learn, because there is nothing else to it.
Say a player makes higher-resolution textures and puts them here:
my_game/
├── natiny.exe
├── data/ the game
└── mods/
└── hd-textures/
└── textures/
└── hero.png same name as in the game
One line turns it on:
natiny.resource.mount("mods/hd-textures")
Now natiny.resource.load("textures/hero.png") reads the player's file.
Everything the mod does not contain still comes from data/. Your code does
not change, and does not need to know the mod exists.
The mod can also add files rather than replace them. A file at
mods/hd-textures/textures/dragon.png is simply there to be loaded as
"textures/dragon.png", even though the game never shipped one.
Match the name exactly
A mod replaces a file by using the same name inside its own folder. If the game loads
"textures/hero.png", the mod needstextures/hero.png, nothero.pngand notdata/textures/hero.png. A path that does not match is not an error — the file is simply never asked for, which is a confusing thing to debug. When a mod seems to do nothing, check the name first.
A mod folder may itself be packed. natiny.exe pack mods/hd-textures turns it
into two files a player can send to a friend, and it keeps working exactly the
same way.
Turn mods on and off
unmount removes a folder from the stack:
natiny.resource.unmount("mods/hd-textures")
The next load of "textures/hero.png" comes from data/ again. Textures you
already created are untouched — a resource keeps its own copy of its bytes. What
changes is where the next load looks.
Mounting a folder that is already mounted moves it to the top instead of adding it twice, which is how you change the order after the fact:
natiny.resource.mount("mods/balance") -- now above hd-textures
Natiny will not go looking for mods on its own. It has no idea what a mod is, or
where you keep them, and that is deliberate — your game decides. The simplest
version is a text file the player edits, kept beside the executable rather than
inside data/:
hd-textures
balance
-- load() raises an error when a file is not there, and a player with
-- no mods has no mods.txt. pcall turns that into "nothing to mount".
local ok, list = pcall(natiny.resource.load, "mods.txt")
if ok then
for name in natiny.resource.get_data(list):gmatch("[^\r\n]+") do
natiny.resource.mount("mods/" .. name)
end
natiny.resource.destroy(list)
end
Three things are happening there worth naming. Each line is mounted in turn, so
the last line in the file wins any conflict — which makes the file a load
order the player can rearrange. mods.txt is read even though no mounted folder
contains it: a name the stack cannot answer is tried as a plain file beside the
executable, which is exactly what you want for a file the player is meant to
edit. And the pcall is there because a missing file is a Lua error rather than
a nil — an empty install is the normal case, not a crash.
From here it is your game's own design. Mods could carry a small Lua file describing themselves; the menu could list them with pictures; a save could remember which were on. Natiny stays out of all of it.
Mods that contain Lua run as your game
A mod that only replaces pictures and sounds is safe. A mod that brings
.luafiles with it is running code inside your program, with everything your program can do. Natiny does not sandbox it. If you accept mods from strangers, say so where your players will read it.
Common problems
The game cannot find a file after you moved to mounting. Check for a
leftover data/ at the front of the path. "data/textures/hero.png" was right
before the folder was mounted; "textures/hero.png" is right after.
A mod appears to do nothing. The name inside the mod folder does not match the name the game loads. See the warning above.
Everything breaks the moment you delete the loose files. Something was
loading a path the packs do not contain. Keep the loose folder around and pack
into a different directory while you check:
natiny.exe pack data build/shipped.
A save cannot be read back. Saving writes a real file and never writes into a pack. If you save under a name a mounted pack already holds, the pack answers first and your save is never seen. Give saves names of their own.
What you have now
One idea did all of it. A mounted folder's contents are named from inside it, and the folder mounted last answers first.
Everything else was a use of that. Packing swaps a folder for a smaller version of itself that answers the same names. A mod is a folder above the game. Turning a mod off is removing the folder. Load order is the order you mounted in.
Because natiny knows nothing about mods, there is nothing in its way when your game wants something different — a patch folder, downloadable content, a level editor that overrides the shipped files while you work. They are all the same three calls.
Next, natiny.resource.mount and its
neighbours have the exact contract for each call.