libultraship (LUS)

repository·main·Indexed 43 days ago

https://github.com/kenix3/libultraship

A library providing reimplementations of Nintendo 64 libultra SDK functions for modern hardware, enabling N64 games to run on PC and other platforms. It includes a Fast3D renderer supporting OpenGL, Direct3D 11, and Metal, as well as an asset loading system compatible with .otr and .o2r archive formats. LUS acts as a header-compatible wrapper to facilitate porting original game source code from decompilations to modern systems.

Tokens
8.4K
Snippets
24
Records
46
Agent score
57%

What's inside libultraship

  1. Overview of the libultraship Scripting System

    main

    The scripting system allows mods to be packaged as .o2r files (standard ZIP archives) containing game assets, a manifest.json metadata file, and either C source files for JIT-compilation or pre-compiled shared libraries.

    When a mod is loaded, the engine uses TCC (Tiny C Compiler) to JIT-compile C sources or loads the appropriate platform-specific binary. The lifecycle is managed through ModInit and ModExit entry points.

  2. Configure Fast3D rendering and windowing

    main

    The Fast3D renderer supports multiple graphics APIs and windowing systems depending on your target platform:

    Supported Graphics APIs:

    • OpenGL
    • Direct3D 11
    • Metal

    Supported Windowing Systems:

    • DXGI (primarily used on Windows)
    • SDL (generic/cross-platform)

    For low-level command definitions, refer to interpreter.h and the GBI headers located in include/libultraship/libultra/gbi.h.

  3. How the Event System works

    main

    The event system allows mods to hook into engine and game loop lifecycle points without modifying source code.

    1. Definition: Use DEFINE_EVENT to create a struct type and a global EventID.
    2. Registration: Every event must be registered once at engine startup using REGISTER_EVENT before it can be used.
    3. Dispatching: Use CALL_EVENT to fire an event. If the event is cancellable, use CALL_CANCELLABLE_EVENT.
    4. Listening: Use REGISTER_LISTENER to attach a callback function to an event.

    Important: Always store the ListenerID returned by REGISTER_LISTENER and call UNREGISTER_LISTENER in your MOD_EXIT function to prevent use-after-free errors when the mod is unloaded.

    // Define an event
    DEFINE_EVENT(MyDataEvent, int someValue; float otherValue;);
    
    // Register it at startup
    REGISTER_EVENT(MyDataEvent);
    
    // Dispatch it
    CALL_EVENT(MyDataEvent, .someValue = 42, .otherValue = 3.14f);
  4. Understand the C Mod Build Pipeline (Amalgamation)

    main

    Because mods are JIT-compiled by TCC at runtime, they cannot use separate object files. They must be packaged as self-contained C translation units.

    1. Amalgamation: The build system uses tools/merge.py to recursively inline all local #include directives into a single .c file per source file.
    2. build.gen: A plain text file listing the paths to these amalgamated .c files (one per line). This file is referenced by the main field in manifest.json.
    3. Namespace Warning: All source files share a single global namespace when loaded. Any helper function that should not be visible outside your file must be declared static to avoid symbol collisions with the engine or other mods.
    # build.gen example
    dist/main.c
    dist/utils.c
  5. Handle cancellable events

    main

    Events fired with CALL_CANCELLABLE_EVENT or CALL_CANCELLABLE_RETURN_EVENT can be stopped by listeners. To cancel an event, set the Cancelled flag on the base IEvent struct.

    When an event is cancelled:

    • Remaining lower-priority listeners still receive the event but can check event->Cancelled.
    • The engine code block gated by CALL_CANCELLABLE_EVENT is skipped.
    • CALL_CANCELLABLE_RETURN_EVENT causes an immediate return in the calling function.
    void OnMyEvent(IEvent* event) {
        MyDataEvent* e = (MyDataEvent*)event;
        if (/* some condition */) {
            e->Event.Cancelled = true;
        }
    }
  6. Choose a context creation model for Epoch 2

    main

    Epoch 2 initialization is centered around ship/core components. You must choose one of two setup styles based on your port's requirements:

    1. CreateDefaultInstance(...) (Helper-driven)

    Use this for the fastest path to a running baseline. LUS handles the standard component graph and ordering internally.

    • Best for: Simple ports or quick baselines.
    • Trade-offs: Less control over concrete component construction and subsystem composition.

    Use this when you need explicit control over startup, dependencies, and injection.

    • Best for: Most ports, especially those with custom window, input, resource, or event behavior.
    • Pros: Full ownership of component add-order, easier to inject custom implementations, and better long-term maintainability.
    • Cons: Requires more boilerplate and manual enforcement of dependency sequencing.
  7. Archive Format for .o2r Mods

    main

    An .o2r file is a ZIP archive. The engine specifically looks for manifest.json and build.gen at the root. The internal structure typically looks like this:

    my-mod.o2r
    ├── manifest.json          # Required: mod metadata
    ├── build.gen              # Optional: list of C source files to JIT-compile
    ├── dist/
    │   └── mod.c              # Amalgamated C source (referenced by build.gen)
    ├── binaries/
    │   ├── linux_x64.so       # Optional pre-compiled binaries
    │   ├── windows_x64.dll
    │   └── darwin.dylib
    └── <game assets ...>      # Other resource files
    my-mod.o2r
    ├── manifest.json          # Required: mod metadata
    ├── build.gen              # Optional: list of C source files to JIT-compile
    ├── dist/
    │   └── mod.c              # Amalgamated C source (referenced by build.gen)
    ├── binaries/
    │   ├── linux_x64.so       # Optional pre-compiled binaries per platform
    │   ├── windows_x64.dll
    │   └── darwin.dylib
    └── <game assets ...>      # Any other resource files the mod needs
  8. How libultraship asset loading works

    main

    libultraship (LUS) uses an asset loading system that separates data from the executable to facilitate modding. Data is stored in archive files that the system automatically detects and uses to replace original data.

    Supported archive formats:

    • .otr files: Compatible with the .mpq format.
    • .o2r files: Compatible with the .zip format.

    To apply changes or mods, you simply need to supply a patch .otr or .o2r file.

  9. Signing and Integrity Verification

    main

    Archive validation occurs in two stages:

    1. Checksum verification: A BLAKE2b-512 digest is computed over all non-manifest files (sorted by path). This is compared against the checksum field in manifest.json.
    2. Signature verification: The raw BLAKE2b digest is verified against the Ed25519 signature using the public_key from the manifest. The engine checks this key against its internal Keystore.

    If a key is untrusted, the host application can prompt the user to trust it. System keys are always accepted.

    Note: An archive without a checksum field is treated as unsigned. It can still be loaded depending on the configured SafeLevel.

  10. Understand libultraship epoch semantic versioning

    main

    libultraship (LUS) uses epoch semantic versioning to manage large-scale architectural refactors. This system uses a four-part version number: epoch.major.minor.patch (e.g., 1.2.3.4 or 2.0.0.0).

    How to interpret version numbers

    • New Epoch (X.y.z.w): When the first digit (the epoch) increments, it signals a major architectural overhaul (e.g., redesigning the context/component model). The main branch moves to this new epoch, meaning it may not be production-ready and commits may change during the refactor.
    • Stable Epoch: The previous epoch remains a maintained branch. It receives critical fixes and allows for small breaking changes (which increment the major digit within that epoch, e.g., 1.23.0.1). This provides a stable baseline for ports that are not yet ready to migrate to the new architecture.

    When a new epoch is triggered

    • Changes to core architectural patterns (e.g., context/component model redesign).
    • Coordinated breaking changes across multiple subsystems.
    • Refactors expected to span several months of incremental work.
  11. Identify the libultraship versioned API scope

    main

    The scope of the versioned API is defined by the epoch. For the current epoch, the versioning scope includes everything that is public within the Ship namespace.

    This definition replaces the previous rule where every C linkage function, variable, struct, class, public class method, or enum included from libultraship.h was considered part of the versioned API.