Skip to content
1689 words 8.5 min read

Architecture Overview ​

NOTE

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

LOICollectionA is a C++ plugin (NativeMod) based on LeviLamina, with an overall microkernel architecture: the core only handles configuration loading, service registration, and module scheduling, while all features are implemented independently as modules, which can be toggled on demand in the configuration file.

Plugin Entry and Lifecycle ​

The plugin entry is located at src/LOICollectionA/LOICollectionA.cpp, registered via LL_REGISTER_MOD:

cpp
LL_REGISTER_MOD(LOICollection::A, LOICollection::A::getInstance());

A inherits ll::mod::NativeMod from LeviLamina, and the four stages drive all modules in sequence:

StageCore actionCalls to modules
loadCompute the config version → generate/merge/load config.json → register global services → initialize the database directory and language directoryCall module load() one by one
enableSet the default language based on ConsoleLanguage → compile LOICollectionAPI scripts →Call module registry() one by one
disable—Call module unregistry() one by one
unload—Call module unload() one by one

TIP

The four stages of a module are not an "on/off" relationship: load/unload handle the resource lifecycle (database, logs, paths), while registry/unregistry handle runtime registration (commands, events, UI). See Module Development Guide for details.

Module System ​

The module framework is located at src/LOICollectionA/include/ and contains three core classes:

ModuleBase (Module Base Class) ​

All modules inherit from LOICollection::modules::ModuleBase and need to implement six pure virtual methods:

cpp
class ModuleBase {
public:
    virtual std::string getName() = 0;                 // module name (registration name)
    virtual ModulePriority getPriority() = 0;          // module priority
    virtual ll::Expected<bool> load() = 0;             // resource loading
    virtual ll::Expected<bool> unload() = 0;           // resource release
    virtual ll::Expected<bool> registry() = 0;         // runtime registration
    virtual ll::Expected<bool> unregistry() = 0;       // runtime unregistration
};

AutoRegister (Auto Registration) ​

A module only needs to inherit from modules::AutoRegister<Derived>; its static initialization automatically calls ModManager::registry(getShared(), name, priority) at program startup, with no manual registration needed:

cpp
class BlacklistPlugin : public std::enable_shared_from_this<BlacklistPlugin>,
                        public modules::ModuleBase,
                        public modules::AutoRegister<BlacklistPlugin> {
    // ...
};

ModManager (Module Manager) ​

MethodDescription
registry(shared_ptr, name, priority)Register a module; priority defaults to Normal
unregistry(name)Unregister a module
getModule(name)Get a module instance by name
mods()Get the list of all module names (sorted by priority)

The ModulePriority enum: Highest (0), High (1), Normal (2), Low (3), Lowest (4). Priority affects the module load/registration order.

Service Container and Dependency Injection ​

Modules do not depend on each other directly; instead, they register and retrieve services by type + name through ServiceProvider / ServiceContainer:

cpp
// Register
ServiceProvider::getInstance().registerInstance<TService>(instance, name);

// Retrieve
auto svc = ServiceProvider::getInstance().getService<TService>(name);

Global services registered at plugin startup:

TypeNameDescription
ReadOnlyWrapper<Config::C_Config>"Config"Read-only configuration (see below)
std::string"DataPath"Plugin data directory (plugins/LOICollectionA/data)
std::string"GuiPath"GUI directory (plugins/LOICollectionA/gui)
std::string"ConfigPath"Configuration directory (plugins/LOICollectionA/config)
BlockRepository"SettingsDB"Global settings database (data/settings.db, block storage)

NOTE

The configuration is registered as ReadOnlyWrapper<Config::C_Config>, so modules can only read the configuration and cannot modify it. This is an intentional design: the configuration is only read once at startup, and modifying it at runtime requires restarting the server.

Configuration System ​

  • The configuration structure is defined in src/LOICollectionA/ConfigPlugin.h, with 105 configuration keys in total (nested structure C_Config → C_ServerConfig → C_ServerPlugins / C_ServerProtableTool)
  • The configuration version is generated by hashing the plugin version number (SynchronousPluginConfigVersion)
  • When upgrading the plugin, the default configuration in code is recursively merged into the existing config.json via MergePatch: new keys are filled in automatically, while existing keys keep the user's values
  • See Data Files for detailed configuration item descriptions

Directory Structure ​

txt
src/LOICollectionA/
├─ LOICollectionA.cpp / .h   # plugin entry (A class)
├─ ConfigPlugin.h / .cpp     # configuration structure definition and loading
├─ base/                     # infrastructure: ServiceContainer, ServiceProvider,
│                            #   ReadOnlyWrapper, LRUKCache, Throttle, ScopeGuard
├─ data/                     # data layer: sqlite/ block storage (BlockStore + WriteBatch +
│                            #   BlockRepository), JsonStorage
├─ frontend/                 # LCUI script engine: Lexer, Parser, SemanticAnalyzer,
│   │                        #   AST, Callback (native binding registry), ir/ (compiler, VM)
│   ├─ sandbox/              # script sandbox: ScriptPermission (permission and execution
│   │                        #   budget configuration), SandboxBudget (fuel metering),
│   │                        #   SandboxReport; permissions use a trust-level + capability model
│   ├─ lsp/                  # LCUI language service core: Protocol (JSON-RPC framing),
│   │                        #   Analysis (diagnostics and symbols), LanguageServer (dispatch)
│   └─ builtin/              # built-in script implementations: Math/Format/String functions,
│                            #   mc/server commands, ui/ (form and Observable native classes)
├─ include/                  # public header files (distributed with the plugin after installation)
│   ├─ ModuleBase.h / ModManager.h / ModulePriority.h
│   ├─ CallbackUtils.h       # LOICollectionAPI variable registration entry
│   ├─ form/GUIManager.h     # GUI manager
│   ├─ server/Events/        # custom event types (see [Custom Events](./events.md))
│   └─ server/Plugins/       # public interfaces of each module (BlacklistPlugin.h, etc.)
├─ modules/                  # module implementations (client/ and server/ isolated by platform)
├─ utils/                    # utilities: I18nUtils, MathUtils, mc-server toolkit
tests/                       # gtest tests (common/ cross-platform, server/ server-only, client/ reserved and empty)

