Skip to content

Advanced Usage

Ghost in a Nutshell

Welcome to the advanced documentation of Nutshell.

While the basic framework handles straightforward, single-window games, scaling a project requires mastering the underlying runtime environment.

Scripts & Assets

Script Modularization with require(path)

The require(path) utility allows you to modularize your code by loading and executing external scripts from within your current script.

This function is registered globally within the Squirrel root table of the active virtual machine. You can use it to keep your codebase clean, decoupled, and scaling beyond a single main.nut file:

main.nut
// Split game states out into dedicated sub-scripts
require("src/update.nut")
require("src/draw.nut")
src/update.nut
function update(dt) {
    // Process game logic and state changes here
}
src/draw.nut
function draw() {
    // Render visual elements here
    Font().draw(10, 10, "Hello Nutshell!")
}

Paths

When loading an image, loading an audio file, or requiring external scripts, file paths are resolved by Nutshell using specific rules.

Let's assume your project uses the following directory structure:

/path/to/my/game
├── main.nut
├── audio
│   ├── crunch.ogg
│   └── background-theme.ogg
├── images
│   ├── acorn.png
│   ├── forest.png
│   └── squirrel.png
└── src
    ├── draw.nut
    └── update.nut

Relative paths

By default, assets are resolved via a path relative to the script that executes the load call.

Relative Path Example

To reference acorn.png from inside src/update.nut, you must look up one directory level using ../images/acorn.png.

Using images/acorn.png would cause Nutshell to search for src/images/acorn.png, which does not exist.

Absolute paths

You can also use system-specific absolute paths to target files anywhere on the host machine.

Absolute Path Example

If you want to load a system font directly from a Linux directory to display localized Arabic text, you can specify its full absolute path:

local sampleFont = Font("/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf");
sampleFont.draw(10, 10, "مرحبا بالعالم")

