Content and the manifest
A Labrador game’s content lives in files beside the executable, listed in a manifest the game loads at startup. Names, handles and resources explains the model; this guide is the practical side: adding a file, getting it beside the executable, and loading content the engine has never heard of.
Adding a texture or a font
Section titled “Adding a texture or a font”-
Put the file in your content folder, under the directory its group names. A project made from the minimal sample has
content/fonts/; addcontent/textures/for textures. -
List it in
content/manifest.jsonby name, without its extension, in a group of the right kind. A new kind of file needs a new group:samples/linesweeper/content/manifest.json {"assets": [{"kind": "font","directory": "./fonts/","names": ["courier_new_bold_16"]},{"kind": "texture","directory": "./textures/","names": ["white"]}]}samples/linesweeper/content/manifest.json at
862e08b -
Make sure the build copies it beside the executable (next section).
-
In code, refer to it by that name once, where you build the thing that draws it. The Sprites and text guide covers the drawing.
If the name in code does not match the manifest, the game stops with an error naming the asset when it resolves the name. If the file is missing, it stops at load, naming the file.
Getting content beside the executable
Section titled “Getting content beside the executable”The game reads its manifest from the folder the executable is in, wherever it was started from, so the build copies the content there. The minimal sample does it with a custom target that runs on every build, so an edited file arrives even when no code changed:
add_custom_target(sample_content ALL)add_custom_command(TARGET sample_content POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/content/manifest.json" "$<TARGET_FILE_DIR:MinimalSample>/manifest.json" VERBATIM)add_custom_command(TARGET sample_content POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory_if_different "${CMAKE_CURRENT_SOURCE_DIR}/content/fonts" "$<TARGET_FILE_DIR:MinimalSample>/fonts" VERBATIM)add_dependencies(MinimalSample sample_content)samples/minimal/CMakeLists.txt, lines 26–39 at 862e08b
When you add a folder of content, add a copy_directory_if_different line for it beside the
fonts one. LineSweeper copies fonts and textures this way.
Reading JSON
Section titled “Reading JSON”Content is the one input a person types by hand, so the engine reads JSON through a checked
wrapper, engine/assets/json.h. Every accessor checks that the key is there and has the right
type, and if not, throws an error that names the file, where in it, and the key:
JsonDocument read_json_file(const char* path); // throws naming the path if it cannot be read or parsed
// On a JsonValue:JsonValue array(const char* key) const;std::string string(const char* key) const;int integer(const char* key) const;float number(const char* key) const;bool boolean(const char* key) const;bool has(const char* key) const; // the one way to make a key optionalsize_t size() const; // for an arrayJsonValue at(size_t index) const; // for an arraystd::string where() const; // e.g. 'levels/forest.json': spawns[2]A JsonValue is a view into its JsonDocument, valid only while the document is alive, so read
what you need into your own types before the document goes out of scope. Use where() in your
own errors when a value is the right type but not an acceptable one, such as an unknown enemy
name.
A kind of asset of your own
Section titled “A kind of asset of your own”The engine knows four kinds: texture, font, sprite_sheet and sound_bank. A game teaches it
more (levels, dialogue, enemy definitions) by registering a loader for each kind before it
loads the manifest:
struct AssetKind{ LoadAsset load; // void(const std::string& directory, const std::string& name, bool optional) LoadAsset reload_device; // the same, after a graphics device is restored; nullptr if not on the GPU};load is called once for each name in each group of that kind, with the group’s directory
already resolved relative to the manifest. Content that does not live on the graphics card leaves
reload_device empty. This is a level kind that reads one JSON file per level into a
Registry the game owns:
#include "engine/assets/json.h"#include "engine/assets/resource_loader.h"#include "engine/core/registry.h"
#include <memory>#include <string>
struct Level{ std::string title; int width = 0; int height = 0;};
{ loader.register_kind("level", { [&levels](const std::string& directory, const std::string& name, bool /*optional*/) {
std::unique_ptr<Level> level = std::make_unique<Level>(); level->title = root.string("title"); level->width = root.integer("width"); level->height = root.integer("height"); levels.add(name, std::move(level)); }, nullptr });}The game creates the registry before the Application, so that it outlives the loader, and
calls register_levels(*app.resource_loader(), levels) before app.load_manifest. The manifest
then lists levels like any other content:
{ "kind": "level", "directory": "./levels/", "names": [ "forest", "caves" ] }and a state that is handed the registry resolves a level once and reads it by handle:
const Level& level = *levels->get(forest);A misspelt kind in the manifest stops the game naming the kind; a misspelt level name throws naming the level, on the line that resolves it.
Development documentation (unreleased). Built from Labrador 862e08b of 2026-10-08.