BranchScene.h header
#include <ew/core/branching/BranchScene.h>
Namespace ew::core::branching
BranchAlternate struct
struct ew::core::branching::BranchAlternate
One of the several ways a line can be said.
A beat often needs more than one phrasing: variation on repeat, a different register for a different player state, or alternates for a director to choose between in the booth. Without this the author's only options are a Condition per phrasing – three boxes on the canvas for one line – or writing the variants into a note nothing can play.
Members
ew::core::foundation::BranchNodeId ew::core::branching::BranchAlternate::lineId
This take's own stable identity.
In the same id space as a node's own id, because that space is already "addressable lines": a recorded take, an approved translation and a studio's line list all hang on it, and a take that could not be addressed could not be independently recorded or translated – which is the whole reason each alternate has one.
QString ew::core::branching::BranchAlternate::text
The words, as the reader hears them if this take is the one chosen.
QString ew::core::branching::BranchAlternate::condition
The expression that has to hold for this take to be eligible, or empty for "always".
Read the way a Condition node's expression is (see BranchNode::conditionExpression), so there is one language for "when does this apply" rather than one per feature. An unreadable expression makes the take ineligible and is reported, exactly as it is on a Condition.
bool operator==(const BranchAlternate &, const BranchAlternate &)=default
Alternates compare equal when every field matches.
BranchDirective struct
struct ew::core::branching::BranchDirective
One instruction in a line's production sequence – a camera cut, an animation, an audio cue.
Ordfoss does not execute these. It authors them, checks them against the vocabulary the project declared (ew::core::branching::DialogueCommand) and exports them; the runtime that reads the script is what performs them. That division is the whole point: a narrative lead can write the direction for a line at the moment they write the line, in the tool they are already in, instead of filing a ticket for somebody who has not read the scene.
Members
QString ew::core::branching::BranchDirective::command
The command's name, as declared in the project's vocabulary.
Free text rather than an index into the vocabulary: a command renamed or removed must leave the direction that used it readable and reportable, not silently repointed at whichever command happens to sit at that index afterwards.
std::vector<QString> ew::core::branching::BranchDirective::arguments
Its arguments, in the order the command declares its parameters.
double ew::core::branching::BranchDirective::delaySeconds = 0.0
How long after the line begins this happens, in seconds; 0 means immediately.
On the directive rather than encoded into an argument, because timing is the thing a director adjusts and an exporter has to be able to write in whatever form its target uses – the Dialogue System's @2.5 suffix, a Yarn parameter, a JSON field.
double ew::core::branching::BranchDirective::durationSeconds = 0.0
How long it lasts, in seconds; 0 means the runtime decides (instant, or until it finishes).
bool operator==(const BranchDirective &, const BranchDirective &)=default
Directives compare equal when every field matches.
BranchEffect struct
struct ew::core::branching::BranchEffect
One thing a node does to the story's state when the reader passes it.
This is the half of the variable system that WRITES. A Condition reads state; with nothing to put anything into it, every condition would test the value its declaration started at for ever, and branching logic would be decoration.
Members
ew::core::foundation::VariableId ew::core::branching::BranchEffect::variableId
The variable it writes.
By id, never by name, for the same reason a Condition tests by id: renaming gold to coins must not silently break what writes to it. A null id is a defect the validator reports rather than something applied to nothing.
BranchEffectOperation ew::core::branching::BranchEffect::operation =
What it does to that variable.
VariableValue ew::core::branching::BranchEffect::value
The value for BranchEffectOperation::Set, or the amount for BranchEffectOperation::Add. Unused by every other operation.
QString ew::core::branching::BranchEffect::entityPath
The CANON property this effect writes, as a stored object path (@<id>.status), instead of a branching variable. Empty for an ordinary effect, which is nearly all of them.
Writing a branching variable is ordinary story state; writing an entity property mutates the world bible, and the two must never look the same. A conversation that quietly rewrote a character's status would be discovered by whoever next read the codex and could not explain it. So this is opt-in per project (ew::core::project::Project::canonWritesAllowed), marked on the node in the graph, and reported by the validator whether or not it is allowed.
Ordfoss authors and exports it; Ordfoss's own playthrough does not perform it. A playthrough is a lens for reading the story, and one that edited the project as it walked would make opening a scene and changing the world the same act – the same division of labour a line's production direction follows.
Written in the syntax ew::app::expression::WorldResolver reads, so a path here and the same path in a condition mean the same value.
ew::core::foundation::VariableId ew::core::branching::BranchEffect::sourceVariableId
The variable READ by BranchEffectOperation::SetFromVariable; null for every other operation.
A second id rather than a VariableValue alternative holding one: a value is what a variable currently contains, and "the contents of `currentTown`" is not a value – it is an instruction to go and look, which is exactly what distinguishes this operation from Set.
bool operator==(const BranchEffect &, const BranchEffect &)=default
Effects compare equal when every field matches.
BranchLink struct
struct ew::core::branching::BranchLink
A directed link from one node to the next.
Members
ew::core::foundation::BranchNodeId ew::core::branching::BranchLink::from
The node the reader leaves.
ew::core::foundation::BranchNodeId ew::core::branching::BranchLink::to
The node the reader arrives at.
BranchLinkGuard ew::core::branching::BranchLink::guard =
When this link is taken. Always except for the two leaving a Condition.
QString ew::core::branching::BranchLink::label
What the reader is offered, for a link leaving a Choice ("Go left"); empty otherwise.
This is why a canvas connection could not carry a branch: it stores two endpoints and nothing else, and a choice with no label is not a choice.
QString ew::core::branching::BranchLink::condition
Whether this way onward is offered at all, as an expression over the story's variables – hasKey, gold >= 50 AND NOT metRhys. Empty means always offered.
**Independent of guard, and they answer different questions.** The guard says which side of a Condition node this link leaves; the condition says whether the option exists for this reader at all. A link can have both: the false branch of a Condition, offered only when the reader is carrying the amulet.
It exists because the alternative was a Condition node per option. Three options with three requirements meant three extra nodes and six extra links, and a graph that tripled in size for a reason the author never chose. Written in the language ew::core::expression::evaluate reads, over the story's variables – the same language a node's conditionExpression uses.
int ew::core::branching::BranchLink::weight = 1
How likely this way is, among the ways leaving a Random node. Ignored everywhere else.
A share rather than a percentage: the weights of the links leaving one node are added up and each takes its own share of the total, so an author adding a fourth way does not have to re-balance the other three. 1 by default, which makes an unweighted Random fork even.
Zero is legal and is reported. It is how an author disables a branch without deleting it – but it is also exactly what a mistyped weight looks like, and a branch that can never be taken is invisible on the canvas, so the validator says so rather than guessing which was meant. Negative weights are treated as zero.
QString ew::core::branching::BranchLink::requirementLabel
Why the option cannot be taken, shown with it when whenUnavailable is ShownDisabled – "Requires 10 Charisma".
The author's own words rather than the expression: charisma >= 10 is what the story tests and "Requires 10 Charisma" is what a reader is told, and showing the expression would leak a variable name into the fiction. Empty shows the option greyed with no explanation, which is a legitimate choice – some games say only that a door is locked.
BranchNode struct
struct ew::core::branching::BranchNode
One node in a branching scene.
Carries its OWN content rather than pointing at another object, which is the reason this is not built on ew::core::canvas: a canvas card is a reference to a note or entity that already exists, while a branch node's text – the line spoken, the choice offered – exists nowhere else.
Members
ew::core::foundation::BranchNodeId ew::core::branching::BranchNode::id
Stable identity. Links reference this.
BranchNodeType ew::core::branching::BranchNode::type =
What the node does.
QString ew::core::branching::BranchNode::text
The prose shown to a reader (Line, Hub), or the author's label for the fork (Choice, Condition, Jump). Never empty in a finished scene, and the validator says so.
ew::core::foundation::ContentId ew::core::branching::BranchNode::speakerId
The codex entity saying this line; null when nobody in particular does (narration, or a node that is not spoken).
By id, never by name. An author renaming a character must not silently orphan every line they speak – the same rule Variable references follow, for the same reason. A speaker pointing at an entity that has been deleted is reported as a dangling reference rather than quietly becoming narration.
Localisation carries it as translator context (a line reads differently depending on who says it), voice extraction derives the cast list from it, and the graph shows it – three things that were each maintained by hand beside a script that already knew the answer.
ew::core::foundation::ContentId ew::core::branching::BranchNode::listenerId
The codex entity being spoken TO, when it matters; null otherwise.
Separate from the speaker because a translator, a director and a camera all need both, and because a line addressed to one of three people present is a different line.
ew::core::foundation::ContentId ew::core::branching::BranchNode::portraitId
The image shown for the speaker on this line, by Media id; null for none.
Drawn from the speaker entity's own gallery rather than being a free-floating file, so a portrait is one of the pictures the codex already holds of that character. A line where the speaker is furious and a line where they are pleading are the same character with different faces, which is why it lives on the node and not on the entity.
QString ew::core::branching::BranchNode::speakerDisplayName
What the reader sees the speaker called here, when it differs from the entity's name – "the hooded figure" before a reveal, "Father" from one character and "the King" from another.
The entity is still the speaker: casting, the cast list and every reference keep working while the reader is told something else. Empty means the entity's own name.
QString ew::core::branching::BranchNode::delivery
How the line is delivered – "weary", "shouted", "under her breath".
Free text rather than a fixed set: no vocabulary of emotions survives contact with a real script, and a closed list would send authors back to writing it in the line itself. It reaches the voice line list, where a director reads it.
QString ew::core::branching::BranchNode::notes
The author's own note about this node; never shown to a reader.
Direction the line needs and the prose cannot carry – "VO: weary, he has said this before", "check this against the treaty date". It travels with the node, is searchable, and is not a comment: a comment is a conversation ABOUT the line and gets resolved, a note is part of writing it. Variable::description is the same idea on a variable, and the editor asks for both with the same words.
std::set<QString> ew::core::branching::BranchNode::tags
This node's own free-form tags, in the project's one tag vocabulary.
A scene already carries tags as a ew::core::content::ContentObject, but a scene is the wrong grain for most of what a team tags: "needs VO", "review this", the emotional register of one exchange, or the UI style a runtime should use for a single line. Marking the whole conversation says something the author did not mean.
The same vocabulary as everything else, deliberately – no separate namespace of node tags. A writer who tags a chapter needs-vo and a line needs-vo means the same thing, and two vocabularies would make "everything still needing VO" two questions.
Compared through ew::core::content::normalizedTag, so Draft and draft are one tag here exactly as they are on a scene. Stored as the writer typed them.
They reach the ink and Yarn exports, so a runtime can key behaviour off them – which is what Twine passage tags and Yarn node tags are for, and the reason this is part of the script rather than an editor-only convenience.
ew::core::foundation::VariableId ew::core::branching::BranchNode::variableId
For a Condition, the variable it tests; null otherwise.
By id, never by name – an author renaming gold to coins must not silently break the branch that tests it. K2 recorded the same rule for the same reason.
BranchComparison ew::core::branching::BranchNode::comparison =
For a Condition, how its variable is compared against comparand.
VariableValue ew::core::branching::BranchNode::comparand
For a Condition, the value its variable is compared against.
A whole value rather than a string, so gold >= 50 compares two NUMBERS rather than two spellings of a number – the same reason VariableValue is a closed set of three alternatives instead of text everyone re-parses.
QString ew::core::branching::BranchNode::conditionExpression
For a Condition, the expression it tests – gold >= 50 AND NOT hasKey – instead of the single variable/comparison/comparand above.
When this is non-empty it is what the condition means, and the three fields above are ignored. They are kept, not replaced: every scene ever saved holds them, a fork on one variable is still what most conditions are, and the editor writes whichever form the author chose. Written in the language ew::core::expression::evaluate reads, over the story's variables (see ew::app::expression::VariableBindings for how they are named).
ew::core::foundation::BranchNodeId ew::core::branching::BranchNode::jumpTarget
For a Jump, the node it sends the reader to; null otherwise.
ew::core::foundation::ContentId ew::core::branching::BranchNode::jumpSceneId
For a Jump that leaves this scene, the scene jumpTarget is in; null for a jump within the same scene.
Null means "here", and that is deliberate: every jump ever authored is a local one, and a field that had to be filled in to mean what it already meant would rewrite every scene file in every project on first save. It also keeps the common case – most jumps are local – free of ceremony.
The target node must be one the other scene declares in BranchScene::entryPointIds. Jumping into the middle of a scene nobody opened up is how a story acquires a path its author never sanctioned, and the validator says so.
std::vector<BranchEffect> ew::core::branching::BranchNode::effects
What passing this node does to the story's state, in order.
Applied when the reader ARRIVES, before this node's outgoing links are chosen – so a Condition that also carries an effect tests the state it arrived with, and the node after it sees the change. Ordered, so two effects on one variable compose the way they are written.
They live on the NODE rather than on a Choice's option link. An effect per option would save a node in some scenes; it would also be a second placement to validate, serialise, edit and explain, and the node behind an option already exists – it is where the consequence prose goes ("You hand over the coins").
int ew::core::branching::BranchNode::maxCharacters = 0
This line's own character limit, overriding the speaker's and the project's; 0 means it has none of its own.
A per-line override because the constraint is occasionally per line: the one exchange that appears over a portrait, the line the UI shows on a button. Without it an author's only way to make room for a single long line is to loosen the speaker's budget and lose the check on every other line they say.
double ew::core::branching::BranchNode::maxSeconds = 0.0
This line's own duration limit in seconds, overriding the speaker's and the project's; 0 means it has none of its own.
QString ew::core::branching::BranchNode::status
Where this line has got to in review, named in the project's one vocabulary (ew::core::workflow::WorkflowStatus). Empty means nobody has said.
Per line, because that is the grain work is actually reviewed at. A scene is approved when its lines are, and a scene-level flag would either be set optimistically – hiding the three lines a reviewer sent back – or never set at all.
By NAME rather than by index into the vocabulary, for the reason a directive names its command: a state renamed or removed must leave the lines that carried it readable and reportable, not silently repointed at whichever state now sits at that position. A status the vocabulary does not know is a finding, not a crash.
QString ew::core::branching::BranchNode::owner
Who is responsible for this line – a display name, matching the one a comment records.
A name rather than a reference to a person object, because Ordfoss has no registry of people and inventing one to hold a string would be ceremony: the identity a team already shares is the name in the review settings, which is what signs their comments. It is also what makes "everything of mine that is not yet approved" answerable without an account system.
double ew::core::branching::BranchNode::timeoutSeconds = 0.0
For a Choice, how long the reader has to decide, in seconds; 0 means as long as they like.
A choice that expires is a recognisable device – the Telltale beat where hesitating is itself an answer – and it could not be written at all: an author's only option was a note asking a programmer to add one, which nothing could then validate or export.
Ordfoss does not run the clock in the shipped game. It authors the timeout, checks it, counts it down in the Play tab so an author can feel whether five seconds is enough, and exports it as data for the runtime to honour – the same division of labour a line's production direction follows (sequence).
ew::core::foundation::BranchNodeId ew::core::branching::BranchNode::timeoutTarget
For a Choice with a timeoutSeconds, the node the reader is sent to when the time runs out; null otherwise.
Required once a timeout is set, and the validator says so. A timer with nowhere to go leaves the story stopped at the moment the author was most deliberately controlling, and it looks exactly like a choice that works until somebody waits.
A node rather than a link, for the reason a Jump names one: it is an address, and the validator checks it is one of the ways this choice actually offers – a default the reader could never have picked themselves is a path nobody authored.
std::vector<BranchAlternate> ew::core::branching::BranchNode::alternates
The several ways this line can be said, in the order an author listed them.
**When this is non-empty it is what the reader hears, and text becomes the author's label for the node** – the same role text already plays on a Condition or a Jump. The alternative was to make text the first take and this the rest, which reads as "a node with three takes holds two alternates" and puts one phrasing in a different place from the other two: a translator's file, a line list and a recording session would all have to know that take one lives somewhere else.
Empty on every node ever authored, so nothing changes for a line that says one thing.
AlternateMode ew::core::branching::BranchNode::alternateMode =
How alternates is chosen between; ignored when the node has none.
std::vector<BranchDirective> ew::core::branching::BranchNode::sequence
The production direction attached to this line, in the order it happens.
Separate from effects because the two are answerable by different people and change different things: an effect alters the STORY (the reader now has the key), a directive alters the PERFORMANCE (the camera is close, the clip plays a beat later). A runtime that dropped every directive would still tell the same story; one that dropped an effect would tell a different one.
Separate from delivery for the same reason it is separate from notes: delivery is a sentence for a human in a booth, and this is a list a machine reads.
Ordered, and the order is meaningful even where BranchDirective::delaySeconds is zero: two things happening "at once" still happen in the order they are written.
double ew::core::branching::BranchNode::x = 0.0
Where the node sits on the canvas. Presentation only – the story does not depend on it, and a scene with every node at the origin still plays correctly (the editor can lay it out).
double ew::core::branching::BranchNode::y = 0.0
The node's y position.
BranchScene class
class ew::core::branching::BranchScene
A branching scene: typed nodes and the directed links between them.
Links are DIRECTED and that is the whole point. A reader goes from one node to the next; a link from A to B is not a link from B to A, and a model that could not tell them apart could not express the thing being authored.
Members
ew::core::branching::BranchScene::BranchScene(ew::core::foundation::ContentId id, QString title)
Creates an empty scene with identity id and title.
const QString & ew::core::branching::BranchScene::title() const
The scene's title.
void ew::core::branching::BranchScene::setTitle(QString title)
Sets the title.
ew::core::foundation::BranchNodeId ew::core::branching::BranchScene::startNodeId() const
The node the reader starts at; null when the scene has no entry point yet.
void ew::core::branching::BranchScene::setStartNodeId(ew::core::foundation::BranchNodeId id)
Sets the starting node.
const std::vector< BranchNode > & ew::core::branching::BranchScene::nodes() const
The nodes, in insertion order.
void ew::core::branching::BranchScene::setNodes(std::vector< BranchNode > nodes)
Replaces every node.
const BranchNode * ew::core::branching::BranchScene::findNode(ew::core::foundation::BranchNodeId id) const
The node with id, or null when the scene has none.
const std::vector< BranchLink > & ew::core::branching::BranchScene::links() const
The links, in insertion order.
void ew::core::branching::BranchScene::setLinks(std::vector< BranchLink > links)
Replaces every link.
std::vector< BranchLink > ew::core::branching::BranchScene::linksFrom(ew::core::foundation::BranchNodeId id) const
The links leaving id, in order – a Choice's options, in the order the reader sees them.
const std::vector< ew::core::foundation::BranchNodeId > & ew::core::branching::BranchScene::entryPointIds() const
The nodes another scene's Jump may enter this one at, in declaration order.
The author's sanctioned ways in. A jump from another scene may land only here – not in the middle of a conversation, where a reader would arrive without the state the surrounding nodes assume. startNodeId is always an entry in effect and does not need declaring.
They are also what makes the Unreachable finding survive cross-scene jumps. Reachability is walked forwards from the start node; a node entered only from elsewhere has nothing leading to it inside its own scene, so without a declaration it would be reported as orphaned prose. Declaring it says "this is reached from outside" once, rather than every validator having to scan every scene in the project for a jump that might land here.
void ew::core::branching::BranchScene::setEntryPointIds(std::vector< ew::core::foundation::BranchNodeId > ids)
Replaces the declared entry points.
std::optional< qint64 > ew::core::branching::BranchScene::sceneInstant() const
When this conversation happens in story time (minutes since the epoch), if the author has said. std::nullopt for a scene that is not placed on the timeline.
This is what lets a condition be checked against canon. A line testing whether a character is alive is a different claim depending on when it is said, and without a moment to say "when" there is nothing to compare a lifespan against. Document::sceneInstant is the same idea on a manuscript scene, and this deliberately uses the same units and the same meaning so a timeline built from one can hold the other.
void ew::core::branching::BranchScene::setSceneInstant(std::optional< qint64 > sceneInstant)
Sets when this conversation happens (std::nullopt when it is not placed).
bool ew::core::branching::BranchScene::isEntryPoint(ew::core::foundation::BranchNodeId id) const
Whether id may be entered from another scene – the start node, or a declared entry point.
Enumerations
enum class AlternateMode { FirstMatch, Sequence, Cycle, Random, Once }
How a node with more than one take decides which one the reader gets.
enum class BranchComparison { Equals, NotEquals, LessThan, AtLeast }
How a Condition compares its variable against a value.
enum class BranchEffectOperation { Set, SetFromVariable, Add, Toggle, Clear }
What an effect does to its variable when the reader passes the node carrying it.
enum class BranchLinkGuard { Always, WhenTrue, WhenFalse }
When a link is taken.
A Condition's two outcomes are named EXPLICITLY rather than taken from link order. Order would work until somebody reordered the links – and then the story would quietly invert, sending the reader down the false branch whenever the condition held.
enum class BranchNodeType { Line, Choice, Condition, Jump, Hub, Call, Random, Return }
What a node in a branching scene does when the reader reaches it.
Functions
std::optional< AlternateMode > ew::core::branching::alternateModeFromToken(QStringView token)
Parses an alternate mode from its serialization token; std::nullopt if unrecognized or absent.
std::optional< BranchComparison > ew::core::branching::branchComparisonFromToken(QStringView token)
Parses a comparison from its serialization token; std::nullopt if unrecognized or absent.
std::optional< BranchEffectOperation > ew::core::branching::branchEffectOperationFromToken(QStringView token)
Parses an effect operation from its serialization token; std::nullopt if unrecognized or absent.
std::optional< BranchLinkGuard > ew::core::branching::branchLinkGuardFromToken(QStringView token)
Parses a link guard from its serialization token; std::nullopt if unrecognized or absent.
std::optional< BranchNodeType > ew::core::branching::branchNodeTypeFromToken(QStringView token)
Parses a node type from its serialization token; std::nullopt if unrecognized or absent.
QString ew::core::branching::toToken(BranchNodeType type)
Returns the stable serialization token for type.
QString ew::core::branching::toToken(BranchComparison comparison)
Returns the stable serialization token for comparison.
QString ew::core::branching::toToken(UnavailableOption option)
Returns the stable serialization token for option.
QString ew::core::branching::toToken(BranchLinkGuard guard)
Returns the stable serialization token for guard.
QString ew::core::branching::toToken(BranchEffectOperation operation)
Returns the stable serialization token for operation.
QString ew::core::branching::toToken(AlternateMode mode)
Returns the stable serialization token for mode.