Skip to content

Dungeon System

Summary: The Dungeon System uses topology-based procedural generation to create deterministic dungeon layouts. UDungeonSubsystem manages runtime state while UEternalDungeonGenerator produces FDungeonInstance from UDomainDungeonConfig. The plugin bridge (AEternalDungeonActor) extends the ProceduralDungeon plugin to handle spatial room placement from our abstract topology.

Table of Contents


Why Topology-Based Generation

Design Philosophy

Our dungeon system separates what rooms exist from where they are placed:

Concern Our System Plugin System
Room roles, slots, count Topology template N/A
Room connections Topology edges Door matching
Physical placement N/A Spatial algorithm
Level streaming N/A Built-in

Key Benefits

  1. Determinism - Same seed + config = identical dungeon (critical for multiplayer)
  2. Designer Control - Topologies define critical path, branching, boss placement
  3. Domain Identity - Each domain has distinct room pools and topology options
  4. Clean Separation - Our topology logic, plugin's spatial logic

Standard vs HQ Dungeons

Aspect Standard Dungeon HQ Dungeon
Terminal Room Exit Boss
Cleared When Exit reached Boss defeated
Triggers Node cleared Node cleared + Domain liberated
Topologies StandardTopologies HQTopologies

Architecture Overview

┌──────────────────────────────────────────────────────────────────┐
│                    UDomainDungeonConfig                           │
│  (Data Asset - Per Domain)                                       │
│  ┌────────────────────┐  ┌────────────────────┐                  │
│  │ StandardTopologies │  │   HQTopologies     │                  │
│  │ (Exit endings)     │  │  (Boss endings)    │                  │
│  └─────────┬──────────┘  └─────────┬──────────┘                  │
│            │                       │                              │
│  ┌─────────┴───────────────────────┴──────────┐                  │
│  │ RoomPools: TMap<FGameplayTag, Pool>        │                  │
│  │ (Room.Start, Room.Combat, Room.Boss, ...)  │                  │
│  └────────────────────────────────────────────┘                  │
└───────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│                  UEternalDungeonGenerator                         │
│  (Pure Function Object)                                          │
│  GenerateDungeon(Config, Seed, bIsHQ) → FDungeonInstance         │
└───────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│                    UDungeonSubsystem                              │
│  (GameInstance Subsystem - Runtime Manager)                      │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │ FDungeonInstance CurrentDungeon                            │  │
│  │  • TArray<FDungeonFloor> Floors                            │  │
│  │  • FGuid StartRoomID, BossRoomID, ExitRoomID               │  │
│  │  • FGuid CurrentRoomID                                     │  │
│  └────────────────────────────────────────────────────────────┘  │
└───────────────────────────┬──────────────────────────────────────┘
                            │ OnDungeonGenerated
┌──────────────────────────────────────────────────────────────────┐
│                   AEternalDungeonActor                            │
│  (Plugin Bridge - Extends ADungeonGenerator)                     │
│  • ChooseFirstRoomData() → Start room from topology              │
│  • ChooseNextRoomData() → Next room based on connections         │
│  • OnPostGeneration() → Register room mappings                   │
└──────────────────────────────────────────────────────────────────┘

Component Ownership

Component Owner Why
UDungeonSubsystem GameInstance Persists across level transitions
UEternalDungeonGenerator DungeonSubsystem Stateless generation logic
AEternalDungeonActor World Plugin-spawned, per-dungeon

Core Concepts

FDungeonRoom

A single room instance within a generated dungeon.

Field Type Purpose
RoomID FGuid Unique identifier
TopologyNodeIndex int32 Link to source topology node
Role ERoomRole Structural role: Start, Normal, Terminal
SlotTag FGameplayTag Content slot (Room.*), selects the room pool
State ERoomState Locked, Available, Current, Cleared
RoomSeed int32 Deterministic content generation
ConnectedRoomIDs TArray\<FGuid> Adjacent rooms
RoomLevel TSoftObjectPtr\<UWorld> Level to load
MapPosition FVector2D UI display position
FloorIndex int32 Vertical organization

ERoomRole + SlotTag (Content Model)

