Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 doPanel page
Create a group, add fields, start from a templateContent, Edit group
Add an entry, edit values, publish or archive an entryEdit entry
See the publish history and the diff, send changes to the gameContent › Publish
Attach a 3D model to a model field3D 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 holdsSettings
    integerAn integer (with an optional decimal display scale)min, max, scale (0–3), unit (at most 12 characters), default
    percentA percentage in basis points (10000 = 100%)min_bp, max_bp, default_bp
    textTextmax_chars (at most 512), multiline
    boolYes/nodefault
    selectOption(s)options (at most 64), multiple
    colorA color#rrggbb
    modelA 3D modelmodel library ID
    imageAn imagethe asset’s sha256 value
    referenceA link to another entrytarget: another group, a built-in item or a built-in character
    tableA matrix with repeated rowscolumns (1–8), min_rows, max_rows (at most 100)
  • Visibility (audience): Each field can be player (goes to the player) or server (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 with formatScaled.

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 pathWhat 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 with visibility = player and their values.
  • Server key (Authorization: Bearer crx_live_<prefix>_<secret>): sees all fields with their visibility and with seq. An invalid Bearer crx_… key gets 401 even with a valid cookie; it does not fall back to the cookie.
Method and pathWhat it returns
GET /v1/catalog/groupsThe 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:

HeaderMeaning
x-crx-envRequired on a player session; with a server key the environment comes from the key
If-None-MatchThe 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-Key or If-Match; you do conditional reads with If-None-Match.
  • The publish counter (seq) grows with every publish; if seq has 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

SymptomCauseFix
404 (group)The group was never published or does not existPublish the group in the panel
401No credential or an invalid Bearer crx_…; x-crx-env missing on a player sessionFix the credential and the header
Publish rejected in the panelA reference (reference) target is missing; the table or publish size is exceededFix the problems the panel lists
I changed the value but the game does not see itThe draft was not published, or the field has server visibilityPublish; check the visibility
304The ETag matchedExpected 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);
}