Skip to content
On this page

Mod

ll/api/mod/ · Common

Overview

The Mod module manages mod lifecycle, metadata (manifest), directory structure, and provides access to mod-specific resources like loggers, config directories, and data directories.

Headers

HeaderDescription
ll/api/mod/Mod.hMod instance and lifecycle
ll/api/mod/Manifest.hMod metadata structure
ll/api/mod/ModManager.hBase mod manager
ll/api/mod/ModManagerRegistry.hRegistry of all mod managers
ll/api/mod/NativeMod.hNative (DLL) mod implementation

Key Classes

Mod

Represents a loaded mod with lifecycle callbacks and resource access.

C++
namespace ll::mod {
class Mod {
public:
    enum class State { Enabled, Disabled };

    State getState() const;
    Manifest const& getManifest() const;
    std::string const& getName() const;
    std::string const& getType() const;

    std::filesystem::path const& getModDir() const;
    std::filesystem::path const& getDataDir() const;
    std::filesystem::path const& getConfigDir() const;
    std::optional<std::filesystem::path> getWorldDataDir() const;
    std::optional<std::filesystem::path> getWorldConfigDir() const;
    std::filesystem::path const& getLangDir() const;
    std::filesystem::path const& getResourceDir() const;
    std::filesystem::path const& getBehaviorDir() const;

    io::Logger& getLogger() const;

    bool isEnabled() const;
    bool isDisabled() const;

    void onLoad(CallbackFn func) noexcept;
    void onUnload(CallbackFn func) noexcept;
    void onEnable(CallbackFn func) noexcept;
    void onDisable(CallbackFn func) noexcept;
};

std::filesystem::path const& getModsRoot();
}

Manifest

Mod metadata loaded from manifest.json.

C++
namespace ll::mod {
struct Dependency {
    std::string                             name;
    std::optional<data::VersionRequirement> version;
};

struct Manifest {
    std::string                                entry;
    std::string                                name;
    std::string                                type;
    std::optional<std::string>                 platform;
    std::optional<bool>                        passive;
    std::optional<data::Version>               version;
    std::optional<std::string>                 author;
    std::optional<std::string>                 description;
    std::optional<SmallStringMap<std::string>> extraInfo;
    std::optional<SmallDenseSet<Dependency>>   dependencies;
    std::optional<SmallDenseSet<Dependency>>   optionalDependencies;
    std::optional<SmallDenseSet<Dependency>>   conflicts;
    std::optional<SmallDenseSet<Dependency>>   loadBefore;
};
}

Dependency entries keep the existing manifest JSON shape:

JSON
{
  "dependencies": [
    { "name": "RequiredMod", "version": "^1.2.3" }
  ],
  "optionalDependencies": [
    { "name": "OptionalIntegration", "version": ">=2.0.0 <3.0.0" }
  ],
  "loadBefore": [
    { "name": "LateInitializer" }
  ]
}

Omitting a dependency's version accepts any declared version, including an unversioned target. If a range is present, the target must declare a matching version. See Data for the complete range syntax. A bare full version such as 1.2.3 temporarily keeps the legacy meaning >=1.2.3 <2.0.0 and emits a migration warning; use =1.2.3 for an exact match.

Mod Types

The manifest's type field decides who loads the mod:

typeLoaded byWhen
nativeLeviLamina's native mod manager (NativeModManagerName)After LeviLamina has started, together with the other LeviLamina mods. Use this for ordinary C++ mods.
preload-nativePreLoaderBefore the game starts, before LeviLamina's own mods. LeviLamina itself uses this type. LeviLamina skips these manifests in its own mod registry, so they are not LeviLamina mods.

Any other type is loaded by the mod manager that was registered for that type through ModManagerRegistry (for example by a script engine). If no manager is registered for it, loading fails with "Unrecognized mod type".

For every type, setting passive to true means the mod is loaded only when another mod depends on it.

Dependency Semantics

Manifest fieldLoad orderingLoad failureDisable and unload protection
dependenciesDependency firstBlocks the dependentYes
optionalDependenciesMatching available dependency firstDoes not block the consumerYes, when bound at load time
loadBeforeDeclaring mod firstDoes not create a dependencyNo

loadBefore is only a startup and bulk-operation ordering constraint. It never prevents either mod from being disabled or unloaded independently. Dynamic loading does not reorder already loaded mods; if a new mod asks to load before an already loaded target, LeviLamina logs a warning and continues. Optional dependencies are bound only when their consumer loads, so loading an optional provider later does not retroactively add lifecycle protection.

Usage

Accessing Mod Instance

C++
#include "ll/api/mod/NativeMod.h"

ll::mod::NativeMod& getSelfMod() {
    return ll::mod::NativeMod::current();
}

Using Mod Resources

C++
#include "ll/api/mod/Mod.h"

void setupMod(ll::mod::Mod& mod) {
    auto& logger = mod.getLogger();
    logger.info("Mod name: {}", mod.getName());

    auto configPath = mod.getConfigDir() / "config.json";
    auto dataPath = mod.getDataDir() / "database.db";
    auto langPath = mod.getLangDir() / "en_US.json";
}

Lifecycle Callbacks

C++
#include "ll/api/mod/Mod.h"

void registerCallbacks(ll::mod::Mod& mod) {
    mod.onLoad([](ll::mod::Mod& self) -> bool {
        self.getLogger().info("Mod loading");
        return true;  // Return false to abort load
    });

    mod.onEnable([](ll::mod::Mod& self) -> bool {
        self.getLogger().info("Mod enabled");
        return true;
    });

    mod.onDisable([](ll::mod::Mod& self) -> bool {
        self.getLogger().info("Mod disabled");
        return true;
    });

    mod.onUnload([](ll::mod::Mod& self) -> bool {
        self.getLogger().info("Mod unloading");
        return true;
    });
}

Directory Structure

Mods live in the mods root (getModsRoot()): plugins/ on the server and mods/ on the client. (PreLoader uses plugins/ when that directory exists, otherwise mods/; modsPath in PreLoaderConfig.json overrides it.) Each mod has the following directory structure:

Text Only
plugins/                       # mods/ on the client
└── MyMod/
    ├── manifest.json          # Mod metadata
    ├── MyMod.dll              # Mod binary
    ├── config/                # Config files (getConfigDir())
    ├── data/                  # Persistent data (getDataDir())
    ├── lang/                  # Translations (getLangDir())
    ├── resource_packs/        # Resource packs (getResourceDir())
    └── behavior_packs/        # Behavior packs (getBehaviorDir())

worlds/
└── Bedrock level/             # The current world's folder
    ├── ll_config/
    │   └── MyMod/             # World-specific config (getWorldConfigDir())
    └── ll_data/
        └── MyMod/             # World-specific data (getWorldDataDir())

getWorldConfigDir() and getWorldDataDir() return std::nullopt while no world is loaded. On the client, when connected to a remote server, they are ll_config/<host>_<port>/MyMod/ and ll_data/<host>_<port>/MyMod/ in the game directory instead.

  • Config — Use getConfigDir() for config file location
  • Data — Use getDataDir() for database storage
  • I18n — Use getLangDir() for translation files
  • IO & Logger — getLogger() returns the mod's logger