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:
LL_REGISTER_MOD(LOICollection::A, LOICollection::A::getInstance());A inherits ll::mod::NativeMod from LeviLamina, and the four stages drive all modules in sequence:
| Stage | Core action | Calls to modules |
|---|---|---|
load | Compute the config version → generate/merge/load config.json → register global services → initialize the database directory and language directory | Call module load() one by one |
enable | Set 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:
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:
class BlacklistPlugin : public std::enable_shared_from_this<BlacklistPlugin>,
public modules::ModuleBase,
public modules::AutoRegister<BlacklistPlugin> {
// ...
};ModManager (Module Manager)
| Method | Description |
|---|---|
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:
// Register
ServiceProvider::getInstance().registerInstance<TService>(instance, name);
// Retrieve
auto svc = ServiceProvider::getInstance().getService<TService>(name);Global services registered at plugin startup:
| Type | Name | Description |
|---|---|---|
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 structureC_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.jsonviaMergePatch: new keys are filled in automatically, while existing keys keep the user's values - See Data Files for detailed configuration item descriptions
Directory Structure
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/*andmodules/client/*are compiled mutually exclusively by target - Header isolation:
include/server/*andinclude/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:
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)| Level | Location | Responsibility |
|---|---|---|
ConnectionPool | data/sqlite/connection/ | SQLite connection management: WAL + synchronous=NORMAL + busy_timeout=5s |
BlockStore | data/sqlite/block/ | Splits data into addressable blocks with incremental key-based access over five system tables: dict / block / prop / link / meta |
WriteBatch | same | Flushes a batch of changes atomically in one commit(), rollback() on failure |
BlockRepository | same | Public surface is only: open / fromStore / store() / metaGet|Set|Del / exec |
TypedTable | same | Lifts 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
| File | Owner | Contents |
|---|---|---|
data/settings.db | global | registered as the SettingsDB service; shared tables of Wallet, Language, PvP, Notice, Chat, Tpa, Market and Statistics live here |
data/blacklist.db | Blacklist | banned words and ban list |
data/mute.db | Mute | mute list |
data/chat.db | Chat | chat logs and interception rules |
data/market.db | Market | market / store / wanted / auction / quotes |
data/statistics.db | Statistics | player statistics |
data/tpa.db | Tpa | teleport requests |
data/behaviorevent.db | BehaviorEvent | behavior 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.