Skip to content

The roster model

The roster subsystem defines the two people-facing entity types: Gracz — the real person, keyed by their Margonem profile ID — and Postać — an in-fiction character owned by exactly one Gracz. This page specifies the exact contracts: the durable roster sections of the index, the retired Gracze.md adoption source and its seed parse rules, the roster invariants, and the API-managed Dodatkowe informacje entries. For the plain introduction, start with the operator page.

Concepts

  • Gracz — the real person. Identity is the Margonem profile ID (a stable integer like 9826541); nicks are fungible, and each Margonem account caps at six game characters. The friendly block heading (e.g. ### Eraster) is a display label.
  • Postać — an in-fiction character (e.g. Eraster, Losso Minewit, Felkissian). Linked to its owner by @należy_do; has its own entity record, currency holdings, session participation, and PU accounting.
  • Active character — each Gracz designates one "current" Postać: @status: Aktywny on that Postać, @status: Nieaktywny on the others. In Gracze.md this was encoded by bold-linking exactly one character.
  • PRFWebhook — the player's personal Discord webhook, carried as @prfwebhook on the Gracz block; PU and Intel notifications for any of that player's characters are delivered there. See Logs & Discord.
  • Tematy zastrzeżone — content triggers the player does not wish to see in sessions they take part in (e.g. Banany), carried as the typed scalar @tematy_zastrzeżone on the Gracz block and projected as RestrictedTopics. Narrators consult it when planning a session.

The split is deliberate: PU, the webhook, triggers, and identity attach to the person; currency, reputation, sessions, and the character sheet attach to the character. One person, several characters, one active.

The roster record: the durable index sections

The roster lives in the ## Gracze and ## Postacie Graczy sections of nerthus.entities.md. The import's bootstrap path seeds them from Gracze.md (Adoption); once present they are durable — never regenerated, and preserved on re-import because they are there. Operators hand-edit them freely (the index header invites it) and the daemon writes them through the roster API. The durable/machine section split and the block format itself (### heading + flat @tag bullets) are specified in the entity model.

## Gracze

### Eraster
- @typ: Gracz
- @margonemid: 9826541
- @prfwebhook: https://discord.com/api/webhooks/1211971813370892408/-7i1JynZrz…
- @status: Aktywny
- @tematy_zastrzeżone: Banany

## Postacie Graczy

### Eraster
- @typ: Postać
- @należy_do: Eraster
- @status: Aktywny
- @plik: /Postaci/Gracze/Eraster.md
- @pu_startowe: 20
- @pu_suma: 127.90
- @pu_zdobyte: 107.90
- @pu_nadmiar: 0.00

Eraster's other three Postacie (Losso Minewit, Felkissian, Klusp) follow the same shape with @status: Nieaktywny; legacy decimal scale is preserved as written (42.1, 0.00), and @pu_zdobyte is derived as SUMA − STARTOWE.

Field reference (Gracz / Postać entities; the full tag schema is in the tag reference):

Tag On Meaning
@typ both Gracz or Postać
@margonemid Gracz authoritative profile ID (identity key)
@należy_do Postać owning Gracz (links character → person)
@status both Aktywny / Nieaktywny / Usunięty — soft-delete only (entity model)
@alias Postać alternate names (Lord Tussal known as Tussal)
@plik Postać the character source file — the distribution target for sessions
@prfwebhook Gracz the player's Discord webhook — the committed index is its one and only source
@tematy_zastrzeżone Gracz the player's content triggers as a typed scalar; the Gracz block carries no freeform prose (projected as RestrictedTopics)
@pu_startowe @pu_suma @pu_zdobyte @pu_nadmiar Postać the four PU accounting fields — semantics owned by the PU model

Two rows carry a design decision:

  • A Gracz carries no @plik, so session distribution skips a Gracz participant. The legacy Gracze.md roster carried @plik: Gracze.md — distribution would then recreate the deleted Gracze.md on every settle run. The import drops the tag.
  • @prfwebhook resolution. Delivery looks up the entity's own @prfwebhook first, then the owning Gracz's, else it records a skip. The committed index is the one webhook source; the same URLs were already committed inside Gracze.md, so the exposure class is unchanged (Logs & Discord).

Identity over nick

The Gracz heading and @należy_do carry the friendly nick for human readability, but identity always reconciles to @margonemid; a nick change updates only the label and @alias. See identity & permissions.

The retired seed source: Gracze.md

Gracze.md was the hand-authored player database and the roster's adoption source. The import's bootstrap path reads it once; once the durable index sections above exist, they are the roster record and a re-import preserves them by presence (the bootstrap path no longer runs). The tool never writes to or deletes Gracze.md — retiring the file is the operator's own git commit after adoption, and git history preserves the archive. Adoption aborts with a structured RosterSourceMissing error only when there is neither a durable ## Gracze section nor a Gracze.md (Adoption).

The seed parses a fixed per-player block shape:

### Eraster
- Postaci:
    - **[Eraster](/Postaci/Gracze/Eraster.md)**:
        - PU: NADMIAR: 0.00, STARTOWE: 20, SUMA: 127.90
        - Wręczył Tussalowi łapówkę w postaci dziesięciu koron
    - [Losso Minewit](/Postaci/Gracze/Losso Minewit.md):
        - PU: NADMIAR: 0.00, STARTOWE: 40, SUMA: 42.1
    - [Felkissian](/Postaci/Gracze/Felkissian.md):
        - PU: NADMIAR: 0.00, STARTOWE: 41, SUMA: 52.95
    - [Klusp](/Postaci/Gracze/Klusp.md):
        - PU: NADMIAR: 0.00, STARTOWE: 64.00, SUMA: 65.40
- Tematy zastrzeżone: Banany
- PRFWebhook: https://discord.com/api/webhooks/1211971813370892408/-7i1JynZrz…
- ID Margonem: 9826541

Parse rules the seed applies:

Source line Meaning Maps to
### Eraster friendly label for the player block Gracz heading (display name)
**[Char](…)** (bold link) the active character Postać @status: Aktywny
[Char](…) (plain link) an inactive character Postać @status: Nieaktywny
(/Postaci/Gracze/Char.md) the character's source file Postać @plik
PU: NADMIAR: …, STARTOWE: …, SUMA: … per-character PU baseline @pu_nadmiar / @pu_startowe / @pu_suma (+ derived @pu_zdobyte)
Aliasy: Tussal alternate names Postać @alias
free bullet under a character (Wręczył Tussalowi łapówkę…) narrator note moved into the charfile's **Dodatkowe informacje:**
Tematy zastrzeżone: Banany content triggers (player-level) Gracz @tematy_zastrzeżone (typed scalar)
other player-level bullets (e.g. Tussal's - Inne:) roster notes moved into the player's active-character **Dodatkowe informacje:**
PRFWebhook: … personal Discord webhook Gracz @prfwebhook
ID Margonem: 9826541 authoritative identity Gracz @margonemid (a #char profile URL is not carried onto the block)

Reading notes:

  • The seed parses only the ## Lista section and skips fenced example blocks and unrecognized lines. The narrator-note bullets under each character are moved into that character's file, deduplicated against notes already there; the move mechanics (fail-early, per-bullet logging) belong to the import model.
  • The PU line accepts ZDOBYTE: in place of SUMA: — the seed reconstructs SUMA = STARTOWE + ZDOBYTE. Decimals accept dots or commas (42,142.1).
  • BRAK is the uninitialized sentinel (this page owns its semantics): STARTOWE: BRAK — or a missing PU line — emits no PU tags, except that a numeric SUMA beside STARTOWE: BRAK still emits @pu_suma; NADMIAR: BRAK seeds @pu_nadmiar: 0. A ripple worth knowing: on such a block the first monthly award computes @pu_zdobyte against a missing STARTOWE (treated as 0), so it lands as the full legacy total — set @pu_startowe by hand or API first when the baseline matters for a new-character STARTOWE preview.
  • ID Margonem may be a bare id or a profile URL (…/profile/view,9826541#…) — the numeric id is extracted either way and kept as @margonemid. The full URL's #char character-sheet anchor is not carried onto the Gracz block: the character's own **Karta Postaci:** link is that sheet's home, and git history keeps the retired Gracze.md verbatim. PRFWebhook: BRAK emits no @prfwebhook.
  • The PU values seeded here are the adoption baseline only; going forward PU is computed and authoritative from session @PU (PU model).
  • A player-character file with no roster entry still seeds as a Postać — @typ/@status/@plik only, no @należy_do. Files under Postaci/Gracze/Martwi/ seed as Nieaktywny.

The character file: Postaci/Gracze/<Char>.md

Each Postać has a hand-authored Markdown file. It is a human-authored Polish source — the daemon reads it, distributes sessions into it (resolved via @plik), and manages the Dodatkowe informacje entries in place — but it never rewrites the freeform prose. Bold-label fields:

Label Contents
**Karta Postaci:** link to the freeform character sheet on the Margonem profile (e.g. https://www.margonem.pl/profile/view,9826541#char_22788,nerthus)
**Tematy zastrzeżone:** optional character-level trigger restatement
**Stan:** current condition (Zdrowy., Brak., injury notes)
**Przedmioty specjalne:** narratively significant items (everyday holdings live in entity currency/items)
**Reputacja:** three buckets — Pozytywna / Neutralna / Negatywna, each a per-town list (Eder, Torneg, Ithan, …)
**Dodatkowe informacje:** standing facts and hooks (blessings, debts, owed items) — API-managed entries, below
**Opisane sesje:** a [[_TOC_]] followed by one block per session

The Karta Postaci is freeform and external (the Margonem profile); the tool tracks only the link — modeling statistics or the diceless ruleset is a deliberate non-goal (Architecture).

Under **Opisane sesje:**, session distribution appends one block per session the character took part in, keyed by the universal session header and copied verbatim from the session record — the character file is a projection, not an authority. Distribution mechanics: the session model.

Dodatkowe informacje — the API-managed entries

Each bullet under **Dodatkowe informacje:** is one entry the daemon lists, adds, edits, and removes in place (the reversed legacy label **Informacje dodatkowe:** is accepted on read — one legacy charfile uses it), through Get-NerthusCharacterInfo, Add-NerthusCharacterInfo, Set-NerthusCharacterInfo, and Remove-NerthusCharacterInfo. An entry may carry the standard temporal (from:to) range, filtered by the all-active rule (entity model). An edit or removal must restate the entry's current text and is refused on a mismatch, so concurrent operators never silently overwrite each other. Entry text is a single line by contract — the API refuses embedded newlines, so an entry can never smuggle a fake session heading into the sheet. The listed text is authoritative; the range-stripped value also drops a trailing colon-bearing parenthetical, while matching and filtering stay exact. The import moves the Gracze.md narrator notes into this section during adoption — a character's own notes into that character's file, and each player's free roster notes (- Inne: …) into their active character's file (Adoption). Routes, bodies, and capabilities: the API reference.

Behavior & invariants

  • One owner, one active. Every Postać has exactly one @należy_do. Setting a Postać's @status to Aktywny demotes the owner's previously active Postać to Nieaktywny in the same write; both changes are audited. Creation is the one path that does not demote — a new Postać seeds the entity-create default @status: Aktywny without touching siblings, so promote/demote explicitly after creating an additional Postać. Status is a temporal tag — each flip is a dated append, never an overwrite (entity model).
  • Soft-delete only. Removing a Gracz or Postać is a @status: Usunięty transition (entity model); the record and its file persist.
  • The durable sections are the roster record. All roster mutations land in ## Gracze / ## Postacie Graczy (above); the tool never touches Gracze.md.
  • PU is computed, not edited. Roster cmdlets do not set PU; it flows from the monthly batch (PU model). Creating a Postać seeds no PU tags — Get-NerthusNewPlayerCharacterPUCount previews the policy-derived STARTOWE, which the operator passes explicitly (e.g. -Tags @{ pu_startowe = '41' }).
  • Margonem id at creation. Creating a Gracz does not require @margonemid, but a Gracz without one cannot be identity-linked (the Margonem/Discord login, identity & permissions) — pass it in -Tags at creation.
  • Ownership-scoped writes. Roster writes — the webhook and character-info routes included — require the player.write capability (player.write.own for the gracz role, scoped to the caller's own Gracz and Postacie) — the scoping rules are owned by identity & permissions; capability ids are listed in the capability reference.
  • Single write gate. Every roster mutation passes the write gate and fires the write hooks — see the API reference.

Cmdlet surface

Players: Get-NerthusPlayer, New-NerthusPlayer, Set-NerthusPlayer, Remove-NerthusPlayer. Player webhook: Get-NerthusPlayerWebhook, Set-NerthusPlayerWebhook (an empty value clears it to BRAK). Characters: Get-NerthusPlayerCharacter, New-NerthusPlayerCharacter, Set-NerthusPlayerCharacter, Remove-NerthusPlayerCharacter. Character info entries: Get-NerthusCharacterInfo, Add-NerthusCharacterInfo, Set-NerthusCharacterInfo, Remove-NerthusCharacterInfo. Starting-PU preview: Get-NerthusNewPlayerCharacterPUCount. A character's owning Gracz is also resolvable through the API's entity surface. Routes, parameters, envelopes, and capabilities: the API reference.

Examples

Eraster is one Gracz (@margonemid: 9826541) owning four characters, with Eraster active:

PS> Get-NerthusPlayerCharacter -Name Eraster | Format-Table Name, Status, PuSuma

Name          Status     PuSuma
----          ------     ------
Eraster       Aktywny    127.90
Losso Minewit Nieaktywny 42.1
Felkissian    Nieaktywny 52.95
Klusp         Nieaktywny 65.40

Switching the active character (demotes Eraster, promotes Felkissian in one write):

PS> Set-NerthusPlayerCharacter -Name Felkissian -Tag status -Value Aktywny

Previewing the starting PU for a new character of Eraster's, then passing it at creation:

PS> Get-NerthusNewPlayerCharacterPUCount -Player Eraster
PS> New-NerthusPlayerCharacter -Name 'Felkissian' -Tags @{ 'należy_do' = 'Eraster'; plik = '/Postaci/Gracze/Felkissian.md'; pu_startowe = '41' }

Reading Eraster's webhook and adding a Dodatkowe informacje entry:

PS> Get-NerthusPlayerWebhook -Name Eraster
PS> Add-NerthusCharacterInfo -Name Eraster -Text 'Wręczył Tussalowi łapówkę w postaci dziesięciu koron'

Design decisions

  • Nick-change history. Old nicks are retained as @alias; a dated nick-change ledger is deferred to identity work (identity & permissions).
  • Player-level vs character-level triggers. The Gracz value is authoritative; character-file restatements are informational.
  • Creation vs the one-active flag. Enforced on update, not creation — see "One owner, one active" above.
  • The roster record. Adoption retires Gracze.md; the durable index sections are the roster record; deleting the file is the operator's commit.
  • Webhook storage. @prfwebhook on the Gracz block in the committed index is the one and only source; the webhook secrets file is retired.

See also