using AcDream.Plugin.Abstractions; using System.Runtime.Loader; namespace AcDream.Plugin.Tests.Fixtures.HostPlugin; /// /// Cross-host LA5 fixture. It deliberately takes the same path on graphical /// and no-window hosts: observe the capability, register UI, and subscribe to /// gameplay events. A headless registry must make the UI call harmless without /// retaining this instance in the default load context. /// public sealed class HostPlugin : IAcDreamPlugin { private IPluginHost? _host; private string? _assemblyDirectory; private bool _throwAfterRegistration; private bool _throwDuringInitialize; private int _entitiesSeen; public void Initialize(IPluginHost host) { _host = host ?? throw new ArgumentNullException(nameof(host)); _assemblyDirectory = Path.GetDirectoryName( typeof(HostPlugin).Assembly.Location); _throwAfterRegistration = File.Exists( Path.Combine(_assemblyDirectory!, "throw-after-register")); _throwDuringInitialize = File.Exists( Path.Combine(_assemblyDirectory!, "throw-during-initialize")); host.Log.Info($"fixture-initialized:hasUi={host.HasUi}"); if (_throwDuringInitialize) { RegisterHostCallbacks(host); AssemblyLoadContext.GetLoadContext(typeof(HostPlugin).Assembly)! .Unloading += OnUnloading; throw new InvalidOperationException( "fixture initialize failed after registering UI, entity, and selection callbacks"); } } public void Enable() { IPluginHost host = _host ?? throw new InvalidOperationException("The fixture was not initialized."); RegisterHostCallbacks(host); if (_throwAfterRegistration) { throw new InvalidOperationException( "fixture enable failed after registering UI and events"); } host.Log.Info( $"fixture-enabled:hasUi={host.HasUi}:entities={host.State.Entities.Count}"); } public void Disable() { IPluginHost? host = _host; if (host is null) return; if (_throwAfterRegistration || _throwDuringInitialize) { throw new InvalidOperationException( "fixture disable intentionally refuses cleanup"); } host.Events.EntitySpawned -= OnEntitySpawned; host.Selection.Changed -= OnSelectionChanged; host.Log.Info($"fixture-disabled:entitiesSeen={_entitiesSeen}"); _host = null; } private void RegisterHostCallbacks(IPluginHost host) { host.Ui.AddMarkupPanel( Path.Combine(AppContext.BaseDirectory, "fixture-panel.xml"), this); host.Events.EntitySpawned += OnEntitySpawned; host.Selection.Changed += OnSelectionChanged; } private void OnEntitySpawned(WorldEntitySnapshot snapshot) { _entitiesSeen++; RecordUnexpectedCallback(snapshot.Id); } private void OnSelectionChanged(SelectionChangedEvent change) => RecordUnexpectedCallback(change.SelectedObjectId ?? 0u); private void RecordUnexpectedCallback(uint objectId) { if ((_throwAfterRegistration || _throwDuringInitialize) && _assemblyDirectory is not null) { File.AppendAllText( Path.Combine(_assemblyDirectory, "unexpected-callback"), $"{objectId}{Environment.NewLine}"); } } private void OnUnloading(AssemblyLoadContext context) { IPluginHost host = _host!; bool uiClosed = Rejects(() => host.Ui.AddMarkupPanel( Path.Combine(AppContext.BaseDirectory, "unloading-panel.xml"), this)); bool eventsClosed = Rejects(() => { host.Events.EntitySpawned += OnEntitySpawned; }); bool selectionClosed = Rejects(() => { host.Selection.Changed += OnSelectionChanged; }); File.WriteAllText( Path.Combine(_assemblyDirectory!, "unload-observation"), $"ui={uiClosed};events={eventsClosed};selection={selectionClosed}"); } private static bool Rejects(Action action) { try { action(); return false; } catch (ObjectDisposedException) { return true; } } }