Dual-Platform Support ​

The plugin supports both server and client targets at the same time, distinguished via the target_type option in xmake:

  • Compile macros: LL_PLAT_S (server) / LL_PLAT_C (client)
  • Source isolation: modules/server/* and modules/client/* are compiled mutually exclusively by target
  • Header isolation: include/server/* and include/client/* are removed from the distribution manifest by target
  • See Build and Test for build methods

Data Layer ​

The data layer is organized into four levels bottom-up; business code only ever touches the topmost one:

txt
TypedTable<E, Version>        # compile-time columns + versioned schema (the only business entry)
        |  reads / writes / transactions
BlockRepository               # facade: open / fromStore / store() / meta* / exec
        |
BlockStore                    # block-model persistence (dict / block / prop / link / meta)
WriteBatch                    # atomic write transaction
        |
ConnectionPool               # SQLite connection pool (WAL mode)
LevelLocationResponsibility
ConnectionPooldata/sqlite/connection/SQLite connection management: WAL + synchronous=NORMAL + busy_timeout=5s
BlockStoredata/sqlite/block/Splits data into addressable blocks with incremental key-based access over five system tables: dict / block / prop / link / meta
WriteBatchsameFlushes a batch of changes atomically in one commit(), rollback() on failure
BlockRepositorysamePublic surface is only: open / fromStore / store() / metaGet|Set|Del / exec
TypedTablesameLifts columns to compile time (declared as an enum class); open writes version + type name + column count to schema:<table> and returns SchemaMismatch on disagreement

Every method returns ll::Expected<T> so errors compose via and_then / transform / or_else; no exceptions are used. See the "Data Layer" section of the Module Development Guide for usage, and the complete SQLite Layer Tutorial.

NOTE

The full data-layer API, schema definition, conditional queries, transactions, and error-code reference are in the SQLite Layer Tutorial.

WARNING

Data-layer constraint: a few statements such as VACUUM cannot run inside a transaction and must go through BlockRepository::exec() (autocommit), while CREATE / DROP TABLE / CREATE INDEX are transactional in SQLite and are safe to place in the same WriteBatch as your DML. Never call repo->exec() while a transaction is open—it takes a second connection and collides with the write lock your transaction holds. Reads inside a transaction reuse the transaction's own connection and never request another one. See section 9 of the tutorial.

Database Files ​

FileOwnerContents
data/settings.dbglobalregistered as the SettingsDB service; shared tables of Wallet, Language, PvP, Notice, Chat, Tpa, Market and Statistics live here
data/blacklist.dbBlacklistbanned words and ban list
data/mute.dbMutemute list
data/chat.dbChatchat logs and interception rules
data/market.dbMarketmarket / store / wanted / auction / quotes
data/statistics.dbStatisticsplayer statistics
data/tpa.dbTpateleport requests
data/behaviorevent.dbBehaviorEventbehavior event logs

Config, Notice (notice.json) and Cdk (cdk.json) do not use block storage; they still use JsonStorage and can be edited directly.

WARNING

BlockRepository::open performs no migration: it only creates the five block-model system tables inside the target SQLite file. Tables written by older versions are neither read nor converted. See Data Migration when upgrading across versions.

Scripting VM ​

LCUI scripts are compiled in frontend/ir/ into three-address MIR and executed directly by VM. Object access and field caching are designed around two ideas:

Object Model and Field Layout ​

Each Object holds an immutable FieldLayout (an ordered map from field names to slot indices). Class fields known at compile/construction time occupy contiguous slots, so slotOf(name) is O(1). Dynamically written fields fall back to a spill hash map, keeping undeclared fields accessible.

Polymorphic Inline Cache (PIC) ​

VM::mFieldSlots was upgraded from monomorphic (a single name+layout+slot) to a polymorphic inline cache of capacity 4: the field name is constant per instruction and up to 4 candidate slots are cached keyed by layout pointer. Same-shape objects hit directly; heterogeneous call sites degrade to sequential probing; beyond 4 shapes it falls back to a linear lookup. This accelerates hot same-shape paths without sacrificing correctness at heterogeneous sites.

Hidden Class / Shape Transitions ​

extendShape(base, name) memoizes an immutable extended FieldLayout keyed by (base, name), so objects of the same kind share one shape. Object::addField() migrates dynamically added fields into real slots (instead of per-object spill), letting objects with the same dynamic shape converge to one layout and thus stabilizing the PIC across objects. Existing fields are written in place, preserving semantics.

NOTE

The shape-transition table and caches follow the project's existing single-threaded (tick model) assumption, consistent with ClassCall's epoch cache; no locks are introduced, keeping it simple and fast.

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