C++ Store
Write game values from C++ and respond to committed changes.
Use calmui::CalmUiStore from Store.h after loading your package. The example
below assumes store is the loaded Store and field is a valid boolean field ID
from your generated schema. Check writes before committing:
Object members are checked against their declared schema, including nested objects inside keyed array rows. An object container alone does not make an undeclared member or a wrong nested scalar type valid.
if (store.BeginUpdate()) {
if (!store.SetBool(field, true)) {
store.Rollback();
} else {
const bool saved = store.Commit();
// If saved is false, inspect store.LastError().
}
}Also: SetInt, SetFloat, SetString, SetObjectField, ReplaceArray, keyed
insert/update/remove/move, and Read. Whole objects use SetObject. Typed reads
are ReadBool, ReadInt, ReadFloat, ReadString, ReadObject, and
ReadArray.
Use SubscribeScoped to watch selected fields and automatically unsubscribe
when its owner is destroyed,
SubscribeAllScoped for every committed batch, or Subscribe, SubscribeAll,
UpdateDependencies, and Unsubscribe for explicit lifetime management. Each
patch includes its kind, field/member IDs, key, old/new operation data, and old/new
index. Equal values do not produce a patch: republishing an unchanged array or
object every tick costs one comparison and notifies nobody.
Writes are cheap enough to publish whole rows. Member validation uses tables the
Store builds once from the schema, field lookups are binary searches, and a
patch's previous value is moved out of the committed state at Commit instead
of being copied. A 64-row keyed ReplaceArray with 50 members per row against
a 1,550-field schema takes about 0.5 ms (Release, CalmUiStoreTests).
UCalmUiLocalPlayerSubsystem::GetLocalStore() exposes the player Store to native
game code. UCalmUiEngineSubsystem::GetGlobalStore() exposes the process-wide
Store. Both must be mutated on the Unreal Game Thread; code running elsewhere
must queue its transaction with AsyncTask(ENamedThreads::GameThread, ...).
Reconfigure preserves compatible fields. Removing a field or changing its
type, scope, member status, or array key is breaking and is rejected unless the
new schema version is higher. A versioned migration keeps compatible values and
drops only incompatible ones.
Explicit save/load code can use SaveSnapshot and RestoreSnapshot. Snapshots
store sorted field IDs plus their typed values and carry the schema version and
hash. Restore is transactional: incompatible values roll back the entire
operation with LastError, while a newer version may omit fields removed by a
versioned migration. Persist the resulting engine-neutral structure in the
game's own SaveGame format; serialization policy remains game-owned.
Use the Unreal player Store
This example assumes a loaded package defining Player.name at player.name
and action MainMenu.NewGame, with the matching generated header available to
your module. Replace the generated names with your project's names. Include
CalmUiLocalPlayerSubsystem.h and use a valid local Player Controller.
The callback shows where to implement your game's new-game behavior:
#include <CalmUiSchema.generated.h>
#include <calmui/Actions.h>
#include <calmui/Store.h>
UCalmUiLocalPlayerSubsystem* ui =
PlayerController->GetLocalPlayer()->GetSubsystem<UCalmUiLocalPlayerSubsystem>();
calmui::CalmUiStore* store = ui->GetLocalStore();
calmui::CalmUiStoreTransaction update(*store);
if (!store->SetString(CalmUiFields::Player_name, "Alex")) {
return; // The transaction cancels its pending changes.
}
const bool saved = update.Commit();
if (!saved) return;
const calmui::ActionRegistrationId action = ui->GetActionDispatcher()->Register(
CalmUiActions::MainMenu_NewGame,
[](const calmui::Value&, const calmui::ActionContext& context) {
// Add the game's new-game logic here. Returning true alone does not start a game.
// context.LocalPlayer identifies the requesting player.
return true;
});Keep the registration handle and unregister it during teardown. Global fields
come from UCalmUiEngineSubsystem::GetGlobalStore(); do not write them through
the per-player Store.