# World Streaming & Procedural Generation

V4's sandboxes are its hardest spatial problem. A Hitman level wants a 50 m-cell
mansion full of patrolling staff; a Commandos or Desperados map wants a wide
exterior with a hundred bodies in it; the roguelike-tactics campaign and the
daily-mission ladder want a _different, fair, fully reachable_ layout every run
— and all of it has to stay inside a console frame budget while six genres share
one engine. Three in-tree C++ subsystems carry that load, and they are
deliberately small and deterministic so they can be reasoned about and tested:
**`V4LevelOps`** owns World-Partition streaming profiles and HLOD pre-build
planning, **`V4Procgen`** is a seeded grid-layout solver with bot-path
playability proofs and daily seeds, and **`V4Crowd`** is a tiered crowd model
with panic, disguise, exfil routing, and a performance governor. They are
separate modules, but they meet at exactly one shared idea: the **integer
streaming cell** — a `FloorToInt(location / cellSize)` lattice that the streamer
loads against and the crowd demotes against, computed by identical math on both
sides so the two halves can never disagree about which cell an NPC is standing
in.

This page is the world-and-content companion to the netcode/modes cluster. It is
the deep dive behind the "Sandbox Streaming," "Procedural Generation Pipeline,"
and "V4Crowd" summaries in the orientation hub
[../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md), and it sits next to
[./game-modes-live-service.md](./game-modes-live-service.md) (the modes that
_consume_ a daily seed or a streamed map),
[./per-cell-deep-dives.md](./per-cell-deep-dives.md) (the genre cores that those
modes layer on), and [./presentation-pipelines.md](./presentation-pipelines.md)
(the audio/VFX/HLOD art these systems reference but do not bake).

## What ships, honestly

Three layers, stated plainly.

- **Real, deterministic, and test-covered.** All three modules compile — the
  linker has produced `Binaries/Linux/libUnrealEditor-V4Procgen.so`,
  `…-V4Crowd.so`, and the `V4LevelOps` library on this box. The algorithms are
  domain-specific, not renamed CRUD: a seed-stable constraint solver with a BFS
  reachability proof (`V4Procgen`), a capacity-aware nearest-exfil flee router
  and a relevance-sorted tick governor (`V4Crowd`), and a concentric loaded-ring
  / distant-HLOD-ring planner with a megabyte budget pruner (`V4LevelOps`). Each
  is pinned by a dedicated automation spec —
  `V4.Procgen.Runtime.GraphsDeterminismValidationDailySeed`,
  `V4.Crowd.Panic.PropagatesAcrossPopulation`,
  `V4.Crowd.PerformanceBudget.DemotesToEightMilliseconds`, and the `V4LevelOps`
  streaming spec — that assert _computed values_, not just truthiness.
- **Modeled as data, not baked art.** Consistent with V4's tree-wide
  0-binary-`.uasset` accounting (see
  [./high-level-architecture.md](./high-level-architecture.md)), every asset
  these systems name is a _soft reference_, not a cooked binary. A cell's
  `PCGGraph` is a `TSoftObjectPtr<UPCGGraphInterface>` pointing at
  `/Game/V4Procgen/PCG/PCG_Tactical`; a chunk's `ChunkAsset` is an
  `FSoftObjectPath` at `/Game/V4Procgen/Chunks/…`; an HLOD layer is a path
  string like `/Game/V4LevelOps/HLOD/DA_HLOD_HitmanHighDetail`. None of those
  targets exist as binaries in-tree. Crucially, **the generation that actually
  runs is the hand-written C++ grid solver, not PCG-node execution** —
  `V4Procgen` links the `PCG` module and stores the soft graph pointer as
  authoring metadata, but `GenerateMissionLayout` never loads or evaluates a
  `UPCGGraph`.
