Player preferences: a complete database example#
What this example teaches#
You will create a table, save one record, change two fields, read the record and delete that same sample. These are create, read, update and delete operations, commonly abbreviated CRUD.
This is deliberately a disposable server-side demo. It runs once when the example bundle starts. It does not listen for player events, ban players, manage a leaderboard or install a gameplay feature. For a real preferences feature, call equivalent save/load methods from your own server-side player event handling and remove the sample cleanup step.
Requirements and scope#
- The connector is installed and
lt-db-healthsucceeds. - The connector and API key both allow
documentation_demo. - You are using a development database. The demo creates
preferencesin that namespace and overwrites/removes the row whoseplayer_idissample_player. Do not use that row for real data. - Use a source checkout to build the example, with .NET 10 and the bundle build prerequisites from Build.
Run from the lt-database source checkout:
dotnet build examples/consumer/consumer.csproj -c Release
The generated file is examples/consumer/bin/Release/net10.0/wasi-wasm/native/consumer.wasm. To run it:
- Create
<server>/data/ugc/preferences_demo/. - Copy that
consumer.wasminto the new folder. - Copy
examples/consumer/config.tomlinto the same folder last. It namesconsumer.wasmasserver_blob; do not reuse the connector manifest. - In the BlackICE server console run:
start lt_database
start preferences_demo
The consumer is separate from the connector: never replace the connector's sv.wasm with it. To run the demo again use restart preferences_demo.
Data model#
| Column | Type | Purpose |
|---|---|---|
player_id |
Text, up to 64 characters, primary key | Identifies one sample/player |
music_volume |
32-bit integer | Example numeric preference; demo changes 25 to 40 |
visits |
32-bit integer | Example counter; demo changes 0 to 1 |
The five steps#
- Create the schema.
Database.Syncdeclares the table. Wait for success before using it. Repeating the unchanged schema declaration does not recreate the table. - Save.
Upsertuses the uniqueplayer_idkey. For this disposable sample, it resets volume to 25 and visits to 0 on every run. - Change.
setJsonreplaces volume with 40;incJsonadds 1 to visits. An increment is not idempotent: running the change twice adds twice. - Read.
Findselects the sample player. A failed request is logged separately from a successful query that found no row. - Clean up.
Deletefilters on the fixed sample key and removes only that row. It leaves the table in place.
All writes use immediate: true so this demonstration also works when write-behind is enabled. Every callback checks errors and stops the sequence on failure. The five calls are not a transaction; a failure can leave the sample row behind, and you can rerun the demo to reset it.
Complete compiled source#
The following file is compiled by the example project in CI. Schema, Filters, Query and Database are readable C# aliases for the library's S, F, Q and Db classes.
using LtDatabase;
using Database = LtDatabaseClient.Db;
using Schema = LtDatabase.S;
using Filters = LtDatabase.F;
using Query = LtDatabase.Q;
namespace Examples;
/// <summary>A disposable server-side CRUD demonstration, not a player event integration.</summary>
public static class PlayerPreferences
{
// Allow this namespace in both the connector and the API key before running.
const string Namespace = "documentation_demo";
const string Table = "preferences";
const string Player = "sample_player";
public static void Run()
{
var schema = Schema.Schema(Schema.Table(Table, ["player_id"],
[Schema.Text("player_id", 64), Schema.Int32("music_volume"), Schema.Int32("visits")]));
Database.Sync(Namespace, schema, result =>
{
if (!Succeeded("Create schema", result)) return;
Save();
});
}
static void Save()
{
var row = Query.Row(("player_id", Player), ("music_volume", 25), ("visits", 0));
Database.Upsert(Namespace, Table, row, ["player_id"], result =>
{
if (!Succeeded("Save preferences", result)) return;
Change();
}, immediate: true);
}
static void Change()
{
Database.Update(Namespace, Table, Filters.Eq("player_id", Player), result =>
{
if (!Succeeded("Change preferences", result)) return;
if (Database.Number(result) != 1)
{
Console.WriteLine("[preferences demo] Expected exactly one changed row; stopping.");
return;
}
Read();
}, setJson: Query.Row(("music_volume", 40)),
incJson: Query.Row(("visits", 1)), immediate: true);
}
static void Read()
{
Database.Find(Namespace, Table, result =>
{
if (!Succeeded("Read preferences", result)) return;
var rows = Database.Rows(result);
if (rows.Count != 1)
{
Console.WriteLine("[preferences demo] Sample row not found; stopping.");
return;
}
var row = rows[0];
Console.WriteLine($"[preferences demo] volume={row["music_volume"]}, visits={row["visits"]}");
RemoveSample();
}, filterJson: Filters.Eq("player_id", Player), limit: 1);
}
static void RemoveSample()
{
Database.Delete(Namespace, Table, Filters.Eq("player_id", Player), result =>
{
if (!Succeeded("Remove sample row", result)) return;
Console.WriteLine($"[preferences demo] Removed {Database.Number(result)} sample row(s). Demo complete.");
}, immediate: true);
}
static bool Succeeded(string step, DbResult result)
{
if (result.State == "ok" && !Database.IsQueued(result)) return true;
var message = Database.IsQueued(result)
? "Write is queued rather than applied; stopping this sequential demonstration."
: $"{result.Code}: {Database.ErrorMessage(result)}";
Console.WriteLine($"[preferences demo] {step} failed: {message}");
return false;
}
}
The example project's entry point is:
public static class App
{
public static void Start() => Examples.PlayerPreferences.Run();
}
Expected result#
[preferences demo] volume=40, visits=1
[preferences demo] Removed 1 sample row(s). Demo complete.
If setup fails, check namespace permissions, connector startup order and the API key. The log includes the failed step and error code. A successful run ends with no sample row; this is intentional cleanup, not lost data.
Adapt it to a real bundle#
Use your own namespace, table and stable player identifier. Declare the schema at startup, then save on the server event that changes a preference and load when it is needed. Keep a clear distinction between request failure and missing data. Decide whether a queued save is acceptable before telling the player it was persisted. See usage for result handling, filters and write-behind semantics.