The content model separates structure from content. ERoomRole describes only where a node sits in the graph; what fills the node is a FGameplayTag slot (Room.*) resolved against the domain config's tag-addressed pool map. Adding a new content kind is a data change (new tag + new pool entry), not a code change.

Role Description
Start Entry room — exactly one per topology, no input pin
Normal Everything in between
Terminal Boss (HQ) or exit (standard) — no output pin

Junction and Optional are not roles: junction is derived (>= 3 connections), optional is bOnCriticalPath == false.

Well-known slot tags currently in use (Room.Start, Room.Combat, Room.Treasure, Room.Junction, Room.Exit, Room.Boss, Room.Optional, Room.RemnantRealm, Room.Landmark) are just tags — the generator does an exact-match lookup into UDomainDungeonConfig::RoomPools and attaches no code meaning to them, with three exceptions: Room.Boss terminal validation, Room.Landmark placement constraints, and Room.RemnantRealm (injected rather than authored — see Remnant Realm Room Injection).

ERoomState

┌──────────────────┐
│      Locked      │  Cannot enter, doors sealed
└────────┬─────────┘
         │ Connected room cleared
┌──────────────────┐
│    Available     │  Can be entered
└────────┬─────────┘
         │ Player enters
┌──────────────────┐
│     Current      │  Player is inside
└────────┬─────────┘
         │ Objectives complete
┌──────────────────┐
│     Cleared      │  Completed, can revisit
└──────────────────┘

FDungeonFloor

Groups rooms by vertical position (depth from start).

Field Type Purpose
FloorIndex int32 0 = top/start
Rooms TArray\<FDungeonRoom> Rooms at this depth
DomainTag FGameplayTag Theming tag

FDungeonInstance

Complete generated dungeon state.

Field Type Purpose
DungeonID FGuid Unique run identifier
DungeonSeed int32 Generation seed
Floors TArray\<FDungeonFloor> All floors
StartRoomID FGuid Entry room
BossRoomID FGuid HQ dungeons only
ExitRoomID FGuid Standard dungeons only
bIsDomainHQ bool Has boss vs exit
CurrentRoomID FGuid Player's current room

Topology Templates

FTopologyNode

A single node in an abstract topology graph.

Field Type Purpose
NodeIndex int32 Unique index (0, 1, 2...)
Role ERoomRole Structural role (Start/Normal/Terminal)
SlotTag FGameplayTag Content slot (Room.*) — which domain pool fills this node
ConnectedNodeIndices TArray\<int32> Edges to other nodes
Depth int32 BFS distance from start
bOnCriticalPath bool Required to reach terminal

FDungeonTopology

Complete topology template.

Field Type Purpose
TopologyName FName Debug identifier
Nodes TArray\<FTopologyNode> All graph nodes
StartNodeIndex int32 Entry point (default: 0)
bIsHQTopology bool Has boss vs exit
BossNodeIndex int32 HQ only, terminal
ExitNodeIndex int32 Standard only, terminal

Example Topology

Standard Dungeon (Linear with Branch):

       [Start]                    Node 0: Start / Room.Start
          │                       Node 1: Normal / Room.Combat (critical)
          ▼                       Node 2: Normal / Room.Combat (critical)
       [Combat]                   Node 3: Normal / Room.Treasure (optional)
          │                       Node 4: Normal / Room.Junction (critical)
          ▼                       Node 5: Normal / Room.Combat (critical)
       [Combat]──────[Treasure]   Node 6: Terminal / Room.Exit (critical)
      [Junction]
       [Combat]
        [Exit]

Connections:
  0 → 1, 1 → 2, 2 → 3, 2 → 4, 4 → 5, 5 → 6

Topology Validation

Check Purpose
IsValid() Basic structure validation
AreAllNodesReachable() No orphaned nodes
CalculateDepths() BFS depth assignment
GetMaxDepth() Floor count calculation

Room Configuration

UDomainDungeonConfig

Per-domain configuration combining topologies and room pools.