- **Library-grade, not yet orchestrated.** Each entry point is exercised by its
  automation spec and by nothing else in the tree. `GenerateMissionLayout` /
  `DispatchDailySeed` have no production caller (the "server broadcasts a seed
  at 00:00 UTC" loop is described in the monolith, not a running service here);
  `BuildStreamingState` and `DemoteAgentsOutsideLoadedCells` likewise have no
  shared orchestrator that calls both per tick. The two streaming halves agree
  on cell math _by construction_, but the glue that feeds the streamer's
  `LoadedCells` into the crowd's demotion pass at runtime is not in-tree. This
  is the honest gap between the assembled game loop and the proven building
  blocks.

## World streaming — `V4LevelOps`

### World-Partition map profiles

Streaming is configured per map by an `FV4WorldPartitionMapProfile`
(`V4LevelOps/Public/V4LevelOpsTypes.h:127`): a `CellSizeMeters`, a
`StreamingRingRadiusCells`, a `HlodPrebuildRingRadiusCells`, a
`HlodBudgetMegabytes` / `EstimatedHlodMegabytesPerCell` pair, and an
`HlodLayerAsset` soft path. `RegisterWorldPartitionProfile`
(`V4LevelOpsSubsystem.cpp:280`) sanitizes the values — clamps the cell size and
ring radii, forces `HlodPrebuildRingRadiusCells ≥ StreamingRingRadiusCells + 1`
so the HLOD ring is always strictly outside the loaded ring, and **hard-pins any
`bHighDetailHitmanMap` profile to 50 m cells** (`:297`).
`BuildDefaultWorldPartitionProfiles` (`:316`) ships four launch rows that encode
the monolith's spec directly: `DefaultSandbox` at 100 m / 128 MB, and `Paris`,
`Sapienza`, `Miami` (the Hitman sandboxes) at 50 m / 96 MB pointing at
`DA_HLOD_HitmanHighDetail`. These are real authored constants the streaming spec
reads back: it asserts the default sandbox uses 100 m cells and a one-cell ring,
and the high-detail Hitman map uses 50 m cells.

### The streaming ring and distant HLOD

`BuildStreamingState` (`V4LevelOpsSubsystem.cpp:358`) is the per-frame planner.
It resolves the player's cell with `GetWorldPartitionCellForLocation` (`:453`) —
`FloorToInt(Location / (CellSizeMeters × 100))`, i.e. world centimetres to a
metre-cell lattice — then fills two arrays. `V4BuildCellRing` (`:59`) emits the
loaded set as the square ring of radius `StreamingRingRadiusCells` around the
player (radius 1 → a 3×3 = 9-cell block). `V4BuildDistantHlodCells` (`:75`)
emits the HLOD set as every cell out to `HlodPrebuildRingRadiusCells` _minus_
the cells already in the loaded ring — the donut between the streamed interior
and the horizon. The streaming spec proves the geometry exactly: a player at
`(5250, −1250)` on the 50 m Paris profile lands in cell `(1, −1)`, the one-cell
ring yields 9 loaded cells including the diagonal neighbour `(0, −2)`, and the
radius-2 HLOD donut yields 16 cells (the 5×5 = 25 outer block minus the 3×3 = 9
inner block).

### HLOD budget enforcement

Pre-built HLOD for distant cells costs VRAM, so it is budgeted, not unbounded.
`BuildHlodForProfile` (`:385`) computes
`EstimatedMegabytes = HlodCellCount × EstimatedHlodMegabytesPerCell` and flags
`bWithinBudget` against the profile's ceiling — for the Hitman profile that is
16 cells × 6 MB = 96 MB, exactly its 96 MB budget, which the spec confirms
passes. When a profile _overflows_, `EnforceHlodBudget` (`:422`) does real work
rather than just reporting failure: it divides the budget by the per-cell cost
to get a maximum cell count and `SetNum`-truncates the HLOD list to fit, then
recomputes the estimate. The spec drives this adversarially — it tightens the
Hitman budget to 24 MB, watches `bWithinBudget` go false, then enforces and
asserts the result is pruned to exactly 4 cells / 24 MB. This is a genuine
bounded-resource planner, not a constant.

### The streaming-crowd seam

The streamer's output is a list of `LoadedCells`, and that is precisely the
input the crowd's `DemoteAgentsOutsideLoadedCells` consumes. Both subsystems
compute a cell with the _same_ formula —
`V4LevelOps::GetWorldPartitionCellForLocation` and
`V4Crowd::GetStreamingCellForLocation` (`V4CrowdSubsystem.cpp:519`) are
byte-for-byte the same `FloorToInt(Location / (CellSizeMeters × 100))` — so an
NPC the streamer considers "in cell `(1,−1)`" is the _same_ cell the crowd
considers it in. That shared contract is what lets the dense crowd ride the
streaming grid for free; the honest caveat from "What ships" applies — the two
are coded to agree, but no in-tree tick loop wires one into the other yet.

## Procedural generation — `V4Procgen`

### Per-cell graphs and the chunk library

`V4Procgen` registers one `FV4ProcgenCellGraph` per procedural cell.
`BuildDefaultCellGraphs` (`V4ProcgenSubsystem.cpp:205`) authors five —
`PCG_Tactical`, `PCG_Stealth`, `PCG_RTST`, `PCG_ARPG`, `PCG_2D` — each with a
default grid size, objective count, `MinimumCriticalPathLength`, and
`BlockerDensity` tuned to its genre (Tactical 6×5 / 15 % blockers, the 2D arcade
cell a wide 8×3 / 6 %). Each graph carries a
`TSoftObjectPtr<UPCGGraphInterface>` (`V4ProcgenTypes.h:43`) — the soft PCG
reference described above. The art vocabulary is `BuildLaunchChunkLibrary`
(`:256`): **55 `FV4ProcgenChunkDefinition` rows** spread across the five graphs
(12 Tactical, 12 Stealth, 11 RTST, 10 ARPG, 10 2D), each with a role
(`Spawn`/`Objective`/`Extraction`/`Connector`/
`Encounter`/`Landmark`/`Blocker`), an archetype (`Room`/`Hallway`/`Building`/
`TerrainTile`/`Arena`/`Platform`), entry sockets, a footprint, and authored
`Procgen.Constraint.*` tags. `ValidateChunkLibrary` (`:321`) is a real linter:
it rejects the set unless it has ≥ 50 chunks, unique IDs, every one of the five
graph IDs, all six archetypes, and a `Procgen.Constraint.` tag on every row —
and the spec asserts all of those counts (55 total, 12/12/11/10/10 per graph,
every archetype present).

### The deterministic constraint solver

The generator that actually runs is `SolveChunkConstraints` (`:433`) — a
hand-written grid solver, not PCG. For each attempt it seeds a fresh
`FRandomStream(Request.Seed + Attempt × 7919)` (`:451`), so the same seed
reproduces the same world and a failed attempt deterministically perturbs the
next. It places `Spawn` on the west edge and `Extraction` on the east edge,
rejects pairs closer than `MinimumExtractionDistance` (Manhattan), scatters the
required objectives subject to a minimum spawn distance, then lays blockers up
to `BlockerDensity × gridArea` — but it only _keeps_ a blocker if the layout
still passes a playability check (`:551`), so the density target can never wall
off the mission. Finally it fills every remaining cell with a role-tagged
`FV4ProcgenChunk` and stable-sorts by `SortKey`. The spec proves determinism the
strong way: it generates the same request twice and asserts the two layouts
produce an _identical signature string_ spanning seed, spawn, extraction, every
objective, and every chunk's coord/role/blocking flag — a test that would fail
instantly against a `Math::random` stub.

### Playability and NPC-placement proofs

A mission is only shipped if a bot can actually finish it. `ValidatePlayability`
(`:622`) builds the waypoint list `spawn → objectives… → extraction` and runs
`FindPathThroughWaypoints` (`:913`), which chains `FindPathSegment` (`:835`) — a
real breadth-first search over the 4-neighbour grid that treats
`bBlocksTraversal` chunks as walls and returns the reconstructed path. If any
segment is unreachable the mission is rejected with a populated `FailureReason`;
the spec confirms a deliberately walled 3×3 layout is rejected and that the bot
path on a valid layout starts at spawn, ends at extraction, and visits every
objective. `ValidateNpcPlacement` (`:661`) layers on top: it requires a playable
bot path first, then counts cells whose role or tags admit an NPC (objective,
encounter, landmark, connector, or an explicit `Procgen.Constraint.NpcSpawn`
tag), excluding blockers and the spawn/extraction tiles, and fails if fewer than
the requested minimum qualify — the spec asks for ≥ 8 and checks the count
mirrors the returned coordinate list.

### Daily seeds

The daily ladder is plumbed but server-less here. `DispatchDailySeed` (`:723`)
stores a `{CellId, DateId, Seed, ServerSignature, DispatchUtc}` record keyed by
`CellId:DateId`; `BuildDailyDateId` (`:786`) formats a UTC calendar day; and
`GenerateDailyMissionLayout` (`:757`) looks up the stored seed and runs the same
solver so every client that downloads the same signed seed assembles a
bit-identical mission. The spec round-trips it: dispatch seed `424242` for
`2026-05-23`, reload it, generate, and assert the layout carries seed `424242`
and the date id. The signature _validation_ and the broadcast service that
issues the seed are the described-not-built part.

## Crowds — `V4Crowd`

### Tiers and the performance governor

`V4Crowd` keeps a flat `TArray<FV4CrowdAgentRecord>` and classifies each agent
into `Background` / `Medium` / `Hero` by distance and relevance.
`EvaluateTierForAgent` (`V4CrowdSubsystem.cpp:111`) promotes to Hero inside
`HeroRadius` (600 cm) or above `HeroRelevanceThreshold` (0.85), to Medium inside
`MediumRadius` (1800 cm) or above 0.35, else Background. The budget governor is
the heart of it. Each tier has a per-agent cost in `GetTierCostMilliseconds`
(`:594`) — Hero 0.035 ms, Medium 0.015 ms, Background 0.005 ms — so a 400-agent
all-Background crowd estimates 2.0 ms, comfortably under the 8 ms
`MassTickBudgetMilliseconds`. `EnforcePerformanceBudget` (`:428`) sorts by
ascending relevance (then descending tier) and demotes the cheapest-to-lose
agents one rung at a time until the estimate fits. The perf spec forces all 400
agents to Hero (14 ms, over budget), enforces, and asserts the estimate returns
≤ 8 ms while _some_ Hero/Medium agents survive — fidelity is shed from the
least-relevant edge inward, not uniformly.

### Panic propagation and exfil routing

A panic stimulus arrives as an `FV4CrowdMassSignal`. `HandlePanicMassSignal`
(`:163`) walks the crowd, and for each agent inside the radius adds panic scaled
by a linear distance falloff clamped to `[0.25, 1.0]` (`:178`); crossing 0.65
promotes a Background agent to Medium so a panicking bystander cannot be a
frozen low-LOD prop. It then calls `AssignFleeTargetsForPanic` (`:338`), a real
capacity-aware router: each fleeing agent picks the nearest exfil node _with
remaining capacity_, falling back to the nearest node overall if all are full,
and arrival time is `distance / FleeSpeedCentimetersPerSecond` (400 cm/s = the
monolith's 4 m/s). `AdvanceFleeingAgents` (`:258`) integrates motion toward the
target each tick and snaps-and-clears on arrival. The panic spec fires a signal
that reaches all 400 generated agents in under 8 ms, confirms the first agent
gets a flee target and a positive arrival estimate, advances a tick and checks
the agent moved _closer_, and — the capacity proof — sets a near node to
capacity 1 and watches the second agent spill to the farther node.

### Disguise line-of-sight checks

The Hitman "blend in until someone who knows better sees you" rule is
`HandleDisguiseLineOfSightMassSignal` (`:195`). On a line-of-sight signal it
runs `RecognizeObservedFaction` (`:646`), which returns `Disguised` if the
observer accepts the presented disguise id, `Friendly` for own/allied factions,
`Hostile` for a listed enemy, else `Neutral`. A hostile recognition compromises
the disguise, bumps the observer's panic, and promotes it off Background. The
spec exercises both edges: a guard whose hostile list contains `Intruder` flags
the disguise as compromised and is promoted, but the _same_ guard presented an
accepted `KitchenStaff` disguise returns zero hostiles — the disguise suppresses
the response.

### Streaming demotion and the Mass integration status

This is where the crowd rejoins the streamer. `DemoteAgentsOutsideLoadedCells`
(`:469`) takes the streamer's `LoadedCells` and a cell size, recomputes each
agent's streaming cell with the shared formula, marks anyone outside the ring
`bOutsideStreamingRing`, and demotes them one tier (Hero→Medium, Medium→
Background) while tallying the transitions. The perf spec builds a 3×3 loaded
ring and three agents — one inside, one Hero far east, one Medium far west — and
asserts exactly two are out-of-ring, the Hero demotes to Medium, the Medium to
Background, and the inside agent is untouched. One honest note on the Mass
framing: `V4Crowd.Build.cs` really does link `MassEntity`, `MassNavigation`,
`MassAIBehavior`, `MassMovement`, `MassReplication`, and `MassSignals`, and
`GetMassIntegrationStatus` (`:539`) reports each flag from a live
`FModuleManager::IsModuleLoaded` query rather than a constant struct — so a
dropped dependency would honestly flip a flag false. But the dense simulation
itself is the `TArray` loop above; each record stamps an `FMassEntityHandle`
(`MakeGeneratedAgent`, `:631`) as a forward-looking handle, and the subsystem
does not yet run Mass processors over an `FMassEntityManager`. It is Mass-linked
and Mass-aware, not Mass-driven.

## How a sandbox tick fits together

The intended flow — assembled from the real functions above, even though no
single in-tree loop wires all three subsystems together yet — looks like this:

```mermaid
flowchart TD
    Seed[Daily/run seed] --> Procgen
    subgraph Procgen [V4Procgen]
        Solve[SolveChunkConstraints<br/>FRandomStream seed+attempt*7919] --> Play{ValidatePlayability<br/>BFS spawn-&gt;objectives-&gt;exfil}
        Play -- unreachable --> Solve
        Play -- ok --> Npc[ValidateNpcPlacement<br/>count NPC-eligible cells]
    end
    Npc --> Layout[FV4ProcgenMissionLayout<br/>grid + roles + bot path]
    Player[Player location] --> LevelOps
    subgraph LevelOps [V4LevelOps]
        Cell[GetWorldPartitionCellForLocation<br/>floor of loc / cellSize] --> Ring[V4BuildCellRing -> LoadedCells]
        Ring --> Hlod[V4BuildDistantHlodCells -> HLOD donut]
        Hlod --> Budget[EnforceHlodBudget -> prune to MB ceiling]
    end
    Ring -- LoadedCells, cellSize --> Crowd
    subgraph Crowd [V4Crowd]
        Demote[DemoteAgentsOutsideLoadedCells<br/>shared cell math] --> Tier[EnforcePerformanceBudget<br/>relevance-sorted to 8 ms]
        Panic[HandlePanicMassSignal] --> Flee[AssignFleeTargetsForPanic<br/>capacity-aware nearest exfil]
    end
    Layout -.feeds NPC spawns.-> Crowd
```

## Related

- [./game-modes-live-service.md](./game-modes-live-service.md) — the modes that
  request a daily seed, host a streamed sandbox, and own the leaderboard the
  seed feeds
- [./per-cell-deep-dives.md](./per-cell-deep-dives.md) — the genre-core modules
  whose crowds, schedules, and disguises this page streams and populates
- [./presentation-pipelines.md](./presentation-pipelines.md) — the HLOD layers,
  ambience, and crowd-density art these systems reference as soft assets
- [./high-level-architecture.md](./high-level-architecture.md) — the module
  split and the 0-binary-`.uasset` content model
- [../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md) — the orientation hub
