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. EmptyFSentenceif 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 ifSourcecurrently 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
TwoWordEffectComponentorThreeWordEffectComponentis 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 = 10so 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 byARuneableObject::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 (seeUNounListenerComponent). Used byComponentVerbStrategyfor verb dispatch. The plainGetObjectsByNounskips listeners —NounTransformStrategyuses 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— forNounTransformrollback.bWasDirectEffect— true for direct-from-sentence effects, false for chain reactions (fire spread). Revert only undoes direct effects.AddedComponentClass—TSubclassOf<UActorComponent>, the classComponentVerbStrategyattached. 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. SetNounswaps mesh, materials, physics, and inherent components in one call (seeARuneableObjectin Rune actors and placement).- Updates the registry atomically.
- Records
PreviousNounDatasoRevertcan 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. ApplyToTargetsis 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.hSource/Rephrased_Demo/RulePriorityManager.hSource/Rephrased_Demo/EffectExecutor.hSource/Rephrased_Demo/RuneSystem/Subsystems/RuneEventBus.hSource/Rephrased_Demo/RuneSystem/Subsystems/RuneRedefinitionManager.hSource/Rephrased_Demo/RuneSystem/Subsystems/RuneableObjectRegistry.hSource/Rephrased_Demo/RuneSystem/Subsystems/RuneEffectHistory.hSource/Rephrased_Demo/RuneSystem/Strategies/EffectStrategy.hSource/Rephrased_Demo/RuneSystem/Strategies/NounTransformStrategy.hSource/Rephrased_Demo/RuneSystem/Strategies/ComponentVerbStrategy.hSource/Rephrased_Demo/RuneSystem/Core/RuneSystemStructs.hSource/Rephrased_Demo/RuneSystem/Core/RuneSystemEnums.h
Known constraints¶
URuneRedefinitionManagerAPI is implemented but not wired into the sentence pipeline yet. Meta-rule sentences are a future gate.URuneEffectHistorydoes 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
ResolveRunebefore reading its component fields.
Change history¶
- 2026-05-23 — Gabriel Li — Redefinition manager links Development plan exposure section.