Category Fields
Identity DomainTag, DomainName, MapMaterial
Topologies StandardTopologies, HQTopologies
Room Pools RoomPoolsTMap<FGameplayTag, FEternalRoomPool> keyed by content slot tag (Room.*); LandmarkMinCellsFromStart
Remnant Realm RemnantRealmChance (0..1, default 0 — rolled off the descent seed, overridable per world-map node), RemnantMinCellsFromStart, RemnantMaxCellsFromStart (0 = no maximum)
Encounters DefaultEncounter, SlotEncounters (per-slot-tag overrides), BossEncounter, DefaultContainerLootTable
Modifiers ModifierPool (UDungeonModifierPoolDataAsset)

FEternalRoomPool is currently just a room array; the struct is the seam where per-entry weight and min/max-occurrence knobs land later. Adding a content kind = adding a RoomPools entry — no code change.

Room Pool Resolution

SelectRoomTemplate(SlotTag, Config, Random)
    ├─ Get pool: Config->GetRoomsBySlot(SlotTag)   (exact tag match into RoomPools)
    ├─ Pool empty?
    │       └─ Return nullptr (generation fails)
    └─ Return Random selection from pool

Encounters resolve the same way: GetEncounterForSlot(SlotTag) checks SlotEncounters, falling back to DefaultEncounter when the slot has no override.

UEternalRoomData

Extends plugin's URoomData with our metadata. Each room in a pool is a data asset containing: - Level reference - Door socket configuration - SlotTag (self-identification + editor validation; resolves the spawner's encounter) - Sub-dungeon entrance config (Landmark rooms only — see Landmark / POI) - Visual theming

Room Resolution Helper

