using System.Numerics; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Physics; using AcDream.Core.World; using AcDream.UI.Abstractions.Input; namespace AcDream.App.Diagnostics; internal interface IRuntimeDiagnosticCommands { bool Handle(InputAction action); void CycleTimeOfDay(); void CycleWeather(); void ToggleCollisionWireframes(); } /// /// Focused deferred edge used by the early-created developer panel and the /// later-created gameplay router. It owns no diagnostic state and binds one /// window-lifetime command owner exactly once. /// internal sealed class RuntimeDiagnosticCommandSlot : IRuntimeDiagnosticCommands { private readonly object _gate = new(); private IRuntimeDiagnosticCommands? _target; private bool _deactivated; public void Bind(IRuntimeDiagnosticCommands target) { ArgumentNullException.ThrowIfNull(target); lock (_gate) { ObjectDisposedException.ThrowIf(_deactivated, this); if (_target is not null && !ReferenceEquals(_target, target)) { throw new InvalidOperationException( "Runtime diagnostic commands are already bound."); } _target = target; } } public void Unbind(IRuntimeDiagnosticCommands target) { ArgumentNullException.ThrowIfNull(target); lock (_gate) { if (ReferenceEquals(_target, target)) _target = null; } } public IDisposable BindOwned(IRuntimeDiagnosticCommands target) { ArgumentNullException.ThrowIfNull(target); lock (_gate) { ObjectDisposedException.ThrowIf(_deactivated, this); if (_target is not null) { throw new InvalidOperationException( "Runtime diagnostic commands are already bound."); } _target = target; } return new Binding(this, target); } public void Deactivate() { lock (_gate) { _deactivated = true; _target = null; } } public bool Handle(InputAction action) { lock (_gate) { if (!_deactivated && _target is { } target) return target.Handle(action); } return RuntimeDiagnosticCommandController.IsDiagnosticAction(action); } public void CycleTimeOfDay() { lock (_gate) { if (!_deactivated) _target?.CycleTimeOfDay(); } } public void CycleWeather() { lock (_gate) { if (!_deactivated) _target?.CycleWeather(); } } public void ToggleCollisionWireframes() { lock (_gate) { if (!_deactivated) _target?.ToggleCollisionWireframes(); } } private sealed class Binding : IDisposable { private RuntimeDiagnosticCommandSlot? _owner; private readonly IRuntimeDiagnosticCommands _expected; public Binding( RuntimeDiagnosticCommandSlot owner, IRuntimeDiagnosticCommands expected) { _owner = owner; _expected = expected; } public void Dispose() => Interlocked.Exchange(ref _owner, null)?.Unbind(_expected); } } internal interface INearbyWorldDiagnosticSource { Vector3 ObserverPosition { get; } int LiveCenterX { get; } int LiveCenterY { get; } int TotalShadowObjects { get; } IReadOnlyList WorldEntities { get; } IEnumerable ShadowEntries { get; } } /// /// Read-only view over canonical runtime owners. It does not cache the current /// player, camera, entity list, origin, or shadow registry. /// internal sealed class RuntimeNearbyWorldDiagnosticSource : INearbyWorldDiagnosticSource { private readonly ILocalPlayerModeSource _mode; private readonly ILocalPlayerControllerSource _player; private readonly CameraController _camera; private readonly LiveWorldOriginState _origin; private readonly GpuWorldState _world; private readonly PhysicsEngine _physics; public RuntimeNearbyWorldDiagnosticSource( ILocalPlayerModeSource mode, ILocalPlayerControllerSource player, CameraController camera, LiveWorldOriginState origin, GpuWorldState world, PhysicsEngine physics) { _mode = mode ?? throw new ArgumentNullException(nameof(mode)); _player = player ?? throw new ArgumentNullException(nameof(player)); _camera = camera ?? throw new ArgumentNullException(nameof(camera)); _origin = origin ?? throw new ArgumentNullException(nameof(origin)); _world = world ?? throw new ArgumentNullException(nameof(world)); _physics = physics ?? throw new ArgumentNullException(nameof(physics)); } public Vector3 ObserverPosition { get { if (_mode.IsPlayerMode && _player.Controller is { } player) return player.Position; Matrix4x4.Invert(_camera.Active.View, out Matrix4x4 inverseView); return new Vector3( inverseView.M41, inverseView.M42, inverseView.M43); } } public int LiveCenterX => _origin.CenterX; public int LiveCenterY => _origin.CenterY; public int TotalShadowObjects => _physics.ShadowObjects.TotalRegistered; public IReadOnlyList WorldEntities => _world.Entities; public IEnumerable ShadowEntries => _physics.ShadowObjects.AllEntriesForDebug(); } internal sealed class NearbyWorldDiagnosticDumper { private const float NearbyDistance = 15f; private readonly INearbyWorldDiagnosticSource _source; private readonly Action _log; public NearbyWorldDiagnosticDumper( INearbyWorldDiagnosticSource source, Action? log = null) { _source = source ?? throw new ArgumentNullException(nameof(source)); _log = log ?? (_ => { }); } public void Dump() { Vector3 position = _source.ObserverPosition; int centerX = _source.LiveCenterX; int centerY = _source.LiveCenterY; int landblockX = centerX + (int)MathF.Floor(position.X / 192f); int landblockY = centerY + (int)MathF.Floor(position.Y / 192f); _log( $"=== F3 DEBUG DUMP ===\n" + $" player pos=({position.X:F2},{position.Y:F2},{position.Z:F2})\n" + $" landblock=0x{(uint)((landblockX << 24) | (landblockY << 16) | 0xFFFF):X8} " + $"local=({position.X - (landblockX - centerX) * 192f:F2}," + $"{position.Y - (landblockY - centerY) * 192f:F2})\n" + $" total shadow objects: {_source.TotalShadowObjects}"); List visibleNearby = _source.WorldEntities .Where(entity => HorizontalDistanceSquared(entity.Position, position) < NearbyDistance * NearbyDistance) .OrderBy(entity => (entity.Position - position).Length()) .ToList(); _log($" VISIBLE entities within 15m: {visibleNearby.Count}"); foreach (WorldEntity entity in visibleNearby.Take(12)) { float distance = (entity.Position - position).Length(); _log( $" VIS id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} " + $"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) " + $"dist={distance:F2} scale={entity.Scale:F2}"); } List<(ShadowEntry Entry, float Distance)> shadows = _source.ShadowEntries .Select(entry => { float dx = entry.Position.X - position.X; float dy = entry.Position.Y - position.Y; return (Entry: entry, Distance: MathF.Sqrt(dx * dx + dy * dy)); }) .Where(item => item.Distance < NearbyDistance) .OrderBy(item => item.Distance) .ToList(); _log($" SHADOW objects within 15m: {shadows.Count}"); foreach ((ShadowEntry entry, float distance) in shadows.Take(12)) { _log( $" SHAD id=0x{entry.EntityId:X8} {entry.CollisionType} " + $"r={entry.Radius:F2} h={entry.CylHeight:F2} " + $"pos=({entry.Position.X:F2},{entry.Position.Y:F2},{entry.Position.Z:F2}) " + $"dist={distance:F2}"); } } private static float HorizontalDistanceSquared(Vector3 left, Vector3 right) { float dx = left.X - right.X; float dy = left.Y - right.Y; return dx * dx + dy * dy; } } /// /// Routes developer-only semantic actions while leaving every mutable value in /// its canonical owner. No diagnostic command owns a second clock, weather, /// camera-sensitivity, collision, entity, or shadow state. /// internal sealed class RuntimeDiagnosticCommandController : IRuntimeDiagnosticCommands { private readonly WorldEnvironmentController _environment; private readonly WorldSceneDebugState _sceneDebug; private readonly IPointerSensitivityCommands? _pointer; private readonly NearbyWorldDiagnosticDumper _nearby; private readonly Action? _toast; public RuntimeDiagnosticCommandController( WorldEnvironmentController environment, WorldSceneDebugState sceneDebug, IPointerSensitivityCommands? pointer, NearbyWorldDiagnosticDumper nearby, Action? toast = null) { _environment = environment ?? throw new ArgumentNullException(nameof(environment)); _sceneDebug = sceneDebug ?? throw new ArgumentNullException(nameof(sceneDebug)); _pointer = pointer; _nearby = nearby ?? throw new ArgumentNullException(nameof(nearby)); _toast = toast; } public bool Handle(InputAction action) { switch (action) { case InputAction.AcdreamToggleCollisionWires: ToggleCollisionWireframes(); return true; case InputAction.AcdreamDumpNearby: _nearby.Dump(); return true; case InputAction.AcdreamCycleTimeOfDay: CycleTimeOfDay(); return true; case InputAction.AcdreamSensitivityDown: if (_pointer is not null) { string message = _pointer.AdjustSensitivity(1f / 1.2f); _toast?.Invoke(message); } return true; case InputAction.AcdreamSensitivityUp: if (_pointer is not null) { string message = _pointer.AdjustSensitivity(1.2f); _toast?.Invoke(message); } return true; case InputAction.AcdreamCycleWeather: CycleWeather(); return true; default: return false; } } public void CycleTimeOfDay() { string message = _environment.CycleTimeOfDay(); _toast?.Invoke(message); } public void CycleWeather() { string message = _environment.CycleWeather(); _toast?.Invoke(message); } public void ToggleCollisionWireframes() { bool visible = _sceneDebug.ToggleCollisionWireframes(); _toast?.Invoke($"Collision wireframes {(visible ? "ON" : "OFF")}"); } internal static bool IsDiagnosticAction(InputAction action) => action is InputAction.AcdreamToggleCollisionWires or InputAction.AcdreamDumpNearby or InputAction.AcdreamCycleTimeOfDay or InputAction.AcdreamSensitivityDown or InputAction.AcdreamSensitivityUp or InputAction.AcdreamCycleWeather; }