🔍 s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
🔗 Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=notpointless.chomnr_adaptive_director&take=20
Showing code results for query:
*
(71 total matches found)
Game
library
namespace SboxDirector;
public sealed class PopulationLedger
{
readonly PopulationConfiguration _configuration;
readonly Dictionary<EncounterKind, int> _reserved = new();
readonly Dictionary<long, PendingReservation> _pending = new();
public PopulationLedger(PopulationConfiguration configuration) { _configuration = configuration; }
public int Available(EncounterKind kind, PopulationSnapshot population)
=> Available(kind, population, ConfiguredLimit(kind));
public int ConfiguredLimit(EncounterKind kind) => _configuration.Limits.TryGetValue(kind, out var configured) ? configured : 0;
public int Available(EncounterKind kind, PopulationSnapshot population, int limit)
{
return Math.Max(0, limit - population[kind] - (_reserved.TryGetValue(kind, out var value) ? value : 0));
}
public bool TryReserve(long requestId, EncounterKind kind, int count, PopulationSnapshot population, double expiresAt = double.PositiveInfinity)
=> TryReserveWithLimit(requestId, kind, count, population, ConfiguredLimit(kind), expiresAt);
public bool TryReserveWithLimit(long requestId, EncounterKind kind, int count, PopulationSnapshot population, int limit, double expiresAt = double.PositiveInfinity)
{
if (count <= 0 || limit < 0 || _pending.ContainsKey(requestId) || Available(kind, population, limit) < count) return false;
_pending[requestId] = new(requestId, kind, count, expiresAt); _reserved[kind] = (_reserved.TryGetValue(kind, out var value) ? value : 0) + count; return true;
}
public void Resolve(SpawnRequestResult result)
{
if (result.Result == RequestResultKind.Deferred) return;
if (!_pending.Remove(result.RequestId, out var pending)) return;
_reserved[pending.Kind] = Math.Max(0, _reserved[pending.Kind] - pending.Count);
}
public IReadOnlyList<long> CancelAll()
{
var ids = _pending.Keys.OrderBy(x => x).ToArray(); _pending.Clear(); _reserved.Clear(); return ids;
}
public IReadOnlyList<long> Expire(double time)
{
var expired = _pending.Values.Where(x => x.ExpiresAt <= time).Select(x => x.RequestId).ToArray();
foreach (var id in expired) Resolve(new SpawnRequestResult(id, RequestResultKind.Failed, 0, "request timeout"));
return expired;
}
public PopulationLedgerState Capture() => new(_pending.Values.OrderBy(x => x.RequestId).ToArray());
public void Restore(PopulationLedgerState state)
{
_pending.Clear(); _reserved.Clear();
foreach (var item in state.Pending) { _pending[item.RequestId] = item; _reserved[item.Kind] = (_reserved.TryGetValue(item.Kind, out var count) ? count : 0) + item.Count; }
}
public IReadOnlyDictionary<EncounterKind, int> Reservations => _reserved;
public IReadOnlyList<PendingReservation> Pending => _pending.Values.OrderBy(x => x.RequestId).ToArray();
}
public readonly record struct PendingReservation(long RequestId, EncounterKind Kind, int Count, double ExpiresAt);
public readonly record struct PopulationLedgerState(IReadOnlyList<PendingReservation> Pending);
Game
library
using SboxDirector;
namespace AdaptiveDirectorExamples;
/// <summary>
/// Centralizes the conversion from game events to the Director's five pressure
/// severities. Call this service only from authoritative gameplay code.
/// </summary>
public sealed class PressureReportingExample
{
readonly Action<PressureEvent, double> _report;
public PressureReportingExample(Action<PressureEvent, double> report)
{
_report = report ?? throw new ArgumentNullException(nameof(report));
}
public static PressureReportingExample ForDirector(DirectorCore director) => new(director.ApplyPressure);
public static PressureReportingExample ForHostRunner(DirectorHostRunner runner) => new(runner.ReportPressure);
/// <summary>normalizedDamage is damage divided by the participant's full health.</summary>
public void OnDamage(string participantId, float normalizedDamage, double gameTime)
{
if (!float.IsFinite(normalizedDamage) || normalizedDamage < 0) throw new ArgumentOutOfRangeException(nameof(normalizedDamage));
var severity = normalizedDamage switch
{
< .20f => PressureSeverity.Moderate,
< .50f => PressureSeverity.Major,
_ => PressureSeverity.Critical
};
Report(participantId, severity, "damage", gameTime);
}
public void OnLedgeDanger(string participantId, double gameTime) => Report(participantId, PressureSeverity.Major, "ledge_danger", gameTime);
public void OnIncapacitated(string participantId, int previousReviveCount, double gameTime)
{
if (previousReviveCount < 0) throw new ArgumentOutOfRangeException(nameof(previousReviveCount));
Report(participantId, previousReviveCount == 0 ? PressureSeverity.Maximum : PressureSeverity.Critical, "incapacitated", gameTime);
}
/// <summary>Compatibility behavior: only cooperative Easy/Normal revival reaches maximum pressure.</summary>
public void OnDefibrillatorRevived(string participantId, string baseMode, string difficulty, double gameTime)
{
var compatible = string.Equals(baseMode, "coop", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(difficulty, "easy", StringComparison.OrdinalIgnoreCase)
|| string.Equals(difficulty, "normal", StringComparison.OrdinalIgnoreCase));
if (compatible) Report(participantId, PressureSeverity.Maximum, "defibrillator_revival", gameTime);
}
/// <summary>Maps the recovered 150/500-unit proximity bands. Set typedThreat for special/nonzero threat types.</summary>
public void OnThreatKilledNearby(string participantId, float distance, bool bossOrDormantHazard, bool typedThreat, float ordinaryThreatSpeed, double gameTime)
{
if (!float.IsFinite(distance) || distance < 0 || !float.IsFinite(ordinaryThreatSpeed) || ordinaryThreatSpeed < 0) throw new ArgumentOutOfRangeException(nameof(distance));
if (distance > 500) return;
PressureSeverity severity;
if (bossOrDormantHazard) severity = distance <= 150 ? PressureSeverity.Critical : PressureSeverity.Major;
else if (typedThreat) severity = distance <= 150 ? PressureSeverity.Moderate : PressureSeverity.Minor;
else severity = distance <= 150 && ordinaryThreatSpeed > 150 ? PressureSeverity.Moderate : PressureSeverity.Minor;
Report(participantId, severity, "nearby_threat_death", gameTime);
}
/// <summary>Use custom mappings for game-specific events while keeping them in one auditable place.</summary>
public void OnCustomDanger(string participantId, PressureSeverity severity, string reason, double gameTime) => Report(participantId, severity, reason, gameTime);
void Report(string participantId, PressureSeverity severity, string reason, double gameTime)
{
if (string.IsNullOrWhiteSpace(participantId)) throw new ArgumentException("A participant id is required.", nameof(participantId));
if (!double.IsFinite(gameTime)) throw new ArgumentOutOfRangeException(nameof(gameTime));
_report(new PressureEvent(participantId, severity, reason), gameTime);
}
}
/// <summary>Example diagnostic wiring for a developer HUD or server log.</summary>
public sealed class DirectorDiagnosticsExample
{
readonly DirectorTelemetryBuffer _telemetry;
public DirectorDiagnosticsExample(DirectorCore director) { _telemetry = new DirectorTelemetryBuffer(director, capacity: 200); }
public IReadOnlyList<DirectorDiagnostic> ReadForDebugHud() => _telemetry.Snapshot();
public void Clear() => _telemetry.Clear();
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public enum MusicDecisionKind { SetMixerLayer, Play, StopEvent, StopTrack, StopAll, General }
public enum MusicPriority { Low = 1, Medium = 2, High = 3, Critical = 4 }
public enum MusicTriggerKind { Play, StopEvent, StopTrack, StopAll, General, Reset, ScenarioEnded, ParticipantRestored, AmbientMob, MobSpawn, MobSpawnBehind, LargeAreaRevealed, LandmarkRevealed }
public enum MusicRuntimeActionKind { Play, StopEvent, StopTrack, StopAll, SetTrackVolume, SetMixerLayer, General }
public enum MusicDecisionResultKind { Accepted, Deferred, RejectedUnknownEvent, RejectedBlocked, RejectedLifecycle, RejectedPriority, FailedAdapter }
[Flags]
public enum MusicMasterFlags
{
None = 0,
PlayToEnd = 1,
PlaySplit = 2,
DontDisengage = 4,
DontEngage = 8,
AllowAfterDeath = 16,
AllowAfterScenarioEnd = 32
}
public static class MusicMixerLayers
{
public const string MobBeating = "mob_beating";
public const string MobRules = "mob_rules";
public const string HazardRage = "hazard_rage";
public const string Combat = "combat";
public const string CombatSecondary = "combat_secondary";
public const string CombatClose = "combat_close";
public const string Adrenaline = "adrenaline";
public const string Checkpoint = "checkpoint";
public const string Ambient = "ambient";
}
public sealed record SpecialMusicThreat(string ArchetypeId, float Distance, string EmitterId = "", DirectorVector? Position = null);
public sealed record PositionalMusicCandidate(string Id, DirectorVector Position, bool Eligible = true);
public sealed record WanderingMusicHazard(string Id, float Anger, DirectorVector Position);
public sealed class MusicParticipantSnapshot
{
public string Id { get; init; } = "";
public bool IsEligible { get; init; } = true;
public bool IsAlive { get; init; } = true;
public bool IsIncapacitated { get; init; }
public bool DynamicMusicAllowed { get; init; } = true;
public float CumulativeDamage { get; init; }
public float WeaponActivity { get; init; }
public bool InflictedDamage { get; init; }
public int VisibleCommonThreats { get; init; }
public int ActiveCommonThreats { get; init; }
public int ScannedCommonThreats { get; init; }
public float AttackingCommonFactor { get; init; }
public float CloseCommonAction { get; init; }
public float VeryCloseCommonFactor { get; init; }
public bool MobPressureActive { get; init; }
public bool GlobalCombatActive { get; init; }
public bool BossPresent { get; init; }
public string BossArchetypeId { get; init; } = "";
public bool HazardPresent { get; init; }
public bool HazardAttacking { get; init; }
public bool HazardBurning { get; init; }
public float HazardRage { get; init; }
public string HazardEmitterId { get; init; } = "";
public DirectorVector? HazardPosition { get; init; }
public float Adrenaline { get; init; }
public bool InCheckpoint { get; init; }
public IReadOnlyList<SpecialMusicThreat> SpecialThreats { get; init; } = Array.Empty<SpecialMusicThreat>();
public IReadOnlyList<PositionalMusicCandidate> PositionalCandidates { get; init; } = Array.Empty<PositionalMusicCandidate>();
public IReadOnlyList<WanderingMusicHazard> WanderingHazards { get; init; } = Array.Empty<WanderingMusicHazard>();
public void Validate()
{
if (string.IsNullOrWhiteSpace(Id)) throw new ArgumentException("A music participant id is required.");
var values = new[] { CumulativeDamage, WeaponActivity, AttackingCommonFactor, CloseCommonAction, VeryCloseCommonFactor, HazardRage, Adrenaline };
if (values.Any(x => !float.IsFinite(x)) || CumulativeDamage < 0 || VisibleCommonThreats < 0 || ActiveCommonThreats < 0 || ScannedCommonThreats < 0) throw new ArgumentException("Music participant measurements must be finite and non-negative.");
if (WeaponActivity is < 0 or > 1 || AttackingCommonFactor is < 0 or > 1 || CloseCommonAction is < 0 or > 1 || VeryCloseCommonFactor is < 0 or > 1 || HazardRage is < 0 or > 1 || Adrenaline is < 0 or > 1) throw new ArgumentException("Normalized music inputs must be in [0,1].");
if (SpecialThreats.Any(x => string.IsNullOrWhiteSpace(x.ArchetypeId) || !float.IsFinite(x.Distance) || x.Distance < 0)) throw new ArgumentException("Special music threats require an archetype and finite non-negative distance.");
if (PositionalCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id))) throw new ArgumentException("Positional music candidates require stable ids.");
if (WanderingHazards.Any(x => string.IsNullOrWhiteSpace(x.Id) || !float.IsFinite(x.Anger) || x.Anger is < 0 or > 1)) throw new ArgumentException("Wandering music hazards require stable ids and anger in [0,1].");
}
}
public sealed class MusicWorldSnapshot
{
public double Time { get; init; }
public IReadOnlyList<MusicParticipantSnapshot> Participants { get; init; } = Array.Empty<MusicParticipantSnapshot>();
public bool ScenarioEnded { get; init; }
public void Validate()
{
if (!double.IsFinite(Time)) throw new ArgumentException("Music world time must be finite.");
foreach (var participant in Participants) participant.Validate();
if (Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException("Music participant ids must be unique.");
}
}
public sealed record MusicGameplayEvent(long Sequence, MusicTriggerKind Kind, string ParticipantId = "", string TargetId = "", float FadeSeconds = 0, string EmitterId = "", DirectorVector? Position = null)
{
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
}
public sealed record MusicDecision(long DecisionId, MusicDecisionKind Kind, string ParticipantId, string TargetId, string Reason)
{
public string LayerId { get; init; } = "";
public float Value { get; init; }
public float FadeSeconds { get; init; }
public float StartOffsetSeconds { get; init; }
public string EmitterId { get; init; } = "";
public DirectorVector? Position { get; init; }
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
}
public sealed class MusicDecisionBatch
{
public double Time { get; init; }
public List<MusicDecision> Decisions { get; } = new();
}
public sealed record MusicTimingTag(int Index, double OffsetSeconds);
public sealed class MusicEventDefinition
{
public string EventId { get; init; } = "";
public string TrackId { get; init; } = "main";
public MusicPriority Priority { get; init; } = MusicPriority.Low;
public MusicMasterFlags Flags { get; init; }
public float DefaultFadeOutSeconds { get; init; }
public double DelaySeconds { get; init; }
public string? AutoQueueEventId { get; init; }
public IReadOnlyList<string> BlockTrackList { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> DuckTrackList { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> StopTrackList { get; init; } = Array.Empty<string>();
public IReadOnlyList<MusicTimingTag> TimingTags { get; init; } = Array.Empty<MusicTimingTag>();
public double LoopSeconds { get; init; }
public string? SyncTrackId { get; init; }
public int? SyncTagIndex { get; init; }
public double SyncTagDelaySeconds { get; init; }
public double SyncTagDelayMultiplier { get; init; } = 1;
public void Validate()
{
if (string.IsNullOrWhiteSpace(EventId) || string.IsNullOrWhiteSpace(TrackId)) throw new ArgumentException("Music events require event and track ids.");
if (!float.IsFinite(DefaultFadeOutSeconds) || DefaultFadeOutSeconds < 0 || !double.IsFinite(DelaySeconds) || DelaySeconds < 0 || !double.IsFinite(LoopSeconds) || LoopSeconds < 0 || !double.IsFinite(SyncTagDelaySeconds) || !double.IsFinite(SyncTagDelayMultiplier) || SyncTagDelayMultiplier < 0) throw new ArgumentException("Music timing values must be finite and non-negative where applicable.");
if (TimingTags.Any(x => x.Index < 0 || !double.IsFinite(x.OffsetSeconds) || x.OffsetSeconds < 0) || TimingTags.Select(x => x.Index).Distinct().Count() != TimingTags.Count) throw new ArgumentException("Music timing tags require unique non-negative indices and offsets.");
if (BlockTrackList.Concat(DuckTrackList).Concat(StopTrackList).Any(string.IsNullOrWhiteSpace)) throw new ArgumentException("Music track lists cannot contain empty ids.");
}
}
public interface IMusicEventCatalog
{
bool TryGet(string eventId, out MusicEventDefinition definition);
}
public sealed class MusicEventCatalog : IMusicEventCatalog
{
readonly Dictionary<string, MusicEventDefinition> _events;
public MusicEventCatalog(IEnumerable<MusicEventDefinition> events)
{
_events = new(StringComparer.Ordinal);
foreach (var definition in events) { definition.Validate(); if (!_events.TryAdd(definition.EventId, definition)) throw new ArgumentException($"Duplicate music event '{definition.EventId}'."); }
foreach (var definition in _events.Values) if (definition.AutoQueueEventId is { Length: > 0 } next && !_events.ContainsKey(next)) throw new ArgumentException($"Music event '{definition.EventId}' queues unknown event '{next}'.");
}
public bool TryGet(string eventId, out MusicEventDefinition definition) => _events.TryGetValue(eventId, out definition!);
}
public sealed record MusicRuntimeAction(long ActionId, MusicRuntimeActionKind Kind, string TargetId)
{
public string EventId { get; init; } = "";
public string TrackId { get; init; } = "";
public string LayerId { get; init; } = "";
public float Value { get; init; }
public float FadeSeconds { get; init; }
public float StartOffsetSeconds { get; init; }
public string EmitterId { get; init; } = "";
public DirectorVector? Position { get; init; }
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
}
public sealed record MusicDecisionResult(long DecisionId, MusicDecisionResultKind Result, string Detail = "", long InstanceId = 0);
public sealed class MusicRuntimeBatch
{
public double Time { get; init; }
public List<MusicRuntimeAction> Actions { get; } = new();
public List<MusicDecisionResult> Results { get; } = new();
}
Game
library
namespace SboxDirector;
public sealed class WaveSequenceConfiguration
{
public int WaveCount { get; init; } = 1;
public double InitialDelayMinimum { get; init; } = 1;
public double InitialDelayMaximum { get; init; } = 2;
public double CombatTimeout { get; init; } = 10;
public double PauseMinimum { get; init; } = 5;
public double PauseMaximum { get; init; } = 10;
public void Validate()
{
var durations = new[] { InitialDelayMinimum, InitialDelayMaximum, CombatTimeout, PauseMinimum, PauseMaximum };
if (WaveCount <= 0 || durations.Any(x => !double.IsFinite(x)) || InitialDelayMinimum < 0 || InitialDelayMaximum < InitialDelayMinimum || CombatTimeout < 0 || PauseMinimum < 0 || PauseMaximum < PauseMinimum) throw new ArgumentException("Invalid wave sequence configuration.");
}
}
public readonly record struct WaveSequenceState(WaveState State, int IssuedWaves, int TargetWaves, double Deadline);
public readonly record struct WaveAction(bool IssueWave, bool Completed, string Reason);
public sealed class WaveSequenceController
{
readonly WaveSequenceConfiguration _configuration; readonly DeterministicRandom _random;
public WaveState State { get; private set; } = WaveState.Inactive;
public int IssuedWaves { get; private set; }
public int TargetWaves { get; private set; }
public double Deadline { get; private set; }
public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random) { configuration.Validate(); _configuration = configuration; _random = random; }
public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random, WaveSequenceState state) : this(configuration, random) { State = state.State; IssuedWaves = state.IssuedWaves; TargetWaves = state.TargetWaves; Deadline = state.Deadline; if (!double.IsFinite(Deadline) || IssuedWaves < 0 || TargetWaves < 0 || IssuedWaves > TargetWaves) throw new ArgumentException("Wave state is invalid.", nameof(state)); }
public WaveSequenceState Snapshot => new(State, IssuedWaves, TargetWaves, Deadline);
public void Start(double time, int? waveCount = null) { RequireTime(time); TargetWaves = waveCount ?? _configuration.WaveCount; if (TargetWaves <= 0) throw new ArgumentOutOfRangeException(nameof(waveCount)); IssuedWaves = 0; State = WaveState.InitialDelay; Deadline = time + Range(_configuration.InitialDelayMinimum, _configuration.InitialDelayMaximum); }
public void Cancel() { State = WaveState.Inactive; IssuedWaves = 0; TargetWaves = 0; Deadline = 0; }
public WaveAction Update(double time, bool relevantCombatantsRemain)
{
RequireTime(time);
if (State is WaveState.Inactive or WaveState.Done) return new(false, State == WaveState.Done, "inactive or complete");
if (time < Deadline) return new(false, false, "waiting for timer");
if (State == WaveState.InitialDelay) { State = WaveState.IssueWave; return new(false, false, "initial delay expired"); }
if (State == WaveState.IssueWave) { IssuedWaves++; State = WaveState.WaitForClear; Deadline = time + _configuration.CombatTimeout; return new(true, false, "wave issued"); }
if (State == WaveState.WaitForClear)
{
if (relevantCombatantsRemain) { Deadline = time + _configuration.CombatTimeout; return new(false, false, "combat remains"); }
if (IssuedWaves >= TargetWaves) { State = WaveState.Done; return new(false, true, "target waves issued and combat clear"); }
State = WaveState.Pause; Deadline = time + Range(_configuration.PauseMinimum, _configuration.PauseMaximum); return new(false, false, "pausing before next wave");
}
if (State == WaveState.Pause) { State = WaveState.IssueWave; return new(false, false, "pause expired"); }
return new(false, false, "no action");
}
double Range(double minimum, double maximum) => minimum + (maximum - minimum) * _random.NextFloat();
static void RequireTime(double time) { if (!double.IsFinite(time)) throw new ArgumentOutOfRangeException(nameof(time)); }
}
Game
library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Adaptive Director" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "chomnr_adaptive_director" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "notpointless" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "notpointless.chomnr_adaptive_director" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-08-01T14:11:11.8900023Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.119.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.119.0")]
Game
library
#nullable enable annotations
namespace SboxDirector;
public sealed class EncounterRule
{
public string Id { get; init; } = "rule";
public EncounterKind Kind { get; init; }
public int Count { get; init; } = 1;
public int Priority { get; init; }
public double CooldownSeconds { get; init; } = 10;
public float MinimumPressure { get; init; }
public float MaximumPressure { get; init; } = 1f;
public float Probability { get; init; } = 1f;
public IReadOnlySet<TempoPhase> Phases { get; init; } = new HashSet<TempoPhase> { TempoPhase.BuildUp, TempoPhase.SustainPeak };
public IReadOnlySet<string> Modes { get; init; } = new HashSet<string>();
public Func<WorldSnapshot, bool>? AdditionalCondition { get; init; }
public void Validate()
{
if (string.IsNullOrWhiteSpace(Id) || Count <= 0 || !double.IsFinite(CooldownSeconds) || CooldownSeconds < 0 || !float.IsFinite(MinimumPressure) || !float.IsFinite(MaximumPressure) || !float.IsFinite(Probability) || MinimumPressure < 0 || MaximumPressure > 1 || MaximumPressure < MinimumPressure || Probability is < 0 or > 1) throw new ArgumentException($"Invalid encounter rule '{Id}'.");
}
}
public interface IEncounterTimingPolicy
{
double AdjustCooldown(EncounterRule rule, double proposedSeconds, WorldSnapshot world, TempoPhase tempo, float teamPressure);
}
public sealed class DefaultEncounterTimingPolicy : IEncounterTimingPolicy
{
public double AdjustCooldown(EncounterRule rule, double proposedSeconds, WorldSnapshot world, TempoPhase tempo, float teamPressure) => proposedSeconds;
}
public sealed class ModeEncounterTimingPolicy : IEncounterTimingPolicy
{
readonly IReadOnlyDictionary<string, double> _modeMultipliers;
readonly double _highPressureMultiplier;
public ModeEncounterTimingPolicy(IReadOnlyDictionary<string, double>? modeMultipliers = null, double highPressureMultiplier = 1)
{
_modeMultipliers = new Dictionary<string, double>(modeMultipliers ?? new Dictionary<string, double>(), StringComparer.Ordinal); if (_modeMultipliers.Any(x => string.IsNullOrWhiteSpace(x.Key) || !double.IsFinite(x.Value) || x.Value < 0) || !double.IsFinite(highPressureMultiplier) || highPressureMultiplier < 0) throw new ArgumentException("Cooldown multipliers must be finite and non-negative."); _highPressureMultiplier = highPressureMultiplier;
}
public double AdjustCooldown(EncounterRule rule, double proposedSeconds, WorldSnapshot world, TempoPhase tempo, float teamPressure)
{
var mode = _modeMultipliers.TryGetValue(world.ModeId, out var multiplier) ? multiplier : 1; var pressure = 1 + (_highPressureMultiplier - 1) * Math.Clamp(teamPressure, 0, 1); return proposedSeconds * mode * pressure;
}
}
public sealed class RuleBasedEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback
{
readonly IReadOnlyList<EncounterRule> _rules;
readonly IEncounterTimingPolicy _timing;
readonly Dictionary<string, double> _nextTimes = new();
EncounterRule? _selected;
double _selectedPreviousTime;
public RuleBasedEncounterSchedule(IEnumerable<EncounterRule> rules, IEncounterTimingPolicy? timing = null)
{
_rules = rules.OrderByDescending(x => x.Priority).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();
if (_rules.Count == 0) throw new ArgumentException("At least one encounter rule is required.", nameof(rules));
foreach (var rule in _rules) rule.Validate();
if (_rules.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != _rules.Count) throw new ArgumentException("Encounter rule ids must be unique.");
_timing = timing ?? new DefaultEncounterTimingPolicy();
}
public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float pressure, PopulationLedger population, DeterministicRandom random)
{
_selected = null;
foreach (var rule in _rules)
{
if (_nextTimes.TryGetValue(rule.Id, out var next) && world.Time < next) continue;
if (!rule.Phases.Contains(tempo) || pressure < rule.MinimumPressure || pressure > rule.MaximumPressure) continue;
if (rule.Modes.Count > 0 && !rule.Modes.Contains(world.ModeId)) continue;
if (rule.AdditionalCondition is not null && !rule.AdditionalCondition(world)) continue;
if (population.Available(rule.Kind, world.Population) <= 0) continue;
var cooldown = _timing.AdjustCooldown(rule, rule.CooldownSeconds, world, tempo, pressure); if (!double.IsFinite(cooldown) || cooldown < 0) throw new InvalidOperationException("Encounter timing policy returned an invalid cooldown.");
if (random.NextFloat() > rule.Probability) { _nextTimes[rule.Id] = world.Time + cooldown; continue; }
_selected = rule; _selectedPreviousTime = _nextTimes.TryGetValue(rule.Id, out var previous) ? previous : double.NegativeInfinity; _nextTimes[rule.Id] = world.Time + cooldown; return rule.Kind;
}
return null;
}
public int RequestedCount(EncounterKind kind, WorldSnapshot world) => _selected?.Kind == kind ? _selected.Count : 1;
public IReadOnlyDictionary<string, string> CaptureScheduleState() => _nextTimes.ToDictionary(x => x.Key, x => x.Value.ToString("R", CultureInfo.InvariantCulture));
public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)
{
_nextTimes.Clear(); foreach (var pair in state) if (double.TryParse(pair.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) _nextTimes[pair.Key] = value;
}
public void SelectionFinalized(bool requestCreated) { if (_selected is not null && !requestCreated) { if (double.IsNegativeInfinity(_selectedPreviousTime)) _nextTimes.Remove(_selected.Id); else _nextTimes[_selected.Id] = _selectedPreviousTime; } _selected = null; }
}
public sealed class MilestoneEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback
{
readonly string _id; readonly EncounterKind _kind; readonly float _minimumProgress; readonly float _maximumProgress; readonly int _count;
readonly HashSet<string> _completedScopes = new(); string? _selectedScope;
public MilestoneEncounterSchedule(string id, EncounterKind kind, float minimumProgress, float maximumProgress = 1f, int count = 1)
{
if (string.IsNullOrWhiteSpace(id) || minimumProgress < 0 || maximumProgress < minimumProgress || count <= 0) throw new ArgumentException("Invalid milestone schedule.");
_id = id; _kind = kind; _minimumProgress = minimumProgress; _maximumProgress = maximumProgress; _count = count;
}
public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)
{
var scope = string.IsNullOrWhiteSpace(world.RoundId) ? world.ModeId : world.ModeId + ":" + world.RoundId;
if (_completedScopes.Contains(scope) || world.TeamProgress < _minimumProgress || world.TeamProgress > _maximumProgress || population.Available(_kind, world.Population) <= 0) return null;
_selectedScope = scope; return _kind;
}
public int RequestedCount(EncounterKind kind, WorldSnapshot world) => kind == _kind && _selectedScope is not null ? _count : 1;
public IReadOnlyDictionary<string, string> CaptureScheduleState() => new Dictionary<string, string> { ["id"] = _id, ["completed"] = string.Join("|", _completedScopes.OrderBy(x => x, StringComparer.Ordinal)) };
public void RestoreScheduleState(IReadOnlyDictionary<string, string> state) { _completedScopes.Clear(); if (state.TryGetValue("completed", out var value)) foreach (var scope in value.Split('|', StringSplitOptions.RemoveEmptyEntries)) _completedScopes.Add(scope); }
public void SelectionFinalized(bool requestCreated) { if (requestCreated && _selectedScope is not null) _completedScopes.Add(_selectedScope); _selectedScope = null; }
}
public sealed class CompositeEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback
{
readonly IReadOnlyList<IEncounterSchedule> _schedules; IEncounterSchedule? _selected;
public CompositeEncounterSchedule(params IEncounterSchedule[] schedules) { _schedules = schedules; if (schedules.Length == 0) throw new ArgumentException("At least one schedule is required."); }
public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)
{
_selected = null; foreach (var schedule in _schedules) { var kind = schedule.SelectEncounter(world, tempo, teamPressure, population, random); if (kind is not null) { _selected = schedule; return kind; } }
return null;
}
public int RequestedCount(EncounterKind kind, WorldSnapshot world) => _selected?.RequestedCount(kind, world) ?? 1;
public IReadOnlyDictionary<string, string> CaptureScheduleState()
{
var output = new Dictionary<string, string>(); for (var i = 0; i < _schedules.Count; i++) if (_schedules[i] is IPersistentEncounterSchedule persistent) foreach (var pair in persistent.CaptureScheduleState()) output[$"{i}:{pair.Key}"] = pair.Value; return output;
}
public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)
{
for (var i = 0; i < _schedules.Count; i++) if (_schedules[i] is IPersistentEncounterSchedule persistent) persistent.RestoreScheduleState(state.Where(x => x.Key.StartsWith(i + ":", StringComparison.Ordinal)).ToDictionary(x => x.Key[(x.Key.IndexOf(':') + 1)..], x => x.Value));
}
public void SelectionFinalized(bool requestCreated) { if (_selected is IEncounterScheduleFeedback feedback) feedback.SelectionFinalized(requestCreated); _selected = null; }
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public enum TempoPhase { BuildUp, SustainPeak, PeakFade, Relax }
public enum PressureSeverity { Minor = 1, Moderate = 2, Major = 3, Critical = 4, Maximum = 5 }
public enum EncounterKind { Ambient, CommonWave, Special, Boss, DormantHazard, Objective, Custom }
public enum RequestResultKind { Succeeded, PartiallySucceeded, Rejected, Deferred, Failed }
public enum WaveState { Inactive, InitialDelay, IssueWave, WaitForClear, Pause, Done }
public enum ScenarioStatus { Inactive, Running, Complete, Failed }
public enum AdvanceRelation { Unknown, Ahead, Behind, Lateral, ObjectiveAdjacent }
public enum PlacementFallbackMode { None, IgnoreRouteConstraints, AnyReachableHidden }
public enum PlacementSelectionMode { WeightedScore, FirstEligible, HighestProgressFirstTie, NativeRandomTranspositionFirst }
public readonly record struct DirectorVector(float X, float Y, float Z)
{
public static float DistanceSquared(DirectorVector a, DirectorVector b)
{
var x = a.X - b.X; var y = a.Y - b.Y; var z = a.Z - b.Z;
return x * x + y * y + z * z;
}
}
public sealed class ParticipantSnapshot
{
public string Id { get; init; } = "";
public bool IsAlive { get; init; } = true;
public bool IsEligible { get; init; } = true;
public bool IsInCombat { get; init; }
public DirectorVector Position { get; init; }
public float Progress { get; init; }
}
public sealed class PopulationSnapshot
{
readonly Dictionary<EncounterKind, int> _counts = new();
public PopulationSnapshot() { }
public PopulationSnapshot(IReadOnlyDictionary<EncounterKind, int> counts) { foreach (var pair in counts) _counts[pair.Key] = Math.Max(0, pair.Value); }
public int this[EncounterKind kind] => _counts.TryGetValue(kind, out var value) ? value : 0;
public IReadOnlyDictionary<EncounterKind, int> Counts => _counts;
}
public sealed class SpawnCandidate
{
public string Id { get; init; } = "";
public DirectorVector Position { get; init; }
public bool Reachable { get; init; }
public bool Spawnable { get; init; }
public bool Visible { get; init; }
public float NearestParticipantDistance { get; init; }
public float Progress { get; init; }
public float AreaCapacity { get; init; } = 1f;
public float ThreatSeparation { get; init; }
public float RelativeProgress { get; init; }
public float PathDistance { get; init; }
public float IngressDistance { get; init; }
public float ClearRadius { get; init; }
public float AreaWidth { get; init; }
public float AreaHeight { get; init; }
public AdvanceRelation AdvanceRelation { get; init; }
public IReadOnlySet<string> Tags { get; init; } = new HashSet<string>();
}
public sealed class WorldSnapshot
{
public double Time { get; init; }
public IReadOnlyList<ParticipantSnapshot> Participants { get; init; } = Array.Empty<ParticipantSnapshot>();
public PopulationSnapshot Population { get; init; } = new();
public IReadOnlyList<SpawnCandidate> SpawnCandidates { get; init; } = Array.Empty<SpawnCandidate>();
public bool CombatActive { get; init; }
public float TeamProgress { get; init; }
public string ModeId { get; init; } = "continuous";
public string? RoundId { get; init; }
public ResourceWorldSnapshot Resources { get; init; } = new();
public void Validate()
{
if (double.IsNaN(Time) || double.IsInfinity(Time)) throw new ArgumentException("World time must be finite.");
if (float.IsNaN(TeamProgress) || float.IsInfinity(TeamProgress)) throw new ArgumentException("Team progress must be finite.");
if (Participants.Any(x => string.IsNullOrWhiteSpace(x.Id)) || Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException("Participant ids must be non-empty and unique.");
if (SpawnCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id)) || SpawnCandidates.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != SpawnCandidates.Count) throw new ArgumentException("Spawn candidate ids must be non-empty and unique.");
if (SpawnCandidates.Any(x => !float.IsFinite(x.NearestParticipantDistance) || !float.IsFinite(x.Progress) || !float.IsFinite(x.RelativeProgress) || !float.IsFinite(x.AreaCapacity) || !float.IsFinite(x.ThreatSeparation) || !float.IsFinite(x.PathDistance) || !float.IsFinite(x.IngressDistance) || !float.IsFinite(x.ClearRadius) || !float.IsFinite(x.AreaWidth) || !float.IsFinite(x.AreaHeight) || x.NearestParticipantDistance < 0 || x.AreaCapacity < 0 || x.ThreatSeparation < 0 || x.PathDistance < 0 || x.IngressDistance < 0 || x.ClearRadius < 0 || x.AreaWidth < 0 || x.AreaHeight < 0)) throw new ArgumentException("Spawn candidate distances, dimensions, and capacity must be finite and non-negative.");
Resources.Validate();
}
}
public sealed record EncounterMember(string Archetype, int Count);
public sealed record SpawnRequest(
long RequestId,
EncounterKind Kind,
int RequestedCount,
string CandidateId,
string Archetype,
string Reason)
{
public IReadOnlyList<EncounterMember> Composition { get; init; } = Array.Empty<EncounterMember>();
public int Attempt { get; init; } = 1;
public bool Equals(SpawnRequest? other) => other is not null && RequestId == other.RequestId && Kind == other.Kind && RequestedCount == other.RequestedCount && CandidateId == other.CandidateId && Archetype == other.Archetype && Reason == other.Reason && Attempt == other.Attempt && Composition.SequenceEqual(other.Composition);
public override int GetHashCode()
{
var hash = HashCode.Combine(RequestId, Kind, RequestedCount, CandidateId, Archetype, Reason, Attempt); foreach (var member in Composition) hash = HashCode.Combine(hash, member); return hash;
}
}
public sealed record SpawnRequestResult(
long RequestId,
RequestResultKind Result,
int SpawnedCount,
string? Detail = null);
public sealed record DirectorEvent(string Name, string Reason, double Time);
public sealed class DirectorDecisionBatch
{
public double Time { get; init; }
public TempoPhase Tempo { get; init; }
public float TeamPressure { get; init; }
public float MusicIntensity { get; init; }
public List<SpawnRequest> SpawnRequests { get; } = new();
public List<ResourceSpawnRequest> ResourceRequests { get; } = new();
public List<DirectorEvent> Events { get; } = new();
}
public sealed record PressureEvent(string ParticipantId, PressureSeverity Severity, string Reason);
public sealed record DirectorDiagnostic(double Time, string Category, string Message);
Game
library
#nullable enable annotations
namespace SboxDirector;
public enum MusicDecisionKind { SetMixerLayer, Play, StopEvent, StopTrack, StopAll, General }
public enum MusicPriority { Low = 1, Medium = 2, High = 3, Critical = 4 }
public enum MusicTriggerKind { Play, StopEvent, StopTrack, StopAll, General, Reset, ScenarioEnded, ParticipantRestored, AmbientMob, MobSpawn, MobSpawnBehind, LargeAreaRevealed, LandmarkRevealed }
public enum MusicRuntimeActionKind { Play, StopEvent, StopTrack, StopAll, SetTrackVolume, SetMixerLayer, General }
public enum MusicDecisionResultKind { Accepted, Deferred, RejectedUnknownEvent, RejectedBlocked, RejectedLifecycle, RejectedPriority, FailedAdapter }
[Flags]
public enum MusicMasterFlags
{
None = 0,
PlayToEnd = 1,
PlaySplit = 2,
DontDisengage = 4,
DontEngage = 8,
AllowAfterDeath = 16,
AllowAfterScenarioEnd = 32
}
public static class MusicMixerLayers
{
public const string MobBeating = "mob_beating";
public const string MobRules = "mob_rules";
public const string HazardRage = "hazard_rage";
public const string Combat = "combat";
public const string CombatSecondary = "combat_secondary";
public const string CombatClose = "combat_close";
public const string Adrenaline = "adrenaline";
public const string Checkpoint = "checkpoint";
public const string Ambient = "ambient";
}
public sealed record SpecialMusicThreat(string ArchetypeId, float Distance, string EmitterId = "", DirectorVector? Position = null);
public sealed record PositionalMusicCandidate(string Id, DirectorVector Position, bool Eligible = true);
public sealed record WanderingMusicHazard(string Id, float Anger, DirectorVector Position);
public sealed class MusicParticipantSnapshot
{
public string Id { get; init; } = "";
public bool IsEligible { get; init; } = true;
public bool IsAlive { get; init; } = true;
public bool IsIncapacitated { get; init; }
public bool DynamicMusicAllowed { get; init; } = true;
public float CumulativeDamage { get; init; }
public float WeaponActivity { get; init; }
public bool InflictedDamage { get; init; }
public int VisibleCommonThreats { get; init; }
public int ActiveCommonThreats { get; init; }
public int ScannedCommonThreats { get; init; }
public float AttackingCommonFactor { get; init; }
public float CloseCommonAction { get; init; }
public float VeryCloseCommonFactor { get; init; }
public bool MobPressureActive { get; init; }
public bool GlobalCombatActive { get; init; }
public bool BossPresent { get; init; }
public string BossArchetypeId { get; init; } = "";
public bool HazardPresent { get; init; }
public bool HazardAttacking { get; init; }
public bool HazardBurning { get; init; }
public float HazardRage { get; init; }
public string HazardEmitterId { get; init; } = "";
public DirectorVector? HazardPosition { get; init; }
public float Adrenaline { get; init; }
public bool InCheckpoint { get; init; }
public IReadOnlyList<SpecialMusicThreat> SpecialThreats { get; init; } = Array.Empty<SpecialMusicThreat>();
public IReadOnlyList<PositionalMusicCandidate> PositionalCandidates { get; init; } = Array.Empty<PositionalMusicCandidate>();
public IReadOnlyList<WanderingMusicHazard> WanderingHazards { get; init; } = Array.Empty<WanderingMusicHazard>();
public void Validate()
{
if (string.IsNullOrWhiteSpace(Id)) throw new ArgumentException("A music participant id is required.");
var values = new[] { CumulativeDamage, WeaponActivity, AttackingCommonFactor, CloseCommonAction, VeryCloseCommonFactor, HazardRage, Adrenaline };
if (values.Any(x => !float.IsFinite(x)) || CumulativeDamage < 0 || VisibleCommonThreats < 0 || ActiveCommonThreats < 0 || ScannedCommonThreats < 0) throw new ArgumentException("Music participant measurements must be finite and non-negative.");
if (WeaponActivity is < 0 or > 1 || AttackingCommonFactor is < 0 or > 1 || CloseCommonAction is < 0 or > 1 || VeryCloseCommonFactor is < 0 or > 1 || HazardRage is < 0 or > 1 || Adrenaline is < 0 or > 1) throw new ArgumentException("Normalized music inputs must be in [0,1].");
if (SpecialThreats.Any(x => string.IsNullOrWhiteSpace(x.ArchetypeId) || !float.IsFinite(x.Distance) || x.Distance < 0)) throw new ArgumentException("Special music threats require an archetype and finite non-negative distance.");
if (PositionalCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id))) throw new ArgumentException("Positional music candidates require stable ids.");
if (WanderingHazards.Any(x => string.IsNullOrWhiteSpace(x.Id) || !float.IsFinite(x.Anger) || x.Anger is < 0 or > 1)) throw new ArgumentException("Wandering music hazards require stable ids and anger in [0,1].");
}
}
public sealed class MusicWorldSnapshot
{
public double Time { get; init; }
public IReadOnlyList<MusicParticipantSnapshot> Participants { get; init; } = Array.Empty<MusicParticipantSnapshot>();
public bool ScenarioEnded { get; init; }
public void Validate()
{
if (!double.IsFinite(Time)) throw new ArgumentException("Music world time must be finite.");
foreach (var participant in Participants) participant.Validate();
if (Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException("Music participant ids must be unique.");
}
}
public sealed record MusicGameplayEvent(long Sequence, MusicTriggerKind Kind, string ParticipantId = "", string TargetId = "", float FadeSeconds = 0, string EmitterId = "", DirectorVector? Position = null)
{
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
}
public sealed record MusicDecision(long DecisionId, MusicDecisionKind Kind, string ParticipantId, string TargetId, string Reason)
{
public string LayerId { get; init; } = "";
public float Value { get; init; }
public float FadeSeconds { get; init; }
public float StartOffsetSeconds { get; init; }
public string EmitterId { get; init; } = "";
public DirectorVector? Position { get; init; }
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
}
public sealed class MusicDecisionBatch
{
public double Time { get; init; }
public List<MusicDecision> Decisions { get; } = new();
}
public sealed record MusicTimingTag(int Index, double OffsetSeconds);
public sealed class MusicEventDefinition
{
public string EventId { get; init; } = "";
public string TrackId { get; init; } = "main";
public MusicPriority Priority { get; init; } = MusicPriority.Low;
public MusicMasterFlags Flags { get; init; }
public float DefaultFadeOutSeconds { get; init; }
public double DelaySeconds { get; init; }
public string? AutoQueueEventId { get; init; }
public IReadOnlyList<string> BlockTrackList { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> DuckTrackList { get; init; } = Array.Empty<string>();
public IReadOnlyList<string> StopTrackList { get; init; } = Array.Empty<string>();
public IReadOnlyList<MusicTimingTag> TimingTags { get; init; } = Array.Empty<MusicTimingTag>();
public double LoopSeconds { get; init; }
public string? SyncTrackId { get; init; }
public int? SyncTagIndex { get; init; }
public double SyncTagDelaySeconds { get; init; }
public double SyncTagDelayMultiplier { get; init; } = 1;
public void Validate()
{
if (string.IsNullOrWhiteSpace(EventId) || string.IsNullOrWhiteSpace(TrackId)) throw new ArgumentException("Music events require event and track ids.");
if (!float.IsFinite(DefaultFadeOutSeconds) || DefaultFadeOutSeconds < 0 || !double.IsFinite(DelaySeconds) || DelaySeconds < 0 || !double.IsFinite(LoopSeconds) || LoopSeconds < 0 || !double.IsFinite(SyncTagDelaySeconds) || !double.IsFinite(SyncTagDelayMultiplier) || SyncTagDelayMultiplier < 0) throw new ArgumentException("Music timing values must be finite and non-negative where applicable.");
if (TimingTags.Any(x => x.Index < 0 || !double.IsFinite(x.OffsetSeconds) || x.OffsetSeconds < 0) || TimingTags.Select(x => x.Index).Distinct().Count() != TimingTags.Count) throw new ArgumentException("Music timing tags require unique non-negative indices and offsets.");
if (BlockTrackList.Concat(DuckTrackList).Concat(StopTrackList).Any(string.IsNullOrWhiteSpace)) throw new ArgumentException("Music track lists cannot contain empty ids.");
}
}
public interface IMusicEventCatalog
{
bool TryGet(string eventId, out MusicEventDefinition definition);
}
public sealed class MusicEventCatalog : IMusicEventCatalog
{
readonly Dictionary<string, MusicEventDefinition> _events;
public MusicEventCatalog(IEnumerable<MusicEventDefinition> events)
{
_events = new(StringComparer.Ordinal);
foreach (var definition in events) { definition.Validate(); if (!_events.TryAdd(definition.EventId, definition)) throw new ArgumentException($"Duplicate music event '{definition.EventId}'."); }
foreach (var definition in _events.Values) if (definition.AutoQueueEventId is { Length: > 0 } next && !_events.ContainsKey(next)) throw new ArgumentException($"Music event '{definition.EventId}' queues unknown event '{next}'.");
}
public bool TryGet(string eventId, out MusicEventDefinition definition) => _events.TryGetValue(eventId, out definition!);
}
public sealed record MusicRuntimeAction(long ActionId, MusicRuntimeActionKind Kind, string TargetId)
{
public string EventId { get; init; } = "";
public string TrackId { get; init; } = "";
public string LayerId { get; init; } = "";
public float Value { get; init; }
public float FadeSeconds { get; init; }
public float StartOffsetSeconds { get; init; }
public string EmitterId { get; init; } = "";
public DirectorVector? Position { get; init; }
public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();
}
public sealed record MusicDecisionResult(long DecisionId, MusicDecisionResultKind Result, string Detail = "", long InstanceId = 0);
public sealed class MusicRuntimeBatch
{
public double Time { get; init; }
public List<MusicRuntimeAction> Actions { get; } = new();
public List<MusicDecisionResult> Results { get; } = new();
}
Game
library
using SboxDirector;
static class Tests
{
static int _count;
static void Check(bool value, string name) { _count++; if (!value) throw new Exception($"FAILED: {name}"); }
static void Near(float actual, float expected, float epsilon, string name) => Check(MathF.Abs(actual - expected) <= epsilon, $"{name}: expected {expected}, got {actual}");
public static void Main()
{
Pressure(); TeamMaximum(); Tempo(); Placement(); AdvancedPlacement(); RecoveredCompatibility(); Population(); Wave(); Scenarios(); Core(); Persistence(); PersistentPolicies(); RuleSchedules(); ConcurrentSchedules(); CompositionAndRetry(); Resources(); Orchestration(); AdaptiveCallbacks(); HostRunner(); OwnershipAndResources(); MusicAuthority(); MusicRuntimeArbitration(); MusicPersistenceAndHost(); Validation(); LongDeterminism(); AdvancedDeterminism();
Console.WriteLine($"PASS: {_count} adaptive-director assertions");
}
static void Pressure()
{
var p = new PressureTracker(new PressureConfiguration(), 0);
p.Add(PressureSeverity.Major, 0); Near(p.Instantaneous, .125f, .00001f, "major coefficient");
p.Update(5); Near(p.Instantaneous, .125f, .00001f, "hold");
p.Update(8); Near(p.Instantaneous, .025f, .0001f, "linear decay");
p.Add(PressureSeverity.Maximum, 8); Near(p.Instantaneous, 1f, .00001f, "maximum saturates");
var q = new PressureTracker(new PressureConfiguration(), 0); q.Add(PressureSeverity.Critical, 0); q.Update(1f / 15f);
Near(q.Averaged, MathF.Pow(.25f, (1f / 15f) / 20f), .0001f, "recovered averaged formula");
}
static void TeamMaximum()
{
var team = new TeamPressure(new PressureConfiguration()); team.Apply(new("a", PressureSeverity.Critical, "test"), 0); team.Apply(new("b", PressureSeverity.Minor, "test"), 0);
var world = new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = "a" }, new ParticipantSnapshot { Id = "b" } } };
Near(team.UpdateAndGetMaximum(world), .25f, .0001f, "team uses max");
}
static void Tempo()
{
var c = new TempoController(new TempoConfiguration { RelaxMinimumSeconds = 1, RelaxMaximumSeconds = 2, RelaxMaximumProgressTravel = .1f });
c.Update(new WorldSnapshot { Time = 1, TeamProgress = .2f }, 0); Check(c.Phase == TempoPhase.BuildUp, "relax to buildup");
c.Update(new WorldSnapshot { Time = 20 }, 1); Check(c.Phase == TempoPhase.SustainPeak, "buildup to peak");
var absoluteClock = new DirectorCore(new DirectorConfiguration { Tempo = new TempoConfiguration { RelaxMinimumSeconds = 20, RelaxMaximumSeconds = 45 } }); Check(absoluteClock.Tick(World(10000)).Tempo == TempoPhase.Relax, "first tick aligns absolute clock");
}
static void Placement()
{
var candidates = new[] { new SpawnCandidate { Id = "visible", Reachable = true, Spawnable = true, Visible = true, NearestParticipantDistance = 1000, ThreatSeparation = 1000 }, new SpawnCandidate { Id = "ok", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 1000 } };
var selected = new PlacementSelector().Select(candidates, new PlacementProfile(), new DeterministicRandom(1)); Check(selected?.Candidate.Id == "ok", "placement filters visibility");
var vetoed = new PlacementSelector().Select(candidates, new PlacementProfile(), new DeterministicRandom(1), new WorldSnapshot(), new RejectAllPlacement()); Check(vetoed is null, "custom placement veto");
var tagged = new SpawnCandidate { Id = "tagged", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 1000, Tags = new HashSet<string> { "outdoor" } }; Check(new PlacementSelector().Select(new[] { tagged }, new PlacementProfile { RequiredTags = new HashSet<string> { "indoor" } }, new DeterministicRandom(1)) is null, "required placement tags");
}
static void AdvancedPlacement()
{
var strict = new SpawnCandidate { Id = "strict", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 2000, PathDistance = 800, IngressDistance = 300, ClearRadius = 500, AdvanceRelation = AdvanceRelation.Ahead };
var fallback = new SpawnCandidate { Id = "fallback", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 2000, PathDistance = 20, IngressDistance = 10, ClearRadius = 20, AdvanceRelation = AdvanceRelation.Behind };
var profile = new PlacementProfile { MinimumPathDistance = 500, MinimumIngressDistance = 200, MinimumClearRadius = 300, AllowedRelations = new HashSet<AdvanceRelation> { AdvanceRelation.Ahead }, FallbackMode = PlacementFallbackMode.IgnoreRouteConstraints };
var selected = new PlacementSelector().Select(new[] { strict, fallback }, profile, new DeterministicRandom(1)); Check(selected?.Candidate.Id == "strict" && !selected.IsFallback, "route-constrained placement");
selected = new PlacementSelector().Select(new[] { fallback }, profile, new DeterministicRandom(1)); Check(selected?.Candidate.Id == "fallback" && selected.IsFallback, "route fallback placement");
}
static void RecoveredCompatibility()
{
var route = new[]
{
new SpawnCandidate { Id = "first-tie", Reachable = true, Spawnable = true, Progress = 50 },
new SpawnCandidate { Id = "lower", Reachable = true, Spawnable = true, Progress = 40 },
new SpawnCandidate { Id = "second-tie", Reachable = true, Spawnable = true, Progress = 50 }
};
var routeChoice = new PlacementSelector().Select(route, RecoveredDirectorDefaults.CreateRouteStepProfile(), new DeterministicRandom(1));
Check(routeChoice?.Candidate.Id == "first-tie", "recovered route uses strict greater-than and first tie");
var threat = new[]
{
new SpawnCandidate { Id = "too-small", Reachable = true, Spawnable = true, AreaWidth = 23, AreaHeight = 24 },
new SpawnCandidate { Id = "a", Reachable = true, Spawnable = true, AreaWidth = 24, AreaHeight = 24 },
new SpawnCandidate { Id = "b", Reachable = true, Spawnable = true, AreaWidth = 30, AreaHeight = 30 }
};
var expectedOrder = threat.ToArray(); var expectedRandom = new DeterministicRandom(17);
for (var index = 0; index < expectedOrder.Length; index++) { var other = expectedRandom.NextInt(expectedOrder.Length); (expectedOrder[index], expectedOrder[other]) = (expectedOrder[other], expectedOrder[index]); }
var expected = expectedOrder.First(x => x.AreaWidth >= 24 && x.AreaHeight >= 24).Id;
var threatChoice = new PlacementSelector().Select(threat, RecoveredDirectorDefaults.CreateThreatAreaProfile(), new DeterministicRandom(17));
Check(threatChoice?.Candidate.Id == expected, "recovered threat shuffle uses full-range random transpositions then first valid");
var samples = HullVisibilitySamples.CreateFivePointPattern(new DirectorVector(10, 0, 2), new DirectorVector(0, 0, 2), 32, 72);
Check(samples.Count == 5, "recovered visibility sample count");
Near(samples[0].Y, 16, .0001f, "lower left hull offset"); Near(samples[1].Y, -16, .0001f, "lower right hull offset");
Near(samples[2].Z, 38, .0001f, "upper center half-height"); Near(samples[3].Z, 74, .0001f, "upper corner full-height");
var uniform = new UniformEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<string>> { [EncounterKind.Special] = new[] { "a", "b", "c" } });
var composition = uniform.Compose(EncounterKind.Special, 20, new WorldSnapshot(), new DefaultDirectorPolicy(), new DeterministicRandom(3));
Check(composition.Sum(x => x.Count) == 20 && composition.All(x => x.Archetype is "a" or "b" or "c"), "uniform recovered composition");
var rotation = new SpecialClassRotation(new[] { "a", "b", "c" });
rotation.RestoreState(new Dictionary<string, double> { ["a"] = 0, ["b"] = 0, ["c"] = 0 });
var selected = rotation.SelectDue(10, x => x != "a", new DeterministicRandom(4));
Check(selected is "b" or "c", "special rotation selects uniformly from due eligible classes");
Near((float)rotation.CaptureState()["a"], 30, .0001f, "ineligible class retry delay");
rotation.ReportSpawnFailure(selected!, 10); Near((float)rotation.CaptureState()[selected!], 15, .0001f, "failed class retry delay");
rotation.ReportSpawnSuccess(selected!, 20); Near((float)rotation.CaptureState()[selected!], 1019, .0001f, "successful class hold");
rotation.ReleaseAfterLifecycle(selected!, 30); Near((float)rotation.CaptureState()[selected!], 75, .0001f, "special respawn interval");
}
static void Population()
{
var ledger = new PopulationLedger(new PopulationConfiguration()); var empty = new PopulationSnapshot(); Check(ledger.TryReserve(1, EncounterKind.Boss, 1, empty), "reserve"); Check(ledger.Available(EncounterKind.Boss, empty) == 0, "reservation counts"); ledger.Resolve(new(1, RequestResultKind.Failed, 0)); Check(ledger.Available(EncounterKind.Boss, empty) == 1, "failure releases");
Check(ledger.TryReserve(2, EncounterKind.Boss, 1, empty, 10), "reserve deferred"); ledger.Resolve(new(2, RequestResultKind.Deferred, 0)); Check(ledger.Available(EncounterKind.Boss, empty) == 0, "deferred remains reserved"); Check(ledger.Expire(10).SequenceEqual(new long[] { 2 }), "reservation expires"); Check(ledger.Available(EncounterKind.Boss, empty) == 1, "expiry releases");
}
static void Wave()
{
var wave = new WaveSequenceController(new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 }, new DeterministicRandom(2)); wave.Start(0); wave.Update(0, false); Check(wave.Update(0, false).IssueWave, "wave issued"); Check(wave.Update(0, false).Completed, "wave completed");
var restored = new WaveSequenceController(new WaveSequenceConfiguration(), new DeterministicRandom(2), wave.Snapshot); Check(restored.State == WaveState.Done, "wave restore"); restored.Cancel(); Check(restored.State == WaveState.Inactive, "wave cancel");
}
static void Scenarios()
{
var scenario = new ScenarioController(new[] { new ScenarioStageDefinition("one") }); scenario.Start(0); Check(scenario.TryAdvance(0, false) && scenario.Status == ScenarioStatus.Complete, "scenario complete"); var rounds = new RoundSessionFlow(); rounds.BeginRound("1"); Check(rounds.IsSessionActive, "round adapter"); rounds.EndRound(); Check(!rounds.IsSessionActive, "round end"); Check(new ContinuousSessionFlow().IsSessionActive, "continuous adapter");
var restored = new ScenarioController(new[] { new ScenarioStageDefinition("one") }, scenario.Snapshot); Check(restored.Status == ScenarioStatus.Complete, "scenario restore"); restored.Reset(); Check(restored.Status == ScenarioStatus.Inactive, "scenario reset");
}
static void Core()
{
var core = new DirectorCore(new DirectorConfiguration { Tempo = new TempoConfiguration { RelaxMinimumSeconds = 0, RelaxMaximumSeconds = 0 } });
var output = core.Tick(new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = "p" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = "s", Reachable = true, Spawnable = true, NearestParticipantDistance = 800, ThreatSeparation = 2000 } } });
Check(output.Events.Count == 1, "core emits tempo transition");
var multi = new DirectorCore(new DirectorConfiguration { MaximumRequestsPerTick = 2 }, schedule: new RepeatingSchedule()); var requests = multi.Tick(World(0)); Check(requests.SpawnRequests.Count == 2, "multiple requests per tick");
}
static WorldSnapshot World(double time, string? round = null) => new() { Time = time, RoundId = round, Participants = new[] { new ParticipantSnapshot { Id = "p" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = "s", Reachable = true, Spawnable = true, NearestParticipantDistance = 1000, ThreatSeparation = 3000 } } };
static void Persistence()
{
var config = new DirectorConfiguration { Seed = 42 }; Check(DirectorStateCodec.ToJson(new DirectorCore(config).CaptureState()).Contains("HasTicked"), "pre-tick state serializes"); var first = new DirectorCore(config); first.ApplyPressure(new("p", PressureSeverity.Major, "test"), 0); var initial = first.Tick(World(0)); Check(initial.SpawnRequests.Count == 1, "state setup request");
var json = DirectorStateCodec.ToJson(first.CaptureState()); Check(json.Contains("ConfigurationVersion"), "state JSON");
var restored = new DirectorCore(config, DirectorStateCodec.FromJson(json));
var a = first.Tick(World(4)); var b = restored.Tick(World(4)); Check(a.Tempo == b.Tempo && a.TeamPressure == b.TeamPressure && a.SpawnRequests.SequenceEqual(b.SpawnRequests), "restored core remains deterministic");
var diagnostics = new DirectorTelemetryBuffer(restored, 2); restored.ApplyPressure(new("p", PressureSeverity.Minor, "one"), 4); restored.ApplyPressure(new("p", PressureSeverity.Minor, "two"), 4); restored.ApplyPressure(new("p", PressureSeverity.Minor, "three"), 4); Check(diagnostics.Snapshot().Count == 2, "telemetry bounded"); diagnostics.Clear(); Check(diagnostics.Snapshot().Count == 0, "telemetry clear");
Check(restored.CancelPendingRequests().Count > 0, "cancel pending");
}
static void RuleSchedules()
{
var population = new PopulationLedger(new PopulationConfiguration()); var world = World(0); var random = new DeterministicRandom(4);
var rule = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = "boss", Kind = EncounterKind.Boss, Count = 1, Priority = 10, CooldownSeconds = 30, Phases = new HashSet<TempoPhase> { TempoPhase.Relax } } });
Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "rule selection"); rule.SelectionFinalized(false); Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "failed rule rolls cooldown back"); rule.SelectionFinalized(true); Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) is null, "rule cooldown committed");
var milestone = new MilestoneEncounterSchedule("boss-half", EncounterKind.Boss, .5f); var halfway = new WorldSnapshot { Time = 0, TeamProgress = .6f, ModeId = "campaign", RoundId = "r1" };
Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "milestone selected"); milestone.SelectionFinalized(false); Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, "milestone retry"); milestone.SelectionFinalized(true); Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) is null, "milestone one-shot per round");
var state = milestone.CaptureScheduleState(); var copy = new MilestoneEncounterSchedule("boss-half", EncounterKind.Boss, .5f); copy.RestoreScheduleState(state); Check(copy.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) is null, "milestone persistence");
}
static void PersistentPolicies()
{
var configuration = new DirectorConfiguration();
var policy = new StatefulPolicy("tests.policy");
var placement = new StatefulPlacementPolicy("tests.placement");
var composer = new StatefulComposer("tests.composer");
var original = new DirectorCore(configuration, policy: policy, placementPolicy: placement, composer: composer);
original.Tick(World(0));
Check(policy.CallCount > 0 && placement.CallCount > 0, "custom policy state setup");
var json = DirectorStateCodec.ToJson(original.CaptureState());
var restoredPolicy = new StatefulPolicy("tests.policy");
var restoredPlacement = new StatefulPlacementPolicy("tests.placement");
var restoredComposer = new StatefulComposer("tests.composer");
_ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json), policy: restoredPolicy, placementPolicy: restoredPlacement, composer: restoredComposer);
Check(restoredPolicy.CallCount == policy.CallCount, "custom Director policy restored");
Check(restoredPlacement.CallCount == placement.CallCount, "custom placement policy restored");
Check(restoredComposer.CallCount == composer.CallCount, "custom composer restored");
var threw = false;
try { _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json), policy: new StatefulPolicy("wrong.policy"), placementPolicy: restoredPlacement, composer: restoredComposer); }
catch (InvalidOperationException) { threw = true; }
Check(threw, "custom policy identity mismatch rejected");
threw = false;
try { _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json)); }
catch (InvalidOperationException) { threw = true; }
Check(threw, "custom policy state cannot be silently discarded");
}
static void ConcurrentSchedules()
{
var schedule = new ConcurrentEncounterSchedule(new[]
{
new ScheduleLane { Id = "ambient", Priority = 10, Schedule = new BasicEncounterSchedule(100, 100) },
new ScheduleLane { Id = "secondary", Priority = 5, Schedule = new BasicEncounterSchedule(100, 100) }
});
var core = new DirectorCore(new DirectorConfiguration { MaximumRequestsPerTick = 2 }, schedule: schedule);
var output = core.Tick(World(0)); Check(output.SpawnRequests.Count == 2, "independent schedule lanes share a tick");
var state = schedule.CaptureScheduleState(); var copy = new ConcurrentEncounterSchedule(new[] { new ScheduleLane { Id = "ambient", Priority = 10, Schedule = new BasicEncounterSchedule(100, 100) }, new ScheduleLane { Id = "secondary", Priority = 5, Schedule = new BasicEncounterSchedule(100, 100) } }); copy.RestoreScheduleState(state); Check(copy.CaptureScheduleState()["coordinator.served"].Contains("ambient"), "concurrent schedule persistence");
}
static void CompositionAndRetry()
{
var composer = new WeightedEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<WeightedArchetype>> { [EncounterKind.Ambient] = new[] { new WeightedArchetype("grunt", 1) } });
var configuration = new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 3, DelaySeconds = 1 } };
var core = new DirectorCore(configuration, composer: composer); var first = core.Tick(World(0)).SpawnRequests.Single(); Check(first.Composition.Single() == new EncounterMember("grunt", 1) && first.Attempt == 1, "weighted composition");
core.ApplyResult(new(first.RequestId, RequestResultKind.Failed, 0, "blocked")); var saved = DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(core.CaptureState())); Check(saved.RetryQueue.Count == 1, "retry persisted");
var restored = new DirectorCore(configuration, saved, composer: composer); Check(restored.Tick(World(.5)).SpawnRequests.Count == 0, "retry delay"); var retry = restored.Tick(World(1)).SpawnRequests.Single(); Check(retry.Attempt == 2 && retry.Composition.SequenceEqual(first.Composition), "retry preserves attempt and composition");
restored.ApplyResult(new(retry.RequestId, RequestResultKind.PartiallySucceeded, 0, "partial")); Check(restored.CaptureState().RetryQueue.Single().RequestedCount == 1, "partial remainder queued");
}
static void Resources()
{
var rules = new[] { new ResourceRule { Category = "healing", TargetCount = 2, MaximumRequestsPerTick = 2 } }; var resources = new ResourcePopulationController(rules, 4);
var core = new DirectorCore(new DirectorConfiguration(), resources: resources); var baseWorld = World(0); var world = new WorldSnapshot { Time = baseWorld.Time, Participants = baseWorld.Participants, SpawnCandidates = baseWorld.SpawnCandidates, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "r1", Category = "healing", AreaCapacity = 2 }, new ResourceCandidate { Id = "r2", Category = "healing", AreaCapacity = 1 } } } };
var output = core.Tick(world); Check(output.ResourceRequests.Count == 2, "resource density requests"); foreach (var request in output.ResourceRequests) core.ApplyResourceResult(new(request.RequestId, RequestResultKind.Succeeded));
var state = core.CaptureState(); Check(state.Resources?.Consumed.Count == 2, "resource revisit state captured");
var restoredResources = new ResourcePopulationController(rules, 9); _ = new DirectorCore(new DirectorConfiguration(), state, resources: restoredResources); Check(restoredResources.CaptureState().Consumed.Count == 2, "resource state restored");
var threw = false; try { _ = new ResourcePopulationController(new[] { new ResourceRule { Category = "healing", TargetCount = 3 } }, state.Resources!.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "resource definition mismatch rejected");
var clusterRules = new[] { new ResourceRule { Category = "ammo", TargetCount = 2, MaximumRequestsPerTick = 2, MaximumPerCluster = 1 } }; var clustered = new ResourcePopulationController(clusterRules); var clusteredWorld = new WorldSnapshot { Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "a", Category = "ammo", ClusterId = "room" }, new ResourceCandidate { Id = "b", Category = "ammo", ClusterId = "room" } } } }; var clusterRequest = clustered.Tick(clusteredWorld.Resources, clusteredWorld).Single(); Check(clusterRequest.CandidateId is "a" or "b", "resource cluster limit"); clustered.ApplyResult(new(clusterRequest.RequestId, RequestResultKind.Succeeded));
var clusterCopy = new ResourcePopulationController(clusterRules, clustered.CaptureState()); var changedSnapshot = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "replacement", Category = "ammo", ClusterId = "room" } } }; Check(clusterCopy.Tick(changedSnapshot, new WorldSnapshot { Resources = changedSnapshot }).Count == 0, "resource cluster history survives absent candidates");
threw = false; try { _ = new ResourcePopulationController(new[] { new ResourceRule { Category = "healing", TargetCount = 2, MaximumRequestsPerTick = 2, RequiredTags = new HashSet<string> { "locked" } } }, state.Resources!.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "resource required-tag mismatch rejected");
}
static void Orchestration()
{
var orchestrationConfiguration = new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("boss", Encounter: EncounterKind.Boss, MusicCue: "boss_intro"), new ScenarioStageDefinition("escape") }, Waves = new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 }, WaveEncounterCount = 3 };
var orchestration = new DirectorOrchestration(orchestrationConfiguration); var core = new DirectorCore(new DirectorConfiguration(), orchestration: orchestration); core.StartScenario(0); var stage = core.Tick(World(0)); Check(stage.SpawnRequests.Single().Kind == EncounterKind.Boss && stage.Events.Any(x => x.Name == "music_cue"), "scenario encounter and music cue"); core.ApplyResult(new(stage.SpawnRequests[0].RequestId, RequestResultKind.Succeeded, 1)); core.Tick(World(1)); Check(orchestration.ScenarioStatus == ScenarioStatus.Running, "scenario advances stages");
var saved = core.CaptureState(); var restoredOrchestration = new DirectorOrchestration(orchestrationConfiguration, saved.Orchestration!.Value); _ = new DirectorCore(new DirectorConfiguration(), saved, orchestration: restoredOrchestration); Check(restoredOrchestration.ScenarioStatus == orchestration.ScenarioStatus, "orchestration persistence");
var threw = false; try { _ = new DirectorOrchestration(new DirectorOrchestrationConfiguration { PersistenceId = "wrong" }, saved.Orchestration.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "orchestration definition mismatch rejected");
threw = false; try { _ = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("changed") } }, saved.Orchestration.Value); } catch (InvalidOperationException) { threw = true; }
Check(threw, "orchestration content mismatch rejected");
threw = false; try { _ = new DirectorCore(new DirectorConfiguration(), saved, orchestration: new DirectorOrchestration(orchestrationConfiguration)); } catch (InvalidOperationException) { threw = true; }
Check(threw, "fresh orchestration cannot replace restored state");
var waves = new DirectorOrchestration(orchestrationConfiguration); var waveCore = new DirectorCore(new DirectorConfiguration(), orchestration: waves); waveCore.StartWaveSequence(0); waveCore.Tick(World(0)); var issued = waveCore.Tick(World(0)); Check(issued.SpawnRequests.Single().Kind == EncounterKind.CommonWave && issued.SpawnRequests[0].RequestedCount == 3, "integrated panic wave");
var retryConfiguration = new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 2, DelaySeconds = 0 } };
var retryScenario = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("required", Encounter: EncounterKind.Boss) } }); var retryScenarioCore = new DirectorCore(retryConfiguration, orchestration: retryScenario); retryScenarioCore.StartScenario(0); var failedStage = retryScenarioCore.Tick(World(0)).SpawnRequests.Single(); retryScenarioCore.ApplyResult(new(failedStage.RequestId, RequestResultKind.Failed, 0)); var stageRetry = retryScenarioCore.Tick(World(0)).SpawnRequests.Single(); Check(stageRetry.Attempt == 2 && retryScenario.ScenarioStatus == ScenarioStatus.Running, "scenario waits for retried host result"); retryScenarioCore.ApplyResult(new(stageRetry.RequestId, RequestResultKind.Succeeded, 1)); Check(retryScenarioCore.Tick(World(0)).Events.Any(x => x.Name == "scenario_advanced") && retryScenario.ScenarioStatus == ScenarioStatus.Complete, "scenario advances after retry success");
var retryWaves = new DirectorOrchestration(new DirectorOrchestrationConfiguration { Waves = new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 } }); var retryWaveCore = new DirectorCore(retryConfiguration, orchestration: retryWaves); retryWaveCore.StartWaveSequence(0); retryWaveCore.Tick(World(0)); var failedWave = retryWaveCore.Tick(World(0)).SpawnRequests.Single(); retryWaveCore.ApplyResult(new(failedWave.RequestId, RequestResultKind.Failed, 0)); var waveRetry = retryWaveCore.Tick(World(0)).SpawnRequests.Single(); Check(waveRetry.Attempt == 2 && retryWaves.WaveActive, "wave retry bypasses ordinary-schedule suppression"); retryWaveCore.ApplyResult(new(waveRetry.RequestId, RequestResultKind.Succeeded, waveRetry.RequestedCount)); Check(retryWaveCore.Tick(World(0)).Events.Any(x => x.Name == "wave_sequence_complete"), "wave waits for retry success before completion");
var terminalOrchestration = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition("required", Encounter: EncounterKind.Boss) } }); var terminalCore = new DirectorCore(new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 1 } }, orchestration: terminalOrchestration); terminalCore.StartScenario(0); var terminalRequest = terminalCore.Tick(World(0)).SpawnRequests.Single(); terminalCore.ApplyResult(new(terminalRequest.RequestId, RequestResultKind.Rejected, 0, "veto")); Check(terminalCore.Tick(World(0)).Events.Any(x => x.Name == "orchestration_failed") && terminalOrchestration.ScenarioStatus == ScenarioStatus.Failed, "terminal orchestration failure is explicit");
}
static void AdaptiveCallbacks()
{
var policy = new AdaptivePolicy(); var core = new DirectorCore(new DirectorConfiguration(), policy: policy, schedule: new SingleKindSchedule(EncounterKind.Boss)); var output = core.Tick(World(0)); Check(output.SpawnRequests.Single().RequestedCount == 1 && output.MusicIntensity == .5f && output.Events.Any(x => x.Name == "boss_music"), "adaptive count music and boss callback");
var populationCore = new DirectorCore(new DirectorConfiguration(), policy: policy, schedule: new SingleKindSchedule(EncounterKind.Ambient)); Check(populationCore.Tick(World(0)).SpawnRequests.Single().RequestedCount == 1, "adaptive population limit");
var raisedLimit = new DirectorCore(new DirectorConfiguration { Population = new PopulationConfiguration { Limits = new Dictionary<EncounterKind, int> { [EncounterKind.Ambient] = 0 } } }, policy: policy, schedule: new SingleKindSchedule(EncounterKind.Ambient)); Check(raisedLimit.Tick(World(0)).SpawnRequests.Single().RequestedCount == 1, "adaptive population policy can raise configured limit");
var timing = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = "timed", Kind = EncounterKind.Ambient, CooldownSeconds = 10, Phases = new HashSet<TempoPhase> { TempoPhase.Relax } } }, new ModeEncounterTimingPolicy(new Dictionary<string, double> { ["survival"] = .5 })); var ledger = new PopulationLedger(new PopulationConfiguration()); var timedWorld = new WorldSnapshot { Time = 0, ModeId = "survival" }; Check(timing.SelectEncounter(timedWorld, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is not null, "adaptive timing initial"); timing.SelectionFinalized(true); Check(timing.SelectEncounter(new WorldSnapshot { Time = 4.9, ModeId = "survival" }, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is null && timing.SelectEncounter(new WorldSnapshot { Time = 5, ModeId = "survival" }, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is not null, "mode cooldown multiplier");
var decisions = 0; var runner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), new ContinuousSessionFlow(), new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(x => new(x.RequestId, RequestResultKind.Succeeded, x.RequestedCount)), notifications: new DelegateNotificationSink(_ => decisions++)); runner.Update(); Check(decisions == 1, "decision notification callback");
}
static void HostRunner()
{
var core = new DirectorCore(new DirectorConfiguration()); var flow = new ContinuousSessionFlow(); var executed = 0;
var runner = new DirectorHostRunner(core, flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(request => { executed++; return new(request.RequestId, RequestResultKind.Succeeded, request.RequestedCount); }));
var result = runner.Update(); Check(result.Status == DirectorRunStatus.Updated && executed == 1 && result.Results.Count == 1, "host executes decisions");
var denied = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => throw new Exception("must not capture")), new DelegateSpawnExecutor(_ => throw new Exception()), new TestAuthority(false)); Check(denied.Update().Status == DirectorRunStatus.NotAuthoritative, "authority gate");
flow.IsSessionActive = false; Check(runner.Update().Status == DirectorRunStatus.SessionInactive, "session gate");
flow.IsSessionActive = true; var failureRunner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(_ => throw new InvalidOperationException("host failure"))); Check(failureRunner.Update().Results[0].Result == RequestResultKind.Failed, "host exception becomes result");
var malformedRunner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(request => new(request.RequestId + 99, RequestResultKind.Succeeded, request.RequestedCount + 1))); var malformed = malformedRunner.Update().Results.Single(); Check(malformed.RequestId == 1 && malformed.Result == RequestResultKind.Failed && malformed.SpawnedCount == 0, "host runner normalizes malformed result");
}
static void OwnershipAndResources()
{
var allocator = new EncounterOwnershipAllocator(); var first = allocator.Select(new[] { new OwnershipCandidate("a", 1, false), new OwnershipCandidate("b", 1) }, new DeterministicRandom(1)); Check(first == "b", "ownership eligibility");
var r1 = new DeterministicRandom(9); var r2 = new DeterministicRandom(9); var candidates = new[] { new OwnershipCandidate("a", 1), new OwnershipCandidate("b", 3) }; Check(allocator.Select(candidates, r1) == allocator.Select(candidates, r2), "ownership deterministic");
var budget = new ResourceBudget(new Dictionary<string, float> { ["health"] = 2 }); Check(budget.TrySpend("health", 1.5f) && !budget.TrySpend("health", 1), "resource budget"); Near(budget.Capture()["health"], .5f, .0001f, "resource capture");
}
static MusicEventCatalog TestMusicCatalog() => new(new[]
{
new MusicEventDefinition { EventId = "ambient.safe", TrackId = "ambient", Priority = MusicPriority.Low },
new MusicEventDefinition { EventId = "mobbed", TrackId = "mob", Priority = MusicPriority.High, DuckTrackList = new[] { "all" } },
new MusicEventDefinition { EventId = "boss", TrackId = "boss", Priority = MusicPriority.Critical, BlockTrackList = new[] { "combat" } },
new MusicEventDefinition { EventId = "combat.low", TrackId = "combat", Priority = MusicPriority.Low },
new MusicEventDefinition { EventId = "combat.high", TrackId = "combat", Priority = MusicPriority.High },
new MusicEventDefinition { EventId = "delayed", TrackId = "stinger", Priority = MusicPriority.Medium, DelaySeconds = 2 },
new MusicEventDefinition { EventId = "intro", TrackId = "intro", Priority = MusicPriority.High, AutoQueueEventId = "loop" },
new MusicEventDefinition { EventId = "loop", TrackId = "loop", Priority = MusicPriority.High },
new MusicEventDefinition { EventId = "death", TrackId = "death", Priority = MusicPriority.Critical, Flags = MusicMasterFlags.AllowAfterDeath },
new MusicEventDefinition { EventId = "tag.master", TrackId = "master", Priority = MusicPriority.High, TimingTags = new[] { new MusicTimingTag(2, 4) }, LoopSeconds = 8 },
new MusicEventDefinition { EventId = "tag.child", TrackId = "child", Priority = MusicPriority.High, SyncTrackId = "master", SyncTagIndex = 2 },
new MusicEventDefinition { EventId = "split.a", TrackId = "split", Priority = MusicPriority.High },
new MusicEventDefinition { EventId = "split.b", TrackId = "split", Priority = MusicPriority.High, Flags = MusicMasterFlags.PlaySplit }
});
static MusicDecisionBatch MusicBatch(double time, params MusicDecision[] decisions)
{
var batch = new MusicDecisionBatch { Time = time }; batch.Decisions.AddRange(decisions); return batch;
}
static void MusicAuthority()
{
var special = new SpecialMusicAlertConfiguration { ArchetypeId = "ambusher", CloseEvent = "alert.close", MiddleEvent = "alert.middle", FarEvent = "alert.far" };
var cues = new MusicCueConfiguration { CombatIntroEvents = new[] { "combat.intro" }, CombatSecondaryEvent = "combat.secondary", CombatCloseEvent = "combat.close", MobbedEvent = "mobbed", BossApproachingEvent = "boss", BossTrackId = "boss", HazardBurningEvent = "hazard.burn", HazardAttackEvent = "hazard.attack", HazardRageEvent = "hazard.rage", SafeAtmosphereEvent = "ambient.safe", DangerAtmosphereEvent = "ambient.danger", PositionalStingerEvent = "choir" };
var configuration = RecoveredMusicDirectorDefaults.CreateConfiguration(cues, new[] { special }, 41);
var core = new MusicDirectorCore(configuration);
var idle = new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } };
var initial = core.Tick(idle); Check(initial.Decisions.Count(x => x.Kind == MusicDecisionKind.SetMixerLayer) == 9, "music authority emits initial mixer snapshot");
var combatInput = new MusicParticipantSnapshot { Id = "p", CumulativeDamage = 10, VisibleCommonThreats = 3, ActiveCommonThreats = 20, ScannedCommonThreats = 5, VeryCloseCommonFactor = 1, AttackingCommonFactor = .4f, CloseCommonAction = .42f, InflictedDamage = true, SpecialThreats = new[] { new SpecialMusicThreat("ambusher", 1000, "s1") } };
var combat = core.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { combatInput } });
Check(combat.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == "combat.intro"), "music authority starts combat group");
Check(combat.Decisions.Any(x => x.TargetId == "alert.close"), "music authority selects close special alert");
Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.MobBeating).Value, .37f, .0001f, "recovered damage duck equation");
Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.MobRules).Value, 0f, .0001f, "recovered mob pressure equation");
Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.CombatClose).Value, 0f, .0001f, "recovered close action equation");
var boss = core.Tick(new MusicWorldSnapshot { Time = 2, Participants = new[] { new MusicParticipantSnapshot { Id = "p", BossPresent = true } } });
Check(boss.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == "boss"), "music boss approach transition");
var defeated = core.Tick(new MusicWorldSnapshot { Time = 3, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } });
Check(defeated.Decisions.Any(x => x.Kind == MusicDecisionKind.StopTrack && x.TargetId == "boss" && x.FadeSeconds == 2), "music boss defeat track stop");
var hazard = core.Tick(new MusicWorldSnapshot { Time = 4, Participants = new[] { new MusicParticipantSnapshot { Id = "p", HazardPresent = true, HazardBurning = true, HazardAttacking = true, HazardRage = .75f, HazardEmitterId = "h", HazardPosition = new DirectorVector(1, 2, 3), WanderingHazards = new[] { new WanderingMusicHazard("w", .2f, new DirectorVector(4, 5, 6)) } } } });
Check(new[] { "hazard.burn", "hazard.attack", "hazard.rage" }.All(id => hazard.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == id)), "hazard transition cues");
Near(hazard.Decisions.Single(x => x.LayerId == MusicMixerLayers.HazardRage).Value, .25f, .0001f, "hazard rage inverse layer");
Check(hazard.Decisions.Any(x => x.TargetId == "hazard.wandering.tier4" && x.Position is not null), "wandering hazard low-anger tier and position");
var generic = core.Tick(new MusicWorldSnapshot { Time = 5, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(1, MusicTriggerKind.Play, "p", "custom.event") });
Check(generic.Decisions.Any(x => x.TargetId == "custom.event"), "generic gameplay music event");
var duplicate = core.Tick(new MusicWorldSnapshot { Time = 6, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(1, MusicTriggerKind.Play, "p", "should.not.repeat") });
Check(!duplicate.Decisions.Any(x => x.TargetId == "should.not.repeat"), "music events apply exactly once");
var reveal = core.Tick(new MusicWorldSnapshot { Time = 7, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(2, MusicTriggerKind.LargeAreaRevealed, "p") });
var revealLimited = core.Tick(new MusicWorldSnapshot { Time = 8, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }, new[] { new MusicGameplayEvent(3, MusicTriggerKind.LargeAreaRevealed, "p") });
Check(reveal.Decisions.Any(x => x.TargetId == "world.large_area_reveal") && !revealLimited.Decisions.Any(x => x.TargetId == "world.large_area_reveal"), "large-area reveal cooldown");
}
static void MusicRuntimeArbitration()
{
var catalog = TestMusicCatalog(); var runtime = new MusicRuntime("p", catalog);
var ambient = runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, "p", "ambient.safe", "test")));
Check(ambient.Actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "ambient.safe"), "runtime plays resolved event");
Check(runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, "p", "ambient.safe", "duplicate"))).Actions.Count == 0, "runtime decisions apply exactly once");
var mob = runtime.Apply(MusicBatch(1, new MusicDecision(2, MusicDecisionKind.Play, "p", "mobbed", "test")));
Check(mob.Actions.Any(x => x.Kind == MusicRuntimeActionKind.SetTrackVolume && x.TrackId == "ambient" && x.Value == .5f), "runtime duck list");
Check(!mob.Actions.Any(x => x.Kind == MusicRuntimeActionKind.SetTrackVolume && x.TrackId == "mob" && x.Value == .5f), "duck source does not duck itself");
runtime.Apply(MusicBatch(2, new MusicDecision(3, MusicDecisionKind.Play, "p", "boss", "test")));
var blocked = runtime.Apply(MusicBatch(3, new MusicDecision(4, MusicDecisionKind.Play, "p", "combat.high", "test")));
Check(blocked.Results.Single(x => x.DecisionId == 4).Result == MusicDecisionResultKind.RejectedBlocked, "runtime active block list");
var priority = new MusicRuntime("p", catalog); priority.Apply(MusicBatch(0, new MusicDecision(10, MusicDecisionKind.Play, "p", "combat.low", "test")));
var high = priority.Apply(MusicBatch(1, new MusicDecision(11, MusicDecisionKind.Play, "p", "combat.high", "test")));
Check(high.Actions.Any(x => x.Kind == MusicRuntimeActionKind.StopEvent && x.EventId == "combat.low") && high.Actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "combat.high"), "higher priority replaces same track");
var rejectedLow = priority.Apply(MusicBatch(2, new MusicDecision(12, MusicDecisionKind.Play, "p", "combat.low", "test")));
Check(rejectedLow.Results.Single(x => x.DecisionId == 12).Result == MusicDecisionResultKind.RejectedPriority, "lower priority cannot replace active track");
var delayed = new MusicRuntime("p", catalog); var deferred = delayed.Apply(MusicBatch(0, new MusicDecision(20, MusicDecisionKind.Play, "p", "delayed", "test")));
Check(deferred.Results.Single().Result == MusicDecisionResultKind.Deferred && delayed.Tick(1).Actions.Count == 0, "runtime delay queue");
Check(delayed.Tick(2).Actions.Any(x => x.EventId == "delayed"), "runtime delayed engagement");
var tagged = new MusicRuntime("p", catalog); tagged.Apply(MusicBatch(0, new MusicDecision(30, MusicDecisionKind.Play, "p", "tag.master", "test")));
var tagDeferred = tagged.Apply(MusicBatch(1, new MusicDecision(31, MusicDecisionKind.Play, "p", "tag.child", "test")));
Check(tagDeferred.Results.Single().Result == MusicDecisionResultKind.Deferred && tagged.Tick(3.9).Actions.Count == 0 && tagged.Tick(4).Actions.Any(x => x.EventId == "tag.child"), "runtime timing-tag alignment");
var split = new MusicRuntime("p", catalog); split.Apply(MusicBatch(0, new MusicDecision(35, MusicDecisionKind.Play, "p", "split.a", "test"))); split.Apply(MusicBatch(1, new MusicDecision(36, MusicDecisionKind.Play, "p", "split.b", "test")));
Check(split.Active.Count == 2, "runtime split flag permits concurrent same-track events");
var auto = new MusicRuntime("p", catalog); var intro = auto.Apply(MusicBatch(0, new MusicDecision(40, MusicDecisionKind.Play, "p", "intro", "test"))); var instance = intro.Results.Single().InstanceId;
Check(auto.ReportCompleted(instance, 1).Actions.Any(x => x.EventId == "loop"), "runtime autoqueue on completion");
auto.SetLifecycle(true, false); var deadReject = auto.Apply(MusicBatch(2, new MusicDecision(41, MusicDecisionKind.Play, "p", "ambient.safe", "test"), new MusicDecision(42, MusicDecisionKind.Play, "p", "death", "test")));
Check(deadReject.Results.Single(x => x.DecisionId == 41).Result == MusicDecisionResultKind.RejectedLifecycle && deadReject.Results.Single(x => x.DecisionId == 42).Result == MusicDecisionResultKind.Accepted, "runtime after-death gate");
var general = auto.Apply(MusicBatch(3, new MusicDecision(43, MusicDecisionKind.General, "p", "custom.command", "test") { Arguments = new[] { "a", "b" } }));
Check(general.Actions.Single(x => x.Kind == MusicRuntimeActionKind.General).Arguments.SequenceEqual(new[] { "a", "b" }), "runtime forwards generalized commands");
}
static void MusicPersistenceAndHost()
{
var config = RecoveredMusicDirectorDefaults.CreateConfiguration(seed: 99); var first = new MusicDirectorCore(config); first.Tick(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } });
first.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = "p", VisibleCommonThreats = 2, WeaponActivity = .5f } } });
var restored = new MusicDirectorCore(config, MusicStateCodec.DirectorFromJson(MusicStateCodec.DirectorToJson(first.CaptureState())));
var nextWorld = new MusicWorldSnapshot { Time = 2, Participants = new[] { new MusicParticipantSnapshot { Id = "p", VisibleCommonThreats = 1 } } };
Check(first.Tick(nextWorld).Decisions.SequenceEqual(restored.Tick(nextWorld).Decisions), "music authority save restore determinism");
var catalog = TestMusicCatalog(); var runtime = new MusicRuntime("p", catalog); runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, "p", "ambient.safe", "test"), new MusicDecision(2, MusicDecisionKind.Play, "p", "delayed", "test")));
var restoredRuntime = new MusicRuntime("p", catalog, MusicStateCodec.RuntimeFromJson(MusicStateCodec.RuntimeToJson(runtime.CaptureState())));
Check(runtime.Tick(2).Actions.SequenceEqual(restoredRuntime.Tick(2).Actions), "music runtime save restore determinism");
var actions = new List<MusicRuntimeAction>(); var hostCore = new MusicDirectorCore(RecoveredMusicDirectorDefaults.CreateConfiguration(new MusicCueConfiguration { CombatIntroEvents = new[] { "ambient.safe" }, CombatSecondaryEvent = "", CombatCloseEvent = "", SafeAtmosphereEvent = "", DangerAtmosphereEvent = "", PositionalStingerEvent = "" })); var hostRuntime = new MusicRuntime("p", catalog); var runner = new MusicDirectorHostRunner(hostCore, hostRuntime, new DelegateMusicAudioSink(actions.Add));
runner.Update(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "p" } } }); runner.Update(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = "p", VisibleCommonThreats = 1 } } });
Check(actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "ambient.safe"), "music host runner bridges decisions to audio sink");
}
static void Validation()
{
var backwards = new DirectorCore(new DirectorConfiguration()); backwards.Tick(World(2)); var threw = false; try { backwards.Tick(World(1)); } catch (InvalidOperationException) { threw = true; }
Check(threw, "reject backwards world time");
threw = false; try { _ = new DirectorConfiguration { MaximumRequestsPerTick = 0 }; new DirectorConfiguration { MaximumRequestsPerTick = 0 }.Validate(); } catch (ArgumentOutOfRangeException) { threw = true; }
Check(threw, "configuration validation");
threw = false; try { _ = DirectorStateCodec.FromJson("{}"); } catch (InvalidOperationException) { threw = true; }
Check(threw, "state validation");
threw = false; try { new WorldSnapshot { Participants = new[] { new ParticipantSnapshot { Id = "same" }, new ParticipantSnapshot { Id = "same" } } }.Validate(); } catch (ArgumentException) { threw = true; }
Check(threw, "duplicate participant validation");
var incomplete = new DirectorCore(new DirectorConfiguration()); var incompleteRequest = incomplete.Tick(World(0)).SpawnRequests.Single(); incomplete.ApplyResult(new(incompleteRequest.RequestId, RequestResultKind.Succeeded, 0)); Check(incomplete.CaptureState().RetryQueue.Count == 1, "incomplete success becomes partial retry");
var excessive = new DirectorCore(new DirectorConfiguration()); var excessiveRequest = excessive.Tick(World(0)).SpawnRequests.Single(); threw = false; try { excessive.ApplyResult(new(excessiveRequest.RequestId, RequestResultKind.Succeeded, excessiveRequest.RequestedCount + 1)); } catch (ArgumentOutOfRangeException) { threw = true; }
Check(threw, "excessive host result rejected");
}
static void LongDeterminism()
{
var configuration = new DirectorConfiguration { Seed = 7823, RequestTimeoutSeconds = 2 }; DirectorCore first = new(configuration); DirectorCore second = new(configuration);
for (var index = 0; index < 1000; index++)
{
var time = index * .1;
if (index % 73 == 0) { var pressure = new PressureEvent("p", (PressureSeverity)(index / 73 % 4 + 1), "simulation"); first.ApplyPressure(pressure, time); second.ApplyPressure(pressure, time); }
var world = World(time); var a = first.Tick(world); var b = second.Tick(world);
Check(a.Tempo == b.Tempo && a.TeamPressure == b.TeamPressure && a.MusicIntensity == b.MusicIntensity && a.SpawnRequests.SequenceEqual(b.SpawnRequests) && a.Events.SequenceEqual(b.Events), $"deterministic tick {index}");
foreach (var request in a.SpawnRequests) { var result = new SpawnRequestResult(request.RequestId, RequestResultKind.Succeeded, request.RequestedCount); first.ApplyResult(result); second.ApplyResult(result); }
if (index == 500) second = new DirectorCore(configuration, DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(second.CaptureState())));
}
}
sealed class RejectAllPlacement : IPlacementPolicy { public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => false; public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0; }
sealed class TestAuthority : IDirectorAuthority { public TestAuthority(bool value) { IsAuthoritative = value; } public bool IsAuthoritative { get; } }
sealed class RepeatingSchedule : IEncounterSchedule { int _index; public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random) => _index++ % 2 == 0 ? EncounterKind.Ambient : EncounterKind.Special; public int RequestedCount(EncounterKind kind, WorldSnapshot world) => 1; }
sealed class SingleKindSchedule : IEncounterSchedule { readonly EncounterKind _kind; public SingleKindSchedule(EncounterKind kind) { _kind = kind; } public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random) => _kind; public int RequestedCount(EncounterKind kind, WorldSnapshot world) => 2; }
sealed class AdaptivePolicy : IDirectorPolicy, IAdaptiveDirectorPolicy, IAdaptivePopulationPolicy
{
public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) => true;
public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => true;
public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => "boss";
public int AdjustEncounterCount(EncounterKind kind, int proposedCount, TempoPhase tempo, float teamPressure, WorldSnapshot world) => 1;
public float AdjustMusicIntensity(float proposedIntensity, TempoPhase tempo, float teamPressure, WorldSnapshot world) => .5f;
public bool ShouldPlayBossMusic(EncounterKind kind, WorldSnapshot world) => true;
public int AdjustPopulationLimit(EncounterKind kind, int configuredLimit, WorldSnapshot world) => 1;
}
static void AdvancedDeterminism()
{
var configuration = new DirectorConfiguration { Seed = 99, MaximumRequestsPerTick = 3, Retry = new RetryConfiguration { MaximumAttempts = 3, DelaySeconds = .5 } };
DirectorCore first = BuildAdvancedCore(configuration); DirectorCore second = BuildAdvancedCore(configuration); first.StartScenario(0); second.StartScenario(0); first.StartWaveSequence(0, 2); second.StartWaveSequence(0, 2);
for (var index = 0; index < 500; index++)
{
var baseWorld = World(index * .25); var world = new WorldSnapshot { Time = baseWorld.Time, Participants = baseWorld.Participants, SpawnCandidates = baseWorld.SpawnCandidates, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "supply-a", Category = "supply", AreaCapacity = 2 }, new ResourceCandidate { Id = "supply-b", Category = "supply", AreaCapacity = 1 } } } };
var a = first.Tick(world); var b = second.Tick(world); Check(a.SpawnRequests.SequenceEqual(b.SpawnRequests) && a.ResourceRequests.SequenceEqual(b.ResourceRequests) && a.Events.SequenceEqual(b.Events) && a.MusicIntensity == b.MusicIntensity, $"advanced deterministic tick {index}");
foreach (var request in a.SpawnRequests) { var result = new SpawnRequestResult(request.RequestId, request.RequestId % 7 == 0 ? RequestResultKind.Failed : RequestResultKind.Succeeded, request.RequestId % 7 == 0 ? 0 : request.RequestedCount); first.ApplyResult(result); second.ApplyResult(result); }
foreach (var request in a.ResourceRequests) { var result = new ResourceSpawnResult(request.RequestId, RequestResultKind.Succeeded); first.ApplyResourceResult(result); second.ApplyResourceResult(result); }
if (index == 250) second = BuildAdvancedCore(configuration, DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(second.CaptureState())));
}
}
static DirectorCore BuildAdvancedCore(DirectorConfiguration configuration, DirectorState? state = null)
{
IEncounterSchedule Schedule() => new ConcurrentEncounterSchedule(new[] { new ScheduleLane { Id = "ambient", Priority = 10, Schedule = new BasicEncounterSchedule(2, 4) }, new ScheduleLane { Id = "special", Priority = 5, Schedule = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = "special", Kind = EncounterKind.Special, Count = 1, CooldownSeconds = 5, Probability = .5f, Phases = new HashSet<TempoPhase> { TempoPhase.Relax, TempoPhase.BuildUp, TempoPhase.SustainPeak } } }) } });
var composer = new WeightedEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<WeightedArchetype>> { [EncounterKind.CommonWave] = new[] { new WeightedArchetype("grunt", 3), new WeightedArchetype("brute", 1) } });
var resources = new ResourcePopulationController(new[] { new ResourceRule { Category = "supply", TargetCount = 2, MaximumRequestsPerTick = 2 } }, 77);
var orchestrationConfiguration = new DirectorOrchestrationConfiguration { PersistenceId = "advanced-test-v1", ScenarioStages = new[] { new ScenarioStageDefinition("opening", .5), new ScenarioStageDefinition("boss", Encounter: EncounterKind.Boss), new ScenarioStageDefinition("done") }, Waves = new WaveSequenceConfiguration { WaveCount = 2, InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = .25, PauseMaximum = .25 } };
if (state is null) return new(configuration, schedule: Schedule(), composer: composer, resources: resources, orchestration: new(orchestrationConfiguration));
var orchestration = state.Value.Orchestration is { } orchestrationState ? new DirectorOrchestration(orchestrationConfiguration, orchestrationState) : null;
return new(configuration, state.Value, schedule: Schedule(), composer: composer, resources: resources, orchestration: orchestration);
}
sealed class StatefulPolicy : IDirectorPolicy, IPersistentDirectorPolicy
{
public StatefulPolicy(string id) { PersistenceId = id; }
public string PersistenceId { get; }
public int CallCount { get; private set; }
public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) { CallCount++; return true; }
public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => true;
public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => kind.ToString();
public IReadOnlyDictionary<string, string> CapturePolicyState() => new Dictionary<string, string> { ["calls"] = CallCount.ToString(CultureInfo.InvariantCulture) };
public void RestorePolicyState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state["calls"], CultureInfo.InvariantCulture);
}
sealed class StatefulPlacementPolicy : IPlacementPolicy, IPersistentPlacementPolicy
{
public StatefulPlacementPolicy(string id) { PersistenceId = id; }
public string PersistenceId { get; }
public int CallCount { get; private set; }
public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) { CallCount++; return true; }
public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0;
public IReadOnlyDictionary<string, string> CapturePlacementState() => new Dictionary<string, string> { ["calls"] = CallCount.ToString(CultureInfo.InvariantCulture) };
public void RestorePlacementState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state["calls"], CultureInfo.InvariantCulture);
}
sealed class StatefulComposer : IEncounterComposer, IPersistentEncounterComposer
{
public StatefulComposer(string id) { PersistenceId = id; }
public string PersistenceId { get; }
public int CallCount { get; private set; }
public IReadOnlyList<EncounterMember> Compose(EncounterKind kind, int requestedCount, WorldSnapshot world, IDirectorPolicy policy, DeterministicRandom random) { CallCount++; return new[] { new EncounterMember("stateful", requestedCount) }; }
public IReadOnlyDictionary<string, string> CaptureComposerState() => new Dictionary<string, string> { ["calls"] = CallCount.ToString(CultureInfo.InvariantCulture) };
public void RestoreComposerState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state["calls"], CultureInfo.InvariantCulture);
}
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public sealed record MusicEnvelopeState(float Damage, float FastActivity, float SlowActivity, float DamageInflicted, float CommonSight, float MobPressure, float Action, float Threat, float Calm, float AmbientBasis);
public sealed record MusicParticipantState(
string ParticipantId,
float PreviousDamage,
MusicEnvelopeState Envelopes,
bool CombatActive,
bool MobbedActive,
bool BossPresent,
bool HazardBurning,
bool HazardAttacking,
bool HazardRaging,
bool WanderingHazardActive,
bool DangerAtmosphereActive,
bool ScenarioEnded,
double AtmosphereReadyAt,
double PositionalStingerReadyAt,
double WanderingHazardReadyAt,
double LargeAreaRevealReadyAt,
IReadOnlyDictionary<string, double> SpecialAlertReadyAt,
IReadOnlyDictionary<string, float> LastMixerLayers);
public sealed record MusicDirectorState(
string ConfigurationVersion,
bool HasTicked,
double LastTickTime,
long LastGameplayEventSequence,
long NextDecisionId,
RandomState Random,
IReadOnlyList<MusicParticipantState> Participants);
public sealed class MusicDirectorCore
{
readonly MusicDirectorConfiguration _configuration;
DeterministicRandom _random;
readonly Dictionary<string, ParticipantMemory> _participants = new(StringComparer.Ordinal);
long _lastEventSequence;
long _nextDecisionId = 1;
double _lastTickTime;
bool _hasTicked;
public MusicDirectorCore(MusicDirectorConfiguration configuration)
{
_configuration = configuration; configuration.Validate(); _random = new(configuration.Seed);
}
public MusicDirectorCore(MusicDirectorConfiguration configuration, MusicDirectorState state)
{
_configuration = configuration; configuration.Validate();
if (!string.Equals(configuration.Version, state.ConfigurationVersion, StringComparison.Ordinal)) throw new InvalidOperationException($"Music state version '{state.ConfigurationVersion}' does not match configuration version '{configuration.Version}'.");
if (state.NextDecisionId <= 0 || state.Participants is null || state.Participants.Select(x => x.ParticipantId).Distinct(StringComparer.Ordinal).Count() != state.Participants.Count) throw new InvalidOperationException("Music Director state is incomplete or invalid.");
_random = new(state.Random); _hasTicked = state.HasTicked; _lastTickTime = state.LastTickTime; _lastEventSequence = state.LastGameplayEventSequence; _nextDecisionId = state.NextDecisionId;
foreach (var participant in state.Participants)
{
if (string.IsNullOrWhiteSpace(participant.ParticipantId) || participant.SpecialAlertReadyAt is null || participant.LastMixerLayers is null) throw new InvalidOperationException("Music participant state is incomplete.");
_participants.Add(participant.ParticipantId, new ParticipantMemory(participant));
}
}
public MusicDecisionBatch Tick(MusicWorldSnapshot world, IReadOnlyList<MusicGameplayEvent>? gameplayEvents = null)
{
if (_configuration.ValidateSnapshots) world.Validate();
if (_hasTicked && world.Time < _lastTickTime) throw new InvalidOperationException("Music world time cannot move backwards.");
var delta = _hasTicked ? world.Time - _lastTickTime : 0;
_hasTicked = true; _lastTickTime = world.Time;
var output = new MusicDecisionBatch { Time = world.Time };
foreach (var input in world.Participants.OrderBy(x => x.Id, StringComparer.Ordinal)) if (!_participants.ContainsKey(input.Id)) _participants.Add(input.Id, new ParticipantMemory(input.Id, input.CumulativeDamage, _configuration.SpecialAlerts));
ApplyGameplayEvents(world, gameplayEvents ?? Array.Empty<MusicGameplayEvent>(), output);
foreach (var input in world.Participants.OrderBy(x => x.Id, StringComparer.Ordinal))
{
var state = _participants[input.Id];
if (world.ScenarioEnded && !state.ScenarioEnded) output.Decisions.Add(NewDecision(MusicDecisionKind.General, input.Id, "scenario_ended", "world scenario ended"));
state.ScenarioEnded = world.ScenarioEnded;
UpdateParticipant(input, state, delta, world.Time, output);
}
return output;
}
void ApplyGameplayEvents(MusicWorldSnapshot world, IReadOnlyList<MusicGameplayEvent> events, MusicDecisionBatch output)
{
if (events.Select(x => x.Sequence).Distinct().Count() != events.Count) throw new ArgumentException("Music gameplay event sequences must be unique within a tick.");
foreach (var input in events.OrderBy(x => x.Sequence))
{
if (input.Sequence <= _lastEventSequence) continue;
if (!float.IsFinite(input.FadeSeconds) || input.FadeSeconds < 0) throw new ArgumentException("Music event fades must be finite and non-negative.");
_lastEventSequence = input.Sequence;
var targets = string.IsNullOrWhiteSpace(input.ParticipantId)
? world.Participants.Select(x => x.Id).OrderBy(x => x, StringComparer.Ordinal).ToArray()
: new[] { input.ParticipantId };
foreach (var participantId in targets)
{
var kind = input.Kind switch
{
MusicTriggerKind.Play => MusicDecisionKind.Play,
MusicTriggerKind.StopEvent => MusicDecisionKind.StopEvent,
MusicTriggerKind.StopTrack => MusicDecisionKind.StopTrack,
MusicTriggerKind.StopAll => MusicDecisionKind.StopAll,
MusicTriggerKind.AmbientMob or MusicTriggerKind.MobSpawn or MusicTriggerKind.MobSpawnBehind or MusicTriggerKind.LargeAreaRevealed or MusicTriggerKind.LandmarkRevealed => MusicDecisionKind.Play,
_ => MusicDecisionKind.General
};
var target = input.Kind switch
{
MusicTriggerKind.Reset => "reset",
MusicTriggerKind.ScenarioEnded => "scenario_ended",
MusicTriggerKind.ParticipantRestored => "participant_restored",
MusicTriggerKind.AmbientMob => _configuration.Cues.AmbientMobEvent,
MusicTriggerKind.MobSpawn => _configuration.Cues.MobSpawnEvent,
MusicTriggerKind.MobSpawnBehind => _configuration.Cues.MobSpawnBehindEvent,
MusicTriggerKind.LargeAreaRevealed => _configuration.Cues.LargeAreaRevealEvent,
_ => input.TargetId
};
if (input.Kind == MusicTriggerKind.LargeAreaRevealed && _participants.TryGetValue(participantId, out var revealState))
{
if (revealState.LargeAreaRevealReadyAt > world.Time) continue;
revealState.LargeAreaRevealReadyAt = world.Time + _configuration.Dynamic.LargeAreaRevealCooldownSeconds;
}
if (kind == MusicDecisionKind.Play && string.IsNullOrWhiteSpace(target)) continue;
output.Decisions.Add(NewDecision(kind, participantId, target, $"gameplay:{input.Kind}") with { FadeSeconds = input.FadeSeconds, EmitterId = input.EmitterId, Position = input.Position, Arguments = input.Arguments });
if ((input.Kind is MusicTriggerKind.Reset or MusicTriggerKind.ParticipantRestored) && _participants.TryGetValue(participantId, out var state))
{
state.ResetDynamic();
var current = world.Participants.FirstOrDefault(x => string.Equals(x.Id, participantId, StringComparison.Ordinal)); if (current is not null) state.PreviousDamage = current.CumulativeDamage;
}
}
}
}
void UpdateParticipant(MusicParticipantSnapshot input, ParticipantMemory state, double delta, double time, MusicDecisionBatch output)
{
var d = _configuration.Dynamic;
var dt = (float)Math.Max(0, delta);
var damageDelta = Math.Max(0, input.CumulativeDamage - state.PreviousDamage); state.PreviousDamage = input.CumulativeDamage;
state.Damage = Advance(state.Damage, damageDelta > 0 ? 1 : 0, dt, d.DamageRisePerSecond, d.DamageDecaySeconds);
state.FastActivity = Advance(state.FastActivity, input.WeaponActivity, dt, d.FastActivityRisePerSecond, d.FastActivityDecaySeconds);
state.SlowActivity = Advance(state.SlowActivity, input.WeaponActivity, dt, d.SlowActivityRisePerSecond, d.SlowActivityDecaySeconds);
state.DamageInflicted = Advance(state.DamageInflicted, input.InflictedDamage ? 1 : 0, dt, d.InflictedDamageRisePerSecond, d.InflictedDamageDecaySeconds);
state.CommonSight = Advance(state.CommonSight, Clamp01(input.VisibleCommonThreats / d.CommonSightNormalizer), dt, float.PositiveInfinity, d.CommonSightDecaySeconds);
var damageDriver = Math.Min(5 * input.VeryCloseCommonFactor, state.Damage);
var damageDuck = RemapClamped(damageDriver, d.DamageDuckInputMinimum, d.DamageDuckInputMaximum, 0, d.DamageDuckOutputMaximum);
var mobDriver = Math.Min(4 * input.VeryCloseCommonFactor, state.Damage);
var mobTarget = mobDriver < d.MobDamageMinimum ? 0 : RemapClamped(mobDriver, d.MobDamageMinimum, d.MobDamageMaximum, d.MobOutputMinimum, d.MobOutputMaximum);
if (input.IsIncapacitated) mobTarget = 0;
state.MobPressure = Advance(state.MobPressure, mobTarget, dt, float.PositiveInfinity, d.MobPressureDecaySeconds);
var rawAction = Math.Max(Math.Max(input.GlobalCombatActive || input.BossPresent ? 1 : 0, 5 * input.AttackingCommonFactor), state.FastActivity);
rawAction = Clamp01(rawAction);
var rawThreat = Clamp01(Math.Max(input.MobPressureActive || input.BossPresent || input.HazardPresent ? 1 : 0, 2 * input.AttackingCommonFactor));
state.Action = Advance(state.Action, rawAction, dt, float.PositiveInfinity, d.ActionDecaySeconds);
state.Threat = Advance(state.Threat, rawThreat, dt, float.PositiveInfinity, d.ThreatDecaySeconds);
state.Calm = Advance(state.Calm, 1 - rawAction, dt, float.PositiveInfinity, d.CalmDecaySeconds);
var ambientTarget = RemapClamped(state.Calm, d.AmbientInputMinimum, d.AmbientInputMaximum, 0, 1);
state.AmbientBasis = Advance(state.AmbientBasis, ambientTarget, dt, float.PositiveInfinity, d.AmbientDecaySeconds);
var ambient = state.AmbientBasis * (1 - input.HazardRage);
var mobNetwork = state.MobPressure < .1f ? 0 : state.MobPressure;
var secondary = Clamp01(2 * state.DamageInflicted);
var close = RemapClamped(input.CloseCommonAction, 0, d.CloseActionMaximum, 0, 1);
UpdateCombat(input, state, time, output);
EmitLayer(state, output, input.Id, MusicMixerLayers.MobBeating, damageDuck, "damage/common pressure");
EmitLayer(state, output, input.Id, MusicMixerLayers.MobRules, 1 - mobNetwork, "mob pressure");
EmitLayer(state, output, input.Id, MusicMixerLayers.HazardRage, 1 - input.HazardRage, "hazard rage");
EmitLayer(state, output, input.Id, MusicMixerLayers.Combat, state.CombatActive ? 1 : 0, "combat state");
EmitLayer(state, output, input.Id, MusicMixerLayers.CombatSecondary, 1 - secondary, "damage inflicted");
EmitLayer(state, output, input.Id, MusicMixerLayers.CombatClose, 1 - close, "close common action");
EmitLayer(state, output, input.Id, MusicMixerLayers.Adrenaline, input.Adrenaline, "adrenaline");
EmitLayer(state, output, input.Id, MusicMixerLayers.Checkpoint, input.InCheckpoint ? 1 : 0, "checkpoint state");
EmitLayer(state, output, input.Id, MusicMixerLayers.Ambient, ambient, "calm and hazard rage");
UpdateMobbed(input, state, mobNetwork, output);
UpdateBoss(input, state, output);
UpdateHazard(input, state, time, output);
UpdateSpecialAlerts(input, state, time, ambient, output);
UpdateAtmosphere(input, state, time, ambient, output);
UpdatePositionalStinger(input, state, time, output);
}
void UpdateCombat(MusicParticipantSnapshot input, ParticipantMemory state, double time, MusicDecisionBatch output)
{
var allowed = input.DynamicMusicAllowed && input.IsEligible && input.IsAlive && !input.IsIncapacitated && !input.BossPresent && !input.HazardAttacking;
if (!state.CombatActive && allowed && input.VisibleCommonThreats > 0)
{
state.CombatActive = true;
var intros = _configuration.Cues.CombatIntroEvents.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
if (intros.Length > 0) Play(output, input.Id, intros[_random.NextInt(intros.Length)], "combat started");
PlayIfConfigured(output, input.Id, _configuration.Cues.CombatSecondaryEvent, "combat secondary started");
PlayIfConfigured(output, input.Id, _configuration.Cues.CombatCloseEvent, "combat close layer started");
}
var stopSignal = Math.Max(state.CommonSight, state.SlowActivity);
var canStop = !input.MobPressureActive && !input.IsIncapacitated && !input.BossPresent && !input.HazardAttacking && input.ActiveCommonThreats <= _configuration.Dynamic.CombatStopActiveCount && input.ScannedCommonThreats <= _configuration.Dynamic.CombatStopScannedCount;
if (state.CombatActive && (!allowed || canStop && stopSignal < _configuration.Dynamic.CombatStopSignal))
{
state.CombatActive = false;
foreach (var track in _configuration.Cues.CombatTrackIds.Where(x => !string.IsNullOrWhiteSpace(x))) output.Decisions.Add(NewDecision(MusicDecisionKind.StopTrack, input.Id, track, "combat ended") with { FadeSeconds = _configuration.CombatStopFadeSeconds });
}
}
void UpdateMobbed(MusicParticipantSnapshot input, ParticipantMemory state, float mobNetwork, MusicDecisionBatch output)
{
var active = mobNetwork > 0;
if (active && !state.MobbedActive) PlayIfConfigured(output, input.Id, _configuration.Cues.MobbedEvent, "mob pressure entered");
else if (!active && state.MobbedActive && !string.IsNullOrWhiteSpace(_configuration.Cues.MobbedEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, input.Id, _configuration.Cues.MobbedEvent, "mob pressure cleared"));
state.MobbedActive = active;
}
void UpdateBoss(MusicParticipantSnapshot input, ParticipantMemory state, MusicDecisionBatch output)
{
if (input.BossPresent && !state.BossPresent) PlayIfConfigured(output, input.Id, _configuration.Cues.BossApproachingEvent, "boss approaching");
else if (!input.BossPresent && state.BossPresent && !string.IsNullOrWhiteSpace(_configuration.Cues.BossTrackId)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopTrack, input.Id, _configuration.Cues.BossTrackId, "boss defeated") with { FadeSeconds = _configuration.BossStopFadeSeconds });
state.BossPresent = input.BossPresent;
}
void UpdateHazard(MusicParticipantSnapshot input, ParticipantMemory state, double time, MusicDecisionBatch output)
{
TransitionEvent(input.Id, input.HazardBurning, ref state.HazardBurning, _configuration.Cues.HazardBurningEvent, _configuration.HazardStopFadeSeconds, "hazard burning", output);
TransitionEvent(input.Id, input.HazardAttacking, ref state.HazardAttacking, _configuration.Cues.HazardAttackEvent, _configuration.HazardStopFadeSeconds, "hazard attacking", output);
var raging = input.HazardRage > 0;
if (raging && !state.HazardRaging && !string.IsNullOrWhiteSpace(_configuration.Cues.HazardRageEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, _configuration.Cues.HazardRageEvent, "hazard rage started") with { EmitterId = input.HazardEmitterId, Position = input.HazardPosition });
else if (!raging && state.HazardRaging && !string.IsNullOrWhiteSpace(_configuration.Cues.HazardRageEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, input.Id, _configuration.Cues.HazardRageEvent, "hazard rage ended") with { FadeSeconds = _configuration.HazardRageStopFadeSeconds });
state.HazardRaging = raging;
var wandering = input.WanderingHazards.OrderBy(x => x.Anger).ThenBy(x => x.Id, StringComparer.Ordinal).FirstOrDefault();
if (wandering is null)
{
if (state.WanderingHazardActive && !string.IsNullOrWhiteSpace(_configuration.Cues.WanderingHazardTrackId)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopTrack, input.Id, _configuration.Cues.WanderingHazardTrackId, "no wandering hazard") with { FadeSeconds = _configuration.HazardStopFadeSeconds });
state.WanderingHazardActive = false;
}
else if (time >= state.WanderingHazardReadyAt)
{
var tier = wandering.Anger < .25f ? 0 : wandering.Anger < .5f ? 1 : wandering.Anger < .75f ? 2 : 3;
output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, _configuration.Cues.WanderingHazardEventsLowToHigh[tier], "wandering hazard anger tier") with { EmitterId = wandering.Id, Position = wandering.Position });
state.WanderingHazardActive = true; state.WanderingHazardReadyAt = time + _configuration.Dynamic.WanderingHazardIntervalSeconds;
}
}
void TransitionEvent(string participantId, bool active, ref bool previous, string eventId, float fade, string reason, MusicDecisionBatch output)
{
if (active && !previous) PlayIfConfigured(output, participantId, eventId, reason + " started");
else if (!active && previous && !string.IsNullOrWhiteSpace(eventId)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, participantId, eventId, reason + " ended") with { FadeSeconds = fade });
previous = active;
}
void UpdateSpecialAlerts(MusicParticipantSnapshot input, ParticipantMemory state, double time, float ambient, MusicDecisionBatch output)
{
if (ambient <= _configuration.SpecialAlertMinimumAmbient) return;
var profiles = _configuration.SpecialAlerts;
foreach (var threat in input.SpecialThreats.OrderBy(x => x.ArchetypeId, StringComparer.Ordinal).ThenBy(x => x.EmitterId, StringComparer.Ordinal))
{
var index = -1;
for (var i = 0; i < profiles.Count; i++) if (string.Equals(profiles[i].ArchetypeId, threat.ArchetypeId, StringComparison.Ordinal)) { index = i; break; }
state.SpecialReadyAt.TryGetValue(threat.ArchetypeId, out var readyAt);
if (index < 0 || threat.Distance > profiles[index].ScanMaximumDistance || readyAt > time) continue;
var profile = profiles[index];
var eventId = threat.Distance <= profile.CloseMaximumDistance ? profile.CloseEvent : threat.Distance >= profile.FarMinimumDistance ? profile.FarEvent : profile.MiddleEvent;
output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, eventId, $"special alert:{threat.ArchetypeId}") with { EmitterId = threat.EmitterId, Position = threat.Position });
var min = threat.Distance <= profile.CloseMaximumDistance ? profile.RandomMultiplierMaximum : threat.Distance >= profile.FarMinimumDistance ? profile.RandomMultiplierMaximum * 2 : profile.RandomMultiplierMaximum;
var max = threat.Distance <= profile.CloseMaximumDistance ? (int)MathF.Round(profile.RandomMultiplierMaximum * 1.4f) : threat.Distance >= profile.FarMinimumDistance ? (int)MathF.Round(profile.RandomMultiplierMaximum * 2.5f) : profile.RandomMultiplierMaximum * 2;
var multiplier = min + _random.NextInt(max - min + 1);
state.SpecialReadyAt[threat.ArchetypeId] = time + multiplier * profile.IntervalBeats * (60.0 / profile.Bpm);
for (var step = 1; step < profiles.Count; step++)
{
var peer = profiles[(index + step) % profiles.Count].ArchetypeId;
var floor = time + _configuration.SpecialPeerStaggerStartSeconds + (step - 1) * _configuration.SpecialPeerStaggerStepSeconds;
state.SpecialReadyAt.TryGetValue(peer, out var peerReady); if (peerReady < floor) state.SpecialReadyAt[peer] = floor;
}
break;
}
}
void UpdateAtmosphere(MusicParticipantSnapshot input, ParticipantMemory state, double time, float ambient, MusicDecisionBatch output)
{
var danger = state.Threat > 0;
if (danger && !state.DangerAtmosphereActive && time >= state.AtmosphereReadyAt)
{
PlayIfConfigured(output, input.Id, _configuration.Cues.DangerAtmosphereEvent, "danger atmosphere"); state.DangerAtmosphereActive = true; state.AtmosphereReadyAt = time + _configuration.AtmosphereRetrySeconds;
}
else if (!danger && state.DangerAtmosphereActive && ambient <= 0)
{
if (!string.IsNullOrWhiteSpace(_configuration.Cues.DangerAtmosphereEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, input.Id, _configuration.Cues.DangerAtmosphereEvent, "danger atmosphere cleared"));
state.DangerAtmosphereActive = false; state.AtmosphereReadyAt = time + _configuration.AtmosphereTransitionSeconds;
}
else if (!danger && !state.DangerAtmosphereActive && ambient > 0 && time >= state.AtmosphereReadyAt)
{
PlayIfConfigured(output, input.Id, _configuration.Cues.SafeAtmosphereEvent, "safe atmosphere"); state.AtmosphereReadyAt = time + _configuration.AtmosphereRetrySeconds + 10;
}
}
void UpdatePositionalStinger(MusicParticipantSnapshot input, ParticipantMemory state, double time, MusicDecisionBatch output)
{
if (state.AmbientBasis <= _configuration.Dynamic.PositionalStingerAmbientMinimum || time < state.PositionalStingerReadyAt || string.IsNullOrWhiteSpace(_configuration.Cues.PositionalStingerEvent)) return;
var candidates = input.PositionalCandidates.Where(x => x.Eligible).OrderBy(x => x.Id, StringComparer.Ordinal).ToArray(); if (candidates.Length == 0) return;
var selected = candidates[_random.NextInt(candidates.Length)];
output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, _configuration.Cues.PositionalStingerEvent, "positional stinger") with { EmitterId = selected.Id, Position = selected.Position, StartOffsetSeconds = .1f });
var multiplier = 1 + _random.NextInt(_configuration.Dynamic.PositionalStingerRandomMultiplierMaximum);
state.PositionalStingerReadyAt = time + multiplier * _configuration.Dynamic.PositionalStingerBeats * (60.0 / _configuration.Dynamic.PositionalStingerBpm);
}
void EmitLayer(ParticipantMemory state, MusicDecisionBatch output, string participantId, string layer, float value, string reason)
{
value = Clamp01(value);
if (state.LastLayers.TryGetValue(layer, out var previous) && MathF.Abs(previous - value) <= .000001f) return;
state.LastLayers[layer] = value;
output.Decisions.Add(NewDecision(MusicDecisionKind.SetMixerLayer, participantId, "", reason) with { LayerId = layer, Value = value });
}
void PlayIfConfigured(MusicDecisionBatch output, string participantId, string eventId, string reason) { if (!string.IsNullOrWhiteSpace(eventId)) Play(output, participantId, eventId, reason); }
void Play(MusicDecisionBatch output, string participantId, string eventId, string reason) => output.Decisions.Add(NewDecision(MusicDecisionKind.Play, participantId, eventId, reason));
MusicDecision NewDecision(MusicDecisionKind kind, string participantId, string target, string reason) => new(_nextDecisionId++, kind, participantId, target, reason);
static float Advance(float current, float target, float delta, float risePerSecond, float decaySeconds)
{
target = Clamp01(target);
if (target >= current) return float.IsPositiveInfinity(risePerSecond) ? target : Math.Min(target, current + risePerSecond * delta);
return Math.Max(target, current - delta / decaySeconds);
}
static float RemapClamped(float value, float inputMin, float inputMax, float outputMin, float outputMax) => inputMax <= inputMin ? outputMax : outputMin + Clamp01((value - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin);
static float Clamp01(float value) => Math.Clamp(value, 0, 1);
public MusicDirectorState CaptureState() => new(
_configuration.Version, _hasTicked, _lastTickTime, _lastEventSequence, _nextDecisionId, _random.State,
_participants.Values.OrderBy(x => x.Id, StringComparer.Ordinal).Select(x => x.Capture()).ToArray());
public bool RemoveParticipant(string participantId) => _participants.Remove(participantId);
public void Reset()
{
_participants.Clear(); _lastEventSequence = 0; _nextDecisionId = 1; _lastTickTime = 0; _hasTicked = false; _random = new(_configuration.Seed);
}
sealed class ParticipantMemory
{
public readonly string Id;
public float PreviousDamage, Damage, FastActivity, SlowActivity, DamageInflicted, CommonSight, MobPressure, Action, Threat, Calm, AmbientBasis;
public bool CombatActive, MobbedActive, BossPresent, HazardBurning, HazardAttacking, HazardRaging, WanderingHazardActive, DangerAtmosphereActive, ScenarioEnded;
public double AtmosphereReadyAt, PositionalStingerReadyAt, WanderingHazardReadyAt, LargeAreaRevealReadyAt;
public readonly Dictionary<string, double> SpecialReadyAt = new(StringComparer.Ordinal);
public readonly Dictionary<string, float> LastLayers = new(StringComparer.Ordinal);
public ParticipantMemory(string id, float previousDamage, IReadOnlyList<SpecialMusicAlertConfiguration> alerts) { Id = id; PreviousDamage = previousDamage; foreach (var alert in alerts) SpecialReadyAt[alert.ArchetypeId] = 0; }
public ParticipantMemory(MusicParticipantState state)
{
Id = state.ParticipantId; PreviousDamage = state.PreviousDamage;
(Damage, FastActivity, SlowActivity, DamageInflicted, CommonSight, MobPressure, Action, Threat, Calm, AmbientBasis) = state.Envelopes;
CombatActive = state.CombatActive; MobbedActive = state.MobbedActive; BossPresent = state.BossPresent; HazardBurning = state.HazardBurning; HazardAttacking = state.HazardAttacking; HazardRaging = state.HazardRaging; WanderingHazardActive = state.WanderingHazardActive; DangerAtmosphereActive = state.DangerAtmosphereActive; ScenarioEnded = state.ScenarioEnded; AtmosphereReadyAt = state.AtmosphereReadyAt; PositionalStingerReadyAt = state.PositionalStingerReadyAt; WanderingHazardReadyAt = state.WanderingHazardReadyAt; LargeAreaRevealReadyAt = state.LargeAreaRevealReadyAt;
foreach (var pair in state.SpecialAlertReadyAt) SpecialReadyAt[pair.Key] = pair.Value; foreach (var pair in state.LastMixerLayers) LastLayers[pair.Key] = pair.Value;
}
public MusicParticipantState Capture() => new(Id, PreviousDamage, new(Damage, FastActivity, SlowActivity, DamageInflicted, CommonSight, MobPressure, Action, Threat, Calm, AmbientBasis), CombatActive, MobbedActive, BossPresent, HazardBurning, HazardAttacking, HazardRaging, WanderingHazardActive, DangerAtmosphereActive, ScenarioEnded, AtmosphereReadyAt, PositionalStingerReadyAt, WanderingHazardReadyAt, LargeAreaRevealReadyAt, new Dictionary<string, double>(SpecialReadyAt), new Dictionary<string, float>(LastLayers));
public void ResetDynamic()
{
Damage = FastActivity = SlowActivity = DamageInflicted = CommonSight = MobPressure = Action = Threat = Calm = AmbientBasis = 0; CombatActive = MobbedActive = BossPresent = HazardBurning = HazardAttacking = HazardRaging = WanderingHazardActive = DangerAtmosphereActive = ScenarioEnded = false; AtmosphereReadyAt = PositionalStingerReadyAt = WanderingHazardReadyAt = LargeAreaRevealReadyAt = 0; LastLayers.Clear(); foreach (var key in SpecialReadyAt.Keys.ToArray()) SpecialReadyAt[key] = 0;
}
}
}
Game
library
namespace SboxDirector;
public interface IDirectorPolicy
{
bool AllowEncounter(EncounterKind kind, WorldSnapshot world);
string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random);
bool CanCoexist(EncounterKind requested, PopulationSnapshot population);
}
/// <summary>Implement this alongside IDirectorPolicy when a custom policy owns mutable deterministic state.</summary>
public interface IPersistentDirectorPolicy
{
string PersistenceId { get; }
IReadOnlyDictionary<string, string> CapturePolicyState();
void RestorePolicyState(IReadOnlyDictionary<string, string> state);
}
/// <summary>Optional mode/difficulty override surface corresponding to recovered scripted Director policy.</summary>
public interface IAdaptiveDirectorPolicy
{
int AdjustEncounterCount(EncounterKind kind, int proposedCount, TempoPhase tempo, float teamPressure, WorldSnapshot world);
float AdjustMusicIntensity(float proposedIntensity, TempoPhase tempo, float teamPressure, WorldSnapshot world);
bool ShouldPlayBossMusic(EncounterKind kind, WorldSnapshot world);
}
public interface IAdaptivePopulationPolicy
{
int AdjustPopulationLimit(EncounterKind kind, int configuredLimit, WorldSnapshot world);
}
public sealed class DefaultDirectorPolicy : IDirectorPolicy
{
public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) => true;
public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => requested != EncounterKind.Boss || population[EncounterKind.Boss] == 0;
public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => kind.ToString().ToLowerInvariant();
}
public interface IEncounterSchedule
{
EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random);
int RequestedCount(EncounterKind kind, WorldSnapshot world);
}
public interface IPersistentEncounterSchedule
{
IReadOnlyDictionary<string, string> CaptureScheduleState();
void RestoreScheduleState(IReadOnlyDictionary<string, string> state);
}
public interface IEncounterScheduleFeedback
{
void SelectionFinalized(bool requestCreated);
}
public sealed class BasicEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback
{
readonly double _ambientInterval;
readonly double _activeInterval;
double _nextEncounterTime;
double _previousEncounterTime;
bool _selectionPending;
public BasicEncounterSchedule(double ambientInterval = 3, double activeInterval = 8)
{
if (ambientInterval < 0 || activeInterval < 0) throw new ArgumentOutOfRangeException("Encounter intervals must be non-negative.");
_ambientInterval = ambientInterval; _activeInterval = activeInterval;
}
public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)
{
if (world.Time < _nextEncounterTime) return null;
if (tempo == TempoPhase.Relax && population.Available(EncounterKind.Ambient, world.Population) > 0)
{
_previousEncounterTime = _nextEncounterTime; _selectionPending = true; _nextEncounterTime = world.Time + _ambientInterval;
return EncounterKind.Ambient;
}
if (tempo is TempoPhase.BuildUp or TempoPhase.SustainPeak)
{
if (population.Available(EncounterKind.Special, world.Population) > 0 && random.NextFloat() < 0.25f) { _previousEncounterTime = _nextEncounterTime; _selectionPending = true; _nextEncounterTime = world.Time + _activeInterval; return EncounterKind.Special; }
if (population.Available(EncounterKind.CommonWave, world.Population) > 0) { _previousEncounterTime = _nextEncounterTime; _selectionPending = true; _nextEncounterTime = world.Time + _activeInterval; return EncounterKind.CommonWave; }
}
return null;
}
public int RequestedCount(EncounterKind kind, WorldSnapshot world) => kind switch { EncounterKind.Ambient => 1, EncounterKind.Special => 1, EncounterKind.CommonWave => 10, _ => 1 };
public IReadOnlyDictionary<string, string> CaptureScheduleState() => new Dictionary<string, string> { ["nextEncounterTime"] = _nextEncounterTime.ToString("R", CultureInfo.InvariantCulture) };
public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)
{
if (state.TryGetValue("nextEncounterTime", out var value) && double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) _nextEncounterTime = parsed;
}
public void SelectionFinalized(bool requestCreated) { if (_selectionPending && !requestCreated) _nextEncounterTime = _previousEncounterTime; _selectionPending = false; }
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public sealed class ResourceCandidate
{
public string Id { get; init; } = "";
public string Category { get; init; } = "";
public bool Reachable { get; init; } = true;
public bool Visible { get; init; }
public float AreaCapacity { get; init; } = 1;
public string ClusterId { get; init; } = "";
public IReadOnlySet<string> Tags { get; init; } = new HashSet<string>();
}
public sealed class ResourceWorldSnapshot
{
public IReadOnlyDictionary<string, int> ExistingCounts { get; init; } = new Dictionary<string, int>();
public IReadOnlyList<ResourceCandidate> Candidates { get; init; } = Array.Empty<ResourceCandidate>();
public void Validate()
{
if (ExistingCounts.Any(x => string.IsNullOrWhiteSpace(x.Key) || x.Value < 0)) throw new ArgumentException("Resource counts require non-empty categories and non-negative counts.");
if (Candidates.Any(x => string.IsNullOrWhiteSpace(x.Id) || string.IsNullOrWhiteSpace(x.Category) || !float.IsFinite(x.AreaCapacity) || x.AreaCapacity < 0 || x.Tags is null || x.Tags.Any(string.IsNullOrWhiteSpace))) throw new ArgumentException("Resource candidates require ids, categories, non-negative finite capacity, and valid tags.");
if (Candidates.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Candidates.Count) throw new ArgumentException("Resource candidate ids must be unique.");
}
}
public sealed class ResourceRule
{
public string Category { get; init; } = "resource";
public int TargetCount { get; init; } = 1;
public int MaximumRequestsPerTick { get; init; } = 1;
public bool AllowVisible { get; init; }
public int MaximumPerCluster { get; init; } = int.MaxValue;
public IReadOnlySet<string> RequiredTags { get; init; } = new HashSet<string>();
public void Validate() { if (string.IsNullOrWhiteSpace(Category) || TargetCount < 0 || MaximumRequestsPerTick <= 0 || MaximumPerCluster <= 0 || RequiredTags is null || RequiredTags.Any(string.IsNullOrWhiteSpace)) throw new ArgumentException("Invalid resource rule."); }
}
public interface IResourcePolicy
{
bool AllowResourceSpawn(string category, ResourceCandidate candidate, WorldSnapshot world);
string ConvertResource(string category, ResourceCandidate candidate, WorldSnapshot world);
bool ShouldAvoidResource(string archetype, ResourceCandidate candidate, WorldSnapshot world);
bool CanPickupObject(string archetype, string participantId, WorldSnapshot world);
}
public interface IPersistentResourcePolicy
{
string PersistenceId { get; }
IReadOnlyDictionary<string, string> CaptureResourcePolicyState();
void RestoreResourcePolicyState(IReadOnlyDictionary<string, string> state);
}
public sealed class DefaultResourcePolicy : IResourcePolicy
{
public bool AllowResourceSpawn(string category, ResourceCandidate candidate, WorldSnapshot world) => true;
public string ConvertResource(string category, ResourceCandidate candidate, WorldSnapshot world) => category;
public bool ShouldAvoidResource(string archetype, ResourceCandidate candidate, WorldSnapshot world) => false;
public bool CanPickupObject(string archetype, string participantId, WorldSnapshot world) => true;
}
public sealed record ResourceSpawnRequest(long RequestId, string Category, string Archetype, string CandidateId, string Reason);
public sealed record ResourceSpawnResult(long RequestId, RequestResultKind Result, string? Detail = null);
public readonly record struct PendingResourceState(long RequestId, string Category, string CandidateId, string ClusterId);
public readonly record struct ResourcePopulationState(string ConfigurationSignature, long NextRequestId, RandomState Random, IReadOnlyList<PendingResourceState> Pending, IReadOnlyList<PendingResourceState> Consumed, ExtensionState Policy);
/// <summary>Generalized density/conversion/revisit controller backed by recovered item-population responsibilities.</summary>
public sealed class ResourcePopulationController
{
readonly IReadOnlyList<ResourceRule> _rules;
readonly IResourcePolicy _policy;
DeterministicRandom _random;
readonly Dictionary<long, PendingResourceState> _pending = new();
readonly Dictionary<string, PendingResourceState> _consumed = new(StringComparer.Ordinal);
readonly string _configurationSignature;
long _nextRequestId = 1;
public ResourcePopulationController(IEnumerable<ResourceRule> rules, int seed = 1, IResourcePolicy? policy = null)
{
_rules = rules.OrderBy(x => x.Category, StringComparer.Ordinal).ToArray(); foreach (var rule in _rules) rule.Validate();
if (_rules.Select(x => x.Category).Distinct(StringComparer.Ordinal).Count() != _rules.Count) throw new ArgumentException("Resource rule categories must be unique.");
_policy = policy ?? new DefaultResourcePolicy();
_random = new DeterministicRandom(seed);
_configurationSignature = string.Join(";", _rules.Select(x => $"{Encode(x.Category)}:{x.TargetCount}:{x.MaximumRequestsPerTick}:{x.AllowVisible}:{x.MaximumPerCluster}:{string.Join(",", x.RequiredTags.OrderBy(tag => tag, StringComparer.Ordinal).Select(Encode))}"));
}
public ResourcePopulationController(IEnumerable<ResourceRule> rules, ResourcePopulationState state, IResourcePolicy? policy = null) : this(rules, 1, policy)
{
RestoreState(state);
}
public IReadOnlyList<ResourceSpawnRequest> Tick(ResourceWorldSnapshot resources, WorldSnapshot world)
{
var output = new List<ResourceSpawnRequest>();
foreach (var rule in _rules)
{
var existing = resources.ExistingCounts.TryGetValue(rule.Category, out var count) ? count : 0;
var reserved = _pending.Values.Count(x => x.Category == rule.Category);
var needed = Math.Min(rule.MaximumRequestsPerTick, Math.Max(0, rule.TargetCount - existing - reserved));
for (var requestIndex = 0; requestIndex < needed; requestIndex++)
{
var candidates = resources.Candidates.Where(x => x.Category == rule.Category && x.Reachable && (rule.AllowVisible || !x.Visible) && !_consumed.ContainsKey(x.Id) && !_pending.Values.Any(p => p.CandidateId == x.Id) && ClusterAvailable(x, rule) && rule.RequiredTags.All(x.Tags.Contains) && _policy.AllowResourceSpawn(rule.Category, x, world)).ToArray();
if (candidates.Length == 0) break;
var bestCapacity = candidates.Max(x => x.AreaCapacity); var best = candidates.Where(x => Math.Abs(x.AreaCapacity - bestCapacity) < .00001f).ToArray(); var candidate = best[_random.NextInt(best.Length)];
var archetype = _policy.ConvertResource(rule.Category, candidate, world); if (string.IsNullOrWhiteSpace(archetype) || _policy.ShouldAvoidResource(archetype, candidate, world)) { _consumed[candidate.Id] = new(0, rule.Category, candidate.Id, candidate.ClusterId); requestIndex--; continue; }
var id = _nextRequestId++; _pending[id] = new(id, rule.Category, candidate.Id, candidate.ClusterId); output.Add(new(id, rule.Category, archetype, candidate.Id, $"target={rule.TargetCount}; existing={existing}; reserved={reserved}; cluster={candidate.ClusterId}")); reserved++;
}
}
return output;
}
public void ApplyResult(ResourceSpawnResult result)
{
if (result.Result == RequestResultKind.Deferred) return;
if (!_pending.Remove(result.RequestId, out var pending)) return;
if (result.Result is RequestResultKind.Succeeded or RequestResultKind.PartiallySucceeded) _consumed[pending.CandidateId] = pending;
}
public void ResetRevisitState() => _consumed.Clear();
public void CancelPending() => _pending.Clear();
public bool CanPickup(string archetype, string participantId, WorldSnapshot world) => _policy.CanPickupObject(archetype, participantId, world);
public ResourcePopulationState CaptureState() => new(_configurationSignature, _nextRequestId, _random.State, _pending.Values.OrderBy(x => x.RequestId).ToArray(), _consumed.Values.OrderBy(x => x.CandidateId, StringComparer.Ordinal).ToArray(), CapturePolicyState());
public void RestoreState(ResourcePopulationState state)
{
if (!string.Equals(state.ConfigurationSignature, _configurationSignature, StringComparison.Ordinal)) throw new InvalidOperationException("Resource population state does not match the configured rules.");
_nextRequestId = state.NextRequestId; _random = new DeterministicRandom(state.Random); _pending.Clear(); _consumed.Clear();
foreach (var item in state.Pending) _pending[item.RequestId] = item; foreach (var item in state.Consumed) _consumed[item.CandidateId] = item;
if (state.Policy.HasState)
{
if (_policy is not IPersistentResourcePolicy persistent || persistent.PersistenceId != state.Policy.PersistenceId) throw new InvalidOperationException($"Resource policy state '{state.Policy.PersistenceId}' requires the matching {nameof(IPersistentResourcePolicy)} implementation.");
persistent.RestoreResourcePolicyState(state.Policy.Values);
}
}
ExtensionState CapturePolicyState() => _policy is IPersistentResourcePolicy persistent ? new(string.IsNullOrWhiteSpace(persistent.PersistenceId) ? throw new InvalidOperationException("Persistent resource policy requires an id.") : persistent.PersistenceId, persistent.CaptureResourcePolicyState() ?? new Dictionary<string, string>()) : ExtensionState.Empty;
static string Encode(string value) => $"{value.Length}:{value}";
bool ClusterAvailable(ResourceCandidate candidate, ResourceRule rule)
{
if (string.IsNullOrWhiteSpace(candidate.ClusterId) || rule.MaximumPerCluster == int.MaxValue) return true;
var pending = _pending.Values.Count(x => x.Category == rule.Category && x.ClusterId == candidate.ClusterId);
var consumed = _consumed.Values.Count(x => x.Category == rule.Category && x.ClusterId == candidate.ClusterId);
return pending + consumed < rule.MaximumPerCluster;
}
}
Game
library
#nullable enable annotations
using Sandbox;
namespace SboxDirector;
/// <summary>
/// Optional s&box component bridge. Derive from this class and translate your
/// game's entities/navigation into a WorldSnapshot and SpawnRequestResult.
/// </summary>
[Category("Gameplay")]
[Title("Adaptive Director")]
[Icon("neurology")]
public abstract class AdaptiveDirectorComponent : Component
{
[Property] public bool DirectorEnabled { get; set; } = true;
[Property] public int Seed { get; set; } = 1;
[Property] public bool RoundBased { get; set; }
[Property] public bool ResetDirectorOnRoundBegin { get; set; } = true;
public DirectorCore? Director { get; private set; }
public ISessionFlow? Session { get; private set; }
public DirectorDecisionBatch? LastDecisions { get; private set; }
public DirectorRunStatus LastRunStatus { get; private set; } = DirectorRunStatus.SessionInactive;
DirectorHostRunner? _runner;
protected override void OnStart() => InitializeDirector();
protected override void OnFixedUpdate()
{
if (!DirectorEnabled || _runner is null) return;
var result = _runner.Update(); LastRunStatus = result.Status; LastDecisions = result.Decisions;
if (result.Decisions is not null) OnDirectorDecisions(result.Decisions, result.Results);
}
public void InitializeDirector()
{
Director = CreateDirector();
Session = RoundBased ? new RoundSessionFlow() : new ContinuousSessionFlow();
BindRunner();
OnDirectorInitialized(Director);
}
public void BeginRound(string roundId)
{
if (Session is not RoundSessionFlow rounds) throw new InvalidOperationException("This component is not configured as round-based.");
_runner?.Stop();
if (ResetDirectorOnRoundBegin) { Director = CreateDirector(); BindRunner(); OnDirectorInitialized(Director); }
rounds.BeginRound(roundId);
}
public void EndRound()
{
if (Session is not RoundSessionFlow rounds) return;
_runner?.Stop(); rounds.EndRound();
}
public void ReportPressure(PressureEvent input, double time) => _runner?.ReportPressure(input, time);
public string SaveDirectorState() => Director is null ? throw new InvalidOperationException("Director has not initialized.") : DirectorStateCodec.ToJson(Director.CaptureState());
public void LoadDirectorState(string json)
{
_runner?.Stop(); Director = RestoreDirector(DirectorStateCodec.FromJson(json)); BindRunner(); OnDirectorInitialized(Director);
}
protected virtual DirectorConfiguration CreateConfiguration() => new() { Seed = Seed };
protected virtual IDirectorPolicy CreatePolicy() => new DefaultDirectorPolicy();
protected virtual IEncounterSchedule CreateSchedule() => new BasicEncounterSchedule();
protected virtual IPlacementPolicy CreatePlacementPolicy() => new DefaultPlacementPolicy();
protected virtual IEncounterComposer CreateComposer() => new DefaultEncounterComposer();
protected virtual ResourcePopulationController? CreateResourceController() => null;
protected virtual DirectorOrchestration? CreateOrchestration() => null;
protected virtual DirectorOrchestration? RestoreOrchestration(DirectorOrchestrationState? state) => state is null ? CreateOrchestration() : throw new InvalidOperationException("Override RestoreOrchestration when orchestration persistence is enabled.");
protected virtual DirectorCore CreateDirector() => new(CreateConfiguration(), CreatePolicy(), CreateSchedule(), CreatePlacementPolicy(), CreateComposer(), CreateResourceController(), CreateOrchestration());
protected virtual DirectorCore RestoreDirector(DirectorState state) => new(CreateConfiguration(), state, CreatePolicy(), CreateSchedule(), CreatePlacementPolicy(), CreateComposer(), CreateResourceController(), RestoreOrchestration(state.Orchestration));
protected virtual bool HasDirectorAuthority => !IsProxy;
protected virtual void OnDirectorInitialized(DirectorCore director) { }
protected virtual void OnDirectorDecisions(DirectorDecisionBatch decisions, IReadOnlyList<SpawnRequestResult> results) { }
protected abstract WorldSnapshot CaptureDirectorWorld();
protected abstract SpawnRequestResult ExecuteDirectorSpawn(SpawnRequest request);
protected virtual ResourceSpawnResult ExecuteDirectorResourceSpawn(ResourceSpawnRequest request) => new(request.RequestId, RequestResultKind.Rejected, "Override ExecuteDirectorResourceSpawn to enable resource population.");
void BindRunner()
{
if (Director is null || Session is null) throw new InvalidOperationException("Director component is incomplete.");
_runner = new DirectorHostRunner(Director, Session, new DelegateWorldSource(CaptureDirectorWorld), new DelegateSpawnExecutor(ExecuteDirectorSpawn), new ComponentAuthority(this), new DelegateResourceExecutor(ExecuteDirectorResourceSpawn));
}
sealed class ComponentAuthority : IDirectorAuthority
{
readonly AdaptiveDirectorComponent _component;
public ComponentAuthority(AdaptiveDirectorComponent component) { _component = component; }
public bool IsAuthoritative => _component.HasDirectorAuthority;
}
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public sealed record ScenarioStageDefinition(
string Id,
double MinimumDuration = 0,
bool RequiresCombatClear = false,
EncounterKind? Encounter = null,
int EncounterCount = 1,
string? MusicCue = null);
public readonly record struct ScenarioState(ScenarioStatus Status, int StageIndex, double StageStarted);
public sealed class ScenarioController
{
readonly IReadOnlyList<ScenarioStageDefinition> _stages;
public ScenarioStatus Status { get; private set; } = ScenarioStatus.Inactive;
public int StageIndex { get; private set; } = -1;
public double StageStarted { get; private set; }
public ScenarioStageDefinition? Current => StageIndex >= 0 && StageIndex < _stages.Count ? _stages[StageIndex] : null;
public ScenarioController(IReadOnlyList<ScenarioStageDefinition> stages)
{
_stages = stages ?? throw new ArgumentNullException(nameof(stages));
if (_stages.Any(x => string.IsNullOrWhiteSpace(x.Id) || !double.IsFinite(x.MinimumDuration) || x.MinimumDuration < 0 || x.EncounterCount <= 0) || _stages.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != _stages.Count) throw new ArgumentException("Scenario stages are invalid.", nameof(stages));
}
public ScenarioController(IReadOnlyList<ScenarioStageDefinition> stages, ScenarioState state) : this(stages) { Status = state.Status; StageIndex = state.StageIndex; StageStarted = state.StageStarted; if (!double.IsFinite(StageStarted) || StageIndex < -1 || StageIndex > _stages.Count || (Status == ScenarioStatus.Running && Current is null) || (Status == ScenarioStatus.Complete && StageIndex != _stages.Count)) throw new ArgumentException("Scenario state does not match the definition."); }
public void Start(double time) { RequireTime(time); if (_stages.Count == 0) { Status = ScenarioStatus.Complete; StageIndex = 0; StageStarted = time; return; } Status = ScenarioStatus.Running; StageIndex = 0; StageStarted = time; }
public bool TryAdvance(double time, bool combatActive)
{
RequireTime(time); if (time < StageStarted) throw new InvalidOperationException("Scenario time cannot move backwards.");
if (Status != ScenarioStatus.Running || Current is null) return false;
if (time - StageStarted < Current.MinimumDuration || (Current.RequiresCombatClear && combatActive)) return false;
StageIndex++; StageStarted = time; if (StageIndex >= _stages.Count) Status = ScenarioStatus.Complete; return true;
}
public void Fail() { if (Status == ScenarioStatus.Running) Status = ScenarioStatus.Failed; }
public void Reset() { Status = ScenarioStatus.Inactive; StageIndex = -1; StageStarted = 0; }
public ScenarioState Snapshot => new(Status, StageIndex, StageStarted);
static void RequireTime(double time) { if (!double.IsFinite(time)) throw new ArgumentOutOfRangeException(nameof(time)); }
}
public interface ISessionFlow
{
string ModeId { get; }
string? CurrentRoundId { get; }
bool IsSessionActive { get; }
}
public sealed class ContinuousSessionFlow : ISessionFlow
{
public string ModeId { get; init; } = "continuous";
public string? CurrentRoundId => null;
public bool IsSessionActive { get; set; } = true;
}
public sealed class RoundSessionFlow : ISessionFlow
{
public string ModeId { get; init; } = "rounds";
public string? CurrentRoundId { get; private set; }
public bool IsSessionActive => CurrentRoundId is not null;
public void BeginRound(string id) { CurrentRoundId = string.IsNullOrWhiteSpace(id) ? throw new ArgumentException("Round id is required.") : id; }
public void EndRound() => CurrentRoundId = null;
}
Game
library
namespace SboxDirector;
public sealed class PressureConfiguration
{
public float Gain { get; init; } = 0.25f;
public float HoldSeconds { get; init; } = 5f;
public float FullDecaySeconds { get; init; } = 30f;
public float AveragedFollowingSeconds { get; init; } = 20f;
public float LockValue { get; init; } = -1f;
public IReadOnlyList<float> SeverityWeights { get; init; } = new[] { 0.05f, 0.20f, 0.50f, 1.00f, 999999.875f };
public void Validate()
{
if (!float.IsFinite(Gain) || Gain < 0f) throw new ArgumentOutOfRangeException(nameof(Gain));
if (!float.IsFinite(HoldSeconds) || HoldSeconds < 0f) throw new ArgumentOutOfRangeException(nameof(HoldSeconds));
if (!float.IsFinite(FullDecaySeconds) || FullDecaySeconds <= 0f) throw new ArgumentOutOfRangeException(nameof(FullDecaySeconds));
if (!float.IsFinite(AveragedFollowingSeconds) || AveragedFollowingSeconds <= 0f) throw new ArgumentOutOfRangeException(nameof(AveragedFollowingSeconds));
if (SeverityWeights.Count != 5 || SeverityWeights.Any(x => !float.IsFinite(x) || x < 0f))
throw new ArgumentException("Exactly five non-negative severity weights are required.");
}
}
public sealed class TempoConfiguration
{
public double BuildUpMinimumSeconds { get; init; } = 15;
public double SustainPeakSeconds { get; init; } = 10;
public double RelaxMinimumSeconds { get; init; } = 20;
public double RelaxMaximumSeconds { get; init; } = 45;
public float PeakThreshold { get; init; } = 0.9f;
public float RelaxThreshold { get; init; } = 0.9f;
public float RelaxMaximumProgressTravel { get; init; } = 0.15f;
public void Validate()
{
if (!double.IsFinite(BuildUpMinimumSeconds) || !double.IsFinite(SustainPeakSeconds) || !double.IsFinite(RelaxMinimumSeconds) || !double.IsFinite(RelaxMaximumSeconds) || BuildUpMinimumSeconds < 0 || SustainPeakSeconds < 0 || RelaxMinimumSeconds < 0) throw new ArgumentOutOfRangeException("Tempo durations must be finite and non-negative.");
if (RelaxMaximumSeconds < RelaxMinimumSeconds) throw new ArgumentException("RelaxMaximumSeconds must be >= RelaxMinimumSeconds.");
if (!float.IsFinite(PeakThreshold) || !float.IsFinite(RelaxThreshold) || !float.IsFinite(RelaxMaximumProgressTravel) || PeakThreshold is < 0f or > 1f || RelaxThreshold is < 0f or > 1f || RelaxMaximumProgressTravel < 0f) throw new ArgumentOutOfRangeException("Tempo thresholds and travel must be finite and non-negative; thresholds must be in [0,1].");
}
}
public sealed class PlacementProfile
{
public PlacementSelectionMode SelectionMode { get; init; } = PlacementSelectionMode.WeightedScore;
public float MinimumDistance { get; init; } = 500f;
public float MaximumDistance { get; init; } = 2500f;
public float IdealDistance { get; init; } = 1200f;
public float MinimumThreatSeparation { get; init; } = 500f;
public bool AllowVisible { get; init; }
public float DistanceWeight { get; init; } = 1f;
public float CapacityWeight { get; init; } = 0.25f;
public float SeparationWeight { get; init; } = 0.25f;
public float BehindProgressWeight { get; init; }
public float MinimumPathDistance { get; init; }
public float MinimumIngressDistance { get; init; }
public float MinimumClearRadius { get; init; }
public float MinimumAreaWidth { get; init; }
public float MinimumAreaHeight { get; init; }
public float PathDistanceWeight { get; init; }
public float IngressWeight { get; init; }
public float ClearRadiusWeight { get; init; }
public IReadOnlySet<AdvanceRelation> AllowedRelations { get; init; } = new HashSet<AdvanceRelation>();
public PlacementFallbackMode FallbackMode { get; init; } = PlacementFallbackMode.None;
public IReadOnlySet<string> RequiredTags { get; init; } = new HashSet<string>();
public IReadOnlySet<string> ExcludedTags { get; init; } = new HashSet<string>();
public void Validate()
{
var measurements = new[] { MinimumDistance, MaximumDistance, IdealDistance, MinimumThreatSeparation, DistanceWeight, CapacityWeight, SeparationWeight, BehindProgressWeight, MinimumPathDistance, MinimumIngressDistance, MinimumClearRadius, MinimumAreaWidth, MinimumAreaHeight, PathDistanceWeight, IngressWeight, ClearRadiusWeight };
if (measurements.Any(x => !float.IsFinite(x)) || MinimumDistance < 0 || MaximumDistance < MinimumDistance || IdealDistance < MinimumDistance || IdealDistance > MaximumDistance || MinimumThreatSeparation < 0 || MinimumPathDistance < 0 || MinimumIngressDistance < 0 || MinimumClearRadius < 0 || MinimumAreaWidth < 0 || MinimumAreaHeight < 0) throw new ArgumentException("Invalid placement distance or area profile.");
if (RequiredTags.Overlaps(ExcludedTags)) throw new ArgumentException("Placement tags cannot be both required and excluded.");
}
}
public sealed class PopulationConfiguration
{
public IReadOnlyDictionary<EncounterKind, int> Limits { get; init; } = new Dictionary<EncounterKind, int>
{
[EncounterKind.Ambient] = 30,
[EncounterKind.CommonWave] = 30,
[EncounterKind.Special] = 4,
[EncounterKind.Boss] = 1,
[EncounterKind.DormantHazard] = 1,
[EncounterKind.Objective] = 8,
[EncounterKind.Custom] = 16
};
}
public sealed class DirectorConfiguration
{
public string Version { get; init; } = "1.2";
public PressureConfiguration Pressure { get; init; } = new();
public TempoConfiguration Tempo { get; init; } = new();
public PopulationConfiguration Population { get; init; } = new();
public IReadOnlyDictionary<EncounterKind, PlacementProfile> Placement { get; init; } = DefaultPlacement();
public int Seed { get; init; } = 1;
public double RequestTimeoutSeconds { get; init; } = 15;
public int MaximumRequestsPerTick { get; init; } = 1;
public bool ValidateWorldSnapshots { get; init; } = true;
public RetryConfiguration Retry { get; init; } = new();
public void Validate()
{
Pressure.Validate(); Tempo.Validate();
if (string.IsNullOrWhiteSpace(Version)) throw new ArgumentException("A configuration version is required.", nameof(Version));
if (RequestTimeoutSeconds <= 0) throw new ArgumentOutOfRangeException(nameof(RequestTimeoutSeconds));
if (MaximumRequestsPerTick <= 0) throw new ArgumentOutOfRangeException(nameof(MaximumRequestsPerTick));
Retry.Validate();
foreach (var pair in Population.Limits) if (pair.Value < 0) throw new ArgumentOutOfRangeException($"Negative population limit for {pair.Key}.");
foreach (var profile in Placement.Values) profile.Validate();
}
static IReadOnlyDictionary<EncounterKind, PlacementProfile> DefaultPlacement() => new Dictionary<EncounterKind, PlacementProfile>
{
[EncounterKind.Ambient] = new() { MinimumDistance = 300f, IdealDistance = 800f, MaximumDistance = 1800f },
[EncounterKind.CommonWave] = new(),
[EncounterKind.Special] = new() { MinimumDistance = 600f, IdealDistance = 1400f },
[EncounterKind.Boss] = new() { MinimumDistance = 900f, IdealDistance = 1800f, MinimumThreatSeparation = 1200f },
[EncounterKind.DormantHazard] = new() { MinimumDistance = 700f, IdealDistance = 1500f, MinimumThreatSeparation = 1000f },
[EncounterKind.Objective] = new() { AllowVisible = true, MinimumDistance = 0f, MaximumDistance = 5000f },
[EncounterKind.Custom] = new()
};
}
public sealed class RetryConfiguration
{
public int MaximumAttempts { get; init; } = 3;
public double DelaySeconds { get; init; } = 1;
public bool RetryRejected { get; init; } = true;
public bool RetryFailed { get; init; } = true;
public bool RetryPartialRemainder { get; init; } = true;
public void Validate()
{
if (MaximumAttempts <= 0 || !double.IsFinite(DelaySeconds) || DelaySeconds < 0) throw new ArgumentException("Invalid request retry configuration.");
}
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public sealed record PlacementChoice(SpawnCandidate Candidate, float Score, bool IsFallback = false);
public interface IPlacementPolicy
{
bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world);
float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world);
}
/// <summary>Implement this alongside IPlacementPolicy when a custom placement policy owns mutable deterministic state.</summary>
public interface IPersistentPlacementPolicy
{
string PersistenceId { get; }
IReadOnlyDictionary<string, string> CapturePlacementState();
void RestorePlacementState(IReadOnlyDictionary<string, string> state);
}
public sealed class DefaultPlacementPolicy : IPlacementPolicy
{
public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => true;
public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0f;
}
public sealed class PlacementSelector
{
public PlacementChoice? Select(IReadOnlyList<SpawnCandidate> candidates, PlacementProfile profile, DeterministicRandom random, WorldSnapshot? world = null, IPlacementPolicy? policy = null)
{
var ordered = OrderCandidates(candidates, profile.SelectionMode, random);
var strict = BuildChoices(ordered, profile, world, policy, profile.SelectionMode, ignoreRoute: false, broadFallback: false);
if (strict.Count > 0) return Choose(strict, profile.SelectionMode, random, false);
if (profile.FallbackMode == PlacementFallbackMode.None) return null;
ordered = OrderCandidates(candidates, profile.SelectionMode, random);
var fallback = BuildChoices(ordered, profile, world, policy, profile.SelectionMode, ignoreRoute: true, broadFallback: profile.FallbackMode == PlacementFallbackMode.AnyReachableHidden);
return fallback.Count == 0 ? null : Choose(fallback, profile.SelectionMode, random, true);
}
static List<PlacementChoice> BuildChoices(IReadOnlyList<SpawnCandidate> candidates, PlacementProfile profile, WorldSnapshot? world, IPlacementPolicy? policy, PlacementSelectionMode mode, bool ignoreRoute, bool broadFallback)
{
var choices = new List<PlacementChoice>();
foreach (var candidate in candidates)
{
if (!candidate.Reachable || !candidate.Spawnable || (!profile.AllowVisible && candidate.Visible)) continue;
if (candidate.AreaWidth < profile.MinimumAreaWidth || candidate.AreaHeight < profile.MinimumAreaHeight) continue;
if (!broadFallback && (candidate.NearestParticipantDistance < profile.MinimumDistance || candidate.NearestParticipantDistance > profile.MaximumDistance)) continue;
if (!broadFallback && candidate.ThreatSeparation < profile.MinimumThreatSeparation) continue;
if (profile.RequiredTags.Any(tag => !candidate.Tags.Contains(tag)) || profile.ExcludedTags.Any(candidate.Tags.Contains)) continue;
if (!ignoreRoute && (candidate.PathDistance < profile.MinimumPathDistance || candidate.IngressDistance < profile.MinimumIngressDistance || candidate.ClearRadius < profile.MinimumClearRadius)) continue;
if (!ignoreRoute && profile.AllowedRelations.Count > 0 && !profile.AllowedRelations.Contains(candidate.AdvanceRelation)) continue;
if (world is not null && policy is not null && !policy.IsAllowed(candidate, profile, world)) continue;
var score = mode == PlacementSelectionMode.HighestProgressFirstTie ? candidate.Progress : 0f;
if (mode == PlacementSelectionMode.WeightedScore)
{
var distanceRange = Math.Max(1f, profile.MaximumDistance - profile.MinimumDistance);
var distanceScore = 1f - Math.Min(1f, Math.Abs(candidate.NearestParticipantDistance - profile.IdealDistance) / distanceRange);
score = distanceScore * profile.DistanceWeight + candidate.AreaCapacity * profile.CapacityWeight + Math.Min(1f, candidate.ThreatSeparation / Math.Max(1f, profile.MinimumThreatSeparation * 2f)) * profile.SeparationWeight;
if (candidate.RelativeProgress < 0) score += Math.Min(1f, -candidate.RelativeProgress) * profile.BehindProgressWeight;
score += Math.Min(1f, candidate.PathDistance / Math.Max(1f, profile.MinimumPathDistance * 2f)) * profile.PathDistanceWeight;
score += Math.Min(1f, candidate.IngressDistance / Math.Max(1f, profile.MinimumIngressDistance * 2f)) * profile.IngressWeight;
score += Math.Min(1f, candidate.ClearRadius / Math.Max(1f, profile.MinimumClearRadius * 2f)) * profile.ClearRadiusWeight;
if (world is not null && policy is not null)
{
var adjustment = policy.ScoreAdjustment(candidate, profile, world); if (!float.IsFinite(adjustment)) throw new InvalidOperationException("Placement policy returned a non-finite score adjustment."); score += adjustment;
}
}
if (!float.IsFinite(score)) throw new InvalidOperationException("Placement scoring produced a non-finite value.");
choices.Add(new PlacementChoice(candidate, score));
}
return choices;
}
static IReadOnlyList<SpawnCandidate> OrderCandidates(IReadOnlyList<SpawnCandidate> candidates, PlacementSelectionMode mode, DeterministicRandom random)
{
if (mode != PlacementSelectionMode.NativeRandomTranspositionFirst) return candidates;
var shuffled = candidates.ToArray();
for (var index = 0; index < shuffled.Length; index++)
{
var other = random.NextInt(shuffled.Length);
(shuffled[index], shuffled[other]) = (shuffled[other], shuffled[index]);
}
return shuffled;
}
static PlacementChoice Choose(List<PlacementChoice> choices, PlacementSelectionMode mode, DeterministicRandom random, bool fallback)
{
if (mode is PlacementSelectionMode.FirstEligible or PlacementSelectionMode.NativeRandomTranspositionFirst) return choices[0] with { IsFallback = fallback };
if (mode == PlacementSelectionMode.HighestProgressFirstTie)
{
var progressChoice = choices[0];
for (var index = 1; index < choices.Count; index++) if (choices[index].Candidate.Progress > progressChoice.Candidate.Progress) progressChoice = choices[index];
return progressChoice with { IsFallback = fallback };
}
var best = choices.Max(x => x.Score); var tied = choices.Where(x => Math.Abs(x.Score - best) < 0.00001f).ToArray();
var selected = tied[random.NextInt(tied.Length)]; return selected with { IsFallback = fallback };
}
}
Game
library
namespace SboxDirector;
public sealed class WaveSequenceConfiguration
{
public int WaveCount { get; init; } = 1;
public double InitialDelayMinimum { get; init; } = 1;
public double InitialDelayMaximum { get; init; } = 2;
public double CombatTimeout { get; init; } = 10;
public double PauseMinimum { get; init; } = 5;
public double PauseMaximum { get; init; } = 10;
public void Validate()
{
var durations = new[] { InitialDelayMinimum, InitialDelayMaximum, CombatTimeout, PauseMinimum, PauseMaximum };
if (WaveCount <= 0 || durations.Any(x => !double.IsFinite(x)) || InitialDelayMinimum < 0 || InitialDelayMaximum < InitialDelayMinimum || CombatTimeout < 0 || PauseMinimum < 0 || PauseMaximum < PauseMinimum) throw new ArgumentException("Invalid wave sequence configuration.");
}
}
public readonly record struct WaveSequenceState(WaveState State, int IssuedWaves, int TargetWaves, double Deadline);
public readonly record struct WaveAction(bool IssueWave, bool Completed, string Reason);
public sealed class WaveSequenceController
{
readonly WaveSequenceConfiguration _configuration; readonly DeterministicRandom _random;
public WaveState State { get; private set; } = WaveState.Inactive;
public int IssuedWaves { get; private set; }
public int TargetWaves { get; private set; }
public double Deadline { get; private set; }
public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random) { configuration.Validate(); _configuration = configuration; _random = random; }
public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random, WaveSequenceState state) : this(configuration, random) { State = state.State; IssuedWaves = state.IssuedWaves; TargetWaves = state.TargetWaves; Deadline = state.Deadline; if (!double.IsFinite(Deadline) || IssuedWaves < 0 || TargetWaves < 0 || IssuedWaves > TargetWaves) throw new ArgumentException("Wave state is invalid.", nameof(state)); }
public WaveSequenceState Snapshot => new(State, IssuedWaves, TargetWaves, Deadline);
public void Start(double time, int? waveCount = null) { RequireTime(time); TargetWaves = waveCount ?? _configuration.WaveCount; if (TargetWaves <= 0) throw new ArgumentOutOfRangeException(nameof(waveCount)); IssuedWaves = 0; State = WaveState.InitialDelay; Deadline = time + Range(_configuration.InitialDelayMinimum, _configuration.InitialDelayMaximum); }
public void Cancel() { State = WaveState.Inactive; IssuedWaves = 0; TargetWaves = 0; Deadline = 0; }
public WaveAction Update(double time, bool relevantCombatantsRemain)
{
RequireTime(time);
if (State is WaveState.Inactive or WaveState.Done) return new(false, State == WaveState.Done, "inactive or complete");
if (time < Deadline) return new(false, false, "waiting for timer");
if (State == WaveState.InitialDelay) { State = WaveState.IssueWave; return new(false, false, "initial delay expired"); }
if (State == WaveState.IssueWave) { IssuedWaves++; State = WaveState.WaitForClear; Deadline = time + _configuration.CombatTimeout; return new(true, false, "wave issued"); }
if (State == WaveState.WaitForClear)
{
if (relevantCombatantsRemain) { Deadline = time + _configuration.CombatTimeout; return new(false, false, "combat remains"); }
if (IssuedWaves >= TargetWaves) { State = WaveState.Done; return new(false, true, "target waves issued and combat clear"); }
State = WaveState.Pause; Deadline = time + Range(_configuration.PauseMinimum, _configuration.PauseMaximum); return new(false, false, "pausing before next wave");
}
if (State == WaveState.Pause) { State = WaveState.IssueWave; return new(false, false, "pause expired"); }
return new(false, false, "no action");
}
double Range(double minimum, double maximum) => minimum + (maximum - minimum) * _random.NextFloat();
static void RequireTime(double time) { if (!double.IsFinite(time)) throw new ArgumentOutOfRangeException(nameof(time)); }
}
Game
library
using SboxDirector;
namespace AdaptiveDirectorExamples;
public sealed record ExampleSaveDocument(string DirectorJson, string MusicAuthorityJson, string MusicRuntimeJson);
/// <summary>Persists encounter direction, authoritative music, and client/local track queues together.</summary>
public sealed class SaveRestoreEverythingExample
{
readonly DirectorConfiguration _directorConfiguration = new() { Version = "save-example-v1", Seed = 42 };
readonly MusicDirectorConfiguration _musicConfiguration = RecoveredMusicDirectorDefaults.CreateConfiguration(ExampleMusicSetup.CreateCues(), ExampleMusicSetup.CreateSpecialAlerts(), seed: 42);
DirectorCore _director;
MusicDirectorCore _music;
MusicRuntime _runtime;
public SaveRestoreEverythingExample(string participantId)
{
ParticipantId = participantId; _director = new DirectorCore(_directorConfiguration); _music = new MusicDirectorCore(_musicConfiguration); _runtime = new MusicRuntime(participantId, ExampleMusicSetup.CreateCatalog());
}
public string ParticipantId { get; }
public DirectorDecisionBatch TickDirector(WorldSnapshot world) => _director.Tick(world);
public void ApplySpawnResult(SpawnRequestResult result) => _director.ApplyResult(result);
public void ReportPressure(PressureEvent pressure, double time) => _director.ApplyPressure(pressure, time);
public MusicRuntimeBatch TickMusic(MusicWorldSnapshot world)
{
var decisions = _music.Tick(world); return _runtime.Apply(decisions);
}
public ExampleSaveDocument Save() => new(
DirectorStateCodec.ToJson(_director.CaptureState()),
MusicStateCodec.DirectorToJson(_music.CaptureState()),
MusicStateCodec.RuntimeToJson(_runtime.CaptureState()));
public void Load(ExampleSaveDocument save)
{
var directorState = DirectorStateCodec.FromJson(save.DirectorJson);
var musicState = MusicStateCodec.DirectorFromJson(save.MusicAuthorityJson);
var runtimeState = MusicStateCodec.RuntimeFromJson(save.MusicRuntimeJson);
_director = new DirectorCore(_directorConfiguration, directorState);
_music = new MusicDirectorCore(_musicConfiguration, musicState);
_runtime = new MusicRuntime(ParticipantId, ExampleMusicSetup.CreateCatalog(), runtimeState);
}
}
Game
library
#nullable enable annotations
namespace SboxDirector;
public sealed class ScheduleLane
{
public string Id { get; init; } = "lane";
public int Priority { get; init; }
public IEncounterSchedule Schedule { get; init; } = null!;
public Func<WorldSnapshot, bool>? Enabled { get; init; }
}
/// <summary>
/// Runs independent encounter lanes during one authoritative tick. This mirrors the proven separation
/// of survival lull, horde, special, and boss schedules without copying their unrecovered formulas.
/// </summary>
public sealed class ConcurrentEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback
{
readonly IReadOnlyList<ScheduleLane> _lanes;
readonly HashSet<string> _served = new(StringComparer.Ordinal);
readonly string _signature;
ScheduleLane? _selected;
double _servedTime = double.NaN;
public ConcurrentEncounterSchedule(IEnumerable<ScheduleLane> lanes)
{
_lanes = lanes.OrderByDescending(x => x.Priority).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();
if (_lanes.Count == 0 || _lanes.Any(x => x.Schedule is null || string.IsNullOrWhiteSpace(x.Id) || x.Id.Contains(':'))) throw new ArgumentException("Concurrent schedule lanes require unique non-empty ids without colons and a schedule.");
if (_lanes.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != _lanes.Count) throw new ArgumentException("Concurrent schedule lane ids must be unique.");
_signature = string.Join("|", _lanes.Select(x => $"{x.Id.Length}:{x.Id}:{x.Priority}"));
}
public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)
{
_selected = null;
if (_servedTime != world.Time) { _servedTime = world.Time; _served.Clear(); }
foreach (var lane in _lanes)
{
if (_served.Contains(lane.Id) || (lane.Enabled is not null && !lane.Enabled(world))) continue;
var kind = lane.Schedule.SelectEncounter(world, tempo, teamPressure, population, random);
if (kind is null) continue;
_selected = lane; _served.Add(lane.Id); return kind;
}
return null;
}
public int RequestedCount(EncounterKind kind, WorldSnapshot world) => _selected?.Schedule.RequestedCount(kind, world) ?? 1;
public void SelectionFinalized(bool requestCreated)
{
if (_selected is null) return;
if (_selected.Schedule is IEncounterScheduleFeedback feedback) feedback.SelectionFinalized(requestCreated);
if (!requestCreated) _served.Remove(_selected.Id);
_selected = null;
}
public IReadOnlyDictionary<string, string> CaptureScheduleState()
{
var state = new Dictionary<string, string> { ["coordinator.signature"] = _signature, ["coordinator.time"] = _servedTime.ToString("R", CultureInfo.InvariantCulture), ["coordinator.served"] = string.Join("|", _served.OrderBy(x => x, StringComparer.Ordinal)) };
foreach (var lane in _lanes)
if (lane.Schedule is IPersistentEncounterSchedule persistent)
foreach (var pair in persistent.CaptureScheduleState()) state[$"lane:{lane.Id}:{pair.Key}"] = pair.Value;
return state;
}
public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)
{
_served.Clear();
if (!state.TryGetValue("coordinator.signature", out var signature) || !string.Equals(signature, _signature, StringComparison.Ordinal)) throw new InvalidOperationException("Concurrent schedule state does not match its configured lanes.");
if (state.TryGetValue("coordinator.time", out var time) && double.TryParse(time, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) _servedTime = parsed;
if (state.TryGetValue("coordinator.served", out var served)) foreach (var id in served.Split('|', StringSplitOptions.RemoveEmptyEntries)) _served.Add(id);
foreach (var lane in _lanes)
{
if (lane.Schedule is not IPersistentEncounterSchedule persistent) continue;
var prefix = $"lane:{lane.Id}:";
persistent.RestoreScheduleState(state.Where(x => x.Key.StartsWith(prefix, StringComparison.Ordinal)).ToDictionary(x => x.Key[prefix.Length..], x => x.Value));
}
}
}
Game
library
namespace SboxDirector;
public sealed class PopulationLedger
{
readonly PopulationConfiguration _configuration;
readonly Dictionary<EncounterKind, int> _reserved = new();
readonly Dictionary<long, PendingReservation> _pending = new();
public PopulationLedger(PopulationConfiguration configuration) { _configuration = configuration; }
public int Available(EncounterKind kind, PopulationSnapshot population)
=> Available(kind, population, ConfiguredLimit(kind));
public int ConfiguredLimit(EncounterKind kind) => _configuration.Limits.TryGetValue(kind, out var configured) ? configured : 0;
public int Available(EncounterKind kind, PopulationSnapshot population, int limit)
{
return Math.Max(0, limit - population[kind] - (_reserved.TryGetValue(kind, out var value) ? value : 0));
}
public bool TryReserve(long requestId, EncounterKind kind, int count, PopulationSnapshot population, double expiresAt = double.PositiveInfinity)
=> TryReserveWithLimit(requestId, kind, count, population, ConfiguredLimit(kind), expiresAt);
public bool TryReserveWithLimit(long requestId, EncounterKind kind, int count, PopulationSnapshot population, int limit, double expiresAt = double.PositiveInfinity)
{
if (count <= 0 || limit < 0 || _pending.ContainsKey(requestId) || Available(kind, population, limit) < count) return false;
_pending[requestId] = new(requestId, kind, count, expiresAt); _reserved[kind] = (_reserved.TryGetValue(kind, out var value) ? value : 0) + count; return true;
}
public void Resolve(SpawnRequestResult result)
{
if (result.Result == RequestResultKind.Deferred) return;
if (!_pending.Remove(result.RequestId, out var pending)) return;
_reserved[pending.Kind] = Math.Max(0, _reserved[pending.Kind] - pending.Count);
}
public IReadOnlyList<long> CancelAll()
{
var ids = _pending.Keys.OrderBy(x => x).ToArray(); _pending.Clear(); _reserved.Clear(); return ids;
}
public IReadOnlyList<long> Expire(double time)
{
var expired = _pending.Values.Where(x => x.ExpiresAt <= time).Select(x => x.RequestId).ToArray();
foreach (var id in expired) Resolve(new SpawnRequestResult(id, RequestResultKind.Failed, 0, "request timeout"));
return expired;
}
public PopulationLedgerState Capture() => new(_pending.Values.OrderBy(x => x.RequestId).ToArray());
public void Restore(PopulationLedgerState state)
{
_pending.Clear(); _reserved.Clear();
foreach (var item in state.Pending) { _pending[item.RequestId] = item; _reserved[item.Kind] = (_reserved.TryGetValue(item.Kind, out var count) ? count : 0) + item.Count; }
}
public IReadOnlyDictionary<EncounterKind, int> Reservations => _reserved;
public IReadOnlyList<PendingReservation> Pending => _pending.Values.OrderBy(x => x.RequestId).ToArray();
}
public readonly record struct PendingReservation(long RequestId, EncounterKind Kind, int Count, double ExpiresAt);
public readonly record struct PopulationLedgerState(IReadOnlyList<PendingReservation> Pending);
Debug: View Raw JSON Response
{
"TotalCount": 71,
"Files": [
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/PopulationLedger.cs",
"FileName": "PopulationLedger.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "namespace SboxDirector;\n\npublic sealed class PopulationLedger\n{\n readonly PopulationConfiguration _configuration;\n readonly Dictionary<EncounterKind, int> _reserved = new();\n readonly Dictionary<long, PendingReservation> _pending = new();\n public PopulationLedger(PopulationConfiguration configuration) { _configuration = configuration; }\n\n public int Available(EncounterKind kind, PopulationSnapshot population)\n => Available(kind, population, ConfiguredLimit(kind));\n public int ConfiguredLimit(EncounterKind kind) => _configuration.Limits.TryGetValue(kind, out var configured) ? configured : 0;\n public int Available(EncounterKind kind, PopulationSnapshot population, int limit)\n {\n return Math.Max(0, limit - population[kind] - (_reserved.TryGetValue(kind, out var value) ? value : 0));\n }\n public bool TryReserve(long requestId, EncounterKind kind, int count, PopulationSnapshot population, double expiresAt = double.PositiveInfinity)\n => TryReserveWithLimit(requestId, kind, count, population, ConfiguredLimit(kind), expiresAt);\n public bool TryReserveWithLimit(long requestId, EncounterKind kind, int count, PopulationSnapshot population, int limit, double expiresAt = double.PositiveInfinity)\n {\n if (count <= 0 || limit < 0 || _pending.ContainsKey(requestId) || Available(kind, population, limit) < count) return false;\n _pending[requestId] = new(requestId, kind, count, expiresAt); _reserved[kind] = (_reserved.TryGetValue(kind, out var value) ? value : 0) + count; return true;\n }\n public void Resolve(SpawnRequestResult result)\n {\n if (result.Result == RequestResultKind.Deferred) return;\n if (!_pending.Remove(result.RequestId, out var pending)) return;\n _reserved[pending.Kind] = Math.Max(0, _reserved[pending.Kind] - pending.Count);\n }\n public IReadOnlyList<long> CancelAll()\n {\n var ids = _pending.Keys.OrderBy(x => x).ToArray(); _pending.Clear(); _reserved.Clear(); return ids;\n }\n public IReadOnlyList<long> Expire(double time)\n {\n var expired = _pending.Values.Where(x => x.ExpiresAt <= time).Select(x => x.RequestId).ToArray();\n foreach (var id in expired) Resolve(new SpawnRequestResult(id, RequestResultKind.Failed, 0, \"request timeout\"));\n return expired;\n }\n public PopulationLedgerState Capture() => new(_pending.Values.OrderBy(x => x.RequestId).ToArray());\n public void Restore(PopulationLedgerState state)\n {\n _pending.Clear(); _reserved.Clear();\n foreach (var item in state.Pending) { _pending[item.RequestId] = item; _reserved[item.Kind] = (_reserved.TryGetValue(item.Kind, out var count) ? count : 0) + item.Count; }\n }\n public IReadOnlyDictionary<EncounterKind, int> Reservations => _reserved;\n public IReadOnlyList<PendingReservation> Pending => _pending.Values.OrderBy(x => x.RequestId).ToArray();\n}\n\npublic readonly record struct PendingReservation(long RequestId, EncounterKind Kind, int Count, double ExpiresAt);\npublic readonly record struct PopulationLedgerState(IReadOnlyList<PendingReservation> Pending);\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "examples/PressureReportingExample.cs",
"FileName": "PressureReportingExample.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "using SboxDirector;\n\nnamespace AdaptiveDirectorExamples;\n\n/// <summary>\n/// Centralizes the conversion from game events to the Director's five pressure\n/// severities. Call this service only from authoritative gameplay code.\n/// </summary>\npublic sealed class PressureReportingExample\n{\n readonly Action<PressureEvent, double> _report;\n\n public PressureReportingExample(Action<PressureEvent, double> report)\n {\n _report = report ?? throw new ArgumentNullException(nameof(report));\n }\n\n public static PressureReportingExample ForDirector(DirectorCore director) => new(director.ApplyPressure);\n public static PressureReportingExample ForHostRunner(DirectorHostRunner runner) => new(runner.ReportPressure);\n\n /// <summary>normalizedDamage is damage divided by the participant's full health.</summary>\n public void OnDamage(string participantId, float normalizedDamage, double gameTime)\n {\n if (!float.IsFinite(normalizedDamage) || normalizedDamage < 0) throw new ArgumentOutOfRangeException(nameof(normalizedDamage));\n var severity = normalizedDamage switch\n {\n < .20f => PressureSeverity.Moderate,\n < .50f => PressureSeverity.Major,\n _ => PressureSeverity.Critical\n };\n Report(participantId, severity, \"damage\", gameTime);\n }\n\n public void OnLedgeDanger(string participantId, double gameTime) => Report(participantId, PressureSeverity.Major, \"ledge_danger\", gameTime);\n\n public void OnIncapacitated(string participantId, int previousReviveCount, double gameTime)\n {\n if (previousReviveCount < 0) throw new ArgumentOutOfRangeException(nameof(previousReviveCount));\n Report(participantId, previousReviveCount == 0 ? PressureSeverity.Maximum : PressureSeverity.Critical, \"incapacitated\", gameTime);\n }\n\n /// <summary>Compatibility behavior: only cooperative Easy/Normal revival reaches maximum pressure.</summary>\n public void OnDefibrillatorRevived(string participantId, string baseMode, string difficulty, double gameTime)\n {\n var compatible = string.Equals(baseMode, \"coop\", StringComparison.OrdinalIgnoreCase)\n && (string.Equals(difficulty, \"easy\", StringComparison.OrdinalIgnoreCase)\n || string.Equals(difficulty, \"normal\", StringComparison.OrdinalIgnoreCase));\n if (compatible) Report(participantId, PressureSeverity.Maximum, \"defibrillator_revival\", gameTime);\n }\n\n /// <summary>Maps the recovered 150/500-unit proximity bands. Set typedThreat for special/nonzero threat types.</summary>\n public void OnThreatKilledNearby(string participantId, float distance, bool bossOrDormantHazard, bool typedThreat, float ordinaryThreatSpeed, double gameTime)\n {\n if (!float.IsFinite(distance) || distance < 0 || !float.IsFinite(ordinaryThreatSpeed) || ordinaryThreatSpeed < 0) throw new ArgumentOutOfRangeException(nameof(distance));\n if (distance > 500) return;\n\n PressureSeverity severity;\n if (bossOrDormantHazard) severity = distance <= 150 ? PressureSeverity.Critical : PressureSeverity.Major;\n else if (typedThreat) severity = distance <= 150 ? PressureSeverity.Moderate : PressureSeverity.Minor;\n else severity = distance <= 150 && ordinaryThreatSpeed > 150 ? PressureSeverity.Moderate : PressureSeverity.Minor;\n Report(participantId, severity, \"nearby_threat_death\", gameTime);\n }\n\n /// <summary>Use custom mappings for game-specific events while keeping them in one auditable place.</summary>\n public void OnCustomDanger(string participantId, PressureSeverity severity, string reason, double gameTime) => Report(participantId, severity, reason, gameTime);\n\n void Report(string participantId, PressureSeverity severity, string reason, double gameTime)\n {\n if (string.IsNullOrWhiteSpace(participantId)) throw new ArgumentException(\"A participant id is required.\", nameof(participantId));\n if (!double.IsFinite(gameTime)) throw new ArgumentOutOfRangeException(nameof(gameTime));\n _report(new PressureEvent(participantId, severity, reason), gameTime);\n }\n}\n\n/// <summary>Example diagnostic wiring for a developer HUD or server log.</summary>\npublic sealed class DirectorDiagnosticsExample\n{\n readonly DirectorTelemetryBuffer _telemetry;\n public DirectorDiagnosticsExample(DirectorCore director) { _telemetry = new DirectorTelemetryBuffer(director, capacity: 200); }\n public IReadOnlyList<DirectorDiagnostic> ReadForDebugHud() => _telemetry.Snapshot();\n public void Clear() => _telemetry.Clear();\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/MusicModels.cs",
"FileName": "MusicModels.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic enum MusicDecisionKind { SetMixerLayer, Play, StopEvent, StopTrack, StopAll, General }\npublic enum MusicPriority { Low = 1, Medium = 2, High = 3, Critical = 4 }\npublic enum MusicTriggerKind { Play, StopEvent, StopTrack, StopAll, General, Reset, ScenarioEnded, ParticipantRestored, AmbientMob, MobSpawn, MobSpawnBehind, LargeAreaRevealed, LandmarkRevealed }\npublic enum MusicRuntimeActionKind { Play, StopEvent, StopTrack, StopAll, SetTrackVolume, SetMixerLayer, General }\npublic enum MusicDecisionResultKind { Accepted, Deferred, RejectedUnknownEvent, RejectedBlocked, RejectedLifecycle, RejectedPriority, FailedAdapter }\n\n[Flags]\npublic enum MusicMasterFlags\n{\n None = 0,\n PlayToEnd = 1,\n PlaySplit = 2,\n DontDisengage = 4,\n DontEngage = 8,\n AllowAfterDeath = 16,\n AllowAfterScenarioEnd = 32\n}\n\npublic static class MusicMixerLayers\n{\n public const string MobBeating = \"mob_beating\";\n public const string MobRules = \"mob_rules\";\n public const string HazardRage = \"hazard_rage\";\n public const string Combat = \"combat\";\n public const string CombatSecondary = \"combat_secondary\";\n public const string CombatClose = \"combat_close\";\n public const string Adrenaline = \"adrenaline\";\n public const string Checkpoint = \"checkpoint\";\n public const string Ambient = \"ambient\";\n}\n\npublic sealed record SpecialMusicThreat(string ArchetypeId, float Distance, string EmitterId = \"\", DirectorVector? Position = null);\npublic sealed record PositionalMusicCandidate(string Id, DirectorVector Position, bool Eligible = true);\npublic sealed record WanderingMusicHazard(string Id, float Anger, DirectorVector Position);\n\npublic sealed class MusicParticipantSnapshot\n{\n public string Id { get; init; } = \"\";\n public bool IsEligible { get; init; } = true;\n public bool IsAlive { get; init; } = true;\n public bool IsIncapacitated { get; init; }\n public bool DynamicMusicAllowed { get; init; } = true;\n public float CumulativeDamage { get; init; }\n public float WeaponActivity { get; init; }\n public bool InflictedDamage { get; init; }\n public int VisibleCommonThreats { get; init; }\n public int ActiveCommonThreats { get; init; }\n public int ScannedCommonThreats { get; init; }\n public float AttackingCommonFactor { get; init; }\n public float CloseCommonAction { get; init; }\n public float VeryCloseCommonFactor { get; init; }\n public bool MobPressureActive { get; init; }\n public bool GlobalCombatActive { get; init; }\n public bool BossPresent { get; init; }\n public string BossArchetypeId { get; init; } = \"\";\n public bool HazardPresent { get; init; }\n public bool HazardAttacking { get; init; }\n public bool HazardBurning { get; init; }\n public float HazardRage { get; init; }\n public string HazardEmitterId { get; init; } = \"\";\n public DirectorVector? HazardPosition { get; init; }\n public float Adrenaline { get; init; }\n public bool InCheckpoint { get; init; }\n public IReadOnlyList<SpecialMusicThreat> SpecialThreats { get; init; } = Array.Empty<SpecialMusicThreat>();\n public IReadOnlyList<PositionalMusicCandidate> PositionalCandidates { get; init; } = Array.Empty<PositionalMusicCandidate>();\n public IReadOnlyList<WanderingMusicHazard> WanderingHazards { get; init; } = Array.Empty<WanderingMusicHazard>();\n\n public void Validate()\n {\n if (string.IsNullOrWhiteSpace(Id)) throw new ArgumentException(\"A music participant id is required.\");\n var values = new[] { CumulativeDamage, WeaponActivity, AttackingCommonFactor, CloseCommonAction, VeryCloseCommonFactor, HazardRage, Adrenaline };\n if (values.Any(x => !float.IsFinite(x)) || CumulativeDamage < 0 || VisibleCommonThreats < 0 || ActiveCommonThreats < 0 || ScannedCommonThreats < 0) throw new ArgumentException(\"Music participant measurements must be finite and non-negative.\");\n if (WeaponActivity is < 0 or > 1 || AttackingCommonFactor is < 0 or > 1 || CloseCommonAction is < 0 or > 1 || VeryCloseCommonFactor is < 0 or > 1 || HazardRage is < 0 or > 1 || Adrenaline is < 0 or > 1) throw new ArgumentException(\"Normalized music inputs must be in [0,1].\");\n if (SpecialThreats.Any(x => string.IsNullOrWhiteSpace(x.ArchetypeId) || !float.IsFinite(x.Distance) || x.Distance < 0)) throw new ArgumentException(\"Special music threats require an archetype and finite non-negative distance.\");\n if (PositionalCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id))) throw new ArgumentException(\"Positional music candidates require stable ids.\");\n if (WanderingHazards.Any(x => string.IsNullOrWhiteSpace(x.Id) || !float.IsFinite(x.Anger) || x.Anger is < 0 or > 1)) throw new ArgumentException(\"Wandering music hazards require stable ids and anger in [0,1].\");\n }\n}\n\npublic sealed class MusicWorldSnapshot\n{\n public double Time { get; init; }\n public IReadOnlyList<MusicParticipantSnapshot> Participants { get; init; } = Array.Empty<MusicParticipantSnapshot>();\n public bool ScenarioEnded { get; init; }\n\n public void Validate()\n {\n if (!double.IsFinite(Time)) throw new ArgumentException(\"Music world time must be finite.\");\n foreach (var participant in Participants) participant.Validate();\n if (Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException(\"Music participant ids must be unique.\");\n }\n}\n\npublic sealed record MusicGameplayEvent(long Sequence, MusicTriggerKind Kind, string ParticipantId = \"\", string TargetId = \"\", float FadeSeconds = 0, string EmitterId = \"\", DirectorVector? Position = null)\n{\n public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();\n}\n\npublic sealed record MusicDecision(long DecisionId, MusicDecisionKind Kind, string ParticipantId, string TargetId, string Reason)\n{\n public string LayerId { get; init; } = \"\";\n public float Value { get; init; }\n public float FadeSeconds { get; init; }\n public float StartOffsetSeconds { get; init; }\n public string EmitterId { get; init; } = \"\";\n public DirectorVector? Position { get; init; }\n public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();\n}\n\npublic sealed class MusicDecisionBatch\n{\n public double Time { get; init; }\n public List<MusicDecision> Decisions { get; } = new();\n}\n\npublic sealed record MusicTimingTag(int Index, double OffsetSeconds);\n\npublic sealed class MusicEventDefinition\n{\n public string EventId { get; init; } = \"\";\n public string TrackId { get; init; } = \"main\";\n public MusicPriority Priority { get; init; } = MusicPriority.Low;\n public MusicMasterFlags Flags { get; init; }\n public float DefaultFadeOutSeconds { get; init; }\n public double DelaySeconds { get; init; }\n public string? AutoQueueEventId { get; init; }\n public IReadOnlyList<string> BlockTrackList { get; init; } = Array.Empty<string>();\n public IReadOnlyList<string> DuckTrackList { get; init; } = Array.Empty<string>();\n public IReadOnlyList<string> StopTrackList { get; init; } = Array.Empty<string>();\n public IReadOnlyList<MusicTimingTag> TimingTags { get; init; } = Array.Empty<MusicTimingTag>();\n public double LoopSeconds { get; init; }\n public string? SyncTrackId { get; init; }\n public int? SyncTagIndex { get; init; }\n public double SyncTagDelaySeconds { get; init; }\n public double SyncTagDelayMultiplier { get; init; } = 1;\n\n public void Validate()\n {\n if (string.IsNullOrWhiteSpace(EventId) || string.IsNullOrWhiteSpace(TrackId)) throw new ArgumentException(\"Music events require event and track ids.\");\n if (!float.IsFinite(DefaultFadeOutSeconds) || DefaultFadeOutSeconds < 0 || !double.IsFinite(DelaySeconds) || DelaySeconds < 0 || !double.IsFinite(LoopSeconds) || LoopSeconds < 0 || !double.IsFinite(SyncTagDelaySeconds) || !double.IsFinite(SyncTagDelayMultiplier) || SyncTagDelayMultiplier < 0) throw new ArgumentException(\"Music timing values must be finite and non-negative where applicable.\");\n if (TimingTags.Any(x => x.Index < 0 || !double.IsFinite(x.OffsetSeconds) || x.OffsetSeconds < 0) || TimingTags.Select(x => x.Index).Distinct().Count() != TimingTags.Count) throw new ArgumentException(\"Music timing tags require unique non-negative indices and offsets.\");\n if (BlockTrackList.Concat(DuckTrackList).Concat(StopTrackList).Any(string.IsNullOrWhiteSpace)) throw new ArgumentException(\"Music track lists cannot contain empty ids.\");\n }\n}\n\npublic interface IMusicEventCatalog\n{\n bool TryGet(string eventId, out MusicEventDefinition definition);\n}\n\npublic sealed class MusicEventCatalog : IMusicEventCatalog\n{\n readonly Dictionary<string, MusicEventDefinition> _events;\n public MusicEventCatalog(IEnumerable<MusicEventDefinition> events)\n {\n _events = new(StringComparer.Ordinal);\n foreach (var definition in events) { definition.Validate(); if (!_events.TryAdd(definition.EventId, definition)) throw new ArgumentException($\"Duplicate music event '{definition.EventId}'.\"); }\n foreach (var definition in _events.Values) if (definition.AutoQueueEventId is { Length: > 0 } next && !_events.ContainsKey(next)) throw new ArgumentException($\"Music event '{definition.EventId}' queues unknown event '{next}'.\");\n }\n public bool TryGet(string eventId, out MusicEventDefinition definition) => _events.TryGetValue(eventId, out definition!);\n}\n\npublic sealed record MusicRuntimeAction(long ActionId, MusicRuntimeActionKind Kind, string TargetId)\n{\n public string EventId { get; init; } = \"\";\n public string TrackId { get; init; } = \"\";\n public string LayerId { get; init; } = \"\";\n public float Value { get; init; }\n public float FadeSeconds { get; init; }\n public float StartOffsetSeconds { get; init; }\n public string EmitterId { get; init; } = \"\";\n public DirectorVector? Position { get; init; }\n public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();\n}\n\npublic sealed record MusicDecisionResult(long DecisionId, MusicDecisionResultKind Result, string Detail = \"\", long InstanceId = 0);\n\npublic sealed class MusicRuntimeBatch\n{\n public double Time { get; init; }\n public List<MusicRuntimeAction> Actions { get; } = new();\n public List<MusicDecisionResult> Results { get; } = new();\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/WaveSequenceController.cs",
"FileName": "WaveSequenceController.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "namespace SboxDirector;\n\npublic sealed class WaveSequenceConfiguration\n{\n public int WaveCount { get; init; } = 1;\n public double InitialDelayMinimum { get; init; } = 1;\n public double InitialDelayMaximum { get; init; } = 2;\n public double CombatTimeout { get; init; } = 10;\n public double PauseMinimum { get; init; } = 5;\n public double PauseMaximum { get; init; } = 10;\n public void Validate()\n {\n var durations = new[] { InitialDelayMinimum, InitialDelayMaximum, CombatTimeout, PauseMinimum, PauseMaximum };\n if (WaveCount <= 0 || durations.Any(x => !double.IsFinite(x)) || InitialDelayMinimum < 0 || InitialDelayMaximum < InitialDelayMinimum || CombatTimeout < 0 || PauseMinimum < 0 || PauseMaximum < PauseMinimum) throw new ArgumentException(\"Invalid wave sequence configuration.\");\n }\n}\n\npublic readonly record struct WaveSequenceState(WaveState State, int IssuedWaves, int TargetWaves, double Deadline);\npublic readonly record struct WaveAction(bool IssueWave, bool Completed, string Reason);\n\npublic sealed class WaveSequenceController\n{\n readonly WaveSequenceConfiguration _configuration; readonly DeterministicRandom _random;\n public WaveState State { get; private set; } = WaveState.Inactive;\n public int IssuedWaves { get; private set; }\n public int TargetWaves { get; private set; }\n public double Deadline { get; private set; }\n public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random) { configuration.Validate(); _configuration = configuration; _random = random; }\n public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random, WaveSequenceState state) : this(configuration, random) { State = state.State; IssuedWaves = state.IssuedWaves; TargetWaves = state.TargetWaves; Deadline = state.Deadline; if (!double.IsFinite(Deadline) || IssuedWaves < 0 || TargetWaves < 0 || IssuedWaves > TargetWaves) throw new ArgumentException(\"Wave state is invalid.\", nameof(state)); }\n public WaveSequenceState Snapshot => new(State, IssuedWaves, TargetWaves, Deadline);\n public void Start(double time, int? waveCount = null) { RequireTime(time); TargetWaves = waveCount ?? _configuration.WaveCount; if (TargetWaves <= 0) throw new ArgumentOutOfRangeException(nameof(waveCount)); IssuedWaves = 0; State = WaveState.InitialDelay; Deadline = time + Range(_configuration.InitialDelayMinimum, _configuration.InitialDelayMaximum); }\n public void Cancel() { State = WaveState.Inactive; IssuedWaves = 0; TargetWaves = 0; Deadline = 0; }\n public WaveAction Update(double time, bool relevantCombatantsRemain)\n {\n RequireTime(time);\n if (State is WaveState.Inactive or WaveState.Done) return new(false, State == WaveState.Done, \"inactive or complete\");\n if (time < Deadline) return new(false, false, \"waiting for timer\");\n if (State == WaveState.InitialDelay) { State = WaveState.IssueWave; return new(false, false, \"initial delay expired\"); }\n if (State == WaveState.IssueWave) { IssuedWaves++; State = WaveState.WaitForClear; Deadline = time + _configuration.CombatTimeout; return new(true, false, \"wave issued\"); }\n if (State == WaveState.WaitForClear)\n {\n if (relevantCombatantsRemain) { Deadline = time + _configuration.CombatTimeout; return new(false, false, \"combat remains\"); }\n if (IssuedWaves >= TargetWaves) { State = WaveState.Done; return new(false, true, \"target waves issued and combat clear\"); }\n State = WaveState.Pause; Deadline = time + Range(_configuration.PauseMinimum, _configuration.PauseMaximum); return new(false, false, \"pausing before next wave\");\n }\n if (State == WaveState.Pause) { State = WaveState.IssueWave; return new(false, false, \"pause expired\"); }\n return new(false, false, \"no action\");\n }\n double Range(double minimum, double maximum) => minimum + (maximum - minimum) * _random.NextFloat();\n static void RequireTime(double time) { if (!double.IsFinite(time)) throw new ArgumentOutOfRangeException(nameof(time)); }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Adaptive Director\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"chomnr_adaptive_director\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"notpointless\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"notpointless.chomnr_adaptive_director\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-08-01T14:11:11.8900023Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.119.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.119.0\")]"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/EncounterSchedules.cs",
"FileName": "EncounterSchedules.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic sealed class EncounterRule\n{\n public string Id { get; init; } = \"rule\";\n public EncounterKind Kind { get; init; }\n public int Count { get; init; } = 1;\n public int Priority { get; init; }\n public double CooldownSeconds { get; init; } = 10;\n public float MinimumPressure { get; init; }\n public float MaximumPressure { get; init; } = 1f;\n public float Probability { get; init; } = 1f;\n public IReadOnlySet<TempoPhase> Phases { get; init; } = new HashSet<TempoPhase> { TempoPhase.BuildUp, TempoPhase.SustainPeak };\n public IReadOnlySet<string> Modes { get; init; } = new HashSet<string>();\n public Func<WorldSnapshot, bool>? AdditionalCondition { get; init; }\n\n public void Validate()\n {\n if (string.IsNullOrWhiteSpace(Id) || Count <= 0 || !double.IsFinite(CooldownSeconds) || CooldownSeconds < 0 || !float.IsFinite(MinimumPressure) || !float.IsFinite(MaximumPressure) || !float.IsFinite(Probability) || MinimumPressure < 0 || MaximumPressure > 1 || MaximumPressure < MinimumPressure || Probability is < 0 or > 1) throw new ArgumentException($\"Invalid encounter rule '{Id}'.\");\n }\n}\n\npublic interface IEncounterTimingPolicy\n{\n double AdjustCooldown(EncounterRule rule, double proposedSeconds, WorldSnapshot world, TempoPhase tempo, float teamPressure);\n}\n\npublic sealed class DefaultEncounterTimingPolicy : IEncounterTimingPolicy\n{\n public double AdjustCooldown(EncounterRule rule, double proposedSeconds, WorldSnapshot world, TempoPhase tempo, float teamPressure) => proposedSeconds;\n}\n\npublic sealed class ModeEncounterTimingPolicy : IEncounterTimingPolicy\n{\n readonly IReadOnlyDictionary<string, double> _modeMultipliers;\n readonly double _highPressureMultiplier;\n public ModeEncounterTimingPolicy(IReadOnlyDictionary<string, double>? modeMultipliers = null, double highPressureMultiplier = 1)\n {\n _modeMultipliers = new Dictionary<string, double>(modeMultipliers ?? new Dictionary<string, double>(), StringComparer.Ordinal); if (_modeMultipliers.Any(x => string.IsNullOrWhiteSpace(x.Key) || !double.IsFinite(x.Value) || x.Value < 0) || !double.IsFinite(highPressureMultiplier) || highPressureMultiplier < 0) throw new ArgumentException(\"Cooldown multipliers must be finite and non-negative.\"); _highPressureMultiplier = highPressureMultiplier;\n }\n public double AdjustCooldown(EncounterRule rule, double proposedSeconds, WorldSnapshot world, TempoPhase tempo, float teamPressure)\n {\n var mode = _modeMultipliers.TryGetValue(world.ModeId, out var multiplier) ? multiplier : 1; var pressure = 1 + (_highPressureMultiplier - 1) * Math.Clamp(teamPressure, 0, 1); return proposedSeconds * mode * pressure;\n }\n}\n\npublic sealed class RuleBasedEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback\n{\n readonly IReadOnlyList<EncounterRule> _rules;\n readonly IEncounterTimingPolicy _timing;\n readonly Dictionary<string, double> _nextTimes = new();\n EncounterRule? _selected;\n double _selectedPreviousTime;\n\n public RuleBasedEncounterSchedule(IEnumerable<EncounterRule> rules, IEncounterTimingPolicy? timing = null)\n {\n _rules = rules.OrderByDescending(x => x.Priority).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();\n if (_rules.Count == 0) throw new ArgumentException(\"At least one encounter rule is required.\", nameof(rules));\n foreach (var rule in _rules) rule.Validate();\n if (_rules.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != _rules.Count) throw new ArgumentException(\"Encounter rule ids must be unique.\");\n _timing = timing ?? new DefaultEncounterTimingPolicy();\n }\n\n public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float pressure, PopulationLedger population, DeterministicRandom random)\n {\n _selected = null;\n foreach (var rule in _rules)\n {\n if (_nextTimes.TryGetValue(rule.Id, out var next) && world.Time < next) continue;\n if (!rule.Phases.Contains(tempo) || pressure < rule.MinimumPressure || pressure > rule.MaximumPressure) continue;\n if (rule.Modes.Count > 0 && !rule.Modes.Contains(world.ModeId)) continue;\n if (rule.AdditionalCondition is not null && !rule.AdditionalCondition(world)) continue;\n if (population.Available(rule.Kind, world.Population) <= 0) continue;\n var cooldown = _timing.AdjustCooldown(rule, rule.CooldownSeconds, world, tempo, pressure); if (!double.IsFinite(cooldown) || cooldown < 0) throw new InvalidOperationException(\"Encounter timing policy returned an invalid cooldown.\");\n if (random.NextFloat() > rule.Probability) { _nextTimes[rule.Id] = world.Time + cooldown; continue; }\n _selected = rule; _selectedPreviousTime = _nextTimes.TryGetValue(rule.Id, out var previous) ? previous : double.NegativeInfinity; _nextTimes[rule.Id] = world.Time + cooldown; return rule.Kind;\n }\n return null;\n }\n\n public int RequestedCount(EncounterKind kind, WorldSnapshot world) => _selected?.Kind == kind ? _selected.Count : 1;\n public IReadOnlyDictionary<string, string> CaptureScheduleState() => _nextTimes.ToDictionary(x => x.Key, x => x.Value.ToString(\"R\", CultureInfo.InvariantCulture));\n public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)\n {\n _nextTimes.Clear(); foreach (var pair in state) if (double.TryParse(pair.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) _nextTimes[pair.Key] = value;\n }\n public void SelectionFinalized(bool requestCreated) { if (_selected is not null && !requestCreated) { if (double.IsNegativeInfinity(_selectedPreviousTime)) _nextTimes.Remove(_selected.Id); else _nextTimes[_selected.Id] = _selectedPreviousTime; } _selected = null; }\n}\n\npublic sealed class MilestoneEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback\n{\n readonly string _id; readonly EncounterKind _kind; readonly float _minimumProgress; readonly float _maximumProgress; readonly int _count;\n readonly HashSet<string> _completedScopes = new(); string? _selectedScope;\n public MilestoneEncounterSchedule(string id, EncounterKind kind, float minimumProgress, float maximumProgress = 1f, int count = 1)\n {\n if (string.IsNullOrWhiteSpace(id) || minimumProgress < 0 || maximumProgress < minimumProgress || count <= 0) throw new ArgumentException(\"Invalid milestone schedule.\");\n _id = id; _kind = kind; _minimumProgress = minimumProgress; _maximumProgress = maximumProgress; _count = count;\n }\n public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)\n {\n var scope = string.IsNullOrWhiteSpace(world.RoundId) ? world.ModeId : world.ModeId + \":\" + world.RoundId;\n if (_completedScopes.Contains(scope) || world.TeamProgress < _minimumProgress || world.TeamProgress > _maximumProgress || population.Available(_kind, world.Population) <= 0) return null;\n _selectedScope = scope; return _kind;\n }\n public int RequestedCount(EncounterKind kind, WorldSnapshot world) => kind == _kind && _selectedScope is not null ? _count : 1;\n public IReadOnlyDictionary<string, string> CaptureScheduleState() => new Dictionary<string, string> { [\"id\"] = _id, [\"completed\"] = string.Join(\"|\", _completedScopes.OrderBy(x => x, StringComparer.Ordinal)) };\n public void RestoreScheduleState(IReadOnlyDictionary<string, string> state) { _completedScopes.Clear(); if (state.TryGetValue(\"completed\", out var value)) foreach (var scope in value.Split('|', StringSplitOptions.RemoveEmptyEntries)) _completedScopes.Add(scope); }\n public void SelectionFinalized(bool requestCreated) { if (requestCreated && _selectedScope is not null) _completedScopes.Add(_selectedScope); _selectedScope = null; }\n}\n\npublic sealed class CompositeEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback\n{\n readonly IReadOnlyList<IEncounterSchedule> _schedules; IEncounterSchedule? _selected;\n public CompositeEncounterSchedule(params IEncounterSchedule[] schedules) { _schedules = schedules; if (schedules.Length == 0) throw new ArgumentException(\"At least one schedule is required.\"); }\n public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)\n {\n _selected = null; foreach (var schedule in _schedules) { var kind = schedule.SelectEncounter(world, tempo, teamPressure, population, random); if (kind is not null) { _selected = schedule; return kind; } }\n return null;\n }\n public int RequestedCount(EncounterKind kind, WorldSnapshot world) => _selected?.RequestedCount(kind, world) ?? 1;\n public IReadOnlyDictionary<string, string> CaptureScheduleState()\n {\n var output = new Dictionary<string, string>(); for (var i = 0; i < _schedules.Count; i++) if (_schedules[i] is IPersistentEncounterSchedule persistent) foreach (var pair in persistent.CaptureScheduleState()) output[$\"{i}:{pair.Key}\"] = pair.Value; return output;\n }\n public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)\n {\n for (var i = 0; i < _schedules.Count; i++) if (_schedules[i] is IPersistentEncounterSchedule persistent) persistent.RestoreScheduleState(state.Where(x => x.Key.StartsWith(i + \":\", StringComparison.Ordinal)).ToDictionary(x => x.Key[(x.Key.IndexOf(':') + 1)..], x => x.Value));\n }\n public void SelectionFinalized(bool requestCreated) { if (_selected is IEncounterScheduleFeedback feedback) feedback.SelectionFinalized(requestCreated); _selected = null; }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/Models.cs",
"FileName": "Models.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic enum TempoPhase { BuildUp, SustainPeak, PeakFade, Relax }\npublic enum PressureSeverity { Minor = 1, Moderate = 2, Major = 3, Critical = 4, Maximum = 5 }\npublic enum EncounterKind { Ambient, CommonWave, Special, Boss, DormantHazard, Objective, Custom }\npublic enum RequestResultKind { Succeeded, PartiallySucceeded, Rejected, Deferred, Failed }\npublic enum WaveState { Inactive, InitialDelay, IssueWave, WaitForClear, Pause, Done }\npublic enum ScenarioStatus { Inactive, Running, Complete, Failed }\npublic enum AdvanceRelation { Unknown, Ahead, Behind, Lateral, ObjectiveAdjacent }\npublic enum PlacementFallbackMode { None, IgnoreRouteConstraints, AnyReachableHidden }\npublic enum PlacementSelectionMode { WeightedScore, FirstEligible, HighestProgressFirstTie, NativeRandomTranspositionFirst }\n\npublic readonly record struct DirectorVector(float X, float Y, float Z)\n{\n public static float DistanceSquared(DirectorVector a, DirectorVector b)\n {\n var x = a.X - b.X; var y = a.Y - b.Y; var z = a.Z - b.Z;\n return x * x + y * y + z * z;\n }\n}\n\npublic sealed class ParticipantSnapshot\n{\n public string Id { get; init; } = \"\";\n public bool IsAlive { get; init; } = true;\n public bool IsEligible { get; init; } = true;\n public bool IsInCombat { get; init; }\n public DirectorVector Position { get; init; }\n public float Progress { get; init; }\n}\n\npublic sealed class PopulationSnapshot\n{\n readonly Dictionary<EncounterKind, int> _counts = new();\n public PopulationSnapshot() { }\n public PopulationSnapshot(IReadOnlyDictionary<EncounterKind, int> counts) { foreach (var pair in counts) _counts[pair.Key] = Math.Max(0, pair.Value); }\n public int this[EncounterKind kind] => _counts.TryGetValue(kind, out var value) ? value : 0;\n public IReadOnlyDictionary<EncounterKind, int> Counts => _counts;\n}\n\npublic sealed class SpawnCandidate\n{\n public string Id { get; init; } = \"\";\n public DirectorVector Position { get; init; }\n public bool Reachable { get; init; }\n public bool Spawnable { get; init; }\n public bool Visible { get; init; }\n public float NearestParticipantDistance { get; init; }\n public float Progress { get; init; }\n public float AreaCapacity { get; init; } = 1f;\n public float ThreatSeparation { get; init; }\n public float RelativeProgress { get; init; }\n public float PathDistance { get; init; }\n public float IngressDistance { get; init; }\n public float ClearRadius { get; init; }\n public float AreaWidth { get; init; }\n public float AreaHeight { get; init; }\n public AdvanceRelation AdvanceRelation { get; init; }\n public IReadOnlySet<string> Tags { get; init; } = new HashSet<string>();\n}\n\npublic sealed class WorldSnapshot\n{\n public double Time { get; init; }\n public IReadOnlyList<ParticipantSnapshot> Participants { get; init; } = Array.Empty<ParticipantSnapshot>();\n public PopulationSnapshot Population { get; init; } = new();\n public IReadOnlyList<SpawnCandidate> SpawnCandidates { get; init; } = Array.Empty<SpawnCandidate>();\n public bool CombatActive { get; init; }\n public float TeamProgress { get; init; }\n public string ModeId { get; init; } = \"continuous\";\n public string? RoundId { get; init; }\n public ResourceWorldSnapshot Resources { get; init; } = new();\n\n public void Validate()\n {\n if (double.IsNaN(Time) || double.IsInfinity(Time)) throw new ArgumentException(\"World time must be finite.\");\n if (float.IsNaN(TeamProgress) || float.IsInfinity(TeamProgress)) throw new ArgumentException(\"Team progress must be finite.\");\n if (Participants.Any(x => string.IsNullOrWhiteSpace(x.Id)) || Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException(\"Participant ids must be non-empty and unique.\");\n if (SpawnCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id)) || SpawnCandidates.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != SpawnCandidates.Count) throw new ArgumentException(\"Spawn candidate ids must be non-empty and unique.\");\n if (SpawnCandidates.Any(x => !float.IsFinite(x.NearestParticipantDistance) || !float.IsFinite(x.Progress) || !float.IsFinite(x.RelativeProgress) || !float.IsFinite(x.AreaCapacity) || !float.IsFinite(x.ThreatSeparation) || !float.IsFinite(x.PathDistance) || !float.IsFinite(x.IngressDistance) || !float.IsFinite(x.ClearRadius) || !float.IsFinite(x.AreaWidth) || !float.IsFinite(x.AreaHeight) || x.NearestParticipantDistance < 0 || x.AreaCapacity < 0 || x.ThreatSeparation < 0 || x.PathDistance < 0 || x.IngressDistance < 0 || x.ClearRadius < 0 || x.AreaWidth < 0 || x.AreaHeight < 0)) throw new ArgumentException(\"Spawn candidate distances, dimensions, and capacity must be finite and non-negative.\");\n Resources.Validate();\n }\n}\n\npublic sealed record EncounterMember(string Archetype, int Count);\n\npublic sealed record SpawnRequest(\n long RequestId,\n EncounterKind Kind,\n int RequestedCount,\n string CandidateId,\n string Archetype,\n string Reason)\n{\n public IReadOnlyList<EncounterMember> Composition { get; init; } = Array.Empty<EncounterMember>();\n public int Attempt { get; init; } = 1;\n public bool Equals(SpawnRequest? other) => other is not null && RequestId == other.RequestId && Kind == other.Kind && RequestedCount == other.RequestedCount && CandidateId == other.CandidateId && Archetype == other.Archetype && Reason == other.Reason && Attempt == other.Attempt && Composition.SequenceEqual(other.Composition);\n public override int GetHashCode()\n {\n var hash = HashCode.Combine(RequestId, Kind, RequestedCount, CandidateId, Archetype, Reason, Attempt); foreach (var member in Composition) hash = HashCode.Combine(hash, member); return hash;\n }\n}\n\npublic sealed record SpawnRequestResult(\n long RequestId,\n RequestResultKind Result,\n int SpawnedCount,\n string? Detail = null);\n\npublic sealed record DirectorEvent(string Name, string Reason, double Time);\n\npublic sealed class DirectorDecisionBatch\n{\n public double Time { get; init; }\n public TempoPhase Tempo { get; init; }\n public float TeamPressure { get; init; }\n public float MusicIntensity { get; init; }\n public List<SpawnRequest> SpawnRequests { get; } = new();\n public List<ResourceSpawnRequest> ResourceRequests { get; } = new();\n public List<DirectorEvent> Events { get; } = new();\n}\n\npublic sealed record PressureEvent(string ParticipantId, PressureSeverity Severity, string Reason);\n\npublic sealed record DirectorDiagnostic(double Time, string Category, string Message);\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/MusicModels.cs",
"FileName": "MusicModels.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic enum MusicDecisionKind { SetMixerLayer, Play, StopEvent, StopTrack, StopAll, General }\npublic enum MusicPriority { Low = 1, Medium = 2, High = 3, Critical = 4 }\npublic enum MusicTriggerKind { Play, StopEvent, StopTrack, StopAll, General, Reset, ScenarioEnded, ParticipantRestored, AmbientMob, MobSpawn, MobSpawnBehind, LargeAreaRevealed, LandmarkRevealed }\npublic enum MusicRuntimeActionKind { Play, StopEvent, StopTrack, StopAll, SetTrackVolume, SetMixerLayer, General }\npublic enum MusicDecisionResultKind { Accepted, Deferred, RejectedUnknownEvent, RejectedBlocked, RejectedLifecycle, RejectedPriority, FailedAdapter }\n\n[Flags]\npublic enum MusicMasterFlags\n{\n None = 0,\n PlayToEnd = 1,\n PlaySplit = 2,\n DontDisengage = 4,\n DontEngage = 8,\n AllowAfterDeath = 16,\n AllowAfterScenarioEnd = 32\n}\n\npublic static class MusicMixerLayers\n{\n public const string MobBeating = \"mob_beating\";\n public const string MobRules = \"mob_rules\";\n public const string HazardRage = \"hazard_rage\";\n public const string Combat = \"combat\";\n public const string CombatSecondary = \"combat_secondary\";\n public const string CombatClose = \"combat_close\";\n public const string Adrenaline = \"adrenaline\";\n public const string Checkpoint = \"checkpoint\";\n public const string Ambient = \"ambient\";\n}\n\npublic sealed record SpecialMusicThreat(string ArchetypeId, float Distance, string EmitterId = \"\", DirectorVector? Position = null);\npublic sealed record PositionalMusicCandidate(string Id, DirectorVector Position, bool Eligible = true);\npublic sealed record WanderingMusicHazard(string Id, float Anger, DirectorVector Position);\n\npublic sealed class MusicParticipantSnapshot\n{\n public string Id { get; init; } = \"\";\n public bool IsEligible { get; init; } = true;\n public bool IsAlive { get; init; } = true;\n public bool IsIncapacitated { get; init; }\n public bool DynamicMusicAllowed { get; init; } = true;\n public float CumulativeDamage { get; init; }\n public float WeaponActivity { get; init; }\n public bool InflictedDamage { get; init; }\n public int VisibleCommonThreats { get; init; }\n public int ActiveCommonThreats { get; init; }\n public int ScannedCommonThreats { get; init; }\n public float AttackingCommonFactor { get; init; }\n public float CloseCommonAction { get; init; }\n public float VeryCloseCommonFactor { get; init; }\n public bool MobPressureActive { get; init; }\n public bool GlobalCombatActive { get; init; }\n public bool BossPresent { get; init; }\n public string BossArchetypeId { get; init; } = \"\";\n public bool HazardPresent { get; init; }\n public bool HazardAttacking { get; init; }\n public bool HazardBurning { get; init; }\n public float HazardRage { get; init; }\n public string HazardEmitterId { get; init; } = \"\";\n public DirectorVector? HazardPosition { get; init; }\n public float Adrenaline { get; init; }\n public bool InCheckpoint { get; init; }\n public IReadOnlyList<SpecialMusicThreat> SpecialThreats { get; init; } = Array.Empty<SpecialMusicThreat>();\n public IReadOnlyList<PositionalMusicCandidate> PositionalCandidates { get; init; } = Array.Empty<PositionalMusicCandidate>();\n public IReadOnlyList<WanderingMusicHazard> WanderingHazards { get; init; } = Array.Empty<WanderingMusicHazard>();\n\n public void Validate()\n {\n if (string.IsNullOrWhiteSpace(Id)) throw new ArgumentException(\"A music participant id is required.\");\n var values = new[] { CumulativeDamage, WeaponActivity, AttackingCommonFactor, CloseCommonAction, VeryCloseCommonFactor, HazardRage, Adrenaline };\n if (values.Any(x => !float.IsFinite(x)) || CumulativeDamage < 0 || VisibleCommonThreats < 0 || ActiveCommonThreats < 0 || ScannedCommonThreats < 0) throw new ArgumentException(\"Music participant measurements must be finite and non-negative.\");\n if (WeaponActivity is < 0 or > 1 || AttackingCommonFactor is < 0 or > 1 || CloseCommonAction is < 0 or > 1 || VeryCloseCommonFactor is < 0 or > 1 || HazardRage is < 0 or > 1 || Adrenaline is < 0 or > 1) throw new ArgumentException(\"Normalized music inputs must be in [0,1].\");\n if (SpecialThreats.Any(x => string.IsNullOrWhiteSpace(x.ArchetypeId) || !float.IsFinite(x.Distance) || x.Distance < 0)) throw new ArgumentException(\"Special music threats require an archetype and finite non-negative distance.\");\n if (PositionalCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id))) throw new ArgumentException(\"Positional music candidates require stable ids.\");\n if (WanderingHazards.Any(x => string.IsNullOrWhiteSpace(x.Id) || !float.IsFinite(x.Anger) || x.Anger is < 0 or > 1)) throw new ArgumentException(\"Wandering music hazards require stable ids and anger in [0,1].\");\n }\n}\n\npublic sealed class MusicWorldSnapshot\n{\n public double Time { get; init; }\n public IReadOnlyList<MusicParticipantSnapshot> Participants { get; init; } = Array.Empty<MusicParticipantSnapshot>();\n public bool ScenarioEnded { get; init; }\n\n public void Validate()\n {\n if (!double.IsFinite(Time)) throw new ArgumentException(\"Music world time must be finite.\");\n foreach (var participant in Participants) participant.Validate();\n if (Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException(\"Music participant ids must be unique.\");\n }\n}\n\npublic sealed record MusicGameplayEvent(long Sequence, MusicTriggerKind Kind, string ParticipantId = \"\", string TargetId = \"\", float FadeSeconds = 0, string EmitterId = \"\", DirectorVector? Position = null)\n{\n public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();\n}\n\npublic sealed record MusicDecision(long DecisionId, MusicDecisionKind Kind, string ParticipantId, string TargetId, string Reason)\n{\n public string LayerId { get; init; } = \"\";\n public float Value { get; init; }\n public float FadeSeconds { get; init; }\n public float StartOffsetSeconds { get; init; }\n public string EmitterId { get; init; } = \"\";\n public DirectorVector? Position { get; init; }\n public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();\n}\n\npublic sealed class MusicDecisionBatch\n{\n public double Time { get; init; }\n public List<MusicDecision> Decisions { get; } = new();\n}\n\npublic sealed record MusicTimingTag(int Index, double OffsetSeconds);\n\npublic sealed class MusicEventDefinition\n{\n public string EventId { get; init; } = \"\";\n public string TrackId { get; init; } = \"main\";\n public MusicPriority Priority { get; init; } = MusicPriority.Low;\n public MusicMasterFlags Flags { get; init; }\n public float DefaultFadeOutSeconds { get; init; }\n public double DelaySeconds { get; init; }\n public string? AutoQueueEventId { get; init; }\n public IReadOnlyList<string> BlockTrackList { get; init; } = Array.Empty<string>();\n public IReadOnlyList<string> DuckTrackList { get; init; } = Array.Empty<string>();\n public IReadOnlyList<string> StopTrackList { get; init; } = Array.Empty<string>();\n public IReadOnlyList<MusicTimingTag> TimingTags { get; init; } = Array.Empty<MusicTimingTag>();\n public double LoopSeconds { get; init; }\n public string? SyncTrackId { get; init; }\n public int? SyncTagIndex { get; init; }\n public double SyncTagDelaySeconds { get; init; }\n public double SyncTagDelayMultiplier { get; init; } = 1;\n\n public void Validate()\n {\n if (string.IsNullOrWhiteSpace(EventId) || string.IsNullOrWhiteSpace(TrackId)) throw new ArgumentException(\"Music events require event and track ids.\");\n if (!float.IsFinite(DefaultFadeOutSeconds) || DefaultFadeOutSeconds < 0 || !double.IsFinite(DelaySeconds) || DelaySeconds < 0 || !double.IsFinite(LoopSeconds) || LoopSeconds < 0 || !double.IsFinite(SyncTagDelaySeconds) || !double.IsFinite(SyncTagDelayMultiplier) || SyncTagDelayMultiplier < 0) throw new ArgumentException(\"Music timing values must be finite and non-negative where applicable.\");\n if (TimingTags.Any(x => x.Index < 0 || !double.IsFinite(x.OffsetSeconds) || x.OffsetSeconds < 0) || TimingTags.Select(x => x.Index).Distinct().Count() != TimingTags.Count) throw new ArgumentException(\"Music timing tags require unique non-negative indices and offsets.\");\n if (BlockTrackList.Concat(DuckTrackList).Concat(StopTrackList).Any(string.IsNullOrWhiteSpace)) throw new ArgumentException(\"Music track lists cannot contain empty ids.\");\n }\n}\n\npublic interface IMusicEventCatalog\n{\n bool TryGet(string eventId, out MusicEventDefinition definition);\n}\n\npublic sealed class MusicEventCatalog : IMusicEventCatalog\n{\n readonly Dictionary<string, MusicEventDefinition> _events;\n public MusicEventCatalog(IEnumerable<MusicEventDefinition> events)\n {\n _events = new(StringComparer.Ordinal);\n foreach (var definition in events) { definition.Validate(); if (!_events.TryAdd(definition.EventId, definition)) throw new ArgumentException($\"Duplicate music event '{definition.EventId}'.\"); }\n foreach (var definition in _events.Values) if (definition.AutoQueueEventId is { Length: > 0 } next && !_events.ContainsKey(next)) throw new ArgumentException($\"Music event '{definition.EventId}' queues unknown event '{next}'.\");\n }\n public bool TryGet(string eventId, out MusicEventDefinition definition) => _events.TryGetValue(eventId, out definition!);\n}\n\npublic sealed record MusicRuntimeAction(long ActionId, MusicRuntimeActionKind Kind, string TargetId)\n{\n public string EventId { get; init; } = \"\";\n public string TrackId { get; init; } = \"\";\n public string LayerId { get; init; } = \"\";\n public float Value { get; init; }\n public float FadeSeconds { get; init; }\n public float StartOffsetSeconds { get; init; }\n public string EmitterId { get; init; } = \"\";\n public DirectorVector? Position { get; init; }\n public IReadOnlyList<string> Arguments { get; init; } = Array.Empty<string>();\n}\n\npublic sealed record MusicDecisionResult(long DecisionId, MusicDecisionResultKind Result, string Detail = \"\", long InstanceId = 0);\n\npublic sealed class MusicRuntimeBatch\n{\n public double Time { get; init; }\n public List<MusicRuntimeAction> Actions { get; } = new();\n public List<MusicDecisionResult> Results { get; } = new();\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "dev/SboxDirector.Tests/Program.cs",
"FileName": "Program.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "using SboxDirector;\n\nstatic class Tests\n{\n static int _count;\n static void Check(bool value, string name) { _count++; if (!value) throw new Exception($\"FAILED: {name}\"); }\n static void Near(float actual, float expected, float epsilon, string name) => Check(MathF.Abs(actual - expected) <= epsilon, $\"{name}: expected {expected}, got {actual}\");\n\n public static void Main()\n {\n Pressure(); TeamMaximum(); Tempo(); Placement(); AdvancedPlacement(); RecoveredCompatibility(); Population(); Wave(); Scenarios(); Core(); Persistence(); PersistentPolicies(); RuleSchedules(); ConcurrentSchedules(); CompositionAndRetry(); Resources(); Orchestration(); AdaptiveCallbacks(); HostRunner(); OwnershipAndResources(); MusicAuthority(); MusicRuntimeArbitration(); MusicPersistenceAndHost(); Validation(); LongDeterminism(); AdvancedDeterminism();\n Console.WriteLine($\"PASS: {_count} adaptive-director assertions\");\n }\n\n static void Pressure()\n {\n var p = new PressureTracker(new PressureConfiguration(), 0);\n p.Add(PressureSeverity.Major, 0); Near(p.Instantaneous, .125f, .00001f, \"major coefficient\");\n p.Update(5); Near(p.Instantaneous, .125f, .00001f, \"hold\");\n p.Update(8); Near(p.Instantaneous, .025f, .0001f, \"linear decay\");\n p.Add(PressureSeverity.Maximum, 8); Near(p.Instantaneous, 1f, .00001f, \"maximum saturates\");\n var q = new PressureTracker(new PressureConfiguration(), 0); q.Add(PressureSeverity.Critical, 0); q.Update(1f / 15f);\n Near(q.Averaged, MathF.Pow(.25f, (1f / 15f) / 20f), .0001f, \"recovered averaged formula\");\n }\n\n static void TeamMaximum()\n {\n var team = new TeamPressure(new PressureConfiguration()); team.Apply(new(\"a\", PressureSeverity.Critical, \"test\"), 0); team.Apply(new(\"b\", PressureSeverity.Minor, \"test\"), 0);\n var world = new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = \"a\" }, new ParticipantSnapshot { Id = \"b\" } } };\n Near(team.UpdateAndGetMaximum(world), .25f, .0001f, \"team uses max\");\n }\n\n static void Tempo()\n {\n var c = new TempoController(new TempoConfiguration { RelaxMinimumSeconds = 1, RelaxMaximumSeconds = 2, RelaxMaximumProgressTravel = .1f });\n c.Update(new WorldSnapshot { Time = 1, TeamProgress = .2f }, 0); Check(c.Phase == TempoPhase.BuildUp, \"relax to buildup\");\n c.Update(new WorldSnapshot { Time = 20 }, 1); Check(c.Phase == TempoPhase.SustainPeak, \"buildup to peak\");\n var absoluteClock = new DirectorCore(new DirectorConfiguration { Tempo = new TempoConfiguration { RelaxMinimumSeconds = 20, RelaxMaximumSeconds = 45 } }); Check(absoluteClock.Tick(World(10000)).Tempo == TempoPhase.Relax, \"first tick aligns absolute clock\");\n }\n\n static void Placement()\n {\n var candidates = new[] { new SpawnCandidate { Id = \"visible\", Reachable = true, Spawnable = true, Visible = true, NearestParticipantDistance = 1000, ThreatSeparation = 1000 }, new SpawnCandidate { Id = \"ok\", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 1000 } };\n var selected = new PlacementSelector().Select(candidates, new PlacementProfile(), new DeterministicRandom(1)); Check(selected?.Candidate.Id == \"ok\", \"placement filters visibility\");\n var vetoed = new PlacementSelector().Select(candidates, new PlacementProfile(), new DeterministicRandom(1), new WorldSnapshot(), new RejectAllPlacement()); Check(vetoed is null, \"custom placement veto\");\n var tagged = new SpawnCandidate { Id = \"tagged\", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 1000, Tags = new HashSet<string> { \"outdoor\" } }; Check(new PlacementSelector().Select(new[] { tagged }, new PlacementProfile { RequiredTags = new HashSet<string> { \"indoor\" } }, new DeterministicRandom(1)) is null, \"required placement tags\");\n }\n\n static void AdvancedPlacement()\n {\n var strict = new SpawnCandidate { Id = \"strict\", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 2000, PathDistance = 800, IngressDistance = 300, ClearRadius = 500, AdvanceRelation = AdvanceRelation.Ahead };\n var fallback = new SpawnCandidate { Id = \"fallback\", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 2000, PathDistance = 20, IngressDistance = 10, ClearRadius = 20, AdvanceRelation = AdvanceRelation.Behind };\n var profile = new PlacementProfile { MinimumPathDistance = 500, MinimumIngressDistance = 200, MinimumClearRadius = 300, AllowedRelations = new HashSet<AdvanceRelation> { AdvanceRelation.Ahead }, FallbackMode = PlacementFallbackMode.IgnoreRouteConstraints };\n var selected = new PlacementSelector().Select(new[] { strict, fallback }, profile, new DeterministicRandom(1)); Check(selected?.Candidate.Id == \"strict\" && !selected.IsFallback, \"route-constrained placement\");\n selected = new PlacementSelector().Select(new[] { fallback }, profile, new DeterministicRandom(1)); Check(selected?.Candidate.Id == \"fallback\" && selected.IsFallback, \"route fallback placement\");\n }\n\n static void RecoveredCompatibility()\n {\n var route = new[]\n {\n new SpawnCandidate { Id = \"first-tie\", Reachable = true, Spawnable = true, Progress = 50 },\n new SpawnCandidate { Id = \"lower\", Reachable = true, Spawnable = true, Progress = 40 },\n new SpawnCandidate { Id = \"second-tie\", Reachable = true, Spawnable = true, Progress = 50 }\n };\n var routeChoice = new PlacementSelector().Select(route, RecoveredDirectorDefaults.CreateRouteStepProfile(), new DeterministicRandom(1));\n Check(routeChoice?.Candidate.Id == \"first-tie\", \"recovered route uses strict greater-than and first tie\");\n\n var threat = new[]\n {\n new SpawnCandidate { Id = \"too-small\", Reachable = true, Spawnable = true, AreaWidth = 23, AreaHeight = 24 },\n new SpawnCandidate { Id = \"a\", Reachable = true, Spawnable = true, AreaWidth = 24, AreaHeight = 24 },\n new SpawnCandidate { Id = \"b\", Reachable = true, Spawnable = true, AreaWidth = 30, AreaHeight = 30 }\n };\n var expectedOrder = threat.ToArray(); var expectedRandom = new DeterministicRandom(17);\n for (var index = 0; index < expectedOrder.Length; index++) { var other = expectedRandom.NextInt(expectedOrder.Length); (expectedOrder[index], expectedOrder[other]) = (expectedOrder[other], expectedOrder[index]); }\n var expected = expectedOrder.First(x => x.AreaWidth >= 24 && x.AreaHeight >= 24).Id;\n var threatChoice = new PlacementSelector().Select(threat, RecoveredDirectorDefaults.CreateThreatAreaProfile(), new DeterministicRandom(17));\n Check(threatChoice?.Candidate.Id == expected, \"recovered threat shuffle uses full-range random transpositions then first valid\");\n\n var samples = HullVisibilitySamples.CreateFivePointPattern(new DirectorVector(10, 0, 2), new DirectorVector(0, 0, 2), 32, 72);\n Check(samples.Count == 5, \"recovered visibility sample count\");\n Near(samples[0].Y, 16, .0001f, \"lower left hull offset\"); Near(samples[1].Y, -16, .0001f, \"lower right hull offset\");\n Near(samples[2].Z, 38, .0001f, \"upper center half-height\"); Near(samples[3].Z, 74, .0001f, \"upper corner full-height\");\n\n var uniform = new UniformEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<string>> { [EncounterKind.Special] = new[] { \"a\", \"b\", \"c\" } });\n var composition = uniform.Compose(EncounterKind.Special, 20, new WorldSnapshot(), new DefaultDirectorPolicy(), new DeterministicRandom(3));\n Check(composition.Sum(x => x.Count) == 20 && composition.All(x => x.Archetype is \"a\" or \"b\" or \"c\"), \"uniform recovered composition\");\n\n var rotation = new SpecialClassRotation(new[] { \"a\", \"b\", \"c\" });\n rotation.RestoreState(new Dictionary<string, double> { [\"a\"] = 0, [\"b\"] = 0, [\"c\"] = 0 });\n var selected = rotation.SelectDue(10, x => x != \"a\", new DeterministicRandom(4));\n Check(selected is \"b\" or \"c\", \"special rotation selects uniformly from due eligible classes\");\n Near((float)rotation.CaptureState()[\"a\"], 30, .0001f, \"ineligible class retry delay\");\n rotation.ReportSpawnFailure(selected!, 10); Near((float)rotation.CaptureState()[selected!], 15, .0001f, \"failed class retry delay\");\n rotation.ReportSpawnSuccess(selected!, 20); Near((float)rotation.CaptureState()[selected!], 1019, .0001f, \"successful class hold\");\n rotation.ReleaseAfterLifecycle(selected!, 30); Near((float)rotation.CaptureState()[selected!], 75, .0001f, \"special respawn interval\");\n }\n\n static void Population()\n {\n var ledger = new PopulationLedger(new PopulationConfiguration()); var empty = new PopulationSnapshot(); Check(ledger.TryReserve(1, EncounterKind.Boss, 1, empty), \"reserve\"); Check(ledger.Available(EncounterKind.Boss, empty) == 0, \"reservation counts\"); ledger.Resolve(new(1, RequestResultKind.Failed, 0)); Check(ledger.Available(EncounterKind.Boss, empty) == 1, \"failure releases\");\n Check(ledger.TryReserve(2, EncounterKind.Boss, 1, empty, 10), \"reserve deferred\"); ledger.Resolve(new(2, RequestResultKind.Deferred, 0)); Check(ledger.Available(EncounterKind.Boss, empty) == 0, \"deferred remains reserved\"); Check(ledger.Expire(10).SequenceEqual(new long[] { 2 }), \"reservation expires\"); Check(ledger.Available(EncounterKind.Boss, empty) == 1, \"expiry releases\");\n }\n\n static void Wave()\n {\n var wave = new WaveSequenceController(new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 }, new DeterministicRandom(2)); wave.Start(0); wave.Update(0, false); Check(wave.Update(0, false).IssueWave, \"wave issued\"); Check(wave.Update(0, false).Completed, \"wave completed\");\n var restored = new WaveSequenceController(new WaveSequenceConfiguration(), new DeterministicRandom(2), wave.Snapshot); Check(restored.State == WaveState.Done, \"wave restore\"); restored.Cancel(); Check(restored.State == WaveState.Inactive, \"wave cancel\");\n }\n\n static void Scenarios()\n {\n var scenario = new ScenarioController(new[] { new ScenarioStageDefinition(\"one\") }); scenario.Start(0); Check(scenario.TryAdvance(0, false) && scenario.Status == ScenarioStatus.Complete, \"scenario complete\"); var rounds = new RoundSessionFlow(); rounds.BeginRound(\"1\"); Check(rounds.IsSessionActive, \"round adapter\"); rounds.EndRound(); Check(!rounds.IsSessionActive, \"round end\"); Check(new ContinuousSessionFlow().IsSessionActive, \"continuous adapter\");\n var restored = new ScenarioController(new[] { new ScenarioStageDefinition(\"one\") }, scenario.Snapshot); Check(restored.Status == ScenarioStatus.Complete, \"scenario restore\"); restored.Reset(); Check(restored.Status == ScenarioStatus.Inactive, \"scenario reset\");\n }\n\n static void Core()\n {\n var core = new DirectorCore(new DirectorConfiguration { Tempo = new TempoConfiguration { RelaxMinimumSeconds = 0, RelaxMaximumSeconds = 0 } });\n var output = core.Tick(new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = \"p\" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = \"s\", Reachable = true, Spawnable = true, NearestParticipantDistance = 800, ThreatSeparation = 2000 } } });\n Check(output.Events.Count == 1, \"core emits tempo transition\");\n var multi = new DirectorCore(new DirectorConfiguration { MaximumRequestsPerTick = 2 }, schedule: new RepeatingSchedule()); var requests = multi.Tick(World(0)); Check(requests.SpawnRequests.Count == 2, \"multiple requests per tick\");\n }\n\n static WorldSnapshot World(double time, string? round = null) => new() { Time = time, RoundId = round, Participants = new[] { new ParticipantSnapshot { Id = \"p\" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = \"s\", Reachable = true, Spawnable = true, NearestParticipantDistance = 1000, ThreatSeparation = 3000 } } };\n\n static void Persistence()\n {\n var config = new DirectorConfiguration { Seed = 42 }; Check(DirectorStateCodec.ToJson(new DirectorCore(config).CaptureState()).Contains(\"HasTicked\"), \"pre-tick state serializes\"); var first = new DirectorCore(config); first.ApplyPressure(new(\"p\", PressureSeverity.Major, \"test\"), 0); var initial = first.Tick(World(0)); Check(initial.SpawnRequests.Count == 1, \"state setup request\");\n var json = DirectorStateCodec.ToJson(first.CaptureState()); Check(json.Contains(\"ConfigurationVersion\"), \"state JSON\");\n var restored = new DirectorCore(config, DirectorStateCodec.FromJson(json));\n var a = first.Tick(World(4)); var b = restored.Tick(World(4)); Check(a.Tempo == b.Tempo && a.TeamPressure == b.TeamPressure && a.SpawnRequests.SequenceEqual(b.SpawnRequests), \"restored core remains deterministic\");\n var diagnostics = new DirectorTelemetryBuffer(restored, 2); restored.ApplyPressure(new(\"p\", PressureSeverity.Minor, \"one\"), 4); restored.ApplyPressure(new(\"p\", PressureSeverity.Minor, \"two\"), 4); restored.ApplyPressure(new(\"p\", PressureSeverity.Minor, \"three\"), 4); Check(diagnostics.Snapshot().Count == 2, \"telemetry bounded\"); diagnostics.Clear(); Check(diagnostics.Snapshot().Count == 0, \"telemetry clear\");\n Check(restored.CancelPendingRequests().Count > 0, \"cancel pending\");\n }\n\n static void RuleSchedules()\n {\n var population = new PopulationLedger(new PopulationConfiguration()); var world = World(0); var random = new DeterministicRandom(4);\n var rule = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = \"boss\", Kind = EncounterKind.Boss, Count = 1, Priority = 10, CooldownSeconds = 30, Phases = new HashSet<TempoPhase> { TempoPhase.Relax } } });\n Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, \"rule selection\"); rule.SelectionFinalized(false); Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, \"failed rule rolls cooldown back\"); rule.SelectionFinalized(true); Check(rule.SelectEncounter(world, TempoPhase.Relax, 0, population, random) is null, \"rule cooldown committed\");\n var milestone = new MilestoneEncounterSchedule(\"boss-half\", EncounterKind.Boss, .5f); var halfway = new WorldSnapshot { Time = 0, TeamProgress = .6f, ModeId = \"campaign\", RoundId = \"r1\" };\n Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, \"milestone selected\"); milestone.SelectionFinalized(false); Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) == EncounterKind.Boss, \"milestone retry\"); milestone.SelectionFinalized(true); Check(milestone.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) is null, \"milestone one-shot per round\");\n var state = milestone.CaptureScheduleState(); var copy = new MilestoneEncounterSchedule(\"boss-half\", EncounterKind.Boss, .5f); copy.RestoreScheduleState(state); Check(copy.SelectEncounter(halfway, TempoPhase.Relax, 0, population, random) is null, \"milestone persistence\");\n }\n\n static void PersistentPolicies()\n {\n var configuration = new DirectorConfiguration();\n var policy = new StatefulPolicy(\"tests.policy\");\n var placement = new StatefulPlacementPolicy(\"tests.placement\");\n var composer = new StatefulComposer(\"tests.composer\");\n var original = new DirectorCore(configuration, policy: policy, placementPolicy: placement, composer: composer);\n original.Tick(World(0));\n Check(policy.CallCount > 0 && placement.CallCount > 0, \"custom policy state setup\");\n\n var json = DirectorStateCodec.ToJson(original.CaptureState());\n var restoredPolicy = new StatefulPolicy(\"tests.policy\");\n var restoredPlacement = new StatefulPlacementPolicy(\"tests.placement\");\n var restoredComposer = new StatefulComposer(\"tests.composer\");\n _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json), policy: restoredPolicy, placementPolicy: restoredPlacement, composer: restoredComposer);\n Check(restoredPolicy.CallCount == policy.CallCount, \"custom Director policy restored\");\n Check(restoredPlacement.CallCount == placement.CallCount, \"custom placement policy restored\");\n Check(restoredComposer.CallCount == composer.CallCount, \"custom composer restored\");\n\n var threw = false;\n try { _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json), policy: new StatefulPolicy(\"wrong.policy\"), placementPolicy: restoredPlacement, composer: restoredComposer); }\n catch (InvalidOperationException) { threw = true; }\n Check(threw, \"custom policy identity mismatch rejected\");\n\n threw = false;\n try { _ = new DirectorCore(configuration, DirectorStateCodec.FromJson(json)); }\n catch (InvalidOperationException) { threw = true; }\n Check(threw, \"custom policy state cannot be silently discarded\");\n }\n\n static void ConcurrentSchedules()\n {\n var schedule = new ConcurrentEncounterSchedule(new[]\n {\n new ScheduleLane { Id = \"ambient\", Priority = 10, Schedule = new BasicEncounterSchedule(100, 100) },\n new ScheduleLane { Id = \"secondary\", Priority = 5, Schedule = new BasicEncounterSchedule(100, 100) }\n });\n var core = new DirectorCore(new DirectorConfiguration { MaximumRequestsPerTick = 2 }, schedule: schedule);\n var output = core.Tick(World(0)); Check(output.SpawnRequests.Count == 2, \"independent schedule lanes share a tick\");\n var state = schedule.CaptureScheduleState(); var copy = new ConcurrentEncounterSchedule(new[] { new ScheduleLane { Id = \"ambient\", Priority = 10, Schedule = new BasicEncounterSchedule(100, 100) }, new ScheduleLane { Id = \"secondary\", Priority = 5, Schedule = new BasicEncounterSchedule(100, 100) } }); copy.RestoreScheduleState(state); Check(copy.CaptureScheduleState()[\"coordinator.served\"].Contains(\"ambient\"), \"concurrent schedule persistence\");\n }\n\n static void CompositionAndRetry()\n {\n var composer = new WeightedEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<WeightedArchetype>> { [EncounterKind.Ambient] = new[] { new WeightedArchetype(\"grunt\", 1) } });\n var configuration = new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 3, DelaySeconds = 1 } };\n var core = new DirectorCore(configuration, composer: composer); var first = core.Tick(World(0)).SpawnRequests.Single(); Check(first.Composition.Single() == new EncounterMember(\"grunt\", 1) && first.Attempt == 1, \"weighted composition\");\n core.ApplyResult(new(first.RequestId, RequestResultKind.Failed, 0, \"blocked\")); var saved = DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(core.CaptureState())); Check(saved.RetryQueue.Count == 1, \"retry persisted\");\n var restored = new DirectorCore(configuration, saved, composer: composer); Check(restored.Tick(World(.5)).SpawnRequests.Count == 0, \"retry delay\"); var retry = restored.Tick(World(1)).SpawnRequests.Single(); Check(retry.Attempt == 2 && retry.Composition.SequenceEqual(first.Composition), \"retry preserves attempt and composition\");\n restored.ApplyResult(new(retry.RequestId, RequestResultKind.PartiallySucceeded, 0, \"partial\")); Check(restored.CaptureState().RetryQueue.Single().RequestedCount == 1, \"partial remainder queued\");\n }\n\n static void Resources()\n {\n var rules = new[] { new ResourceRule { Category = \"healing\", TargetCount = 2, MaximumRequestsPerTick = 2 } }; var resources = new ResourcePopulationController(rules, 4);\n var core = new DirectorCore(new DirectorConfiguration(), resources: resources); var baseWorld = World(0); var world = new WorldSnapshot { Time = baseWorld.Time, Participants = baseWorld.Participants, SpawnCandidates = baseWorld.SpawnCandidates, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = \"r1\", Category = \"healing\", AreaCapacity = 2 }, new ResourceCandidate { Id = \"r2\", Category = \"healing\", AreaCapacity = 1 } } } };\n var output = core.Tick(world); Check(output.ResourceRequests.Count == 2, \"resource density requests\"); foreach (var request in output.ResourceRequests) core.ApplyResourceResult(new(request.RequestId, RequestResultKind.Succeeded));\n var state = core.CaptureState(); Check(state.Resources?.Consumed.Count == 2, \"resource revisit state captured\");\n var restoredResources = new ResourcePopulationController(rules, 9); _ = new DirectorCore(new DirectorConfiguration(), state, resources: restoredResources); Check(restoredResources.CaptureState().Consumed.Count == 2, \"resource state restored\");\n var threw = false; try { _ = new ResourcePopulationController(new[] { new ResourceRule { Category = \"healing\", TargetCount = 3 } }, state.Resources!.Value); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"resource definition mismatch rejected\");\n var clusterRules = new[] { new ResourceRule { Category = \"ammo\", TargetCount = 2, MaximumRequestsPerTick = 2, MaximumPerCluster = 1 } }; var clustered = new ResourcePopulationController(clusterRules); var clusteredWorld = new WorldSnapshot { Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = \"a\", Category = \"ammo\", ClusterId = \"room\" }, new ResourceCandidate { Id = \"b\", Category = \"ammo\", ClusterId = \"room\" } } } }; var clusterRequest = clustered.Tick(clusteredWorld.Resources, clusteredWorld).Single(); Check(clusterRequest.CandidateId is \"a\" or \"b\", \"resource cluster limit\"); clustered.ApplyResult(new(clusterRequest.RequestId, RequestResultKind.Succeeded));\n var clusterCopy = new ResourcePopulationController(clusterRules, clustered.CaptureState()); var changedSnapshot = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = \"replacement\", Category = \"ammo\", ClusterId = \"room\" } } }; Check(clusterCopy.Tick(changedSnapshot, new WorldSnapshot { Resources = changedSnapshot }).Count == 0, \"resource cluster history survives absent candidates\");\n threw = false; try { _ = new ResourcePopulationController(new[] { new ResourceRule { Category = \"healing\", TargetCount = 2, MaximumRequestsPerTick = 2, RequiredTags = new HashSet<string> { \"locked\" } } }, state.Resources!.Value); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"resource required-tag mismatch rejected\");\n }\n\n static void Orchestration()\n {\n var orchestrationConfiguration = new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition(\"boss\", Encounter: EncounterKind.Boss, MusicCue: \"boss_intro\"), new ScenarioStageDefinition(\"escape\") }, Waves = new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 }, WaveEncounterCount = 3 };\n var orchestration = new DirectorOrchestration(orchestrationConfiguration); var core = new DirectorCore(new DirectorConfiguration(), orchestration: orchestration); core.StartScenario(0); var stage = core.Tick(World(0)); Check(stage.SpawnRequests.Single().Kind == EncounterKind.Boss && stage.Events.Any(x => x.Name == \"music_cue\"), \"scenario encounter and music cue\"); core.ApplyResult(new(stage.SpawnRequests[0].RequestId, RequestResultKind.Succeeded, 1)); core.Tick(World(1)); Check(orchestration.ScenarioStatus == ScenarioStatus.Running, \"scenario advances stages\");\n var saved = core.CaptureState(); var restoredOrchestration = new DirectorOrchestration(orchestrationConfiguration, saved.Orchestration!.Value); _ = new DirectorCore(new DirectorConfiguration(), saved, orchestration: restoredOrchestration); Check(restoredOrchestration.ScenarioStatus == orchestration.ScenarioStatus, \"orchestration persistence\");\n var threw = false; try { _ = new DirectorOrchestration(new DirectorOrchestrationConfiguration { PersistenceId = \"wrong\" }, saved.Orchestration.Value); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"orchestration definition mismatch rejected\");\n threw = false; try { _ = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition(\"changed\") } }, saved.Orchestration.Value); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"orchestration content mismatch rejected\");\n threw = false; try { _ = new DirectorCore(new DirectorConfiguration(), saved, orchestration: new DirectorOrchestration(orchestrationConfiguration)); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"fresh orchestration cannot replace restored state\");\n var waves = new DirectorOrchestration(orchestrationConfiguration); var waveCore = new DirectorCore(new DirectorConfiguration(), orchestration: waves); waveCore.StartWaveSequence(0); waveCore.Tick(World(0)); var issued = waveCore.Tick(World(0)); Check(issued.SpawnRequests.Single().Kind == EncounterKind.CommonWave && issued.SpawnRequests[0].RequestedCount == 3, \"integrated panic wave\");\n\n var retryConfiguration = new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 2, DelaySeconds = 0 } };\n var retryScenario = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition(\"required\", Encounter: EncounterKind.Boss) } }); var retryScenarioCore = new DirectorCore(retryConfiguration, orchestration: retryScenario); retryScenarioCore.StartScenario(0); var failedStage = retryScenarioCore.Tick(World(0)).SpawnRequests.Single(); retryScenarioCore.ApplyResult(new(failedStage.RequestId, RequestResultKind.Failed, 0)); var stageRetry = retryScenarioCore.Tick(World(0)).SpawnRequests.Single(); Check(stageRetry.Attempt == 2 && retryScenario.ScenarioStatus == ScenarioStatus.Running, \"scenario waits for retried host result\"); retryScenarioCore.ApplyResult(new(stageRetry.RequestId, RequestResultKind.Succeeded, 1)); Check(retryScenarioCore.Tick(World(0)).Events.Any(x => x.Name == \"scenario_advanced\") && retryScenario.ScenarioStatus == ScenarioStatus.Complete, \"scenario advances after retry success\");\n var retryWaves = new DirectorOrchestration(new DirectorOrchestrationConfiguration { Waves = new WaveSequenceConfiguration { InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = 0, PauseMaximum = 0 } }); var retryWaveCore = new DirectorCore(retryConfiguration, orchestration: retryWaves); retryWaveCore.StartWaveSequence(0); retryWaveCore.Tick(World(0)); var failedWave = retryWaveCore.Tick(World(0)).SpawnRequests.Single(); retryWaveCore.ApplyResult(new(failedWave.RequestId, RequestResultKind.Failed, 0)); var waveRetry = retryWaveCore.Tick(World(0)).SpawnRequests.Single(); Check(waveRetry.Attempt == 2 && retryWaves.WaveActive, \"wave retry bypasses ordinary-schedule suppression\"); retryWaveCore.ApplyResult(new(waveRetry.RequestId, RequestResultKind.Succeeded, waveRetry.RequestedCount)); Check(retryWaveCore.Tick(World(0)).Events.Any(x => x.Name == \"wave_sequence_complete\"), \"wave waits for retry success before completion\");\n var terminalOrchestration = new DirectorOrchestration(new DirectorOrchestrationConfiguration { ScenarioStages = new[] { new ScenarioStageDefinition(\"required\", Encounter: EncounterKind.Boss) } }); var terminalCore = new DirectorCore(new DirectorConfiguration { Retry = new RetryConfiguration { MaximumAttempts = 1 } }, orchestration: terminalOrchestration); terminalCore.StartScenario(0); var terminalRequest = terminalCore.Tick(World(0)).SpawnRequests.Single(); terminalCore.ApplyResult(new(terminalRequest.RequestId, RequestResultKind.Rejected, 0, \"veto\")); Check(terminalCore.Tick(World(0)).Events.Any(x => x.Name == \"orchestration_failed\") && terminalOrchestration.ScenarioStatus == ScenarioStatus.Failed, \"terminal orchestration failure is explicit\");\n }\n\n static void AdaptiveCallbacks()\n {\n var policy = new AdaptivePolicy(); var core = new DirectorCore(new DirectorConfiguration(), policy: policy, schedule: new SingleKindSchedule(EncounterKind.Boss)); var output = core.Tick(World(0)); Check(output.SpawnRequests.Single().RequestedCount == 1 && output.MusicIntensity == .5f && output.Events.Any(x => x.Name == \"boss_music\"), \"adaptive count music and boss callback\");\n var populationCore = new DirectorCore(new DirectorConfiguration(), policy: policy, schedule: new SingleKindSchedule(EncounterKind.Ambient)); Check(populationCore.Tick(World(0)).SpawnRequests.Single().RequestedCount == 1, \"adaptive population limit\");\n var raisedLimit = new DirectorCore(new DirectorConfiguration { Population = new PopulationConfiguration { Limits = new Dictionary<EncounterKind, int> { [EncounterKind.Ambient] = 0 } } }, policy: policy, schedule: new SingleKindSchedule(EncounterKind.Ambient)); Check(raisedLimit.Tick(World(0)).SpawnRequests.Single().RequestedCount == 1, \"adaptive population policy can raise configured limit\");\n var timing = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = \"timed\", Kind = EncounterKind.Ambient, CooldownSeconds = 10, Phases = new HashSet<TempoPhase> { TempoPhase.Relax } } }, new ModeEncounterTimingPolicy(new Dictionary<string, double> { [\"survival\"] = .5 })); var ledger = new PopulationLedger(new PopulationConfiguration()); var timedWorld = new WorldSnapshot { Time = 0, ModeId = \"survival\" }; Check(timing.SelectEncounter(timedWorld, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is not null, \"adaptive timing initial\"); timing.SelectionFinalized(true); Check(timing.SelectEncounter(new WorldSnapshot { Time = 4.9, ModeId = \"survival\" }, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is null && timing.SelectEncounter(new WorldSnapshot { Time = 5, ModeId = \"survival\" }, TempoPhase.Relax, 0, ledger, new DeterministicRandom(1)) is not null, \"mode cooldown multiplier\");\n var decisions = 0; var runner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), new ContinuousSessionFlow(), new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(x => new(x.RequestId, RequestResultKind.Succeeded, x.RequestedCount)), notifications: new DelegateNotificationSink(_ => decisions++)); runner.Update(); Check(decisions == 1, \"decision notification callback\");\n }\n\n static void HostRunner()\n {\n var core = new DirectorCore(new DirectorConfiguration()); var flow = new ContinuousSessionFlow(); var executed = 0;\n var runner = new DirectorHostRunner(core, flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(request => { executed++; return new(request.RequestId, RequestResultKind.Succeeded, request.RequestedCount); }));\n var result = runner.Update(); Check(result.Status == DirectorRunStatus.Updated && executed == 1 && result.Results.Count == 1, \"host executes decisions\");\n var denied = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => throw new Exception(\"must not capture\")), new DelegateSpawnExecutor(_ => throw new Exception()), new TestAuthority(false)); Check(denied.Update().Status == DirectorRunStatus.NotAuthoritative, \"authority gate\");\n flow.IsSessionActive = false; Check(runner.Update().Status == DirectorRunStatus.SessionInactive, \"session gate\");\n flow.IsSessionActive = true; var failureRunner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(_ => throw new InvalidOperationException(\"host failure\"))); Check(failureRunner.Update().Results[0].Result == RequestResultKind.Failed, \"host exception becomes result\");\n var malformedRunner = new DirectorHostRunner(new DirectorCore(new DirectorConfiguration()), flow, new DelegateWorldSource(() => World(0)), new DelegateSpawnExecutor(request => new(request.RequestId + 99, RequestResultKind.Succeeded, request.RequestedCount + 1))); var malformed = malformedRunner.Update().Results.Single(); Check(malformed.RequestId == 1 && malformed.Result == RequestResultKind.Failed && malformed.SpawnedCount == 0, \"host runner normalizes malformed result\");\n }\n\n static void OwnershipAndResources()\n {\n var allocator = new EncounterOwnershipAllocator(); var first = allocator.Select(new[] { new OwnershipCandidate(\"a\", 1, false), new OwnershipCandidate(\"b\", 1) }, new DeterministicRandom(1)); Check(first == \"b\", \"ownership eligibility\");\n var r1 = new DeterministicRandom(9); var r2 = new DeterministicRandom(9); var candidates = new[] { new OwnershipCandidate(\"a\", 1), new OwnershipCandidate(\"b\", 3) }; Check(allocator.Select(candidates, r1) == allocator.Select(candidates, r2), \"ownership deterministic\");\n var budget = new ResourceBudget(new Dictionary<string, float> { [\"health\"] = 2 }); Check(budget.TrySpend(\"health\", 1.5f) && !budget.TrySpend(\"health\", 1), \"resource budget\"); Near(budget.Capture()[\"health\"], .5f, .0001f, \"resource capture\");\n }\n\n static MusicEventCatalog TestMusicCatalog() => new(new[]\n {\n new MusicEventDefinition { EventId = \"ambient.safe\", TrackId = \"ambient\", Priority = MusicPriority.Low },\n new MusicEventDefinition { EventId = \"mobbed\", TrackId = \"mob\", Priority = MusicPriority.High, DuckTrackList = new[] { \"all\" } },\n new MusicEventDefinition { EventId = \"boss\", TrackId = \"boss\", Priority = MusicPriority.Critical, BlockTrackList = new[] { \"combat\" } },\n new MusicEventDefinition { EventId = \"combat.low\", TrackId = \"combat\", Priority = MusicPriority.Low },\n new MusicEventDefinition { EventId = \"combat.high\", TrackId = \"combat\", Priority = MusicPriority.High },\n new MusicEventDefinition { EventId = \"delayed\", TrackId = \"stinger\", Priority = MusicPriority.Medium, DelaySeconds = 2 },\n new MusicEventDefinition { EventId = \"intro\", TrackId = \"intro\", Priority = MusicPriority.High, AutoQueueEventId = \"loop\" },\n new MusicEventDefinition { EventId = \"loop\", TrackId = \"loop\", Priority = MusicPriority.High },\n new MusicEventDefinition { EventId = \"death\", TrackId = \"death\", Priority = MusicPriority.Critical, Flags = MusicMasterFlags.AllowAfterDeath },\n new MusicEventDefinition { EventId = \"tag.master\", TrackId = \"master\", Priority = MusicPriority.High, TimingTags = new[] { new MusicTimingTag(2, 4) }, LoopSeconds = 8 },\n new MusicEventDefinition { EventId = \"tag.child\", TrackId = \"child\", Priority = MusicPriority.High, SyncTrackId = \"master\", SyncTagIndex = 2 },\n new MusicEventDefinition { EventId = \"split.a\", TrackId = \"split\", Priority = MusicPriority.High },\n new MusicEventDefinition { EventId = \"split.b\", TrackId = \"split\", Priority = MusicPriority.High, Flags = MusicMasterFlags.PlaySplit }\n });\n\n static MusicDecisionBatch MusicBatch(double time, params MusicDecision[] decisions)\n {\n var batch = new MusicDecisionBatch { Time = time }; batch.Decisions.AddRange(decisions); return batch;\n }\n\n static void MusicAuthority()\n {\n var special = new SpecialMusicAlertConfiguration { ArchetypeId = \"ambusher\", CloseEvent = \"alert.close\", MiddleEvent = \"alert.middle\", FarEvent = \"alert.far\" };\n var cues = new MusicCueConfiguration { CombatIntroEvents = new[] { \"combat.intro\" }, CombatSecondaryEvent = \"combat.secondary\", CombatCloseEvent = \"combat.close\", MobbedEvent = \"mobbed\", BossApproachingEvent = \"boss\", BossTrackId = \"boss\", HazardBurningEvent = \"hazard.burn\", HazardAttackEvent = \"hazard.attack\", HazardRageEvent = \"hazard.rage\", SafeAtmosphereEvent = \"ambient.safe\", DangerAtmosphereEvent = \"ambient.danger\", PositionalStingerEvent = \"choir\" };\n var configuration = RecoveredMusicDirectorDefaults.CreateConfiguration(cues, new[] { special }, 41);\n var core = new MusicDirectorCore(configuration);\n var idle = new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } };\n var initial = core.Tick(idle); Check(initial.Decisions.Count(x => x.Kind == MusicDecisionKind.SetMixerLayer) == 9, \"music authority emits initial mixer snapshot\");\n var combatInput = new MusicParticipantSnapshot { Id = \"p\", CumulativeDamage = 10, VisibleCommonThreats = 3, ActiveCommonThreats = 20, ScannedCommonThreats = 5, VeryCloseCommonFactor = 1, AttackingCommonFactor = .4f, CloseCommonAction = .42f, InflictedDamage = true, SpecialThreats = new[] { new SpecialMusicThreat(\"ambusher\", 1000, \"s1\") } };\n var combat = core.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { combatInput } });\n Check(combat.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == \"combat.intro\"), \"music authority starts combat group\");\n Check(combat.Decisions.Any(x => x.TargetId == \"alert.close\"), \"music authority selects close special alert\");\n Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.MobBeating).Value, .37f, .0001f, \"recovered damage duck equation\");\n Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.MobRules).Value, 0f, .0001f, \"recovered mob pressure equation\");\n Near(combat.Decisions.Single(x => x.LayerId == MusicMixerLayers.CombatClose).Value, 0f, .0001f, \"recovered close action equation\");\n\n var boss = core.Tick(new MusicWorldSnapshot { Time = 2, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\", BossPresent = true } } });\n Check(boss.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == \"boss\"), \"music boss approach transition\");\n var defeated = core.Tick(new MusicWorldSnapshot { Time = 3, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } });\n Check(defeated.Decisions.Any(x => x.Kind == MusicDecisionKind.StopTrack && x.TargetId == \"boss\" && x.FadeSeconds == 2), \"music boss defeat track stop\");\n var hazard = core.Tick(new MusicWorldSnapshot { Time = 4, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\", HazardPresent = true, HazardBurning = true, HazardAttacking = true, HazardRage = .75f, HazardEmitterId = \"h\", HazardPosition = new DirectorVector(1, 2, 3), WanderingHazards = new[] { new WanderingMusicHazard(\"w\", .2f, new DirectorVector(4, 5, 6)) } } } });\n Check(new[] { \"hazard.burn\", \"hazard.attack\", \"hazard.rage\" }.All(id => hazard.Decisions.Any(x => x.Kind == MusicDecisionKind.Play && x.TargetId == id)), \"hazard transition cues\");\n Near(hazard.Decisions.Single(x => x.LayerId == MusicMixerLayers.HazardRage).Value, .25f, .0001f, \"hazard rage inverse layer\");\n Check(hazard.Decisions.Any(x => x.TargetId == \"hazard.wandering.tier4\" && x.Position is not null), \"wandering hazard low-anger tier and position\");\n\n var generic = core.Tick(new MusicWorldSnapshot { Time = 5, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } }, new[] { new MusicGameplayEvent(1, MusicTriggerKind.Play, \"p\", \"custom.event\") });\n Check(generic.Decisions.Any(x => x.TargetId == \"custom.event\"), \"generic gameplay music event\");\n var duplicate = core.Tick(new MusicWorldSnapshot { Time = 6, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } }, new[] { new MusicGameplayEvent(1, MusicTriggerKind.Play, \"p\", \"should.not.repeat\") });\n Check(!duplicate.Decisions.Any(x => x.TargetId == \"should.not.repeat\"), \"music events apply exactly once\");\n var reveal = core.Tick(new MusicWorldSnapshot { Time = 7, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } }, new[] { new MusicGameplayEvent(2, MusicTriggerKind.LargeAreaRevealed, \"p\") });\n var revealLimited = core.Tick(new MusicWorldSnapshot { Time = 8, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } }, new[] { new MusicGameplayEvent(3, MusicTriggerKind.LargeAreaRevealed, \"p\") });\n Check(reveal.Decisions.Any(x => x.TargetId == \"world.large_area_reveal\") && !revealLimited.Decisions.Any(x => x.TargetId == \"world.large_area_reveal\"), \"large-area reveal cooldown\");\n }\n\n static void MusicRuntimeArbitration()\n {\n var catalog = TestMusicCatalog(); var runtime = new MusicRuntime(\"p\", catalog);\n var ambient = runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, \"p\", \"ambient.safe\", \"test\")));\n Check(ambient.Actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == \"ambient.safe\"), \"runtime plays resolved event\");\n Check(runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, \"p\", \"ambient.safe\", \"duplicate\"))).Actions.Count == 0, \"runtime decisions apply exactly once\");\n var mob = runtime.Apply(MusicBatch(1, new MusicDecision(2, MusicDecisionKind.Play, \"p\", \"mobbed\", \"test\")));\n Check(mob.Actions.Any(x => x.Kind == MusicRuntimeActionKind.SetTrackVolume && x.TrackId == \"ambient\" && x.Value == .5f), \"runtime duck list\");\n Check(!mob.Actions.Any(x => x.Kind == MusicRuntimeActionKind.SetTrackVolume && x.TrackId == \"mob\" && x.Value == .5f), \"duck source does not duck itself\");\n runtime.Apply(MusicBatch(2, new MusicDecision(3, MusicDecisionKind.Play, \"p\", \"boss\", \"test\")));\n var blocked = runtime.Apply(MusicBatch(3, new MusicDecision(4, MusicDecisionKind.Play, \"p\", \"combat.high\", \"test\")));\n Check(blocked.Results.Single(x => x.DecisionId == 4).Result == MusicDecisionResultKind.RejectedBlocked, \"runtime active block list\");\n\n var priority = new MusicRuntime(\"p\", catalog); priority.Apply(MusicBatch(0, new MusicDecision(10, MusicDecisionKind.Play, \"p\", \"combat.low\", \"test\")));\n var high = priority.Apply(MusicBatch(1, new MusicDecision(11, MusicDecisionKind.Play, \"p\", \"combat.high\", \"test\")));\n Check(high.Actions.Any(x => x.Kind == MusicRuntimeActionKind.StopEvent && x.EventId == \"combat.low\") && high.Actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == \"combat.high\"), \"higher priority replaces same track\");\n var rejectedLow = priority.Apply(MusicBatch(2, new MusicDecision(12, MusicDecisionKind.Play, \"p\", \"combat.low\", \"test\")));\n Check(rejectedLow.Results.Single(x => x.DecisionId == 12).Result == MusicDecisionResultKind.RejectedPriority, \"lower priority cannot replace active track\");\n\n var delayed = new MusicRuntime(\"p\", catalog); var deferred = delayed.Apply(MusicBatch(0, new MusicDecision(20, MusicDecisionKind.Play, \"p\", \"delayed\", \"test\")));\n Check(deferred.Results.Single().Result == MusicDecisionResultKind.Deferred && delayed.Tick(1).Actions.Count == 0, \"runtime delay queue\");\n Check(delayed.Tick(2).Actions.Any(x => x.EventId == \"delayed\"), \"runtime delayed engagement\");\n\n var tagged = new MusicRuntime(\"p\", catalog); tagged.Apply(MusicBatch(0, new MusicDecision(30, MusicDecisionKind.Play, \"p\", \"tag.master\", \"test\")));\n var tagDeferred = tagged.Apply(MusicBatch(1, new MusicDecision(31, MusicDecisionKind.Play, \"p\", \"tag.child\", \"test\")));\n Check(tagDeferred.Results.Single().Result == MusicDecisionResultKind.Deferred && tagged.Tick(3.9).Actions.Count == 0 && tagged.Tick(4).Actions.Any(x => x.EventId == \"tag.child\"), \"runtime timing-tag alignment\");\n\n var split = new MusicRuntime(\"p\", catalog); split.Apply(MusicBatch(0, new MusicDecision(35, MusicDecisionKind.Play, \"p\", \"split.a\", \"test\"))); split.Apply(MusicBatch(1, new MusicDecision(36, MusicDecisionKind.Play, \"p\", \"split.b\", \"test\")));\n Check(split.Active.Count == 2, \"runtime split flag permits concurrent same-track events\");\n\n var auto = new MusicRuntime(\"p\", catalog); var intro = auto.Apply(MusicBatch(0, new MusicDecision(40, MusicDecisionKind.Play, \"p\", \"intro\", \"test\"))); var instance = intro.Results.Single().InstanceId;\n Check(auto.ReportCompleted(instance, 1).Actions.Any(x => x.EventId == \"loop\"), \"runtime autoqueue on completion\");\n auto.SetLifecycle(true, false); var deadReject = auto.Apply(MusicBatch(2, new MusicDecision(41, MusicDecisionKind.Play, \"p\", \"ambient.safe\", \"test\"), new MusicDecision(42, MusicDecisionKind.Play, \"p\", \"death\", \"test\")));\n Check(deadReject.Results.Single(x => x.DecisionId == 41).Result == MusicDecisionResultKind.RejectedLifecycle && deadReject.Results.Single(x => x.DecisionId == 42).Result == MusicDecisionResultKind.Accepted, \"runtime after-death gate\");\n var general = auto.Apply(MusicBatch(3, new MusicDecision(43, MusicDecisionKind.General, \"p\", \"custom.command\", \"test\") { Arguments = new[] { \"a\", \"b\" } }));\n Check(general.Actions.Single(x => x.Kind == MusicRuntimeActionKind.General).Arguments.SequenceEqual(new[] { \"a\", \"b\" }), \"runtime forwards generalized commands\");\n }\n\n static void MusicPersistenceAndHost()\n {\n var config = RecoveredMusicDirectorDefaults.CreateConfiguration(seed: 99); var first = new MusicDirectorCore(config); first.Tick(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } });\n first.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\", VisibleCommonThreats = 2, WeaponActivity = .5f } } });\n var restored = new MusicDirectorCore(config, MusicStateCodec.DirectorFromJson(MusicStateCodec.DirectorToJson(first.CaptureState())));\n var nextWorld = new MusicWorldSnapshot { Time = 2, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\", VisibleCommonThreats = 1 } } };\n Check(first.Tick(nextWorld).Decisions.SequenceEqual(restored.Tick(nextWorld).Decisions), \"music authority save restore determinism\");\n\n var catalog = TestMusicCatalog(); var runtime = new MusicRuntime(\"p\", catalog); runtime.Apply(MusicBatch(0, new MusicDecision(1, MusicDecisionKind.Play, \"p\", \"ambient.safe\", \"test\"), new MusicDecision(2, MusicDecisionKind.Play, \"p\", \"delayed\", \"test\")));\n var restoredRuntime = new MusicRuntime(\"p\", catalog, MusicStateCodec.RuntimeFromJson(MusicStateCodec.RuntimeToJson(runtime.CaptureState())));\n Check(runtime.Tick(2).Actions.SequenceEqual(restoredRuntime.Tick(2).Actions), \"music runtime save restore determinism\");\n\n var actions = new List<MusicRuntimeAction>(); var hostCore = new MusicDirectorCore(RecoveredMusicDirectorDefaults.CreateConfiguration(new MusicCueConfiguration { CombatIntroEvents = new[] { \"ambient.safe\" }, CombatSecondaryEvent = \"\", CombatCloseEvent = \"\", SafeAtmosphereEvent = \"\", DangerAtmosphereEvent = \"\", PositionalStingerEvent = \"\" })); var hostRuntime = new MusicRuntime(\"p\", catalog); var runner = new MusicDirectorHostRunner(hostCore, hostRuntime, new DelegateMusicAudioSink(actions.Add));\n runner.Update(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\" } } }); runner.Update(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = \"p\", VisibleCommonThreats = 1 } } });\n Check(actions.Any(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == \"ambient.safe\"), \"music host runner bridges decisions to audio sink\");\n }\n\n static void Validation()\n {\n var backwards = new DirectorCore(new DirectorConfiguration()); backwards.Tick(World(2)); var threw = false; try { backwards.Tick(World(1)); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"reject backwards world time\");\n threw = false; try { _ = new DirectorConfiguration { MaximumRequestsPerTick = 0 }; new DirectorConfiguration { MaximumRequestsPerTick = 0 }.Validate(); } catch (ArgumentOutOfRangeException) { threw = true; }\n Check(threw, \"configuration validation\");\n threw = false; try { _ = DirectorStateCodec.FromJson(\"{}\"); } catch (InvalidOperationException) { threw = true; }\n Check(threw, \"state validation\");\n threw = false; try { new WorldSnapshot { Participants = new[] { new ParticipantSnapshot { Id = \"same\" }, new ParticipantSnapshot { Id = \"same\" } } }.Validate(); } catch (ArgumentException) { threw = true; }\n Check(threw, \"duplicate participant validation\");\n var incomplete = new DirectorCore(new DirectorConfiguration()); var incompleteRequest = incomplete.Tick(World(0)).SpawnRequests.Single(); incomplete.ApplyResult(new(incompleteRequest.RequestId, RequestResultKind.Succeeded, 0)); Check(incomplete.CaptureState().RetryQueue.Count == 1, \"incomplete success becomes partial retry\");\n var excessive = new DirectorCore(new DirectorConfiguration()); var excessiveRequest = excessive.Tick(World(0)).SpawnRequests.Single(); threw = false; try { excessive.ApplyResult(new(excessiveRequest.RequestId, RequestResultKind.Succeeded, excessiveRequest.RequestedCount + 1)); } catch (ArgumentOutOfRangeException) { threw = true; }\n Check(threw, \"excessive host result rejected\");\n }\n\n static void LongDeterminism()\n {\n var configuration = new DirectorConfiguration { Seed = 7823, RequestTimeoutSeconds = 2 }; DirectorCore first = new(configuration); DirectorCore second = new(configuration);\n for (var index = 0; index < 1000; index++)\n {\n var time = index * .1;\n if (index % 73 == 0) { var pressure = new PressureEvent(\"p\", (PressureSeverity)(index / 73 % 4 + 1), \"simulation\"); first.ApplyPressure(pressure, time); second.ApplyPressure(pressure, time); }\n var world = World(time); var a = first.Tick(world); var b = second.Tick(world);\n Check(a.Tempo == b.Tempo && a.TeamPressure == b.TeamPressure && a.MusicIntensity == b.MusicIntensity && a.SpawnRequests.SequenceEqual(b.SpawnRequests) && a.Events.SequenceEqual(b.Events), $\"deterministic tick {index}\");\n foreach (var request in a.SpawnRequests) { var result = new SpawnRequestResult(request.RequestId, RequestResultKind.Succeeded, request.RequestedCount); first.ApplyResult(result); second.ApplyResult(result); }\n if (index == 500) second = new DirectorCore(configuration, DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(second.CaptureState())));\n }\n }\n\n sealed class RejectAllPlacement : IPlacementPolicy { public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => false; public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0; }\n sealed class TestAuthority : IDirectorAuthority { public TestAuthority(bool value) { IsAuthoritative = value; } public bool IsAuthoritative { get; } }\n sealed class RepeatingSchedule : IEncounterSchedule { int _index; public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random) => _index++ % 2 == 0 ? EncounterKind.Ambient : EncounterKind.Special; public int RequestedCount(EncounterKind kind, WorldSnapshot world) => 1; }\n sealed class SingleKindSchedule : IEncounterSchedule { readonly EncounterKind _kind; public SingleKindSchedule(EncounterKind kind) { _kind = kind; } public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random) => _kind; public int RequestedCount(EncounterKind kind, WorldSnapshot world) => 2; }\n sealed class AdaptivePolicy : IDirectorPolicy, IAdaptiveDirectorPolicy, IAdaptivePopulationPolicy\n {\n public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) => true;\n public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => true;\n public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => \"boss\";\n public int AdjustEncounterCount(EncounterKind kind, int proposedCount, TempoPhase tempo, float teamPressure, WorldSnapshot world) => 1;\n public float AdjustMusicIntensity(float proposedIntensity, TempoPhase tempo, float teamPressure, WorldSnapshot world) => .5f;\n public bool ShouldPlayBossMusic(EncounterKind kind, WorldSnapshot world) => true;\n public int AdjustPopulationLimit(EncounterKind kind, int configuredLimit, WorldSnapshot world) => 1;\n }\n\n static void AdvancedDeterminism()\n {\n var configuration = new DirectorConfiguration { Seed = 99, MaximumRequestsPerTick = 3, Retry = new RetryConfiguration { MaximumAttempts = 3, DelaySeconds = .5 } };\n DirectorCore first = BuildAdvancedCore(configuration); DirectorCore second = BuildAdvancedCore(configuration); first.StartScenario(0); second.StartScenario(0); first.StartWaveSequence(0, 2); second.StartWaveSequence(0, 2);\n for (var index = 0; index < 500; index++)\n {\n var baseWorld = World(index * .25); var world = new WorldSnapshot { Time = baseWorld.Time, Participants = baseWorld.Participants, SpawnCandidates = baseWorld.SpawnCandidates, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = \"supply-a\", Category = \"supply\", AreaCapacity = 2 }, new ResourceCandidate { Id = \"supply-b\", Category = \"supply\", AreaCapacity = 1 } } } };\n var a = first.Tick(world); var b = second.Tick(world); Check(a.SpawnRequests.SequenceEqual(b.SpawnRequests) && a.ResourceRequests.SequenceEqual(b.ResourceRequests) && a.Events.SequenceEqual(b.Events) && a.MusicIntensity == b.MusicIntensity, $\"advanced deterministic tick {index}\");\n foreach (var request in a.SpawnRequests) { var result = new SpawnRequestResult(request.RequestId, request.RequestId % 7 == 0 ? RequestResultKind.Failed : RequestResultKind.Succeeded, request.RequestId % 7 == 0 ? 0 : request.RequestedCount); first.ApplyResult(result); second.ApplyResult(result); }\n foreach (var request in a.ResourceRequests) { var result = new ResourceSpawnResult(request.RequestId, RequestResultKind.Succeeded); first.ApplyResourceResult(result); second.ApplyResourceResult(result); }\n if (index == 250) second = BuildAdvancedCore(configuration, DirectorStateCodec.FromJson(DirectorStateCodec.ToJson(second.CaptureState())));\n }\n }\n\n static DirectorCore BuildAdvancedCore(DirectorConfiguration configuration, DirectorState? state = null)\n {\n IEncounterSchedule Schedule() => new ConcurrentEncounterSchedule(new[] { new ScheduleLane { Id = \"ambient\", Priority = 10, Schedule = new BasicEncounterSchedule(2, 4) }, new ScheduleLane { Id = \"special\", Priority = 5, Schedule = new RuleBasedEncounterSchedule(new[] { new EncounterRule { Id = \"special\", Kind = EncounterKind.Special, Count = 1, CooldownSeconds = 5, Probability = .5f, Phases = new HashSet<TempoPhase> { TempoPhase.Relax, TempoPhase.BuildUp, TempoPhase.SustainPeak } } }) } });\n var composer = new WeightedEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<WeightedArchetype>> { [EncounterKind.CommonWave] = new[] { new WeightedArchetype(\"grunt\", 3), new WeightedArchetype(\"brute\", 1) } });\n var resources = new ResourcePopulationController(new[] { new ResourceRule { Category = \"supply\", TargetCount = 2, MaximumRequestsPerTick = 2 } }, 77);\n var orchestrationConfiguration = new DirectorOrchestrationConfiguration { PersistenceId = \"advanced-test-v1\", ScenarioStages = new[] { new ScenarioStageDefinition(\"opening\", .5), new ScenarioStageDefinition(\"boss\", Encounter: EncounterKind.Boss), new ScenarioStageDefinition(\"done\") }, Waves = new WaveSequenceConfiguration { WaveCount = 2, InitialDelayMinimum = 0, InitialDelayMaximum = 0, CombatTimeout = 0, PauseMinimum = .25, PauseMaximum = .25 } };\n if (state is null) return new(configuration, schedule: Schedule(), composer: composer, resources: resources, orchestration: new(orchestrationConfiguration));\n var orchestration = state.Value.Orchestration is { } orchestrationState ? new DirectorOrchestration(orchestrationConfiguration, orchestrationState) : null;\n return new(configuration, state.Value, schedule: Schedule(), composer: composer, resources: resources, orchestration: orchestration);\n }\n sealed class StatefulPolicy : IDirectorPolicy, IPersistentDirectorPolicy\n {\n public StatefulPolicy(string id) { PersistenceId = id; }\n public string PersistenceId { get; }\n public int CallCount { get; private set; }\n public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) { CallCount++; return true; }\n public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => true;\n public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => kind.ToString();\n public IReadOnlyDictionary<string, string> CapturePolicyState() => new Dictionary<string, string> { [\"calls\"] = CallCount.ToString(CultureInfo.InvariantCulture) };\n public void RestorePolicyState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state[\"calls\"], CultureInfo.InvariantCulture);\n }\n sealed class StatefulPlacementPolicy : IPlacementPolicy, IPersistentPlacementPolicy\n {\n public StatefulPlacementPolicy(string id) { PersistenceId = id; }\n public string PersistenceId { get; }\n public int CallCount { get; private set; }\n public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) { CallCount++; return true; }\n public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0;\n public IReadOnlyDictionary<string, string> CapturePlacementState() => new Dictionary<string, string> { [\"calls\"] = CallCount.ToString(CultureInfo.InvariantCulture) };\n public void RestorePlacementState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state[\"calls\"], CultureInfo.InvariantCulture);\n }\n sealed class StatefulComposer : IEncounterComposer, IPersistentEncounterComposer\n {\n public StatefulComposer(string id) { PersistenceId = id; }\n public string PersistenceId { get; }\n public int CallCount { get; private set; }\n public IReadOnlyList<EncounterMember> Compose(EncounterKind kind, int requestedCount, WorldSnapshot world, IDirectorPolicy policy, DeterministicRandom random) { CallCount++; return new[] { new EncounterMember(\"stateful\", requestedCount) }; }\n public IReadOnlyDictionary<string, string> CaptureComposerState() => new Dictionary<string, string> { [\"calls\"] = CallCount.ToString(CultureInfo.InvariantCulture) };\n public void RestoreComposerState(IReadOnlyDictionary<string, string> state) => CallCount = int.Parse(state[\"calls\"], CultureInfo.InvariantCulture);\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/MusicDirectorCore.cs",
"FileName": "MusicDirectorCore.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic sealed record MusicEnvelopeState(float Damage, float FastActivity, float SlowActivity, float DamageInflicted, float CommonSight, float MobPressure, float Action, float Threat, float Calm, float AmbientBasis);\n\npublic sealed record MusicParticipantState(\n string ParticipantId,\n float PreviousDamage,\n MusicEnvelopeState Envelopes,\n bool CombatActive,\n bool MobbedActive,\n bool BossPresent,\n bool HazardBurning,\n bool HazardAttacking,\n bool HazardRaging,\n bool WanderingHazardActive,\n bool DangerAtmosphereActive,\n bool ScenarioEnded,\n double AtmosphereReadyAt,\n double PositionalStingerReadyAt,\n double WanderingHazardReadyAt,\n double LargeAreaRevealReadyAt,\n IReadOnlyDictionary<string, double> SpecialAlertReadyAt,\n IReadOnlyDictionary<string, float> LastMixerLayers);\n\npublic sealed record MusicDirectorState(\n string ConfigurationVersion,\n bool HasTicked,\n double LastTickTime,\n long LastGameplayEventSequence,\n long NextDecisionId,\n RandomState Random,\n IReadOnlyList<MusicParticipantState> Participants);\n\npublic sealed class MusicDirectorCore\n{\n readonly MusicDirectorConfiguration _configuration;\n DeterministicRandom _random;\n readonly Dictionary<string, ParticipantMemory> _participants = new(StringComparer.Ordinal);\n long _lastEventSequence;\n long _nextDecisionId = 1;\n double _lastTickTime;\n bool _hasTicked;\n\n public MusicDirectorCore(MusicDirectorConfiguration configuration)\n {\n _configuration = configuration; configuration.Validate(); _random = new(configuration.Seed);\n }\n\n public MusicDirectorCore(MusicDirectorConfiguration configuration, MusicDirectorState state)\n {\n _configuration = configuration; configuration.Validate();\n if (!string.Equals(configuration.Version, state.ConfigurationVersion, StringComparison.Ordinal)) throw new InvalidOperationException($\"Music state version '{state.ConfigurationVersion}' does not match configuration version '{configuration.Version}'.\");\n if (state.NextDecisionId <= 0 || state.Participants is null || state.Participants.Select(x => x.ParticipantId).Distinct(StringComparer.Ordinal).Count() != state.Participants.Count) throw new InvalidOperationException(\"Music Director state is incomplete or invalid.\");\n _random = new(state.Random); _hasTicked = state.HasTicked; _lastTickTime = state.LastTickTime; _lastEventSequence = state.LastGameplayEventSequence; _nextDecisionId = state.NextDecisionId;\n foreach (var participant in state.Participants)\n {\n if (string.IsNullOrWhiteSpace(participant.ParticipantId) || participant.SpecialAlertReadyAt is null || participant.LastMixerLayers is null) throw new InvalidOperationException(\"Music participant state is incomplete.\");\n _participants.Add(participant.ParticipantId, new ParticipantMemory(participant));\n }\n }\n\n public MusicDecisionBatch Tick(MusicWorldSnapshot world, IReadOnlyList<MusicGameplayEvent>? gameplayEvents = null)\n {\n if (_configuration.ValidateSnapshots) world.Validate();\n if (_hasTicked && world.Time < _lastTickTime) throw new InvalidOperationException(\"Music world time cannot move backwards.\");\n var delta = _hasTicked ? world.Time - _lastTickTime : 0;\n _hasTicked = true; _lastTickTime = world.Time;\n var output = new MusicDecisionBatch { Time = world.Time };\n\n foreach (var input in world.Participants.OrderBy(x => x.Id, StringComparer.Ordinal)) if (!_participants.ContainsKey(input.Id)) _participants.Add(input.Id, new ParticipantMemory(input.Id, input.CumulativeDamage, _configuration.SpecialAlerts));\n ApplyGameplayEvents(world, gameplayEvents ?? Array.Empty<MusicGameplayEvent>(), output);\n foreach (var input in world.Participants.OrderBy(x => x.Id, StringComparer.Ordinal))\n {\n var state = _participants[input.Id];\n if (world.ScenarioEnded && !state.ScenarioEnded) output.Decisions.Add(NewDecision(MusicDecisionKind.General, input.Id, \"scenario_ended\", \"world scenario ended\"));\n state.ScenarioEnded = world.ScenarioEnded;\n UpdateParticipant(input, state, delta, world.Time, output);\n }\n return output;\n }\n\n void ApplyGameplayEvents(MusicWorldSnapshot world, IReadOnlyList<MusicGameplayEvent> events, MusicDecisionBatch output)\n {\n if (events.Select(x => x.Sequence).Distinct().Count() != events.Count) throw new ArgumentException(\"Music gameplay event sequences must be unique within a tick.\");\n foreach (var input in events.OrderBy(x => x.Sequence))\n {\n if (input.Sequence <= _lastEventSequence) continue;\n if (!float.IsFinite(input.FadeSeconds) || input.FadeSeconds < 0) throw new ArgumentException(\"Music event fades must be finite and non-negative.\");\n _lastEventSequence = input.Sequence;\n var targets = string.IsNullOrWhiteSpace(input.ParticipantId)\n ? world.Participants.Select(x => x.Id).OrderBy(x => x, StringComparer.Ordinal).ToArray()\n : new[] { input.ParticipantId };\n foreach (var participantId in targets)\n {\n var kind = input.Kind switch\n {\n MusicTriggerKind.Play => MusicDecisionKind.Play,\n MusicTriggerKind.StopEvent => MusicDecisionKind.StopEvent,\n MusicTriggerKind.StopTrack => MusicDecisionKind.StopTrack,\n MusicTriggerKind.StopAll => MusicDecisionKind.StopAll,\n MusicTriggerKind.AmbientMob or MusicTriggerKind.MobSpawn or MusicTriggerKind.MobSpawnBehind or MusicTriggerKind.LargeAreaRevealed or MusicTriggerKind.LandmarkRevealed => MusicDecisionKind.Play,\n _ => MusicDecisionKind.General\n };\n var target = input.Kind switch\n {\n MusicTriggerKind.Reset => \"reset\",\n MusicTriggerKind.ScenarioEnded => \"scenario_ended\",\n MusicTriggerKind.ParticipantRestored => \"participant_restored\",\n MusicTriggerKind.AmbientMob => _configuration.Cues.AmbientMobEvent,\n MusicTriggerKind.MobSpawn => _configuration.Cues.MobSpawnEvent,\n MusicTriggerKind.MobSpawnBehind => _configuration.Cues.MobSpawnBehindEvent,\n MusicTriggerKind.LargeAreaRevealed => _configuration.Cues.LargeAreaRevealEvent,\n _ => input.TargetId\n };\n if (input.Kind == MusicTriggerKind.LargeAreaRevealed && _participants.TryGetValue(participantId, out var revealState))\n {\n if (revealState.LargeAreaRevealReadyAt > world.Time) continue;\n revealState.LargeAreaRevealReadyAt = world.Time + _configuration.Dynamic.LargeAreaRevealCooldownSeconds;\n }\n if (kind == MusicDecisionKind.Play && string.IsNullOrWhiteSpace(target)) continue;\n output.Decisions.Add(NewDecision(kind, participantId, target, $\"gameplay:{input.Kind}\") with { FadeSeconds = input.FadeSeconds, EmitterId = input.EmitterId, Position = input.Position, Arguments = input.Arguments });\n if ((input.Kind is MusicTriggerKind.Reset or MusicTriggerKind.ParticipantRestored) && _participants.TryGetValue(participantId, out var state))\n {\n state.ResetDynamic();\n var current = world.Participants.FirstOrDefault(x => string.Equals(x.Id, participantId, StringComparison.Ordinal)); if (current is not null) state.PreviousDamage = current.CumulativeDamage;\n }\n }\n }\n }\n\n void UpdateParticipant(MusicParticipantSnapshot input, ParticipantMemory state, double delta, double time, MusicDecisionBatch output)\n {\n var d = _configuration.Dynamic;\n var dt = (float)Math.Max(0, delta);\n var damageDelta = Math.Max(0, input.CumulativeDamage - state.PreviousDamage); state.PreviousDamage = input.CumulativeDamage;\n state.Damage = Advance(state.Damage, damageDelta > 0 ? 1 : 0, dt, d.DamageRisePerSecond, d.DamageDecaySeconds);\n state.FastActivity = Advance(state.FastActivity, input.WeaponActivity, dt, d.FastActivityRisePerSecond, d.FastActivityDecaySeconds);\n state.SlowActivity = Advance(state.SlowActivity, input.WeaponActivity, dt, d.SlowActivityRisePerSecond, d.SlowActivityDecaySeconds);\n state.DamageInflicted = Advance(state.DamageInflicted, input.InflictedDamage ? 1 : 0, dt, d.InflictedDamageRisePerSecond, d.InflictedDamageDecaySeconds);\n state.CommonSight = Advance(state.CommonSight, Clamp01(input.VisibleCommonThreats / d.CommonSightNormalizer), dt, float.PositiveInfinity, d.CommonSightDecaySeconds);\n\n var damageDriver = Math.Min(5 * input.VeryCloseCommonFactor, state.Damage);\n var damageDuck = RemapClamped(damageDriver, d.DamageDuckInputMinimum, d.DamageDuckInputMaximum, 0, d.DamageDuckOutputMaximum);\n var mobDriver = Math.Min(4 * input.VeryCloseCommonFactor, state.Damage);\n var mobTarget = mobDriver < d.MobDamageMinimum ? 0 : RemapClamped(mobDriver, d.MobDamageMinimum, d.MobDamageMaximum, d.MobOutputMinimum, d.MobOutputMaximum);\n if (input.IsIncapacitated) mobTarget = 0;\n state.MobPressure = Advance(state.MobPressure, mobTarget, dt, float.PositiveInfinity, d.MobPressureDecaySeconds);\n\n var rawAction = Math.Max(Math.Max(input.GlobalCombatActive || input.BossPresent ? 1 : 0, 5 * input.AttackingCommonFactor), state.FastActivity);\n rawAction = Clamp01(rawAction);\n var rawThreat = Clamp01(Math.Max(input.MobPressureActive || input.BossPresent || input.HazardPresent ? 1 : 0, 2 * input.AttackingCommonFactor));\n state.Action = Advance(state.Action, rawAction, dt, float.PositiveInfinity, d.ActionDecaySeconds);\n state.Threat = Advance(state.Threat, rawThreat, dt, float.PositiveInfinity, d.ThreatDecaySeconds);\n state.Calm = Advance(state.Calm, 1 - rawAction, dt, float.PositiveInfinity, d.CalmDecaySeconds);\n var ambientTarget = RemapClamped(state.Calm, d.AmbientInputMinimum, d.AmbientInputMaximum, 0, 1);\n state.AmbientBasis = Advance(state.AmbientBasis, ambientTarget, dt, float.PositiveInfinity, d.AmbientDecaySeconds);\n var ambient = state.AmbientBasis * (1 - input.HazardRage);\n var mobNetwork = state.MobPressure < .1f ? 0 : state.MobPressure;\n var secondary = Clamp01(2 * state.DamageInflicted);\n var close = RemapClamped(input.CloseCommonAction, 0, d.CloseActionMaximum, 0, 1);\n\n UpdateCombat(input, state, time, output);\n EmitLayer(state, output, input.Id, MusicMixerLayers.MobBeating, damageDuck, \"damage/common pressure\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.MobRules, 1 - mobNetwork, \"mob pressure\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.HazardRage, 1 - input.HazardRage, \"hazard rage\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.Combat, state.CombatActive ? 1 : 0, \"combat state\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.CombatSecondary, 1 - secondary, \"damage inflicted\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.CombatClose, 1 - close, \"close common action\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.Adrenaline, input.Adrenaline, \"adrenaline\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.Checkpoint, input.InCheckpoint ? 1 : 0, \"checkpoint state\");\n EmitLayer(state, output, input.Id, MusicMixerLayers.Ambient, ambient, \"calm and hazard rage\");\n\n UpdateMobbed(input, state, mobNetwork, output);\n UpdateBoss(input, state, output);\n UpdateHazard(input, state, time, output);\n UpdateSpecialAlerts(input, state, time, ambient, output);\n UpdateAtmosphere(input, state, time, ambient, output);\n UpdatePositionalStinger(input, state, time, output);\n }\n\n void UpdateCombat(MusicParticipantSnapshot input, ParticipantMemory state, double time, MusicDecisionBatch output)\n {\n var allowed = input.DynamicMusicAllowed && input.IsEligible && input.IsAlive && !input.IsIncapacitated && !input.BossPresent && !input.HazardAttacking;\n if (!state.CombatActive && allowed && input.VisibleCommonThreats > 0)\n {\n state.CombatActive = true;\n var intros = _configuration.Cues.CombatIntroEvents.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();\n if (intros.Length > 0) Play(output, input.Id, intros[_random.NextInt(intros.Length)], \"combat started\");\n PlayIfConfigured(output, input.Id, _configuration.Cues.CombatSecondaryEvent, \"combat secondary started\");\n PlayIfConfigured(output, input.Id, _configuration.Cues.CombatCloseEvent, \"combat close layer started\");\n }\n var stopSignal = Math.Max(state.CommonSight, state.SlowActivity);\n var canStop = !input.MobPressureActive && !input.IsIncapacitated && !input.BossPresent && !input.HazardAttacking && input.ActiveCommonThreats <= _configuration.Dynamic.CombatStopActiveCount && input.ScannedCommonThreats <= _configuration.Dynamic.CombatStopScannedCount;\n if (state.CombatActive && (!allowed || canStop && stopSignal < _configuration.Dynamic.CombatStopSignal))\n {\n state.CombatActive = false;\n foreach (var track in _configuration.Cues.CombatTrackIds.Where(x => !string.IsNullOrWhiteSpace(x))) output.Decisions.Add(NewDecision(MusicDecisionKind.StopTrack, input.Id, track, \"combat ended\") with { FadeSeconds = _configuration.CombatStopFadeSeconds });\n }\n }\n\n void UpdateMobbed(MusicParticipantSnapshot input, ParticipantMemory state, float mobNetwork, MusicDecisionBatch output)\n {\n var active = mobNetwork > 0;\n if (active && !state.MobbedActive) PlayIfConfigured(output, input.Id, _configuration.Cues.MobbedEvent, \"mob pressure entered\");\n else if (!active && state.MobbedActive && !string.IsNullOrWhiteSpace(_configuration.Cues.MobbedEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, input.Id, _configuration.Cues.MobbedEvent, \"mob pressure cleared\"));\n state.MobbedActive = active;\n }\n\n void UpdateBoss(MusicParticipantSnapshot input, ParticipantMemory state, MusicDecisionBatch output)\n {\n if (input.BossPresent && !state.BossPresent) PlayIfConfigured(output, input.Id, _configuration.Cues.BossApproachingEvent, \"boss approaching\");\n else if (!input.BossPresent && state.BossPresent && !string.IsNullOrWhiteSpace(_configuration.Cues.BossTrackId)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopTrack, input.Id, _configuration.Cues.BossTrackId, \"boss defeated\") with { FadeSeconds = _configuration.BossStopFadeSeconds });\n state.BossPresent = input.BossPresent;\n }\n\n void UpdateHazard(MusicParticipantSnapshot input, ParticipantMemory state, double time, MusicDecisionBatch output)\n {\n TransitionEvent(input.Id, input.HazardBurning, ref state.HazardBurning, _configuration.Cues.HazardBurningEvent, _configuration.HazardStopFadeSeconds, \"hazard burning\", output);\n TransitionEvent(input.Id, input.HazardAttacking, ref state.HazardAttacking, _configuration.Cues.HazardAttackEvent, _configuration.HazardStopFadeSeconds, \"hazard attacking\", output);\n var raging = input.HazardRage > 0;\n if (raging && !state.HazardRaging && !string.IsNullOrWhiteSpace(_configuration.Cues.HazardRageEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, _configuration.Cues.HazardRageEvent, \"hazard rage started\") with { EmitterId = input.HazardEmitterId, Position = input.HazardPosition });\n else if (!raging && state.HazardRaging && !string.IsNullOrWhiteSpace(_configuration.Cues.HazardRageEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, input.Id, _configuration.Cues.HazardRageEvent, \"hazard rage ended\") with { FadeSeconds = _configuration.HazardRageStopFadeSeconds });\n state.HazardRaging = raging;\n\n var wandering = input.WanderingHazards.OrderBy(x => x.Anger).ThenBy(x => x.Id, StringComparer.Ordinal).FirstOrDefault();\n if (wandering is null)\n {\n if (state.WanderingHazardActive && !string.IsNullOrWhiteSpace(_configuration.Cues.WanderingHazardTrackId)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopTrack, input.Id, _configuration.Cues.WanderingHazardTrackId, \"no wandering hazard\") with { FadeSeconds = _configuration.HazardStopFadeSeconds });\n state.WanderingHazardActive = false;\n }\n else if (time >= state.WanderingHazardReadyAt)\n {\n var tier = wandering.Anger < .25f ? 0 : wandering.Anger < .5f ? 1 : wandering.Anger < .75f ? 2 : 3;\n output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, _configuration.Cues.WanderingHazardEventsLowToHigh[tier], \"wandering hazard anger tier\") with { EmitterId = wandering.Id, Position = wandering.Position });\n state.WanderingHazardActive = true; state.WanderingHazardReadyAt = time + _configuration.Dynamic.WanderingHazardIntervalSeconds;\n }\n }\n\n void TransitionEvent(string participantId, bool active, ref bool previous, string eventId, float fade, string reason, MusicDecisionBatch output)\n {\n if (active && !previous) PlayIfConfigured(output, participantId, eventId, reason + \" started\");\n else if (!active && previous && !string.IsNullOrWhiteSpace(eventId)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, participantId, eventId, reason + \" ended\") with { FadeSeconds = fade });\n previous = active;\n }\n\n void UpdateSpecialAlerts(MusicParticipantSnapshot input, ParticipantMemory state, double time, float ambient, MusicDecisionBatch output)\n {\n if (ambient <= _configuration.SpecialAlertMinimumAmbient) return;\n var profiles = _configuration.SpecialAlerts;\n foreach (var threat in input.SpecialThreats.OrderBy(x => x.ArchetypeId, StringComparer.Ordinal).ThenBy(x => x.EmitterId, StringComparer.Ordinal))\n {\n var index = -1;\n for (var i = 0; i < profiles.Count; i++) if (string.Equals(profiles[i].ArchetypeId, threat.ArchetypeId, StringComparison.Ordinal)) { index = i; break; }\n state.SpecialReadyAt.TryGetValue(threat.ArchetypeId, out var readyAt);\n if (index < 0 || threat.Distance > profiles[index].ScanMaximumDistance || readyAt > time) continue;\n var profile = profiles[index];\n var eventId = threat.Distance <= profile.CloseMaximumDistance ? profile.CloseEvent : threat.Distance >= profile.FarMinimumDistance ? profile.FarEvent : profile.MiddleEvent;\n output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, eventId, $\"special alert:{threat.ArchetypeId}\") with { EmitterId = threat.EmitterId, Position = threat.Position });\n var min = threat.Distance <= profile.CloseMaximumDistance ? profile.RandomMultiplierMaximum : threat.Distance >= profile.FarMinimumDistance ? profile.RandomMultiplierMaximum * 2 : profile.RandomMultiplierMaximum;\n var max = threat.Distance <= profile.CloseMaximumDistance ? (int)MathF.Round(profile.RandomMultiplierMaximum * 1.4f) : threat.Distance >= profile.FarMinimumDistance ? (int)MathF.Round(profile.RandomMultiplierMaximum * 2.5f) : profile.RandomMultiplierMaximum * 2;\n var multiplier = min + _random.NextInt(max - min + 1);\n state.SpecialReadyAt[threat.ArchetypeId] = time + multiplier * profile.IntervalBeats * (60.0 / profile.Bpm);\n for (var step = 1; step < profiles.Count; step++)\n {\n var peer = profiles[(index + step) % profiles.Count].ArchetypeId;\n var floor = time + _configuration.SpecialPeerStaggerStartSeconds + (step - 1) * _configuration.SpecialPeerStaggerStepSeconds;\n state.SpecialReadyAt.TryGetValue(peer, out var peerReady); if (peerReady < floor) state.SpecialReadyAt[peer] = floor;\n }\n break;\n }\n }\n\n void UpdateAtmosphere(MusicParticipantSnapshot input, ParticipantMemory state, double time, float ambient, MusicDecisionBatch output)\n {\n var danger = state.Threat > 0;\n if (danger && !state.DangerAtmosphereActive && time >= state.AtmosphereReadyAt)\n {\n PlayIfConfigured(output, input.Id, _configuration.Cues.DangerAtmosphereEvent, \"danger atmosphere\"); state.DangerAtmosphereActive = true; state.AtmosphereReadyAt = time + _configuration.AtmosphereRetrySeconds;\n }\n else if (!danger && state.DangerAtmosphereActive && ambient <= 0)\n {\n if (!string.IsNullOrWhiteSpace(_configuration.Cues.DangerAtmosphereEvent)) output.Decisions.Add(NewDecision(MusicDecisionKind.StopEvent, input.Id, _configuration.Cues.DangerAtmosphereEvent, \"danger atmosphere cleared\"));\n state.DangerAtmosphereActive = false; state.AtmosphereReadyAt = time + _configuration.AtmosphereTransitionSeconds;\n }\n else if (!danger && !state.DangerAtmosphereActive && ambient > 0 && time >= state.AtmosphereReadyAt)\n {\n PlayIfConfigured(output, input.Id, _configuration.Cues.SafeAtmosphereEvent, \"safe atmosphere\"); state.AtmosphereReadyAt = time + _configuration.AtmosphereRetrySeconds + 10;\n }\n }\n\n void UpdatePositionalStinger(MusicParticipantSnapshot input, ParticipantMemory state, double time, MusicDecisionBatch output)\n {\n if (state.AmbientBasis <= _configuration.Dynamic.PositionalStingerAmbientMinimum || time < state.PositionalStingerReadyAt || string.IsNullOrWhiteSpace(_configuration.Cues.PositionalStingerEvent)) return;\n var candidates = input.PositionalCandidates.Where(x => x.Eligible).OrderBy(x => x.Id, StringComparer.Ordinal).ToArray(); if (candidates.Length == 0) return;\n var selected = candidates[_random.NextInt(candidates.Length)];\n output.Decisions.Add(NewDecision(MusicDecisionKind.Play, input.Id, _configuration.Cues.PositionalStingerEvent, \"positional stinger\") with { EmitterId = selected.Id, Position = selected.Position, StartOffsetSeconds = .1f });\n var multiplier = 1 + _random.NextInt(_configuration.Dynamic.PositionalStingerRandomMultiplierMaximum);\n state.PositionalStingerReadyAt = time + multiplier * _configuration.Dynamic.PositionalStingerBeats * (60.0 / _configuration.Dynamic.PositionalStingerBpm);\n }\n\n void EmitLayer(ParticipantMemory state, MusicDecisionBatch output, string participantId, string layer, float value, string reason)\n {\n value = Clamp01(value);\n if (state.LastLayers.TryGetValue(layer, out var previous) && MathF.Abs(previous - value) <= .000001f) return;\n state.LastLayers[layer] = value;\n output.Decisions.Add(NewDecision(MusicDecisionKind.SetMixerLayer, participantId, \"\", reason) with { LayerId = layer, Value = value });\n }\n\n void PlayIfConfigured(MusicDecisionBatch output, string participantId, string eventId, string reason) { if (!string.IsNullOrWhiteSpace(eventId)) Play(output, participantId, eventId, reason); }\n void Play(MusicDecisionBatch output, string participantId, string eventId, string reason) => output.Decisions.Add(NewDecision(MusicDecisionKind.Play, participantId, eventId, reason));\n MusicDecision NewDecision(MusicDecisionKind kind, string participantId, string target, string reason) => new(_nextDecisionId++, kind, participantId, target, reason);\n\n static float Advance(float current, float target, float delta, float risePerSecond, float decaySeconds)\n {\n target = Clamp01(target);\n if (target >= current) return float.IsPositiveInfinity(risePerSecond) ? target : Math.Min(target, current + risePerSecond * delta);\n return Math.Max(target, current - delta / decaySeconds);\n }\n static float RemapClamped(float value, float inputMin, float inputMax, float outputMin, float outputMax) => inputMax <= inputMin ? outputMax : outputMin + Clamp01((value - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin);\n static float Clamp01(float value) => Math.Clamp(value, 0, 1);\n\n public MusicDirectorState CaptureState() => new(\n _configuration.Version, _hasTicked, _lastTickTime, _lastEventSequence, _nextDecisionId, _random.State,\n _participants.Values.OrderBy(x => x.Id, StringComparer.Ordinal).Select(x => x.Capture()).ToArray());\n\n public bool RemoveParticipant(string participantId) => _participants.Remove(participantId);\n public void Reset()\n {\n _participants.Clear(); _lastEventSequence = 0; _nextDecisionId = 1; _lastTickTime = 0; _hasTicked = false; _random = new(_configuration.Seed);\n }\n\n sealed class ParticipantMemory\n {\n public readonly string Id;\n public float PreviousDamage, Damage, FastActivity, SlowActivity, DamageInflicted, CommonSight, MobPressure, Action, Threat, Calm, AmbientBasis;\n public bool CombatActive, MobbedActive, BossPresent, HazardBurning, HazardAttacking, HazardRaging, WanderingHazardActive, DangerAtmosphereActive, ScenarioEnded;\n public double AtmosphereReadyAt, PositionalStingerReadyAt, WanderingHazardReadyAt, LargeAreaRevealReadyAt;\n public readonly Dictionary<string, double> SpecialReadyAt = new(StringComparer.Ordinal);\n public readonly Dictionary<string, float> LastLayers = new(StringComparer.Ordinal);\n public ParticipantMemory(string id, float previousDamage, IReadOnlyList<SpecialMusicAlertConfiguration> alerts) { Id = id; PreviousDamage = previousDamage; foreach (var alert in alerts) SpecialReadyAt[alert.ArchetypeId] = 0; }\n public ParticipantMemory(MusicParticipantState state)\n {\n Id = state.ParticipantId; PreviousDamage = state.PreviousDamage;\n (Damage, FastActivity, SlowActivity, DamageInflicted, CommonSight, MobPressure, Action, Threat, Calm, AmbientBasis) = state.Envelopes;\n CombatActive = state.CombatActive; MobbedActive = state.MobbedActive; BossPresent = state.BossPresent; HazardBurning = state.HazardBurning; HazardAttacking = state.HazardAttacking; HazardRaging = state.HazardRaging; WanderingHazardActive = state.WanderingHazardActive; DangerAtmosphereActive = state.DangerAtmosphereActive; ScenarioEnded = state.ScenarioEnded; AtmosphereReadyAt = state.AtmosphereReadyAt; PositionalStingerReadyAt = state.PositionalStingerReadyAt; WanderingHazardReadyAt = state.WanderingHazardReadyAt; LargeAreaRevealReadyAt = state.LargeAreaRevealReadyAt;\n foreach (var pair in state.SpecialAlertReadyAt) SpecialReadyAt[pair.Key] = pair.Value; foreach (var pair in state.LastMixerLayers) LastLayers[pair.Key] = pair.Value;\n }\n public MusicParticipantState Capture() => new(Id, PreviousDamage, new(Damage, FastActivity, SlowActivity, DamageInflicted, CommonSight, MobPressure, Action, Threat, Calm, AmbientBasis), CombatActive, MobbedActive, BossPresent, HazardBurning, HazardAttacking, HazardRaging, WanderingHazardActive, DangerAtmosphereActive, ScenarioEnded, AtmosphereReadyAt, PositionalStingerReadyAt, WanderingHazardReadyAt, LargeAreaRevealReadyAt, new Dictionary<string, double>(SpecialReadyAt), new Dictionary<string, float>(LastLayers));\n public void ResetDynamic()\n {\n Damage = FastActivity = SlowActivity = DamageInflicted = CommonSight = MobPressure = Action = Threat = Calm = AmbientBasis = 0; CombatActive = MobbedActive = BossPresent = HazardBurning = HazardAttacking = HazardRaging = WanderingHazardActive = DangerAtmosphereActive = ScenarioEnded = false; AtmosphereReadyAt = PositionalStingerReadyAt = WanderingHazardReadyAt = LargeAreaRevealReadyAt = 0; LastLayers.Clear(); foreach (var key in SpecialReadyAt.Keys.ToArray()) SpecialReadyAt[key] = 0;\n }\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/Policies.cs",
"FileName": "Policies.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "namespace SboxDirector;\n\npublic interface IDirectorPolicy\n{\n bool AllowEncounter(EncounterKind kind, WorldSnapshot world);\n string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random);\n bool CanCoexist(EncounterKind requested, PopulationSnapshot population);\n}\n\n/// <summary>Implement this alongside IDirectorPolicy when a custom policy owns mutable deterministic state.</summary>\npublic interface IPersistentDirectorPolicy\n{\n string PersistenceId { get; }\n IReadOnlyDictionary<string, string> CapturePolicyState();\n void RestorePolicyState(IReadOnlyDictionary<string, string> state);\n}\n\n/// <summary>Optional mode/difficulty override surface corresponding to recovered scripted Director policy.</summary>\npublic interface IAdaptiveDirectorPolicy\n{\n int AdjustEncounterCount(EncounterKind kind, int proposedCount, TempoPhase tempo, float teamPressure, WorldSnapshot world);\n float AdjustMusicIntensity(float proposedIntensity, TempoPhase tempo, float teamPressure, WorldSnapshot world);\n bool ShouldPlayBossMusic(EncounterKind kind, WorldSnapshot world);\n}\n\npublic interface IAdaptivePopulationPolicy\n{\n int AdjustPopulationLimit(EncounterKind kind, int configuredLimit, WorldSnapshot world);\n}\n\npublic sealed class DefaultDirectorPolicy : IDirectorPolicy\n{\n public bool AllowEncounter(EncounterKind kind, WorldSnapshot world) => true;\n public bool CanCoexist(EncounterKind requested, PopulationSnapshot population) => requested != EncounterKind.Boss || population[EncounterKind.Boss] == 0;\n public string SelectArchetype(EncounterKind kind, WorldSnapshot world, DeterministicRandom random) => kind.ToString().ToLowerInvariant();\n}\n\npublic interface IEncounterSchedule\n{\n EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random);\n int RequestedCount(EncounterKind kind, WorldSnapshot world);\n}\n\npublic interface IPersistentEncounterSchedule\n{\n IReadOnlyDictionary<string, string> CaptureScheduleState();\n void RestoreScheduleState(IReadOnlyDictionary<string, string> state);\n}\n\npublic interface IEncounterScheduleFeedback\n{\n void SelectionFinalized(bool requestCreated);\n}\n\npublic sealed class BasicEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback\n{\n readonly double _ambientInterval;\n readonly double _activeInterval;\n double _nextEncounterTime;\n double _previousEncounterTime;\n bool _selectionPending;\n\n public BasicEncounterSchedule(double ambientInterval = 3, double activeInterval = 8)\n {\n if (ambientInterval < 0 || activeInterval < 0) throw new ArgumentOutOfRangeException(\"Encounter intervals must be non-negative.\");\n _ambientInterval = ambientInterval; _activeInterval = activeInterval;\n }\n\n public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)\n {\n if (world.Time < _nextEncounterTime) return null;\n if (tempo == TempoPhase.Relax && population.Available(EncounterKind.Ambient, world.Population) > 0)\n {\n _previousEncounterTime = _nextEncounterTime; _selectionPending = true; _nextEncounterTime = world.Time + _ambientInterval;\n return EncounterKind.Ambient;\n }\n if (tempo is TempoPhase.BuildUp or TempoPhase.SustainPeak)\n {\n if (population.Available(EncounterKind.Special, world.Population) > 0 && random.NextFloat() < 0.25f) { _previousEncounterTime = _nextEncounterTime; _selectionPending = true; _nextEncounterTime = world.Time + _activeInterval; return EncounterKind.Special; }\n if (population.Available(EncounterKind.CommonWave, world.Population) > 0) { _previousEncounterTime = _nextEncounterTime; _selectionPending = true; _nextEncounterTime = world.Time + _activeInterval; return EncounterKind.CommonWave; }\n }\n return null;\n }\n public int RequestedCount(EncounterKind kind, WorldSnapshot world) => kind switch { EncounterKind.Ambient => 1, EncounterKind.Special => 1, EncounterKind.CommonWave => 10, _ => 1 };\n public IReadOnlyDictionary<string, string> CaptureScheduleState() => new Dictionary<string, string> { [\"nextEncounterTime\"] = _nextEncounterTime.ToString(\"R\", CultureInfo.InvariantCulture) };\n public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)\n {\n if (state.TryGetValue(\"nextEncounterTime\", out var value) && double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) _nextEncounterTime = parsed;\n }\n public void SelectionFinalized(bool requestCreated) { if (_selectionPending && !requestCreated) _nextEncounterTime = _previousEncounterTime; _selectionPending = false; }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/ResourcePopulation.cs",
"FileName": "ResourcePopulation.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic sealed class ResourceCandidate\n{\n public string Id { get; init; } = \"\";\n public string Category { get; init; } = \"\";\n public bool Reachable { get; init; } = true;\n public bool Visible { get; init; }\n public float AreaCapacity { get; init; } = 1;\n public string ClusterId { get; init; } = \"\";\n public IReadOnlySet<string> Tags { get; init; } = new HashSet<string>();\n}\n\npublic sealed class ResourceWorldSnapshot\n{\n public IReadOnlyDictionary<string, int> ExistingCounts { get; init; } = new Dictionary<string, int>();\n public IReadOnlyList<ResourceCandidate> Candidates { get; init; } = Array.Empty<ResourceCandidate>();\n public void Validate()\n {\n if (ExistingCounts.Any(x => string.IsNullOrWhiteSpace(x.Key) || x.Value < 0)) throw new ArgumentException(\"Resource counts require non-empty categories and non-negative counts.\");\n if (Candidates.Any(x => string.IsNullOrWhiteSpace(x.Id) || string.IsNullOrWhiteSpace(x.Category) || !float.IsFinite(x.AreaCapacity) || x.AreaCapacity < 0 || x.Tags is null || x.Tags.Any(string.IsNullOrWhiteSpace))) throw new ArgumentException(\"Resource candidates require ids, categories, non-negative finite capacity, and valid tags.\");\n if (Candidates.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Candidates.Count) throw new ArgumentException(\"Resource candidate ids must be unique.\");\n }\n}\n\npublic sealed class ResourceRule\n{\n public string Category { get; init; } = \"resource\";\n public int TargetCount { get; init; } = 1;\n public int MaximumRequestsPerTick { get; init; } = 1;\n public bool AllowVisible { get; init; }\n public int MaximumPerCluster { get; init; } = int.MaxValue;\n public IReadOnlySet<string> RequiredTags { get; init; } = new HashSet<string>();\n public void Validate() { if (string.IsNullOrWhiteSpace(Category) || TargetCount < 0 || MaximumRequestsPerTick <= 0 || MaximumPerCluster <= 0 || RequiredTags is null || RequiredTags.Any(string.IsNullOrWhiteSpace)) throw new ArgumentException(\"Invalid resource rule.\"); }\n}\n\npublic interface IResourcePolicy\n{\n bool AllowResourceSpawn(string category, ResourceCandidate candidate, WorldSnapshot world);\n string ConvertResource(string category, ResourceCandidate candidate, WorldSnapshot world);\n bool ShouldAvoidResource(string archetype, ResourceCandidate candidate, WorldSnapshot world);\n bool CanPickupObject(string archetype, string participantId, WorldSnapshot world);\n}\n\npublic interface IPersistentResourcePolicy\n{\n string PersistenceId { get; }\n IReadOnlyDictionary<string, string> CaptureResourcePolicyState();\n void RestoreResourcePolicyState(IReadOnlyDictionary<string, string> state);\n}\n\npublic sealed class DefaultResourcePolicy : IResourcePolicy\n{\n public bool AllowResourceSpawn(string category, ResourceCandidate candidate, WorldSnapshot world) => true;\n public string ConvertResource(string category, ResourceCandidate candidate, WorldSnapshot world) => category;\n public bool ShouldAvoidResource(string archetype, ResourceCandidate candidate, WorldSnapshot world) => false;\n public bool CanPickupObject(string archetype, string participantId, WorldSnapshot world) => true;\n}\n\npublic sealed record ResourceSpawnRequest(long RequestId, string Category, string Archetype, string CandidateId, string Reason);\npublic sealed record ResourceSpawnResult(long RequestId, RequestResultKind Result, string? Detail = null);\npublic readonly record struct PendingResourceState(long RequestId, string Category, string CandidateId, string ClusterId);\npublic readonly record struct ResourcePopulationState(string ConfigurationSignature, long NextRequestId, RandomState Random, IReadOnlyList<PendingResourceState> Pending, IReadOnlyList<PendingResourceState> Consumed, ExtensionState Policy);\n\n/// <summary>Generalized density/conversion/revisit controller backed by recovered item-population responsibilities.</summary>\npublic sealed class ResourcePopulationController\n{\n readonly IReadOnlyList<ResourceRule> _rules;\n readonly IResourcePolicy _policy;\n DeterministicRandom _random;\n readonly Dictionary<long, PendingResourceState> _pending = new();\n readonly Dictionary<string, PendingResourceState> _consumed = new(StringComparer.Ordinal);\n readonly string _configurationSignature;\n long _nextRequestId = 1;\n\n public ResourcePopulationController(IEnumerable<ResourceRule> rules, int seed = 1, IResourcePolicy? policy = null)\n {\n _rules = rules.OrderBy(x => x.Category, StringComparer.Ordinal).ToArray(); foreach (var rule in _rules) rule.Validate();\n if (_rules.Select(x => x.Category).Distinct(StringComparer.Ordinal).Count() != _rules.Count) throw new ArgumentException(\"Resource rule categories must be unique.\");\n _policy = policy ?? new DefaultResourcePolicy();\n _random = new DeterministicRandom(seed);\n _configurationSignature = string.Join(\";\", _rules.Select(x => $\"{Encode(x.Category)}:{x.TargetCount}:{x.MaximumRequestsPerTick}:{x.AllowVisible}:{x.MaximumPerCluster}:{string.Join(\",\", x.RequiredTags.OrderBy(tag => tag, StringComparer.Ordinal).Select(Encode))}\"));\n }\n\n public ResourcePopulationController(IEnumerable<ResourceRule> rules, ResourcePopulationState state, IResourcePolicy? policy = null) : this(rules, 1, policy)\n {\n RestoreState(state);\n }\n\n public IReadOnlyList<ResourceSpawnRequest> Tick(ResourceWorldSnapshot resources, WorldSnapshot world)\n {\n var output = new List<ResourceSpawnRequest>();\n foreach (var rule in _rules)\n {\n var existing = resources.ExistingCounts.TryGetValue(rule.Category, out var count) ? count : 0;\n var reserved = _pending.Values.Count(x => x.Category == rule.Category);\n var needed = Math.Min(rule.MaximumRequestsPerTick, Math.Max(0, rule.TargetCount - existing - reserved));\n for (var requestIndex = 0; requestIndex < needed; requestIndex++)\n {\n var candidates = resources.Candidates.Where(x => x.Category == rule.Category && x.Reachable && (rule.AllowVisible || !x.Visible) && !_consumed.ContainsKey(x.Id) && !_pending.Values.Any(p => p.CandidateId == x.Id) && ClusterAvailable(x, rule) && rule.RequiredTags.All(x.Tags.Contains) && _policy.AllowResourceSpawn(rule.Category, x, world)).ToArray();\n if (candidates.Length == 0) break;\n var bestCapacity = candidates.Max(x => x.AreaCapacity); var best = candidates.Where(x => Math.Abs(x.AreaCapacity - bestCapacity) < .00001f).ToArray(); var candidate = best[_random.NextInt(best.Length)];\n var archetype = _policy.ConvertResource(rule.Category, candidate, world); if (string.IsNullOrWhiteSpace(archetype) || _policy.ShouldAvoidResource(archetype, candidate, world)) { _consumed[candidate.Id] = new(0, rule.Category, candidate.Id, candidate.ClusterId); requestIndex--; continue; }\n var id = _nextRequestId++; _pending[id] = new(id, rule.Category, candidate.Id, candidate.ClusterId); output.Add(new(id, rule.Category, archetype, candidate.Id, $\"target={rule.TargetCount}; existing={existing}; reserved={reserved}; cluster={candidate.ClusterId}\")); reserved++;\n }\n }\n return output;\n }\n\n public void ApplyResult(ResourceSpawnResult result)\n {\n if (result.Result == RequestResultKind.Deferred) return;\n if (!_pending.Remove(result.RequestId, out var pending)) return;\n if (result.Result is RequestResultKind.Succeeded or RequestResultKind.PartiallySucceeded) _consumed[pending.CandidateId] = pending;\n }\n\n public void ResetRevisitState() => _consumed.Clear();\n public void CancelPending() => _pending.Clear();\n public bool CanPickup(string archetype, string participantId, WorldSnapshot world) => _policy.CanPickupObject(archetype, participantId, world);\n public ResourcePopulationState CaptureState() => new(_configurationSignature, _nextRequestId, _random.State, _pending.Values.OrderBy(x => x.RequestId).ToArray(), _consumed.Values.OrderBy(x => x.CandidateId, StringComparer.Ordinal).ToArray(), CapturePolicyState());\n public void RestoreState(ResourcePopulationState state)\n {\n if (!string.Equals(state.ConfigurationSignature, _configurationSignature, StringComparison.Ordinal)) throw new InvalidOperationException(\"Resource population state does not match the configured rules.\");\n _nextRequestId = state.NextRequestId; _random = new DeterministicRandom(state.Random); _pending.Clear(); _consumed.Clear();\n foreach (var item in state.Pending) _pending[item.RequestId] = item; foreach (var item in state.Consumed) _consumed[item.CandidateId] = item;\n if (state.Policy.HasState)\n {\n if (_policy is not IPersistentResourcePolicy persistent || persistent.PersistenceId != state.Policy.PersistenceId) throw new InvalidOperationException($\"Resource policy state '{state.Policy.PersistenceId}' requires the matching {nameof(IPersistentResourcePolicy)} implementation.\");\n persistent.RestoreResourcePolicyState(state.Policy.Values);\n }\n }\n ExtensionState CapturePolicyState() => _policy is IPersistentResourcePolicy persistent ? new(string.IsNullOrWhiteSpace(persistent.PersistenceId) ? throw new InvalidOperationException(\"Persistent resource policy requires an id.\") : persistent.PersistenceId, persistent.CaptureResourcePolicyState() ?? new Dictionary<string, string>()) : ExtensionState.Empty;\n static string Encode(string value) => $\"{value.Length}:{value}\";\n bool ClusterAvailable(ResourceCandidate candidate, ResourceRule rule)\n {\n if (string.IsNullOrWhiteSpace(candidate.ClusterId) || rule.MaximumPerCluster == int.MaxValue) return true;\n var pending = _pending.Values.Count(x => x.Category == rule.Category && x.ClusterId == candidate.ClusterId);\n var consumed = _consumed.Values.Count(x => x.Category == rule.Category && x.ClusterId == candidate.ClusterId);\n return pending + consumed < rule.MaximumPerCluster;\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/Sandbox/AdaptiveDirectorComponent.cs",
"FileName": "AdaptiveDirectorComponent.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nusing Sandbox;\n\nnamespace SboxDirector;\n\n/// <summary>\n/// Optional s&box component bridge. Derive from this class and translate your\n/// game's entities/navigation into a WorldSnapshot and SpawnRequestResult.\n/// </summary>\n[Category(\"Gameplay\")]\n[Title(\"Adaptive Director\")]\n[Icon(\"neurology\")]\npublic abstract class AdaptiveDirectorComponent : Component\n{\n [Property] public bool DirectorEnabled { get; set; } = true;\n [Property] public int Seed { get; set; } = 1;\n [Property] public bool RoundBased { get; set; }\n [Property] public bool ResetDirectorOnRoundBegin { get; set; } = true;\n\n public DirectorCore? Director { get; private set; }\n public ISessionFlow? Session { get; private set; }\n public DirectorDecisionBatch? LastDecisions { get; private set; }\n public DirectorRunStatus LastRunStatus { get; private set; } = DirectorRunStatus.SessionInactive;\n\n DirectorHostRunner? _runner;\n\n protected override void OnStart() => InitializeDirector();\n\n protected override void OnFixedUpdate()\n {\n if (!DirectorEnabled || _runner is null) return;\n var result = _runner.Update(); LastRunStatus = result.Status; LastDecisions = result.Decisions;\n if (result.Decisions is not null) OnDirectorDecisions(result.Decisions, result.Results);\n }\n\n public void InitializeDirector()\n {\n Director = CreateDirector();\n Session = RoundBased ? new RoundSessionFlow() : new ContinuousSessionFlow();\n BindRunner();\n OnDirectorInitialized(Director);\n }\n\n public void BeginRound(string roundId)\n {\n if (Session is not RoundSessionFlow rounds) throw new InvalidOperationException(\"This component is not configured as round-based.\");\n _runner?.Stop();\n if (ResetDirectorOnRoundBegin) { Director = CreateDirector(); BindRunner(); OnDirectorInitialized(Director); }\n rounds.BeginRound(roundId);\n }\n\n public void EndRound()\n {\n if (Session is not RoundSessionFlow rounds) return;\n _runner?.Stop(); rounds.EndRound();\n }\n\n public void ReportPressure(PressureEvent input, double time) => _runner?.ReportPressure(input, time);\n public string SaveDirectorState() => Director is null ? throw new InvalidOperationException(\"Director has not initialized.\") : DirectorStateCodec.ToJson(Director.CaptureState());\n public void LoadDirectorState(string json)\n {\n _runner?.Stop(); Director = RestoreDirector(DirectorStateCodec.FromJson(json)); BindRunner(); OnDirectorInitialized(Director);\n }\n\n protected virtual DirectorConfiguration CreateConfiguration() => new() { Seed = Seed };\n protected virtual IDirectorPolicy CreatePolicy() => new DefaultDirectorPolicy();\n protected virtual IEncounterSchedule CreateSchedule() => new BasicEncounterSchedule();\n protected virtual IPlacementPolicy CreatePlacementPolicy() => new DefaultPlacementPolicy();\n protected virtual IEncounterComposer CreateComposer() => new DefaultEncounterComposer();\n protected virtual ResourcePopulationController? CreateResourceController() => null;\n protected virtual DirectorOrchestration? CreateOrchestration() => null;\n protected virtual DirectorOrchestration? RestoreOrchestration(DirectorOrchestrationState? state) => state is null ? CreateOrchestration() : throw new InvalidOperationException(\"Override RestoreOrchestration when orchestration persistence is enabled.\");\n protected virtual DirectorCore CreateDirector() => new(CreateConfiguration(), CreatePolicy(), CreateSchedule(), CreatePlacementPolicy(), CreateComposer(), CreateResourceController(), CreateOrchestration());\n protected virtual DirectorCore RestoreDirector(DirectorState state) => new(CreateConfiguration(), state, CreatePolicy(), CreateSchedule(), CreatePlacementPolicy(), CreateComposer(), CreateResourceController(), RestoreOrchestration(state.Orchestration));\n protected virtual bool HasDirectorAuthority => !IsProxy;\n protected virtual void OnDirectorInitialized(DirectorCore director) { }\n protected virtual void OnDirectorDecisions(DirectorDecisionBatch decisions, IReadOnlyList<SpawnRequestResult> results) { }\n protected abstract WorldSnapshot CaptureDirectorWorld();\n protected abstract SpawnRequestResult ExecuteDirectorSpawn(SpawnRequest request);\n protected virtual ResourceSpawnResult ExecuteDirectorResourceSpawn(ResourceSpawnRequest request) => new(request.RequestId, RequestResultKind.Rejected, \"Override ExecuteDirectorResourceSpawn to enable resource population.\");\n void BindRunner()\n {\n if (Director is null || Session is null) throw new InvalidOperationException(\"Director component is incomplete.\");\n _runner = new DirectorHostRunner(Director, Session, new DelegateWorldSource(CaptureDirectorWorld), new DelegateSpawnExecutor(ExecuteDirectorSpawn), new ComponentAuthority(this), new DelegateResourceExecutor(ExecuteDirectorResourceSpawn));\n }\n\n sealed class ComponentAuthority : IDirectorAuthority\n {\n readonly AdaptiveDirectorComponent _component;\n public ComponentAuthority(AdaptiveDirectorComponent component) { _component = component; }\n public bool IsAuthoritative => _component.HasDirectorAuthority;\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/Scenarios.cs",
"FileName": "Scenarios.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic sealed record ScenarioStageDefinition(\n string Id,\n double MinimumDuration = 0,\n bool RequiresCombatClear = false,\n EncounterKind? Encounter = null,\n int EncounterCount = 1,\n string? MusicCue = null);\npublic readonly record struct ScenarioState(ScenarioStatus Status, int StageIndex, double StageStarted);\n\npublic sealed class ScenarioController\n{\n readonly IReadOnlyList<ScenarioStageDefinition> _stages;\n public ScenarioStatus Status { get; private set; } = ScenarioStatus.Inactive;\n public int StageIndex { get; private set; } = -1;\n public double StageStarted { get; private set; }\n public ScenarioStageDefinition? Current => StageIndex >= 0 && StageIndex < _stages.Count ? _stages[StageIndex] : null;\n public ScenarioController(IReadOnlyList<ScenarioStageDefinition> stages)\n {\n _stages = stages ?? throw new ArgumentNullException(nameof(stages));\n if (_stages.Any(x => string.IsNullOrWhiteSpace(x.Id) || !double.IsFinite(x.MinimumDuration) || x.MinimumDuration < 0 || x.EncounterCount <= 0) || _stages.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != _stages.Count) throw new ArgumentException(\"Scenario stages are invalid.\", nameof(stages));\n }\n public ScenarioController(IReadOnlyList<ScenarioStageDefinition> stages, ScenarioState state) : this(stages) { Status = state.Status; StageIndex = state.StageIndex; StageStarted = state.StageStarted; if (!double.IsFinite(StageStarted) || StageIndex < -1 || StageIndex > _stages.Count || (Status == ScenarioStatus.Running && Current is null) || (Status == ScenarioStatus.Complete && StageIndex != _stages.Count)) throw new ArgumentException(\"Scenario state does not match the definition.\"); }\n public void Start(double time) { RequireTime(time); if (_stages.Count == 0) { Status = ScenarioStatus.Complete; StageIndex = 0; StageStarted = time; return; } Status = ScenarioStatus.Running; StageIndex = 0; StageStarted = time; }\n public bool TryAdvance(double time, bool combatActive)\n {\n RequireTime(time); if (time < StageStarted) throw new InvalidOperationException(\"Scenario time cannot move backwards.\");\n if (Status != ScenarioStatus.Running || Current is null) return false;\n if (time - StageStarted < Current.MinimumDuration || (Current.RequiresCombatClear && combatActive)) return false;\n StageIndex++; StageStarted = time; if (StageIndex >= _stages.Count) Status = ScenarioStatus.Complete; return true;\n }\n public void Fail() { if (Status == ScenarioStatus.Running) Status = ScenarioStatus.Failed; }\n public void Reset() { Status = ScenarioStatus.Inactive; StageIndex = -1; StageStarted = 0; }\n public ScenarioState Snapshot => new(Status, StageIndex, StageStarted);\n static void RequireTime(double time) { if (!double.IsFinite(time)) throw new ArgumentOutOfRangeException(nameof(time)); }\n}\n\npublic interface ISessionFlow\n{\n string ModeId { get; }\n string? CurrentRoundId { get; }\n bool IsSessionActive { get; }\n}\n\npublic sealed class ContinuousSessionFlow : ISessionFlow\n{\n public string ModeId { get; init; } = \"continuous\";\n public string? CurrentRoundId => null;\n public bool IsSessionActive { get; set; } = true;\n}\n\npublic sealed class RoundSessionFlow : ISessionFlow\n{\n public string ModeId { get; init; } = \"rounds\";\n public string? CurrentRoundId { get; private set; }\n public bool IsSessionActive => CurrentRoundId is not null;\n public void BeginRound(string id) { CurrentRoundId = string.IsNullOrWhiteSpace(id) ? throw new ArgumentException(\"Round id is required.\") : id; }\n public void EndRound() => CurrentRoundId = null;\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/Configuration.cs",
"FileName": "Configuration.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "namespace SboxDirector;\n\npublic sealed class PressureConfiguration\n{\n public float Gain { get; init; } = 0.25f;\n public float HoldSeconds { get; init; } = 5f;\n public float FullDecaySeconds { get; init; } = 30f;\n public float AveragedFollowingSeconds { get; init; } = 20f;\n public float LockValue { get; init; } = -1f;\n public IReadOnlyList<float> SeverityWeights { get; init; } = new[] { 0.05f, 0.20f, 0.50f, 1.00f, 999999.875f };\n\n public void Validate()\n {\n if (!float.IsFinite(Gain) || Gain < 0f) throw new ArgumentOutOfRangeException(nameof(Gain));\n if (!float.IsFinite(HoldSeconds) || HoldSeconds < 0f) throw new ArgumentOutOfRangeException(nameof(HoldSeconds));\n if (!float.IsFinite(FullDecaySeconds) || FullDecaySeconds <= 0f) throw new ArgumentOutOfRangeException(nameof(FullDecaySeconds));\n if (!float.IsFinite(AveragedFollowingSeconds) || AveragedFollowingSeconds <= 0f) throw new ArgumentOutOfRangeException(nameof(AveragedFollowingSeconds));\n if (SeverityWeights.Count != 5 || SeverityWeights.Any(x => !float.IsFinite(x) || x < 0f))\n throw new ArgumentException(\"Exactly five non-negative severity weights are required.\");\n }\n}\n\npublic sealed class TempoConfiguration\n{\n public double BuildUpMinimumSeconds { get; init; } = 15;\n public double SustainPeakSeconds { get; init; } = 10;\n public double RelaxMinimumSeconds { get; init; } = 20;\n public double RelaxMaximumSeconds { get; init; } = 45;\n public float PeakThreshold { get; init; } = 0.9f;\n public float RelaxThreshold { get; init; } = 0.9f;\n public float RelaxMaximumProgressTravel { get; init; } = 0.15f;\n\n public void Validate()\n {\n if (!double.IsFinite(BuildUpMinimumSeconds) || !double.IsFinite(SustainPeakSeconds) || !double.IsFinite(RelaxMinimumSeconds) || !double.IsFinite(RelaxMaximumSeconds) || BuildUpMinimumSeconds < 0 || SustainPeakSeconds < 0 || RelaxMinimumSeconds < 0) throw new ArgumentOutOfRangeException(\"Tempo durations must be finite and non-negative.\");\n if (RelaxMaximumSeconds < RelaxMinimumSeconds) throw new ArgumentException(\"RelaxMaximumSeconds must be >= RelaxMinimumSeconds.\");\n if (!float.IsFinite(PeakThreshold) || !float.IsFinite(RelaxThreshold) || !float.IsFinite(RelaxMaximumProgressTravel) || PeakThreshold is < 0f or > 1f || RelaxThreshold is < 0f or > 1f || RelaxMaximumProgressTravel < 0f) throw new ArgumentOutOfRangeException(\"Tempo thresholds and travel must be finite and non-negative; thresholds must be in [0,1].\");\n }\n}\n\npublic sealed class PlacementProfile\n{\n public PlacementSelectionMode SelectionMode { get; init; } = PlacementSelectionMode.WeightedScore;\n public float MinimumDistance { get; init; } = 500f;\n public float MaximumDistance { get; init; } = 2500f;\n public float IdealDistance { get; init; } = 1200f;\n public float MinimumThreatSeparation { get; init; } = 500f;\n public bool AllowVisible { get; init; }\n public float DistanceWeight { get; init; } = 1f;\n public float CapacityWeight { get; init; } = 0.25f;\n public float SeparationWeight { get; init; } = 0.25f;\n public float BehindProgressWeight { get; init; }\n public float MinimumPathDistance { get; init; }\n public float MinimumIngressDistance { get; init; }\n public float MinimumClearRadius { get; init; }\n public float MinimumAreaWidth { get; init; }\n public float MinimumAreaHeight { get; init; }\n public float PathDistanceWeight { get; init; }\n public float IngressWeight { get; init; }\n public float ClearRadiusWeight { get; init; }\n public IReadOnlySet<AdvanceRelation> AllowedRelations { get; init; } = new HashSet<AdvanceRelation>();\n public PlacementFallbackMode FallbackMode { get; init; } = PlacementFallbackMode.None;\n public IReadOnlySet<string> RequiredTags { get; init; } = new HashSet<string>();\n public IReadOnlySet<string> ExcludedTags { get; init; } = new HashSet<string>();\n public void Validate()\n {\n var measurements = new[] { MinimumDistance, MaximumDistance, IdealDistance, MinimumThreatSeparation, DistanceWeight, CapacityWeight, SeparationWeight, BehindProgressWeight, MinimumPathDistance, MinimumIngressDistance, MinimumClearRadius, MinimumAreaWidth, MinimumAreaHeight, PathDistanceWeight, IngressWeight, ClearRadiusWeight };\n if (measurements.Any(x => !float.IsFinite(x)) || MinimumDistance < 0 || MaximumDistance < MinimumDistance || IdealDistance < MinimumDistance || IdealDistance > MaximumDistance || MinimumThreatSeparation < 0 || MinimumPathDistance < 0 || MinimumIngressDistance < 0 || MinimumClearRadius < 0 || MinimumAreaWidth < 0 || MinimumAreaHeight < 0) throw new ArgumentException(\"Invalid placement distance or area profile.\");\n if (RequiredTags.Overlaps(ExcludedTags)) throw new ArgumentException(\"Placement tags cannot be both required and excluded.\");\n }\n}\n\npublic sealed class PopulationConfiguration\n{\n public IReadOnlyDictionary<EncounterKind, int> Limits { get; init; } = new Dictionary<EncounterKind, int>\n {\n [EncounterKind.Ambient] = 30,\n [EncounterKind.CommonWave] = 30,\n [EncounterKind.Special] = 4,\n [EncounterKind.Boss] = 1,\n [EncounterKind.DormantHazard] = 1,\n [EncounterKind.Objective] = 8,\n [EncounterKind.Custom] = 16\n };\n}\n\npublic sealed class DirectorConfiguration\n{\n public string Version { get; init; } = \"1.2\";\n public PressureConfiguration Pressure { get; init; } = new();\n public TempoConfiguration Tempo { get; init; } = new();\n public PopulationConfiguration Population { get; init; } = new();\n public IReadOnlyDictionary<EncounterKind, PlacementProfile> Placement { get; init; } = DefaultPlacement();\n public int Seed { get; init; } = 1;\n public double RequestTimeoutSeconds { get; init; } = 15;\n public int MaximumRequestsPerTick { get; init; } = 1;\n public bool ValidateWorldSnapshots { get; init; } = true;\n public RetryConfiguration Retry { get; init; } = new();\n\n public void Validate()\n {\n Pressure.Validate(); Tempo.Validate();\n if (string.IsNullOrWhiteSpace(Version)) throw new ArgumentException(\"A configuration version is required.\", nameof(Version));\n if (RequestTimeoutSeconds <= 0) throw new ArgumentOutOfRangeException(nameof(RequestTimeoutSeconds));\n if (MaximumRequestsPerTick <= 0) throw new ArgumentOutOfRangeException(nameof(MaximumRequestsPerTick));\n Retry.Validate();\n foreach (var pair in Population.Limits) if (pair.Value < 0) throw new ArgumentOutOfRangeException($\"Negative population limit for {pair.Key}.\");\n foreach (var profile in Placement.Values) profile.Validate();\n }\n\n static IReadOnlyDictionary<EncounterKind, PlacementProfile> DefaultPlacement() => new Dictionary<EncounterKind, PlacementProfile>\n {\n [EncounterKind.Ambient] = new() { MinimumDistance = 300f, IdealDistance = 800f, MaximumDistance = 1800f },\n [EncounterKind.CommonWave] = new(),\n [EncounterKind.Special] = new() { MinimumDistance = 600f, IdealDistance = 1400f },\n [EncounterKind.Boss] = new() { MinimumDistance = 900f, IdealDistance = 1800f, MinimumThreatSeparation = 1200f },\n [EncounterKind.DormantHazard] = new() { MinimumDistance = 700f, IdealDistance = 1500f, MinimumThreatSeparation = 1000f },\n [EncounterKind.Objective] = new() { AllowVisible = true, MinimumDistance = 0f, MaximumDistance = 5000f },\n [EncounterKind.Custom] = new()\n };\n}\n\npublic sealed class RetryConfiguration\n{\n public int MaximumAttempts { get; init; } = 3;\n public double DelaySeconds { get; init; } = 1;\n public bool RetryRejected { get; init; } = true;\n public bool RetryFailed { get; init; } = true;\n public bool RetryPartialRemainder { get; init; } = true;\n public void Validate()\n {\n if (MaximumAttempts <= 0 || !double.IsFinite(DelaySeconds) || DelaySeconds < 0) throw new ArgumentException(\"Invalid request retry configuration.\");\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/PlacementSelector.cs",
"FileName": "PlacementSelector.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic sealed record PlacementChoice(SpawnCandidate Candidate, float Score, bool IsFallback = false);\n\npublic interface IPlacementPolicy\n{\n bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world);\n float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world);\n}\n\n/// <summary>Implement this alongside IPlacementPolicy when a custom placement policy owns mutable deterministic state.</summary>\npublic interface IPersistentPlacementPolicy\n{\n string PersistenceId { get; }\n IReadOnlyDictionary<string, string> CapturePlacementState();\n void RestorePlacementState(IReadOnlyDictionary<string, string> state);\n}\n\npublic sealed class DefaultPlacementPolicy : IPlacementPolicy\n{\n public bool IsAllowed(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => true;\n public float ScoreAdjustment(SpawnCandidate candidate, PlacementProfile profile, WorldSnapshot world) => 0f;\n}\n\npublic sealed class PlacementSelector\n{\n public PlacementChoice? Select(IReadOnlyList<SpawnCandidate> candidates, PlacementProfile profile, DeterministicRandom random, WorldSnapshot? world = null, IPlacementPolicy? policy = null)\n {\n var ordered = OrderCandidates(candidates, profile.SelectionMode, random);\n var strict = BuildChoices(ordered, profile, world, policy, profile.SelectionMode, ignoreRoute: false, broadFallback: false);\n if (strict.Count > 0) return Choose(strict, profile.SelectionMode, random, false);\n if (profile.FallbackMode == PlacementFallbackMode.None) return null;\n ordered = OrderCandidates(candidates, profile.SelectionMode, random);\n var fallback = BuildChoices(ordered, profile, world, policy, profile.SelectionMode, ignoreRoute: true, broadFallback: profile.FallbackMode == PlacementFallbackMode.AnyReachableHidden);\n return fallback.Count == 0 ? null : Choose(fallback, profile.SelectionMode, random, true);\n }\n\n static List<PlacementChoice> BuildChoices(IReadOnlyList<SpawnCandidate> candidates, PlacementProfile profile, WorldSnapshot? world, IPlacementPolicy? policy, PlacementSelectionMode mode, bool ignoreRoute, bool broadFallback)\n {\n var choices = new List<PlacementChoice>();\n foreach (var candidate in candidates)\n {\n if (!candidate.Reachable || !candidate.Spawnable || (!profile.AllowVisible && candidate.Visible)) continue;\n if (candidate.AreaWidth < profile.MinimumAreaWidth || candidate.AreaHeight < profile.MinimumAreaHeight) continue;\n if (!broadFallback && (candidate.NearestParticipantDistance < profile.MinimumDistance || candidate.NearestParticipantDistance > profile.MaximumDistance)) continue;\n if (!broadFallback && candidate.ThreatSeparation < profile.MinimumThreatSeparation) continue;\n if (profile.RequiredTags.Any(tag => !candidate.Tags.Contains(tag)) || profile.ExcludedTags.Any(candidate.Tags.Contains)) continue;\n if (!ignoreRoute && (candidate.PathDistance < profile.MinimumPathDistance || candidate.IngressDistance < profile.MinimumIngressDistance || candidate.ClearRadius < profile.MinimumClearRadius)) continue;\n if (!ignoreRoute && profile.AllowedRelations.Count > 0 && !profile.AllowedRelations.Contains(candidate.AdvanceRelation)) continue;\n if (world is not null && policy is not null && !policy.IsAllowed(candidate, profile, world)) continue;\n var score = mode == PlacementSelectionMode.HighestProgressFirstTie ? candidate.Progress : 0f;\n if (mode == PlacementSelectionMode.WeightedScore)\n {\n var distanceRange = Math.Max(1f, profile.MaximumDistance - profile.MinimumDistance);\n var distanceScore = 1f - Math.Min(1f, Math.Abs(candidate.NearestParticipantDistance - profile.IdealDistance) / distanceRange);\n score = distanceScore * profile.DistanceWeight + candidate.AreaCapacity * profile.CapacityWeight + Math.Min(1f, candidate.ThreatSeparation / Math.Max(1f, profile.MinimumThreatSeparation * 2f)) * profile.SeparationWeight;\n if (candidate.RelativeProgress < 0) score += Math.Min(1f, -candidate.RelativeProgress) * profile.BehindProgressWeight;\n score += Math.Min(1f, candidate.PathDistance / Math.Max(1f, profile.MinimumPathDistance * 2f)) * profile.PathDistanceWeight;\n score += Math.Min(1f, candidate.IngressDistance / Math.Max(1f, profile.MinimumIngressDistance * 2f)) * profile.IngressWeight;\n score += Math.Min(1f, candidate.ClearRadius / Math.Max(1f, profile.MinimumClearRadius * 2f)) * profile.ClearRadiusWeight;\n if (world is not null && policy is not null)\n {\n var adjustment = policy.ScoreAdjustment(candidate, profile, world); if (!float.IsFinite(adjustment)) throw new InvalidOperationException(\"Placement policy returned a non-finite score adjustment.\"); score += adjustment;\n }\n }\n if (!float.IsFinite(score)) throw new InvalidOperationException(\"Placement scoring produced a non-finite value.\");\n choices.Add(new PlacementChoice(candidate, score));\n }\n return choices;\n }\n\n static IReadOnlyList<SpawnCandidate> OrderCandidates(IReadOnlyList<SpawnCandidate> candidates, PlacementSelectionMode mode, DeterministicRandom random)\n {\n if (mode != PlacementSelectionMode.NativeRandomTranspositionFirst) return candidates;\n var shuffled = candidates.ToArray();\n for (var index = 0; index < shuffled.Length; index++)\n {\n var other = random.NextInt(shuffled.Length);\n (shuffled[index], shuffled[other]) = (shuffled[other], shuffled[index]);\n }\n return shuffled;\n }\n\n static PlacementChoice Choose(List<PlacementChoice> choices, PlacementSelectionMode mode, DeterministicRandom random, bool fallback)\n {\n if (mode is PlacementSelectionMode.FirstEligible or PlacementSelectionMode.NativeRandomTranspositionFirst) return choices[0] with { IsFallback = fallback };\n if (mode == PlacementSelectionMode.HighestProgressFirstTie)\n {\n var progressChoice = choices[0];\n for (var index = 1; index < choices.Count; index++) if (choices[index].Candidate.Progress > progressChoice.Candidate.Progress) progressChoice = choices[index];\n return progressChoice with { IsFallback = fallback };\n }\n var best = choices.Max(x => x.Score); var tied = choices.Where(x => Math.Abs(x.Score - best) < 0.00001f).ToArray();\n var selected = tied[random.NextInt(tied.Length)]; return selected with { IsFallback = fallback };\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "Code/SboxDirector/WaveSequenceController.cs",
"FileName": "WaveSequenceController.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "namespace SboxDirector;\n\npublic sealed class WaveSequenceConfiguration\n{\n public int WaveCount { get; init; } = 1;\n public double InitialDelayMinimum { get; init; } = 1;\n public double InitialDelayMaximum { get; init; } = 2;\n public double CombatTimeout { get; init; } = 10;\n public double PauseMinimum { get; init; } = 5;\n public double PauseMaximum { get; init; } = 10;\n public void Validate()\n {\n var durations = new[] { InitialDelayMinimum, InitialDelayMaximum, CombatTimeout, PauseMinimum, PauseMaximum };\n if (WaveCount <= 0 || durations.Any(x => !double.IsFinite(x)) || InitialDelayMinimum < 0 || InitialDelayMaximum < InitialDelayMinimum || CombatTimeout < 0 || PauseMinimum < 0 || PauseMaximum < PauseMinimum) throw new ArgumentException(\"Invalid wave sequence configuration.\");\n }\n}\n\npublic readonly record struct WaveSequenceState(WaveState State, int IssuedWaves, int TargetWaves, double Deadline);\npublic readonly record struct WaveAction(bool IssueWave, bool Completed, string Reason);\n\npublic sealed class WaveSequenceController\n{\n readonly WaveSequenceConfiguration _configuration; readonly DeterministicRandom _random;\n public WaveState State { get; private set; } = WaveState.Inactive;\n public int IssuedWaves { get; private set; }\n public int TargetWaves { get; private set; }\n public double Deadline { get; private set; }\n public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random) { configuration.Validate(); _configuration = configuration; _random = random; }\n public WaveSequenceController(WaveSequenceConfiguration configuration, DeterministicRandom random, WaveSequenceState state) : this(configuration, random) { State = state.State; IssuedWaves = state.IssuedWaves; TargetWaves = state.TargetWaves; Deadline = state.Deadline; if (!double.IsFinite(Deadline) || IssuedWaves < 0 || TargetWaves < 0 || IssuedWaves > TargetWaves) throw new ArgumentException(\"Wave state is invalid.\", nameof(state)); }\n public WaveSequenceState Snapshot => new(State, IssuedWaves, TargetWaves, Deadline);\n public void Start(double time, int? waveCount = null) { RequireTime(time); TargetWaves = waveCount ?? _configuration.WaveCount; if (TargetWaves <= 0) throw new ArgumentOutOfRangeException(nameof(waveCount)); IssuedWaves = 0; State = WaveState.InitialDelay; Deadline = time + Range(_configuration.InitialDelayMinimum, _configuration.InitialDelayMaximum); }\n public void Cancel() { State = WaveState.Inactive; IssuedWaves = 0; TargetWaves = 0; Deadline = 0; }\n public WaveAction Update(double time, bool relevantCombatantsRemain)\n {\n RequireTime(time);\n if (State is WaveState.Inactive or WaveState.Done) return new(false, State == WaveState.Done, \"inactive or complete\");\n if (time < Deadline) return new(false, false, \"waiting for timer\");\n if (State == WaveState.InitialDelay) { State = WaveState.IssueWave; return new(false, false, \"initial delay expired\"); }\n if (State == WaveState.IssueWave) { IssuedWaves++; State = WaveState.WaitForClear; Deadline = time + _configuration.CombatTimeout; return new(true, false, \"wave issued\"); }\n if (State == WaveState.WaitForClear)\n {\n if (relevantCombatantsRemain) { Deadline = time + _configuration.CombatTimeout; return new(false, false, \"combat remains\"); }\n if (IssuedWaves >= TargetWaves) { State = WaveState.Done; return new(false, true, \"target waves issued and combat clear\"); }\n State = WaveState.Pause; Deadline = time + Range(_configuration.PauseMinimum, _configuration.PauseMaximum); return new(false, false, \"pausing before next wave\");\n }\n if (State == WaveState.Pause) { State = WaveState.IssueWave; return new(false, false, \"pause expired\"); }\n return new(false, false, \"no action\");\n }\n double Range(double minimum, double maximum) => minimum + (maximum - minimum) * _random.NextFloat();\n static void RequireTime(double time) { if (!double.IsFinite(time)) throw new ArgumentOutOfRangeException(nameof(time)); }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "examples/SaveRestoreEverythingExample.cs",
"FileName": "SaveRestoreEverythingExample.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "using SboxDirector;\n\nnamespace AdaptiveDirectorExamples;\n\npublic sealed record ExampleSaveDocument(string DirectorJson, string MusicAuthorityJson, string MusicRuntimeJson);\n\n/// <summary>Persists encounter direction, authoritative music, and client/local track queues together.</summary>\npublic sealed class SaveRestoreEverythingExample\n{\n readonly DirectorConfiguration _directorConfiguration = new() { Version = \"save-example-v1\", Seed = 42 };\n readonly MusicDirectorConfiguration _musicConfiguration = RecoveredMusicDirectorDefaults.CreateConfiguration(ExampleMusicSetup.CreateCues(), ExampleMusicSetup.CreateSpecialAlerts(), seed: 42);\n DirectorCore _director;\n MusicDirectorCore _music;\n MusicRuntime _runtime;\n\n public SaveRestoreEverythingExample(string participantId)\n {\n ParticipantId = participantId; _director = new DirectorCore(_directorConfiguration); _music = new MusicDirectorCore(_musicConfiguration); _runtime = new MusicRuntime(participantId, ExampleMusicSetup.CreateCatalog());\n }\n\n public string ParticipantId { get; }\n public DirectorDecisionBatch TickDirector(WorldSnapshot world) => _director.Tick(world);\n public void ApplySpawnResult(SpawnRequestResult result) => _director.ApplyResult(result);\n public void ReportPressure(PressureEvent pressure, double time) => _director.ApplyPressure(pressure, time);\n public MusicRuntimeBatch TickMusic(MusicWorldSnapshot world)\n {\n var decisions = _music.Tick(world); return _runtime.Apply(decisions);\n }\n\n public ExampleSaveDocument Save() => new(\n DirectorStateCodec.ToJson(_director.CaptureState()),\n MusicStateCodec.DirectorToJson(_music.CaptureState()),\n MusicStateCodec.RuntimeToJson(_runtime.CaptureState()));\n\n public void Load(ExampleSaveDocument save)\n {\n var directorState = DirectorStateCodec.FromJson(save.DirectorJson);\n var musicState = MusicStateCodec.DirectorFromJson(save.MusicAuthorityJson);\n var runtimeState = MusicStateCodec.RuntimeFromJson(save.MusicRuntimeJson);\n _director = new DirectorCore(_directorConfiguration, directorState);\n _music = new MusicDirectorCore(_musicConfiguration, musicState);\n _runtime = new MusicRuntime(ParticipantId, ExampleMusicSetup.CreateCatalog(), runtimeState);\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/ConcurrentSchedules.cs",
"FileName": "ConcurrentSchedules.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "#nullable enable annotations\nnamespace SboxDirector;\n\npublic sealed class ScheduleLane\n{\n public string Id { get; init; } = \"lane\";\n public int Priority { get; init; }\n public IEncounterSchedule Schedule { get; init; } = null!;\n public Func<WorldSnapshot, bool>? Enabled { get; init; }\n}\n\n/// <summary>\n/// Runs independent encounter lanes during one authoritative tick. This mirrors the proven separation\n/// of survival lull, horde, special, and boss schedules without copying their unrecovered formulas.\n/// </summary>\npublic sealed class ConcurrentEncounterSchedule : IEncounterSchedule, IPersistentEncounterSchedule, IEncounterScheduleFeedback\n{\n readonly IReadOnlyList<ScheduleLane> _lanes;\n readonly HashSet<string> _served = new(StringComparer.Ordinal);\n readonly string _signature;\n ScheduleLane? _selected;\n double _servedTime = double.NaN;\n\n public ConcurrentEncounterSchedule(IEnumerable<ScheduleLane> lanes)\n {\n _lanes = lanes.OrderByDescending(x => x.Priority).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();\n if (_lanes.Count == 0 || _lanes.Any(x => x.Schedule is null || string.IsNullOrWhiteSpace(x.Id) || x.Id.Contains(':'))) throw new ArgumentException(\"Concurrent schedule lanes require unique non-empty ids without colons and a schedule.\");\n if (_lanes.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != _lanes.Count) throw new ArgumentException(\"Concurrent schedule lane ids must be unique.\");\n _signature = string.Join(\"|\", _lanes.Select(x => $\"{x.Id.Length}:{x.Id}:{x.Priority}\"));\n }\n\n public EncounterKind? SelectEncounter(WorldSnapshot world, TempoPhase tempo, float teamPressure, PopulationLedger population, DeterministicRandom random)\n {\n _selected = null;\n if (_servedTime != world.Time) { _servedTime = world.Time; _served.Clear(); }\n foreach (var lane in _lanes)\n {\n if (_served.Contains(lane.Id) || (lane.Enabled is not null && !lane.Enabled(world))) continue;\n var kind = lane.Schedule.SelectEncounter(world, tempo, teamPressure, population, random);\n if (kind is null) continue;\n _selected = lane; _served.Add(lane.Id); return kind;\n }\n return null;\n }\n\n public int RequestedCount(EncounterKind kind, WorldSnapshot world) => _selected?.Schedule.RequestedCount(kind, world) ?? 1;\n\n public void SelectionFinalized(bool requestCreated)\n {\n if (_selected is null) return;\n if (_selected.Schedule is IEncounterScheduleFeedback feedback) feedback.SelectionFinalized(requestCreated);\n if (!requestCreated) _served.Remove(_selected.Id);\n _selected = null;\n }\n\n public IReadOnlyDictionary<string, string> CaptureScheduleState()\n {\n var state = new Dictionary<string, string> { [\"coordinator.signature\"] = _signature, [\"coordinator.time\"] = _servedTime.ToString(\"R\", CultureInfo.InvariantCulture), [\"coordinator.served\"] = string.Join(\"|\", _served.OrderBy(x => x, StringComparer.Ordinal)) };\n foreach (var lane in _lanes)\n if (lane.Schedule is IPersistentEncounterSchedule persistent)\n foreach (var pair in persistent.CaptureScheduleState()) state[$\"lane:{lane.Id}:{pair.Key}\"] = pair.Value;\n return state;\n }\n\n public void RestoreScheduleState(IReadOnlyDictionary<string, string> state)\n {\n _served.Clear();\n if (!state.TryGetValue(\"coordinator.signature\", out var signature) || !string.Equals(signature, _signature, StringComparison.Ordinal)) throw new InvalidOperationException(\"Concurrent schedule state does not match its configured lanes.\");\n if (state.TryGetValue(\"coordinator.time\", out var time) && double.TryParse(time, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) _servedTime = parsed;\n if (state.TryGetValue(\"coordinator.served\", out var served)) foreach (var id in served.Split('|', StringSplitOptions.RemoveEmptyEntries)) _served.Add(id);\n foreach (var lane in _lanes)\n {\n if (lane.Schedule is not IPersistentEncounterSchedule persistent) continue;\n var prefix = $\"lane:{lane.Id}:\";\n persistent.RestoreScheduleState(state.Where(x => x.Key.StartsWith(prefix, StringComparison.Ordinal)).ToDictionary(x => x.Key[prefix.Length..], x => x.Value));\n }\n }\n}\n"
},
{
"Ident": "notpointless.chomnr_adaptive_director",
"Path": "SboxDirector/PopulationLedger.cs",
"FileName": "PopulationLedger.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 337987,
"Code": "namespace SboxDirector;\n\npublic sealed class PopulationLedger\n{\n readonly PopulationConfiguration _configuration;\n readonly Dictionary<EncounterKind, int> _reserved = new();\n readonly Dictionary<long, PendingReservation> _pending = new();\n public PopulationLedger(PopulationConfiguration configuration) { _configuration = configuration; }\n\n public int Available(EncounterKind kind, PopulationSnapshot population)\n => Available(kind, population, ConfiguredLimit(kind));\n public int ConfiguredLimit(EncounterKind kind) => _configuration.Limits.TryGetValue(kind, out var configured) ? configured : 0;\n public int Available(EncounterKind kind, PopulationSnapshot population, int limit)\n {\n return Math.Max(0, limit - population[kind] - (_reserved.TryGetValue(kind, out var value) ? value : 0));\n }\n public bool TryReserve(long requestId, EncounterKind kind, int count, PopulationSnapshot population, double expiresAt = double.PositiveInfinity)\n => TryReserveWithLimit(requestId, kind, count, population, ConfiguredLimit(kind), expiresAt);\n public bool TryReserveWithLimit(long requestId, EncounterKind kind, int count, PopulationSnapshot population, int limit, double expiresAt = double.PositiveInfinity)\n {\n if (count <= 0 || limit < 0 || _pending.ContainsKey(requestId) || Available(kind, population, limit) < count) return false;\n _pending[requestId] = new(requestId, kind, count, expiresAt); _reserved[kind] = (_reserved.TryGetValue(kind, out var value) ? value : 0) + count; return true;\n }\n public void Resolve(SpawnRequestResult result)\n {\n if (result.Result == RequestResultKind.Deferred) return;\n if (!_pending.Remove(result.RequestId, out var pending)) return;\n _reserved[pending.Kind] = Math.Max(0, _reserved[pending.Kind] - pending.Count);\n }\n public IReadOnlyList<long> CancelAll()\n {\n var ids = _pending.Keys.OrderBy(x => x).ToArray(); _pending.Clear(); _reserved.Clear(); return ids;\n }\n public IReadOnlyList<long> Expire(double time)\n {\n var expired = _pending.Values.Where(x => x.ExpiresAt <= time).Select(x => x.RequestId).ToArray();\n foreach (var id in expired) Resolve(new SpawnRequestResult(id, RequestResultKind.Failed, 0, \"request timeout\"));\n return expired;\n }\n public PopulationLedgerState Capture() => new(_pending.Values.OrderBy(x => x.RequestId).ToArray());\n public void Restore(PopulationLedgerState state)\n {\n _pending.Clear(); _reserved.Clear();\n foreach (var item in state.Pending) { _pending[item.RequestId] = item; _reserved[item.Kind] = (_reserved.TryGetValue(item.Kind, out var count) ? count : 0) + item.Count; }\n }\n public IReadOnlyDictionary<EncounterKind, int> Reservations => _reserved;\n public IReadOnlyList<PendingReservation> Pending => _pending.Values.OrderBy(x => x.RequestId).ToArray();\n}\n\npublic readonly record struct PendingReservation(long RequestId, EncounterKind Kind, int Count, double ExpiresAt);\npublic readonly record struct PopulationLedgerState(IReadOnlyList<PendingReservation> Pending);\n"
}
]
}