Files
MAGI/docs/DOCUMENTATION.md

871 lines
24 KiB
Markdown
Raw Normal View History

2026-03-16 21:38:49 +01:00
# Guildlib v2 — Complete Documentation
---
## Table of contents
1. [How it works — overview](#1-how-it-works)
2. [Unity attributes — complete reference](#2-unity-attributes)
3. [Creating your own shards and types](#3-creating-shards-and-types)
4. [Platform and tag system](#4-platform-and-tag-system)
5. [The sync cycle — step by step](#5-the-sync-cycle)
6. [Server API reference](#6-server-api-reference)
7. [Web editor guide](#7-web-editor-guide)
8. [Local testing (Windows / PowerShell)](#8-local-testing)
9. [Docker deployment](#9-docker-deployment)
10. [Changing things — common tasks](#10-changing-things)
---
## 1. How it works
```
Unity C# classes
│ [ShardTarget], [GuildLabel], [GuildPlatforms] …
│ (attributes on your classes describe everything)
SchemaExporter.Export()
│ produces schema.json
POST /schema ──────────────────────────► Bun server
│ stores schema.json
│ auto-creates .sqlite per shard
Web editor reads schema.json and renders type-safe forms
Users create / edit entries in the browser
Changes stored in weapons.sqlite, audio.sqlite, etc.
Unity Sync Panel clicks "Pull All Shards":
1. GET /manifest/weapons → [{id, version, checksum}] (lightweight!)
2. diff against local cache
3. POST /pull { ids: [only what changed] }
4. receive only changed rows
5. upsert into local weapons.sqlite
```
Key design decisions inherited from the Perforce paper:
- **Manifest-first**: only checksums cross the wire on the first pass.
No data is transferred if nothing changed.
- **Shard independence**: each shard is a completely separate .sqlite file.
You can pull just `audio` without touching `weapons`.
- **Server-side filtering**: platform and tag filters are applied on the server
before rows are returned, so a Switch build only downloads Switch data.
---
## 2. Unity attributes
All attributes live in `Guildlib.Runtime`. Import the namespace at the top of
your C# files.
### `[ShardTarget("name")]` ← **REQUIRED**
Declares which shard database this type belongs to.
```csharp
[ShardTarget("characters")]
public class CharacterStats : DataEntry { ... }
[ShardTarget("ui")]
public class UIButtonConfig : DataEntry { ... }
[ShardTarget("vfx_particles")] // underscores OK, lowercase
public class ParticlePreset : DataEntry { ... }
```
Rules:
- Lowercase, no spaces, no special characters except underscore
- The server auto-creates a new `<name>.sqlite` file the first time an entry
of that type is pushed — you do not need to pre-register shards anywhere
- If two classes share a `[ShardTarget]` they live in the same database file
(the shard is a file, not a type)
---
### `[GuildLabel("Display Name")]` ← optional
Sets the human-readable name shown in the web editor's type list.
Defaults to the C# class name if omitted.
```csharp
[ShardTarget("characters")]
[GuildLabel("Character Stats")]
public class CharacterStats : DataEntry { ... }
// Shows as "Character Stats" in the web editor, not "CharacterStats"
```
---
### `[GuildDescription("...")]` ← optional
A one-sentence description shown in the web editor's form panel under the type name.
Use it to explain what this entry type is for.
```csharp
[GuildDescription("Defines base stats for all playable and enemy characters.")]
public class CharacterStats : DataEntry { ... }
```
---
### `[GuildPlatforms("p1", "p2", ...)]` ← optional
Sets the **default** platform tags for newly created entries of this type.
Can be overridden per-entry in the web editor.
```csharp
// This entry type ships on all platforms by default
[GuildPlatforms("all")]
public class CharacterStats : DataEntry { ... }
// PC and console only — Switch gets different data
[GuildPlatforms("pc", "ps5", "xbox_series")]
public class HighResTextureConfig : DataEntry { ... }
// Switch-specific preset
[GuildPlatforms("switch", "mobile_ios", "mobile_android")]
public class LowResTextureConfig : DataEntry { ... }
```
If omitted, defaults to `["all"]` — meaning every platform gets this data.
---
### `[GuildTags("t1", "t2", ...)]` ← optional
Sets the **default** content/build tags for new entries of this type.
Can be overridden per-entry in the web editor.
```csharp
[GuildTags("base_game", "release")]
public class CharacterStats : DataEntry { ... }
[GuildTags("dlc_1")]
public class DLC1Character : CharacterStats { ... }
[GuildTags("debug")]
public class DebugTestCharacter : CharacterStats { ... }
```
---
### `[GuildDisplayName]` ← optional, on a field
Marks one field as the "display name" for the entry list in the web editor.
Without this, the entry ID is shown. Put it on whichever field is most useful
as a human-readable label.
```csharp
public class CharacterStats : DataEntry
{
[GuildDisplayName]
public string characterName; // web editor shows this in the list
public float maxHealth;
}
```
---
### `[GuildEnum("A", "B", "C")]` ← optional, on a string field
Constrains a string field to a fixed set of options.
The web editor renders a dropdown instead of a plain text input.
```csharp
[GuildEnum("Warrior", "Mage", "Rogue", "Ranger")]
public string characterClass;
[GuildEnum("Common", "Uncommon", "Rare", "Epic", "Legendary")]
public string rarity;
[GuildEnum("Fire", "Ice", "Lightning", "Physical", "Poison")]
public string damageType;
```
---
### `[GuildRef("shard_name")]` ← optional, on a string field
Marks a string field as a foreign key reference to an entry in another shard.
The web editor shows an annotated hint (full picker UI is a planned enhancement).
The actual stored value is still the entryId string.
```csharp
public class CharacterStats : DataEntry
{
[GuildRef("audio")]
public string deathSoundId; // points to an entry in audio.sqlite
[GuildRef("models")]
public string modelId; // points to an entry in models.sqlite
}
```
---
### `[GuildMultiline]` ← optional, on a string field
Makes the web editor render a textarea instead of a single-line input.
Use for long descriptions or multi-line text content.
```csharp
[GuildMultiline]
public string loreText;
[GuildMultiline]
public string debugNotes;
```
---
### `[GuildExclude]` ← optional, on a field
Excludes a field from schema export and sync entirely.
Use for Unity-only fields (Texture2D refs, EditorGUILayout state, etc.)
that cannot be serialised to the database.
```csharp
[GuildExclude]
public Texture2D previewTexture; // Unity asset ref — never synced
[GuildExclude]
public bool editorFoldout; // editor UI state — never synced
```
---
## 3. Creating shards and types
### Step 1 — Write a C# class
Create a new .cs file anywhere under `Assets/` in your Unity project.
```csharp
using UnityEngine;
using Guildlib.Runtime;
// ── A brand new shard called "abilities" ─────────────────────────────────────
[ShardTarget("abilities")]
[GuildLabel("Ability Definition")]
[GuildDescription("Defines a single player or enemy ability.")]
[GuildPlatforms("all")]
[GuildTags("base_game", "release")]
[CreateAssetMenu(menuName = "Guildlib/Ability")]
public class AbilityDefinition : DataEntry
{
[GuildDisplayName]
public string abilityName;
[GuildMultiline]
public string description;
[GuildEnum("Active", "Passive", "Toggle")]
public string abilityType;
public float cooldown;
public float manaCost;
public int maxLevel;
[GuildRef("audio")]
public string castSoundId;
[GuildRef("vfx_particles")]
public string castVFXId;
}
// ── A child type — inherits all AbilityDefinition fields ─────────────────────
[ShardTarget("abilities")]
[GuildLabel("Area Ability")]
[GuildDescription("An ability that affects an area rather than a single target.")]
public class AreaAbility : AbilityDefinition
{
public float radius;
public bool falloffDamage;
[GuildEnum("Circle", "Cone", "Rectangle")]
public string areaShape;
}
```
### Step 2 — Upload the schema
Open **Window → Guildlib → Sync Panel**, then click **Upload Schema**.
This sends all your C# type definitions to the server. The server stores
the schema as `config/schema.json`. The web editor reads this file to know
what types exist, what their fields are, and what options are available.
The `abilities` shard now exists on the server. Its `.sqlite` file is created
automatically the first time an entry is pushed.
### Step 3 — Use in the web editor
Refresh the web editor. You will see the `abilities` shard appear in the left
column. Click it to see `Ability Definition` and `Area Ability` in the type tree.
### Inheritance rules
- A child type appears nested under its parent in the web editor
- The form shows inherited fields first (marked "inherited"), then own fields
- Child types go into the **same shard** as their parent
- You can nest arbitrarily deep: `DataEntry → Ability → AreaAbility → AOEBomb`
- Multiple children of the same parent are all shown in the tree
---
## 4. Platform and tag system
### What they are
**Platforms** describe which shipping targets an entry is relevant for.
**Tags** describe which content group, build configuration, or DLC an entry belongs to.
Both are stored per-entry (not per-type) and can be edited in the web editor.
Type-level `[GuildPlatforms]` and `[GuildTags]` attributes only set the default
for newly created entries — users can change them freely in the web editor.
### Defining your vocabulary
Both lists live in `DataEntry.cs`:
```csharp
public static class GuildPlatformRegistry
{
public static readonly string[] All = new[]
{
"pc",
"ps5",
"ps4",
"xbox_series",
"xbox_one",
"switch",
"mobile_ios",
"mobile_android",
"vr",
};
}
public static class GuildTagRegistry
{
public static readonly string[] All = new[]
{
"release",
"debug",
"demo",
"base_game",
"dlc_1",
"dlc_2",
"high",
"medium",
"low",
"global",
"region_jp",
"region_eu",
"region_na",
};
}
```
**To add a platform or tag**: add a string to the appropriate array and run
**Upload Schema** from the Sync Panel. The web editor will show the new option
immediately.
**To remove one**: remove it from the array and re-upload the schema. Existing
entries that had the old value keep it stored in the database — the web editor
won't show the removed value as a selectable checkbox, but the data is preserved.
### The special "all" platform
An entry tagged `"all"` is returned for every platform filter. This means if
you add a new platform later, entries tagged "all" are automatically included
without any data migration.
### Using platform/tag filters in Unity
In the Sync Panel:
1. Toggle on **Filter by platform**
2. Enter comma-separated platforms: `pc,ps5`
3. Toggle on **Filter by tag** (optional)
4. Enter tags: `release,base_game`
5. Click **Pull All Shards**
Only rows matching the filters come down the wire.
Programmatically from C#:
```csharp
var client = new ShardClient(config, "abilities");
// Pull only PC/PS5 release content
await client.PullAsync(
platformFilter: new List<string> { "pc", "ps5" },
tagFilter: new List<string> { "release", "base_game" }
);
// Pull everything (no filter)
await client.PullAsync();
```
### Using filters on the server API directly
```
GET /entries/abilities?platforms=pc,ps5&tags=release,base_game
POST /pull { "shard":"abilities", "ids":[...], "filterPlatforms":["switch"], "filterTags":["base_game"] }
```
---
## 5. The sync cycle
### Full cycle (first time or clean slate)
```
1. GET /manifest/weapons
← { shard:"weapons", rows: [] } ← empty, nothing on server yet
2. (nothing to diff — skip pull)
3. (user creates entries via web editor)
4. GET /manifest/weapons
← { rows: [{id:"abc", version:1, checksum:"d41d..."}, ...] }
5. Diff: local has nothing → need all IDs
6. POST /pull { shard:"weapons", ids:["abc", ...] }
← { rows: [{id:"abc", weaponType:"Sword", damage:45, ...}, ...] }
7. Upsert into local weapons.sqlite
8. Save manifest cache
```
### Incremental sync (normal operation)
```
1. GET /manifest/weapons
← 10,000 rows of {id, version, checksum}
2. Compare with local manifest:
- 9,995 rows unchanged → skip
- 5 rows have newer version → add to needIds
3. POST /pull { ids: ["id1","id2","id3","id4","id5"] }
← only those 5 rows
4. Upsert 5 rows locally
5. Update manifest cache
```
This means a sync where nothing changed transfers only the manifest
(a flat JSON list of id/version/checksum — a few KB even for large datasets)
and then completes immediately.
---
## 6. Server API reference
All endpoints accept and return `application/json`.
If `GUILDLIB_API_KEY` is set, include `X-Api-Key: <key>` on every request
except `/health`.
| Method | Path | Description |
|--------|------|-------------|
| GET | `/health` | Server status, version, shard list |
| GET | `/manifest/:shard` | Lightweight manifest for one shard |
| POST | `/pull` | Fetch rows by ID list with optional filters |
| POST | `/push` | Upsert one entry from Unity |
| POST | `/schema` | Upload schema.json from Unity |
| GET | `/schema` | Retrieve current schema.json |
| GET | `/entries/:shard` | List entries (supports query params) |
| POST | `/entries/:shard` | Create entry |
| GET | `/entries/:shard/:id` | Single entry |
| PUT | `/entries/:shard/:id` | Update entry |
| DELETE | `/entries/:shard/:id` | Soft-delete entry |
### `/manifest/:shard`
```
GET /manifest/weapons
→ {
"shard": "weapons",
"rows": [
{ "id": "abc123", "version": 3, "checksum": "d41d8cd9..." },
...
]
}
```
### `/pull`
```
POST /pull
{
"shard": "weapons",
"ids": ["abc123", "def456"],
"filterPlatforms": ["pc", "ps5"], // optional
"filterTags": ["release"] // optional
}
→ {
"shard": "weapons",
"rows": [
{ "id":"abc123", "type_name":"...", "platforms":["all"],
"tags":["release","base_game"], "weaponType":"Sword", ... },
...
]
}
```
### `/entries/:shard` (list)
Query params:
- `type` — filter by C# type name
- `platforms` — comma-separated platform filter
- `tags` — comma-separated tag filter
- `parent` — filter by parent entry ID
- `limit` — default 100
- `offset` — default 0
```
GET /entries/weapons?type=Guildlib.Runtime.SwordData&platforms=pc,ps5&tags=release
```
### `/entries/:shard` (create)
```
POST /entries/weapons
{
"typeName": "MyGame.AbilityDefinition",
"platforms": ["all"],
"tags": ["base_game", "release"],
"data": {
"abilityName": "Fireball",
"cooldown": 3.5,
"manaCost": 25
}
}
→ { "id": "generated-uuid", "version": 1 }
```
### `/entries/:shard/:id` (update)
```
PUT /entries/weapons/abc123
{
"platforms": ["pc", "ps5"],
"tags": ["release"],
"data": { "cooldown": 4.0 }
}
```
Only fields present in `data` are merged. Other fields are preserved.
---
## 7. Web editor guide
Open `web/index.html` in Chrome or Edge (no build step, no server needed for the file itself).
### Connecting
Enter your server URL (default `http://localhost:3000`) and click **Connect**.
The editor fetches `/health` to verify, then loads `/schema` to build the UI.
### Left column — shard and type tree
Lists all shards the schema describes. Each shard has a colour dot.
Click a shard to expand its type tree. Types are shown as root + children:
```
weapons
├─ WeaponData
│ └─ SwordData [child]
│ └─ DaggerData [child]
└─ ...
abilities
├─ AbilityDefinition
│ └─ AreaAbility [child]
```
Click any type to load its entries.
### Middle column — entry list
Shows all entries of the selected type. Each row shows:
- The display name (from `[GuildDisplayName]` field, or entry ID)
- A short ID chip
- Row version
- Up to 3 platform tags (blue)
- Up to 2 content tags (yellow)
Use the search box to filter by display name.
### Right column — entry form
**Platform tags**: checkbox grid of all defined platforms. Click to toggle.
Click "all" to mark as platform-agnostic.
**Content tags**: checkbox grid of all defined tags. Click to toggle.
**Fields**: rendered based on the type's field schema:
- `[GuildEnum]` → dropdown
- `bool` → toggle switch
- `int`/`float` → number input
- `string[]` → textarea (one value per line)
- `[GuildMultiline]` → textarea
- `[GuildRef]` → text input with shard hint
- Everything else → text input
Inherited fields appear first with an "inherited" badge.
Click **Create entry** or **Save changes** to persist. Click **Delete** to soft-delete
(the row is marked `deleted=1` and version-bumped so Unity clients know to remove it).
---
## 8. Local testing (Windows / PowerShell)
### Install Bun (once)
```powershell
# Run as Administrator
powershell -c "irm bun.sh/install.ps1 | iex"
# Close and reopen PowerShell, then verify:
bun --version
```
### Start the server
```powershell
cd C:\path\to\guildlib-v2\server
bun install # first time only
New-Item -ItemType Directory -Force -Path ".\shards",".\config"
$env:GUILDLIB_API_KEY = "" # blank = auth disabled for local dev
$env:SHARD_DIR = ".\shards"
$env:SCHEMA_PATH = ".\config\schema.json"
bun run dev
```
Expected output:
```
[Guildlib] :3000 shards: auth: OFF
[ShardManager] Opened: weapons.sqlite
[ShardManager] Opened: audio.sqlite
...
```
### Verify it works
```powershell
Invoke-RestMethod http://localhost:3000/health
```
### Push a test schema (without Unity)
```powershell
$schema = Get-Content -Raw "C:\path\to\guildlib-v2\docs\example-schema.json"
Invoke-RestMethod -Method POST `
-Uri http://localhost:3000/schema `
-ContentType "application/json" `
-Body $schema
```
Or use the example at the bottom of this document.
### Create an entry via PowerShell
```powershell
$body = '{"typeName":"MyGame.AbilityDefinition","platforms":["all"],"tags":["base_game"],"data":{"abilityName":"Fireball","cooldown":3.5}}'
Invoke-RestMethod -Method POST `
-Uri http://localhost:3000/entries/abilities `
-ContentType "application/json" `
-Body $body
```
### Test the delta sync
```powershell
# 1. Get manifest
Invoke-RestMethod http://localhost:3000/manifest/abilities
# 2. Pull specific IDs
$pull = '{"shard":"abilities","ids":["<paste-id-here>"]}'
Invoke-RestMethod -Method POST `
-Uri http://localhost:3000/pull `
-ContentType "application/json" `
-Body $pull
# 3. Test platform filter
$pull = '{"shard":"abilities","ids":["<id>"],"filterPlatforms":["switch"]}'
Invoke-RestMethod -Method POST `
-Uri http://localhost:3000/pull `
-ContentType "application/json" `
-Body $pull
```
---
## 9. Docker deployment
```bash
cd guildlib-v2/docker
# 1. Copy env template
cp .env.example .env
# Edit .env — set GUILDLIB_API_KEY to a real secret
# 2. Build and start
docker compose up -d
# 3. Check it's running
curl http://localhost:3000/health
```
The `guildlib-shards` and `guildlib-config` Docker volumes persist your data
across container restarts and updates.
To update the server:
```bash
docker compose pull
docker compose up -d
```
For HTTPS in production, uncomment the `caddy` block in `docker-compose.yml`
and set `DOMAIN=yourdomain.com` in `.env`.
---
## 10. Changing things — common tasks
### Add a new shard
Just write a class with `[ShardTarget("new_shard_name")]` and upload the schema.
No server config changes needed. The `.sqlite` file is created automatically
on first push.
### Add a new platform
1. Open `DataEntry.cs`
2. Add your platform string to `GuildPlatformRegistry.All`
3. Click **Upload Schema** in the Sync Panel
4. The web editor now shows the new platform as a checkbox
### Add a new tag
Same as adding a platform, but edit `GuildTagRegistry.All`.
### Add a field to an existing type
1. Add the `public` field to your C# class
2. Click **Upload Schema**
3. Existing entries in the database keep all their old data — the new field
will be empty/default until entries are edited and re-saved
### Rename a field
Rename it in C# and re-upload the schema. The old field name remains in existing
database rows (the data is stored as JSON internally). Use the web editor to
open and re-save affected entries so they write the new field name.
### Remove a field
Remove it from C# and re-upload. Existing data with the old field name is
preserved in the database JSON blob but won't appear in the web editor forms.
It won't be synced to new Unity clients.
### Change a shard name
This is a breaking change. Rename the `[ShardTarget]` value, re-upload schema,
then manually copy the old `.sqlite` file on the server to the new name.
The server will open it automatically on restart.
### Add a child type
Subclass an existing DataEntry subclass, give it the same `[ShardTarget]`,
add its own fields, and upload the schema. It appears nested in the web editor
under its parent type immediately.
---
## Example schema JSON (for testing without Unity)
Save this as `example-schema.json` and POST it to `/schema`:
```json
{
"version": "2",
"exportedAt": 1710000000,
"unityProjectId": "test",
"shards": ["abilities", "characters"],
"platformConfig": {
"platforms": ["pc","ps5","xbox_series","switch","mobile_ios"],
"tags": ["release","debug","demo","base_game","dlc_1","high","low"]
},
"types": [
{
"typeName": "MyGame.AbilityDefinition",
"displayName": "Ability Definition",
"description": "Defines a single player or enemy ability.",
"shard": "abilities",
"parentType": null,
"childTypes": ["MyGame.AreaAbility"],
"defaultPlatforms": ["all"],
"defaultTags": ["base_game", "release"],
"fields": [
{"name":"abilityName","type":"string","isInherited":false,"isDisplayName":true},
{"name":"description","type":"string","isInherited":false,"isMultiline":true},
{"name":"abilityType","type":"string","isInherited":false,"enumOptions":["Active","Passive","Toggle"]},
{"name":"cooldown","type":"float","isInherited":false},
{"name":"manaCost","type":"float","isInherited":false},
{"name":"maxLevel","type":"int","isInherited":false},
{"name":"castSoundId","type":"string","isInherited":false,"refShard":"audio"}
]
},
{
"typeName": "MyGame.AreaAbility",
"displayName": "Area Ability",
"description": "An ability that affects an area.",
"shard": "abilities",
"parentType": "MyGame.AbilityDefinition",
"childTypes": [],
"defaultPlatforms": ["all"],
"defaultTags": ["base_game", "release"],
"fields": [
{"name":"abilityName","type":"string","isInherited":true,"isDisplayName":true},
{"name":"cooldown","type":"float","isInherited":true},
{"name":"radius","type":"float","isInherited":false},
{"name":"falloffDamage","type":"bool","isInherited":false},
{"name":"areaShape","type":"string","isInherited":false,"enumOptions":["Circle","Cone","Rectangle"]}
]
},
{
"typeName": "MyGame.CharacterStats",
"displayName": "Character Stats",
"description": "Base stats for all characters.",
"shard": "characters",
"parentType": null,
"childTypes": [],
"defaultPlatforms": ["all"],
"defaultTags": ["base_game", "release"],
"fields": [
{"name":"characterName","type":"string","isInherited":false,"isDisplayName":true},
{"name":"maxHealth","type":"float","isInherited":false},
{"name":"moveSpeed","type":"float","isInherited":false},
{"name":"characterClass","type":"string","isInherited":false,"enumOptions":["Warrior","Mage","Rogue","Ranger"]},
{"name":"loreText","type":"string","isInherited":false,"isMultiline":true}
]
}
]
}
```