@idosgames/mcp 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/host.json +13 -5
- package/registry/index.json +28 -26
- package/registry/modules/board-game.json +12 -15
- package/registry/modules/idle-rpg.json +14 -17
- package/registry/modules/voxelcraft.json +11 -7
- package/registry/skills/blockchain-system.json +1 -1
- package/registry/skills/cloud-code.json +2 -2
- package/registry/skills/idosgames-agent-debug-surface.json +6 -0
- package/registry/skills/idosgames-getting-started.json +1 -1
- package/registry/skills/idosgames-module-contract.json +1 -1
- package/registry/skills/quest-system.json +3 -3
- package/registry/skills/title-custom-data.json +6 -0
- package/registry/skills/title-system.json +2 -2
- package/registry/skills/user-custom-data.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-custom-data",
|
|
3
3
|
"description": "Build a generic per-player key-value data store in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.userCustomData (UserCustomDataService): set/get/delete private (only-you-readable) and public (readable-by-others) string keys, batch set/delete many keys atomically, batch-read public data for many players at once, and load the title's schema-managed key registry. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants player settings/preferences storage, profile flair or badges visible to other players, arbitrary save-data slots, or otherwise touches client.userCustomData, UserCustomDataService, UserCustomDataModels, CustomDataBucket, or UserCustomDataRecord — even if they don't name the module explicitly.",
|
|
4
|
-
"content": "---\nname: user-custom-data\ndescription: >-\n Build a generic per-player key-value data store in a game on the iDosGames\n TypeScript SDK (@idosgames/core) via client.userCustomData\n (UserCustomDataService): set/get/delete private (only-you-readable) and\n public (readable-by-others) string keys, batch set/delete many keys\n atomically, batch-read public data for many players at once, and load the\n title's schema-managed key registry. Use this whenever the user is working\n in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and\n wants player settings/preferences storage, profile flair or badges visible\n to other players, arbitrary save-data slots, or otherwise touches\n client.userCustomData, UserCustomDataService, UserCustomDataModels,\n CustomDataBucket, or UserCustomDataRecord — even if they don't name the\n module explicitly.\n---\n\n# User custom data (iDosGames TS SDK)\n\nUserCustomData is a generic per-player key-value store: arbitrary string\nvalues under string keys, split into buckets by visibility. It's\nself-contained — no coupling to currencies, items, or any other economy\nmodule, and no resource cost is ever charged for using it. Use it for\nanything that doesn't fit a purpose-built module: player settings, UI\npreferences, cosmetic flair shown on a profile, small save-data blobs,\nfeature flags per player, etc.\n\nEverything is **server-authoritative**: the backend owns bucket assignment,\nsize/count limits, and (for schema-registered keys) value-format validation.\nThis skill is for **using** the production `UserCustomDataService`, not for\nporting or extending it. If a call is rejected, that's the backend enforcing\na rule (key not registered, value too large/wrong format, bucket full) —\nsurface the error, don't try to reproduce the check client-side.\n\n## Buckets\n\nEvery key lives in exactly one bucket (`CustomDataBucket`), and the bucket\ndecides both who can read it and who can write it:\n\n- **`Private`** — readable only by the owning player. Set with\n `setPrivateData`.\n- **`Public`** — readable by other players (e.g. via\n `getPublicUserCustomDataOf`). Set with `setPublicData`.\n- **`ReadOnly`** — server-written only (e.g. another backend action marking a\n tutorial step done). The client can read it — it shows up in\n `getMyUserCustomData()`'s `ReadOnly` map and in the cached\n `CustomData.ReadOnly` — but the client SDK exposes no write method for it.\n- **`Internal`** — never returned to the client in any form, by any endpoint\n (including cross-player reads). Backend jobs/admin/analytics only; treat it\n as fully invisible.\n\nOnly `Private` and `Public` are writable/deletable from the client\n(`setPrivateData`/`setPublicData`/`deleteKey`/batch variants all reject other\nbuckets with a client-side validation error before hitting the network — and\nthe backend independently enforces the same restriction).\n\nValues are always **strings**. If you need structured data, JSON-encode it\nyourself. The config registry has a `Json` `ValueType` hint — for\nschema-registered keys the **backend validates** that the string parses as\nthe declared type (`Int`/`Bool`/`Json`) on every write — but the SDK itself\ndoes not parse, validate, or decode it for you; see\n[references/data-model.md](references/data-model.md).\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any auth.* method\n\nconst customData = client.userCustomData; // the UserCustomDataService\n```\n\nEvery method requires an authenticated session. Without one they return\n`{ ok: false, reason: \"unauthorized\" }` — they do not throw. There is one\n`client` per player; don't share it across sessions.\n\n## Methods\n\nAll methods return `Promise<OperationResult<T>>`: a discriminated union that is\neither `{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args — empty/malformed key, empty batch), `\"unauthorized\"`,\n`\"throttled\"` (fired the same endpoint again inside the client-side throttle\nwindow), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe human-readable reason, e.g. unregistered key, value type/size limit,\nbucket full, too many batch items).\n\n| Method | Purpose | `data` on success |\n| ----------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- |\n| `getUserCustomDataDefinitions()` | Load the title's key registry/config. | `UserCustomDataDefinitions` |\n| `getMyUserCustomData()` | Load all of this player's data (all buckets). | `GetMyUserCustomDataResponse` |\n| `getPublicUserCustomDataOf(targetUserID)` | Read another player's `Public` bucket only. | `GetPublicUserCustomDataResponse` |\n| `setPrivateData(keyID, value)` | Set/overwrite one key in `Private`. | `SetUserCustomDataResponse` |\n| `setPublicData(keyID, value)` | Set/overwrite one key in `Public`. | `SetUserCustomDataResponse` |\n| `deleteKey(keyID, bucket)` | Delete one key (`Private` or `Public` only). | `SuccessResponse` |\n| `batchSet(items)` | Set many keys (any mix of buckets) atomically. | `BatchSetUserCustomDataResponse` (`Results`) |\n| `batchDelete(items)` | Delete many keys atomically. | `BatchDeleteUserCustomDataResponse` (`DeletedCount`) |\n| `batchGetPublicUserCustomDataOf(targetUserIDs)` | Read `Public` bucket for many players in one call. | `BatchGetPublicUserCustomDataResponse` |\n\nKey rules (checked client-side before any request, and re-checked\nserver-side): a `KeyID` must be non-empty and must not contain `\".\"` or `\"$\"`\n(MongoDB path-safety rule, same convention as Character/Item IDs elsewhere in\nthe SDK).\n\nOn success, the single-key and batch **write/delete** methods mirror the\nconfirmed change into the cache and emit an event. `getPublicUserCustomDataOf`\nand `batchGetPublicUserCustomDataOf` (reading _another_ player's data) do\n**not** touch the cache — there's nothing local to patch since it's someone\nelse's data; treat their results as transient render data.\n\n## Reading state and reacting to changes\n\n```ts\nconst cd = client.data.user.state?.CustomData;\ncd?.Version; // increments on every local write/delete (client-side change counter)\ncd?.Private?.[\"settings\"]?.Value; // string | undefined\ncd?.Public?.[\"title\"]?.Value;\ncd?.ReadOnly?.[\"serverFlag\"]?.Value; // written server-side only\n\n// Config (registry of known keys, if the title schema-manages them):\nimport type { UserCustomDataDefinitions } from \"@idosgames/core\";\nconst defs =\n client.data.config.getSection<UserCustomDataDefinitions>(\"UserCustomData\");\n```\n\nEach `UserCustomDataRecord` also carries `UpdatedAt`, `Version` (a per-key\ncounter that increments every time that specific key is overwritten —\nunrelated to the bucket-wide `CustomData.Version` change counter),\n`LastWriter` (`\"Client\" | \"Server\" | \"System\"`), and `ExpiresAt` (if the key\nhas a TTL).\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\n\n- `userCustomData:definitionsLoaded` → `UserCustomDataDefinitions`\n- `userCustomData:myDataLoaded` → `GetMyUserCustomDataResponse`\n- `userCustomData:publicDataLoaded` → `GetPublicUserCustomDataResponse` (cache untouched)\n- `userCustomData:privateDataSet` → `SetUserCustomDataResponse`\n- `userCustomData:publicDataSet` → `SetUserCustomDataResponse`\n- `userCustomData:keyDeleted` → `void`\n- `userCustomData:batchSet` → `BatchSetUserCustomDataResponse`\n- `userCustomData:batchDeleted` → `BatchDeleteUserCustomDataResponse`\n- `userCustomData:batchPublicDataLoaded` → `BatchGetPublicUserCustomDataResponse` (cache untouched)\n\nThe coarse `user:customDataUpdated` (and `user:anyUpdated`) also fire on every\nlocal write/delete — handy for a \"re-render everything\" hook.\n\n```ts\nconst off = client.on(\"userCustomData:privateDataSet\", (r) => {\n console.log(`${r.KeyID} saved at version ${r.Version}`);\n});\n// later: off();\n```\n\n## Recipes\n\n### Save and read a player setting privately\n\n```ts\nconst res = await client.userCustomData.setPrivateData(\"theme\", \"dark\");\nif (!res.ok) return showError(res.error);\n\n// later, anywhere in the app:\nconst theme = client.data.user.state?.CustomData?.Private?.[\"theme\"]?.Value;\n```\n\n### Bootstrap all of the player's own data at login\n\n```ts\nawait client.userCustomData.getMyUserCustomData();\nconst cd = client.data.user.state?.CustomData;\n// cd.Private / cd.Public / cd.ReadOnly are now populated from the server.\n// If a schema-registered key has a DefaultValue and the player has never set\n// it, the server materializes that default in the response (Version: 0,\n// LastWriter: \"System\") without writing it to the DB — treat it as a real\n// display value, just don't expect a subsequent read to differ before you Set it.\n```\n\n### Expose a profile badge publicly\n\n```ts\nconst res = await client.userCustomData.setPublicData(\"title\", \"Dragon Slayer\");\nif (!res.ok) return showError(res.error);\n\n// anyone can now read it:\nconst other =\n await client.userCustomData.getPublicUserCustomDataOf(\"player-42\");\nif (other.ok) {\n const badge = other.data.Public?.[\"title\"]?.Value;\n}\n```\n\n### Batch-set several keys in one atomic call\n\n```ts\nimport { CustomDataBucket } from \"@idosgames/core\";\n\nconst res = await client.userCustomData.batchSet([\n { Bucket: CustomDataBucket.Private, KeyID: \"theme\", Value: \"dark\" },\n { Bucket: CustomDataBucket.Public, KeyID: \"title\", Value: \"Dragon Slayer\" },\n]);\nif (!res.ok) return showError(res.error);\nfor (const item of res.data.Results ?? []) {\n // item.Bucket, item.KeyID, item.Version, item.ExpiresAt\n}\n```\n\nThis is genuinely all-or-nothing: every item is validated (schema bucket\nmatch, value type/size, no `(Bucket, KeyID)` duplicates) **before** anything\ntouches the database, and a single invalid item fails the whole call with no\npartial writes — unlike some other modules' batch APIs, there is no\nper-item `Success`/`Error` result array here to sift through.\n\n### Batch-delete, and batch-read public data for a leaderboard/friends screen\n\n```ts\nawait client.userCustomData.batchDelete([\n { Bucket: CustomDataBucket.Private, KeyID: \"tempFlag\" },\n]);\n\nconst res = await client.userCustomData.batchGetPublicUserCustomDataOf([\n \"player-1\",\n \"player-2\",\n]);\nif (res.ok) {\n for (const profile of res.data.Results ?? []) {\n // profile.UserID, profile.Public\n }\n res.data.NotFoundUserIDs; // ids that don't exist / had nothing public\n}\n```\n\n`batchDelete` is idempotent per item — deleting a key that's already gone (or\nnever existed) is not an error; it's simply excluded from `DeletedCount`. If\nnone of the requested keys exist, the call still succeeds with\n`DeletedCount: 0` and never touches the database.\n\n## Gotchas\n\n- **Values are strings only.** JSON-encode/decode structured data yourself;\n the SDK does not serialize for you. Note that for a schema-registered key\n with `ValueType: \"Json\"`, the **backend** does validate that your string\n parses as JSON (and similarly `Int`→`long.TryParse`, `Bool`→`\"true\"`/`\"false\"`)\n — a malformed value is rejected server-side with a `\"server\"` reason before\n it reaches the database.\n- **`KeyID` can't contain `\".\"` or `\"$\"`.** Same MongoDB path-safety rule as\n other modules' IDs — validated client-side before any network call, and\n re-validated server-side.\n- **`ReadOnly` and `Internal` have no client write path.** `deleteKey` and\n `batchSet`/`batchDelete` explicitly reject buckets other than `Private`/\n `Public` with `reason: \"client\"`. `Internal` is never returned to the client\n at all, by any endpoint — not even your own.\n- **Reading another player's data never touches your cache.** Only your own\n reads/writes (`getMyUserCustomData`, `setPrivateData`, `setPublicData`,\n `deleteKey`, `batchSet`, `batchDelete`) patch `client.data.user.state.CustomData`.\n- **Guard against double-submit.** Each call is a real write with no\n idempotency key — writes are last-write-wins with no compare-and-swap on the\n client API, so a double-clicked \"Save\" can silently overwrite itself twice\n in a row (harmless for a plain overwrite, but a race if two different\n values are in flight). Disable the control while a call is in flight.\n Firing the same endpoint again within the client-side throttle window\n (600 ms) is rejected with `reason: \"throttled\"` rather than sent twice.\n- **Schema enforcement is permissive by default.** Whether an unregistered\n `KeyID` is accepted depends entirely on the title's\n `RejectUnregisteredKeys` flag (default `false`, i.e. permissive/free-form).\n When `true`, only keys listed in the registry's `Keys` map can be written at\n all — check [references/data-model.md](references/data-model.md) and load\n `getUserCustomDataDefinitions()` before assuming a string key will be\n accepted.\n- **Real server-side caps exist, not just per-key size.** Per-title limits\n gate every write: a max value size per key, a max key _count_ per bucket,\n and a max _total_ size across all four buckets combined — see\n [references/data-model.md](references/data-model.md) for the exact fields\n and default values. Hitting any of them fails the write (or the whole batch)\n with a `\"server\"` error naming the limit; nothing is silently dropped or\n truncated.\n- **Batches are capped at 50 items and reject outright past that**, unlike\n some other modules' batches which silently drop overflow entries — sending\n 51+ items in one `batchSet`/`batchDelete`/`batchGetPublicUserCustomDataOf`\n call fails the entire call with a `\"server\"` error; chunk larger sets into\n multiple calls yourself.\n- **TTL is enforced, not cosmetic.** A key with a TTL (from the registry's\n `TtlSeconds` or the title's `DefaultTtlSeconds` for free-form keys) simply\n stops being returned once expired — reads filter it out as if it were never\n set. The expired record itself is only actually purged from storage on the\n _next_ write to that bucket, so don't rely on `ExpiresAt` for anything other\n than \"will this still be readable.\"\n- **Render from the cache, handle the error from the result.** The happy path\n updates the cache + emits an event; the failure path gives you `reason` +\n `error`. Use `reason` to decide behavior (retry on `\"connection\"`, re-auth on\n `\"unauthorized\"`, toast the `error` on `\"server\"`).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — every request/response\ntype, the key-registry config shape, and the exact size/count/TTL limit\nfields and their defaults. Read it when building config-driven UI (e.g.\nshowing max length or TTL before the player types) or when a server error\npoints at a registry rule you need to understand.\n",
|
|
4
|
+
"content": "---\nname: user-custom-data\ndescription: >-\n Build a generic per-player key-value data store in a game on the iDosGames\n TypeScript SDK (@idosgames/core) via client.userCustomData\n (UserCustomDataService): set/get/delete private (only-you-readable) and\n public (readable-by-others) string keys, batch set/delete many keys\n atomically, batch-read public data for many players at once, and load the\n title's schema-managed key registry. Use this whenever the user is working\n in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and\n wants player settings/preferences storage, profile flair or badges visible\n to other players, arbitrary save-data slots, or otherwise touches\n client.userCustomData, UserCustomDataService, UserCustomDataModels,\n CustomDataBucket, or UserCustomDataRecord — even if they don't name the\n module explicitly.\n---\n\n# User custom data (iDosGames TS SDK)\n\nUserCustomData is a generic per-player key-value store: arbitrary string\nvalues under string keys, split into buckets by visibility. It's\nself-contained — no coupling to currencies, items, or any other economy\nmodule, and no resource cost is ever charged for using it. Use it for\nanything that doesn't fit a purpose-built module: player settings, UI\npreferences, cosmetic flair shown on a profile, small save-data blobs,\nfeature flags per player, etc.\n\n**This is where player data goes.** The project is client-side code in the\nplayer's browser: `localStorage`, module fields and React state survive neither\na device change nor a cleared cache, and the server trusts none of them. If a\ndedicated module owns the data (currency, inventory, quests, characters,\nleaderboards) use that module — it enforces the rules. Everything else that must\npersist goes here. Don't invent a save system.\n\nEverything is **server-authoritative**: the backend owns bucket assignment,\nsize/count limits, and (for schema-registered keys) value-format validation.\nThis skill is for **using** the production `UserCustomDataService`, not for\nporting or extending it. If a call is rejected, that's the backend enforcing\na rule (key not registered, value too large/wrong format, bucket full) —\nsurface the error, don't try to reproduce the check client-side.\n\n## Buckets\n\nEvery key lives in exactly one bucket (`CustomDataBucket`), and the bucket\ndecides both who can read it and who can write it:\n\n- **`Private`** — readable only by the owning player. Set with\n `setPrivateData`.\n- **`Public`** — readable by other players (e.g. via\n `getPublicUserCustomDataOf`). Set with `setPublicData`.\n- **`ReadOnly`** — server-written only (e.g. a script marking a tutorial step\n done). The client can read it — it shows up in `getMyUserCustomData()`'s\n `ReadOnly` map and in the cached `CustomData.ReadOnly` — but the client SDK\n exposes no write method for it.\n- **`Internal`** — never returned to the client in any form, by any endpoint\n (including cross-player reads). Backend jobs/admin/analytics only; treat it\n as fully invisible.\n\nOnly `Private` and `Public` are writable/deletable from the client\n(`setPrivateData`/`setPublicData`/`deleteKey`/batch variants all reject other\nbuckets with a client-side validation error before hitting the network — and\nthe backend independently enforces the same restriction).\n\n**Choosing a bucket is a security decision, not a taste one.** A client-written\nvalue is a value the player can set to anything: they own the browser. Put\npreferences and cosmetics in `Private`/`Public`; put anything worth cheating for\n— progress, unlocks, earned rewards, reward cooldowns, anti-abuse flags — in\n`ReadOnly` or `Internal`.\n\nThose two buckets are written **only from a CloudCode script**\n(`server.SetUserCustomData(\"ReadOnly\", key, value)` and friends). That is the\nwhole point of the split: the write happens on the server, inside logic the\nplayer cannot edit. See **cloud-code** for writing and publishing the handler,\nand call it with `client.cloudCode.execute(...)`. Title-wide values shared by\nevery player belong in **title-custom-data** instead.\n\nValues are always **strings**. If you need structured data, JSON-encode it\nyourself. The config registry has a `Json` `ValueType` hint — for\nschema-registered keys the **backend validates** that the string parses as\nthe declared type (`Int`/`Bool`/`Json`) on every write — but the SDK itself\ndoes not parse, validate, or decode it for you; see\n[references/data-model.md](references/data-model.md).\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any auth.* method\n\nconst customData = client.userCustomData; // the UserCustomDataService\n```\n\nEvery method requires an authenticated session. Without one they return\n`{ ok: false, reason: \"unauthorized\" }` — they do not throw. There is one\n`client` per player; don't share it across sessions.\n\n## Methods\n\nAll methods return `Promise<OperationResult<T>>`: a discriminated union that is\neither `{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args — empty/malformed key, empty batch), `\"unauthorized\"`,\n`\"throttled\"` (fired the same endpoint again inside the client-side throttle\nwindow), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe human-readable reason, e.g. unregistered key, value type/size limit,\nbucket full, too many batch items).\n\n| Method | Purpose | `data` on success |\n| ----------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- |\n| `getUserCustomDataDefinitions()` | Load the title's key registry/config. | `UserCustomDataDefinitions` |\n| `getMyUserCustomData()` | Load all of this player's data (all buckets). | `GetMyUserCustomDataResponse` |\n| `getPublicUserCustomDataOf(targetUserID)` | Read another player's `Public` bucket only. | `GetPublicUserCustomDataResponse` |\n| `setPrivateData(keyID, value)` | Set/overwrite one key in `Private`. | `SetUserCustomDataResponse` |\n| `setPublicData(keyID, value)` | Set/overwrite one key in `Public`. | `SetUserCustomDataResponse` |\n| `deleteKey(keyID, bucket)` | Delete one key (`Private` or `Public` only). | `SuccessResponse` |\n| `batchSet(items)` | Set many keys (any mix of buckets) atomically. | `BatchSetUserCustomDataResponse` (`Results`) |\n| `batchDelete(items)` | Delete many keys atomically. | `BatchDeleteUserCustomDataResponse` (`DeletedCount`) |\n| `batchGetPublicUserCustomDataOf(targetUserIDs)` | Read `Public` bucket for many players in one call. | `BatchGetPublicUserCustomDataResponse` |\n\nKey rules (checked client-side before any request, and re-checked\nserver-side): a `KeyID` must be non-empty and must not contain `\".\"` or `\"$\"`\n(MongoDB path-safety rule, same convention as Character/Item IDs elsewhere in\nthe SDK).\n\nOn success, the single-key and batch **write/delete** methods mirror the\nconfirmed change into the cache and emit an event. `getPublicUserCustomDataOf`\nand `batchGetPublicUserCustomDataOf` (reading _another_ player's data) do\n**not** touch the cache — there's nothing local to patch since it's someone\nelse's data; treat their results as transient render data.\n\n## Reading state and reacting to changes\n\n```ts\nconst cd = client.data.user.state?.CustomData;\ncd?.Version; // increments on every local write/delete (client-side change counter)\ncd?.Private?.[\"settings\"]?.Value; // string | undefined\ncd?.Public?.[\"title\"]?.Value;\ncd?.ReadOnly?.[\"serverFlag\"]?.Value; // written server-side only\n\n// Config (registry of known keys, if the title schema-manages them):\nimport type { UserCustomDataDefinitions } from \"@idosgames/core\";\nconst defs =\n client.data.config.getSection<UserCustomDataDefinitions>(\"UserCustomData\");\n```\n\nEach `UserCustomDataRecord` also carries `UpdatedAt`, `Version` (a per-key\ncounter that increments every time that specific key is overwritten —\nunrelated to the bucket-wide `CustomData.Version` change counter),\n`LastWriter` (`\"Client\" | \"Server\" | \"System\"`), and `ExpiresAt` (if the key\nhas a TTL).\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\n\n- `userCustomData:definitionsLoaded` → `UserCustomDataDefinitions`\n- `userCustomData:myDataLoaded` → `GetMyUserCustomDataResponse`\n- `userCustomData:publicDataLoaded` → `GetPublicUserCustomDataResponse` (cache untouched)\n- `userCustomData:privateDataSet` → `SetUserCustomDataResponse`\n- `userCustomData:publicDataSet` → `SetUserCustomDataResponse`\n- `userCustomData:keyDeleted` → `void`\n- `userCustomData:batchSet` → `BatchSetUserCustomDataResponse`\n- `userCustomData:batchDeleted` → `BatchDeleteUserCustomDataResponse`\n- `userCustomData:batchPublicDataLoaded` → `BatchGetPublicUserCustomDataResponse` (cache untouched)\n\nThe coarse `user:customDataUpdated` (and `user:anyUpdated`) also fire on every\nlocal write/delete — handy for a \"re-render everything\" hook.\n\n```ts\nconst off = client.on(\"userCustomData:privateDataSet\", (r) => {\n console.log(`${r.KeyID} saved at version ${r.Version}`);\n});\n// later: off();\n```\n\n## Recipes\n\n### Save and read a player setting privately\n\n```ts\nconst res = await client.userCustomData.setPrivateData(\"theme\", \"dark\");\nif (!res.ok) return showError(res.error);\n\n// later, anywhere in the app:\nconst theme = client.data.user.state?.CustomData?.Private?.[\"theme\"]?.Value;\n```\n\n### Bootstrap all of the player's own data at login\n\n```ts\nawait client.userCustomData.getMyUserCustomData();\nconst cd = client.data.user.state?.CustomData;\n// cd.Private / cd.Public / cd.ReadOnly are now populated from the server.\n// If a schema-registered key has a DefaultValue and the player has never set\n// it, the server materializes that default in the response (Version: 0,\n// LastWriter: \"System\") without writing it to the DB — treat it as a real\n// display value, just don't expect a subsequent read to differ before you Set it.\n```\n\n### Expose a profile badge publicly\n\n```ts\nconst res = await client.userCustomData.setPublicData(\"title\", \"Dragon Slayer\");\nif (!res.ok) return showError(res.error);\n\n// anyone can now read it:\nconst other =\n await client.userCustomData.getPublicUserCustomDataOf(\"player-42\");\nif (other.ok) {\n const badge = other.data.Public?.[\"title\"]?.Value;\n}\n```\n\n### Batch-set several keys in one atomic call\n\n```ts\nimport { CustomDataBucket } from \"@idosgames/core\";\n\nconst res = await client.userCustomData.batchSet([\n { Bucket: CustomDataBucket.Private, KeyID: \"theme\", Value: \"dark\" },\n { Bucket: CustomDataBucket.Public, KeyID: \"title\", Value: \"Dragon Slayer\" },\n]);\nif (!res.ok) return showError(res.error);\nfor (const item of res.data.Results ?? []) {\n // item.Bucket, item.KeyID, item.Version, item.ExpiresAt\n}\n```\n\nThis is genuinely all-or-nothing: every item is validated (schema bucket\nmatch, value type/size, no `(Bucket, KeyID)` duplicates) **before** anything\ntouches the database, and a single invalid item fails the whole call with no\npartial writes — unlike some other modules' batch APIs, there is no\nper-item `Success`/`Error` result array here to sift through.\n\n### Batch-delete, and batch-read public data for a leaderboard/friends screen\n\n```ts\nawait client.userCustomData.batchDelete([\n { Bucket: CustomDataBucket.Private, KeyID: \"tempFlag\" },\n]);\n\nconst res = await client.userCustomData.batchGetPublicUserCustomDataOf([\n \"player-1\",\n \"player-2\",\n]);\nif (res.ok) {\n for (const profile of res.data.Results ?? []) {\n // profile.UserID, profile.Public\n }\n res.data.NotFoundUserIDs; // ids that don't exist / had nothing public\n}\n```\n\n`batchDelete` is idempotent per item — deleting a key that's already gone (or\nnever existed) is not an error; it's simply excluded from `DeletedCount`. If\nnone of the requested keys exist, the call still succeeds with\n`DeletedCount: 0` and never touches the database.\n\n## Gotchas\n\n- **Values are strings only.** JSON-encode/decode structured data yourself;\n the SDK does not serialize for you. Note that for a schema-registered key\n with `ValueType: \"Json\"`, the **backend** does validate that your string\n parses as JSON (and similarly `Int`→`long.TryParse`, `Bool`→`\"true\"`/`\"false\"`)\n — a malformed value is rejected server-side with a `\"server\"` reason before\n it reaches the database.\n- **`KeyID` can't contain `\".\"` or `\"$\"`.** Same MongoDB path-safety rule as\n other modules' IDs — validated client-side before any network call, and\n re-validated server-side.\n- **`ReadOnly` and `Internal` have no client write path.** `deleteKey` and\n `batchSet`/`batchDelete` explicitly reject buckets other than `Private`/\n `Public` with `reason: \"client\"`. `Internal` is never returned to the client\n at all, by any endpoint — not even your own. Writing either one means writing\n a CloudCode handler; there is no SDK method that will do it, now or later.\n- **Register the keys a feature depends on.** A key listed in the title's\n `UserCustomData` config section has its bucket pinned, its value type checked\n (`String`/`Int`/`Bool`/`Json`), a size cap, an optional TTL and a default\n value returned on read. Unregistered keys are accepted (unless the title sets\n `RejectUnregisteredKeys`) but nothing then catches a typo or a wrong bucket.\n Add the definition in the same change that adds the code using it.\n- **Reading another player's data never touches your cache.** Only your own\n reads/writes (`getMyUserCustomData`, `setPrivateData`, `setPublicData`,\n `deleteKey`, `batchSet`, `batchDelete`) patch `client.data.user.state.CustomData`.\n- **Guard against double-submit.** Each call is a real write with no\n idempotency key — writes are last-write-wins with no compare-and-swap on the\n client API, so a double-clicked \"Save\" can silently overwrite itself twice\n in a row (harmless for a plain overwrite, but a race if two different\n values are in flight). Disable the control while a call is in flight.\n Firing the same endpoint again within the client-side throttle window\n (600 ms) is rejected with `reason: \"throttled\"` rather than sent twice.\n- **Schema enforcement is permissive by default.** Whether an unregistered\n `KeyID` is accepted depends entirely on the title's\n `RejectUnregisteredKeys` flag (default `false`, i.e. permissive/free-form).\n When `true`, only keys listed in the registry's `Keys` map can be written at\n all — check [references/data-model.md](references/data-model.md) and load\n `getUserCustomDataDefinitions()` before assuming a string key will be\n accepted.\n- **Real server-side caps exist, not just per-key size.** Per-title limits\n gate every write: a max value size per key, a max key _count_ per bucket,\n and a max _total_ size across all four buckets combined — see\n [references/data-model.md](references/data-model.md) for the exact fields\n and default values. Hitting any of them fails the write (or the whole batch)\n with a `\"server\"` error naming the limit; nothing is silently dropped or\n truncated.\n- **Batches are capped at 50 items and reject outright past that**, unlike\n some other modules' batches which silently drop overflow entries — sending\n 51+ items in one `batchSet`/`batchDelete`/`batchGetPublicUserCustomDataOf`\n call fails the entire call with a `\"server\"` error; chunk larger sets into\n multiple calls yourself.\n- **TTL is enforced, not cosmetic.** A key with a TTL (from the registry's\n `TtlSeconds` or the title's `DefaultTtlSeconds` for free-form keys) simply\n stops being returned once expired — reads filter it out as if it were never\n set. The expired record itself is only actually purged from storage on the\n _next_ write to that bucket, so don't rely on `ExpiresAt` for anything other\n than \"will this still be readable.\"\n- **Render from the cache, handle the error from the result.** The happy path\n updates the cache + emits an event; the failure path gives you `reason` +\n `error`. Use `reason` to decide behavior (retry on `\"connection\"`, re-auth on\n `\"unauthorized\"`, toast the `error` on `\"server\"`).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — every request/response\ntype, the key-registry config shape, and the exact size/count/TTL limit\nfields and their defaults. Read it when building config-driven UI (e.g.\nshowing max length or TTL before the player types) or when a server error\npoints at a registry rule you need to understand.\n",
|
|
5
5
|
"references": [
|
|
6
6
|
{
|
|
7
7
|
"path": "data-model.md",
|