Content groups
This page describes content groups, which let you change game data such as tools, weapons, NPCs or police units without shipping code. Read it to learn how the data is defined and published in the panel and how your game reads it.
What it does
A content group is a typed table you define: the group has fields (for example speed, damage, color, model), and every entry (for example sedan) carries a value for those fields. An operator edits the values in the panel and publishes them; the game reads only the current published state. This way you do not have to redeploy the game to change a value.
Content groups are part of the core: there is no separate module switch, and they work in every environment.
Where it lives in the panel
| What you want to do | Panel page |
|---|---|
| Create a group, add fields, start from a template | Content, Edit group |
| Add an entry, edit values, publish or archive an entry | Edit entry |
| See the publish history and the diff, send changes to the game | Content › Publish |
| Attach a 3D model to a model field | 3D models |
Note
Panel help pages are not translated yet; the panel links above point to the Turkish source, and the panel itself is shown in Turkish.
Data model (in plain words)
-
Group: A
key(^[a-z][a-z0-9_]{1,47}$, that is 2–48 characters), a name and fields. The key does not change after creation. -
Field: A
key(the same rule) and a kind. There are ten kinds:Kind ( kind)What it holds Settings integerAn integer (with an optional decimal display scale) min,max,scale(0–3),unit(at most 12 characters),defaultpercentA percentage in basis points (10000 = 100%) min_bp,max_bp,default_bptextText max_chars(at most 512),multilineboolYes/no defaultselectOption(s) options(at most 64),multiplecolorA color #rrggbbmodelA 3D model model library ID imageAn image the asset’s sha256valuereferenceA link to another entry target: another group, a built-in item or a built-in character tableA matrix with repeated rows columns(1–8),min_rows,max_rows(at most 100) -
Visibility (audience): Each field can be
player(goes to the player) orserver(only a server key sees it). -
Entry: A
key(^[a-z0-9][a-z0-9_-]{0,63}$) and field values. Its status is one of: Published (Yayında), Has draft changes (Taslak değişikliği var), Archived (Arşivli), Not published yet (Henüz yayınlanmadı); the panel shows these labels in Turkish. -
Publish: Done in the panel with “Değişiklikleri oyuna gönder” (send changes to the game); it takes a note of at most 280 characters and produces a sequential publish counter (
seq). The next publish does not change the previous one, it adds a new one; the history and the diff are visible in the panel. If someone published while you were looking, the panel refreshes the diff and asks for confirmation again. -
Numbers: Integers; fractional or exponent notation (
40.0,1e2) is rejected. Display a scaled integer in the game withformatScaled.
Note
Item and character templates also appear in the panel as built-in groups; they behave as “live in the game as soon as you save” and need no publish step. User-defined groups need a publish.
Limits: at most 64 groups per environment, 48 fields per group and 1000 entries; one entry’s table data cannot exceed 128 KiB, and the publish snapshot for a server audience cannot exceed 1 MiB.
Catalog: items and characters
The game reads item and character definitions (key, name, typed data rows and the 3D model address) from the catalog endpoints:
| Method and path | What it returns |
|---|---|
GET /v1/catalog/items, GET /v1/catalog/items/{key} | Item manifests |
GET /v1/catalog/characters, GET /v1/catalog/characters/{key} | Character manifests |
These depend on the items and characters modules; 404 if the module is disabled. The list is paginated (limit 1–100, default 50; cursor). Each manifest carries kind, key, name, definition_version, data (typed rows), model (null if none) and content_sha256. For details, watching with ETag and showing models with three: Game catalog integration (docs/oyun-katalog-entegrasyonu.md) (Turkish) and Scenes, assets and 3D models.
Calls your game makes (content groups)
Content groups are exposed through a read-only surface; the game cannot reach drafts. There are two credential types:
- Player session (cookie or session token +
x-crx-env: <environment uuid>): sees only fields withvisibility = playerand their values. - Server key (
Authorization: Bearer crx_live_<prefix>_<secret>): sees all fields with their visibility and withseq. An invalidBearer crx_…key gets401even with a valid cookie; it does not fall back to the cookie.
| Method and path | What it returns |
|---|---|
GET /v1/catalog/groups | The key and name of published groups and a content_sha256 per audience |
GET /v1/catalog/groups/{group} | The group’s current publish: group, fields, entries, seq |
GET /v1/catalog/groups/{group}/entries/{key} | The same shape, only that entry |
Headers:
| Header | Meaning |
|---|---|
x-crx-env | Required on a player session; with a server key the environment comes from the key |
If-None-Match | The ETag you hold; if it matches, 304 and no body |
Responses carry an audience-specific ETag. The player ETag and the content_sha256 value are computed from the filtered body; so changing a server-only field shows no change on the player side. A group that was never published, or does not exist, returns 404.
Note
Server-only rows are not a secret store: operators in the panel and holders of a server key can see them. Do not put passwords, keys or personal data in them.
Table field
A table field holds rows repeated within an entry (a weapon’s level → damage table, an NPC loot list). Column kinds are integer, percent, text, bool, select and reference. The value is an array of objects that preserves row order; changing the order means a new publish digest. A server-only table is absent from the player body entirely.
Reading from the game
The TypeScript SDK provides a typed reader for both audiences (player.catalog.groups, and CrxClient.catalog.groups on the server; list, get, entry). There are watchCatalogGroup for watching and formatScaled for displaying a scaled number. The Unity SDK has no catalog; use raw REST.
Versioning and replay safety
- The read endpoints have no
Idempotency-KeyorIf-Match; you do conditional reads withIf-None-Match. - The publish counter (
seq) grows with every publish; ifseqhas not changed, the content has not changed. - Group, field and entry keys do not change after creation; a field kind change is checked, and the panel lists the problems before publishing.
Events
Publishing a content group does not write an event to the outbox (only an audit record is made), so it does not produce a webhook. To catch a change in the game, poll with ETag: a change reaches the player at most one polling interval later; a one-minute interval is enough.
Limits
- 64 groups / 48 fields / 1000 entries; a table has at most 8 columns and 100 rows; an entry’s table data 128 KiB; the server publish snapshot 1 MiB.
- A text field is at most 512 characters; a select field at most 64 options.
- A publish note is at most 280 characters.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
404 (group) | The group was never published or does not exist | Publish the group in the panel |
401 | No credential or an invalid Bearer crx_…; x-crx-env missing on a player session | Fix the credential and the header |
| Publish rejected in the panel | A reference (reference) target is missing; the table or publish size is exceeded | Fix the problems the panel lists |
| I changed the value but the game does not see it | The draft was not published, or the field has server visibility | Publish; check the visibility |
304 | The ETag matched | Expected behavior; your copy is current |
Working example
Reading the current publish of the weapons group with an ETag:
/** İçerik grubunu (panelde yayınlanan oyun verisi) oyuncu oturumuyla okur; ETag ile değişmediyse 304 döner. */
import type { PlayerClient } from "@ferdiunal/open-play-ts";
/** `weapons` grubunun güncel yayınındaki kayıt anahtarlarını döndürür; değişmediyse boş liste. */
export async function readWeapons(player: PlayerClient, previousEtag?: string): Promise<readonly string[]> {
const result = await player.catalog.groups.get("weapons", previousEtag === undefined ? {} : { ifNoneMatch: previousEtag });
if (result.status === 304) return [];
return result.value.entries.map((entry) => entry.key);
}
Related pages
- Content groups as a reward source · Inventory and items · Characters · Scenes, assets and 3D models
- Game catalog integration (detailed guide, Turkish) (
docs/oyun-katalog-entegrasyonu.md) · TypeScript SDK