Relationship System

v1.1.0

Architecture

Understand the internal subsystem architecture and hierarchical resolution.

Subsystem Overview

The Relationship System uses a facade pattern where the public RelationshipManager delegates to 11 internal subsystems. This design improves testability, performance, and extensibility while keeping the public API unchanged.

Clean API Surface: The public API is kept minimal and straightforward. The subsystem decomposition is entirely internal — you interact only with RelationshipManager.

Facade Pattern

RelationshipManager (MonoBehaviour Singleton — Public Facade)
│
├─ EntityRegistry          Entity registration & lookup
├─ RelationshipStore       CRUD for faction relationships & entity overrides
├─ RelationshipResolver    Hierarchical lookup with inheritance
├─ RelationshipEventBus    Central event aggregation & distribution
├─ DecayProcessor          Interval-batched decay with dirty-set tracking
├─ ModifierProcessor       Temporary buffs/debuffs with stacking
├─ InheritancePropagator   Push-based parent→child propagation
├─ HistoryTracker          Per-relationship audit log
├─ CapProcessor            Min/max value caps per relationship
├─ DiplomacyManager        Discrete faction states & transitions
└─ RuntimeFactionRegistry  Dynamic faction creation & lifecycle

All subsystems are internal — users interact only with the RelationshipManager facade or GC2 visual scripting components. Each subsystem can be unit-tested in isolation using mock interfaces.

Subsystem Details

EntityRegistryIEntityRegistry

Manages entity registration, lookup by ID, and faction resolution. Tracks which entities are active and provides GetPrimaryFaction() for hierarchical resolution.

RelationshipStoreIRelationshipStore

Storage layer with separate dictionaries for faction relationships and entity overrides. Handles multi-dimensional queries (F1), the pending relationship queue (R1), and save/load serialization.

RelationshipResolverIRelationshipResolver

Implements the hierarchical lookup chain. Supports symmetric, directional, scoped, and inherited lookups. Returns a RelationshipResult with source information and inheritance multiplier.

RelationshipEventBus

Central event hub. All subsystems fire events through the bus, and the manager exposes them publicly. Events include value changes, level transitions, faction membership, modifier lifecycle, cap reached, diplomacy transitions, and runtime faction creation/dissolution.

DecayProcessor

Processes relationship decay using interval batching and a dirty-set. Only relationships with active decay are tracked, and processing happens at configurable intervals (default: 5 seconds) instead of every frame. Supports Linear, Curve, and Asymmetric modes.

ModifierProcessor

Manages temporary modifiers (buffs/debuffs). Processes modifier stacking in order: Flat (additive) → Percentage (multiplicative) → Override (last wins). Handles time-based expiry and target filtering by entity, definition, or faction.

InheritancePropagator

Push-based inheritance propagation. When a parent faction's relationship changes, the propagator applies scaled deltas to child factions based on theirinheritanceRate andPropagationMode.

HistoryTracker

Optional per-definition audit log. Uses circular buffers to track relationship changes with timestamps, old/new values, delta, source labels, and level transitions. Enable viaenableHistory on the definition.

CapProcessor

Enforces min/max value caps per relationship. Caps can be added and removed dynamically (e.g., quest completion removes a cap). Fires events when a modification is constrained.

DiplomacyManager

Manages discrete faction-to-faction states (War, Peace, Alliance, Ceasefire, Embargo). Supports auto-transitions based on relationship value thresholds and applies state-specific modifiers to relationship changes.

RuntimeFactionRegistry

Manages factions created at runtime (player guilds, dynamic alliances). RuntimeFactions implement IRelationshipEntity and integrate fully with the existing hierarchy, save/load, and resolution systems.

Hierarchical Resolution

When you query a relationship, the resolver walks a prioritized lookup chain. The first match wins. This allows entity-level overrides to take priority over faction defaults, and faction relationships to serve as fallbacks.

Lookup Chain (Symmetric)

GetRelationship("player", "merchant", definition)

1. Entity Override (scoped)    → player↔merchant [definition_id]
2. Entity Override (default)   → player↔merchant [null]
3. Faction Relationship (scoped) → factionA↔factionB [definition_id]
4. Faction Relationship (default) → factionA↔factionB [null]
5. Parent Faction Inheritance  → parentA↔factionB (×inheritanceRate)
   └─ Walks up hierarchy: grandparent×rate×rate (max depth: 3)
6. None                        → No relationship found
For directional (asymmetric) definitions, the resolver first tries the directional path (A→B), then falls back to symmetric (A↔B). This means asymmetric definitions can still inherit from symmetric faction relationships.

The result includes a RelationshipSource enum indicating where the value came from (EntityOverride, Faction, FactionInherited, or None) and, for inherited results, the InheritanceMultiplier and originating faction.

Multi-Dimensional Lookup

When a RelationshipDefinition is specified, the lookup is scoped to that dimension. Without a definition, the resolver falls back to the default (unscoped) relationship — a simple one-value-per-pair lookup.

// Two independent relationship dimensions between the same entities:
GetRelationship("player", "merchant", trustDefinition)    → 75 (Trusted)
GetRelationship("player", "merchant", tradeDefinition)    → 30 (Neutral)
GetRelationship("player", "merchant")                     → Default/legacy lookup

Event System

The RelationshipEventBus aggregates events from all subsystems. The manager exposes these as standard C# events on its public API.

EventSignature
OnRelationshipValueChanged(entityAId, entityBId, oldValue, newValue)
OnRelationshipLevelChanged(entityAId, entityBId, oldLevel, newLevel)
OnRelationshipCreated(entityAId, entityBId, state)
OnFactionMemberAdded(entity, faction)
OnFactionMemberRemoved(entity, faction)
OnCapReached(entityAId, entityBId, cap)
OnRuntimeFactionCreated(runtimeFaction)
OnRuntimeFactionDissolved(factionId, memberIds)

RelationshipKey

The RelationshipKey struct is the unified key used throughout the system for identifying relationships. It supports four modes:

Symmetric Defaultnew RelationshipKey("a", "b")

Legacy mode. Entities sorted lexicographically (A↔B == B↔A). No definition scope.

Symmetric Scopednew RelationshipKey("a", "b", "trust")

Multi-dimensional. Same entity pair, different definition scopes (F1).

Directional Scopednew RelationshipKey("a", "b", "trust", true)

Asymmetric. Order preserved: A→B ≠ B→A (F2).

Directional Defaultnew RelationshipKey("a", "b", null, true)

Asymmetric without definition scope.

Keys serialize as pipe-separated strings for save/load:"entityA|entityB","entityA|entityB|defId", or"entityA|entityB|defId|D" for directional. Old format keys are automatically recognized for backward compatibility.