Burnsies HideoutDocsBurnsie.Common

Crowd Engine — civil-disorder scenes

Burnsie.Common.Lspdfr.Crowds is a toolkit for building crowd scenes — protests, brawls, riots — that stay performant, stay in tone, and clean up after themselves. It composes with the rest of the library rather than replacing it: entities go in your EntityBag, dispatch goes through BackupDispatcher, natives through the fenced CrowdNatives.

The pieces

TypeRole
CrowdEngineOptionsPerformance budgets + tone guardrails, populated from your ConfigSchema
CrowdSpawnerModel-agnostic ped/vehicle spawning: load-with-timeout, bad-model memory, ground-snap
FactionManagerScene-scoped relationship groups so factions fight each other autonomously
CrowdA managed crowd: members, per-ped behavior driven by hostility, dispersal state machine
HostilityMeter0–100 hostility model with named bands, event deltas and passive decay
CrowdArsenalVerified weapon pools with tone-gated arming (melee / incendiary / firearm)
EscalationLadderPolice response waves over BackupDispatcher, re-tasking arrivals to scene posts
RoadClosureRoad-node shutdown + lit police blockers around a road-blocking scene

A scene, end to end

// Setup (inside a BurnsieCallout.OnAccepted, typically):
var options  = new CrowdEngineOptions();               // or populated from your config
var spawner  = new CrowdSpawner(Log);
var factions = new FactionManager(Log, CalloutName);   // scope prevents cross-callout collisions
var meter    = new HostilityMeter(Log, initial: 30f);
var crowd    = new Crowd(Log, Scene, spawner, factions, rng) { MaxMembers = options.MaxPeds };

// Factions that hate each other, but not the police (until provoked):
RelationshipGroup mob   = factions.Faction("Mob");
RelationshipGroup rival = factions.Faction("Rival");
FactionManager.MakeMutualEnemies(mob, rival);

// Populate in budget-friendly batches (yields between batches — no frame spikes):
crowd.Populate(mobModels,   Anchor, 3f, 14f, 12, mob,   canFight: true,
               options.SpawnBatchSize, options.SpawnBatchDelayMs);
crowd.Populate(rivalModels, Anchor, 3f, 14f, 12, rival, canFight: true,
               options.SpawnBatchSize, options.SpawnBatchDelayMs);
crowd.MarkRingleaders(2);   // red-blipped anchors of the scene

// Tick (from your callout loop):
meter.Tick();               // passive decay (real time — dilation-proof)
crowd.Tick(meter.Band);     // drives per-ped behavior from the current band

// Scenario events feed the meter:
meter.Add(+15f);            // bottle thrown
meter.Add(-20f);            // ringleader arrested

HostilityMeter — the scene's mood

A 0–100 value with named bands that drive everything else:

BandRangeCrowd reads as
Calm0–24containable by presence alone
Agitated25–49tension building; members wander restlessly
Volatile50–74sporadic violence; fighters engage
Rioting75–100full crowd violence
  • Scenarios feed discrete events via Add(delta) (player wades in, gas deployed, ringleader arrested); Tick() applies passive decay (default 0.6/s, real time).
  • BandChanged fires synchronously on band transitions — bind HUD updates or escalation triggers to it.
  • Fraction (0–1) feeds a TimerBarHud progress bar directly; BandLabel() gives a HUD-ready string ("VOLATILE").
  • Not internally locked: drive it from one fiber (your callout tick), which is the intended usage.

Crowd — members and behavior

  • Populate(models, anchor, minRadius, maxRadius, count, faction, canFight, …) spawns in batches with a hard cap (MaxMembers); returns the honest created count. Spawned peds are registered in the scene EntityBag and assigned to their faction automatically.
  • MarkRingleaders(n) promotes random fighters to red-blipped ringleaders — arrest targets that anchor the scene.
  • Tick(band) prunes dead/missing members and drives the state machine: Idle → Agitated → Fighting → Fleeing/Cowering → Gone. Fighters engage their nearest hated target (via faction relationships) in Volatile/Rioting; everyone wanders restlessly in Agitated.
  • DisperseAll() sends everyone fleeing (stand-down); CowerNear(position, radius) makes members near a point cower (gas deployment).
  • ActiveCount / RingleadersRemaining feed objectives ("disperse the crowd", "arrest the ringleaders").