Project Root & Resource Paths (res://)

The Project Root is explicitly defined as the directory containing your game's entry point, main.nut.

To reference files from anywhere in your codebase without writing complex relative paths, you can use a Resource Path. A resource path bypasses the current script's location and evaluates paths relative to the project root using the res:// prefix.

This approach is highly recommended because it prevents broken paths if you decide to reorganize or move scripts into different subfolders later.

Resource Path Example

To safely reference acorn.png inside src/update.nut, you can use res://images/acorn.png.

If you move update.nut to a different folder later, the asset reference remains perfectly valid and unbroken.

Dynamic Resolution: System.script_path()

While relative tracks and the res:// protocol handle the vast majority of asset loading use cases, you may occasionally need to inspect or dynamically manipulate paths at runtime—especially when writing reusable plugins, shared libraries, or advanced logging tools.

The System.script_path() static method returns a string containing the full filesystem path of the currently executing script file.

The simplest and most common use case for this is loading adjacent data configurations. If you separate your game logic into isolated modules (like an achievement manager or an enemy spawner), you often want that script's data files to live in the exact same folder so everything stays organized.

Loading Adjacent Data Files

Imagine you have an enemy setup script (src/entities/enemy.nut) and you want it to load a balance configuration file (src/entities/enemy_stats.json) that sits in the exact same directory:

// Inside src/entities/enemy.nut
local currentScript = System.script_path();

local lastSlashIdx = -1;
local searchIdx = currentScript.find("/");

// Keep finding the next slash until find() returns null
while (searchIdx != null) {
    lastSlashIdx = searchIdx;
    searchIdx = currentScript.find("/", searchIdx + 1);
}

if (lastSlashIdx != -1) {
    local folderPath = currentScript.slice(0, lastSlashIdx);
    local dataPath = folderPath + "/enemy_stats.json";

    print("Loading stats from: " + dataPath);
}

By using System.script_path(), you can safely move the entire entities folder anywhere else in your project tree later on, and the script will never break or lose track of its data file.

Fonts

Font paths follow usual path management.

However, you can use font aliases to safely request system or user-installed fonts without hardcoding physical filesystem tracks.

No fallbacks

Nevertheless, if you specify any path or alias for a font, the font must exist on the user's system. Otherwise, an exception will be thrown. The paths array handles character/glyph-level fallbacks, not file-missing fallbacks.

Embedded font

The only font guaranteed to always be available is the Bitstream Vera Sans embedded font.

Cross-platform font loading

This is highly recommended for cross-platform games, as absolute font paths vary wildly between operating systems (e.g., /usr/share/fonts/ on Linux vs C:\Windows\Fonts\ on Windows) and predicting font availability across systems is unreliable.

Here is a robust example on how to query platform-specific system fonts and safely fall back to the embedded asset if a file is missing or corrupted:

function loadPlatformFont(fontSize) {
    // Default to an empty string to gracefully target the embedded font
    local alias = ""

    switch (System.os) {
        case "windows":
            alias = "Arial"
            break
        case "linux":
            alias = "DejaVu Sans"
            break
        case "macos":
        case "darwin":
            alias = "Helvetica"
            break
    }

    try {
        // Attempt to load the preferred system font
        return Font(alias, fontSize)
    } catch (exception) {
        // If the font doesn't exist, load the embedded font
        return Font(fontSize)
    }
}
local sampleFont = loadPlatformFont(20)

Primary Fonts

When you provide an array of paths or aliases, the first font in the list is designated as the primary font.

Nutshell uses this primary font to calculate the overall line height, baseline, ascent, and descent for the text. Think of it as creating an invisible bounding box based entirely on the first font's design.

If the primary font is missing a character (like a Japanese Kanji or an Arabic glyph), Nutshell pulls that character's shape from one of the available fallbacks fonts. However, it forces that fallback character to fit inside the primary font's invisible bounding box.

Mixing standard and vector fonts

If your primary font is a compact pixel-art font (like m5x7) loaded at size 48, its bounding box is very small.

If your fallback font is a standard vector font (like Noto Sans), its characters at size 48 are physically much taller. Forcing the large vector glyphs into the tight pixel-font bounding box will cause the fallback text to appear vertically misaligned.

Multi face fonts best practices

Match Styles: Use fallback fonts that share similar vertical proportions. If your primary font is a tiny pixel font, use a dedicated pixel-art fallback font (like Unifont) for CJK characters.

Order Matters: If you place the larger vector font first, its generous bounding box easily accommodates smaller fallback fonts without clipping (though any characters present in both fonts will default to the first one).

Font Modifiers Syntax

If you use an alias, you can specify custom weight or style modifiers by appending a colon (:) followed by the style= keyword:

Family Name:style=Modifier

Font aliases and modifiers

Some examples:

  • Standard Font Alias: Liberation Sans
  • Explicit Style Modifier: DejaVu Sans:style=ExtraLight
  • Combined Modifiers: Liberation Sans:style=BoldItalic

Emoji Fonts

To maintain high rendering performance and cross-platform consistency, Nutshell handles emoji characters using pre-rendered emoji atlases rather than parsing complex vector color font formats directly at runtime.

An emoji atlas consists of a directory containing a JSON manifest and one or more packed PNG image sheets.

The Emoji Atlas Structure

When Nutshell looks up an emoji font, it scans the atlas directory for a manifest.json file alongside the texture pages.

You can organize your assets to support either a single explicit layout size or multiple size variants nested under subdirectories (e.g., 32/, 64/).

A valid atlas directory must contain the following components:

  • manifest.json: A metadata file specifying the cell size and mapping individual emoji characters to their exact pixel coordinates and page indices.

  • Texture Pages (1.png, 2.png, etc.): High-density sprite sheets containing the emoji glyphs.

The manifest.json format follows this structural schema:

{
  "size": 32,
  "emojis": {
    "😀": [0, 0, 1],
    "👍": [34, 0, 1],
    "🚀": [0, 34, 2]
  }
}

The array payload represents [x, y, page_number] respectively. Nutshell automatically normalizes these inputs and handles variation selectors (like \uFE0F) to ensure reliable character lookups.

Input

Keyboard Layouts

Nutshell handles keyboard inputs using a physical mapping architecture based on a standard US qwerty blueprint layout.

When a script checks for Input.KeyW, it does not care about the literal letter W. It evaluates the physical switch located on the keyboard grid.

Positional vs. Mnemonic Key Controls

When designing inputs, group your actions into two distinct categories:

  • Positional Controls (Movement): Use the raw Input.Key* constants directly.

    • If you use Input.KeyW for moving forward, a US user presses W and a French user presses Z. The physical hand posture remains identical across all layout variants globally.
  • Mnemonic Controls (UI Toggles, Hotkeys): Use Input.keychar_to_key() to dynamically resolve the keycap letter.

    • If pressing the letter M should open the Map, hardcoding Input.KeyM would place the map shortcut at an unexpected position on an azerty layout. Using Input.keychar_to_key("m") guarantees the letter M triggers the action regardless of where it lives on the user's keyboard.

Operating System Dead Keys (Linux / X11)

Certain international layout slots are treated as low-level hardware text composers by specific operating systems (for example, the ^ / ¨ circumflex key directly to the right of P on French azerty setups).

On Linux environments like Debian, the desktop display manager intercepts these dead keys to construct accent characters (like ê). Because this consumes the event before it can dispatch down to Nutshell, Nutshell cannot detect them as instant discrete button presses.

Gamepads

To handle local multiplayer and hot-plugging smoothly, Nutshell splits gamepad processing into a physical layer and a virtual gameplay layer.

Understanding the distinction between hardware indices, logical slots, and hardware offsets is useful for building custom lobbies or rebinding screens.

Hardware Index vs. Logical Slot

First, some notions:

  • Hardware count: Input.gamepad_count() returns the count of currently connected physical controller.
  • Hardware index (hw): The raw physical port identifier assigned sequentially as gamepads are connected or disconnected.
  • Logical slot count: Input.GamepadSlots returns the count of virtual slots available.
  • Logical slot (slot): A stable virtual input channel mapped to a specific gameplay character (e.g., Slot 0 for Player 1, Slot 1 for Player 2).

Gameplay logic should always query the logical slot rather than a raw hardware index.

You bridge these two layers using Input.gamepad_reassign_slot(hw, slot) to route a physical controller's data to a virtual slot.

The Hardware Offset

Standard input functions like Input.gamepad_button_pressed(btn, slot) expect a valid logical slot index.

When a physical controller is unassigned, querying its slot via Input.gamepad_slot(hw) returns -1.

To poll inputs directly from unassigned physical hardware, you must use a virtual hardware offset slot located at Input.GamepadSlots + hw.

Passing an index equal to or greater than Input.GamepadSlots signals Nutshell to bypass the logical mapping table completely and read the raw button state straight from that specific physical hardware port.

Ready player one?

This offset allows you to implement a standard "Press Start to Join" system by listening exclusively to unassigned controllers:

// Scan through all physical hardware ports
for (local hw = 0; hw < Input.gamepad_count(); hw++) {

    // Process only if this hardware device is currently unassigned (-1)
    if (Input.gamepad_slot(hw) == -1) {

        // Calculate the hardware bypass index
        local offset_slot = Input.GamepadSlots + hw;
        local pressed = false;

        // Listen for any button press on this specific unassigned controller
        for (local btn = 0; btn < Input.GamepadButtonCount; btn++) {
            if (Input.gamepad_button_pressed(btn, offset_slot)) {
                pressed = true;
                break;
            }
        }

        // Assign the physical device to the next available gameplay slot
        if (pressed && next_player_assignment_slot < Input.GamepadSlots) {
            if (Input.gamepad_reassign_slot(hw, next_player_assignment_slot)) {
                next_player_assignment_slot++;
            }
        }
    }
}

Gamepads vs. Raw Joysticks

Nutshell categorizes connected input hardware into two distinct runtime archetypes: Standard Gamepads and Raw Joysticks.

Standard modern gamepads (such as Xbox, PlayStation, or Nintendo Switch controllers) follow a predictable geometric shape. Because their physical layouts are almost identical, Nutshell automatically maps their inputs to semantic, layout-invariant constants like Input.GamepadButtonNorth (Y / Triangle) or Input.GamepadAxisLeftX.

Raw joysticks - such as flight simulation sticks, racing steering wheels, retro arcade decks, or unprofiled generic controllers - do not have a standardized layout. Instead of an ABXY diamond or dual thumbsticks, they report their inputs to the operating system as a flat sequence of integers: Axis 0, 1, 2... and Button 0, 1, 2....

Because a raw joystick's Button 0 is entirely hardware-dependent, querying it directly using standard gamepad constants would cause code conflicts (for example, the integer value for Input.GamepadButtonSouth is also 0).

Isolating your logic into two paths prevents your gamepad controls from misfiring when a raw joystick is bound.

Handling a generic raw joystick requires branching your code when the device type is identified, then passing raw integers into safety-wrapper functions to isolate their IDs.

  • Always use Input.is_joystick(slot) to determine if a connected slot requires a generic layout routing instead of a traditional standard layout.
  • Because you cannot guess how many buttons or axes an arbitrary flight stick or steering wheel possesses, query the device hardware capacities dynamically using Input.gamepad_axis_count(slot) and Input.gamepad_button_count(slot).
  • To safely look up inputs on a raw device without bleeding into standard gamepad maps, wrap your raw indices using Input.joystick_axis(axisIndex) and Input.joystick_button(buttonIndex). These generate shifted, unique identifiers safe to supply to standard polling functions.

Multi-Virtual Machine Architecture

By default, Nutshell initializes and runs a single virtual machine. However, you can spawn and run multiple independent virtual machines simultaneously using the System API.

Virtual Machine Initialization Rules

Every new virtual machine MUST be initialized with a valid target script path. These initialization scripts carry the same architectural requirements as your main entry point: they must define, at minimum, a global update(dt) function.

If a specific VM ID is requested during spawning, that ID MUST be unique in the current Nutshell application. Attempting to spawn a VM with an identifier that is already in use will result in a failure.

Inter-VM Communication

At the moment, you can't share data between two virtual machines.

The only actions you can take in a virtual machine are either to spawn or kill another virtual machine by its identifier.

Multi-Window Management

Nutshell supports multiple windows. You can spawn and configure multiple concurrent game windows using the Window API.

Contextual Limitations

A disclaimer though:

Windows are bound to their Parent VM

Nutshell windows are strictly bound to the specific virtual machine that spawned them.

A script running inside one virtual machine CANNOT access, manipulate, or draw to windows owned by a separate virtual machine.

Assets are bound to Windows

Image assets are cached and allocated explicitly per window context.

You CANNOT render an image or onto Window B if that asset resource was loaded while Window A was active. When loading assets, ensure the target window is actively set beforehand.

Active Window

An active window is where all current canvas drawing operations are actively directed.

  • To get the active window reference: Window.active().
  • To bind drawing operations to a specific window: window.activate().

Focused Window

A focused window is the one currently intercepting active OS hardware mouse, keyboard, or controller inputs.

  • To get the currently focused window: Window.focused_id().
  • To check if a window instance has the focus: window.focused().
  • To forcefully request focus for a specific window: window.focus().

Automatic Windows

By default, Nutshell automatically instantiates a new window with default dimensions whenever a new virtual machine is initialized.

You can pass the following command-line flags to the nutshell binary to alter or override this automatic window behavior:

Option Type Default Description
--title string nutshell Title string for automatic windows
--width int 640 Width for automatic windows
--height int 480 Height for automatic windows
--no-window bool false When set to true, disable the automatic creation of windows for new virtual machines.

Security Considerations

Script Execution Privileges

Nutshell does not currently sandbox script execution. Scripts have full access to the host filesystem via absolute paths and run with the same permissions as the compiled binary executable.

  • Players: Only run games or binaries from creators you trust.
  • Developers: Never pass unsanitized player input, chat commands, or remote network payloads into functions like require() or System.spawn_vm().