Docs / Developer / RoleCall format
RoleCall format
RoleCall is Vaudeville Studios' own roleplay client. It has no wire-distinct "RC character" file: an RC
character serializes as an ordinary Character Card V2/V3 (PNG chara/ccv3 chunk or bare JSON), with
every RC-native field riding inside data.extensions.rolecall. Its lorebook is a dedicated v1 export
envelope. Its persona, regex-script, and preset exports are separate JSON shapes. Because RoleCall is Vaudeville's
own product, its source can be read and ported freely, unlike the GPL-licensed formats bunzai treats as
interop-facts-only.
One folder, src/formats/rolecall/, default-exports seven codecs:
rolecall, kindcharacter, reads PNG or JSON, writes.json.rolecall-lorebook, kindlorebook, reads JSON, writes.json.rolecall-compendium, kindlorebook, reads and writes RoleCall's compendium export (.compendium.json).rolecall-persona, kindpersona, reads JSON, writes.json.rolecall-regex, kindregex, reads JSON, writes.json.rolecall-preset, kindpreset, reads a RoleCall-flavored flat preset, writes.json.rolecall-script-sheet, kindscriptsheet, reads a RoleCall preset file that carries a Script Sheet, writes.json.
This page documents the character and lorebook codecs in full; the persona, regex, preset, and Script Sheet codecs are covered
in summary under Quirks. For canonical field meanings, see
entities/character.md, entities/lorebook.md, and
entities/regex.md, entities/persona.md, and
entities/preset.md. For the hub-and-spoke, escrow, and
detection model, see architecture.md. For the whole matrix of formats, see
FORMAT-SUPPORT.md.
Detection
The registry runs every adapter's detect() and keeps the single highest score above 0.5
(architecture.md, "Detection and the registry").
Character
detect() returns 1.0 for a CCv2/V3 card that carries an extensions.rolecall block, 0 otherwise
(index.ts:348-356). It requires spec to be chara_card_v3 or chara_card_v2, a data object, and arolecallExt(data) result whose type is not the literal "persona" (index.ts:352-355, rolecallExt
at index.ts:92-95). The 1.0 outranks the generic SillyTavern reader's 0.9 on the same card
(sillytavern.md, "Detection"; architecture.md, "Detection and the registry").
Cross-kind firewall. RC's own persona-export route emits this exact same CCv2/V3 shape, withextensions.rolecall.type set to the literal "persona" - a PERSONA, not a character. The character
adapter's detect() returns 0 on that discriminator, and toCanonical throws pointing atrolecall-persona, instead of the two adapters tying at 1.0 on the same file (index.ts:355,366). See
"Persona codec" under Quirks.
Lorebook
detect() returns 1.0 for a RoleCall v1 export - a { schemaVersion, lorebook } envelope whoselorebook.entries is an array and lorebook is present as an object - 0 otherwise
(lorebook.ts:308-312, readExport at lorebook.ts:37-45). isRcVersion requires schemaVersion to be
a string that contains "1.0.0", not an exact match (lorebook.ts:27,48-49) - a substring test, so a
value like "1.0.0-beta" also passes; the codec never hard-rejects an unfamiliar version, it just parses
structurally. exportDate is optional for detection and import. This envelope shape (top-levelschemaVersion plus a nested lorebook wrapper) does not
collide with the SillyTavern worldbook, which has a top-level entries object and no lorebook wrapper at
all - no firewall is needed between the two lorebook codecs.
The compendium export (rolecall-compendium, compendium.ts) is a different, flat file: format: "rolecall-compendium" or a compendium- schemaVersion, a lorebook object, and the entries as raw database rows at the top level beside categories, tree and nodeConstraints. detect() returns 1 for it and 0 for anything else; the v1 lorebook codec returns 0 for it (it has no nested lorebook.entries). Reading maps each row's snake_case columns to the canonical entry (a cell of the wrong kind falls back to the field's default, an unknown position or role word too), restores is_featured as metadata.featured, and keeps the parsed file as the original; writing back overwrites only the columns that changed, so RoleCall-only columns, the tree and the node constraints survive. A Bunzai export carries tree: null. Bunzai's Dashboard EXPORT streams this file entry by entry.
Field map
Character
The shared Tavern data mapping lives in src/formats/_shared/tavern-fields.ts (dataToBody on import,applyBodyToData on export) and is documented in full in sillytavern.md. This
table covers only what the RC layer adds on top, read out of extensions.rolecall (rolecallExt,index.ts:92-95) and its nested details casting-card object (RcDetails, index.ts:53-75).
| Source field | Canonical path | Notes |
|---|---|---|
extensions.rolecall.tagline |
identity.tagline |
cardToBody, index.ts:154 |
extensions.rolecall.genre |
discovery.genre |
index.ts:155 |
extensions.rolecall.fandom |
discovery.fandom |
index.ts:156 |
extensions.rolecall.content_rating |
discovery.rating |
Exact 3-way map, no boolean-nsfw inference: all_hours/late_night/after_dark <-> all-ages/mature/explicit (RATING_IN/RATING_OUT, index.ts:34-43,157,221). |
extensions.rolecall.source_url |
attribution.sourceUrl |
index.ts:158 |
extensions.rolecall.creators_note |
attribution.publicNote |
Public "from the creator" note on the card page; distinct from the base-spec data.creator_notes (index.ts:159). |
extensions.rolecall.details.full_name |
identity.fullName |
index.ts:163 |
extensions.rolecall.details.title |
identity.title |
index.ts:164 |
extensions.rolecall.details.age |
identity.age |
Free TEXT in RC ("23", "ageless"), never an int (index.ts:58,165). |
extensions.rolecall.details.pronouns |
identity.pronouns |
index.ts:166 |
data.alternate_greetings + extensions.rolecall.alternate_greeting_titles[] |
greetings.alternateGreetings[].title |
Parallel array merged in by index; title omitted per-greeting when its slot is null (titledGreetings, index.ts:97-108,168-169). |
extensions.rolecall.details.prompt_depth_injections[] |
prompts.depthInjections[] (origin rolecall_details) |
content->text, depth default 4, role default "system"; CONCATENATED with the shared extensions.depth_prompt entry (origin depth_prompt), details first (depthInjections, index.ts:110-121,174-175). See Escrow and round-trip for the write-side asymmetry. |
extensions.rolecall.details.{signature_color,gradient_colors,colors,fieldOrder,default_background,publicDefinitionDisplay,media_links} |
presentation.* |
The casting-card layer: signature color, gradient, palette, background ref, field display order, spoiler config, external media links (presentation, index.ts:123-149). |
data.assets[] |
media.portrait + media.assets[] |
CCv3 asset array via the shared assetsToMedia, dialect "rolecall"; real RC emits expression sprites as type:"expression", mapped to canonical role "emotion" (assets.ts:69-78,147-148, index.ts:180-181). |
| PNG carrier pixels | original.rolecall.sourceMedia |
The PNG file is kept as a raw-bytes twin (pngSourceMedia, png.ts:39-42, index.ts:373). |
data.extensions.rolecall.details is not a flat object: full_name/title/age/pronouns land onidentity, everything else lands on presentation (see the row above). id, nsfw (the legacy boolean,
superseded by content_rating), image_url/thumbnail_url, loadout, recommendations,trackerPreset, and linkedLorebooks/linkedRegexScripts have no canonical slot on this codec today and
ride the whole-card escrow untouched; see Escrow and round-trip.
The portable data.character_book relationship is owned by the shared bundle layer rather than the
RoleCall extension block. Export re-embeds the resolved canonical lorebooks, while an explicitly empty
resolution removes a stale book inherited from the cloned card twin (index.ts:394-395,character-book.ts:462-478).
Lorebook
The canonical lorebook entry is flat, but the RC v1 wire nests each entry into named groups
(RoleCallEntryV1). The codec re-nests on export (entryToWire, lorebook.ts:191-254) and flattens on
import (entryToCanonical, lorebook.ts:67-138):
| Canonical | RC v1 wire group | Notes |
|---|---|---|
triggerMode, triggers, secondaryTriggers, selectiveLogic |
triggers.{mode,primary,secondary,selectiveLogic} |
lorebook.ts:88-91,201-206 |
caseSensitive, matchWholeWords, scanDepth |
matching.* |
lorebook.ts:93-95,207-211 |
position, depth, role |
injection.* |
lorebook.ts:97-99,212 |
sortOrder, priority |
priority.* |
lorebook.ts:101-102,213 |
sticky, cooldown, delay |
timing.* |
lorebook.ts:104-106,214 |
groupName, categoryId, groupWeight |
grouping.* |
lorebook.ts:108-110,215 |
useMemo, excludeRecursion, preventRecursion, delayUntilRecursion, ignoreBudget |
advanced.* |
lorebook.ts:114-117,126,217-223 |
characterFilter |
top-level characterFilter |
Via the shared parseCharacterFilter (lore-enums.ts:28-36, lorebook.ts:119,224-226). |
scanCharacterDescription, scanCharacterPersonality, scanUserPersona, scanScenario |
scanSources.* |
lorebook.ts:121-124,227-232 |
probability, sideEffects, metadata |
top-level probability/sideEffects/metadata |
sideEffects/metadata are additive: emitted only when present (lorebook.ts:112,135-136,216,235-238). |
vectorized, groupOverride, useGroupScoring, automationId, scanCharacterDepthPrompt, scanCreatorNotes |
unsupportedFields.{vectorized,groupOverride,useGroupScoring,automationId,matchCharacterDepthPrompt,matchCreatorNotes} |
ST-origin toggles RC preserves in its own escape hatch; first-classed here as canonical slots (lorebook.ts:128-133,239-252). |
Book-level settings map to lorebook.settings.*, lorebook.metadata.*; categories tolorebook.categories (bookToCanonical/bookToWire, lorebook.ts:140-177,256-300). RC v1 emits exactly
four scan sources (characterDescription, characterPersonality, userPersona, scenario) - canonicalscanPreset has no RC v1 wire slot and is not read or written by this codec.
Escrow and round-trip
The escrow envelope is the canonical wrapper's original field, keyed by format id (canonical.ts:39-51,80).
The architecture calls this concept "escrow"; the wrapper field is named original. The character and
lorebook codecs each store their twin under their own adapter id.
Character. On import, the whole parsed card rides in original.rolecall.raw, alongside the PNG
carrier when the input was a PNG (index.ts:367-374). On export, fromCanonical clones that raw card (or
starts a fresh V3 envelope when there is none), overlays the shared Tavern fields via applyBodyToData,
merges media via applyMediaToTavernData under dialect "rolecall", then overlays the RC layer viaapplyBodyToRcExt (index.ts:377-397). applyBodyToRcExt uses the same three-state overlay as the shared
writer for the four presentation fields it promotes to first-class slots (gradientColors, palette,background, fieldOrder): it decodes the twin's presentation BEFORE mutating details so "untouched",
"edited", and "cleared" are distinguished correctly even though it is rebuilding one nested JSON object in
place, not swapping a top-level key (index.ts:230-249,271-334). Scalars (tagline, genre, fandom,rating, sourceUrl, publicNote, the four details identity fields) are written whenever the canonical
value is defined; a cleared value that was previously on the twin is left as-is on these particular fields
rather than deleted (index.ts:214-226).
Depth-injection asymmetry. Import concatenates the RC-native rolecall_details-origin entries with
the single shared depth_prompt-origin entry (index.ts:110-121,174-175). Export only re-derives thedepth_prompt-origin entry, via the shared applyBodyToData, which finds the first canonical entry whoseorigin is "depth_prompt" and writes it back onto extensions.depth_prompt
(tavern-fields.ts:284-296). applyBodyToRcExt never writes details.prompt_depth_injections at all - so
editing, adding, or removing a rolecall_details-origin entry in canonical has no effect on export; that
wire slot keeps re-emitting verbatim from the untouched raw twin. This is confirmed by a regression test
that edits only the depth_prompt entry and asserts the details array still has its original length
(rolecall.test.ts:176-184).
Lorebook. The whole parsed export rides in original["rolecall-lorebook"].raw, so raw-only carriers
the canonical body does not model (the category tree, per-entry runtime junk inside unsupportedFields)
survive a round-trip verbatim. Export overlays the canonical body onto a clone of the raw wire and matches
entries by id; a from-scratch canonical (no escrow) serializes fresh defaults (lorebook.ts:314-336).budgetMode/entryBudget are written only when the twin already had them, or (for a from-scratch book)
only when they differ from their defaults - a real RC export omits both at defaults, and always writing
them would mutate an unedited real export by two fabricated keys (lorebook.ts:265-267,279-280).exportDate is preserved from the twin when present and only regenerated for a from-scratch export
(lorebook.ts:330-334) - note this differs from real
RoleCall, which always mints a fresh exportDate on every export; that behavior belongs to the source product's
serializer, not this codec, and code wins.
Quirks
rolecall-presetshares SillyTavern's flat preset grammar and is distinguished by RoleCall-native
keys such asmacro_engine_yaml,choice_groups, orextensions.linkedRegexScripts. A
from-scratch export writes an emptychoice_groupsarray when no native choices exist. That array
is a neutral RoleCall fingerprint, so the emitted file remains distinguishable from a generic
SillyTavern preset and can be read back by the same adapter (preset.ts).rolecall-script-sheetreads a RoleCall preset file whose top level carriesscript_sheet: { version: 1, content }
(orpromptbook, the key RoleCall used before its rename) as a Script Sheet, outrankingrolecall-presetfor such
files. It writes the sheet as RoleCall exports it: the blocks the sheet sends through the RoleCall preset writer,
withscript_sheetat the top (script-sheet.ts; entities/scriptsheet.md).- RC's V3
sourceis a nonstandard OBJECT array ([{name}], not the base spec'sstring[]). The shared
Tavern reader only decodes astring[]shape, so RC's object-formsourcereads as canonicalundefined; provenance is carried instead onattribution.creator. Because the shared writer's
three-state overlay only clears a slot it can represent, it never writes[]over a twin it decoded as
undefined, so the untouched object-formsourcerides the raw twin unmolested
(tavern-fields.ts:233-236). alternate_greeting_titlesis written only when at least one greeting actually carries a title; an
all-untitled greetings edit omits the key rather than writing an array ofnulls (index.ts:225-226).default_background's ref can live atcustomUrlorbackgroundId. Export keeps whichever slot the
twin already used; only a from-scratch background (no prior slot on either) falls back to sniffing the
ref string itself viaisUrlOrDataRef(index.ts:204-206,311-327).- Real RC sprites emit
type:"expression", nottype:"emotion"; the shared asset-role map accepts both
spellings and reads either to canonical role"emotion"(assets.ts:69-78,147-148). bunzai's own test
fixtures (as opposed to the samples undersamples/rolecall/) use the unrealistic"emotion"spelling -
seesamples/rolecall/SOURCES.md, "Why these differ from the in-repo bunzai fixtures". - Persona codec (
rolecall-persona, kindpersona). The USER-identity entity:{{user}}'s voice, first
person. Two shapes, both claimed at1.0(persona.ts:254-256): the nativercpersonaenvelope
({ spec: "rolecall_persona", spec_version, data },personaRcExt/detectShapeatpersona.ts:48-62)
and RC's live production export, achara_card_v2-lookalike whoseextensions.rolecall.typeis the
literal"persona"- the exact discriminator the character adapter checks to step aside (see
Detection).rcpersona:data.descriptionis the BRIEF (library blurb),data.contentthe injected text; whencontentis empty,sectionscompile in fixed order (appearance, body, personality, quirks, history)
as"Label: text"blocks (compileSections,persona.ts:69-80). Canonical keeps BOTHcontentandsectionsso the editor can offer structured editing either way.rc-v2-export: the V2descriptionslot is a FOLD ofcontentplusAppearance:/Body:paragraphs,
reversed on read byunfoldDescriptionand rebuilt on write byfoldDescription
(persona.ts:149-162,203-208);creator_notescarries the brief,personality/scenariocarry the
personality/history sections, and therolecallside-channel carries identity attrs
(tagline/age/height/pronouns), theming (signature_color/colors/image_url),section_order, lorebook
linkage (lorebook_id), andis_after_dark- OMITTED, never writtenfalse, when the rating is not
explicit or mature (persona.ts:164-242,238-239).- The load-bearing rule across both shapes:
briefandcontentnever swap - that exact swap was a live
RC production bug, fixed and pinned by RC's own persona-roundtrip suite; bunzai replicates the fixed
behavior (persona.ts:1-11). Legacy library-export wrappers ({ exportedAt, type, data }then{ persona }) unwrap outer-first before shape matching (unwrapPersonaJson,persona.ts:35-41). A
missing name fails the parse - never synthesized (persona.ts:264). - Not built in this codec: an
aspirational PNG-keyword precedence reader (ccv3>chara>rcpersona>persona) and a legacy
RoleOut import path. Neither exists in this repo's code: the sharedreadCardJsonused by both the
character and persona adapters only ever extracts achara/ccv3tEXt chunk
(card-io.ts:9-21,png.ts:52-65); a PNG carrying only anrcpersonachunk is not read by any codec
here today. Flagged as design intent, not verified against source.
- Regex codec (
rolecall-regex, kindregex). Models RC's live chat-pipeline subsystem only (subsystem A,find_pattern/replace_string/placement[]); a documented-dead second subsystem in RC's own source is
not modeled here at all (regex.ts:1-27).detect()scores0.9for a script object whoserules[]is
non-empty and every rule looks RC-shaped; an array of scripts is deliberately not claimed, since one file
is one entity and a multi-script dump would be several sets in one file (regex.ts:176-200,208-210).
Placements map 1:1 to canonical phases (user_input/ai_output/display_only/prompt_only/lorebook/reasoning<->input/output/display/prompt/lorebook/reasoning,regex.ts:35-64); an unmapped
placement string passes through verbatim (open union, forward-compatible), and a canonical phase this
codec cannot map back to a placement is dropped on export, never guessed (regex.ts:67-71). RC's own
wire has nosubstitutionfield, so this codec never reads or writes canonicalsubstituteFindfor
RoleCall - a correction againstcore/regex/platform-fields.ts, which currently lists it as an RC-owned
extra (regex.ts:14-18). Unknown rule-level keys rideRegexRule.extrasand re-emit verbatim
(regex.ts:74-88,96-99,129). coverage.tsis the RC tab's ground truth for the studio's editor lens, not a round-trip mechanism; it
is not consulted bytoCanonical/fromCanonical.
Source of truth
| Concern | File |
|---|---|
| Character adapter (detect, toCanonical, fromCanonical) | src/formats/rolecall/index.ts |
| Lorebook codec | src/formats/rolecall/lorebook.ts |
| Compendium export codec (read, write, streamed chunks) | src/formats/rolecall/compendium.ts |
| Persona codec | src/formats/rolecall/persona.ts |
| Regex codec | src/formats/rolecall/regex.ts |
| Preset codec | src/formats/rolecall/preset.ts |
Shared Tavern field map (data <-> canonical) |
src/formats/_shared/tavern-fields.ts |
| Shared lore int-enum + character-filter decoders | src/formats/_shared/lore-enums.ts |
CCv3 assets[] <-> media |
src/formats/_shared/assets.ts |
| PNG chunk read/write | src/formats/_shared/png.ts |
| Card/JSON input readers | src/formats/_shared/card-io.ts |
| Coverage declaration | src/formats/rolecall/coverage.ts |
Canonical wrapper, escrow (original), id policy |
src/core/canonical.ts |
| Persona canonical schema | src/entities/persona/schema.ts, entities/persona.md |
| Samples | samples/rolecall/ (a character card and a lorebook export, constructed from RoleCall's own serializer source - no genuine RC export file was found on disk; see samples/rolecall/SOURCES.md) |