Skip to content

Rune system core

The pieces that drive the sentence pipeline. None of these are placed in the level — they are subsystems and stateless strategies that the calculator hands work to.

Pipeline at a glance

slot change
   |
   v
ARuneCalculator::OnSlotChanged
   |
   v
USentenceValidator::ValidateSentence  --> FSentence (Subject, Verb, Object)
   |
   v
URulePriorityManager::ShouldApplyRule  (timestamp wins on conflict)
   |
   v
UEffectExecutor::ApplyEffect
   |
   +-- pick strategy based on verb's data
   |
   +--> UNounTransformStrategy        (IS verb: identity swap)
   +--> UComponentVerbStrategy        (everything else: attach a URuneEffectComponent)

When a rune is removed and the sentence breaks, the same path runs in reverse: RemoveEffect looks up the calculator's affected targets, calls the strategy's Revert, and URuneEffectHistory rolls state back.

USentenceValidator (UGameInstanceSubsystem)

Pure grammar. Takes an array of ARuneSlot* and returns an FSentence.

Patterns:

  • 3-word: NOUN + VERB + NOUN ("TREE IS ROCK")
  • 2-word: NOUN + VERB ("TREE FLOAT")

Public:

  • ValidateSentence(Slots)FSentence. Empty FSentence if grammar fails.
  • IsValidStructure3(EWordType, EWordType, EWordType)
  • IsValidStructure2(EWordType, EWordType)
  • SentenceToString(FSentence) — human-readable for logs.

FSentence lives in RuneSystem/Core/RuneSystemStructs.h. Helpers: Is2Word(), Is3Word(), IsValid(). Equality is field-wise pointer equality.

URulePriorityManager (UGameInstanceSubsystem)

Resolves conflicts when multiple calculators target the same subject noun. Most-recent wins by timestamp.

Public:

  • RegisterRule(Sentence, Source) — calculator registers when its sentence becomes active.
  • UnregisterRule(Source) — calculator unregisters when its sentence breaks.
  • ShouldApplyRule(NounRune, Source) — true if Source currently owns this noun.
  • GetActiveRuleForNoun(NounRune)FActiveRule { Sentence, Source, Timestamp }.
  • ClearAllRules() — for level transitions.

Internal state: TMap<URuneDataAsset*, FActiveRule> keyed by subject noun, plus a reverse map from calculator to noun for O(1) unregister.

UEffectExecutor (UGameInstanceSubsystem)

Picks the strategy for a sentence and dispatches.

Strategy selection (PickStrategyForSentence):

  • Verb's TwoWordEffectComponent or ThreeWordEffectComponent is set for the current sentence length → ComponentVerbStrategy.
  • Both null → NounTransformStrategy (the IS verb).

Public:

  • ApplyEffect(Sentence, Source, SpecificTargets) — apply to a target list. Empty list means "search the registry for matching subjects."
  • ApplyEffectToSingleTarget(Sentence, Source, Target) — used by the expanding ring as it reaches each target. Filters internally; if the target's noun doesn't match, no-op.
  • RemoveEffect(Source) — revert all effects from a calculator.
  • FindTargetsByNoun(NounRune) — registry passthrough.
  • RefreshAllEffects() — re-apply active effects after spawning new objects (rare).
  • CanHandleSentence(Sentence) — false if the verb has no effect for this sentence length.
  • GetAffectedTargets(Source) — for debug overlays.

Holds a permanent instance of each strategy:

UPROPERTY() UNounTransformStrategy* TransformIntoStrategy;
UPROPERTY() UComponentVerbStrategy* CompVerbStrategy;

Tracks per-calculator targets in TMap<ARuneCalculator*, TArray<ARuneableObject*>> so revert knows what to roll back.

URuneEventBus (UGameInstanceSubsystem)

Loose-coupled event broadcaster. Today it has exactly one event: OnFireSpread. Components broadcast FFireSpreadEvent and other components subscribe.

Pattern for adding a new event: DECLARE_DYNAMIC_MULTICAST_DELEGATE_* next to OnFireSpread, add a UPROPERTY(BlueprintAssignable) delegate, add a BroadcastFoo(...) UFUNCTION. That's the entire contract.

FFireSpreadEvent carries SourceObject, Location, SpreadRadius, Intensity. See RuneSystem/Subsystems/RuneEventBus.h.

URuneRedefinitionManager (UGameInstanceSubsystem)

Implements the meta-rule pattern from the TDD: "BURN is WET" turns "TREE is BURN" into effectively "TREE is WET". A redirection map.

Public:

  • RedefineRune(From, To, Calculator) — install the override.
  • ResolveRune(InputRune) — follows the chain. MaxRedirectionDepth = 10 so cycles don't loop forever.
  • ClearRedefinition(Rune), ClearRedefinitionsForCalculator(Calculator), ClearAllRedefinitions().
  • IsRuneRedefined, GetDirectRedirect, GetAllRedefinitions.

Status: live wiring from sentences into RedefineRune is not yet active. The class exists and is correct; meta-rule sentences are a planned feature that has not been turned on. See Development plan for the exposure plan.

URuneableObjectRegistry (UWorldSubsystem)

O(1) lookup of every ARuneableObject by its current noun. Replaces "iterate every actor in the world" with a hash query.

