Editor/SboxDirector/DirectorCompileGate.cs

Editor utility that runs once on editor startup to perform a self-test of Director and Music systems and write a JSON result to a path provided via the ADAPTIVE_DIRECTOR_GATE_RESULT environment variable. It constructs configurations, runs a few ticks and state roundtrips, records pass/fail flags, writes the result file, and then quits the editor or exits the process.

File Access
#nullable enable annotations
using System;
using System.IO;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Editor;
using Sandbox;

namespace SboxDirector.Editor;

public static class DirectorCompileGate
{
    static bool _started;
    [EditorEvent.Frame]
    public static void Tick()
    {
        if (_started) return;
        _started = true;
        var path = Environment.GetEnvironmentVariable("ADAPTIVE_DIRECTOR_GATE_RESULT");
        if (string.IsNullOrWhiteSpace(path) || !File.Exists(path + ".arm")) return;
        try { File.Delete(path + ".arm"); } catch { return; }
        _ = Run(path);
    }

    static async Task Run(string path)
    {
        var result = new GateResult();
        try
        {
            result.ProjectPath = Project.Current?.GetRootPath();
            if ((result.ProjectPath ?? "").IndexOf("adaptive-director-editor-rig", StringComparison.OrdinalIgnoreCase) < 0) return;
            var configuration = new DirectorConfiguration { MaximumRequestsPerTick = 2, Placement = new Dictionary<EncounterKind, PlacementProfile> { [EncounterKind.Boss] = RecoveredDirectorDefaults.CreateThreatAreaProfile() } };
            var orchestrationConfiguration = new DirectorOrchestrationConfiguration { PersistenceId = "editor-gate-v1", ScenarioStages = new[] { new ScenarioStageDefinition("boss", Encounter: EncounterKind.Boss) } };
            var resourceRules = new[] { new ResourceRule { Category = "supply", TargetCount = 1 } };
            var composer = new UniformEncounterComposer(new Dictionary<EncounterKind, IReadOnlyList<string>> { [EncounterKind.Boss] = new[] { "gate_boss" } });
            var core = new DirectorCore(configuration, composer: composer, resources: new ResourcePopulationController(resourceRules), orchestration: new DirectorOrchestration(orchestrationConfiguration)); core.StartScenario(0);
            core.ApplyPressure(new PressureEvent("gate", PressureSeverity.Major, "editor gate"), 0);
            var world = new WorldSnapshot { Time = 0, Participants = new[] { new ParticipantSnapshot { Id = "gate" } }, SpawnCandidates = new[] { new SpawnCandidate { Id = "spawn", Reachable = true, Spawnable = true, NearestParticipantDistance = 1200, ThreatSeparation = 3000, PathDistance = 1000, IngressDistance = 500, ClearRadius = 500, AreaWidth = 24, AreaHeight = 24 } }, Resources = new ResourceWorldSnapshot { Candidates = new[] { new ResourceCandidate { Id = "resource", Category = "supply" } } } };
            var batch = core.Tick(world);
            var json = DirectorStateCodec.ToJson(core.CaptureState()); var state = DirectorStateCodec.FromJson(json);
            var restoredResources = new ResourcePopulationController(resourceRules); var restoredOrchestration = new DirectorOrchestration(orchestrationConfiguration, state.Orchestration!.Value); var restored = new DirectorCore(configuration, state, composer: composer, resources: restoredResources, orchestration: restoredOrchestration);
            var restoredBatch = restored.Tick(new WorldSnapshot { Time = 1, Participants = new[] { new ParticipantSnapshot { Id = "gate" } } });
            var samples = HullVisibilitySamples.CreateFivePointPattern(new DirectorVector(10, 0, 0), new DirectorVector(0, 0, 0), RecoveredDirectorDefaults.BossHullWidth, RecoveredDirectorDefaults.BossHullHeight);
            var rotation = new SpecialClassRotation(new[] { "gate_special" }); rotation.RestoreState(new Dictionary<string, double> { ["gate_special"] = 0 });
            result.CoreSmokePassed = batch.Time == 0 && batch.TeamPressure > 0 && batch.SpawnRequests.Count >= 1 && batch.SpawnRequests[0].Kind == EncounterKind.Boss && batch.SpawnRequests[0].Composition[0].Archetype == "gate_boss" && batch.ResourceRequests.Count == 1 && restoredBatch.TeamPressure == batch.TeamPressure && samples.Count == 5 && rotation.SelectDue(0, _ => true, new DeterministicRandom(1)) == "gate_special";

            var musicCues = new MusicCueConfiguration { CombatIntroEvents = new[] { "gate.combat" }, CombatSecondaryEvent = "", CombatCloseEvent = "", MobbedEvent = "", BossApproachingEvent = "", SafeAtmosphereEvent = "", DangerAtmosphereEvent = "", PositionalStingerEvent = "" };
            var musicConfig = RecoveredMusicDirectorDefaults.CreateConfiguration(musicCues, seed: 7); var music = new MusicDirectorCore(musicConfig);
            music.Tick(new MusicWorldSnapshot { Time = 0, Participants = new[] { new MusicParticipantSnapshot { Id = "gate" } } });
            var musicDecisions = music.Tick(new MusicWorldSnapshot { Time = 1, Participants = new[] { new MusicParticipantSnapshot { Id = "gate", VisibleCommonThreats = 2, VeryCloseCommonFactor = 1, CumulativeDamage = 5 } } });
            var catalog = new MusicEventCatalog(new[] { new MusicEventDefinition { EventId = "gate.combat", TrackId = "combat", Priority = MusicPriority.Critical } }); var musicRuntime = new MusicRuntime("gate", catalog); var musicActions = musicRuntime.Apply(musicDecisions);
            var musicState = MusicStateCodec.DirectorFromJson(MusicStateCodec.DirectorToJson(music.CaptureState())); var runtimeState = MusicStateCodec.RuntimeFromJson(MusicStateCodec.RuntimeToJson(musicRuntime.CaptureState()));
            var restoredMusic = new MusicDirectorCore(musicConfig, musicState); var restoredRuntime = new MusicRuntime("gate", catalog, runtimeState);
            result.MusicSmokePassed = musicDecisions.Decisions.Exists(x => x.Kind == MusicDecisionKind.Play && x.TargetId == "gate.combat") && musicActions.Actions.Exists(x => x.Kind == MusicRuntimeActionKind.Play && x.EventId == "gate.combat") && restoredMusic.CaptureState().NextDecisionId == musicState.NextDecisionId && restoredRuntime.Active.Count == 1;
        }
        catch (Exception e) { result.Error = e.ToString(); }
        result.Completed = true; result.Passed = result.CoreSmokePassed && result.MusicSmokePassed;
        File.WriteAllText(path, JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
        await Task.Delay(500); EditorUtility.Quit(true);
        await Task.Delay(10000); Environment.Exit(result.Passed ? 0 : 1);
    }

    sealed class GateResult
    {
        public bool Completed { get; set; }
        public bool Passed { get; set; }
        public bool CoreSmokePassed { get; set; }
        public bool MusicSmokePassed { get; set; }
        public string? ProjectPath { get; set; }
        public string? Error { get; set; }
    }
}