Skip to content
1660 words 8.5 min read

LOICollectionAPI Extension Guide ​

NOTE

The following content is derived from the code structure of LOICollectionA 1.17.1 and may differ in later versions.

This article explains how to extend LOICollectionAPI with C++ — that is, registering custom variables, functions, and classes for LCUI scripts and string templates (the {variable} syntax).

TIP

If you are a script user (using LOICollectionAPI in .lcui or configuration strings), please read LOICollectionAPI and LCUI Script Syntax. This article is intended for C++ developers who wish to extend these capabilities.

Type System ​

Value types exchanged between scripts and C++:

cpp
using TypedValue = std::variant<int, float, std::string, bool, ObjectRef, FunctionRefPtr, ArrayRef, std::monostate>;

enum class ParamType { INT, FLOAT, STRING, BOOL, OBJECT, FUNCTION, ARRAY };

using CallbackTypeArgs   = std::vector<ParamType>;          // parameter type signature
using CallbackTypeValues = std::vector<TypedValue>;         // actual argument values
using CallbackTypePlaces = std::unordered_map<int, std::any>; // context placeholders (e.g. the current player)
  • TypedValue is a std::variant; use std::get<T> or std::visit to read it
  • std::monostate represents "empty" (None in scripts)
  • CallbackTypePlaces carries the script execution context; index 0 is currently defined as std::reference_wrapper<Player> (the current player)

Registering Variables (String Template {xxx}) ​

LOICollectionAPI::CallbackUtils::registerVariable has 4 overloads, corresponding to the four usages in scripts:

OverloadScript UsageDescription
registerVariable(name, f()){name}Parameterless variable, e.g. {server_tps}
registerVariable(name, f(Player&)){name}Player-context variable, e.g. {player}
registerVariable(name, f(CallbackTypeValues), args){name(args)}Variable with parameters, e.g. {tr(languageId)}
registerVariable(name, f(Player&, CallbackTypeValues), args){name(args)}Player context + parameters

NOTE

When a variable is not registered, getValueForVariable returns the string "None". Registering a variable also automatically registers a macro with the same name (MacroCall), so both {name} and name() can be used in scripts.

Example (see modules/CallbackUtils.cpp):

cpp
#include "LOICollectionA/include/CallbackUtils.h"

// parameterless variable
CallbackUtils::getInstance().registerVariable("server_tps", []() -> ll::Expected<frontend::TypedValue> {
    return ServerStatus::getInstance().getTps();   // returns int/float/string, etc.
});

// player variable
CallbackUtils::getInstance().registerVariable("player_realname",
    [](Player& player) -> ll::Expected<frontend::TypedValue> {
        return player.getRealName();
    });

// variable with parameters: {score(ScoreboardName)}
CallbackUtils::getInstance().registerVariable("score",
    [](const frontend::CallbackTypeValues& args) -> ll::Expected<frontend::TypedValue> {
        const auto& name = std::get<std::string>(args[0]);
        return getScore(name);
    }, { frontend::ParamType::STRING });

Registering Functions (namespace::function) ​

frontend::FunctionCall::registerFunction registers namespace functions; the same signature can be overloaded:

cpp
#include "LOICollectionA/frontend/Callback.h"
using namespace LOICollection::frontend;

ll::Expected<TypedValue> myAbs(const CallbackTypeValues& args) {
    return std::visit([](auto&& arg) -> ll::Expected<TypedValue> {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, int> || std::is_same_v<T, float>)
            return std::abs(arg);
        return ll::makeStringError("my::abs: requires a numeric argument");
    }, args[0]);
}

void registerMyFunctions(const std::string& namespaces) {
    FunctionCall& functions = FunctionCall::getInstance();
    functions.registerFunction(namespaces, "abs", myAbs, { ParamType::INT });
    functions.registerFunction(namespaces, "abs", myAbs, { ParamType::FLOAT });
}

TIP

See the real implementations: how functions such as math::abs and math::min are registered in frontend/builtin/MathBuiltin.cpp; frontend/builtin/mc/server/CommandBuiltin.cpp demonstrates server functions such as mc::runCmd.

Combination Variant ​

When you need to access the script context (the player), use CallbackFuncCombination (with an extra CallbackTypePlaces parameter) and the corresponding registerFunction overload:

cpp
functions.registerFunction(namespaces, "runCmd", myRunCmd, { ParamType::STRING });
// myRunCmd: ll::Expected<TypedValue>(const CallbackTypeValues&, const CallbackTypePlaces& placeholders)
// get the current player: std::any_cast<std::reference_wrapper<Player>>(placeholders.at(0))