Public:

  • RegisterObject(Object, NounData) / UnregisterObject(Object, NounData) — called by ARuneableObject::BeginPlay / EndPlay.
  • UpdateObjectNoun(Object, OldNoun, NewNoun) — atomic swap when the IS verb fires.
  • GetObjectsByNoun(NounData)TArray<ARuneableObject*>. Null-safe; cleans destroyed entries lazily on read.
  • GetObjectsAffectedByNoun(NounData) — primary noun matches plus any objects registered as listeners for this noun (see UNounListenerComponent). Used by ComponentVerbStrategy for verb dispatch. The plain GetObjectsByNoun skips listeners — NounTransformStrategy uses it because IS only acts on actual identity.
  • RegisterListener(Object, NounData) / UnregisterListener(...) — for the listener pattern.
  • ClearRegistry().

Storage uses the FRuneableObjectArray wrapper struct because UPROPERTY doesn't support nested templates (TMap<K, TArray<V>>).

URuneEffectHistory (UWorldSubsystem)

Per-level record of every effect a calculator applied, used for clean undo when a sentence breaks.

FEffectRecord:

  • AffectedObject — the runeable that got hit.
  • PreviousNounData — for NounTransform rollback.
  • bWasDirectEffect — true for direct-from-sentence effects, false for chain reactions (fire spread). Revert only undoes direct effects.
  • AddedComponentClassTSubclassOf<UActorComponent>, the class ComponentVerbStrategy attached. Used to find and remove the component during revert.
  • Timestamp, ChangeDescription — debug.

Public:

  • RecordEffect(Calculator, Record).
  • GetDirectEffects(Calculator) / GetAllEffects(Calculator) — the strategies use direct-only.
  • ClearHistory(Calculator) / ClearAllHistory().
  • GetRecordArray(Calculator) — direct mutation access used by strategies that need to selectively pull their own records out.

Same wrapper-struct pattern as the registry.

Strategies

UEffectStrategy (abstract base)

BlueprintNativeEvent virtuals so concrete strategies can also be subclassed in Blueprint:

  • Apply(Targets, EffectRune, Calculator) — flat target list, single rune (used by NounTransform).
  • ApplyFromSentence(Sentence, Calculator) — full sentence access (used by ComponentVerb).
  • Revert(Targets, EffectRune, Calculator) — undo.
  • CanHandle(EffectRune) — gating.

Helpers: GetEffectHistory, GetRegistry, GetRedefinitionManager. Strategies are stateless — all context comes through parameters.

UNounTransformStrategy

The IS verb. NOUN + IS + NOUN.

  • Pulls all targets matching the Subject noun.
  • Calls ARuneableObject::SetNoun(NewNoun) on each.
  • SetNoun swaps mesh, materials, physics, and inherent components in one call (see ARuneableObject in Rune actors and placement).
  • Updates the registry atomically.
  • Records PreviousNounData so Revert can roll back.

UComponentVerbStrategy

Every other verb. Reads TwoWordEffectComponent or ThreeWordEffectComponent off the verb's data asset and attaches that URuneEffectComponent subclass to each target.

Behavior:

  • 2-word: component goes on every Subject target.
  • 3-word: bidirectional verbs (the default — see URuneEffectComponent::bBidirectional) attach to both Subject and Object targets. Override the flag to false on the component for Subject-only verbs (e.g. MoveToward).
  • Per-noun overrides: if the target's noun has an entry in VerbComponentOverrides, that subclass is used instead of the verb's default. Lets a specific noun react differently to a verb.
  • ApplyToTargets is the per-target entry point used by the expanding ring; it bypasses the registry and operates on the explicit list the ring is feeding it.

Revert finds the recorded component via AddedComponentClass, calls OnEffectRemoved, and destroys it.

Source files

  • Source/Rephrased_Demo/SentenceValidator.h
  • Source/Rephrased_Demo/RulePriorityManager.h
  • Source/Rephrased_Demo/EffectExecutor.h
  • Source/Rephrased_Demo/RuneSystem/Subsystems/RuneEventBus.h
  • Source/Rephrased_Demo/RuneSystem/Subsystems/RuneRedefinitionManager.h
  • Source/Rephrased_Demo/RuneSystem/Subsystems/RuneableObjectRegistry.h
  • Source/Rephrased_Demo/RuneSystem/Subsystems/RuneEffectHistory.h
  • Source/Rephrased_Demo/RuneSystem/Strategies/EffectStrategy.h
  • Source/Rephrased_Demo/RuneSystem/Strategies/NounTransformStrategy.h
  • Source/Rephrased_Demo/RuneSystem/Strategies/ComponentVerbStrategy.h
  • Source/Rephrased_Demo/RuneSystem/Core/RuneSystemStructs.h
  • Source/Rephrased_Demo/RuneSystem/Core/RuneSystemEnums.h

Known constraints

  • URuneRedefinitionManager API is implemented but not wired into the sentence pipeline yet. Meta-rule sentences are a future gate.
  • URuneEffectHistory does not persist across level loads. Cross-level rune state is out of scope today.
  • Strategy selection ignores the redefinition manager. If we turn on meta-rules, the executor will need to resolve the verb through ResolveRune before reading its component fields.

Change history

  • 2026-05-23 — Gabriel Li — Redefinition manager links Development plan exposure section.