From 52dd5e8acba98fd9fd6a3902074a0f4d51c5e6c7 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 4 Nov 2026 16:11:05 +0000 Subject: [PATCH] docs: docs/start onboarding page with tested CultCache first calls in C# and TypeScript A language x package matrix (each cell a tested snippet or "not yet"), a C# first-call test and a TypeScript first-call test that the docs link, and a pointer from the top of README.md. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 + docs/start/README.md | 115 ++++++++++++++++++ packages/cultcache-ts/test/first-call.test.ts | 51 ++++++++ .../GameCult.Caching.Tests/FirstCallTests.cs | 54 ++++++++ 4 files changed, 222 insertions(+) create mode 100644 docs/start/README.md create mode 100644 packages/cultcache-ts/test/first-call.test.ts create mode 100644 tests/GameCult.Caching.Tests/FirstCallTests.cs diff --git a/README.md b/README.md index e2ea607d..6e6de55c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # CultLib +**New here? Start at [`docs/start`](docs/start/README.md)**: a language x package matrix with tested first-call snippets. + CultLib is GameCult's shared state, transport, and runtime substrate. It started as reusable C# game infrastructure, but the useful thing now is larger: typed `.cc` persistence, schema-stable wire formats, ergonomic reliable-UDP diff --git a/docs/start/README.md b/docs/start/README.md new file mode 100644 index 00000000..91447789 --- /dev/null +++ b/docs/start/README.md @@ -0,0 +1,115 @@ +# Start Here + +CultLib is one stack in several languages: **CultCache** (typed `.cc` persistence), +**CultNet** (schema-v0 wire messages and transports) on top of it, and **CultMesh** (Verse +discovery, replication, authority) on top of that. **CultMath** and **Geometry** sit beside the +stack as portable numerics and engine-neutral geometry. The C# runtime in `src/` is the +reference; the other runtimes keep wire parity with it. + +If you want typed state on disk, start with CultCache. Everything else assumes it. + +## Language x package + +A cell links a snippet that a test in this repo runs, or says **not yet**. "Package" links +the package's own README or source, which is real but carries no tested first-call snippet from +this page. + +| | CultCache | CultMath | Geometry | CultNet | CultMesh | +| --- | --- | --- | --- | --- | --- | +| **C#** | [first call, tested](#c-first-call) | [package](../../packages/cultmath/README.md); snippet not yet | [package](../../src/GameCult.Geometry/README.md); snippet not yet | [package](../../src/GameCult.Networking); snippet not yet | [quickstart](../../src/GameCult.Mesh/docs/durable-node-quickstart.md); run by `scripts/verify-eve-getting-started.ps1` | +| **TypeScript** | [first call, tested](#typescript-first-call) | not yet | not yet | [package](../../packages/cultnet-ts/README.md); snippet not yet | [package](../../packages/cultmesh-ts/README.md); snippet not yet | +| **Rust** | [package](../../packages/cultcache-rs/README.md); snippet not yet | [native core](../../packages/cultmath/native/cultmath-core); snippet not yet | not yet | [package](../../packages/cultnet-rs/README.md), [examples](../../packages/cultnet-rs/examples); snippet not yet | [package](../../packages/cultmesh-rs); snippet not yet | +| **Python** | [package](../../packages/cultcache-py/README.md); snippet not yet | not yet | not yet | [package](../../packages/cultnet-py/README.md); snippet not yet | [package](../../packages/cultmesh-py/README.md); snippet not yet | + +The Rust and Python CultCache first calls are the next cut for this page. For the whole +picture of what each piece is for, see the +[package chooser in the top-level README](../../README.md#which-package-do-i-want). + +## C# first call + +Define a typed document, put it in a cache backed by a `.cc` file, read it back through a second +cache. The source is +[`tests/GameCult.Caching.Tests/FirstCallTests.cs`](../../tests/GameCult.Caching.Tests/FirstCallTests.cs), +which is what `dotnet test tests/GameCult.Caching.Tests --filter FirstCallTests` runs, so the snippet +cannot drift without the test failing. + +```csharp +using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; + +[CultDocument("gamecult.first_call_item", "gamecult.first_call_item.v1")] +[MessagePackObject] +public sealed class Item +{ + [Key(0)] [CultName] public string Name = string.Empty; + [Key(1)] [CultIndex] public string Category = string.Empty; + [Key(2)] public int Value; +} + +var store = new SingleFileMessagePackBackingStore("first-call.cc"); +var cache = new CultCache(); +cache.AddBackingStore(store); +var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 }); +store.PushAll(); // write the .cc file + +var reopened = new CultCache(); +reopened.AddBackingStore(new SingleFileMessagePackBackingStore("first-call.cc")); +await reopened.PullAllBackingStoresAsync(); // read it back +var potion = reopened.Get(handle.Key); // by key +var byName = reopened.GetByName("Potion"); // by [CultName] +``` + +The test's `Item` is nested and internal, so it carries `[MessagePackObject(AllowPrivate = true)]`; the snippet above is the public-class form and is not itself compiled. More: `src/GameCult.Caching/README.md`. + +## TypeScript first call + +Same shape in TypeScript: the document is a name, a schema id and a `parse`-able schema (zod here), +and the `.cc` file is wire-compatible with the C# one. The source is +[`packages/cultcache-ts/test/first-call.test.ts`](../../packages/cultcache-ts/test/first-call.test.ts), +run by `npm test` in `packages/cultcache-ts` (or `node --test dist-test/test/first-call.test.js` +after `tsc`). + +```ts +import { z } from "zod"; +import { + CultCache, + SingleFileMessagePackBackingStore, + defineDocumentRegistry, + defineDocumentType, +} from "@gamecult/cultcache-ts"; + +const itemDocument = defineDocumentType({ + type: "item", + schemaId: "gamecult.first_call_item.v1", + schemaName: "gamecult.first_call_item", + schemaVersion: "gamecult.first_call_item.v1", + schema: z.object({ name: z.string(), category: z.string(), value: z.number().int() }), + name: "name", + indexes: { category: "category" }, +}); +const registry = defineDocumentRegistry(itemDocument); + +const cache = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore("first-call.cc")) + .build(); +await cache.pullAllBackingStores(); +await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 }); + +const reopened = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore("first-call.cc")) + .build(); +await reopened.pullAllBackingStores(); +const potion = reopened.get(itemDocument, "item:potion"); +const byName = reopened.getByName(itemDocument, "Potion"); +``` + +The test imports from `../src/index`; an application imports `@gamecult/cultcache-ts`. More: +`packages/cultcache-ts/README.md`. + +## Where next + +- Smallest durable mesh path: `src/GameCult.Mesh/docs/durable-node-quickstart.md`. +- Application path, four chapters: `src/GameCult.Mesh/docs/getting-started/README.md`. diff --git a/packages/cultcache-ts/test/first-call.test.ts b/packages/cultcache-ts/test/first-call.test.ts new file mode 100644 index 00000000..aec9faa1 --- /dev/null +++ b/packages/cultcache-ts/test/first-call.test.ts @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { z } from "zod"; + +import { + CultCache, + SingleFileMessagePackBackingStore, + defineDocumentRegistry, + defineDocumentType, +} from "../src/index"; + +// The TypeScript first-call snippet that docs/start/README.md links: define a typed document, +// store it in a .cc file, read it back through a second cache. Keep it small; it is the front door. +const itemDocument = defineDocumentType({ + type: "item", + schemaId: "gamecult.first_call_item.v1", + schemaName: "gamecult.first_call_item", + schemaVersion: "gamecult.first_call_item.v1", + schema: z.object({ name: z.string(), category: z.string(), value: z.number().int() }), + name: "name", + indexes: { category: "category" }, +}); +const registry = defineDocumentRegistry(itemDocument); + +test("define, store and read back a typed document", async () => { + const dir = await mkdtemp(join(tmpdir(), "cultcache-first-call-")); + const path = join(dir, "first-call.cc"); + try { + const cache = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore(path)) + .build(); + await cache.pullAllBackingStores(); + await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 }); + + const reopened = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore(path)) + .build(); + await reopened.pullAllBackingStores(); + + assert.equal(reopened.get(itemDocument, "item:potion")?.value, 50); + assert.equal(reopened.getByName(itemDocument, "Potion")?.category, "Consumable"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/tests/GameCult.Caching.Tests/FirstCallTests.cs b/tests/GameCult.Caching.Tests/FirstCallTests.cs new file mode 100644 index 00000000..d14ad72c --- /dev/null +++ b/tests/GameCult.Caching.Tests/FirstCallTests.cs @@ -0,0 +1,54 @@ +#nullable enable +using System; +using System.IO; +using System.Threading.Tasks; +using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; +using NUnit.Framework; + +namespace GameCult.Caching.Tests +{ + // The C# first-call snippet that docs/start/README.md links: define a typed document, store it in a + // .cc file, read it back through a second cache. Keep it small; it is the front door. + public class FirstCallTests + { + [CultDocument("gamecult.first_call_item", "gamecult.first_call_item.v1")] + [MessagePackObject(AllowPrivate = true)] + internal sealed class Item + { + [Key(0)] [CultName] public string Name = string.Empty; + [Key(1)] [CultIndex] public string Category = string.Empty; + [Key(2)] public int Value; + } + + [Test] + public async Task DefineStoreAndReadBackATypedDocument() + { + var path = Path.Combine(Path.GetTempPath(), $"first-call-{Guid.NewGuid():N}.cc"); + try + { + // Write: a cache with one backing store, one typed document. + var store = new SingleFileMessagePackBackingStore(path); + var cache = new CultCache(); + cache.AddBackingStore(store); + var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 }); + store.PushAll(); + + // Read: a fresh cache over the same file. + var reopened = new CultCache(); + reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path)); + await reopened.PullAllBackingStoresAsync(); + + var byKey = reopened.Get(handle.Key); + Assert.That(byKey, Is.Not.Null); + Assert.That(byKey!.Value, Is.EqualTo(50)); + Assert.That(reopened.GetByName("Potion")!.Category, Is.EqualTo("Consumable")); + } + finally + { + File.Delete(path); + } + } + } +} -- 2.47.3