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 where your assets live. It has no default folder and no folder name built into it. What it knows is a list of folders you gave it, and one rule for reading from that list.

You add a folder with natiny_resource_mount():

C
natiny_resource_mount("data", NULL);

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:

TEXT
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

Nothing is mounted until you say so, so a native program mounts before it loads:

C
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) return 1;

natiny_resource_mount("data", NULL);

NatinyResource res  = natiny_resource_load("textures/hero.png");
NatinyTexture  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 second argument is the passphrase for an encrypted folder. NULL is right until you pack with one, which we will do below.

Run the program. If the texture appears, the mount is working, and it will keep working unchanged through everything below.

Loading before mounting

A path no mount answers is tried as a plain file, relative to the directory the program is running in. So a program that forgets to mount does not fail loudly — it reads a different file, or nothing. If a load returns 0 right after you added mounting, check that the mount happens first.

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:

C
natiny_resource_pack("data", "data", 0, "my secret");

The first argument is the folder to pack, the second is where the result goes — here, back into the same folder. 0 asks for the default pack size, and the last argument is the passphrase. You now have:

TEXT
data/
├── index.pack      the list of what is inside
├── 0000.pack
├── 0001.pack
└── textures/       ← still there, still yours

Tell the mount about the passphrase, and run again:

C
natiny_resource_mount("data", "my secret");

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:

TEXT
my_game/
├── my_game.exe
└── data/
    ├── index.pack
    ├── 0000.pack
    └── 0001.pack

That folder is the whole game. Not one line of loading code changed.

Packing is a build step, not a startup step

It runs on your own machine while you prepare a release, so it belongs in a tool of your own rather than in the game. The whole tool is this:

C
#include "natiny.h"

int main(void)
{
    return natiny_resource_pack("data", "data", 0, "my secret") ? 0 : 1;
}

Link it against natiny the way you link the game, run it from your build script, and read the exit code. It needs no window and no backend: packing touches files and nothing else.

On phones and in the browser there is no folder of loose files to gather, so packing there returns 0.

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 many small files share most of their content.
  • 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, when you pass a passphrase.

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:

TEXT
my_game/
├── my_game.exe
├── data/                     the game
└── mods/
    └── hd-textures/
        └── textures/
            └── hero.png      same name as in the game

One line turns it on:

C
natiny_resource_mount("mods/hd-textures", NULL);

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.

Note the NULL: the key belongs to the folder, not to the program. Your own assets can be packed with your passphrase while a player's mod is an ordinary folder, and both sit on the same stack.

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 needs textures/hero.png, not hero.png and not data/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.

Turn mods on and off

natiny_resource_unmount() removes a folder from the stack:

C
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:

C
natiny_resource_mount("mods/balance", NULL);   /* now above hd-textures */

natiny_resource_mount() returns 0 in one case worth handling: the folder holds an index.pack that cannot be read, because the passphrase is wrong or the file is damaged. Natiny leaves such a folder off the stack rather than quietly falling back to loose files you meant to replace, and says which it was on stderr:

C
if (!natiny_resource_mount("data", key)) {
    /* the packs are there and unreadable - do not carry on */
    return 1;
}

A folder that does not exist is not that case. It mounts, and answers nothing.

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. Read a list from a configuration file, take them from a menu, get them from a server: mount each one in turn, and remember that the last one mounted wins any conflict, so the order you mount in is your load order.

Mods that contain code run as your program

A mod that only replaces pictures and sounds is safe. Anything else is running inside your process with everything your process can do. Natiny does not sandbox it. If you accept mods from strangers, say so where your players will read it.

Common problems

The program cannot find a file after you added 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_resource_pack("data", "build/shipped", 0, key).

A save cannot be read back. natiny_resource_save() 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.