Config and flags
Config values and feature flags let you change how your game behaves without shipping a build. Both are per environment: a value you change in test is not seen in production until you make the same change there.
Switch the Config and Feature flags services on for your project in the console. Until you do, their routes answer service-disabled.
Config values
A config value is a key with one typed value, edited on the environment's Config page.
| Part | Rules |
|---|---|
| Key | 1 to 120 characters: letters, digits and . _ : -. Unique within the environment |
| Group | Optional, up to 120 characters. Lets a client fetch related values together |
| Type | string, int, float or bool |
| Value | Of that type. A string is at most 10,000 characters |
Saving a key that exists replaces its value and type. Deleting a key removes it from what clients read; saving the same key again brings it back.
Reading config
curl https://api.greenmindmedia.com/api/platform/v1/config/values \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl "https://api.greenmindmedia.com/api/platform/v1/config/values?groupName=combat" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Either credential works: a player's access token from the client, or a server key from your server. Without groupName you get every value in the environment; with it, only that group's. A group name is 1 to 120 characters, the same as the console takes; an empty groupName= is refused with invalid-request.
The answer is a page of rows ordered by key, up to 50 unless you ask for up to 200 with limit; next is the cursor of the page after, or null on the last:
{
"next": null,
"items": [
{
"key": "maxPartySize",
"groupName": null,
"type": "int",
"valueStr": null,
"valueInt": 4,
"valueFloat": null,
"valueBool": null,
"GmLevelRateConfig": null
},
{
"key": "motd",
"groupName": "lobby",
"type": "string",
"valueStr": "Double XP this weekend",
"valueInt": null,
"valueFloat": null,
"valueBool": null,
"GmLevelRateConfig": null
}
]
}
type says which value field holds the value; the others are null. GmLevelRateConfig is always null for values made in the console: it carries a weight curve for a value of type rate, which only older GreenMind clients read and which config tables (below) replace. Read it as a field that may appear, and ignore a type your client does not recognise rather than failing.
A typed reader in TypeScript:
interface ConfigRow {
key: string
groupName: string | null
type: string
valueStr: string | null
valueInt: number | null
valueFloat: number | null
valueBool: boolean | null
}
function valueOf(row: ConfigRow): string | number | boolean | null {
switch (row.type) {
case 'string':
return row.valueStr
case 'int':
return row.valueInt
case 'float':
return row.valueFloat
case 'bool':
return row.valueBool
default:
return null
}
}
/** Every row, page by page. */
async function readConfig(accessToken: string): Promise<ConfigRow[]> {
const rows: ConfigRow[] = []
let cursor: string | null = null
do {
const query = cursor ? `?limit=200&cursor=${cursor}` : '?limit=200'
const response = await fetch(
`https://api.greenmindmedia.com/api/platform/v1/config/values${query}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
)
const page = (await response.json()) as { items: ConfigRow[]; next: string | null }
rows.push(...page.items)
cursor = page.next
} while (cursor)
return rows
}
const config = new Map((await readConfig(accessToken)).map((row) => [row.key, valueOf(row)]))
Values that affect a match
If every player in a match must see the same values (anything that seeds a map, sets damage, or decides a rule both sides check), have your match server read config once at the start of the match and send it to the clients. Clients that each fetch on their own can straddle a change and disagree.
Config tables
A config table is config as a grid: named rows across typed columns, for anything that is one value per row per column. Drop weights by chest level, stats by rarity, prices by region. You shape it on the environment's Config tables page: add, rename, reorder and remove rows and columns, and fill the cells in.
| Part | Rules |
|---|---|
| Key | Of the table, a row or a column: 1 to 200 characters, with no spaces, /, \, ? or %. Unique within its table |
| Column | A key, a label for people, and a type: string, int, float or bool. Up to 64 columns |
| Row | A key and one cell per column. Up to 1,000 rows |
| Cell | A value of its column's type, or empty. A string cell is at most 2,000 characters |
Saving replaces the whole table. A row you remove stops reaching your game; one that other config still points at cannot be removed. Renaming a row keeps it the same row. A table's key is fixed once it is saved: make a new table to change it.
Reading tables
curl https://api.greenmindmedia.com/api/platform/v1/config/tables \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl https://api.greenmindmedia.com/api/platform/v1/config/tables/chest.drops \
-H "Authorization: Bearer $ACCESS_TOKEN"
Either credential works, and both need the Config service. The first lists the environment's tables a page at a time, ordered by key, as { "items": [...], "next": ... }; the second answers one table, or 404 with not-found.
{
"key": "chest.drops",
"name": "Chest drops",
"description": "What a chest holds, by chest tier",
"groupName": null,
"columns": [
{ "key": "weight", "label": "Weight", "type": "int" },
{ "key": "rare", "label": "Rare", "type": "bool" }
],
"rows": [
{ "key": "gold", "sortOrder": 0, "cells": { "weight": 10, "rare": false } },
{ "key": "gem", "sortOrder": 1, "cells": { "weight": 1, "rare": true } }
]
}
Rows come in the order they have in the editor. An empty cell is either null or missing from cells: read both as "no value", and fall back to your own default. A column added after a row was saved has no cell in that row until somebody fills it in.
A typed reader in TypeScript:
type Cell = string | number | boolean | null
interface ConfigTable {
key: string
name: string
description: string | null
groupName: string | null
columns: { key: string; label: string; type: 'string' | 'int' | 'float' | 'bool' }[]
rows: { key: string; sortOrder: number; cells: Record<string, Cell> }[]
}
const response = await fetch(
'https://api.greenmindmedia.com/api/platform/v1/config/tables/chest.drops',
{ headers: { Authorization: `Bearer ${accessToken}` } },
)
const table = (await response.json()) as ConfigTable
const weightOf = (row: string) => {
const cell = table.rows.find((each) => each.key === row)?.cells.weight
return typeof cell === 'number' ? cell : 0
}
Weighted rolls
A table whose rows are the things to roll and whose columns are weights (one per tier, level or zone) tunes a weighted roll without a build: pick the column for the roll, then draw a row with probability proportional to its cell. The editor can show each number's share of its column, which is the chance a roll in that column lands on the row.
Feature flags
A feature flag is a name that is on or off, managed on the environment's Flags page.
curl https://api.greenmindmedia.com/api/platform/v1/flags \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"items": [
{ "name": "new-shop", "isEnabled": false },
{ "name": "winter-event", "isEnabled": true }
],
"next": null
}
Either credential works. Treat a flag your client knows about but does not find as off.
Caching
Read config, tables and flags when the game starts or a match begins, not on every frame or request. Values change only when somebody edits them in the console.
Each of these answers carries an ETag. To check for a change cheaply, send it back as If-None-Match: the answer is 304 Not Modified, with no body, until something changes. A change made in the console reaches the API at once; one made some other way, such as a config migration, within 15 seconds.
curl -i https://api.greenmindmedia.com/api/platform/v1/flags \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'If-None-Match: "3x0Qj7Yk..."'