Registering Macros (name(args)) ​

frontend::MacroCall::registerMacro registers macros that can be called directly in expressions and string templates. It is usually not needed directly — registerVariable automatically registers a macro with the same name. Use it when you need custom argument validation or a macro independent of variables:

cpp
MacroCall::getInstance().registerMacro("entity",
    [](const CallbackTypeValues& args) -> ll::Expected<TypedValue> {
        return countEntities(std::get<std::string>(args[0]));
    }, { ParamType::STRING });

Registering Native Classes (new ClassName(...)) ​

frontend::ClassCall provides native classes to scripts. Take CustomForm as an example (see frontend/builtin/ui/form/CustomFormClass.cpp):

cpp
#include "LOICollectionA/frontend/Callback.h"
using namespace LOICollection::frontend;

// constructor: returns ObjectRef (native object handle)
ll::Expected<ObjectRef> makeMyClass(const CallbackTypeValues& args, const CallbackTypePlaces& placeholders) {
    auto handle = std::make_shared<MyClassHandle>();       // custom handle
    auto obj = std::make_shared<Object>();
    obj->className = "MyClass";
    obj->classIndex = -1;
    obj->native = handle;                                   // store the native handle in Object
    return obj;
}

// instance method: the first parameter is self (ObjectRef); retrieve the handle from native
ll::Expected<TypedValue> myMethod(const ObjectRef& self, const CallbackTypeValues& args) {
    auto* handle = static_cast<MyClassHandle*>(self->native.get());
    return handle->doSomething();
}

void registerMyClass(const std::string&) {
    ClassCall& classes = ClassCall::getInstance();
    classes.registerClass("MyClass", { "field1", "field2" });              // declare fields
    classes.registerConstructor("MyClass", makeMyClass, { ParamType::STRING });
    classes.registerMethod("MyClass", "myMethod", myMethod, {});
    classes.registerStaticMethod("MyClass", "staticFn", myStatic, { ParamType::INT });
}
ClassCall methodDescription
registerClass(name, fields)Registers a class and its instance fields
registerConstructor(name, cb, args)Registers a constructor (multiple overloads allowed)
registerMethod(className, method, cb, args)Registers an instance method (multiple overloads allowed)
registerStaticMethod(className, method, cb, args)Registers a static method
registerField(className, field, defaultValue?)Appends a field (with optional default value)
registerStaticField(className, field, defaultValue?)Appends a static field

NOTE

A native method returning self enables chained calls in scripts (such as form.label(...).button(...)). The Object structure contains className, classIndex, and native (std::any, which stores any native handle).

Registration Entry Point: the REGISTER_CALLBACK Macro ​

The macro at the end of frontend/Callback.h invokes registration functions during static initialization:

cpp
#define REGISTER_CALLBACK(NAME, BINDER) const auto NAME##_RegisterHelper = []() -> bool {     BINDER(#NAME);     return true; }();

// usage: the namespaces parameter will be passed "math"
REGISTER_CALLBACK(math, MathBuiltin::registerFunctions)
REGISTER_CALLBACK(CustomForm, CustomFormClass::registerClasses)

Therefore, registration functions should have the signature void(const std::string&), where the string parameter is the namespace name (for functions) or the class-name prefix (for classes).

Complete Example: Registering a Namespace with Variables ​

cpp
// MyBuiltin.cpp
#include <ll/api/Expected.h>
#include "LOICollectionA/frontend/Callback.h"
#include "LOICollectionA/include/CallbackUtils.h"

using namespace LOICollection::frontend;
using namespace LOICollection::LOICollectionAPI;

namespace MyBuiltin {
    ll::Expected<TypedValue> greet(const CallbackTypeValues&, const CallbackTypePlaces& places) {
        auto& player = std::any_cast<std::reference_wrapper<Player>>(places.at(0));
        return "Hello, " + player.getRealName();
    }

    void registerFunctions(const std::string& ns) {
        FunctionCall::getInstance().registerFunction(ns, "greet", greet, {});
    }
}

REGISTER_CALLBACK(my, MyBuiltin::registerFunctions)

After registration, scripts can use my::greet() and the {variable} syntax. After making changes, run /xxx reload or restart the server for the changes to take effect.

Correspondence with the Script-Side Documentation ​

This article (C++ registration)Script-side usage documentation
registerVariableDefault Variables and Functions
registerFunctionBuilt-in Namespaces and Functions
ClassCallNative UI Classes and Classes and Inheritance

LOICollectionA · A ready-to-use multifunctional plugin set for LeviLamina