FactionManager — who hates whom

  • Faction(name) creates (or reuses) a relationship group named BC_<scope>_<name>. Groups persist in the game's registry for the session (there is no managed delete API), so names are deliberately bounded and reused rather than leaked per callout.
  • MakeMutualEnemies(a, b), MakeMutual(a, b, level), SetRelationship(a, b, level) — declare the hate matrix once.
  • MakeHostileToCops(faction) turns a faction on police and player — use sparingly; most crowd scenes should stay faction-vs-faction until provoked. CalmTowardCops reverses it (de-escalation).
  • FactionManager.Cops / PlayerGroup expose the stock groups.

CrowdArsenal — tone-gated arming

Named weapon pools (all verified WeaponHash members) instead of raw hashes:

CrowdArsenal.GiveMelee(ped, rng);                        // bat/crowbar/hammer/club/knife
CrowdArsenal.GiveIncendiary(ped, rng, options);          // molotov — IF tone gates allow
bool armed = CrowdArsenal.GiveEscalationFirearm(ped, rng, options);  // pistol — IF allowed

The gates live in CrowdEngineOptions: with HeaviestScenariosEnabled off (or the specific AllowIncendiaryWeapons / AllowFirearmEscalation sub-flags), those requests silently degrade to common melee — scenario code never needs to check the config itself. GiveEscalationFirearm returns whether a firearm was actually given, so the scenario can adjust its dispatch response.

EscalationLadder — the police response

A thin composition over BackupDispatcher (dispatch, tracking, correlation and cancellation stay there) that adds two things:

  1. The civil-disorder wave ladder: DefaultLadder(includeFire, includeEms) builds L1 patrol → L2 state+support → L3 SWAT → L4 NOOSE+air. The heaviest waves are omitted when HeaviestScenariosEnabled is false.
  2. Deployment: arriving units are re-tasked to scene posts — occupants leave the vehicle, walk to the post, then hold or engage.
var ladder = new EscalationLadder(Log, dispatcher, options);
ladder.Run(Fibers, Flag,
    ladder.DefaultLadder(includeFire: true, includeEms: true),
    incidentPosition: () => Anchor,
    postProvider: unit => NextPost(unit),      // null = leave unit to LSPDFR's own AI
    continueEscalation: () => meter.Band >= HostilityBand.Volatile);

// posts:
new DeployPost { Position = p, Heading = h, Role = DeployRole.SkirmishLine }; // engage hated factions
new DeployPost { Position = p, Heading = h, Role = DeployRole.Cordon };       // hold facing outward
new DeployPost { Position = p, Heading = h, Role = DeployRole.Overwatch };    // observe; drivers stay in cars

// stand-down (must run on a GameFiber):
ladder.StandDown(fadeBlips: true);

StandDown un-busies every cop the ladder tasked — busy flags and task-control flags (SetCopAsBusy, SetCopIgnoreAmbientCombatControl, BlockPermanentEvents, KeepTasks) — then ends the dispatch session. Restore-what-you-set applies to cops too: miss this and LSPDFR's response AI never gets its officers back. PostsManned reports deployed units.

RoadClosure — sealing the streets

For road-blocking scenes: disables the vehicle road nodes in a box (ambient traffic reroutes instead of driving through your event) and parks lit police blockers across the approaches.

var closure = new RoadClosure(Log);
closure.Close(Scene, Anchor, radius: 30f, cruisers: 3, rng);
// heavier public-order look — riot vans instead of cruisers:
closure.Close(Scene, Anchor, 30f, 2, rng, RoadClosure.RiotVanModels);

// on EVERY exit path (OnCleanup): idempotent
closure.Restore();

Details handled for you: blocker spots are probed in 8 compass directions across several distance rings (overpasses and odd geometry only resolve street nodes at some probes), deduped and spread apart; blockers get lights-no-siren and a guarding officer each; spawned vehicles/officers go in your EntityBag. Restore() is mandatory — closed road nodes outlive your scene otherwise.

CrowdEngineOptions — budgets and guardrails

Populated by your plugin from its ConfigSchema so users tune everything in one INI.

Performance: MaxPeds (30), MaxVehicles (8), PedBudgetFraction (0.5 of World.PedCapacity), SpawnBatchSize (4) / SpawnBatchDelayMs (60), DismissRadius (120 m), SuppressAmbients + DensitySuppression (0.10 — remember density natives are per-frame; see Safe natives).

Tone: HeaviestScenariosEnabled (master gate for riot fires, molotovs, lethal escalation), AllowIncendiaryWeapons, AllowFirearmEscalation (off by default).

Rules

  • Everything here runs on GameFibers.
  • The crowd's peds belong to your scene EntityBag; teardown is the bag plus RoadClosure.Restore() plus ladder.StandDown() — wire all three into your callout's cleanup path.
  • Respect the budgets: the caps exist because 60 script peds with combat AI is a slideshow on a mid-range machine.