UEternalRoomData::GetRoomDataForActor(const AActor* Actor) is the canonical way to resolve which room an actor is standing in. It walks from the actor to its owning ARoomLevel (the plugin's room-level script) and returns that level's UEternalRoomData, or nullptr if the actor is not inside a generated room. Prefer this over manual level/room lookups so encounter, portal, and gate actors bind to a room consistently.


Landmark / POI (Sub-Dungeons)

A Landmark is an off-path POI room (Room.Landmark slot tag) whose entrance descends into its own sub-dungeon — a full dungeon generated from a separate UDomainDungeonConfig. It is Option A (run-ender): the sub-dungeon's AEndPortalActor returns the player to the world map and marks the current (parent) node cleared; there is no return trip up through the parent dungeon.

Authoring Model

"What's behind this entrance" is authored on the room data, not the placed actor. The Landmark room's UEternalRoomData carries the sub-dungeon configuration:

Field Type Purpose
SubDungeonConfig UDomainDungeonConfig The dungeon this entrance descends into
bSubDungeonIsHQ bool Sub-dungeon ends in a boss (HQ topology) vs exit
bSubDungeonRequiresKey bool Descent consumes a key item
SubDungeonKeyItemId FPrimaryAssetId Key item consumed on descent

ASubDungeonEntrance

An interactable actor (cave mouth, castle gate, stairs, portal) placed inside the Landmark room level. It needs no per-instance config — it reads its owning room:

Player interacts (server-only)
    ├─ GetOwningRoomData() — resolve UEternalRoomData via GetRoomDataForActor
    ├─ TryConsumeKey() — no key required, or key found and consumed from interactor's inventory
    └─ WorldMapSubsystem->EnterSubDungeon(SubDungeonConfig, bSubDungeonIsHQ)
            │  reuses the dungeon-entry flow (transition + generate)
            │  WITHOUT changing the current world-map node
    Sub-dungeon's AEndPortalActor → back to world map, parent node marked cleared

Placement Constraint

UDomainDungeonConfig::LandmarkMinCellsFromStart sets a minimum cell distance between the Room.Start room and any Room.Landmark room. During generation the server rejects and retries layouts that place a landmark closer than this (0 = no constraint).

Reference: ImplementationDocs/LandmarkPOI.plan.md.


Remnant Realm Room Injection

Room.RemnantRealm is the one content slot that no topology template authors. It is injected into the topology-derived room set after Step 3, so a domain can grow an optional Remnant portal room on any of its existing topologies without forking them.

Roll

UDungeonSubsystem::ResolveRemnantRealmChance resolves the chance before generation (node override → domain RemnantRealmChance), and UEternalDungeonGenerator::TryInjectRemnantRealmRoom rolls it.

The roll runs on its own FRandomStream, salted off the descent seed with the hash of the slot tag's string (never the FGameplayTag/FName, whose hash varies per process and would break seed replay). Because the stream is separate, the roll neither consumes nor perturbs the layout stream — a domain that turns Remnants on does not reshuffle its existing dungeons — and the same descent always answers the same way.

Host Selection and Retag

The injected room hangs off a host road room, not a free field cell:

Step Rule
Pool guard Both Room.RemnantRealm and Room.Junction pools must be non-empty, or injection is skipped entirely (no remnant beats a dungeon that retries itself to death)
Host candidates ERoomRole::Normal rooms with slot Room.Combat and exactly 2 connections — plain road segments. Critical-path candidates preferred; off-path road rooms are the fallback so a guaranteed chance still lands on all-optional topologies
Pick Deterministic — topology node order, indexed by the Remnant stream
Injection New FDungeonRoom on the host's floor, TopologyNodeIndex = INDEX_NONE, bidirectionally connected to the host
Host retag The host becomes 3-way, so its slot is retagged to Room.Junction — only the junction pool has a third door socket and fork art. Side effect: the host's encounter now resolves through the junction slot

Never hosted by Start, Terminal, an existing junction, or any slot carrying its own authored content (Landmark, Treasure, …) — the retag would silently erase that content and change its encounter.

Distance Band

AEternalDungeonActor::SatisfiesRemnantPlacement gates the spatial result the same way landmarks are gated: after the plugin places rooms, the Manhattan distance from the Start room to each placed Room.RemnantRealm room must sit inside [RemnantMinCellsFromStart, RemnantMaxCellsFromStart]. Layouts outside the band are rejected and retried, not relaxed.

Keep the band generous. The remnant's graph host is fixed per seed, so a band the layout cannot reach fails the whole dungeon once the plugin exhausts its retry budget.

Realm World Placement

The room hosts an ARemnantPortalActor; the realm itself is a sub-level streamed into the persistent world at URemnantRealmSubsystem's reserved world offset (0, 500000, 0) — lateral and far beyond any generated dungeon's extent, at the same Z so height-dependent sky/fog reads as authored. A portal that leaves RealmSpawnLocation at zero resolves to that offset, which is why procedurally placed portals need no hand-authored world position. A future multi-realm allocator hands out reserved-offset + N × stride slots from the same origin. See Remnant System.


Environment Tiles / Forest Domain Generation

Dungeon generation runs as a two-phase pipeline. The same pipeline serves sealed dungeons and open D3/PoE-style zones (first target: a Forest domain); "open vs closed" is authored tile content plus a fill policy, not a separate algorithm.

Phase What Where
A — Content placement Plugin door-walks the topology graph and places designer-authored CONTENT rooms (Start, Combat, Treasure, Boss, Exit …), collision-checked and streamed. Unchanged. ChooseFirstRoomData / ChooseNextRoomData
B — Space fill Places doorless environment tiles (Filler interior, Edge border) into empty grid cells around the content footprint so the world reads as one continuous place instead of floating slabs. AEternalDungeonActor fill pass

Fill runs server-only during generation, before the Load state (invoked from the generator's OnStateEnd(Generation) hook, once). Tiles are placed as first-class plugin Graph rooms, so they ride the existing Graph subobject replication and clients stream them from the replicated list — determinism is by construction, not by re-rolling a seed.

UEternalEnvironmentTile

A doorless space tile, subclassed from the plugin's URoomData so it rides AddRoomToDungeon natively, but kept conceptually separate from content rooms (own pools, own canvas node, never a Room.* content slot).

Field Type Purpose
TileKind ETileKind Filler (interior walkable ground) or Edge (blocking border)
DisplayName FText Editor/UI label
EdgeKeyNorth/East/South/West FGameplayTag Reserved for future edge-key matching / WFC (coherent rivers, coastlines); unused in v1

ETileKind = { Filler, Edge }; EEdgeStyle = { Treeline, Cliff, Water } selects which authored edge variant a border prefers. The tile overrides IsDataValid to drop the plugin's >=1-door requirement (environment tiles are intentionally doorless) while keeping the other validations.

The exemption is environment tiles only. Content rooms — Room.RemnantRealm included — are door-connected and validate normally; UEternalRoomData::IsDataValid adds only a SlotTag-is-set check on top of the plugin's.

FFillPolicy (config knobs)

The per-domain policy for Phase B — the "field generator" knobs. Global scalars only; no spatial/directional rules (those belong to edge keys / future WFC).

Field Default Purpose
bFillEnclosedGaps true Always fill cells fully enclosed by occupied neighbors (fixes self-fold voids), regardless of FillRadius
FillRadius 0 Cells within this many tiles of any content room become walkable Filler (0 = enclosed gaps only; 1-2 = road shoulders; large = open wander-field)
BorderThickness 1 Tiles of blocking Edge placed as a ring beyond the filled field
EdgeStyle Treeline Which EEdgeStyle variant to prefer from the domain's Edge pool

FillRadius + BorderThickness span the whole spectrum: tight corridor -> road shoulders -> open field with a guiding road (PoE model), with one placement strategy.

UDomainDungeonConfig Environment Fields

Field Type Purpose
FillerTiles TArray\<UEternalEnvironmentTile*> Interior walkable ground pool
EdgeTiles TArray\<UEternalEnvironmentTile*> Blocking border pool (becomes the playable bounds)
FillPolicy FFillPolicy Fill radius / border / edge style
GetEnvironmentTiles(Kind) accessor Returns the Filler or Edge pool for a given ETileKind

Fill Data Flow

AEternalDungeonActor (server) generates
    ├─ Phase A: plugin door-walks CONTENT rooms (ChooseFirst/NextRoomData)
    └─ OnStateEnd(Generation): FillEnvironmentTiles()
            │  build Occupied from CONTENT rooms only (Cast-skip env tiles)
            ├─ Pass 1 (Filler): dilate(Occupied, FillRadius) + enclosed gaps
            └─ Pass 2 (Edge): ring around (content ∪ filler), BorderThickness deep
                    │  each placed via CreateRoomInstance + AddRoomToDungeon(bFailIfNotConnected=false)
        plugin replicates Graph subobject → clients SynchronizeRooms / LoadAllRooms
        Load state → nav build (content + edge tiles included)

Reference: ImplementationDocs/ForestDomainGeneration.plan.md.


Generation Pipeline

5-Step Process

GenerateDungeon(Config, Seed, bIsHQ)
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: SelectTopology                                          │
│   • Pick from Config->StandardTopologies or HQTopologies        │
│   • Random selection using FRandomStream(Seed)                  │
│   • Call CalculateDepths() for floor assignment                 │
└────────────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: CreateRoomsFromTopology                                 │
│   • For each FTopologyNode:                                     │
│     • Create FDungeonRoom with new FGuid                        │
│     • Copy Role + SlotTag from node                             │
│     • SelectRoomTemplate(Node.SlotTag, Config, Random)          │
│     • Assign RoomSeed = Random.RandHelper()                     │
│     • Set FloorIndex from Node.Depth                            │
│   • Group into FDungeonFloor by depth                           │
└────────────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: BuildRoomConnections                                    │
│   • For each topology edge (NodeA → NodeB):                     │
│     • Get RoomA.RoomID, RoomB.RoomID                            │
│     • Add bidirectional connection                              │
└────────────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: CalculateMapPositions                                   │
│   • Assign FVector2D positions for UI display                   │
│   • X: Distribute rooms horizontally within floor               │
│   • Y: Based on FloorIndex                                      │
└────────────────────────────────┬────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Step 5: InitializeRoomStates                                    │
│   • StartRoom → Available                                       │
│   • All other rooms → Locked                                    │
│   • Set CurrentRoomID = InvalidGuid (player not yet inside)     │
└─────────────────────────────────────────────────────────────────┘

Determinism Guarantee

Same inputs always produce identical output:

Input Effect
Seed Initializes FRandomStream (derived from node identity + clear count — FWorldMapNode::GetDescentSeed())
Config Provides topology + room pools
bIsHQ Selects Standard vs HQ topologies

For multiplayer: only the server generates. Clients never regenerate — they receive the plugin's replicated UDungeonGraph (room instances, positions, connections) and stream the same levels the server placed. The seed matters for reproducibility (re-entering a node re-rolls it via clear count), not for client sync.


Plugin Bridge

AEternalDungeonActor

Extends ADungeonGenerator from ProceduralDungeon plugin to use our topology.

Our System                          Plugin System
┌────────────────────┐              ┌────────────────────┐
│ FDungeonInstance   │              │ ADungeonGenerator  │
│  • FDungeonRoom    │──────────────│  • URoom (spatial) │
│  • Connections     │  Bridged by  │  • Doors           │
│  • Roles/SlotTags  │  EternalDung │  • Level Streaming │
└────────────────────┘  eonActor    └────────────────────┘

Key Overrides

Override Purpose
ChooseFirstRoomData() Return Start room's UEternalRoomData
ChooseNextRoomData() Return connected room based on topology
IsValidDungeon() All topology rooms placed
ContinueToAddRoom() Rooms remaining in queue
ChooseDoor() Spawn AEternalDoor between rooms
OnPostGeneration() Register URoom ↔ FGuid mappings

Room Mapping Flow

OnDungeonGenerated(Topology)
InitializeFromTopology()
    │ Build placement queue (BFS from start)
Plugin calls ChooseFirstRoomData() / ChooseNextRoomData()
    │ We return URoomData from our pools
    │ Plugin handles spatial placement
OnPostGeneration()
For each placed URoom:
DungeonSubsystem->RegisterPluginRoom(URoom, FGuid)
OnDungeonReady broadcast

Bidirectional Mapping

Direction Map Use Case
PluginRoomToRoomID URoom* → FGuid Door transitions
RoomIDToPluginRoom FGuid → URoom* State queries

Runtime State Management

Room State Transitions

TryEnterRoom(RoomID)
    ├─ CanEnterRoom(RoomID)?
    │       ├─ Room.State == Available or Cleared → YES
    │       └─ Otherwise → NO, return false
    ├─ Previous room exists?
    │       └─ Keep as Cleared
    ├─ SetRoomState(RoomID, Current)
    └─ OnCurrentRoomChanged broadcast


MarkRoomCleared(RoomID)
    ├─ SetRoomState(RoomID, Cleared)
    ├─ UnlockConnectedRooms(RoomID)
    │       │
    │       └─ For each connected room:
    │               If Locked → SetRoomState(Available)
    └─ Is terminal room (Boss/Exit)?
            └─ OnDungeonCleared broadcast

Area Context

When entering a dungeon node, WorldMapSubsystem sets context:

SetAreaContext(AreaLevel, Encounter, AreaModifiers)
Property Source Used By
CurrentAreaLevel Node.Depth Enemy scaling
CurrentEncounter WorldMapData resolution Spawners
CurrentAreaModifiers Node.RolledModifiers DungeonModifierComponent

Area modifiers are applied to each player via UDungeonModifierComponent during the spawn flow. The modifier pool is also registered with ModifierSubsystem so that GetDefinition() works for both item and dungeon modifiers.


WorldMap Integration

Entry Flow

WorldMapSubsystem::TravelToNode(DungeonNode)
    ├─ Node.GeneratesDungeon() → true
    ├─ GetEffectiveDungeonConfig(Node)
    ├─ SetAreaContext(Depth, Encounter, RolledModifiers)
    │       └─ Registers modifier pool with ModifierSubsystem
    └─ DungeonSubsystem->GenerateDungeon(Config, bIsHQ)
    OnDungeonGenerated → AEternalDungeonActor starts generation
    OnDungeonReady → Player can enter Start room

Exit Flow

Player clears Exit/Boss room
MarkRoomCleared(TerminalRoomID)
OnDungeonCleared broadcast
WorldMapSubsystem->MarkNodeCleared(CurrentNodeID)
    ├─ UpdateUnlockedNodes() → adjacent nodes become Available
    └─ (If HQ) LiberateDomain(DomainTag)

Fixed Level Unloading

When traveling to a Hub/Camp, dungeons are unloaded:

AEternalDungeonActor::OnFixedLevelEntered(NodeID, Level)
Destroy generated dungeon
DungeonSubsystem->ClearDungeon()

Public Contracts

Methods

Method Parameters Purpose
GenerateDungeon Config, bIsHQ Generate with random seed
GenerateDungeonWithSeed Config, Seed, bIsHQ Generate with specific seed
ClearDungeon - Clear current dungeon
SetAreaContext AreaLevel, Encounter, AreaModifiers Set spawner/modifier context
ApplyAreaModifiersToPlayer AEternalPlayerCharacter* Apply current modifiers via DungeonModifierComponent
RemoveAreaModifiersFromAllPlayers - Clear modifiers from all players
HasActiveDungeon - Check if dungeon exists
GetDungeon - Get current FDungeonInstance
GetCurrentRoom - Get player's current room
GetRoom RoomID Get room by ID
GetAccessibleRoomIDs - Get enterable room IDs
CanEnterRoom RoomID Check if room is accessible
TryEnterRoom RoomID Attempt room transition
MarkRoomCleared RoomID Complete a room
IsDungeonCleared - Check if terminal reached
RegisterPluginRoom URoom*, FGuid Map plugin room to our ID
GetRoomIDFromPluginRoom URoom* Lookup our ID from plugin room
GetPluginRoomFromRoomID FGuid Lookup plugin room from our ID
OnPlayerEnteredPluginRoom URoom* Handle door transition
GetRoomIDForLevel ULevel* Resolve RoomID from streaming level instance

Delegates

Delegate Payload When Fired
OnDungeonGenerated FDungeonInstance& Topology created, rooms not streamed
OnDungeonReady - All rooms loaded, ready to play
OnRoomStateChanged FGuid, ERoomState Any room state changes
OnCurrentRoomChanged FGuid Player entered new room
OnDungeonCleared - Terminal room (Exit/Boss) cleared

Source References

Core Types

  • FDungeonRoom - Source/ProjectEternal/Public/Dungeon/DungeonTypes.h:39
  • FDungeonFloor - Source/ProjectEternal/Public/Dungeon/DungeonTypes.h:102
  • FDungeonInstance - Source/ProjectEternal/Public/Dungeon/DungeonTypes.h:127
  • ERoomRole - Source/ProjectEternal/Public/Dungeon/DungeonTypes.h:15
  • ERoomState - Source/ProjectEternal/Public/Dungeon/DungeonTypes.h:26

Topology

  • FTopologyNode - Source/ProjectEternal/Public/Dungeon/DungeonTopology.h:13
  • FDungeonTopology - Source/ProjectEternal/Public/Dungeon/DungeonTopology.h:49

Subsystem & Generator

  • UDungeonSubsystem - Source/ProjectEternal/Public/Dungeon/DungeonSubsystem.h:27
  • UEternalDungeonGenerator - Source/ProjectEternal/Public/Dungeon/EternalDungeonGenerator.h:19

Plugin Bridge

  • AEternalDungeonActor - Source/ProjectEternal/Public/Dungeon/EternalDungeonActor.h:23

Configuration

  • UDomainDungeonConfig - Source/ProjectEternal/Public/Dungeon/DataAssets/DomainDungeonConfig.h:20
  • UDungeonModifierPoolDataAsset - Source/ProjectEternal/Public/Dungeon/DataAssets/DungeonModifierPoolDataAsset.h
  • RemnantRealmChance / RemnantMinCellsFromStart / RemnantMaxCellsFromStart - Source/ProjectEternal/Public/Dungeon/DataAssets/DomainDungeonConfig.h:112

Remnant Realm Injection

  • UEternalDungeonGenerator::TryInjectRemnantRealmRoom - Source/ProjectEternal/Private/Dungeon/EternalDungeonGenerator.cpp:243
  • UDungeonSubsystem::ResolveRemnantRealmChance - Source/ProjectEternal/Private/Dungeon/DungeonSubsystem.cpp:97
  • AEternalDungeonActor::SatisfiesRemnantPlacement - Source/ProjectEternal/Private/Dungeon/EternalDungeonActor.cpp:617
  • URemnantRealmSubsystem reserved offset - Source/ProjectEternal/Private/Remnant/Subsystems/RemnantRealmSubsystem.cpp:17

Room & Environment Tiles

  • UEternalRoomData (+ GetRoomDataForActor) - Source/ProjectEternal/Public/Dungeon/DataAssets/EternalRoomData.h
  • UEternalEnvironmentTile / ETileKind / EEdgeStyle / FFillPolicy - Source/ProjectEternal/Public/Dungeon/DataAssets/EternalEnvironmentTile.h
  • FillerTiles / EdgeTiles / FillPolicy / GetEnvironmentTiles - Source/ProjectEternal/Public/Dungeon/DataAssets/DomainDungeonConfig.h:88

Dungeon Actors

  • ARoomEncounterActor - Source/ProjectEternal/Public/Dungeon/Actors/RoomEncounterActor.h
  • AEndPortalActor - Source/ProjectEternal/Public/Dungeon/Actors/EndPortalActor.h
  • AEncounterGateActor - Source/ProjectEternal/Public/Dungeon/Actors/EncounterGateActor.h
  • ASubDungeonEntrance - Source/ProjectEternal/Public/Dungeon/Actors/SubDungeonEntrance.h

Area Modifiers

  • UDungeonModifierComponent - Source/ProjectEternal/Public/Dungeon/Components/DungeonModifierComponent.h


Recent Changes

Date Change Impact
2026-08-06 Remnant Realm rooms are injected, door-connected content TryInjectRemnantRealmRoom hangs a Room.RemnantRealm room off a 2-edge road host (host retagged to Room.Junction), rolled on its own descent-seed stream; SatisfiesRemnantPlacement rejects and retries layouts outside RemnantMin/MaxCellsFromStart. Supersedes the 2026-07-29 row below: there is no door-count exemption for Room.RemnantRealm — only UEternalEnvironmentTile is exempt
2026-07-29 Enemy density policy on DomainDungeonConfig FEnemyDensityPolicy (EnvironmentSpawnerChance + MaxEnvironmentEnemies) gates environment-tile spawners in MarkRoomsReady; content-room spawners exempt. See Enemy Performance
2026-07-29 Room.RemnantRealm exempt from ≥1-door validation Remnant realm rooms are space-fill tiles placed without doors; the door-count validator now skips them
2026-07-03 Content model: ERoomRole + SlotTag ERoomType/GetRoomsByType removed; structure = ERoomRole (Start/Normal/Terminal), content = FGameplayTag slot (Room.*) resolved via tag-addressed RoomPools map (GetRoomsBySlot); per-slot encounter overrides (SlotEncounters)
2026-07-03 Landmark / POI sub-dungeons Room.Landmark slot rooms host ASubDungeonEntrance descending into room-data-authored SubDungeonConfig via EnterSubDungeon (run-ender, optional key consumption); LandmarkMinCellsFromStart placement constraint
2026-06-29 Two-phase generation (Forest domain) Phase B space-fill places doorless UEternalEnvironmentTile (Filler/Edge) as server-authored plugin Graph rooms around the content footprint; open vs closed zones are data (FFillPolicy + tile pools), one pipeline
2026-06-29 Environment-tile config knobs FillerTiles/EdgeTiles/FillPolicy + GetEnvironmentTiles() on UDomainDungeonConfig; FillRadius+BorderThickness span tight corridor → open field-with-road
2026-06-29 GetRoomDataForActor Canonical room-resolution helper on UEternalRoomData (actor → owning ARoomLevel → room data)
2026-03 Encounter gate actor AEncounterGateActor blocks room exits until encounter clears, designer-wired to ARoomEncounterActor
2026-03 Custom shader directory /ProjectEternal/ virtual shader path registered in FProjectEternalModule for custom material functions
2026-03 Dungeon completion actors ARoomEncounterActor tracks encounter, AEndPortalActor handles exit interaction
2026-03 GetRoomIDForLevel Resolve RoomID from streaming level instance for encounter/portal room binding
2026-02 Area modifier system ModifierPool on config, DungeonModifierComponent applies GAS effects to players
2026-02 SetAreaContext expanded Now accepts AreaModifiers, registers pool with ModifierSubsystem
2025-01-17 Initial documentation Document dungeon system architecture