diff --git a/Intersect (Core)/Compression/GzipCompression.cs b/Framework/Intersect.Framework.Core/Compression/GzipCompression.cs similarity index 92% rename from Intersect (Core)/Compression/GzipCompression.cs rename to Framework/Intersect.Framework.Core/Compression/GzipCompression.cs index ac0c79725c..12873cd46c 100644 --- a/Intersect (Core)/Compression/GzipCompression.cs +++ b/Framework/Intersect.Framework.Core/Compression/GzipCompression.cs @@ -23,7 +23,7 @@ private static void CreateProvider() // Take a few bytes out of this delicious morsel and grow stronk. var keyBytes = ASCIIEncoding.ASCII.GetBytes(cryptoKey); - cryptoProvider.Key = keyBytes.Take(16).ToArray(); + cryptoProvider.Key = keyBytes.Take(16).ToArray(); cryptoProvider.IV = keyBytes.Reverse().Take(16).ToArray(); } @@ -56,11 +56,11 @@ public static CryptoStream CreateDecompressedFileStream(string fileName) } /// - /// Read decompressed unencrypted data from an existing filestream. + /// Read decompressed unencrypted data from an existing . /// - /// The Filestream to write data from. + /// The to write data from. /// Returns a decompressed of the stream's content. - public static CryptoStream CreateDecompressedFileStream(FileStream stream) + public static CryptoStream CreateDecompressedFileStream(Stream stream) { if (cryptoProvider == null) { diff --git a/Framework/Intersect.Framework.Core/Config/OptionsStrings.Designer.cs b/Framework/Intersect.Framework.Core/Config/OptionsStrings.Designer.cs index 6d9382468b..af42dbc1ed 100644 --- a/Framework/Intersect.Framework.Core/Config/OptionsStrings.Designer.cs +++ b/Framework/Intersect.Framework.Core/Config/OptionsStrings.Designer.cs @@ -8,9 +8,6 @@ //------------------------------------------------------------------------------ namespace Intersect.Framework.Core.Config { - using System; - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] diff --git a/Framework/Intersect.Framework.Core/Core/IApplicationContext.cs b/Framework/Intersect.Framework.Core/Core/IApplicationContext.cs index b41d9f0abe..65db8d77c8 100644 --- a/Framework/Intersect.Framework.Core/Core/IApplicationContext.cs +++ b/Framework/Intersect.Framework.Core/Core/IApplicationContext.cs @@ -51,6 +51,11 @@ bool IsDebug /// bool IsRunning { get; } + /// + /// The name of the application. + /// + string Name { get; } + /// /// The options the application was started with. /// diff --git a/Intersect (Core)/Core/IApplicationContext`1.cs b/Framework/Intersect.Framework.Core/Core/IApplicationContext`1.cs similarity index 100% rename from Intersect (Core)/Core/IApplicationContext`1.cs rename to Framework/Intersect.Framework.Core/Core/IApplicationContext`1.cs diff --git a/Framework/Intersect.Framework.Core/GameObjects/DescriptorStrings.Designer.cs b/Framework/Intersect.Framework.Core/GameObjects/DescriptorStrings.Designer.cs index 6e74f2a7d5..f3c9b6810d 100644 --- a/Framework/Intersect.Framework.Core/GameObjects/DescriptorStrings.Designer.cs +++ b/Framework/Intersect.Framework.Core/GameObjects/DescriptorStrings.Designer.cs @@ -8,9 +8,6 @@ //------------------------------------------------------------------------------ namespace Intersect.Framework.Core.GameObjects { - using System; - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] diff --git a/Framework/Intersect.Framework.Core/Intersect.Framework.Core.csproj b/Framework/Intersect.Framework.Core/Intersect.Framework.Core.csproj index 7b31eb89fd..d55a61d3be 100644 --- a/Framework/Intersect.Framework.Core/Intersect.Framework.Core.csproj +++ b/Framework/Intersect.Framework.Core/Intersect.Framework.Core.csproj @@ -28,6 +28,7 @@ + diff --git a/Framework/Intersect.Framework.Core/Point.cs b/Framework/Intersect.Framework.Core/Point.cs index ea38e38113..e28bf45ea2 100644 --- a/Framework/Intersect.Framework.Core/Point.cs +++ b/Framework/Intersect.Framework.Core/Point.cs @@ -76,4 +76,9 @@ public static Point FromString(string val) public static Point operator /(Point point, float scalar) => new((int)(point.X / scalar), (int)(point.Y / scalar)); + public void Deconstruct(out int x, out int y) + { + x = X; + y = Y; + } } diff --git a/Intersect (Core)/Utilities/Timing.cs b/Framework/Intersect.Framework.Core/Timing.cs similarity index 98% rename from Intersect (Core)/Utilities/Timing.cs rename to Framework/Intersect.Framework.Core/Timing.cs index 0d6efa6f78..0f747950b4 100644 --- a/Intersect (Core)/Utilities/Timing.cs +++ b/Framework/Intersect.Framework.Core/Timing.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace Intersect.Utilities; +namespace Intersect.Framework.Core; /// /// Utility class for timing. diff --git a/Framework/Intersect.Framework/Collections/ConcurrentConditionalDequeue.cs b/Framework/Intersect.Framework/Collections/ConcurrentConditionalDequeue.cs new file mode 100644 index 0000000000..6e977524f2 --- /dev/null +++ b/Framework/Intersect.Framework/Collections/ConcurrentConditionalDequeue.cs @@ -0,0 +1,127 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Intersect.Framework.Collections; + +public class ConcurrentConditionalDequeue : IReadOnlyCollection, IProducerConsumerCollection +{ + private static readonly Func IsEqual = + typeof(TValue).IsValueType ? StructEquals : (a, b) => ReferenceEquals(a, b); + + private readonly object _dequeueLock = new(); + private readonly ConcurrentQueue _queue = []; + + private ICollection QueueAsGenericCollection => _queue; + + private IProducerConsumerCollection QueueAsProducerConsumerCollection => _queue; + + public bool IsReadOnly => false; + + public void CopyTo(TValue[] array, int arrayIndex) + { + _queue.CopyTo(array, arrayIndex); + } + + public TValue[] ToArray() + { + return _queue.ToArray(); + } + + bool IProducerConsumerCollection.TryAdd(TValue item) + { + return QueueAsProducerConsumerCollection.TryAdd(item); + } + + bool IProducerConsumerCollection.TryTake([MaybeNullWhen(false)] out TValue item) + { + lock (_dequeueLock) + { + return QueueAsProducerConsumerCollection.TryTake(out item); + } + } + + void ICollection.CopyTo(Array array, int index) + { + QueueAsGenericCollection.CopyTo(array, index); + } + + bool ICollection.IsSynchronized => QueueAsProducerConsumerCollection.IsSynchronized; + + object ICollection.SyncRoot => QueueAsProducerConsumerCollection.SyncRoot; + + public int Count => _queue.Count; + + public IEnumerator GetEnumerator() + { + return _queue.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Clear() + { + lock (_dequeueLock) + { + _queue.Clear(); + } + } + + public void Enqueue(TValue item) + { + _queue.Enqueue(item); + } + + public bool TryDequeueIf(Func consumer) + { + return TryDequeueIf(consumer, out _); + } + + public bool TryDequeueIf(Func consumer, [NotNullWhen(true)] out TValue? value) + { + ArgumentNullException.ThrowIfNull(consumer); + + lock (_dequeueLock) + { + if (!_queue.TryPeek(out value)) + { + return false; + } + + if (value == null) + { + return false; + } + + if (!consumer(value)) + { + return false; + } + + if (!_queue.TryDequeue(out var dequeuedValue)) + { + throw new InvalidOperationException("Dequeue failed, collection may have been removed from or cleared without being locked."); + } + + if (!IsEqual(value, dequeuedValue)) + { + throw new InvalidOperationException("Unexpected dequeued value, collection may have been removed from or cleared without being locked."); + } + + return true; + } + } + + public bool TryPeek([NotNullWhen(true)] out TValue? value) + { + lock (_dequeueLock) + { + return _queue.TryPeek(out value); + } + } + + private static bool StructEquals(TValue a, TValue b) => a!.Equals(b); +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Collections/ListExtensions.cs b/Framework/Intersect.Framework/Collections/ListExtensions.cs new file mode 100644 index 0000000000..305e45652c --- /dev/null +++ b/Framework/Intersect.Framework/Collections/ListExtensions.cs @@ -0,0 +1,109 @@ +namespace Intersect.Framework.Collections; + +public static class ListExtensions +{ + public static void AddSorted(this List @this, T item) where T : IComparable + { + if (@this.Count == 0) + { + @this.Add(item); + return; + } + + if (@this[^1].CompareTo(item) <= 0) + { + @this.Add(item); + return; + } + + if (@this[0].CompareTo(item) >= 0) + { + @this.Insert(0, item); + return; + } + + int index = @this.BinarySearch(item); + if (index < 0) + { + index = ~index; + } + @this.Insert(index, item); + } + + public static void Resort(this List @this, T item) where T : IComparable + { + if (@this.Count < 1) + { + @this.AddSorted(item); + return; + } + + @this.Remove(item); + @this.AddSorted(item); + } + + private sealed class KeyComparer(Func keySelector) + : IComparer where TKey : IComparable + { + public int Compare(TItem? x, TItem? y) + { + var keyX = keySelector(x); + var keyY = keySelector(y); + + if (keyX is not null) + { + return keyX.CompareTo(keyY); + } + + if (keyY is null) + { + return 0; + } + + return -keyY.CompareTo(keyX); + } + } + + public static void AddSorted(this List @this, TItem item, Func keySelector) where TKey : IComparable + { + if (@this.Count == 0) + { + @this.Add(item); + return; + } + + KeyComparer comparer = new(keySelector); + + if (comparer.Compare(@this[^1], item) <= 0) + { + @this.Add(item); + return; + } + + if (comparer.Compare(@this[0], item) >= 0) + { + @this.Insert(0, item); + return; + } + + int index = @this.BinarySearch(item, comparer); + if (index < 0) + { + index = ~index; + } + @this.Insert(index, item); + } + + public static void Resort(this List @this, TItem item, Func keySelector) + where TKey : IComparable + { + if (@this.Count < 1) + { + @this.AddSorted(item, keySelector); + return; + } + + @this.Remove(item); + @this.AddSorted(item, keySelector); + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Eventing/EventHandler`2.cs b/Framework/Intersect.Framework/Eventing/EventHandler`2.cs new file mode 100644 index 0000000000..24c6fd9722 --- /dev/null +++ b/Framework/Intersect.Framework/Eventing/EventHandler`2.cs @@ -0,0 +1,3 @@ +namespace Intersect.Framework.Eventing; + +public delegate void EventHandler(TSender sender, TArgs args) where TArgs : EventArgs; \ No newline at end of file diff --git a/Framework/Intersect.Framework/Intersect.Framework.csproj b/Framework/Intersect.Framework/Intersect.Framework.csproj index 74a0400d74..d34c4bcc40 100644 --- a/Framework/Intersect.Framework/Intersect.Framework.csproj +++ b/Framework/Intersect.Framework/Intersect.Framework.csproj @@ -1,6 +1,7 @@ + @@ -32,4 +33,8 @@ + + + + diff --git a/Framework/Intersect.Framework/Reflection/ReflectionStrings.Designer.cs b/Framework/Intersect.Framework/Reflection/ReflectionStrings.Designer.cs index 616e26defa..9c78d48b75 100755 --- a/Framework/Intersect.Framework/Reflection/ReflectionStrings.Designer.cs +++ b/Framework/Intersect.Framework/Reflection/ReflectionStrings.Designer.cs @@ -8,9 +8,6 @@ //------------------------------------------------------------------------------ namespace Intersect.Framework.Reflection { - using System; - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] diff --git a/Framework/Intersect.Framework/SystemInformation/IGPUStatisticsProvider.cs b/Framework/Intersect.Framework/SystemInformation/IGPUStatisticsProvider.cs new file mode 100644 index 0000000000..ac74fbd920 --- /dev/null +++ b/Framework/Intersect.Framework/SystemInformation/IGPUStatisticsProvider.cs @@ -0,0 +1,8 @@ +namespace Intersect.Framework.SystemInformation; + +public interface IGPUStatisticsProvider +{ + long? AvailableMemory { get; } + + long? TotalMemory { get; } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/SystemInformation/PlatformStatistics.cs b/Framework/Intersect.Framework/SystemInformation/PlatformStatistics.cs new file mode 100644 index 0000000000..42830b434a --- /dev/null +++ b/Framework/Intersect.Framework/SystemInformation/PlatformStatistics.cs @@ -0,0 +1,44 @@ +using Hardware.Info; +using Microsoft.Extensions.Logging; + +namespace Intersect.Framework.SystemInformation; + +public class PlatformStatistics +{ + private static readonly HardwareInfo HardwareInfo; + + public static IGPUStatisticsProvider? GPUStatisticsProvider { get; set; } + + public static ILogger? Logger { get; set; } + + public static long AvailablePhysicalMemory => (long)HardwareInfo.MemoryStatus.AvailablePhysical; + + public static long TotalPhysicalMemory => (long)HardwareInfo.MemoryStatus.TotalPhysical; + + public static long AvailableGPUMemory => GPUStatisticsProvider?.AvailableMemory ?? AvailableSystemMemory; + + public static long TotalGPUMemory => GPUStatisticsProvider?.TotalMemory ?? TotalSystemMemory; + + public static long AvailableSystemMemory => (long)HardwareInfo.MemoryStatus.AvailableVirtual; + + public static long TotalSystemMemory => (long)HardwareInfo.MemoryStatus.TotalVirtual; + + public static void Refresh() + { + try + { + HardwareInfo.RefreshMemoryStatus(); + } + catch + { + // Do nothing + } + } + + static PlatformStatistics() + { + HardwareInfo = new HardwareInfo(); + + Refresh(); + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ActionQueue.Return`1.cs b/Framework/Intersect.Framework/Threading/ActionQueue.Return`1.cs new file mode 100644 index 0000000000..4bf5d38346 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ActionQueue.Return`1.cs @@ -0,0 +1,40 @@ +namespace Intersect.Framework.Threading; + +partial class ActionQueue +{ + public TReturn EnqueueReturn(Func func, TState state) + { + if (IsActive) + { + return func(state); + } + + Return @return = new(func); + Enqueue( + Return.Wrapper, + new State>( + Return.FuncWrapper, + state, + @return + ) + ); + return @return.Value; + } + + private sealed record Return(Func Func) + { + public static readonly Action>> Wrapper = state => + state.Action( + state.State0, + state.State1 + ); + + public static readonly Action> FuncWrapper = + (state, returnValue) => + { + returnValue.Value = returnValue.Func(state); + }; + + public TReturn Value { get; private set; } = default!; + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ActionQueue.Return`2.cs b/Framework/Intersect.Framework/Threading/ActionQueue.Return`2.cs new file mode 100644 index 0000000000..7d8b73d0e3 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ActionQueue.Return`2.cs @@ -0,0 +1,43 @@ +namespace Intersect.Framework.Threading; + +partial class ActionQueue +{ + public TReturn EnqueueReturn( + Func func, + TState0 state0, + TState1 state1 + ) + { + if (IsActive) + { + return func(state0, state1); + } + + Return @return = new(func); + Enqueue( + Return.Wrapper, + new State>( + Return.FuncWrapper, + state0, + state1, + @return + ) + ); + return @return.Value; + } + + private sealed record Return(Func Func) + { + public static readonly Action>> + Wrapper = + state => state.Action(state.State0, state.State1, state.State2); + + public static readonly Action> FuncWrapper = + (state0, state1, returnValue) => + { + returnValue.Value = returnValue.Func(state0, state1); + }; + + public TReturn Value { get; private set; } = default!; + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ActionQueue.State`1.cs b/Framework/Intersect.Framework/Threading/ActionQueue.State`1.cs new file mode 100644 index 0000000000..ca36c30490 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ActionQueue.State`1.cs @@ -0,0 +1,30 @@ +namespace Intersect.Framework.Threading; + +public abstract partial class ActionQueue +{ + private record struct State + { + public static readonly Queue ActionQueue = []; + + // ReSharper disable once StaticMemberInGenericType + public static readonly Action Next = InternalNext; + + private static void InternalNext() + { + if (!ActionQueue.TryDequeue(out var deferredAction)) + { + throw new InvalidOperationException("Action queue is not being properly synchronized"); + } + + deferredAction.Action(deferredAction.ActionState); + deferredAction.PostInvocationAction(deferredAction.EnqueueState); + } + + public record struct DeferredAction( + Action Action, + TState ActionState, + Action PostInvocationAction, + TEnqueueState EnqueueState + ); + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ActionQueue.State`2.cs b/Framework/Intersect.Framework/Threading/ActionQueue.State`2.cs new file mode 100644 index 0000000000..4a7994b0f1 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ActionQueue.State`2.cs @@ -0,0 +1,26 @@ +namespace Intersect.Framework.Threading; + +partial class ActionQueue +{ + public void Enqueue(Action action, TState0 state0, TState1 state1) + { + if (IsActive) + { + action(state0, state1); + return; + } + + Enqueue( + State.Wrapper, + new State(action, state0, state1) + ); + } + + private record struct State(Action Action, TState0 State0, TState1 State1) + { + public static readonly Action> Wrapper = state => state.Action( + state.State0, + state.State1 + ); + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ActionQueue.State`3.cs b/Framework/Intersect.Framework/Threading/ActionQueue.State`3.cs new file mode 100644 index 0000000000..39a098b7b3 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ActionQueue.State`3.cs @@ -0,0 +1,37 @@ +namespace Intersect.Framework.Threading; + +partial class ActionQueue +{ + public void Enqueue( + Action action, + TState0 state0, + TState1 state1, + TState2 state2 + ) + { + if (IsActive) + { + action(state0, state1, state2); + return; + } + + Enqueue( + State.Wrapper, + new State(action, state0, state1, state2) + ); + } + + private record struct State( + Action Action, + TState0 State0, + TState1 State1, + TState2 State2 + ) + { + public static readonly Action> Wrapper = state => state.Action( + state.State0, + state.State1, + state.State2 + ); + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ActionQueue.cs b/Framework/Intersect.Framework/Threading/ActionQueue.cs new file mode 100644 index 0000000000..7228df8a17 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ActionQueue.cs @@ -0,0 +1,116 @@ +namespace Intersect.Framework.Threading; + +public abstract partial class ActionQueue where TActionQueue : ActionQueue +{ + private readonly TActionQueue @this; + private readonly Queue _actionQueue = []; + private readonly Action _statelessAction = action => action(); + private readonly Action? _beginInvokePending; + private readonly Action? _endInvokePending; + + private bool _empty; + + protected ActionQueue(Action? beginInvokePending, Action? endInvokePending) + { + @this = this as TActionQueue ?? throw new InvalidCastException(); + _beginInvokePending = beginInvokePending; + _endInvokePending = endInvokePending; + } + + public event Action? QueueNotEmpty; + + protected abstract bool IsActive { get; } + + protected abstract Action PostInvocationAction { get; } + + /// + /// + /// + /// Returns true if the queue was non-empty and is now empty. + public bool InvokePending() + { + if (_empty) + { + return false; + } + + _beginInvokePending?.Invoke(@this); + + _empty = true; + + lock (_actionQueue) + { + while (_actionQueue.TryDequeue(out var deferredAction)) + { + deferredAction(); + } + } + + _endInvokePending?.Invoke(@this); + return _actionQueue.Count < 1; + } + + public void Enqueue(Action action) + { + if (IsActive) + { + action(); + return; + } + + Enqueue(_statelessAction, action); + } + + public void Enqueue(Action action, TState state) + { + ArgumentNullException.ThrowIfNull(action, nameof(action)); + + if (IsActive) + { + action(state); + return; + } + + var enqueueState = EnqueueCreateState(); + + try + { + State.DeferredAction deferredAction = new( + action, + state, + PostInvocationAction, + enqueueState + ); + + lock (_actionQueue) + { + State.ActionQueue.Enqueue(deferredAction); + _actionQueue.Enqueue(State.Next); + try + { + if (_empty) + { + QueueNotEmpty?.Invoke(); + } + } + catch + { + // Ignore, application context not available here + } + _empty = false; + } + + EnqueueSuccessful(enqueueState); + } + finally + { + EnqueueFinally(enqueueState); + } + } + + protected abstract TEnqueueState EnqueueCreateState(); + + protected abstract void EnqueueSuccessful(TEnqueueState enqueueState); + + protected abstract void EnqueueFinally(TEnqueueState enqueueState); +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ManualActionQueue.cs b/Framework/Intersect.Framework/Threading/ManualActionQueue.cs new file mode 100644 index 0000000000..e24cd3ac88 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ManualActionQueue.cs @@ -0,0 +1,69 @@ +namespace Intersect.Framework.Threading; + +public interface IManualActionQueueParent +{ + bool IsExecuting { get; } +} + +public sealed class ManualActionQueueParent : IManualActionQueueParent +{ + public bool IsExecuting { get; set; } +} + +public sealed class ManualActionQueue : ActionQueue +{ + private readonly object _lock = new(); + + private bool _active; + private readonly IManualActionQueueParent? _parent; + + public ManualActionQueue(IManualActionQueueParent? parent = null) : base( + beginInvokePending: BeginInvokePending, + endInvokePending: EndInvokePending + ) + { + _parent = parent; + } + + // Can't actually convert it because we can't add a private setter here + // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter + protected override bool IsActive => _active; + + protected override Action PostInvocationAction => static _ => { }; + + private static void BeginInvokePending(ManualActionQueue @this) + { + if (@this._parent is { IsExecuting: false } || !Monitor.TryEnter(@this._lock)) + { + throw new InvalidOperationException("Tried to invoke pending actions from an invalid source"); + } + + @this._active = true; + } + + private static void EndInvokePending(ManualActionQueue @this) + { + @this._active = false; + + Monitor.Exit(@this._lock); + } + + protected override bool EnqueueCreateState() + { + var lockTaken = false; + Monitor.Enter(_lock, ref lockTaken); + return lockTaken; + } + + protected override void EnqueueSuccessful(bool lockTaken) + { + } + + protected override void EnqueueFinally(bool lockTaken) + { + if (lockTaken) + { + Monitor.Exit(_lock); + } + } +} \ No newline at end of file diff --git a/Framework/Intersect.Framework/Threading/ThreadQueue.cs b/Framework/Intersect.Framework/Threading/ThreadQueue.cs new file mode 100644 index 0000000000..6daf146119 --- /dev/null +++ b/Framework/Intersect.Framework/Threading/ThreadQueue.cs @@ -0,0 +1,123 @@ +using System.Runtime.CompilerServices; + +namespace Intersect.Framework.Threading; + +public sealed partial class ThreadQueue : ActionQueue +{ + public static readonly ThreadQueue Default = new(); + + private readonly object _lock = new(); + private readonly Stack _resetEventPool = []; + private readonly int? _spinCount; + + private int _mainThreadId; + + public ThreadQueue(int? spinCount = null) : base(beginInvokePending: BeginInvokePending, endInvokePending: null) + { + _spinCount = spinCount; + + SetMainThreadId(); + } + + public ThreadQueue(ThreadQueue parent) : base(beginInvokePending: BeginInvokePending, endInvokePending: null) + { + _spinCount = parent._spinCount; + _mainThreadId = parent._mainThreadId; + } + + protected override bool IsActive => IsOnMainThread; + + public bool IsOnMainThread + { + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _mainThreadId == Environment.CurrentManagedThreadId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void BeginInvokePending(ThreadQueue @this) => @this.ThrowIfNotOnMainThread(); + + protected override ManualResetEventSlim EnqueueCreateState() => ResetEventPoolPop(); + + protected override void EnqueueSuccessful(ManualResetEventSlim resetEvent) => resetEvent.Wait(); + + protected override void EnqueueFinally(ManualResetEventSlim resetEvent) => ResetEventPoolPush(resetEvent); + + protected override Action PostInvocationAction { get; } = static resetEvent => resetEvent.Set(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RunOnMainThread(Action action) => Enqueue(action); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RunOnMainThread(Action action, TState state) => Enqueue(action, state); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TReturn RunOnMainThread(Func func, TState state) => EnqueueReturn(func, state); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RunOnMainThread(Action action, TState0 state0, TState1 state1) => + Enqueue(action, state0, state1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TReturn RunOnMainThread(Func func, TState0 state0, TState1 state1) => + EnqueueReturn(func, state0, state1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RunOnMainThread( + Action action, + TState0 state0, + TState1 state1, + TState2 state2 + ) => Enqueue(action, state0, state1, state2); + + private ManualResetEventSlim ResetEventPoolPop() + { + lock (_resetEventPool) + { + if (_resetEventPool.TryPop(out var resetEvent)) + { + return resetEvent; + } + } + + return _spinCount.HasValue + ? new ManualResetEventSlim(false, _spinCount.Value) + : new ManualResetEventSlim(); + } + + private void ResetEventPoolPush(ManualResetEventSlim resetEvent) + { + resetEvent.Reset(); + lock (_resetEventPool) + { + _resetEventPool.Push(resetEvent); + } + } + + public void SetMainThreadId(int? mainThreadId = null) + { + lock (_lock) + { + _mainThreadId = mainThreadId ?? Environment.CurrentManagedThreadId; + } + } + + public void SetMainThreadId(ThreadQueue other) + { + lock (_lock) + { + _mainThreadId = other._mainThreadId; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ThrowIfNotOnMainThread() + { + if (IsOnMainThread) + { + return; + } + + throw new InvalidOperationException("Operation was not called on the main thread."); + } +} \ No newline at end of file diff --git a/Intersect (Core)/Core/ApplicationContext`2.cs b/Intersect (Core)/Core/ApplicationContext`2.cs index 6ed277bd34..6e4fe792dc 100644 --- a/Intersect (Core)/Core/ApplicationContext`2.cs +++ b/Intersect (Core)/Core/ApplicationContext`2.cs @@ -22,31 +22,36 @@ public abstract partial class ApplicationContext : IA { #region Lifecycle Fields - private bool mIsRunning; + private bool _running; - private bool mNeedsLockPulse; + private bool _needsLockPulse; - private readonly object mDisposeLock; + private readonly object _disposeLock = new(); - private readonly object mShutdownLock; + private readonly object _shutdownLock = new(); #endregion Lifecycle Fields /// /// Initializes general pieces of the . /// + /// /// the the application was started with /// the application-level /// - protected ApplicationContext(TStartupOptions startupOptions, ILogger logger, IPacketHelper packetHelper) + /// + protected ApplicationContext( + Assembly entryAssembly, + string fallbackName, + TStartupOptions startupOptions, + ILogger logger, + IPacketHelper packetHelper + ) { - mDisposeLock = new object(); - mShutdownLock = new object(); + Name = entryAssembly.GetName().Name ?? fallbackName; ApplicationContext.Context.Value = this; - mServices = new ConcurrentDictionary(); - StartupOptions = startupOptions; Logger = logger; PacketHelper = packetHelper; @@ -54,6 +59,8 @@ protected ApplicationContext(TStartupOptions startupOptions, ILogger logger, IPa ConcurrentInstance.Set(This); } + public string Name { get; } + ICommandLineOptions IApplicationContext.StartupOptions => StartupOptions; /// @@ -91,15 +98,15 @@ protected ApplicationContext(TStartupOptions startupOptions, ILogger logger, IPa /// public bool IsRunning { - get => mIsRunning && !IsShutdownRequested; - private set => mIsRunning = value; + get => _running && !IsShutdownRequested; + private set => _running = value; } #endregion Lifecycle Properties #region Services - private readonly IDictionary mServices; + private readonly IDictionary mServices = new ConcurrentDictionary(); /// public List Services => mServices.Values.ToList(); @@ -257,16 +264,16 @@ public void Start(bool lockUntilShutdown = true) #region Wait for application thread - mNeedsLockPulse = lockUntilShutdown; + _needsLockPulse = lockUntilShutdown; - if (!mNeedsLockPulse) + if (!_needsLockPulse) { return; } - lock (mShutdownLock) + lock (_shutdownLock) { - Monitor.Wait(mShutdownLock); + Monitor.Wait(_shutdownLock); ApplicationContext.Context.Value?.Logger.LogTrace(DeveloperStrings.ApplicationContextExited); } @@ -284,9 +291,9 @@ public ILockingActionQueue StartWithActionQueue() { Start(false); - mNeedsLockPulse = true; + _needsLockPulse = true; - return new LockingActionQueue(mShutdownLock); + return new LockingActionQueue(_shutdownLock); } /// @@ -302,7 +309,7 @@ public void RequestShutdown(bool join = false) { Task disposeTask; - lock (mDisposeLock) + lock (_disposeLock) { if (IsDisposed || IsDisposing || IsShutdownRequested) { @@ -315,9 +322,9 @@ public void RequestShutdown(bool join = false) { Dispose(); - lock (mShutdownLock) + lock (_shutdownLock) { - Monitor.PulseAll(mShutdownLock); + Monitor.PulseAll(_shutdownLock); } } ); @@ -513,7 +520,7 @@ public void Dispose() throw new ObjectDisposedException(typeof(TContext).Name); } - lock (mDisposeLock) + lock (_disposeLock) { if (IsDisposing) { diff --git a/Intersect (Core)/Intersect.Core.csproj b/Intersect (Core)/Intersect.Core.csproj index 011e14d326..16fe42810d 100644 --- a/Intersect (Core)/Intersect.Core.csproj +++ b/Intersect (Core)/Intersect.Core.csproj @@ -68,7 +68,6 @@ - diff --git a/Intersect (Core)/Network/AbstractNetwork.cs b/Intersect (Core)/Network/AbstractNetwork.cs index fd24f79cf6..d25a5a1626 100644 --- a/Intersect (Core)/Network/AbstractNetwork.cs +++ b/Intersect (Core)/Network/AbstractNetwork.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Net; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Framework.Reflection; using Intersect.Memory; using Intersect.Plugins.Interfaces; diff --git a/Intersect (Core)/Network/ConnectionPacket.cs b/Intersect (Core)/Network/ConnectionPacket.cs index 2b9e074a87..d988c87c0e 100644 --- a/Intersect (Core)/Network/ConnectionPacket.cs +++ b/Intersect (Core)/Network/ConnectionPacket.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using Intersect.Framework.Core; using Intersect.Network.Packets; using Intersect.Utilities; using MessagePack; diff --git a/Intersect (Core)/Network/Packets/AbstractTimedPacket.cs b/Intersect (Core)/Network/Packets/AbstractTimedPacket.cs index 146fa6ccb6..4172977068 100644 --- a/Intersect (Core)/Network/Packets/AbstractTimedPacket.cs +++ b/Intersect (Core)/Network/Packets/AbstractTimedPacket.cs @@ -1,4 +1,5 @@ -using Intersect.Network.Packets.Client; +using Intersect.Framework.Core; +using Intersect.Network.Packets.Client; using Intersect.Network.Packets.Server; using Intersect.Utilities; using MessagePack; diff --git a/Intersect.Client.Core/Core/Audio.cs b/Intersect.Client.Core/Core/Audio.cs index 9b9ca0720a..345c0b2107 100644 --- a/Intersect.Client.Core/Core/Audio.cs +++ b/Intersect.Client.Core/Core/Audio.cs @@ -5,7 +5,7 @@ using Intersect.Client.Framework.File_Management; using Intersect.Client.General; using Intersect.Core; -using Intersect.Utilities; +using Intersect.Framework.Core; using Microsoft.Extensions.Logging; namespace Intersect.Client.Core; diff --git a/Intersect.Client.Core/Core/Bootstrapper.cs b/Intersect.Client.Core/Core/Bootstrapper.cs index 1031d9a7d0..734a500fc7 100644 --- a/Intersect.Client.Core/Core/Bootstrapper.cs +++ b/Intersect.Client.Core/Core/Bootstrapper.cs @@ -5,6 +5,7 @@ using Intersect.Core; using Intersect.Factories; using Intersect.Framework.Logging; +using Intersect.Framework.SystemInformation; using Intersect.Network; using Intersect.Plugins; using Intersect.Plugins.Contexts; @@ -19,7 +20,7 @@ namespace Intersect.Client.Core; internal static partial class Bootstrapper { - public static void Start(params string[] args) + public static void Start(Assembly entryAssembly, params string[] args) { var parser = new Parser( parserSettings => @@ -43,13 +44,14 @@ public static void Start(params string[] args) LoggingLevelSwitch loggingLevelSwitch = new(Debugger.IsAttached ? LogEventLevel.Debug : LogEventLevel.Information); - var executingAssembly = Assembly.GetExecutingAssembly(); - var (_, logger) = new LoggerConfiguration().CreateLoggerForIntersect( - executingAssembly, + var (loggerFactory, logger) = new LoggerConfiguration().CreateLoggerForIntersect( + entryAssembly, "Client", loggingLevelSwitch ); + PlatformStatistics.Logger = loggerFactory.CreateLogger(); + var packetTypeRegistry = new PacketTypeRegistry(logger, typeof(SharedConstants).Assembly); if (!packetTypeRegistry.TryRegisterBuiltIn()) { @@ -71,7 +73,11 @@ public static void Start(params string[] args) } else { - ApplicationContext.Context.Value?.Logger.LogWarning($"Failed to set working directory to '{workingDirectory}', path does not exist: {resolvedWorkingDirectory}"); + ApplicationContext.Context.Value?.Logger.LogWarning( + "Failed to set working directory to '{Path}', path does not exist: {ResolvedPath}", + workingDirectory, + resolvedWorkingDirectory + ); } } @@ -99,7 +105,7 @@ public static void Start(params string[] args) Server = $"{clientConfiguration.Host}:{clientConfiguration.Port}", }; - ClientContext context = new(commandLineOptions, clientConfiguration, logger, packetHelper); + ClientContext context = new(entryAssembly, commandLineOptions, clientConfiguration, logger, packetHelper); context.Start(); } @@ -108,9 +114,9 @@ private static ClientCommandLineOptions HandleParsedArguments(ClientCommandLineO private static ClientCommandLineOptions HandleParserErrors(IEnumerable errors) { - var errorsAsList = errors?.ToList(); - var fatalParsingError = errorsAsList?.Any(error => error?.StopsProcessing ?? false) ?? false; - var errorString = string.Join(", ", errorsAsList?.ToList().Select(error => error?.ToString()) ?? []); + var errorsAsList = errors.ToList(); + var fatalParsingError = errorsAsList.Any(error => error.StopsProcessing); + var errorString = string.Join(", ", errorsAsList.ToList().Select(error => error.ToString())); var exception = new ArgumentException( $@"Error parsing command line arguments, received the following errors: {errorString}" diff --git a/Intersect.Client.Core/Core/ClientContext.cs b/Intersect.Client.Core/Core/ClientContext.cs index 2b783c9a61..fc55c23599 100644 --- a/Intersect.Client.Core/Core/ClientContext.cs +++ b/Intersect.Client.Core/Core/ClientContext.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Sockets; +using System.Reflection; using Intersect.Client.Networking; using Intersect.Client.Plugins.Contexts; using Intersect.Configuration; @@ -23,9 +24,13 @@ internal sealed partial class ClientContext : ApplicationContext[,]? RenderingEntities; private static GameContentManager sContentManager = null!; - private static GameRenderTexture? sDarknessTexture; + private static IGameRenderTexture? sDarknessTexture; private static readonly List sLightQueue = []; @@ -531,36 +532,38 @@ public static void DrawInGame(TimeSpan deltaTime) //Game Rendering public static void Render(TimeSpan deltaTime, TimeSpan totalTime) { - var takingScreenshot = false; - if (Renderer?.ScreenshotRequests.Count > 0) + if (Renderer is not { } renderer) { - takingScreenshot = Renderer.BeginScreenshot(); + return; } - if (Renderer == default) + var takingScreenshot = false; + if (renderer is { HasScreenshotRequests: true }) { - return; + takingScreenshot = renderer.BeginScreenshot(); } - Renderer.Scale = Globals.GameState == GameStates.InGame ? Globals.Database.WorldZoom : 1.0f; + var gameState = Globals.GameState; - if (!Renderer.Begin()) + renderer.Scale = gameState == GameStates.InGame ? Globals.Database.WorldZoom : 1.0f; + + if (!renderer.Begin()) { return; } - if (Renderer.GetScreenWidth() != sOldWidth || - Renderer.GetScreenHeight() != sOldHeight || - Renderer.DisplayModeChanged()) + if (renderer.ScreenWidth != sOldWidth || + renderer.ScreenHeight != sOldHeight || + renderer.DisplayModeChanged()) { sDarknessTexture = null; Interface.Interface.DestroyGwen(); Interface.Interface.InitGwen(); - sOldWidth = Renderer.GetScreenWidth(); - sOldHeight = Renderer.GetScreenHeight(); + sOldWidth = renderer.ScreenWidth; + sOldHeight = renderer.ScreenHeight; } - Renderer.Clear(Color.Black); + renderer.Clear(Color.Black); DrawCalls = 0; MapsDrawn = 0; EntitiesDrawn = 0; @@ -568,51 +571,67 @@ public static void Render(TimeSpan deltaTime, TimeSpan totalTime) UpdateView(); - switch (Globals.GameState) + switch (gameState) { case GameStates.Intro: DrawIntro(); - break; + case GameStates.Menu: DrawMenu(); - break; + case GameStates.Loading: break; + case GameStates.InGame: DrawInGame(deltaTime); - break; + case GameStates.Error: break; + default: - throw new ArgumentOutOfRangeException(); + throw Exceptions.UnreachableInvalidEnum(gameState); } - Renderer.Scale = Globals.Database.UIScale; + renderer.Scale = Globals.Database.UIScale; Interface.Interface.DrawGui(deltaTime, totalTime); DrawGameTexture( - Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), CurrentView, - new Color((int)Fade.Alpha, 0, 0, 0), null, GameBlendModes.None + tex: renderer.WhitePixel, + srcRectangle: new FloatRect(0, 0, 1, 1), + targetRect: CurrentView, + renderColor: new Color((int)Fade.Alpha, 0, 0, 0), + renderTarget: null, + blendMode: GameBlendModes.None ); // Draw our mousecursor at the very end, but not when taking screenshots. if (!takingScreenshot && !string.IsNullOrWhiteSpace(ClientConfiguration.Instance.MouseCursor)) { - var renderLoc = ConvertToWorldPointNoZoom(Globals.InputManager.GetMousePosition()); - DrawGameTexture( - Globals.ContentManager.GetTexture(Framework.Content.TextureType.Misc, ClientConfiguration.Instance.MouseCursor), renderLoc.X, renderLoc.Y - ); + var cursorTexture = Globals.ContentManager.GetTexture( + TextureType.Misc, + ClientConfiguration.Instance.MouseCursor + ); + + if (cursorTexture is not null) + { + var cursorPosition = ConvertToWorldPointNoZoom(Globals.InputManager.GetMousePosition()); + DrawGameTexture( + cursorTexture, + cursorPosition.X, + cursorPosition.Y + ); + } } - Renderer.End(); + renderer.End(); if (takingScreenshot) { - Renderer.EndScreenshot(); + renderer.EndScreenshot(); } } @@ -774,40 +793,40 @@ public static void DrawOverlay() } } - DrawGameTexture(Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), CurrentView, OverlayColor, null); + DrawGameTexture(Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), CurrentView, OverlayColor, null); sOverlayUpdate = Timing.Global.MillisecondsUtc; } - public static FloatRect GetSourceRect(GameTexture gameTexture) + public static FloatRect GetSourceRect(IGameTexture gameTexture) { return gameTexture == null ? new FloatRect() : new FloatRect(0, 0, gameTexture.Width, gameTexture.Height); } - public static void DrawFullScreenTexture(GameTexture tex, float alpha = 1f) + public static void DrawFullScreenTexture(IGameTexture tex, float alpha = 1f) { if (Renderer == default) { return; } - var bgx = Renderer.GetScreenWidth() / 2 - tex.Width / 2; - var bgy = Renderer.GetScreenHeight() / 2 - tex.Height / 2; + var bgx = Renderer.ScreenWidth / 2 - tex.Width / 2; + var bgy = Renderer.ScreenHeight / 2 - tex.Height / 2; var bgw = tex.Width; var bgh = tex.Height; int diff; - if (bgw < Renderer.GetScreenWidth()) + if (bgw < Renderer.ScreenWidth) { - diff = Renderer.GetScreenWidth() - bgw; + diff = Renderer.ScreenWidth - bgw; bgx -= diff / 2; bgw += diff; } - if (bgh < Renderer.GetScreenHeight()) + if (bgh < Renderer.ScreenHeight) { - diff = Renderer.GetScreenHeight() - bgh; + diff = Renderer.ScreenHeight - bgh; bgy -= diff / 2; bgh += diff; } @@ -819,15 +838,15 @@ public static void DrawFullScreenTexture(GameTexture tex, float alpha = 1f) ); } - public static void DrawFullScreenTextureCentered(GameTexture tex, float alpha = 1f) + public static void DrawFullScreenTextureCentered(IGameTexture tex, float alpha = 1f) { if (Renderer == default) { return; } - var bgx = Renderer.GetScreenWidth() / 2 - tex.Width / 2; - var bgy = Renderer.GetScreenHeight() / 2 - tex.Height / 2; + var bgx = Renderer.ScreenWidth / 2 - tex.Width / 2; + var bgy = Renderer.ScreenHeight / 2 - tex.Height / 2; var bgw = tex.Width; var bgh = tex.Height; @@ -838,7 +857,7 @@ public static void DrawFullScreenTextureCentered(GameTexture tex, float alpha = ); } - public static void DrawFullScreenTextureStretched(GameTexture tex) + public static void DrawFullScreenTextureStretched(IGameTexture tex) { if (Renderer == default) { @@ -848,55 +867,55 @@ public static void DrawFullScreenTextureStretched(GameTexture tex) DrawGameTexture( tex, GetSourceRect(tex), new FloatRect( - Renderer.GetView().X, Renderer.GetView().Y, Renderer.GetScreenWidth(), Renderer.GetScreenHeight() + Renderer.GetView().X, Renderer.GetView().Y, Renderer.ScreenWidth, Renderer.ScreenHeight ), Color.White ); } - public static void DrawFullScreenTextureFitWidth(GameTexture tex) + public static void DrawFullScreenTextureFitWidth(IGameTexture tex) { if (Renderer == default) { return; } - var scale = Renderer.GetScreenWidth() / (float)tex.Width; + var scale = Renderer.ScreenWidth / (float)tex.Width; var scaledHeight = tex.Height * scale; - var offsetY = (Renderer.GetScreenHeight() - tex.Height) / 2f; + var offsetY = (Renderer.ScreenHeight - tex.Height) / 2f; DrawGameTexture( tex, GetSourceRect(tex), new FloatRect( - Renderer.GetView().X, Renderer.GetView().Y + offsetY, Renderer.GetScreenWidth(), scaledHeight + Renderer.GetView().X, Renderer.GetView().Y + offsetY, Renderer.ScreenWidth, scaledHeight ), Color.White ); } - public static void DrawFullScreenTextureFitHeight(GameTexture tex) + public static void DrawFullScreenTextureFitHeight(IGameTexture tex) { if (Renderer == default) { return; } - var scale = Renderer.GetScreenHeight() / (float)tex.Height; + var scale = Renderer.ScreenHeight / (float)tex.Height; var scaledWidth = tex.Width * scale; - var offsetX = (Renderer.GetScreenWidth() - scaledWidth) / 2f; + var offsetX = (Renderer.ScreenWidth - scaledWidth) / 2f; DrawGameTexture( tex, GetSourceRect(tex), new FloatRect( - Renderer.GetView().X + offsetX, Renderer.GetView().Y, scaledWidth, Renderer.GetScreenHeight() + Renderer.GetView().X + offsetX, Renderer.GetView().Y, scaledWidth, Renderer.ScreenHeight ), Color.White ); } - public static void DrawFullScreenTextureFitMinimum(GameTexture tex) + public static void DrawFullScreenTextureFitMinimum(IGameTexture tex) { if (Renderer == default) { return; } - if (Renderer.GetScreenWidth() > Renderer.GetScreenHeight()) + if (Renderer.ScreenWidth > Renderer.ScreenHeight) { DrawFullScreenTextureFitHeight(tex); } @@ -906,14 +925,14 @@ public static void DrawFullScreenTextureFitMinimum(GameTexture tex) } } - public static void DrawFullScreenTextureFitMaximum(GameTexture tex) + public static void DrawFullScreenTextureFitMaximum(IGameTexture tex) { if (Renderer == default) { return; } - if (Renderer.GetScreenWidth() < Renderer.GetScreenHeight()) + if (Renderer.ScreenWidth < Renderer.ScreenHeight) { DrawFullScreenTextureFitHeight(tex); } @@ -934,8 +953,8 @@ private static void UpdateView() if (Globals.GameState != GameStates.InGame || !MapInstance.TryGet(Globals.Me?.MapId ?? Guid.Empty, out var map)) { - var sw = Renderer.GetScreenWidth(); - var sh = Renderer.GetScreenHeight(); + var sw = Renderer.ScreenWidth; + var sh = Renderer.ScreenHeight; var sx = 0; var sy = 0; CurrentView = new FloatRect(sx, sy, sw / scale, sh / scale); @@ -1043,7 +1062,7 @@ private static void ClearDarknessTexture() return; } - sDarknessTexture ??= Renderer.CreateRenderTexture(Renderer.GetScreenWidth(), Renderer.GetScreenHeight()); + sDarknessTexture ??= Renderer.CreateRenderTexture(Renderer.ScreenWidth, Renderer.ScreenHeight); sDarknessTexture.Clear(Color.Black); } @@ -1079,7 +1098,7 @@ private static void GenerateLightMap() if (map.IsIndoors) { DrawGameTexture( - Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), + Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), destRect, new Color((byte)BrightnessLevel, 255, 255, 255), sDarknessTexture, GameBlendModes.Add ); @@ -1087,13 +1106,13 @@ private static void GenerateLightMap() else { DrawGameTexture( - Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), + Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), destRect, new Color(255, 255, 255, 255), sDarknessTexture, GameBlendModes.Add ); DrawGameTexture( - Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), + Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), destRect, new Color( (int)Time.GetTintColor().A, (int)Time.GetTintColor().R, (int)Time.GetTintColor().G, @@ -1175,7 +1194,7 @@ private static void DrawLights() radialShader.SetFloat("Expand", l.Expand / 100f); DrawGameTexture( - Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), + Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), new FloatRect(x, y, l.Size * 2, l.Size * 2), new Color(255, 255, 255, 255), sDarknessTexture, GameBlendModes.Add, radialShader, 0, false ); @@ -1421,10 +1440,10 @@ public static Pointf ConvertToWorldPointNoZoom(Pointf windowPoint) /// How much to rotate the texture in degrees /// If true, the texture will be drawn immediately. If false, it will be queued for drawing. public static void DrawGameTexture( - GameTexture tex, + IGameTexture tex, float x, float y, - GameRenderTexture? renderTarget = null, + IGameRenderTexture? renderTarget = null, GameBlendModes blendMode = GameBlendModes.None, GameShader? shader = null, float rotationDegrees = 0.0f, @@ -1454,11 +1473,11 @@ public static void DrawGameTexture( /// How much to rotate the texture in degrees /// If true, the texture will be drawn immediately. If false, it will be queued for drawing. public static void DrawGameTexture( - GameTexture tex, + IGameTexture tex, float x, float y, Color renderColor, - GameRenderTexture? renderTarget = null, + IGameRenderTexture? renderTarget = null, GameBlendModes blendMode = GameBlendModes.None, GameShader? shader = null, float rotationDegrees = 0.0f, @@ -1490,14 +1509,14 @@ public static void DrawGameTexture( /// How much to rotate the texture in degrees /// If true, the texture will be drawn immediately. If false, it will be queued for drawing. public static void DrawGameTexture( - GameTexture tex, + IGameTexture tex, float dx, float dy, float sx, float sy, float w, float h, - GameRenderTexture? renderTarget = null, + IGameRenderTexture? renderTarget = null, GameBlendModes blendMode = GameBlendModes.None, GameShader? shader = null, float rotationDegrees = 0.0f, @@ -1516,11 +1535,11 @@ public static void DrawGameTexture( } public static void DrawGameTexture( - GameTexture tex, + IGameTexture tex, FloatRect srcRectangle, FloatRect targetRect, Color renderColor, - GameRenderTexture? renderTarget = null, + IGameRenderTexture? renderTarget = null, GameBlendModes blendMode = GameBlendModes.None, GameShader? shader = null, float rotationDegrees = 0.0f, diff --git a/Intersect.Client.Core/Core/Input.cs b/Intersect.Client.Core/Core/Input.cs index b1687064c0..1b0aec3b90 100644 --- a/Intersect.Client.Core/Core/Input.cs +++ b/Intersect.Client.Core/Core/Input.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; using Intersect.Admin.Actions; -using Intersect.Client.Core.Controls; using Intersect.Client.Entities; using Intersect.Client.Framework.GenericClasses; using Intersect.Client.Framework.Graphics; @@ -13,7 +11,7 @@ using Intersect.Client.Networking; using Intersect.Configuration; using Intersect.Enums; -using Intersect.Utilities; +using Intersect.Framework.Core; namespace Intersect.Client.Core; @@ -146,7 +144,7 @@ public static void OnKeyPressed(Keys modifier, Keys key) if (simplifiedEscapeMenuSetting) { - if (gameUi.EscapeMenu.IsVisible) + if (gameUi.EscapeMenu.IsVisibleInTree) { gameUi.EscapeMenu.ToggleHidden(); } @@ -217,7 +215,7 @@ public static void OnKeyPressed(Keys modifier, Keys key) } case Control.OpenDebugger: - _ = MutableInterface.ToggleDebug(); + Interface.Interface.CurrentInterface.ToggleDebug(); break; } diff --git a/Intersect.Client.Core/Core/Main.cs b/Intersect.Client.Core/Core/Main.cs index a727e85057..53921e2f56 100644 --- a/Intersect.Client.Core/Core/Main.cs +++ b/Intersect.Client.Core/Core/Main.cs @@ -4,9 +4,9 @@ using Intersect.Client.Networking; using Intersect.Configuration; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Maps; -using Intersect.Utilities; // ReSharper disable All @@ -106,7 +106,7 @@ private static void ProcessIntro() { if (ClientConfiguration.Instance.IntroImages.Count > 0) { - GameTexture imageTex = Globals.ContentManager.GetTexture( + IGameTexture imageTex = Globals.ContentManager.GetTexture( Framework.Content.TextureType.Image, ClientConfiguration.Instance.IntroImages[Globals.IntroIndex] ); @@ -243,7 +243,7 @@ private static void ProcessGame() //Update Game Animations if (_animationTimer < millisecondsNow) { - Globals.AnimFrame = (Globals.AnimFrame + 1) % 3; + Globals.AnimationFrame = (Globals.AnimationFrame + 1) % 3; _animationTimer = millisecondsNow + 500; } diff --git a/Intersect.Client.Core/Core/Sounds/Sound.cs b/Intersect.Client.Core/Core/Sounds/Sound.cs index 7fbd074173..05d80ac571 100644 --- a/Intersect.Client.Core/Core/Sounds/Sound.cs +++ b/Intersect.Client.Core/Core/Sounds/Sound.cs @@ -2,7 +2,7 @@ using Intersect.Client.Framework.Core.Sounds; using Intersect.Client.Framework.File_Management; using Intersect.Client.General; -using Intersect.Utilities; +using Intersect.Framework.Core; namespace Intersect.Client.Core.Sounds; diff --git a/Intersect.Client.Core/Entities/Animation.cs b/Intersect.Client.Core/Entities/Animation.cs index 44f4d930a5..6ba80b9e1a 100644 --- a/Intersect.Client.Core/Entities/Animation.cs +++ b/Intersect.Client.Core/Entities/Animation.cs @@ -5,6 +5,7 @@ using Intersect.Client.Framework.Graphics; using Intersect.Client.General; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Animations; using Intersect.GameObjects; using Intersect.GameObjects.Animations; diff --git a/Intersect.Client.Core/Entities/ChatBubble.cs b/Intersect.Client.Core/Entities/ChatBubble.cs index 617693e388..fb5f548fcd 100644 --- a/Intersect.Client.Core/Entities/ChatBubble.cs +++ b/Intersect.Client.Core/Entities/ChatBubble.cs @@ -4,13 +4,14 @@ using Intersect.Client.Framework.Graphics; using Intersect.Client.Framework.Gwen.ControlInternal; using Intersect.Client.General; +using Intersect.Framework.Core; using Intersect.Utilities; namespace Intersect.Client.Entities; public partial class ChatBubble { - private readonly GameTexture? mBubbleTex; + private readonly IGameTexture? mBubbleTex; private readonly Entity? mOwner; diff --git a/Intersect.Client.Core/Entities/Critter.cs b/Intersect.Client.Core/Entities/Critter.cs index 77c919b18e..6e00af5b15 100644 --- a/Intersect.Client.Core/Entities/Critter.cs +++ b/Intersect.Client.Core/Entities/Critter.cs @@ -4,6 +4,7 @@ using Intersect.Client.General; using Intersect.Client.Maps; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Maps; using Intersect.Utilities; diff --git a/Intersect.Client.Core/Entities/Dash.cs b/Intersect.Client.Core/Entities/Dash.cs index 4ce82c4f43..5cead5526b 100644 --- a/Intersect.Client.Core/Entities/Dash.cs +++ b/Intersect.Client.Core/Entities/Dash.cs @@ -2,6 +2,7 @@ using Intersect.Client.Maps; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Utilities; using Microsoft.Extensions.Logging; diff --git a/Intersect.Client.Core/Entities/Entity.cs b/Intersect.Client.Core/Entities/Entity.cs index bfe8e0ad66..fa7e7f11b0 100644 --- a/Intersect.Client.Core/Entities/Entity.cs +++ b/Intersect.Client.Core/Entities/Entity.cs @@ -15,6 +15,7 @@ using Intersect.Client.Spells; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Animations; using Intersect.GameObjects; using Intersect.GameObjects.Animations; @@ -192,13 +193,13 @@ public Guid SpellCast IReadOnlyDictionary IEntity.Stats => Enum.GetValues().ToDictionary(stat => stat, stat => Stat[(int)stat]); - public GameTexture? Texture { get; set; } + public IGameTexture? Texture { get; set; } #region "Animation Textures and Timing" public SpriteAnimations SpriteAnimation { get; set; } = SpriteAnimations.Normal; - public Dictionary AnimatedTextures { get; set; } = []; + public Dictionary AnimatedTextures { get; set; } = []; public int SpriteFrame { get; set; } = 0; @@ -1379,7 +1380,7 @@ public virtual void DrawEquipment(string filename, Color renderColor) } // Paperdoll textures and Frames. - GameTexture? paperdollTex = null; + IGameTexture? paperdollTex = null; var spriteFrames = SpriteFrames; // Extract filename without it's extension. @@ -1537,7 +1538,7 @@ public void DrawLabels( if (backgroundColor != Color.Transparent) { Graphics.DrawGameTexture( - Graphics.Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), + Graphics.Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), new FloatRect(x - textSize.X / 2f - 4, y, textSize.X + 8, textSize.Y), backgroundColor ); } @@ -1600,7 +1601,7 @@ public virtual void DrawName(Color? textColor, Color? borderColor = null, Color? if (backgroundColor != Color.Transparent) { Graphics.DrawGameTexture( - Graphics.Renderer.GetWhiteTexture(), new FloatRect(0, 0, 1, 1), + Graphics.Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), new FloatRect(x - textSize.X / 2f - 4, y, textSize.X + 8, textSize.Y), backgroundColor ); } @@ -1727,7 +1728,7 @@ protected bool ShouldNotDrawHpBar } } - public GameTexture GetBoundingHpBarTexture() + public IGameTexture GetBoundingHpBarTexture() { return GameTexture.GetBoundingTexture( BoundsComparison.Height, @@ -2196,7 +2197,7 @@ protected virtual void LoadAnimationTexture(string textureName, SpriteAnimations } } - protected virtual bool TryGetAnimationTexture(string textureName, SpriteAnimations spriteAnimation, string textureOverride, out GameTexture texture) + protected virtual bool TryGetAnimationTexture(string textureName, SpriteAnimations spriteAnimation, string textureOverride, out IGameTexture texture) { var baseFilename = Path.GetFileNameWithoutExtension(textureName); var extension = Path.GetExtension(textureName); diff --git a/Intersect.Client.Core/Entities/Events/Event.cs b/Intersect.Client.Core/Entities/Events/Event.cs index cf27dcb67a..c7606738d3 100644 --- a/Intersect.Client.Core/Entities/Events/Event.cs +++ b/Intersect.Client.Core/Entities/Events/Event.cs @@ -93,7 +93,7 @@ public override bool Update() return success; } - protected bool TryEnsureTexture(out GameTexture? texture) + protected bool TryEnsureTexture(out IGameTexture? texture) { if (_drawCompletedWithoutTexture) { diff --git a/Intersect.Client.Core/Entities/Player.cs b/Intersect.Client.Core/Entities/Player.cs index aec2d4672b..1e016f6463 100644 --- a/Intersect.Client.Core/Entities/Player.cs +++ b/Intersect.Client.Core/Entities/Player.cs @@ -22,6 +22,7 @@ using Intersect.Core; using Intersect.Enums; using Intersect.Extensions; +using Intersect.Framework.Core; using Intersect.Framework.Reflection; using Intersect.GameObjects; using Intersect.GameObjects.Maps; @@ -504,7 +505,7 @@ private static int GetQuantityOfItemIn(IEnumerable items, Guid itemId) return (int)Math.Min(count, int.MaxValue); } - public static int GetQuantityOfItemInBank(Guid itemId) => GetQuantityOfItemIn(Globals.Bank, itemId); + public static int GetQuantityOfItemInBank(Guid itemId) => GetQuantityOfItemIn(Globals.BankSlots, itemId); public int GetQuantityOfItemInInventory(Guid itemId) => GetQuantityOfItemIn(Inventory, itemId); @@ -852,7 +853,7 @@ public bool TryStoreItemInBank( ) { // Permission Check for Guild Bank - if (Globals.GuildBank && !IsGuildBankDepositAllowed()) + if (Globals.IsGuildBank && !IsGuildBankDepositAllowed()) { ChatboxMsg.AddMessage(new ChatboxMsg(Strings.Guilds.NotAllowedDeposit.ToString(Globals.Me?.Guild), CustomColors.Alerts.Error, ChatMessageType.Bank)); return false; @@ -876,7 +877,7 @@ public bool TryStoreItemInBank( var sourceQuantity = GetQuantityOfItemInInventory(itemDescriptor.Id); var quantity = quantityHint < 0 ? sourceQuantity : quantityHint; - var targetSlots = Globals.Bank.ToArray(); + var targetSlots = Globals.BankSlots.ToArray(); var movableQuantity = Item.FindSpaceForItem( itemDescriptor.Id, @@ -979,13 +980,13 @@ public bool TryRetrieveItemFromBank( ) { // Permission Check for Guild Bank - if (Globals.GuildBank && !IsGuildBankWithdrawAllowed()) + if (Globals.IsGuildBank && !IsGuildBankWithdrawAllowed()) { ChatboxMsg.AddMessage(new ChatboxMsg(Strings.Guilds.NotAllowedWithdraw.ToString(Globals.Me?.Guild), CustomColors.Alerts.Error, ChatMessageType.Bank)); return false; } - slot ??= Globals.Bank[bankSlotIndex]; + slot ??= Globals.BankSlots[bankSlotIndex]; if (!ItemBase.TryGet(slot.ItemId, out var itemDescriptor)) { ApplicationContext.Context.Value?.Logger.LogWarning($"Tried to move item that does not exist from slot {bankSlotIndex}: {itemDescriptor.Id}"); @@ -1137,7 +1138,7 @@ private static void TryStoreItemInBagOnSubmit(Base sender, InputSubmissionEventA public void TryRetrieveItemFromBag(int bagSlotIndex, int inventorySlotIndex) { - var bagSlot = Globals.Bag[bagSlotIndex]; + var bagSlot = Globals.BagSlots[bagSlotIndex]; if (bagSlot == default) { return; @@ -2704,7 +2705,7 @@ public virtual void DrawGuildName(Color textColor, Color? borderColor = default, if (backgroundColor != Color.Transparent) { Graphics.DrawGameTexture( - Graphics.Renderer.GetWhiteTexture(), + Graphics.Renderer.WhitePixel, new FloatRect(0, 0, 1, 1), new FloatRect(x - textSize.X / 2f - 4, y, textSize.X + 8, textSize.Y), backgroundColor diff --git a/Intersect.Client.Core/Entities/Projectiles/Projectile.cs b/Intersect.Client.Core/Entities/Projectiles/Projectile.cs index fc92251d36..97eee67dd1 100644 --- a/Intersect.Client.Core/Entities/Projectiles/Projectile.cs +++ b/Intersect.Client.Core/Entities/Projectiles/Projectile.cs @@ -1,6 +1,7 @@ using Intersect.Client.Framework.Entities; using Intersect.Client.General; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Maps; using Intersect.Network.Packets.Server; diff --git a/Intersect.Client.Core/Entities/Projectiles/ProjectileSpawns.cs b/Intersect.Client.Core/Entities/Projectiles/ProjectileSpawns.cs index 79ab188e86..0bef4b6938 100644 --- a/Intersect.Client.Core/Entities/Projectiles/ProjectileSpawns.cs +++ b/Intersect.Client.Core/Entities/Projectiles/ProjectileSpawns.cs @@ -1,4 +1,5 @@ using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; diff --git a/Intersect.Client.Core/Entities/Status.cs b/Intersect.Client.Core/Entities/Status.cs index 70c9659c17..9fa0dedf2a 100644 --- a/Intersect.Client.Core/Entities/Status.cs +++ b/Intersect.Client.Core/Entities/Status.cs @@ -1,5 +1,6 @@ using Intersect.Client.Framework.Entities; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Utilities; namespace Intersect.Client.Entities; diff --git a/Intersect.Client.Core/General/Globals.cs b/Intersect.Client.Core/General/Globals.cs index a71215fd1e..a5238f4f00 100644 --- a/Intersect.Client.Core/General/Globals.cs +++ b/Intersect.Client.Core/General/Globals.cs @@ -15,59 +15,139 @@ namespace Intersect.Client.General; - public static partial class Globals { + //Entities and stuff + //public static List Entities = new List(); + public static readonly Dictionary Entities = []; + + public static readonly List EntitiesToDispose = []; + + //Control Objects + public static readonly List EventDialogs = []; + + public static readonly Dictionary EventHolds = []; + + //Game Lock + public static readonly object GameLock = new(); + + //Crucial game variables + + internal static readonly List ClientLifecycleHelpers = []; + + private static GameStates mGameState = GameStates.Intro; + + public static readonly Dictionary GridMaps = []; + + public static bool HasGameData = false; + + public static bool InBag = false; + + public static bool InBank = false; + + //Crafting station + public static bool InCraft = false; + + public static bool InTrade = false; + + public static GameInput InputManager; + + public static bool IntroComing = true; + + public static readonly long IntroDelay = 2000; + + //Engine Progression + public static int IntroIndex = 0; + + public static long IntroStartTime = -1; + + public static bool IsRunning = true; + + public static bool JoiningGame = false; + + public static bool LoggedIn = false; + + //Map/Chunk Array + public static Guid[,]? MapGrid; + + public static long MapGridHeight; + + public static long MapGridWidth; + + //Local player information + public static Player? Me; + + public static bool MoveRouteActive = false; + + public static bool NeedsMaps = true; + + //Event Guid and the Map its associated with + public static readonly Dictionary> PendingEvents = new(); + + //Event Show Pictures + public static ShowPicturePacket? Picture; + + public static readonly List QuestOffers = new(); + + public static readonly Random Random = new(); + + public static GameSystem System; + + //Trading (Only 2 people can trade at once) + public static Item[,] Trade; + + //Scene management //Only need 1 table, and that is the one we see at a given moment in time. - public static CraftingTableBase ActiveCraftingTable; + public static CraftingTableBase? ActiveCraftingTable { get; set; } - public static int AnimFrame = 0; + public static int AnimationFrame { get; set; } //Bag - public static Item[] Bag = null; + public static Item[]? BagSlots { get; set; } //Bank - public static Item[] Bank; - public static bool GuildBank; - public static int BankSlots; + public static Item[]? BankSlots { get; set; } + public static bool IsGuildBank { get; set; } + public static int BankSlotCount { get; set; } - public static bool ConnectionLost; + public static bool ConnectionLost { get; set; } /// - /// This is used to prevent the client from showing unnecessary disconnect messages + /// This is used to prevent the client from showing unnecessary disconnect messages /// - public static bool SoftLogout; + public static bool SoftLogout { get; set; } //Game Systems - public static GameContentManager ContentManager; - - public static int CurrentMap = -1; + public static GameContentManager? ContentManager { get; set; } - public static GameDatabase Database; - - //Entities and stuff - //public static List Entities = new List(); - public static Dictionary Entities = new Dictionary(); + public static GameDatabase? Database { get; set; } - public static List EntitiesToDispose = new List(); + //Game Shop + //Only need 1 shop, and that is the one we see at a given moment in time. + public static ShopBase? GameShop { get; set; } - //Control Objects - public static List EventDialogs = new List(); + /// + public static GameStates GameState + { + get => mGameState; + set + { + mGameState = value; + OnLifecycleChangeState(); + } + } - public static Dictionary EventHolds = new Dictionary(); + public static bool InShop => GameShop != null; - //Game Lock - public static object GameLock = new object(); + public static bool CanCloseInventory => !(InBag || InBank || InCraft || InShop || InTrade); - //Game Shop - //Only need 1 shop, and that is the one we see at a given moment in time. - public static ShopBase? GameShop; + public static bool HoldToSoftRetargetOnSelfCast { get; set; } - //Crucial game variables + public static bool ShouldSoftRetargetOnSelfCast => + HoldToSoftRetargetOnSelfCast || Database.AutoSoftRetargetOnSelfCast; - internal static List ClientLifecycleHelpers { get; } = - new List(); + public static bool WaitingOnServer { get; set; } internal static void OnLifecycleChangeState() { @@ -115,113 +195,20 @@ internal static void OnGameUpdate(TimeSpan deltaTime) } ClientLifecycleHelpers.ForEach( - clientLifecycleHelper => clientLifecycleHelper?.OnGameUpdate(GameState, Globals.Me, knownEntities, deltaTime) + clientLifecycleHelper => clientLifecycleHelper?.OnGameUpdate(GameState, Me, knownEntities, deltaTime) ); } internal static void OnGameDraw(DrawStates state, TimeSpan deltaTime) { - ClientLifecycleHelpers.ForEach( - clientLifecycleHelper => clientLifecycleHelper?.OnGameDraw(state, deltaTime) - ); + ClientLifecycleHelpers.ForEach(clientLifecycleHelper => clientLifecycleHelper?.OnGameDraw(state, deltaTime)); } internal static void OnGameDraw(DrawStates state, IEntity entity, TimeSpan deltaTime) { ClientLifecycleHelpers.ForEach( - clientLifecycleHelper => clientLifecycleHelper?.OnGameDraw(state, entity, deltaTime) - ); - } - - private static GameStates mGameState = GameStates.Intro; - - /// - public static GameStates GameState - { - get => mGameState; - set - { - mGameState = value; - OnLifecycleChangeState(); - } - } - - public static Dictionary GridMaps = []; - - public static bool HasGameData = false; - - public static bool InBag = false; - - public static bool InBank = false; - - //Crafting station - public static bool InCraft = false; - - public static bool InShop => GameShop != null; - - public static bool InTrade = false; - - public static bool CanCloseInventory => !(InBag || InBank || InCraft || InShop || InTrade); - - public static GameInput InputManager; - - public static bool IntroComing = true; - - public static long IntroDelay = 2000; - - //Engine Progression - public static int IntroIndex = 0; - - public static long IntroStartTime = -1; - - public static bool IsRunning = true; - - public static bool JoiningGame = false; - - public static bool LoggedIn = false; - - //Map/Chunk Array - public static Guid[,]? MapGrid; - - public static long MapGridHeight; - - public static long MapGridWidth; - - //Local player information - public static Player? Me; - - public static bool HoldToSoftRetargetOnSelfCast { get; set; } - - public static bool ShouldSoftRetargetOnSelfCast => - HoldToSoftRetargetOnSelfCast || Database.AutoSoftRetargetOnSelfCast; - - public static bool MoveRouteActive = false; - - public static bool NeedsMaps = true; - - //Event Guid and the Map its associated with - public static Dictionary> PendingEvents = - new Dictionary>(); - - //Event Show Pictures - public static ShowPicturePacket? Picture; - - public static List QuestOffers = new List(); - - public static Random Random = new Random(); - - public static GameSystem System; - - //Trading (Only 2 people can trade at once) - public static Item[,] Trade; - - //Scene management - private static bool _waitingOnServer; - - public static bool WaitingOnServer - { - get => _waitingOnServer; - set => _waitingOnServer = value; + clientLifecycleHelper => clientLifecycleHelper?.OnGameDraw(state, entity, deltaTime) + ); } public static Entity GetEntity(Guid id, EntityType type) @@ -243,5 +230,4 @@ public static Entity GetEntity(Guid id, EntityType type) return default; } - -} +} \ No newline at end of file diff --git a/Intersect.Client.Core/Interface/Debugging/TexturesSearchableTreeDataProvider.cs b/Intersect.Client.Core/Interface/Debugging/AssetsSearchableTreeDataProvider.cs similarity index 96% rename from Intersect.Client.Core/Interface/Debugging/TexturesSearchableTreeDataProvider.cs rename to Intersect.Client.Core/Interface/Debugging/AssetsSearchableTreeDataProvider.cs index 858daecfad..7dd8a5f361 100644 --- a/Intersect.Client.Core/Interface/Debugging/TexturesSearchableTreeDataProvider.cs +++ b/Intersect.Client.Core/Interface/Debugging/AssetsSearchableTreeDataProvider.cs @@ -11,7 +11,7 @@ namespace Intersect.Client.Interface.Debugging; -public sealed class TexturesSearchableTreeDataProvider : ISearchableTreeDataProvider +public sealed class AssetsSearchableTreeDataProvider : ISearchableTreeDataProvider { private readonly GameContentManager _contentManager; private readonly Dictionary _entries; @@ -19,7 +19,7 @@ public sealed class TexturesSearchableTreeDataProvider : ISearchableTreeDataProv private readonly Dictionary> _entriesByParent; private readonly Base _parent; - public TexturesSearchableTreeDataProvider(GameContentManager contentManager, Base parent) + public AssetsSearchableTreeDataProvider(GameContentManager contentManager, Base parent) { _contentManager = contentManager; _parent = parent; @@ -152,7 +152,7 @@ private static SearchableTreeDataEntry EntryForAsset(string parentId, IAsset ass var assetName = asset.Name ?? asset.Id; var displayText = assetName; - if (asset is GameTexture { TexturePackFrame: not null }) + if (asset is IGameTexture { AtlasReference: not null }) { displayText = Strings.Debug.FormatTextureFromAtlas.ToString(displayText); } diff --git a/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs b/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs index 68551f05a0..71a4494177 100644 --- a/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs +++ b/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs @@ -1,14 +1,11 @@ using System.Diagnostics; -using Intersect.Async; using Intersect.Client.Core; -using Intersect.Client.Entities; using Intersect.Client.Framework.Content; -using Intersect.Client.Framework.File_Management; using Intersect.Client.Framework.GenericClasses; using Intersect.Client.Framework.Graphics; using Intersect.Client.Framework.Gwen; using Intersect.Client.Framework.Gwen.Control; -using Intersect.Client.Framework.Gwen.Control.Data; +using Intersect.Client.Framework.Gwen.Control.EventArguments; using Intersect.Client.Framework.Gwen.Control.Layout; using Intersect.Client.Framework.Gwen.Control.Utility; using Intersect.Client.Framework.Input; @@ -17,8 +14,9 @@ using Intersect.Client.Interface.Debugging.Providers; using Intersect.Client.Localization; using Intersect.Client.Maps; -using Intersect.Client.MonoGame.NativeInterop.OpenGL; using Intersect.Framework.Reflection; +using Intersect.Framework.SystemInformation; +using Intersect.IO.Files; using static Intersect.Client.Framework.File_Management.GameContentManager; namespace Intersect.Client.Interface.Debugging; @@ -50,7 +48,7 @@ public DebugWindow(Base parent) : base(parent, Strings.Debug.Title, false, nameo CheckboxDrawDebugOutlines = CreateInfoCheckboxDrawDebugOutlines(TabInfo.Page); CheckboxEnableLayoutHotReloading = CreateInfoCheckboxEnableLayoutHotReloading(TabInfo.Page); CheckboxIncludeTextNodesInHover = CreateInfoCheckboxIncludeTextNodesInHover(TabInfo.Page); - CheckboxViewClickedComponentInDebugger = CreateInfoCheckboxViewClickedNodeInDebugger(TabInfo.Page); + CheckboxViewClickedNodeInDebugger = CreateInfoCheckboxViewClickedNodeInDebugger(TabInfo.Page); ButtonShutdownServer = CreateInfoButtonShutdownServer(TabInfo.Page); ButtonShutdownServerAndExit = CreateInfoButtonShutdownServerAndExit(TabInfo.Page); TableDebugStats = CreateInfoTableDebugStats(TabInfo.Page); @@ -60,12 +58,326 @@ public DebugWindow(Base parent) : base(parent, Strings.Debug.Title, false, nameo AssetsList = CreateAssetsList(TabAssets.Page); AssetsButtonReloadAsset = CreateAssetsButtonReloadAsset(AssetsToolsTable, AssetsList); + TabSystem = Tabs.AddPage(Strings.Debug.TabLabelSystem, nameof(TabSystem)); + _ = new Label(TabSystem.Page, name: "HeaderSectionSystemStatistics") + { + Dock = Pos.Top, + Font = _defaultFont, + Text = Strings.Debug.SectionSystemStatistics, + TextAlign = Pos.CenterH, + TextColorOverride = new Color(r: 191, g: 255, b: 191), + }; + SystemStatisticsTable = CreateSystemStatisticsTable(TabSystem.Page); + _ = new Label(TabSystem.Page, name: "HeaderSectionGPUStatistics") + { + Dock = Pos.Top, + Font = _defaultFont, + Text = Strings.Debug.SectionGPUStatistics, + TextAlign = Pos.CenterH, + TextColorOverride = new Color(r: 191, g: 255, b: 191), + }; + GPUStatisticsTable = CreateGPUStatisticsTable(TabSystem.Page); + _ = new Label(TabSystem.Page, name: "HeaderSectionGPUAllocations") + { + Dock = Pos.Top, + Font = _defaultFont, + Text = Strings.Debug.SectionGPUAllocations, + TextAlign = Pos.CenterH, + TextColorOverride = new Color(r: 191, g: 255, b: 191), + }; + GPUAllocationsTable = CreateGPUAllocationsTable(TabSystem.Page); + AssetsToolsTable.SizeToChildren(); } + protected override void Dispose(bool disposing) + { + UnsubscribeGPU(); + RemoveIntercepts(); + + base.Dispose(disposing); + } + + private Table GPUStatisticsTable { get; } + + private Table SystemStatisticsTable { get; } + + private Table GPUAllocationsTable { get; } + + private Table CreateGPUAllocationsTable(Base parent) + { + Panel gpuAllocationsPanel = new(parent: parent, name: nameof(gpuAllocationsPanel)) + { + Dock = Pos.Fill, + }; + + ScrollControl gpuAllocationsScroller = new(parent: gpuAllocationsPanel, name: nameof(gpuAllocationsScroller)) + { + Dock = Pos.Fill, + InnerPanelPadding = new Padding(horizontal: 8, vertical: 0), + ShouldCacheToTexture = true, + }; + + gpuAllocationsScroller.VerticalScrollBar.BaseNudgeAmount *= 2; + + var table = new Table(parent: gpuAllocationsScroller, name: nameof(GPUAllocationsTable)) + { + AutoSizeToContentHeightOnChildResize = true, + AutoSizeToContentWidthOnChildResize = true, + CellSpacing = new Point(x: 8, y: 2), + ColumnCount = 2, + ColumnWidths = [null, 100], + Dock = Pos.Fill, + Font = _defaultFont, + MinimumSize = new Point(x: 408, y: 0), + SizeToContents = true, + }; + table.VisibilityChanged += (sender, args) => + { + if (!args.IsVisibleInTree || !_resizeGPUAllocationsTable) + { + return; + } + + _resizeGPUAllocationsTable = false; + sender.SizeToChildren(recursive: true); + sender.InvalidateParent(); + }; + table.SizeChanged += ResizeTableToChildrenOnSizeChanged; + + var existingTextures = Graphics.Renderer.Textures; + foreach (var texture in existingTextures) + { + _ = EnsureGPUAllocationsRowFor(gameTexture: texture, creating: true, table: table); + } + + SubscribeGPU(); + + return table; + } + + private static void ResizeTableHeightToChildrenOnSizeChanged(Base sender, ValueChangedEventArgs args) + { + sender.SizeToChildren(resizeX: false, resizeY: true, recursive: true); + } + + private static void ResizeTableToChildrenOnSizeChanged(Base sender, ValueChangedEventArgs args) + { + sender.SizeToChildren(resizeX: args.Value.X > args.OldValue.X, resizeY: true, recursive: true); + } + + private void SubscribeGPU() + { + Graphics.Renderer.TextureAllocated += RendererOnTextureAllocated; + Graphics.Renderer.TextureCreated += RendererOnTextureCreated; + Graphics.Renderer.TextureDisposed += RendererOnTextureDisposed; + Graphics.Renderer.TextureFreed += RendererOnTextureFreed; + } + + private void UnsubscribeGPU() + { + Graphics.Renderer.TextureAllocated -= RendererOnTextureAllocated; + Graphics.Renderer.TextureCreated -= RendererOnTextureCreated; + Graphics.Renderer.TextureDisposed -= RendererOnTextureDisposed; + Graphics.Renderer.TextureFreed -= RendererOnTextureFreed; + } + + private void RendererOnTextureDisposed(object? _, TextureEventArgs args) + { + var gameTexture = args.GameTexture; + if (_gpuAllocationsRowByTextureLookup.TryGetValue(gameTexture, out var existingRow)) + { + GPUAllocationsTable.RemoveRow(existingRow); + } + } + + private void RendererOnTextureFreed(object? _, TextureEventArgs args) + { + var x = this; + var gameTexture = args.GameTexture; + var row = EnsureGPUAllocationsRowFor(gameTexture, creating: false); + if (row.GetCellContents(1) is not Label statusLabel) + { + return; + } + + statusLabel.Text = "Freed"; + statusLabel.TextColorOverride = new Color(255, 63, 63); + } + + private void RendererOnTextureAllocated(object? _, TextureEventArgs args) + { + var gameTexture = args.GameTexture; + var row = EnsureGPUAllocationsRowFor(gameTexture, creating: false); + if (row.GetCellContents(1) is not Label statusLabel) + { + return; + } + + statusLabel.Text = "Allocated"; + statusLabel.TextColorOverride = new Color(63, 255, 63); + } + + private void RendererOnTextureCreated(object? _, TextureEventArgs args) + { + var gameTexture = args.GameTexture; + EnsureGPUAllocationsRowFor(gameTexture, creating: true); + } + + private readonly Dictionary _gpuAllocationsRowByTextureLookup = []; + + private TableRow EnsureGPUAllocationsRowFor(IGameTexture gameTexture, bool creating, Table? table = null) + { + if (_gpuAllocationsRowByTextureLookup.TryGetValue(gameTexture, out var existingRow)) + { + if (creating) + { + throw new InvalidOperationException(); + } + + return existingRow; + } + + table ??= GPUAllocationsTable; + + existingRow = table.InsertRowSorted( + gameTexture.Name, + userData: gameTexture, + keySelector: SelectRowUserDataGameTextureName + ); + + var nameCell = existingRow.GetCellContents(0); + nameCell.UserData = gameTexture; + nameCell.MouseInputEnabled = true; + nameCell.Clicked += CopyAssetNameToClipboardOnNameCellClicked; + existingRow.SetCellText(1, "Created"); + if (existingRow.GetCellContents(1) is Label statusLabel) + { + statusLabel.TextColorOverride = new Color(191, 191, 191); + } + _gpuAllocationsRowByTextureLookup[gameTexture] = existingRow; + + if (table.IsVisibleInTree) + { + table.SizeToChildren(recursive: true); + } + else + { + _resizeGPUAllocationsTable = true; + } + + return existingRow; + } + + private bool _resizeGPUAllocationsTable; + + private static void CopyAssetNameToClipboardOnNameCellClicked(Base sender, MouseButtonState _) + { + if (sender.UserData is IAsset asset) + { + GameClipboard.Instance.SetText(asset.Name); + } + } + + private static string? SelectRowUserDataGameTextureName(Base? node) + { + return node?.UserData is IGameTexture gameTexture ? gameTexture.Name : null; + } + + private Table CreateGPUStatisticsTable(Base parent) + { + ScrollControl scroller = new(parent, nameof(scroller)) + { + Dock = Pos.Top, + MinimumSize = new Point(0, 100), + }; + + scroller.VerticalScrollBar.BaseNudgeAmount *= 2; + + var table = new Table(scroller, nameof(SystemStatisticsTable)) + { + AutoSizeToContentHeightOnChildResize = true, + AutoSizeToContentWidthOnChildResize = true, + CellSpacing = new Point(8, 2), + ColumnCount = 2, + ColumnWidths = [180, null], + Dock = Pos.Fill, + Font = _defaultFont, + }; + table.SizeChanged += (sender, args) => + { + ResizeTableHeightToChildrenOnSizeChanged(sender, args); + var minimumSize = new Point( + 0, + table.OuterHeight + scroller.InnerPanelPadding.Top + scroller.InnerPanelPadding.Bottom + ); + scroller.MinimumSize = minimumSize; + }; + + table.AddRow(Strings.Debug.Fps, name: "FPSRow").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.FPS), NoValue); + // table.AddRow(Strings.Debug.Draws, name: "DrawsRow").Listen(1, new DelegateDataProvider(() => Graphics.DrawCalls), NoValue); + + table.AddRow(Strings.Debug.MapsDrawn, name: "MapsDrawnRow").Listen(1, new DelegateDataProvider(() => Graphics.MapsDrawn), NoValue); + table.AddRow(Strings.Debug.EntitiesDrawn, name: "EntitiesDrawnRow").Listen(1, new DelegateDataProvider(() => Graphics.EntitiesDrawn), NoValue); + table.AddRow(Strings.Debug.LightsDrawn, name: "LightsDrawnRow").Listen(1, new DelegateDataProvider(() => Graphics.LightsDrawn), NoValue); + table.AddRow(Strings.Debug.InterfaceObjects, name: "InterfaceObjectsRow").Listen(1, new DelegateDataProvider(() => Interface.CurrentInterface?.NodeCount, delayMilliseconds: 1000), NoValue); + + table.AddRow(Strings.Debug.RenderTargetAllocations, name: "GPURenderTargetAllocations").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.RenderTargetAllocations), NoValue); + table.AddRow(Strings.Debug.TextureAllocations, name: "GPUTextureAllocations").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.TextureAllocations), NoValue); + table.AddRow(Strings.Debug.TextureCount, name: "GPUTextureCount").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.TextureCount), NoValue); + + table.AddRow(Strings.Debug.UsedVRAM, name: "GPUUsedVRAM").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(Graphics.Renderer.UsedMemory)), NoValue); + table.AddRow(Strings.Debug.FreeVRAM, name: "GPUFreeVRAM").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.AvailableGPUMemory)), NoValue); + table.AddRow(Strings.Debug.TotalVRAM, name: "GPUTotalVRAM").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.TotalGPUMemory)), NoValue); + + table.PreLayout.Enqueue(t => t.SizeToChildren(recursive: true), table); + + return table; + } + + private Table CreateSystemStatisticsTable(Base parent) + { + ScrollControl scroller = new(parent, nameof(scroller)) + { + Dock = Pos.Top, + }; + + scroller.VerticalScrollBar.BaseNudgeAmount *= 2; + + var table = new Table(scroller, nameof(SystemStatisticsTable)) + { + AutoSizeToContentHeightOnChildResize = true, + AutoSizeToContentWidthOnChildResize = true, + CellSpacing = new Point(8, 2), + ColumnCount = 2, + ColumnWidths = [180, null], + Dock = Pos.Top, + Font = _defaultFont, + }; + table.SizeChanged += (sender, args) => + { + ResizeTableHeightToChildrenOnSizeChanged(sender, args); + var minimumSize = new Point( + 0, + table.OuterHeight + scroller.InnerPanelPadding.Top + scroller.InnerPanelPadding.Bottom + ); + scroller.MinimumSize = minimumSize; + }; + + table.AddRow(Strings.Debug.FreeVirtualRAM, name: "RAMFreeVirtual").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.AvailableSystemMemory)), NoValue); + table.AddRow(Strings.Debug.TotalVirtualRAM, name: "RAMTotalVirtual").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.TotalSystemMemory)), NoValue); + + table.AddRow(Strings.Debug.FreePhysicalRAM, name: "RAMFreePhysical").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.AvailablePhysicalMemory)), NoValue); + table.AddRow(Strings.Debug.TotalPhysicalRAM, name: "RAMTotalPhysical").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.TotalPhysicalMemory)), NoValue); + + table.PreLayout.Enqueue(t => t.SizeToChildren(recursive: true), table); + + return table; + } + private SearchableTree CreateAssetsList(Base parent) { - var dataProvider = new TexturesSearchableTreeDataProvider(Current, this); + var dataProvider = new AssetsSearchableTreeDataProvider(Current, this); SearchableTree assetList = new(parent, dataProvider, name: nameof(AssetsList)) { Dock = Pos.Fill, @@ -105,37 +417,41 @@ private Button CreateAssetsButtonReloadAsset(Table table, SearchableTree assetLi }; row.SetCellContents(0, buttonReloadAsset); - assetList.SelectionChanged += (_, _) => - buttonReloadAsset.IsDisabled = assetList.SelectedNodes.All( - node => node.UserData is not SearchableTreeDataEntry { UserData: GameTexture } - ); + assetList.SelectionChanged += AssetListOnSelectionChanged; + + buttonReloadAsset.Clicked += ButtonReloadAssetOnClicked; + row.SetCellContents(0, buttonReloadAsset, enableMouseInput: true); - buttonReloadAsset.Clicked += (_, _) => + return buttonReloadAsset; + } + + private void ButtonReloadAssetOnClicked(Base @base, MouseButtonState mouseButtonState) + { + foreach (var node in AssetsList.SelectedNodes) { - foreach (var node in assetList.SelectedNodes) + if (node.UserData is not SearchableTreeDataEntry entry) { - if (node.UserData is not SearchableTreeDataEntry entry) - { - continue; - } - - if (entry.UserData is not IAsset asset) - { - continue; - } - - // TODO: Audio reloading? - if (asset is not GameTexture texture) - { - continue; - } - - texture.Reload(); + continue; } - }; - row.SetCellContents(0, buttonReloadAsset, enableMouseInput: true); - return buttonReloadAsset; + if (entry.UserData is not IAsset asset) + { + continue; + } + + // TODO: Audio reloading? + if (asset is not IGameTexture texture) + { + continue; + } + + texture.Reload(); + } + } + + private void AssetListOnSelectionChanged(Base @base, EventArgs eventArgs) + { + AssetsButtonReloadAsset.IsDisabled = AssetsList.SelectedNodes.All(node => node.UserData is not SearchableTreeDataEntry { UserData: IGameTexture }); } private TabControl Tabs { get; } @@ -144,6 +460,8 @@ private Button CreateAssetsButtonReloadAsset(Table table, SearchableTree assetLi private TabButton TabAssets { get; } + private TabButton TabSystem { get; } + private SearchableTree AssetsList { get; } private Table AssetsToolsTable { get; } @@ -156,7 +474,7 @@ private Button CreateAssetsButtonReloadAsset(Table table, SearchableTree assetLi private LabeledCheckBox CheckboxIncludeTextNodesInHover { get; } - private LabeledCheckBox CheckboxViewClickedComponentInDebugger { get; } + private LabeledCheckBox CheckboxViewClickedNodeInDebugger { get; } private Button ButtonShutdownServer { get; } @@ -166,9 +484,6 @@ private Button CreateAssetsButtonReloadAsset(Table table, SearchableTree assetLi protected override void EnsureInitialized() { - TableDebugStats.SizeToChildren(); - - LoadJsonUi(UI.Debug, Graphics.Renderer?.GetResolutionString()); } protected override void OnAttached(Base parent) @@ -210,18 +525,20 @@ private LabeledCheckBox CreateInfoCheckboxDrawDebugOutlines(Base parent) Text = Strings.Debug.DrawDebugOutlines, }; - checkbox.CheckChanged += (_, _) => - { - _drawDebugOutlinesEnabled = checkbox.IsChecked; - if (Root is { } root) - { - root.DrawDebugOutlines = _drawDebugOutlinesEnabled; - } - }; + checkbox.CheckChanged += CheckboxDrawDebugOutlinesOnCheckChanged; return checkbox; } + private void CheckboxDrawDebugOutlinesOnCheckChanged(ICheckbox sender, ValueChangedEventArgs eventArgs) + { + _drawDebugOutlinesEnabled = eventArgs.Value; + if (Root is { } root) + { + root.DrawDebugOutlines = _drawDebugOutlinesEnabled; + } + } + private LabeledCheckBox CreateInfoCheckboxEnableLayoutHotReloading(Base parent) { var checkbox = new LabeledCheckBox(parent, nameof(CheckboxEnableLayoutHotReloading)) @@ -233,7 +550,7 @@ private LabeledCheckBox CreateInfoCheckboxEnableLayoutHotReloading(Base parent) TextColorOverride = Color.Yellow, }; - checkbox.CheckChanged += (_, _) => Globals.ContentManager.ContentWatcher.Enabled = checkbox.IsChecked; + checkbox.CheckChanged += CheckboxEnableLayoutHotReloadOnCheckChanged; checkbox.SetToolTipText(Strings.Internals.ExperimentalFeatureTooltip); checkbox.TooltipFont = Skin.DefaultFont; @@ -241,6 +558,11 @@ private LabeledCheckBox CreateInfoCheckboxEnableLayoutHotReloading(Base parent) return checkbox; } + private static void CheckboxEnableLayoutHotReloadOnCheckChanged(ICheckbox sender, ValueChangedEventArgs args) + { + Current.ContentWatcher.Enabled = args.Value; + } + private LabeledCheckBox CreateInfoCheckboxIncludeTextNodesInHover(Base parent) { var checkbox = new LabeledCheckBox(parent, nameof(CheckboxIncludeTextNodesInHover)) @@ -251,24 +573,26 @@ private LabeledCheckBox CreateInfoCheckboxIncludeTextNodesInHover(Base parent) Text = Strings.Debug.IncludeTextNodesInHover, }; - checkbox.CheckChanged += (_, _) => - { - if (_nodeUnderCursorProvider.Filter.HasFlag(NodeFilter.IncludeText)) - { - _nodeUnderCursorProvider.Filter &= ~NodeFilter.IncludeText; - } - else - { - _nodeUnderCursorProvider.Filter |= NodeFilter.IncludeText; - } - }; + checkbox.CheckChanged += CheckboxIncludesTextNodesInHoverOnCheckChanged; return checkbox; } + private void CheckboxIncludesTextNodesInHoverOnCheckChanged(ICheckbox checkbox, ValueChangedEventArgs eventArgs) + { + if (eventArgs.Value) + { + _nodeUnderCursorProvider.Filter |= NodeFilter.IncludeText; + } + else + { + _nodeUnderCursorProvider.Filter &= ~NodeFilter.IncludeText; + } + } + private LabeledCheckBox CreateInfoCheckboxViewClickedNodeInDebugger(Base parent) { - var checkbox = new LabeledCheckBox(parent, nameof(CheckboxViewClickedComponentInDebugger)) + var checkbox = new LabeledCheckBox(parent, nameof(CheckboxViewClickedNodeInDebugger)) { Dock = Pos.Top, Font = _defaultFont, @@ -277,20 +601,27 @@ private LabeledCheckBox CreateInfoCheckboxViewClickedNodeInDebugger(Base parent) TooltipText = Strings.Debug.ViewClickedNodeInDebuggerTooltip, }; - checkbox.CheckChanged += (_, _) => + checkbox.CheckChanged += CheckboxViewClickedNodeInDebuggerOnCheckChanged; + + return checkbox; + } + + private void CheckboxViewClickedNodeInDebuggerOnCheckChanged(ICheckbox checkbox, ValueChangedEventArgs eventArgs) + { + var wasClicked = _viewClickedNodeInDebugger; + + _viewClickedNodeInDebugger = eventArgs.Value; + if (_viewClickedNodeInDebugger) { - _viewClickedNodeInDebugger = !_viewClickedNodeInDebugger; - if (_viewClickedNodeInDebugger) + if (!wasClicked) { AddIntercepts(); } - else - { - RemoveIntercepts(); - } - }; - - return checkbox; + } + else if (wasClicked) + { + RemoveIntercepts(); + } } private void AddIntercepts() @@ -307,7 +638,7 @@ private void RemoveIntercepts() private bool MouseDownIntercept(Keys modifier, MouseButton mouseButton) { - if (IsVisible && _viewClickedNodeInDebugger) + if (IsVisibleInTree && _viewClickedNodeInDebugger) { return true; } @@ -318,7 +649,7 @@ private bool MouseDownIntercept(Keys modifier, MouseButton mouseButton) private bool MouseUpIntercept(Keys modifier, MouseButton mouseButton) { - if (!IsVisible || !_viewClickedNodeInDebugger) + if (!IsVisibleInTree || !_viewClickedNodeInDebugger) { RemoveIntercepts(); return false; @@ -384,13 +715,10 @@ private Table CreateInfoTableDebugStats(Base parent) Dock = Pos.Fill, Font = _defaultFont, }; - table.SizeChanged += (_, args) => - { - table.SizeToChildren(resizeX: args.Value.X > args.OldValue.X, resizeY: true, recursive: true); - }; + table.SizeChanged += ResizeTableToChildrenOnSizeChanged; - table.AddRow(Strings.Debug.Fps, name: "FPSRow").Listen(1, new DelegateDataProvider(() => Graphics.Renderer?.Fps), NoValue); - // table.AddRow(Strings.Debug.Draws, name: "DrawsRow").Listen(1, new DelegateDataProvider(() => Graphics.DrawCalls), NoValue); + table.AddRow(Strings.Debug.Fps, name: "FPSRow").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.FPS), NoValue); + table.AddRow(Strings.Debug.Draws, name: "DrawsRow").Listen(1, new DelegateDataProvider(() => Graphics.DrawCalls), NoValue); table.AddRow(Strings.Debug.Ping, name: "PingRow").Listen(1, new DelegateDataProvider(() => Networking.Network.Ping, delayMilliseconds: 5000), NoValue); table.AddRow(Strings.Debug.Map, name: "MapRow").Listen(1, new DelegateDataProvider(() => MapInstance.TryGet(Globals.Me?.MapId ?? default, out var mapInstance) ? mapInstance.Name : default), NoValue); @@ -406,17 +734,34 @@ private Table CreateInfoTableDebugStats(Base parent) table.AddRow(Strings.Debug.LightsDrawn, name: "LightsDrawnRow").Listen(1, new DelegateDataProvider(() => Graphics.LightsDrawn), NoValue); table.AddRow(Strings.Debug.InterfaceObjects, name: "InterfaceObjectsRow").Listen(1, new DelegateDataProvider(() => Interface.CurrentInterface?.NodeCount, delayMilliseconds: 1000), NoValue); - _ = table.AddRow(Strings.Debug.SectionGPUStatistics, columnCount: 2, name: "SectionGPU", columnIndex: 1); + var rowSectionGPU = table.AddRow(Strings.Debug.SectionGPUStatistics, columnCount: 2, name: "SectionGPU", columnIndex: 0); + if (rowSectionGPU.GetCellContents(0) is Label labelSectionGPU) + { + labelSectionGPU.TextColorOverride = new Color(r: 191, g: 255, b: 191); + } + + table.AddRow(Strings.Debug.RenderTargetAllocations, name: "GPURenderTargetAllocations").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.RenderTargetAllocations), NoValue); + table.AddRow(Strings.Debug.TextureAllocations, name: "GPUTextureAllocations").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.TextureAllocations), NoValue); + table.AddRow(Strings.Debug.TextureCount, name: "GPUTextureCount").Listen(1, new DelegateDataProvider(() => Graphics.Renderer.TextureCount), NoValue); + + table.AddRow(Strings.Debug.FreeVRAM, name: "GPUFreeVRAM").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.AvailableGPUMemory)), NoValue); + table.AddRow(Strings.Debug.TotalVRAM, name: "GPUTotalVRAM").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.TotalGPUMemory)), NoValue); - table.AddRow(Strings.Debug.RenderBufferVRAMFree, name: "GPUVRAMRenderBuffers").Listen(1, new DelegateDataProvider(() => Strings.FormatBytes(GL.AvailableRenderBufferMemory)), NoValue); - table.AddRow(Strings.Debug.TextureVRAMFree, name: "GPUVRAMTextures").Listen(1, new DelegateDataProvider(() => Strings.FormatBytes(GL.AvailableTextureMemory)), NoValue); - table.AddRow(Strings.Debug.VBOVRAMFree, name: "GPUVRAMVBOs").Listen(1, new DelegateDataProvider(() => Strings.FormatBytes(GL.AvailableVBOMemory)), NoValue); + table.AddRow(Strings.Debug.FreeVirtualRAM, name: "RAMFreeVirtual").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.AvailableSystemMemory)), NoValue); + table.AddRow(Strings.Debug.TotalVirtualRAM, name: "RAMTotalVirtual").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.TotalSystemMemory)), NoValue); - _ = table.AddRow(Strings.Debug.ControlUnderCursor, columnCount: 2, name: "SectionUI", columnIndex: 1); + table.AddRow(Strings.Debug.FreePhysicalRAM, name: "RAMFreePhysical").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.AvailablePhysicalMemory)), NoValue); + table.AddRow(Strings.Debug.TotalPhysicalRAM, name: "RAMTotalPhysical").Listen(1, new DelegateDataProvider(() => FileSystemHelper.FormatSize(PlatformStatistics.TotalPhysicalMemory)), NoValue); + + var rowSectionUI = table.AddRow(Strings.Debug.ControlUnderCursor, columnCount: 2, name: "SectionUI", columnIndex: 0); + if (rowSectionUI.GetCellContents(0) is Label labelSectionUI) + { + labelSectionUI.TextColorOverride = new Color(r: 191, g: 255, b: 191); + } table.AddRow(Strings.Internals.Type, name: "TypeRow").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.GetType().GetName(), Strings.Internals.NotApplicable); table.AddRow(Strings.Internals.Name, name: "NameRow").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.ParentQualifiedName, NoValue); - table.AddRow(Strings.Internals.IsVisible, name: "IsVisible").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.IsVisible, NoValue); + table.AddRow(Strings.Internals.IsVisibleInTree, name: "IsVisible").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.IsVisibleInTree, NoValue); table.AddRow(Strings.Internals.IsVisibleInParent, name: "IsVisibleInParent").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.IsVisibleInParent, NoValue); table.AddRow(Strings.Internals.IsDisabled, name: "IsDisabled").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.IsDisabled, NoValue); table.AddRow(Strings.Internals.IsDisabledByTree, name: "IsDisabledByTree").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.IsDisabledByTree, NoValue); diff --git a/Intersect.Client.Core/Interface/Game/Admin/AdminWindow.cs b/Intersect.Client.Core/Interface/Game/Admin/AdminWindow.cs index 6c8e05a466..220e33f9f8 100644 --- a/Intersect.Client.Core/Interface/Game/Admin/AdminWindow.cs +++ b/Intersect.Client.Core/Interface/Game/Admin/AdminWindow.cs @@ -495,7 +495,7 @@ private void MuteButtonOnClicked(Base sender, MouseButtonState arguments) ); } - private void MapSortCheckboxOnCheckChanged(Base @base, EventArgs eventArgs) + private void MapSortCheckboxOnCheckChanged(ICheckbox sender, ValueChangedEventArgs eventArgs) { UpdateMapList(); } diff --git a/Intersect.Client.Core/Interface/Game/Admin/BanMuteBox.cs b/Intersect.Client.Core/Interface/Game/Admin/BanMuteBox.cs index fd95ca360a..c2b10bc919 100644 --- a/Intersect.Client.Core/Interface/Game/Admin/BanMuteBox.cs +++ b/Intersect.Client.Core/Interface/Game/Admin/BanMuteBox.cs @@ -120,12 +120,12 @@ public BanMuteBox(string title, string prompt, EventHandler okayHandler) : base( _ = richLabelPrompt.SizeToChildren(false, true); } - public override void Dispose() + protected override void Dispose(bool disposing) { Close(); Interface.GameUi.GameCanvas.RemoveChild(this, false); - base.Dispose(); - GC.SuppressFinalize(this); + + base.Dispose(disposing); } public int GetDuration() diff --git a/Intersect.Client.Core/Interface/Game/Admin/TexturePicker.cs b/Intersect.Client.Core/Interface/Game/Admin/TexturePicker.cs index 221b818d9d..14a9f98773 100644 --- a/Intersect.Client.Core/Interface/Game/Admin/TexturePicker.cs +++ b/Intersect.Client.Core/Interface/Game/Admin/TexturePicker.cs @@ -124,7 +124,7 @@ private void SubmitButtonOnClicked(Base sender, MouseButtonState arguments) public bool AllowNone { get => _allowNone; - set => SetAndDoIfChanged(ref _allowNone, value, UpdateNone); + set => SetAndDoIfChanged(ref _allowNone, value, UpdateNone, this); } public GameFont? ButtonFont @@ -170,7 +170,7 @@ public string? LabelText public TextureType TextureType { get => _textureType; - set => SetAndDoIfChanged(ref _textureType, value, InvalidateSelector); + set => SetAndDoIfChanged(ref _textureType, value, InvalidateSelector, this); } string? ITextContainer.Text @@ -181,27 +181,29 @@ public TextureType TextureType public Color? TextPaddingDebugColor { get; set; } - private void UpdateNone() + private static void UpdateNone(TexturePicker @this) { - if (_allowNone) + if (@this._allowNone) { - _noneItem ??= AddNone(); + @this._noneItem ??= @this.AddNone(); } - else if (_noneItem is not null) + else if (@this._noneItem is not null) { - RemoveChild(_noneItem, dispose: true); - _noneItem = null; + @this.RemoveChild(@this._noneItem, dispose: true); + @this._noneItem = null; } } - public void InvalidateSelector() + public void InvalidateSelector() => RunOnMainThread(InvalidateSelector, this); + + private static void InvalidateSelector(TexturePicker @this) { - if (!_selectorDirty) + if (!@this._selectorDirty) { - _selectorDirty = true; + @this._selectorDirty = true; } - Invalidate(); + @this.Invalidate(); } private MenuItem AddNone() @@ -223,7 +225,7 @@ protected override void Layout(Framework.Gwen.Skin.Base skin) _noneItem = null; _textureSelector.ClearItems(); - UpdateNone(); + UpdateNone(this); foreach (var textureName in textureNames) { diff --git a/Intersect.Client.Core/Interface/Game/AnnouncementWindow.cs b/Intersect.Client.Core/Interface/Game/AnnouncementWindow.cs index 2b73e26726..89bec69386 100644 --- a/Intersect.Client.Core/Interface/Game/AnnouncementWindow.cs +++ b/Intersect.Client.Core/Interface/Game/AnnouncementWindow.cs @@ -1,6 +1,7 @@ using Intersect.Client.Core; using Intersect.Client.Framework.File_Management; using Intersect.Client.Framework.Gwen.Control; +using Intersect.Framework.Core; using Intersect.Utilities; namespace Intersect.Client.Interface.Game; diff --git a/Intersect.Client.Core/Interface/Game/Bag/BagItem.cs b/Intersect.Client.Core/Interface/Game/Bag/BagItem.cs index 55b4fa8091..f47442fd84 100644 --- a/Intersect.Client.Core/Interface/Game/Bag/BagItem.cs +++ b/Intersect.Client.Core/Interface/Game/Bag/BagItem.cs @@ -7,6 +7,7 @@ using Intersect.Client.Interface.Game.DescriptionWindows; using Intersect.Client.Networking; using Intersect.Configuration; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; @@ -129,11 +130,11 @@ void pnl_HoverEnter(Base sender, EventArgs arguments) mDescWindow = null; } - if (Globals.Bag[mMySlot]?.Base != null) + if (Globals.BagSlots[mMySlot]?.Base != null) { mDescWindow = new ItemDescriptionWindow( - Globals.Bag[mMySlot].Base, Globals.Bag[mMySlot].Quantity, mBagWindow.X, mBagWindow.Y, - Globals.Bag[mMySlot].ItemProperties + Globals.BagSlots[mMySlot].Base, Globals.BagSlots[mMySlot].Quantity, mBagWindow.X, mBagWindow.Y, + Globals.BagSlots[mMySlot].ItemProperties ); } } @@ -153,10 +154,10 @@ public FloatRect RenderBounds() public void Update() { - if (Globals.Bag[mMySlot].ItemId != mCurrentItemId) + if (Globals.BagSlots[mMySlot].ItemId != mCurrentItemId) { - mCurrentItemId = Globals.Bag[mMySlot].ItemId; - var item = ItemBase.Get(Globals.Bag[mMySlot].ItemId); + mCurrentItemId = Globals.BagSlots[mMySlot].ItemId; + var item = ItemBase.Get(Globals.BagSlots[mMySlot].ItemId); if (item != null) { var itemTex = Globals.ContentManager.GetTexture(Framework.Content.TextureType.Item, item.Icon); @@ -244,7 +245,7 @@ public void Update() //Check inventory first. if (mBagWindow.RenderBounds().IntersectsWith(dragRect)) { - for (var i = 0; i < Globals.Bag.Length; i++) + for (var i = 0; i < Globals.BagSlots.Length; i++) { if (mBagWindow.Items[i].RenderBounds().IntersectsWith(dragRect)) { diff --git a/Intersect.Client.Core/Interface/Game/Bag/BagWindow.cs b/Intersect.Client.Core/Interface/Game/Bag/BagWindow.cs index e68348dd6e..c754816931 100644 --- a/Intersect.Client.Core/Interface/Game/Bag/BagWindow.cs +++ b/Intersect.Client.Core/Interface/Game/Bag/BagWindow.cs @@ -49,7 +49,7 @@ public BagWindow(Canvas gameCanvas) mContextMenu.IsHidden = true; mContextMenu.IconMarginDisabled = true; //TODO: Is this a memory leak? - mContextMenu.Children.Clear(); + mContextMenu.ClearChildren(); mWithdrawContextItem = mContextMenu.AddItem(Strings.BagContextMenu.Withdraw); mWithdrawContextItem.Clicked += MWithdrawContextItem_Clicked; mContextMenu.LoadJsonUi(GameContentManager.UI.InGame, Graphics.Renderer.GetResolutionString()); @@ -57,7 +57,7 @@ public BagWindow(Canvas gameCanvas) public void OpenContextMenu(int slot) { - var item = ItemBase.Get(Globals.Bag[slot].ItemId); + var item = ItemBase.Get(Globals.BagSlots[slot].ItemId); // No point showing a menu for blank space. if (item == null) @@ -107,24 +107,24 @@ public void Hide() public void Update() { - if (mBagWindow.IsHidden == true || Globals.Bag == null) + if (mBagWindow.IsHidden == true || Globals.BagSlots == null) { return; } - for (var i = 0; i < Globals.Bag.Length; i++) + for (var i = 0; i < Globals.BagSlots.Length; i++) { - if (Globals.Bag[i] != null && Globals.Bag[i].ItemId != Guid.Empty) + if (Globals.BagSlots[i] != null && Globals.BagSlots[i].ItemId != Guid.Empty) { - var item = ItemBase.Get(Globals.Bag[i].ItemId); + var item = ItemBase.Get(Globals.BagSlots[i].ItemId); if (item != null) { Items[i].Pnl.IsHidden = false; if (item.IsStackable) { - mValues[i].IsHidden = Globals.Bag[i].Quantity <= 1; - mValues[i].Text = Strings.FormatQuantityAbbreviated(Globals.Bag[i].Quantity); + mValues[i].IsHidden = Globals.BagSlots[i].Quantity <= 1; + mValues[i].Text = Strings.FormatQuantityAbbreviated(Globals.BagSlots[i].Quantity); } else { @@ -150,7 +150,7 @@ public void Update() private void InitItemContainer() { - for (var i = 0; i < Globals.Bag.Length; i++) + for (var i = 0; i < Globals.BagSlots.Length; i++) { Items.Add(new BagItem(this, i)); Items[i].Container = new ImagePanel(mItemContainer, "BagItem"); diff --git a/Intersect.Client.Core/Interface/Game/Bank/BankItem.cs b/Intersect.Client.Core/Interface/Game/Bank/BankItem.cs index 73e289039c..6da7edf3dc 100644 --- a/Intersect.Client.Core/Interface/Game/Bank/BankItem.cs +++ b/Intersect.Client.Core/Interface/Game/Bank/BankItem.cs @@ -10,6 +10,7 @@ using Intersect.Client.Networking; using Intersect.Configuration; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; @@ -81,7 +82,7 @@ private void Pnl_DoubleClicked(Base sender, MouseButtonState arguments) } else { - var slot = Globals.Bank[mMySlot]; + var slot = Globals.BankSlots[mMySlot]; Globals.Me.TryRetrieveItemFromBank( mMySlot, slot, @@ -147,11 +148,11 @@ void pnl_HoverEnter(Base sender, EventArgs arguments) mDescWindow = null; } - if (Globals.Bank[mMySlot]?.Base != null) + if (Globals.BankSlots[mMySlot]?.Base != null) { mDescWindow = new ItemDescriptionWindow( - Globals.Bank[mMySlot].Base, Globals.Bank[mMySlot].Quantity, mBankWindow.X, mBankWindow.Y, - Globals.Bank[mMySlot].ItemProperties + Globals.BankSlots[mMySlot].Base, Globals.BankSlots[mMySlot].Quantity, mBankWindow.X, mBankWindow.Y, + Globals.BankSlots[mMySlot].ItemProperties ); } } @@ -171,10 +172,10 @@ public FloatRect RenderBounds() public void Update() { - if (Globals.Bank[mMySlot].ItemId != mCurrentItemId) + if (Globals.BankSlots[mMySlot].ItemId != mCurrentItemId) { - mCurrentItemId = Globals.Bank[mMySlot].ItemId; - var item = ItemBase.Get(Globals.Bank[mMySlot].ItemId); + mCurrentItemId = Globals.BankSlots[mMySlot].ItemId; + var item = ItemBase.Get(Globals.BankSlots[mMySlot].ItemId); if (item != null) { var itemTex = Globals.ContentManager.GetTexture(Framework.Content.TextureType.Item, item.Icon); @@ -263,7 +264,7 @@ public void Update() if (mBankWindow.RenderBounds().IntersectsWith(dragRect)) { var bankSlotComponents = mBankWindow.Items.ToArray(); - var bankSlotLimit = Math.Min(Globals.BankSlots, bankSlotComponents.Length); + var bankSlotLimit = Math.Min(Globals.BankSlotCount, bankSlotComponents.Length); for (var bankSlotIndex = 0; bankSlotIndex < bankSlotLimit; bankSlotIndex++) { var bankSlotComponent = bankSlotComponents[bankSlotIndex]; @@ -290,7 +291,7 @@ public void Update() var allowed = true; //Permission Check - if (Globals.GuildBank) + if (Globals.IsGuildBank) { var rank = Globals.Me.GuildRank; if (string.IsNullOrWhiteSpace(Globals.Me.Guild) || @@ -347,7 +348,7 @@ public void Update() if (bestIntersectIndex > -1) { - var slot = Globals.Bank[mMySlot]; + var slot = Globals.BankSlots[mMySlot]; Globals.Me.TryRetrieveItemFromBank( mMySlot, inventorySlotIndex: bestIntersectIndex, diff --git a/Intersect.Client.Core/Interface/Game/Bank/BankWindow.cs b/Intersect.Client.Core/Interface/Game/Bank/BankWindow.cs index c7a4f26706..58ece3b764 100644 --- a/Intersect.Client.Core/Interface/Game/Bank/BankWindow.cs +++ b/Intersect.Client.Core/Interface/Game/Bank/BankWindow.cs @@ -42,7 +42,7 @@ public BankWindow(Canvas gameCanvas) { // Create a new window to display the contents of the bank. mBankWindow = new WindowControl(gameCanvas, - Globals.GuildBank + Globals.IsGuildBank ? Strings.Guilds.Bank.ToString(Globals.Me?.Guild) : Strings.Bank.Title.ToString(), false, "BankWindow"); @@ -65,7 +65,7 @@ public BankWindow(Canvas gameCanvas) mContextMenu.IconMarginDisabled = true; // Clear the children of the context menu and add a "Withdraw" option. - mContextMenu.Children.Clear(); + mContextMenu.ClearChildren(); mWithdrawContextItem = mContextMenu.AddItem(Strings.BankContextMenu.Withdraw); mWithdrawContextItem.Clicked += MWithdrawContextItem_Clicked; mContextMenu.LoadJsonUi(GameContentManager.UI.InGame, Graphics.Renderer.GetResolutionString()); @@ -76,7 +76,7 @@ public BankWindow(Canvas gameCanvas) public void OpenContextMenu(int slot) { - var item = ItemBase.Get(Globals.Bank[slot].ItemId); + var item = ItemBase.Get(Globals.BankSlots[slot].ItemId); // No point showing a menu for blank space. if (item == null) @@ -109,7 +109,7 @@ public void Close() public void Open() { // Hide unavailable bank slots - var currentBankSlots = Math.Max(0, Globals.BankSlots); + var currentBankSlots = Math.Max(0, Globals.BankSlotCount); // For any slot beyond the current bank's maximum slots for (var i = currentBankSlots; i < Options.Instance.Bank.MaxSlots; i++) { @@ -146,11 +146,11 @@ public void Update() X = mBankWindow.X; Y = mBankWindow.Y; - for (var i = 0; i < Math.Min(Globals.BankSlots, Options.Instance.Bank.MaxSlots); i++) + for (var i = 0; i < Math.Min(Globals.BankSlotCount, Options.Instance.Bank.MaxSlots); i++) { var bankItem = Items[i]; var bankLabel = mValues[i]; - var globalBankItem = Globals.Bank[i]; + var globalBankItem = Globals.BankSlots[i]; bankItem.Container.Show(); SetItemPosition(i); @@ -200,7 +200,7 @@ private void InitItemContainer() bankLabel.Text = string.Empty; bankItem.Container.LoadJsonUi(GameContentManager.UI.InGame, Graphics.Renderer.GetResolutionString()); - + Items.Add(bankItem); mValues.Add(bankLabel); } diff --git a/Intersect.Client.Core/Interface/Game/Character/CharacterWindow.cs b/Intersect.Client.Core/Interface/Game/Character/CharacterWindow.cs index a1493a0817..90ad84f55c 100644 --- a/Intersect.Client.Core/Interface/Game/Character/CharacterWindow.cs +++ b/Intersect.Client.Core/Interface/Game/Character/CharacterWindow.cs @@ -222,7 +222,7 @@ public void Update() ); //Load Portrait - //UNCOMMENT THIS LINE IF YOU'D RATHER HAVE A FACE HERE GameTexture faceTex = Globals.ContentManager.GetTexture(Framework.Content.TextureType.Face, Globals.Me.Face); + //UNCOMMENT THIS LINE IF YOU'D RATHER HAVE A FACE HERE IGameTexture faceTex = Globals.ContentManager.GetTexture(Framework.Content.TextureType.Face, Globals.Me.Face); var entityTex = Globals.ContentManager.GetTexture( Framework.Content.TextureType.Entity, Globals.Me.Sprite ); diff --git a/Intersect.Client.Core/Interface/Game/Chat/Chatbox.cs b/Intersect.Client.Core/Interface/Game/Chat/Chatbox.cs index 64aab3c569..0c2c7edc0e 100644 --- a/Intersect.Client.Core/Interface/Game/Chat/Chatbox.cs +++ b/Intersect.Client.Core/Interface/Game/Chat/Chatbox.cs @@ -14,6 +14,7 @@ using Intersect.Configuration; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Localization; using Intersect.Utilities; using Microsoft.Extensions.Logging; @@ -42,7 +43,7 @@ public partial class Chatbox private Button _chatboxClearLogButton; - private readonly GameTexture _chatboxTexture; + private readonly IGameTexture _chatboxTexture; private Label mChatboxText; @@ -189,7 +190,7 @@ public Chatbox(Canvas gameCanvas, GameInterface gameUi) mContextMenu.IsHidden = true; mContextMenu.IconMarginDisabled = true; //TODO: Is this a memory leak? - mContextMenu.Children.Clear(); + mContextMenu.ClearChildren(); mPMContextItem = mContextMenu.AddItem(Strings.ChatContextMenu.PM); mPMContextItem.Clicked += MPMContextItem_Clicked; mFriendInviteContextItem = mContextMenu.AddItem(Strings.ChatContextMenu.FriendInvite); @@ -210,7 +211,7 @@ public void OpenContextMenu(string name) mContextMenu.RemoveChild(mFriendInviteContextItem, false); mContextMenu.RemoveChild(mPartyInviteContextItem, false); mContextMenu.RemoveChild(mGuildInviteContextItem, false); - mContextMenu.Children.Clear(); + mContextMenu.ClearChildren(); // No point showing a menu for blank space. if (string.IsNullOrWhiteSpace(name)) @@ -415,18 +416,23 @@ private void Update() if (scrollToBottom) { - mChatboxMessages.Defer(mChatboxMessages.ScrollToBottom); + mChatboxMessages.PostLayout.Enqueue(scroller => scroller.ScrollToBottom(), mChatboxMessages); + mChatboxMessages.RunOnMainThread(mChatboxMessages.ScrollToBottom); } else { - mChatboxMessages.Defer(() => + mChatboxMessages.PostLayout.Enqueue( + (scroller, position) => { ApplicationContext.CurrentContext.Logger.LogTrace( - "Scrolling chat to {ScrollY}", - scrollPosition + "Scrolling chat ({ChatNode}) to {ScrollY}", + scroller.CanonicalName, + position ); - mChatboxMessages.ScrollToY(scrollPosition); - } + scroller.ScrollToY(position); + }, + mChatboxMessages, + scrollPosition ); } diff --git a/Intersect.Client.Core/Interface/Game/Crafting/CraftingWindow.cs b/Intersect.Client.Core/Interface/Game/Crafting/CraftingWindow.cs index b18fc70cc0..9413d17149 100644 --- a/Intersect.Client.Core/Interface/Game/Crafting/CraftingWindow.cs +++ b/Intersect.Client.Core/Interface/Game/Crafting/CraftingWindow.cs @@ -9,6 +9,7 @@ using Intersect.Client.Networking; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Reflection; using Intersect.GameObjects; using Intersect.GameObjects.Crafting; @@ -452,9 +453,9 @@ private void craftAll_Clicked(Base sender, MouseButtonState arguments) } } - protected override void Prelayout(Framework.Gwen.Skin.Base skin) + protected override void DoPrelayout(Framework.Gwen.Skin.Base skin) { - base.Prelayout(skin); + base.DoPrelayout(skin); if (IsCrafting) { diff --git a/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/ComponentBase.cs b/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/ComponentBase.cs index 5beb2e8d33..0f9797ceb3 100644 --- a/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/ComponentBase.cs +++ b/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/ComponentBase.cs @@ -35,7 +35,7 @@ protected virtual void GenerateComponents() /// /// Is this component current visible? /// - public bool IsVisible => mContainer.IsVisible; + public bool IsVisible => mContainer.IsVisibleInTree; /// /// The current X location of the control. diff --git a/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/HeaderComponent.cs b/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/HeaderComponent.cs index 0fadf052c9..0648ae4114 100644 --- a/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/HeaderComponent.cs +++ b/Intersect.Client.Core/Interface/Game/DescriptionWindows/Components/HeaderComponent.cs @@ -35,9 +35,9 @@ protected override void GenerateComponents() /// /// Set the icon on this header. /// - /// The to use for display purposes. + /// The to use for display purposes. /// The to use to display the texture. - public void SetIcon(GameTexture texture, Color color) + public void SetIcon(IGameTexture texture, Color color) { mIcon.Texture = texture; mIcon.RenderColor = color; diff --git a/Intersect.Client.Core/Interface/Game/Draggable.cs b/Intersect.Client.Core/Interface/Game/Draggable.cs index 856e5316f0..04d7b64810 100644 --- a/Intersect.Client.Core/Interface/Game/Draggable.cs +++ b/Intersect.Client.Core/Interface/Game/Draggable.cs @@ -16,7 +16,7 @@ partial class Draggable ImagePanel mPnl; - public Draggable(int x, int y, GameTexture tex, Color color) + public Draggable(int x, int y, IGameTexture tex, Color color) { mPnl = new ImagePanel(Interface.GameUi.GameCanvas, "Draggable"); mPnl.LoadJsonUi(GameContentManager.UI.InGame, Graphics.Renderer.GetResolutionString()); diff --git a/Intersect.Client.Core/Interface/Game/EntityPanel/EntityBox.cs b/Intersect.Client.Core/Interface/Game/EntityPanel/EntityBox.cs index 2a92cff49b..62ee1285eb 100644 --- a/Intersect.Client.Core/Interface/Game/EntityPanel/EntityBox.cs +++ b/Intersect.Client.Core/Interface/Game/EntityPanel/EntityBox.cs @@ -10,6 +10,7 @@ using Intersect.Configuration; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; using Microsoft.Extensions.Logging; @@ -892,14 +893,13 @@ private void UpdateImage() public void Dispose() { EntityWindow.Hide(); - Interface.GameUi.GameCanvas.RemoveChild(EntityWindow, false); - EntityWindow.Dispose(); + EntityWindow.Canvas?.RemoveChild(EntityWindow, true); } public bool IsVisible { - get => EntityWindow.IsVisible; - set => EntityWindow.IsVisible = value; + get => EntityWindow.IsVisibleInTree; + set => EntityWindow.IsVisibleInTree = value; } public void Hide() diff --git a/Intersect.Client.Core/Interface/Game/EntityPanel/PlayerStatusWindow.cs b/Intersect.Client.Core/Interface/Game/EntityPanel/PlayerStatusWindow.cs index 7632009a82..53c36d74ff 100644 --- a/Intersect.Client.Core/Interface/Game/EntityPanel/PlayerStatusWindow.cs +++ b/Intersect.Client.Core/Interface/Game/EntityPanel/PlayerStatusWindow.cs @@ -54,14 +54,14 @@ public void Update() } ShouldUpdateStatuses = _activeStatuses.Count > 0; - if (!IsVisible) + if (!IsVisibleInTree) { - IsVisible = ShouldUpdateStatuses; + IsVisibleInTree = ShouldUpdateStatuses; } } - else if (IsVisible) + else if (IsVisibleInTree) { - IsVisible = false; + IsVisibleInTree = false; } } } diff --git a/Intersect.Client.Core/Interface/Game/EscapeMenu.cs b/Intersect.Client.Core/Interface/Game/EscapeMenu.cs index 940e004d85..a2908f2af3 100644 --- a/Intersect.Client.Core/Interface/Game/EscapeMenu.cs +++ b/Intersect.Client.Core/Interface/Game/EscapeMenu.cs @@ -5,6 +5,7 @@ using Intersect.Client.General; using Intersect.Client.Interface.Shared; using Intersect.Client.Localization; +using Intersect.Framework.Core; using Intersect.Utilities; namespace Intersect.Client.Interface.Game; @@ -91,7 +92,7 @@ public override void Invalidate() base.Invalidate(); if (Interface.GameUi?.GameCanvas != null) { - Interface.GameUi.GameCanvas.MouseInputEnabled = IsVisible; + Interface.GameUi.GameCanvas.MouseInputEnabled = IsVisibleInTree; } } @@ -99,7 +100,7 @@ public override void Invalidate() public override void ToggleHidden() { var settingsWindow = _settingsWindowProvider(); - if (settingsWindow.IsVisible) + if (settingsWindow.IsVisibleInTree) { return; } diff --git a/Intersect.Client.Core/Interface/Game/EventWindow.cs b/Intersect.Client.Core/Interface/Game/EventWindow.cs index 81abf006cf..f707f0df93 100644 --- a/Intersect.Client.Core/Interface/Game/EventWindow.cs +++ b/Intersect.Client.Core/Interface/Game/EventWindow.cs @@ -15,6 +15,7 @@ using Intersect.Core; using Intersect.Enums; using Intersect.Framework.Core; +using Intersect.Framework.Threading; using Intersect.Utilities; using Microsoft.Extensions.Logging; @@ -42,6 +43,8 @@ public partial class EventWindow : Panel private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof(EventWindow)) { + ThreadQueue.Default.ThrowIfNotOnMainThread(); + _dialog = dialog; _defaultFont = GameContentManager.Current.GetFont(name: "sourcesansproblack", 12); @@ -84,7 +87,10 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( _optionButtons[optionIndex] = optionButton; } - _optionsPanel.SizeToChildren(recursive: true); + _optionsPanel.PostLayout.Enqueue( + ResizeOptionsPanelToChildren, + _optionsPanel + ); _faceImage = new ImagePanel(_promptPanel, nameof(_faceImage)) { @@ -103,7 +109,7 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( _promptTemplateLabel = new Label(_promptScroller, nameof(_promptTemplateLabel)) { Font = _defaultFont, - IsVisible = false, + IsVisibleInTree = false, }; _promptLabel = new RichLabel(_promptScroller, nameof(_promptLabel)) @@ -115,7 +121,6 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( _promptPanel.SizeToChildren(recursive: true); - #region Configure and Display if (_dialog.Face is { } faceTextureName) @@ -124,18 +129,18 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( _faceImage.Texture = faceTexture; if (faceTexture is not null) { - _faceImage.IsVisible = true; + _faceImage.IsVisibleInTree = true; _faceImage.SizeToContents(); } else { - _faceImage.IsVisible = false; + _faceImage.IsVisibleInTree = false; } } else { _faceImage.Texture = null; - _faceImage.IsVisible = false; + _faceImage.IsVisibleInTree = false; } var visibleOptions = _dialog.Options.Where(option => !string.IsNullOrEmpty(option)).ToArray(); @@ -150,11 +155,11 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( if (optionIndex < visibleOptions.Length) { optionButton.Text = visibleOptions[optionIndex]; - optionButton.IsVisible = true; + optionButton.IsVisibleInTree = true; } else { - optionButton.IsVisible = false; + optionButton.IsVisibleInTree = false; } } @@ -180,18 +185,22 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( if (_typewriting) { _promptLabel.ClearText(); - _writer = new Typewriter(parsedText.ToArray(), (text, color) => - { - _promptLabel.AppendText(text, color, Alignments.Left, _promptTemplateLabel.Font); - }); + _writer = new Typewriter( + parsedText.ToArray(), + (text, color) => + { + _promptLabel.AppendText(text, color, Alignments.Left, _promptTemplateLabel.Font); + } + ); } - Defer( - () => - { - SizeToChildren(recursive: true); - _promptScroller.ScrollToTop(); - } + RunOnMainThread( + static @this => + { + @this.SizeToChildren(recursive: true); + @this._promptScroller.ScrollToTop(); + }, + this ); MakeModal(dim: true); @@ -202,6 +211,11 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( #endregion Configure and Display } + private static void ResizeOptionsPanelToChildren(Panel optionsPanel) + { + optionsPanel.SizeToChildren(new SizeToChildrenArgs(Recurse: true)); + } + protected override void OnMouseClicked(MouseButton mouseButton, Point mousePosition, bool userAction = true) { base.OnMouseClicked(mouseButton, mousePosition, userAction); @@ -209,17 +223,17 @@ protected override void OnMouseClicked(MouseButton mouseButton, Point mousePosit SkipTypewriting(); } - public override void Dispose() + protected override void Dispose(bool disposing) { EnsureControlRestored(); - base.Dispose(); + base.Dispose(disposing); } private static EventWindow? _instance; private void Update() { - if (!IsVisible || !_typewriting) + if (!IsVisibleInTree || !_typewriting) { return; } @@ -233,7 +247,7 @@ private void Update() foreach (var optionButton in _optionButtons) { - optionButton.IsVisible = writerCompleted && !string.IsNullOrEmpty(optionButton.Text); + optionButton.IsVisibleInTree = writerCompleted && !string.IsNullOrEmpty(optionButton.Text); } if (writerCompleted) @@ -248,7 +262,7 @@ private void Update() else if (Controls.IsControlJustPressed(Control.AttackInteract)) { SkipTypewriting(); - Defer(_promptScroller.ScrollToBottom); + PostLayout.Enqueue(_promptScroller.ScrollToBottom); } else { diff --git a/Intersect.Client.Core/Interface/Game/GameInterface.cs b/Intersect.Client.Core/Interface/Game/GameInterface.cs index df40de4298..4698124c2c 100644 --- a/Intersect.Client.Core/Interface/Game/GameInterface.cs +++ b/Intersect.Client.Core/Interface/Game/GameInterface.cs @@ -96,7 +96,7 @@ private SettingsWindow GetOrCreateSettingsWindow() { _settingsWindow ??= new SettingsWindow(GameCanvas) { - IsVisible = false, + IsVisibleInTree = false, }; return _settingsWindow; @@ -105,23 +105,27 @@ private SettingsWindow GetOrCreateSettingsWindow() public GameInterface(Canvas canvas) : base(canvas) { GameCanvas = canvas; - EscapeMenu = new EscapeMenu(GameCanvas, GetOrCreateSettingsWindow) {IsHidden = true}; - SimplifiedEscapeMenu = new SimplifiedEscapeMenu(GameCanvas, GetOrCreateSettingsWindow) {IsHidden = true}; - TargetContextMenu = new TargetContextMenu(GameCanvas) {IsHidden = true}; - AnnouncementWindow = new AnnouncementWindow(GameCanvas) { IsHidden = true }; InitGameGui(); } public Canvas GameCanvas { get; } - public EscapeMenu EscapeMenu { get; } + private AnnouncementWindow? _announcementWindow; + private EscapeMenu? _escapeMenu; + private SimplifiedEscapeMenu? _simplifiedEscapeMenu; + private TargetContextMenu? _targetContextMenu; - public SimplifiedEscapeMenu SimplifiedEscapeMenu { get; } + public EscapeMenu EscapeMenu => _escapeMenu ??= new EscapeMenu(GameCanvas, GetOrCreateSettingsWindow) + { + IsHidden = true, + }; + + public SimplifiedEscapeMenu SimplifiedEscapeMenu => _simplifiedEscapeMenu ??= new SimplifiedEscapeMenu(GameCanvas, GetOrCreateSettingsWindow) {IsHidden = true}; - public TargetContextMenu TargetContextMenu { get; } + public TargetContextMenu TargetContextMenu => _targetContextMenu ??= new TargetContextMenu(GameCanvas) {IsHidden = true}; - public AnnouncementWindow AnnouncementWindow { get; } + public AnnouncementWindow AnnouncementWindow => _announcementWindow ??= new AnnouncementWindow(GameCanvas) { IsHidden = true }; public MenuContainer GameMenu { get; private set; } @@ -441,7 +445,7 @@ public void Update(TimeSpan elapsed, TimeSpan total) if (mCraftingWindow != null) { - if (!mCraftingWindow.IsVisible || mShouldCloseCraftingTable) + if (!mCraftingWindow.IsVisibleInTree || mShouldCloseCraftingTable) { CloseCraftingTable(); } @@ -575,7 +579,7 @@ public bool CloseAllWindows() closedWindows = true; } - if (mCraftingWindow is { IsVisible: true, IsCrafting: false }) + if (mCraftingWindow is { IsVisibleInTree: true, IsCrafting: false }) { CloseCraftingTable(); closedWindows = true; @@ -593,7 +597,7 @@ public bool CloseAllWindows() closedWindows = true; } - if (TargetContextMenu.IsVisible) + if (TargetContextMenu.IsVisibleInTree) { TargetContextMenu.ToggleHidden(); closedWindows = true; diff --git a/Intersect.Client.Core/Interface/Game/GuildWindow.cs b/Intersect.Client.Core/Interface/Game/GuildWindow.cs index f7887fa846..7793058d9e 100644 --- a/Intersect.Client.Core/Interface/Game/GuildWindow.cs +++ b/Intersect.Client.Core/Interface/Game/GuildWindow.cs @@ -21,7 +21,7 @@ partial class GuildWindow : Window private readonly Button _buttonAdd; private readonly Button _buttonLeave; private readonly Button _buttonAddPopup; - private readonly Framework.Gwen.Control.Menu _contextMenu; + private readonly ContextMenu _contextMenu; private readonly MenuItem _privateMessageOption; private readonly MenuItem[] _promoteOptions; private readonly MenuItem[] _demoteOptions; @@ -115,7 +115,7 @@ public GuildWindow(Canvas gameCanvas) : base(gameCanvas, Globals.Me?.Guild, fals #region Context Menu Options // Context Menu - _contextMenu = new Framework.Gwen.Control.Menu(gameCanvas, "GuildContextMenu") + _contextMenu = new ContextMenu(gameCanvas, "GuildContextMenu") { IsHidden = true, IconMarginDisabled = true @@ -123,7 +123,7 @@ public GuildWindow(Canvas gameCanvas) : base(gameCanvas, Globals.Me?.Guild, fals //Add Context Menu Options //TODO: Is this a memory leak? - _contextMenu.Children.Clear(); + _contextMenu.ClearChildren(); // Private Message _privateMessageOption = _contextMenu.AddItem(Strings.Guilds.PM); @@ -326,8 +326,10 @@ private void member_RightClicked(Base sender, MouseButtonState arguments) _contextMenu.AddChild(_transferOption); } - _ = _contextMenu.SizeToChildren(); - _contextMenu.Open(Framework.Gwen.Pos.None); + if (_contextMenu.Children.Count > 0) + { + _contextMenu.Open(Framework.Gwen.Pos.None); + } } #endregion diff --git a/Intersect.Client.Core/Interface/Game/Hotbar/HotbarItem.cs b/Intersect.Client.Core/Interface/Game/Hotbar/HotbarItem.cs index f791b677bd..80ec52a428 100644 --- a/Intersect.Client.Core/Interface/Game/Hotbar/HotbarItem.cs +++ b/Intersect.Client.Core/Interface/Game/Hotbar/HotbarItem.cs @@ -11,6 +11,7 @@ using Intersect.Client.Items; using Intersect.Client.Localization; using Intersect.Client.Spells; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; @@ -276,7 +277,7 @@ public void Update() { if (binding?.Key is null or Keys.None) { - _keyLabel.IsVisible = false; + _keyLabel.IsVisibleInTree = false; } else { @@ -299,7 +300,7 @@ public void Update() } _keyLabel.Text = assembledKeyText; - _keyLabel.IsVisible = true; + _keyLabel.IsVisibleInTree = true; } _hotKey = binding == null ? null : new ControlBinding(binding); diff --git a/Intersect.Client.Core/Interface/Game/Inventory/InventoryItem.cs b/Intersect.Client.Core/Interface/Game/Inventory/InventoryItem.cs index f5ccf8ac3c..b0e9daa3f5 100644 --- a/Intersect.Client.Core/Interface/Game/Inventory/InventoryItem.cs +++ b/Intersect.Client.Core/Interface/Game/Inventory/InventoryItem.cs @@ -9,6 +9,7 @@ using Intersect.Client.Localization; using Intersect.Client.Networking; using Intersect.Configuration; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; @@ -76,7 +77,7 @@ public void Setup() Pnl.Clicked += pnl_Clicked; Pnl.DoubleClicked += Pnl_DoubleClicked; EquipPanel = new ImagePanel(Pnl, "InventoryItemEquippedIcon"); - EquipPanel.Texture = Graphics.Renderer.GetWhiteTexture(); + EquipPanel.Texture = Graphics.Renderer.WhitePixel; EquipLabel = new Label(Pnl, "InventoryItemEquippedLabel"); EquipLabel.IsHidden = true; EquipLabel.Text = Strings.Inventory.EquippedSymbol; @@ -484,7 +485,7 @@ public void Update() if (bagWindow.RenderBounds().IntersectsWith(dragRect)) { var bagSlotComponents = bagWindow.Items.ToArray(); - var bagSlotLimit = Math.Min(Globals.Bag.Length, bagSlotComponents.Length); + var bagSlotLimit = Math.Min(Globals.BagSlots.Length, bagSlotComponents.Length); for (var bagSlotIndex = 0; bagSlotIndex < bagSlotLimit; bagSlotIndex++) { var bagSlotComponent = bagSlotComponents[bagSlotIndex]; @@ -517,7 +518,7 @@ public void Update() { var bankSlotComponents = bankWindow.Items.ToArray(); var bankSlotLimit = Math.Min( - Math.Min(Globals.Bank.Length, Globals.BankSlots), + Math.Min(Globals.BankSlots.Length, Globals.BankSlotCount), bankSlotComponents.Length ); diff --git a/Intersect.Client.Core/Interface/Game/Inventory/InventoryWindow.cs b/Intersect.Client.Core/Interface/Game/Inventory/InventoryWindow.cs index 5d38f7cfb0..352637c601 100644 --- a/Intersect.Client.Core/Interface/Game/Inventory/InventoryWindow.cs +++ b/Intersect.Client.Core/Interface/Game/Inventory/InventoryWindow.cs @@ -28,7 +28,7 @@ public partial class InventoryWindow private List public abstract void End(); - public override void Reload() { } + private protected override void InternalReload() + { + // Render targets should not be reloaded + } /// /// Clears everything off the render target with a specified color. /// public abstract void Clear(Color color); - - public abstract override object? GetTexture(); - - public override void Dispose() - { - base.Dispose(); - RenderTextureCount--; - } -} +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/GameRenderer.cs b/Intersect.Client.Framework/Graphics/GameRenderer.cs index 2684f8accd..94cf0cf30d 100644 --- a/Intersect.Client.Framework/Graphics/GameRenderer.cs +++ b/Intersect.Client.Framework/Graphics/GameRenderer.cs @@ -1,27 +1,85 @@ +using System.Collections.Concurrent; +using System.Diagnostics; using Intersect.Client.Framework.GenericClasses; +using Intersect.Core; +using Intersect.Framework.Collections; +using Intersect.Framework.Core; +using Intersect.Framework.SystemInformation; +using Microsoft.Extensions.Logging; namespace Intersect.Client.Framework.Graphics; - public abstract partial class GameRenderer : IGameRenderer, ITextHelper { + // To test the unloading system, lower InactiveTimeCutoff to 5000 (5 seconds) and raise + // MinimumAvailableVRAM to above your GPU RAM (if using Nvidia) or system RAM (if not using Nvidia) + + /// + /// 30 seconds in milliseconds + /// + private const long InactiveTimeCutoff = 30_000; + + /// + /// 128MiB, which is 2x 4096px² atlases, half an 8192px² atlas + /// + private const long MinimumAvailableVRAM = 134217728; + + /// + /// 30 minutes in milliseconds + /// + private const long StaleTimeCutoff = 30 * 60 * 1000; + + /// + /// 1GiB, which is 16x 4096px² atlases, 4x 8192px² atlases, and 1x 16384px² atlas + /// + private const long StaleVRAMCutoff = MinimumAvailableVRAM * 8; + + /// + /// 6 hours in milliseconds + /// + private const long UnusedTimeCutoff = 6 * 60 * 60 * 1000; - public GameRenderer() + /// + /// 8GiB, which is 128x 4096px² atlases, 32x 8192px² atlases, and 8x 16384px² atlases + /// + private const long UnusedVRAMCutoff = StaleVRAMCutoff * 8; + + private readonly ConcurrentDictionary _screenshotRequestLookup = []; + private readonly ConcurrentConditionalDequeue _screenshotRequests = []; + + private readonly HashSet _textures = []; + private readonly List _texturesSortedByAccessTime = []; + + private float _scale = 1.0f; + + private IGameTexture? _whitePixel; + + public IGameTexture[] Textures { - ScreenshotRequests = new List(); + get + { + try + { + var buffer = new IGameTexture[_textures.Count]; + _textures.CopyTo(buffer); + return buffer; + } + catch + { + return _textures.ToArray(); + } + } } - public List ScreenshotRequests { get; } - - public Resolution ActiveResolution => new Resolution(PreferredResolution, OverrideResolution); + public ulong TextureCount => (ulong)Math.Max(0, _textures.Count); - public bool HasOverrideResolution => OverrideResolution != Resolution.Empty; + public ulong RenderTargetAllocations { get; private set; } - public Resolution OverrideResolution { get; set; } + public ulong TextureAllocations { get; private set; } - public Resolution PreferredResolution { get; set; } + public bool HasOverrideResolution => OverrideResolution != Resolution.Empty; - protected float _scale = 1.0f; + public bool HasScreenshotRequests => _screenshotRequests.Count > 0; public float Scale { @@ -38,44 +96,33 @@ public float Scale } } - public abstract void Init(); + public abstract long UsedMemory { get; } - /// - /// Called before a frame is drawn, if the renderer must re-created or anything it does it here. - /// - /// - public abstract bool Begin(); + public abstract int FPS { get; } - public abstract bool BeginScreenshot(); + public IGameTexture WhitePixel => _whitePixel ??= CreateWhitePixel(); - protected abstract bool RecreateSpriteBatch(); + public Resolution ActiveResolution => new(PreferredResolution, OverrideResolution); - /// - /// Called when the frame is done being drawn, generally used to finally display the content to the screen. - /// - public abstract void End(); + public Resolution OverrideResolution { get; set; } - public abstract void EndScreenshot(); + public Resolution PreferredResolution { get; set; } /// /// Clears everything off the render target with a specified color. /// public abstract void Clear(Color color); - public abstract void SetView(FloatRect view); - public FloatRect CurrentView { - get { return GetView(); } - set { SetView(value); } + get => GetView(); + set => SetView(value); } - public abstract FloatRect GetView(); - - public abstract GameFont LoadFont(string filename); + public string ResolutionAsString => GetResolutionString(); - public abstract void DrawTexture( - GameTexture tex, + void IGameRenderer.DrawTexture( + IGameTexture tex, float sx, float sy, float sw, @@ -85,30 +132,325 @@ public abstract void DrawTexture( float tw, float th, Color renderColor, - GameRenderTexture? renderTarget = null, - GameBlendModes blendMode = GameBlendModes.None, - GameShader? shader = null, - float rotationDegrees = 0.0f, - bool isUi = false, - bool drawImmediate = false + IGameRenderTexture? renderTarget, + GameBlendModes blendMode, + GameShader? shader, + float rotationDegrees + ) + { + DrawTexture( + tex, + sx, + sy, + sw, + sh, + tx, + ty, + tw, + th, + renderColor, + renderTarget, + blendMode, + shader, + rotationDegrees + ); + } + + public abstract IGameRenderTexture CreateRenderTexture(int width, int height); + + public abstract Pointf MeasureText(string text, GameFont? gameFont, float fontScale); + + public abstract void DrawString( + string text, + GameFont? gameFont, + float x, + float y, + float fontScale, + Color? fontColor, + bool worldPos = true, + IGameRenderTexture? renderTexture = null, + Color? borderColor = null ); - public int Fps => GetFps(); + public abstract void DrawString( + string text, + GameFont? gameFont, + float x, + float y, + float fontScale, + Color fontColor, + bool worldPos, + IGameRenderTexture renderTexture, + FloatRect clipRect, + Color? borderColor = null + ); - public abstract int GetFps(); + public void RequestScreenshot(string? pathToScreenshots = default) + { + if (string.IsNullOrWhiteSpace(pathToScreenshots)) + { + var pathToPictures = Environment.GetFolderPath( + Environment.SpecialFolder.MyPictures, + Environment.SpecialFolderOption.Create + ); + var pathToApplicationPictures = Path.Combine(pathToPictures, ApplicationContext.CurrentContext.Name); + pathToScreenshots = Path.Combine(pathToApplicationPictures, "screenshots"); + } - public int ScreenWidth => GetScreenWidth(); + if (!Directory.Exists(pathToScreenshots)) + { + Directory.CreateDirectory(pathToScreenshots); + } - public abstract int GetScreenWidth(); + var screenshotNumber = 1; + var screenshotFileName = $"{DateTime.Now:yyyyMMdd-HHmmssfff}.png"; + var pathToScreenshotFile = Path.Combine(pathToScreenshots, screenshotFileName); + while (ScreenshotOrRequestExistsFor(pathToScreenshotFile) && screenshotNumber < 100) + { + screenshotFileName = $"{DateTime.Now:yyyyMMdd-HHmmssfff}.{screenshotNumber++:000}.png"; + pathToScreenshotFile = Path.Combine(pathToScreenshots ?? string.Empty, screenshotFileName); + } - public int ScreenHeight => GetScreenHeight(); + if (ScreenshotOrRequestExistsFor(pathToScreenshotFile)) + { + ApplicationContext.CurrentContext.Logger.LogWarning("Failed to request screenshot"); + return; + } - public abstract int GetScreenHeight(); + ScreenshotRequest screenshotRequest = new( + () => File.OpenWrite(pathToScreenshotFile), + screenshotFileName + ); - public string ResolutionAsString => GetResolutionString(); + if (_screenshotRequestLookup.TryAdd(screenshotFileName, screenshotRequest)) + { + _screenshotRequests.Enqueue(screenshotRequest); + } + } + + public abstract List ValidVideoModes { get; } + + public event EventHandler? TextureAllocated; + + public event EventHandler? TextureCreated; + + public event EventHandler? TextureDisposed; + + public event EventHandler? TextureFreed; + + public Pointf MeasureText(string text, GameFont? font) + { + return MeasureText(text, font, 1); + } + + protected internal void MarkConstructed(IGameTexture texture, bool markAllocated = false) + { + _textures.Add(texture); + + TextureCreated?.Invoke(this, new TextureEventArgs(texture)); + + if (markAllocated) + { + MarkAllocated(texture); + } + } + + protected internal virtual void MarkDisposed(IGameTexture texture) + { + _textures.Remove(texture); + TextureDisposed?.Invoke(this, new TextureEventArgs(texture)); + } + + protected internal void MarkAllocated(IGameTexture texture) + { + if (!_textures.Contains(texture)) + { + throw new InvalidOperationException("Texture must be added first"); + } + + if (!texture.IsPinned) + { + _texturesSortedByAccessTime.AddSorted(texture); + } + + if (texture is IGameRenderTexture) + { + ++RenderTargetAllocations; + } + + ++TextureAllocations; + + TextureAllocated?.Invoke(this, new TextureEventArgs(texture)); + } + + // ReSharper disable once MemberCanBeProtected.Global + protected internal void MarkFreed(IGameTexture texture) + { + if (!texture.IsPinned) + { + _texturesSortedByAccessTime.Remove(texture); + } + + if (texture is IGameRenderTexture) + { + if (RenderTargetAllocations > 0) + { + --RenderTargetAllocations; + } + else + { + ApplicationContext.CurrentContext.Logger.LogCritical("RenderTextureAllocations out of sync"); + } + } + + if (TextureAllocations > 0) + { + --TextureAllocations; + } + else + { + ApplicationContext.CurrentContext.Logger.LogCritical("TextureAllocations out of sync"); + } + + TextureFreed?.Invoke(this, new TextureEventArgs(texture)); + } + + internal void UpdateAccessTime(IGameTexture gameTexture) + { + _texturesSortedByAccessTime.Resort(gameTexture); + } + + public abstract void Init(); + + /// + /// Called before a frame is drawn, if the renderer must recreated or anything it does it here. + /// + /// + public abstract bool Begin(); + + public abstract bool BeginScreenshot(); + + protected abstract bool RecreateSpriteBatch(); - void IGameRenderer.DrawTexture( - GameTexture tex, + /// + /// Called when the frame is done being drawn, generally used to finally display the content to the screen. + /// + public void End() + { + DoEnd(); + + var usedVRAM = UsedMemory; + var availableVRAM = PlatformStatistics.AvailableGPUMemory; + var totalVRAM = PlatformStatistics.TotalGPUMemory; + + if (availableVRAM < 0) + { + if (totalVRAM < 0) + { + availableVRAM = PlatformStatistics.AvailableSystemMemory; + } + else + { + availableVRAM = totalVRAM - usedVRAM; + } + } + + if (totalVRAM < 0) + { + totalVRAM = availableVRAM + usedVRAM; + } + + var now = Timing.Global.MillisecondsUtc; + + var unusedCutoffTime = now - UnusedTimeCutoff; + var staleCutoffTime = now - StaleTimeCutoff; + var inactiveCutoffTime = now - InactiveTimeCutoff; + + var unusedCutoffSize = UnusedVRAMCutoff < totalVRAM ? UnusedVRAMCutoff : totalVRAM - MinimumAvailableVRAM; + var staleCutoffSize = StaleVRAMCutoff < totalVRAM ? StaleVRAMCutoff : totalVRAM - MinimumAvailableVRAM; + + while (_texturesSortedByAccessTime.FirstOrDefault() is { } texture) + { + var accessTime = texture.AccessTime; + if (availableVRAM > MinimumAvailableVRAM) + { + if (availableVRAM > staleCutoffSize) + { + if (availableVRAM > unusedCutoffSize) + { + // Don't unload anything if we've still got (at the time of writing) 8GiB of VRAM + break; + } + + if (texture.AccessTime > unusedCutoffTime) + { + // It has been used in the last 6 hours, don't unload + break; + } + + // We're below our unused VRAM threshold and the texture is considered unused, unload it + ApplicationContext.CurrentContext.Logger.LogTrace( + "Unloading {TextureName} because it's unused and we have {AvailableVRAM} bytes of VRAM available (cutoff is {CutoffVRAM})", + texture.Name, + availableVRAM, + unusedCutoffSize + ); + } + else if (texture.AccessTime > staleCutoffTime) + { + // We are below our stale VRAM threshold, but the texture is not yet considered stale, don't unload + } + else + { + // We're below our stale VRAM threshold and the texture is considered stale, unload it + ApplicationContext.CurrentContext.Logger.LogTrace( + "Unloading {TextureName} because it's stale and we have {AvailableVRAM} bytes of VRAM available (cutoff is {CutoffVRAM})", + texture.Name, + availableVRAM, + staleCutoffSize + ); + } + } + else if (accessTime > inactiveCutoffTime) + { + // We are below our minimum VRAM threshold, but the texture is still active, don't unload + break; + } + else + { + ApplicationContext.CurrentContext.Logger.LogTrace( + "Unloading {TextureName} because it's inactive and we have {AvailableVRAM} bytes of VRAM available (cutoff is {CutoffVRAM})", + texture.Name, + availableVRAM, + MinimumAvailableVRAM + ); + } + + // If we've reached this point in the code one of the following is true: + // - We are below the unused memory threshold, and the texture is considered to be unused + // - We are below the stale memory threshold, and the texture is considered to be stale + // - We are below the minimum memory threshold, and the texture is considered to be inactive + // In any of these three cases we want to unload the texture + + if (texture.Unload()) + { + _texturesSortedByAccessTime.Remove(texture); + } + } + } + + protected abstract void DoEnd(); + + public abstract void EndScreenshot(); + + public abstract void SetView(FloatRect view); + + public abstract FloatRect GetView(); + + public abstract GameFont LoadFont(string filename); + + public abstract void DrawTexture( + IGameTexture tex, float sx, float sy, float sw, @@ -118,55 +460,53 @@ void IGameRenderer.DrawTexture( float tw, float th, Color renderColor, - GameRenderTexture? renderTarget = null, + IGameRenderTexture? renderTarget = null, GameBlendModes blendMode = GameBlendModes.None, GameShader? shader = null, - float rotationDegrees = 0.0f - ) => DrawTexture(tex, sx, sy, sw, sh, tx, ty, tw, th, renderColor, renderTarget, blendMode, shader, rotationDegrees); + float rotationDegrees = 0.0f, + bool isUi = false, + bool drawImmediate = false + ); - public GameRenderTexture CreateWhiteTexture() => GetWhiteTexture() as GameRenderTexture; + public abstract int ScreenWidth { get; } + + public abstract int ScreenHeight { get; } public abstract string GetResolutionString(); public abstract bool DisplayModeChanged(); - public abstract GameRenderTexture CreateRenderTexture(int width, int height); - - public abstract GameTexture LoadTexture(string filename, string realFilename); + public IGameTexture LoadTexture(string assetName, string filePath) + { + return AtlasReference.TryGet(assetName, out var atlasReference) + ? CreateGameTextureFromAtlasReference(assetName, atlasReference) + : CreateTextureFromStreamFactory( + assetName, + () => + { + try + { + return File.OpenRead(filePath); + } + catch + { +#if DEBUG + Debugger.Break(); +#endif + throw; + } + } + ); + } - public abstract GameTexture LoadTexture( + protected abstract IGameTexture CreateGameTextureFromAtlasReference( string assetName, - Func createStream + AtlasReference atlasReference ); - public abstract GameTexture GetWhiteTexture(); + public abstract IGameTexture CreateTextureFromStreamFactory(string assetName, Func streamFactory); - public abstract Pointf MeasureText(string text, GameFont? gameFont, float fontScale); - - public abstract void DrawString( - string text, - GameFont? gameFont, - float x, - float y, - float fontScale, - Color? fontColor, - bool worldPos = true, - GameRenderTexture? renderTexture = null, - Color? borderColor = null - ); - - public abstract void DrawString( - string text, - GameFont gameFont, - float x, - float y, - float fontScale, - Color fontColor, - bool worldPos, - GameRenderTexture renderTexture, - FloatRect clipRect, - Color borderColor = null - ); + protected abstract IGameTexture CreateWhitePixel(); //Buffers public abstract GameTileBuffer CreateTileBuffer(); @@ -175,30 +515,19 @@ public abstract void DrawString( public abstract void Close(); - public List ValidVideoModes => GetValidVideoModes(); - public abstract List GetValidVideoModes(); - public abstract GameShader LoadShader(string shaderName); - public void RequestScreenshot(string screenshotDir = "screenshots") + private bool ScreenshotOrRequestExistsFor(string pathToScreenshotFile) { - if (!Directory.Exists(screenshotDir)) - { - Directory.CreateDirectory(screenshotDir ?? ""); - } - - var screenshotNumber = 0; - string screenshotFile; - do - { - screenshotFile = Path.Combine( - screenshotDir ?? "", $"{DateTime.Now:yyyyMMdd-HHmmssfff}{screenshotNumber}.png" - ); - - ++screenshotNumber; - } while (File.Exists(screenshotFile) && screenshotNumber < 4); - - ScreenshotRequests.Add(File.OpenWrite(screenshotFile)); + return File.Exists(pathToScreenshotFile) || _screenshotRequestLookup.ContainsKey(pathToScreenshotFile); } -} + protected void ProcessScreenshots(WriteTextureToStreamDelegate writeTexture) + { + Func consumer = screenshotRequest => writeTexture( + screenshotRequest.StreamFactory(), + screenshotRequest.Hint + ); + while (_screenshotRequests.TryDequeueIf(consumer)) { } + } +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/GameShader.cs b/Intersect.Client.Framework/Graphics/GameShader.cs index e36a6c4851..30379b600a 100644 --- a/Intersect.Client.Framework/Graphics/GameShader.cs +++ b/Intersect.Client.Framework/Graphics/GameShader.cs @@ -2,10 +2,8 @@ namespace Intersect.Client.Framework.Graphics; - public abstract partial class GameShader { - public GameShader(string shaderName) { } @@ -23,5 +21,4 @@ public GameShader(string shaderName) public abstract void ResetChanged(); public abstract object GetShader(); - -} +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/GameTexture.cs b/Intersect.Client.Framework/Graphics/GameTexture.cs index 4b40d67054..0095d6cdab 100644 --- a/Intersect.Client.Framework/Graphics/GameTexture.cs +++ b/Intersect.Client.Framework/Graphics/GameTexture.cs @@ -1,29 +1,368 @@ using System.Runtime.CompilerServices; using Intersect.Client.Framework.Content; using Intersect.Client.Framework.GenericClasses; +using Intersect.Compression; using Intersect.Core; +using Intersect.Framework.Core; +using Intersect.Framework.Reflection; using Microsoft.Extensions.Logging; namespace Intersect.Client.Framework.Graphics; +public static class GameTexture +{ + public static IGameTexture? GetBoundingTexture(BoundsComparison boundsComparison, params IGameTexture[] textures) + { + IGameTexture? boundingTexture = default; + + foreach (var texture in textures) + { + if (boundingTexture == default) + { + boundingTexture = texture; + continue; + } + + var select = false; + switch (boundsComparison) + { + case BoundsComparison.Width: + select = texture.Width > boundingTexture.Width; + break; + + case BoundsComparison.Height: + select = texture.Height > boundingTexture.Height; + break; + + case BoundsComparison.Dimensions: + select = texture.Width >= boundingTexture.Width && texture.Height >= boundingTexture.Height; + break; + + case BoundsComparison.Area: + select = texture.Area > boundingTexture.Area; + break; + + default: + ApplicationContext.Context.Value?.Logger.LogError( + new ArgumentOutOfRangeException(nameof(boundsComparison), boundsComparison.ToString()), + "Failed to get bounding texture" + ); + break; + } + + if (select) + { + boundingTexture = texture; + } + } + + return boundingTexture; + } +} -public abstract partial class GameTexture : IAsset, IDisposable +public abstract partial class GameTexture : IGameTexture + where TPlatformTexture : class, IDisposable where TPlatformRenderer : GameRenderer { + // ReSharper disable once StaticMemberInGenericType private static ulong _nextId; - private readonly ulong _id = ++_nextId; + protected readonly ulong Id = ++_nextId; + + protected readonly TPlatformRenderer Renderer; private bool _disposed; + private Func? _streamFactory; + private TPlatformTexture? _platformTexture; + + private GameTexture(TPlatformRenderer renderer, string? name, bool pinned) + { + ArgumentNullException.ThrowIfNull(renderer); + + Renderer = renderer; + Name = name ?? $"{GetType().GetName(qualified: true)}#{Id}"; + + IsPinned = pinned; + if (pinned) + { + AccessTime = long.MaxValue; + } + + Renderer.MarkConstructed(this); + } + + protected GameTexture( + TPlatformRenderer renderer, + string? name, + TPlatformTexture platformTexture + ) : this(renderer: renderer, name: name, pinned: true) + { + ArgumentNullException.ThrowIfNull(platformTexture); + + _platformTexture = platformTexture; + + Renderer.MarkAllocated(this); + } + + protected GameTexture( + TPlatformRenderer renderer, + string name, + Func streamFactory, + bool pinned = false + ) : this(renderer, name, pinned) + { + ArgumentNullException.ThrowIfNull(streamFactory); + + _streamFactory = streamFactory; + } + + protected GameTexture( + TPlatformRenderer renderer, + string name, + AtlasReference atlasReference + ) : this(renderer, name, pinned: true) + { + ArgumentNullException.ThrowIfNull(atlasReference); + + AtlasReference = atlasReference; + } + + protected TPlatformTexture? PlatformTexture + { + get + { + if (AtlasReference is not null) + { + return AtlasReference.Texture.GetTexture(); + } + + if (_platformTexture == null) + { + LoadPlatformTexture(); + } + else + { + UpdateAccessTime(); + } + + return _platformTexture; + } + set + { + if (_platformTexture is not null && value is not null) + { + throw new InvalidOperationException(); + } + + _platformTexture = value; + } + } public event Action? Disposed; public event Action? Loaded; public event Action? Unloaded; - protected GameTexture(string name) + public Color this[int x, int y] => GetPixel(x, y); + + public Color this[Point point] => GetPixel(point.X, point.Y); + + public long AccessTime { get; private set; } + + // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter + public bool IsDisposed => _disposed; + + public bool IsLoaded => !_disposed && _platformTexture != null; + + public bool IsMissingOrCorrupt { get; private set; } + + public bool IsPinned { get; } + + string IAsset.Id => Name; + + public string Name { get; } + + public int Area => Width * Height; + + public FloatRect Bounds => new(0, 0, Width, Height); + + public Pointf Dimensions => new(Width, Height); + + public Pointf Center => Dimensions / 2; + + public AtlasReference? AtlasReference + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + public abstract int Width { get; } + + public abstract int Height { get; } + + public int CompareTo(IGameTexture? other) { - Name = name; + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (other is not GameTexture otherGameTexture) + { + return -1; + } + + var accessTimeComparison = (int)Math.Clamp(AccessTime - other.AccessTime, int.MinValue, int.MaxValue); + if (accessTimeComparison != 0) + { + return accessTimeComparison; + } + + if (Id != otherGameTexture.Id) + { + return Id < otherGameTexture.Id ? -1 : 1; + } + + ApplicationContext.Context.Value?.Logger.LogError( + "Texture '{NameA}' and '{NameB}' both have the same Id '{Id}' which is unexpected", + Name, + other.Name, + Id + ); + return 0; } + protected abstract TPlatformTexture? CreatePlatformTextureFromStream(Stream stream); + + private void LoadPlatformTexture(bool force = false) + { + if (!force) + { + if (_platformTexture != null) + { + return; + } + + if (IsMissingOrCorrupt) + { + return; + } + } + + if (AtlasReference != null) + { + if (AtlasReference.Texture is not GameTexture atlasTexture) + { + IsMissingOrCorrupt = true; + throw new InvalidOperationException(); + } + + atlasTexture.LoadPlatformTexture(force); + return; + } + + if (_streamFactory == null) + { + IsMissingOrCorrupt = true; + throw new InvalidOperationException(); + } + + try + { + _platformTexture = null; + + using var stream = _streamFactory(); + + if (Name.EndsWith(".asset")) + { + using var gzipStream = GzipCompression.CreateDecompressedFileStream(stream); + _platformTexture = CreatePlatformTextureFromStream(gzipStream); + } + else + { + _platformTexture = CreatePlatformTextureFromStream(stream); + } + + Renderer.MarkAllocated(this); + UpdateAccessTime(); + EmitLoaded(); + } + catch (Exception exception) + { + ApplicationContext.CurrentContext.Logger.LogError( + exception, + "Exception thrown while trying to load '{TextureName}'", + Name + ); + } + finally + { + IsMissingOrCorrupt = _platformTexture == null; + } + } + + public bool Unload() + { + if (AccessTime == long.MaxValue) + { + ApplicationContext.CurrentContext.Logger.LogWarning( + "Tried to unload pinned texture {TextureId} ({TextureName})", + Id, + Name + ); + return false; + } + + _platformTexture?.Dispose(); + _platformTexture = null; + + AccessTime = 0; + EmitUnloaded(); + OnUnload(); + return true; + } + + public object? GetTexture() => PlatformTexture; + + public void Reload() => InternalReload(); + + private protected virtual void InternalReload() => LoadPlatformTexture(force: true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TRequestedPlatformTexture? GetTexture() where TRequestedPlatformTexture : class + { + if (typeof(TRequestedPlatformTexture) != typeof(TPlatformTexture)) + { + return null; + } + + return GetTexture() as TRequestedPlatformTexture; + } + + public abstract Color GetPixel(int x, int y); + + public override string ToString() + { + return $"{GetType().Name} ({Name})"; + } + + public void Dispose() + { + Dispose(true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void UpdateAccessTime() + { + if (AccessTime == long.MaxValue) + { + return; + } + + AccessTime = Timing.Global.MillisecondsUtc; + Renderer.UpdateAccessTime(this); + } + + protected virtual void OnUnload() { } + private void EmitDisposed() { try @@ -56,7 +395,7 @@ protected void EmitLoaded() } } - protected void EmitUnloaded() + private void EmitUnloaded() { try { @@ -72,107 +411,39 @@ protected void EmitUnloaded() } } - // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter - public bool IsDisposed => _disposed; - - public virtual bool IsLoaded => !_disposed; - - public string Id => Name ?? _id.ToString(); - - public string? Name { get; } - - public int Area => Width * Height; - - public Pointf Dimensions => new(Width, Height); - - public FloatRect Bounds => new(0, 0, Width, Height); - - public Pointf Center => Dimensions / 2; - - public object? PlatformTextureObject - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => GetTexture(); - } - - public GameTexturePackFrame? TexturePackFrame + ~GameTexture() { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => GetTexturePackFrame(); + Dispose(false); } - public abstract int Width { get; } - - public abstract int Height { get; } - - public abstract object? GetTexture(); - - public abstract void Reload(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TPlatformTexture? GetTexture() where TPlatformTexture : class => - GetTexture() as TPlatformTexture; - - public abstract Color GetPixel(int x1, int y1); - - public abstract GameTexturePackFrame? GetTexturePackFrame(); - - public static GameTexture? GetBoundingTexture(BoundsComparison boundsComparison, params GameTexture[] textures) + private void Dispose(bool disposing) { - GameTexture? boundingTexture = default; - - foreach (var texture in textures) + if (!disposing && _disposed) { - if (boundingTexture == default) - { - boundingTexture = texture; - continue; - } - - var select = false; - switch (boundsComparison) - { - case BoundsComparison.Width: - select = texture.Width > boundingTexture.Width; - break; - - case BoundsComparison.Height: - select = texture.Height > boundingTexture.Height; - break; + return; + } - case BoundsComparison.Dimensions: - select = texture.Width >= boundingTexture.Width && texture.Height >= boundingTexture.Height; - break; + ObjectDisposedException.ThrowIf(_disposed, this); - case BoundsComparison.Area: - select = texture.Area > boundingTexture.Area; - break; + _disposed = true; - default: - ApplicationContext.Context.Value?.Logger.LogError( - new ArgumentOutOfRangeException(nameof(boundsComparison), boundsComparison.ToString()), - "Failed to get bounding texture" - ); - break; - } + try + { + _platformTexture?.Dispose(); + _platformTexture = null; - if (select) - { - boundingTexture = texture; - } + Renderer.MarkDisposed(this); + } + catch + { + throw; } - return boundingTexture; + EmitDisposed(); + OnDispose(true); } - public override string ToString() => $"{GetType().Name} ({Name})"; - - public virtual void Dispose() + protected virtual void OnDispose(bool disposing) { - ObjectDisposedException.ThrowIf(_disposed, this); - - _disposed = true; - - EmitDisposed(); } -} +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/GameTexturePack.cs b/Intersect.Client.Framework/Graphics/GameTexturePack.cs deleted file mode 100644 index 8c3f07d085..0000000000 --- a/Intersect.Client.Framework/Graphics/GameTexturePack.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Intersect.Client.Framework.GenericClasses; - -namespace Intersect.Client.Framework.Graphics; - - -public partial class GameTexturePacks -{ - - private static List mFrames = new List(); - - private static Dictionary> mFrameTypes = - new Dictionary>(); - - public static void AddFrame(GameTexturePackFrame frame) - { - mFrames.Add(frame); - - //find the sub folder - var sep = new char[] {'/', '\\'}; - var subFolder = frame.Filename.Split(sep)[1].ToLower(); - if (!mFrameTypes.ContainsKey(subFolder)) - { - mFrameTypes.Add(subFolder, new List()); - } - - if (!mFrameTypes[subFolder].Contains(frame)) - { - mFrameTypes[subFolder].Add(frame); - } - } - - public static GameTexturePackFrame[] GetFolderFrames(string folder) - { - if (mFrameTypes.ContainsKey(folder.ToLower())) - { - return mFrameTypes[folder.ToLower()].ToArray(); - } - - return null; - } - - public static GameTexturePackFrame GetFrame(string filename) - { - filename = filename.Replace("\\", "/"); - return mFrames.Where(p => p.Filename.ToLower() == filename).FirstOrDefault(); - } - -} - -public partial class GameTexturePackFrame -{ - - public GameTexturePackFrame( - string filename, - Rectangle rect, - bool rotated, - Rectangle sourceSpriteRect, - GameTexture packTexture - ) - { - Filename = filename.Replace('\\', '/'); - Rect = rect; - Rotated = rotated; - SourceRect = sourceSpriteRect; - PackTexture = packTexture; - } - - public string Filename { get; set; } - - public Rectangle Rect { get; set; } - - public bool Rotated { get; set; } - - public Rectangle SourceRect { get; set; } - - public GameTexture PackTexture { get; set; } - -} diff --git a/Intersect.Client.Framework/Graphics/GameTileBuffer.cs b/Intersect.Client.Framework/Graphics/GameTileBuffer.cs index 33f3f00fd3..2846191418 100644 --- a/Intersect.Client.Framework/Graphics/GameTileBuffer.cs +++ b/Intersect.Client.Framework/Graphics/GameTileBuffer.cs @@ -1,19 +1,16 @@ namespace Intersect.Client.Framework.Graphics; - public abstract partial class GameTileBuffer { - public static int TileBufferCount { get; set; } public abstract bool Supported { get; } - public abstract bool TryAddTile(GameTexture texture, int x, int y, int srcX, int srcY, int srcW, int srcH); + public abstract bool TryAddTile(IGameTexture texture, int x, int y, int srcX, int srcY, int srcW, int srcH); - public abstract bool TryUpdateTile(GameTexture texture, int x, int y, int srcX, int srcY, int srcW, int srcH); + public abstract bool TryUpdateTile(IGameTexture texture, int x, int y, int srcX, int srcY, int srcW, int srcH); public abstract bool SetData(); public abstract void Dispose(); - -} +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/IGameRenderTexture.cs b/Intersect.Client.Framework/Graphics/IGameRenderTexture.cs new file mode 100644 index 0000000000..8eaf1a2b32 --- /dev/null +++ b/Intersect.Client.Framework/Graphics/IGameRenderTexture.cs @@ -0,0 +1,10 @@ +namespace Intersect.Client.Framework.Graphics; + +public interface IGameRenderTexture : IGameTexture +{ + bool Begin(); + + void Clear(Color black); + + void End(); +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/IGameRenderer.cs b/Intersect.Client.Framework/Graphics/IGameRenderer.cs index b9fe954dc1..27d5c9ae25 100644 --- a/Intersect.Client.Framework/Graphics/IGameRenderer.cs +++ b/Intersect.Client.Framework/Graphics/IGameRenderer.cs @@ -5,79 +5,68 @@ namespace Intersect.Client.Framework.Graphics; public interface IGameRenderer { /// - /// The current active resolution of the client. + /// The current active resolution of the client. /// Resolution ActiveResolution { get; } /// - /// The current active resolution of the client as a string. + /// The current active resolution of the client as a string. /// string ResolutionAsString { get; } /// - /// The current height of the client screen. + /// The current height of the client screen. /// int ScreenHeight { get; } /// - /// The current width of the client screen. + /// The current width of the client screen. /// int ScreenWidth { get; } /// - /// The current override resolution of the client. + /// The current override resolution of the client. /// Resolution OverrideResolution { get; set; } /// - /// The preferred resolution of the client. + /// The preferred resolution of the client. /// Resolution PreferredResolution { get; set; } /// - /// The current viewport of the client. + /// The current viewport of the client. /// FloatRect CurrentView { get; set; } /// - /// All currently standing request for screenshots of the client. + /// The current framerate at which the client is drawing frames. /// - List ScreenshotRequests { get; } + int FPS { get; } /// - /// The current framerate at which the client is drawing frames. - /// - int Fps { get; } - - /// - /// All valid video modes that the client can render at. + /// All valid video modes that the client can render at. /// List ValidVideoModes { get; } - + /// - /// Clear the screen. + /// Clear the screen. /// - /// The to clear the screen with. + /// The to clear the screen with. void Clear(Color color); /// - /// Create a new empty render texture in memory. + /// Create a new empty render texture in memory. /// /// The width of the render texture. /// The height of the render texture. - /// Returns a new with the configured height and width. - GameRenderTexture CreateRenderTexture(int width, int height); - - /// - /// Create a new render texture in memory that's white. - /// - /// Returns a new white . - GameRenderTexture CreateWhiteTexture(); + /// Returns a new with the configured height and width. + IGameRenderTexture CreateRenderTexture(int width, int height); /// - /// Draw a texture to the client display. + /// Draw a texture to the client display. /// - /// The to render to the screen. + /// The to render to the screen. /// The X position to use on the texture to draw from. /// The Y position to use on the texture to draw from. /// The width to use on the texture to draw from. @@ -86,54 +75,111 @@ public interface IGameRenderer /// The destination Y position on screen. /// The destination width on screen. /// The destination height on screen. - /// The to render this texture as. Use to retain original colors. - /// Overrides this method to draw to the specified instead of the screen. - /// The to use to render this texture to the screen with. - /// The to use to render this to the screen with. + /// + /// The to render this texture as. Use to retain + /// original colors. + /// + /// + /// Overrides this method to draw to the specified instead of + /// the screen. + /// + /// The to use to render this texture to the screen with. + /// The to use to render this to the screen with. /// The angle to render this texture at. - void DrawTexture(GameTexture texture, float sourceX, float sourceY, float sourceWidth, float sourceHeight, float targetX, float targetY, float targetWidth, float targetHeight, Color renderColor, GameRenderTexture renderTarget = null, GameBlendModes blendMode = GameBlendModes.None, GameShader shader = null, float rotationDegrees = 0); - - /// - /// Draw text to the client display. + void DrawTexture( + IGameTexture texture, + float sourceX, + float sourceY, + float sourceWidth, + float sourceHeight, + float targetX, + float targetY, + float targetWidth, + float targetHeight, + Color renderColor, + IGameRenderTexture renderTarget = null, + GameBlendModes blendMode = GameBlendModes.None, + GameShader shader = null, + float rotationDegrees = 0 + ); + + /// + /// Draw text to the client display. /// /// The text to render to the screen. - /// The to use to render the text. + /// The to use to render the text. /// The X location on the screen to render the text to. /// The Y location on the screen to render the text to. /// The scale of the font to render the text with. - /// The to use to render the text with. - /// Determines if this text is rendered in a world relative location. When false it will render screen relative. - /// Overrides this method to draw to the specified instead of the screen. - /// The to use to render the border of this text with. - void DrawString(string text, GameFont gameFont, float x, float y, float fontScale, Color fontColor, bool worldPos = true, GameRenderTexture renderTexture = null, Color borderColor = null); - - /// - /// Draw text to the client display. + /// The to use to render the text with. + /// + /// Determines if this text is rendered in a world relative location. When false it will render + /// screen relative. + /// + /// + /// Overrides this method to draw to the specified instead of + /// the screen. + /// + /// The to use to render the border of this text with. + void DrawString( + string text, + GameFont gameFont, + float x, + float y, + float fontScale, + Color fontColor, + bool worldPos = true, + IGameRenderTexture renderTexture = null, + Color borderColor = null + ); + + /// + /// Draw text to the client display. /// /// The text to render to the screen. - /// The to use to render the text. + /// The to use to render the text. /// The X location on the screen to render the text to. /// The Y location on the screen to render the text to. /// The scale of the font to render the text with. - /// The to use to render the text with. - /// Determines if this text is rendered in a world relative location. When false it will render screen relative. - /// Overrides this method to draw to the specified instead of the screen. - /// The to use to render the border of this text with. - /// The containing locations that this text can not be rendered outside of, cutting off anything outside of it. - void DrawString(string text, GameFont gameFont, float x, float y, float fontScale, Color fontColor, bool worldPos, GameRenderTexture renderTexture, FloatRect clipRect, Color borderColor = null); - - /// - /// Measures a string of text. + /// The to use to render the text with. + /// + /// Determines if this text is rendered in a world relative location. When false it will render + /// screen relative. + /// + /// + /// Overrides this method to draw to the specified instead of + /// the screen. + /// + /// The to use to render the border of this text with. + /// + /// The containing locations that this text can not be rendered outside of, + /// cutting off anything outside of it. + /// + void DrawString( + string text, + GameFont gameFont, + float x, + float y, + float fontScale, + Color fontColor, + bool worldPos, + IGameRenderTexture renderTexture, + FloatRect clipRect, + Color borderColor = null + ); + + /// + /// Measures a string of text. /// /// The text to measure the size of. - /// The to use to measure the text with. + /// The to use to measure the text with. /// The scale of the font to measure the text with. - /// Returns a containing the width and height of the measured text. + /// Returns a containing the width and height of the measured text. Pointf MeasureText(string text, GameFont gameFont, float fontScale); /// - /// Send a request for the client to take a screenshot the next draw cycle. + /// Send a request for the client to take a screenshot the next draw cycle. /// - /// The directory (relative to the client directory) to store the screenshot in. - void RequestScreenshot(string screenshotDir = "screenshots"); + /// The directory (relative to the client directory) to store the screenshot in. + void RequestScreenshot(string? pathToScreenshots = default); } \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/IGameTexture.cs b/Intersect.Client.Framework/Graphics/IGameTexture.cs new file mode 100644 index 0000000000..56bb65d4cb --- /dev/null +++ b/Intersect.Client.Framework/Graphics/IGameTexture.cs @@ -0,0 +1,33 @@ +using System.Runtime.CompilerServices; +using Intersect.Client.Framework.Content; +using Intersect.Client.Framework.GenericClasses; + +namespace Intersect.Client.Framework.Graphics; + +public interface IGameTexture : IAsset, IComparable, IDisposable +{ + Color this[int x, int y] { get; } + Color this[Point point] { get; } + long AccessTime { get; } + bool IsMissingOrCorrupt { get; } + bool IsPinned { get; } + int Area { get; } + Pointf Dimensions { get; } + FloatRect Bounds { get; } + Pointf Center { get; } + + AtlasReference? AtlasReference + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + int Width { get; } + int Height { get; } + bool Unload(); + object? GetTexture(); + TPlatformTexture? GetTexture() where TPlatformTexture : class; + void Reload(); + Color GetPixel(int x, int y); + string ToString(); +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/Resolution.cs b/Intersect.Client.Framework/Graphics/Resolution.cs index 9faa857c76..0d0469cf4e 100644 --- a/Intersect.Client.Framework/Graphics/Resolution.cs +++ b/Intersect.Client.Framework/Graphics/Resolution.cs @@ -1,12 +1,13 @@ namespace Intersect.Client.Framework.Graphics; - public partial struct Resolution : IComparable { - public static readonly Resolution Empty = default; - private static readonly char[] Separators = { 'x', ',', ' ', '/', '-', '_', '.', '~' }; + private static readonly char[] Separators = + { + 'x', ',', ' ', '/', '-', '_', '.', '~', + }; public readonly ushort X; @@ -25,10 +26,9 @@ public Resolution(ulong x = 800, ulong y = 600) } public Resolution(Resolution resolution, long overrideX = 0, long overrideY = 0) - : this( - overrideX > 0 ? overrideX : resolution.X, overrideY > 0 ? overrideY : resolution.Y - ) - { } + : this(overrideX > 0 ? overrideX : resolution.X, overrideY > 0 ? overrideY : resolution.Y) + { + } public Resolution(Resolution resolution, Resolution? overrideResolution = null) { @@ -45,13 +45,25 @@ public int CompareTo(Resolution other) return diffX != 0 ? diffX : Y - other.Y; } - public override bool Equals(object obj) => obj is Resolution resolution && Equals(resolution); + public override bool Equals(object obj) + { + return obj is Resolution resolution && Equals(resolution); + } - public bool Equals(Resolution other) => X == other.X && Y == other.Y; + public bool Equals(Resolution other) + { + return X == other.X && Y == other.Y; + } - public override int GetHashCode() => (X << 16) & Y; + public override int GetHashCode() + { + return (X << 16) & Y; + } - public override string ToString() => $"{X},{Y}"; + public override string ToString() + { + return $"{X},{Y}"; + } public static Resolution Parse(string resolution) { @@ -88,9 +100,13 @@ public static bool TryParse(string resolutionString, out Resolution resolution) } } - public static bool operator ==(Resolution left, Resolution right) => - left.X == right.X && left.Y == right.Y; + public static bool operator ==(Resolution left, Resolution right) + { + return left.X == right.X && left.Y == right.Y; + } - public static bool operator !=(Resolution left, Resolution right) => - left.X != right.X && left.Y != right.Y; -} + public static bool operator !=(Resolution left, Resolution right) + { + return left.X != right.X && left.Y != right.Y; + } +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/ScreenshotRequest.cs b/Intersect.Client.Framework/Graphics/ScreenshotRequest.cs new file mode 100644 index 0000000000..70fd086117 --- /dev/null +++ b/Intersect.Client.Framework/Graphics/ScreenshotRequest.cs @@ -0,0 +1,3 @@ +namespace Intersect.Client.Framework.Graphics; + +public record struct ScreenshotRequest(Func StreamFactory, string? Hint = default); \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/TextureEventArgs.cs b/Intersect.Client.Framework/Graphics/TextureEventArgs.cs new file mode 100644 index 0000000000..6542c913a2 --- /dev/null +++ b/Intersect.Client.Framework/Graphics/TextureEventArgs.cs @@ -0,0 +1,6 @@ +namespace Intersect.Client.Framework.Graphics; + +public class TextureEventArgs(IGameTexture gameTexture) : EventArgs +{ + public IGameTexture GameTexture { get; } = gameTexture; +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Graphics/WriteTextureToStreamDelegate.cs b/Intersect.Client.Framework/Graphics/WriteTextureToStreamDelegate.cs new file mode 100644 index 0000000000..808d67aec4 --- /dev/null +++ b/Intersect.Client.Framework/Graphics/WriteTextureToStreamDelegate.cs @@ -0,0 +1,3 @@ +namespace Intersect.Client.Framework.Graphics; + +public delegate bool WriteTextureToStreamDelegate(Stream stream, string? hint); \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/Base.DataProvider.cs b/Intersect.Client.Framework/Gwen/Control/Base.DataProvider.cs index c9fa03c23c..1ea1cfb3ee 100644 --- a/Intersect.Client.Framework/Gwen/Control/Base.DataProvider.cs +++ b/Intersect.Client.Framework/Gwen/Control/Base.DataProvider.cs @@ -8,7 +8,13 @@ namespace Intersect.Client.Framework.Gwen.Control; public partial class Base { private readonly HashSet _dataProviders = []; - private readonly Dictionary _updatableDataProviders = []; + private readonly Dictionary _updatableDataProviderRefCounts = []; + + private struct DataProviderRefCount + { + public int Listening; + public int Visible; + } /// /// Adds a to this node. @@ -24,7 +30,11 @@ public bool AddDataProvider(IDataProvider dataProvider) if (dataProvider is IUpdatableDataProvider updatableDataProvider) { - AddUpdatableDataProvider(updatableDataProvider); + ListenUpdatableDataProvider(updatableDataProvider, true); + if (IsVisibleInTree) + { + VisibleUpdatableDataProvider(updatableDataProvider, true); + } } return true; @@ -44,66 +54,101 @@ public bool RemoveDataProvider(IDataProvider dataProvider) if (dataProvider is IUpdatableDataProvider updatableDataProvider) { - RemoveUpdatableDataProvider(updatableDataProvider); + ListenUpdatableDataProvider(updatableDataProvider, false); + if (IsVisibleInTree) + { + VisibleUpdatableDataProvider(updatableDataProvider, false); + } } return true; } - private void AddUpdatableDataProvider(IUpdatableDataProvider updatableDataProvider) + private void ListenUpdatableDataProviders(IEnumerable providers, bool listen) + { + foreach (var provider in providers) + { + ListenUpdatableDataProvider(provider, listen); + } + } + + private void ListenUpdatableDataProvider(IUpdatableDataProvider provider, bool listen) { - var refCount = _updatableDataProviders.GetValueOrDefault(updatableDataProvider, 0); - ++refCount; - _updatableDataProviders[updatableDataProvider] = refCount; + var refCount = _updatableDataProviderRefCounts.GetValueOrDefault(key: provider, defaultValue: default); + if (listen) + { + ++refCount.Listening; + } + else + { + --refCount.Listening; + } + + if (refCount.Listening > 0) + { + _updatableDataProviderRefCounts[key: provider] = refCount; + } + else + { + _updatableDataProviderRefCounts.Remove(key: provider); + } var root = Root; if (root != this) { - root.AddUpdatableDataProvider(updatableDataProvider); + root.ListenUpdatableDataProvider(provider: provider, listen: listen); } } - private void AddUpdatableDataProviders(IEnumerable updatableDataProviders) + private void VisibleUpdatableDataProviders(IEnumerable providers, bool visible) { - foreach (var updatableDataProvider in updatableDataProviders) + foreach (var provider in providers) { - AddUpdatableDataProvider(updatableDataProvider); + VisibleUpdatableDataProvider(provider, visible); } } - private void RemoveUpdatableDataProvider(IUpdatableDataProvider updatableDataProvider) + private void VisibleUpdatableDataProvider(IUpdatableDataProvider updatableDataProvider, bool visible) { - var refCount = _updatableDataProviders.GetValueOrDefault(updatableDataProvider, 0); - --refCount; + if (!_updatableDataProviderRefCounts.TryGetValue(updatableDataProvider, out var refCount)) + { + throw new InvalidOperationException("Cannot mark a data provider as visible before listening to it"); + } - if (refCount > 0) + if (visible) + { + ++refCount.Visible; + } + else if (refCount.Visible > 0) { - _updatableDataProviders[updatableDataProvider] = refCount; + --refCount.Visible; } else { - _updatableDataProviders.Remove(updatableDataProvider); + ApplicationContext.CurrentContext.Logger.LogError( + "Tried to decrease visible count below 0 of a data provider" + ); } + _updatableDataProviderRefCounts[key: updatableDataProvider] = refCount; + var root = Root; if (root != this) { - root.RemoveUpdatableDataProvider(updatableDataProvider); + root.VisibleUpdatableDataProvider(updatableDataProvider: updatableDataProvider, visible: visible); } } - private void RemoveUpdatableDataProviders(IEnumerable updatableDataProviders) - { - foreach (var updatableDataProvider in updatableDataProviders) - { - RemoveUpdatableDataProvider(updatableDataProvider); - } - } protected void UpdateDataProviders(TimeSpan elapsed, TimeSpan total) { - foreach (var (dataProvider, _) in _updatableDataProviders) + foreach (var (dataProvider, refCount) in _updatableDataProviderRefCounts) { + if (refCount.Visible < 1) + { + continue; + } + try { if (dataProvider.TryUpdate(elapsed, total)) diff --git a/Intersect.Client.Framework/Gwen/Control/Base.Threading.cs b/Intersect.Client.Framework/Gwen/Control/Base.Threading.cs new file mode 100644 index 0000000000..202d4080f1 --- /dev/null +++ b/Intersect.Client.Framework/Gwen/Control/Base.Threading.cs @@ -0,0 +1,83 @@ +using Intersect.Framework.Threading; + +namespace Intersect.Client.Framework.Gwen.Control; + +public partial class Base +{ + private readonly ThreadQueue _threadQueue; + private readonly ManualActionQueueParent _preLayoutActionsParent = new(); + public readonly ManualActionQueue PreLayout; + private readonly ManualActionQueueParent _postLayoutActionsParent = new(); + public readonly ManualActionQueue PostLayout; + + private int _pendingThreadQueues; + + private void UpdatePendingThreadQueues(int increment) + { + _pendingThreadQueues = Math.Max(0, _pendingThreadQueues + increment); + Parent?.UpdatePendingThreadQueues(increment); + } + + public void RunOnMainThread(Action action) + { + Invalidate(); + _threadQueue.RunOnMainThread(action); + } + + public void RunOnMainThread(Action action) => RunOnMainThread(action, this); + + public void RunOnMainThread(Action action, TState state) + { + Invalidate(); + _threadQueue.RunOnMainThread(action, state); + } + + public TReturn RunOnMainThread(Func func) + { + Invalidate(); + return _threadQueue.RunOnMainThread(func, this); + } + + public void RunOnMainThread(Action action, TState state) + { + Invalidate(); + _threadQueue.RunOnMainThread(action, this, state); + } + + public TReturn RunOnMainThread( + Func func, + TState state, + bool invalidate = true + ) + { + if (invalidate) + { + Invalidate(); + } + + return _threadQueue.RunOnMainThread(func, this, state); + } + + public void RunOnMainThread(Action action, TState0 state0, TState1 state1) + { + Invalidate(); + _threadQueue.RunOnMainThread(action, state0, state1); + } + + public void RunOnMainThread(Action action, TState0 state0, TState1 state1) + { + Invalidate(); + _threadQueue.RunOnMainThread(action, this, state0, state1); + } + + public void RunOnMainThread( + Action action, + TState0 state0, + TState1 state1, + TState2 state2 + ) + { + Invalidate(); + _threadQueue.RunOnMainThread(action, state0, state1, state2); + } +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/Base.cs b/Intersect.Client.Framework/Gwen/Control/Base.cs index 500fa19cad..0da5886a66 100644 --- a/Intersect.Client.Framework/Gwen/Control/Base.cs +++ b/Intersect.Client.Framework/Gwen/Control/Base.cs @@ -1,10 +1,12 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Text; using Intersect.Client.Framework.File_Management; using Intersect.Client.Framework.GenericClasses; using Intersect.Client.Framework.Graphics; using Intersect.Client.Framework.Gwen.Anim; using Intersect.Client.Framework.Gwen.Control.EventArguments; +using Intersect.Client.Framework.Gwen.Control.Layout; using Intersect.Client.Framework.Gwen.ControlInternal; using Intersect.Client.Framework.Gwen.DragDrop; using Intersect.Client.Framework.Gwen.Input; @@ -16,17 +18,26 @@ using Intersect.Client.Framework.Input; using Intersect.Core; using Intersect.Framework; +using Intersect.Framework.Collections; using Intersect.Framework.Reflection; using Intersect.Framework.Serialization; +using Intersect.Framework.Threading; using Microsoft.Extensions.Logging; namespace Intersect.Client.Framework.Gwen.Control; +public record struct NodePair(Base This, Base? Parent); + /// /// Base control class. /// public partial class Base : IDisposable { + public delegate void GwenEventHandler(Base sender, TArgs arguments) where TArgs : EventArgs; + + public delegate void GwenEventHandler(TSender sender, TArgs arguments) + where TSender : Base where TArgs : EventArgs; + private const string PropertyNameInnerPanel = "InnerPanel"; private bool _inheritParentEnablementProperties; @@ -42,107 +53,139 @@ internal bool InheritParentEnablementProperties } } - public delegate void GwenEventHandler(Base sender, TArgs arguments) where TArgs : EventArgs; - - public delegate void GwenEventHandler(TSender sender, TArgs arguments) - where TSender : Base where TArgs : EventArgs; - - /// - /// Accelerator map. - /// - private readonly Dictionary> mAccelerators; - - /// - /// Real list of children. - /// - private readonly List mChildren; + private bool _disposed; + private bool _disposeCompleted; + private StackTrace? _disposeStack; private Canvas? _canvas; - /// - /// This is the panel's actual parent - most likely the logical - /// parent's InnerPanel (if it has one). You should rarely need this. - /// - private Base? mActualParent; + protected Modal? _modal; + private Base? _previousParent; + private Base? _parent; + private Base? _host { get; set; } + protected Base? _innerPanel; + private readonly List _children = []; - private List _alignments = []; - private Padding _alignmentPadding; - private Point _alignmentTranslation; + private bool _disabled; + private bool _skipRender; + private bool _visible; - private Rectangle mBounds; - private Rectangle mBoundsOnDisk; + private Package? _dragPayload; + private object? _userData; - private bool mCacheTextureDirty; + private bool _drawDebugOutlines; - private bool mCacheToTexture; + private readonly Dictionary> _accelerators = []; + private bool _keyboardInputEnabled; + private bool _mouseInputEnabled; + private bool _tabEnabled; - private Color mColor; + private Rectangle _bounds; + private Rectangle _boundsOnDisk; + private Rectangle _innerBounds; + private Rectangle _outerBounds; + private Rectangle _renderBounds; - private Cursor mCursor; + private Point _maximumSize; + private Point _minimumSize; + private bool _restrictToParent; - private bool _disabled; + private Margin _margin; + private Padding _padding; - private bool mDisposed; + private List _alignments = []; + private Padding _alignmentPadding; + private Point _alignmentTranslation; private Pos _dock; private Padding _dockChildSpacing; - private Package mDragAndDrop_package; - - private bool mDrawBackground = true; - - private bool mDrawDebugOutlines; - - private bool mHidden; - private bool _skipRender; - - private bool mHideToolTip; - - private Rectangle _outerBounds; - private Rectangle mInnerBounds; - - /// - /// If the innerpanel exists our children will automatically become children of that - /// instead of us - allowing us to move them all around by moving that panel (useful for scrolling etc). - /// - protected Base? _innerPanel; + private string? _name; + private string? _cachedToString; - private bool mKeyboardInputEnabled; + private bool _cacheTextureDirty; + private bool _cacheToTexture; - private Margin mMargin; + private bool _needsAlignment; + private bool _layoutDirty; + private bool _dockDirty; - private Point _maximumSize = default; + private bool _drawBackground; + private Color _color; + private Cursor _cursor; + private Skin.Base? _skin; - private Point _minimumSize = default; + private Base? _tooltip; + private bool _tooltipEnabled; + private IGameTexture? _tooltipBackground { get; set; } + private string? _tooltipBackgroundName; + private GameFont? _tooltipFont; + private string? _tooltipFontInfo; + private Color? _tooltipTextColor; - private bool mMouseInputEnabled; + protected virtual string InternalToString() + { + StringBuilder builder = new(); - private string? _name; + builder.Append(GetType().GetName(qualified: false)); - private bool _needsAlignment; - private bool _needsLayout; + var name = _name?.Trim(); + if (!string.IsNullOrWhiteSpace(name)) + { + builder.Append($"({name})"); + } - private Padding mPadding; + return builder.ToString(); + } - protected Modal? mModal; - private Base? mOldParent; - private Base? mParent { get; set; } + /// + /// Initializes a new instance of the class. + /// + /// parent control + /// name of this control + public Base(Base? parent = default, string? name = default) + { + _threadQueue = new ThreadQueue(); + _threadQueue.QueueNotEmpty += () => UpdatePendingThreadQueues(1); + _name = name ?? string.Empty; + _visible = true; + _dockDirty = true; + _layoutDirty = true; + _disabled = false; + _tooltip = null; + _tooltipEnabled = true; + _cacheTextureDirty = true; + _cacheToTexture = false; + _drawBackground = true; - private Rectangle mRenderBounds; + _keyboardInputEnabled = false; + _mouseInputEnabled = true; + _tabEnabled = false; - private bool mRestrictToParent; + PreLayout = new ManualActionQueue(_preLayoutActionsParent); + PostLayout = new ManualActionQueue(_postLayoutActionsParent); - private Skin.Base mSkin; + if (this is Canvas canvas) + { + _canvas = canvas; + } - private bool mTabable; + Parent = parent; - private Base? _tooltip; + _bounds = new Rectangle(0, 0, 10, 10); + _padding = Padding.Zero; + _margin = Margin.Zero; + _color = Color.White; + _alignmentPadding = Padding.Zero; - private string? _tooltipBackgroundName; + RestrictToParent = false; - private GameTexture? _tooltipBackground { get; set; } + _cursor = Cursors.Default; - private Color? _tooltipTextColor; + BoundsOutlineColor = Color.Red; + MarginOutlineColor = Color.Green; + PaddingOutlineColor = Color.Blue; + } public virtual string? TooltipFontName { @@ -172,6 +215,32 @@ public virtual int TooltipFontSize } } + public virtual IGameTexture? TooltipBackground + { + get => _tooltipBackground; + set + { + if (value != _tooltipBackground) + { + return; + } + + if (value?.Name == _tooltipBackgroundName) + { + _tooltipBackground = value; + return; + } + + _tooltipBackground = value; + _tooltipBackgroundName = value?.Name; + + if (Tooltip is Label label) + { + label.ToolTipBackground = _tooltipBackground; + } + } + } + public virtual string? TooltipBackgroundName { get => _tooltipBackgroundName; @@ -183,7 +252,7 @@ public virtual string? TooltipBackgroundName } _tooltipBackgroundName = value; - GameTexture? texture = null; + IGameTexture? texture = null; if (!string.IsNullOrWhiteSpace(_tooltipBackgroundName)) { texture = GameContentManager.Current.GetTexture( @@ -219,57 +288,6 @@ public virtual Color? TooltipTextColor } } - private GameFont? _tooltipFont; - - private string? _tooltipFontInfo; - - private object mUserData; - - /// - /// Initializes a new instance of the class. - /// - /// parent control - /// name of this control - public Base(Base? parent = default, string? name = default) - { - if (this is Canvas canvas) - { - _canvas = canvas; - } - - _name = name ?? string.Empty; - mChildren = []; - mAccelerators = []; - - Parent = parent; - - mHidden = false; - mBounds = new Rectangle(0, 0, 10, 10); - mPadding = Padding.Zero; - mMargin = Margin.Zero; - mColor = Color.White; - _alignmentPadding = Padding.Zero; - - RestrictToParent = false; - - MouseInputEnabled = true; - KeyboardInputEnabled = false; - - Invalidate(); - Cursor = Cursors.Default; - - //ToolTip = null; - IsTabable = false; - ShouldDrawBackground = true; - _disabled = false; - mCacheTextureDirty = true; - mCacheToTexture = false; - - BoundsOutlineColor = Color.Red; - MarginOutlineColor = Color.Green; - PaddingOutlineColor = Color.Blue; - } - /// /// Font. /// @@ -293,43 +311,70 @@ public GameFont? TooltipFont /// /// Logical list of children. If InnerPanel is not null, returns InnerPanel's children. /// - public List Children => _innerPanel?.Children ?? mChildren; + public IReadOnlyList Children => _innerPanel?.Children ?? _children; /// /// The logical parent. It's usually what you expect, the control you've parented it to. /// public Base? Parent { - get => mParent; + get => _host; set { - if (mParent == value) + if (ReferenceEquals(_host, value)) { return; } - if (mParent is { } oldParent) + if (_host is { } oldParent) { - NotifyDetachingFromRoot(oldParent.Root); - NotifyDetaching(oldParent); - oldParent.RemoveChild(this, false); + // Detach from the previous parent on their thread + oldParent.RunOnMainThread(ProcessDetachingFromParent, new NodePair(this, oldParent)); } - PropagateCanvas(value?._canvas); - - mParent = value; - mActualParent = default; - - if (mParent is not { } newParent) + if (value is null) { - return; + // If we aren't being attached to a new parent, run this immediately + ProcessAttachingToParent(this, null); } + else + { + // Otherwise, we should wait until it's run on the new parent's thread + value.RunOnMainThread(ProcessAttachingToParent, new NodePair(this, value)); + } + } + } + + private static void ProcessDetachingFromParent(NodePair pair) + { + pair.This.NotifyDetachingFromRoot(pair.Parent!.Root); + pair.This.NotifyDetaching(pair.Parent); + pair.Parent.RemoveChild(pair.This, false); + } + + private static void ProcessAttachingToParent(NodePair pair) => ProcessAttachingToParent(pair.This, pair.Parent); + + private static void ProcessAttachingToParent(Base @this, Base? parent) + { + if (parent?._threadQueue is { } parentThreadQueue) + { + @this._threadQueue.SetMainThreadId(parentThreadQueue); + } - newParent.AddChild(this); + @this.PropagateCanvas(parent?._canvas); - NotifyAttachingToRoot(newParent.Root); - NotifyAttaching(newParent); + @this._host = parent; + @this._parent = default; + + if (parent is not { } newParent) + { + return; } + + parent.AddChild(@this); + + @this.NotifyAttachingToRoot(parent.Root); + @this.NotifyAttaching(parent); } protected virtual void OnAttached(Base parent) @@ -344,6 +389,7 @@ private void NotifyAttaching(Base parent) { try { + parent.UpdatePendingThreadQueues(_pendingThreadQueues); OnAttaching(parent); } catch (Exception exception) @@ -368,6 +414,7 @@ private void NotifyDetaching(Base parent) { try { + parent.UpdatePendingThreadQueues(-_pendingThreadQueues); OnDetaching(parent); } catch (Exception exception) @@ -386,7 +433,11 @@ private void NotifyDetachingFromRoot(Base root) { try { - Root.RemoveUpdatableDataProviders(_updatableDataProviders.Keys); + var visibleProviders = _updatableDataProviderRefCounts.Where(e => e.Value.Visible > 0) + .Select(e => e.Key) + .ToArray(); + root.VisibleUpdatableDataProviders(visibleProviders, false); + root.ListenUpdatableDataProviders(_updatableDataProviderRefCounts.Keys, false); try { @@ -401,7 +452,7 @@ private void NotifyDetachingFromRoot(Base root) ); } - foreach (var child in mChildren) + foreach (var child in _children) { child.NotifyDetachingFromRoot(root); } @@ -423,7 +474,11 @@ private void NotifyAttachingToRoot(Base root) { try { - Root.AddUpdatableDataProviders(_updatableDataProviders.Keys); + var visibleProviders = _updatableDataProviderRefCounts.Where(e => e.Value.Visible > 0) + .Select(e => e.Key) + .ToArray(); + Root.ListenUpdatableDataProviders(_updatableDataProviderRefCounts.Keys, true); + Root.VisibleUpdatableDataProviders(visibleProviders, true); try { @@ -438,7 +493,7 @@ private void NotifyAttachingToRoot(Base root) ); } - foreach (var child in mChildren) + foreach (var child in _children) { child.NotifyAttachingToRoot(root); } @@ -460,8 +515,7 @@ private void PropagateCanvas(Canvas? canvas) { _canvas = canvas; - var children = mChildren.ToArray(); - foreach (var child in children) + foreach (var child in _children) { child.PropagateCanvas(canvas); } @@ -518,6 +572,8 @@ public void InvalidateAlignment() Invalidate(); } + protected static void InvalidateAlignment(Base @this) => @this.InvalidateAlignment(); + public virtual bool IsBlockingInput => true; /// @@ -537,8 +593,8 @@ public Pos Dock _dock = value; OnDockChanged(oldDock, value); - Invalidate(); - InvalidateParent(); + InvalidateDock(); + InvalidateParentDock(); } } @@ -559,7 +615,9 @@ public Padding DockChildSpacing } } - protected bool HasSkin => mSkin != null || (mParent?.HasSkin ?? false); + protected bool HasSkin => _skin != null || (_host?.HasSkin ?? false); + + protected Skin.Base? SafeSkin => _skin ?? _parent?.SafeSkin; /// /// Current skin. @@ -568,20 +626,17 @@ public Skin.Base Skin { get { - if (mSkin != null) - { - return mSkin; - } - - if (mParent != null) + if (SafeSkin is { } skin) { - return mParent.Skin; + return skin; } throw new InvalidOperationException("GetSkin: null"); } } + private Skin.Base? DisposeSkin => _skin ?? _host?.DisposeSkin; + /// /// Current tooltip. /// @@ -646,12 +701,12 @@ internal virtual bool IsMenuComponent { get { - if (mParent == null) + if (_host == null) { return false; } - return mParent.IsMenuComponent; + return _host.IsMenuComponent; } } @@ -665,15 +720,15 @@ internal virtual bool IsMenuComponent /// public Padding Padding { - get => mPadding; + get => _padding; set { - if (mPadding == value) + if (_padding == value) { return; } - mPadding = value; + _padding = value; Invalidate(); InvalidateParent(); } @@ -693,15 +748,15 @@ public Padding AlignmentPadding /// public Color RenderColor { - get => mColor; + get => _color; set { - if (mColor == value) + if (_color == value) { return; } - mColor = value; + _color = value; Invalidate(); InvalidateParent(); } @@ -712,15 +767,15 @@ public Color RenderColor /// public Margin Margin { - get => mMargin; + get => _margin; set { - if (mMargin == value) + if (_margin == value) { return; } - mMargin = value; + _margin = value; Invalidate(); InvalidateParent(); } @@ -729,18 +784,15 @@ public Margin Margin /// /// Indicates whether the control is on top of its parent's children. /// - public virtual bool IsOnTop - - // todo: validate - => this == Parent.mChildren.First(); + public bool IsOnTop => Parent?.IndexOf(this) == (Parent?.Children.Count ?? -1); /// /// User data associated with the control. /// public object? UserData { - get => mUserData; - set => mUserData = value; + get => _userData; + set => _userData = value; } /// @@ -770,35 +822,86 @@ protected virtual void OnDisabledChanged(bool oldValue, bool newValue) /// /// Indicates whether the control is hidden. /// - public virtual bool IsHidden + public bool IsHidden { - get => ((_inheritParentEnablementProperties && Parent is {} parent) ? parent.IsHidden : mHidden); - set + get => ((_inheritParentEnablementProperties && Parent is {} parent) ? parent.IsHidden : !_visible); + set => RunOnMainThread(SetVisible, !value); + } + + private static void SetVisible(Base @this, bool value) + { + var wasVisibleInParent = @this._visible; + + if (@this is { _inheritParentEnablementProperties: true, Parent: { } parent }) { - if (value == mHidden) + if (value != parent._visible) { - // ApplicationContext.CurrentContext.Logger.LogTrace( - // "{ComponentTypeName} (\"{ComponentName}\") set to same visibility ({Visibility})", - // GetType().GetName(qualified: true), - // CanonicalName, - // !value - // ); - return; + ApplicationContext.CurrentContext.Logger.LogTrace( + "Tried to change visibility of restricted node '{NodeName}' to {Visible} when the parent ({ParentNodeName}) is set to {ParentVisible}", + @this.CanonicalName, + value, + parent.CanonicalName, + !value + ); } - var hidden = _inheritParentEnablementProperties ? (Parent?.IsHidden ?? value) : value; - mHidden = hidden; + value = parent._visible; + } + + if (value == wasVisibleInParent) + { + // ApplicationContext.CurrentContext.Logger.LogTrace( + // "{ComponentTypeName} (\"{ComponentName}\") set to same visibility ({Visibility})", + // GetType().GetName(qualified: true), + // CanonicalName, + // !value + // ); + + // Skip the rest of this method since nothing actually changed for this node + return; + } - VisibilityChangedEventArgs eventArgs = new() + var visibilityInTreeChanging = value != @this.IsVisibleInTree; + + @this._visible = value; + + if (visibilityInTreeChanging) + { + @this.NotifyVisibilityInTreeChanged(value); + } + + if (@this._dock != default) + { + @this.InvalidateParentDock(); + } + + VisibilityChangedEventArgs eventArgs = new(value, visibilityInTreeChanging ? value : !value); + @this.OnVisibilityChanged(@this, eventArgs); + @this.VisibilityChanged?.Invoke(@this, eventArgs); + + @this.Invalidate(); + @this.InvalidateParent(); + } + + private void NotifyVisibilityInTreeChanged(bool visible) + { + VisibleUpdatableDataProviders(_updatableDataProviderRefCounts.Keys, visible); + + foreach (var child in _children) + { + if (child._inheritParentEnablementProperties) { - IsVisible = !hidden, - }; + SetVisible(child, _visible); + continue; + } - OnVisibilityChanged(this, eventArgs); - VisibilityChanged?.Invoke(this, eventArgs); + if (!child._visible) + { + continue; + } - Invalidate(); - InvalidateParent(); + child.NotifyVisibilityInTreeChanged(visible); + child.VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs(child._visible, visible)); } } @@ -809,7 +912,7 @@ protected virtual void OnVisibilityChanged(object? sender, VisibilityChangedEven protected virtual Point InnerPanelSizeFrom(Point size) => size; - public virtual bool IsHiddenByTree => mHidden || (Parent?.IsHiddenByTree ?? false); + public virtual bool IsHiddenByTree => !_visible || (Parent?.IsHiddenByTree ?? false); public virtual bool IsDisabledByTree => _disabled || (Parent?.IsDisabledByTree ?? false); @@ -818,8 +921,8 @@ protected virtual void OnVisibilityChanged(object? sender, VisibilityChangedEven /// public bool RestrictToParent { - get => mRestrictToParent; - set => mRestrictToParent = value; + get => _restrictToParent; + set => _restrictToParent = value; } /// @@ -827,15 +930,15 @@ public bool RestrictToParent /// public bool MouseInputEnabled { - get => mMouseInputEnabled; + get => _mouseInputEnabled; set { - if (value == mMouseInputEnabled) + if (value == _mouseInputEnabled) { return; } - mMouseInputEnabled = value; + _mouseInputEnabled = value; } } @@ -844,8 +947,8 @@ public bool MouseInputEnabled /// public bool KeyboardInputEnabled { - get => mKeyboardInputEnabled; - set => mKeyboardInputEnabled = value; + get => _keyboardInputEnabled; + set => _keyboardInputEnabled = value; } /// @@ -853,15 +956,15 @@ public bool KeyboardInputEnabled /// public Cursor Cursor { - get => mCursor; - set => mCursor = value; + get => _cursor; + set => _cursor = value; } public int GlobalX => X + (Parent?.GlobalX ?? 0); public int GlobalY => Y + (Parent?.GlobalY ?? 0); - public Point PositionGlobal => new Point(X, Y) + (mActualParent?.PositionGlobal ?? Point.Empty); + public Point PositionGlobal => new Point(X, Y) + (_parent?.PositionGlobal ?? Point.Empty); public Rectangle GlobalBounds => new(PositionGlobal, Size); @@ -870,8 +973,8 @@ public Cursor Cursor /// public bool IsTabable { - get => mTabable; - set => mTabable = value; + get => _tabEnabled; + set => _tabEnabled = value; } /// @@ -879,8 +982,8 @@ public bool IsTabable /// public bool ShouldDrawBackground { - get => mDrawBackground; - set => mDrawBackground = value; + get => _drawBackground; + set => _drawBackground = value; } /// @@ -888,18 +991,18 @@ public bool ShouldDrawBackground /// public bool ShouldCacheToTexture { - get => mCacheToTexture; - set => mCacheToTexture = value; + get => _cacheToTexture; + set => SetAndDoIfChanged(ref _cacheToTexture, value, Invalidate); } /// /// Gets the control's internal canonical name. /// public string ParentQualifiedName => - mParent is { } parent ? $"{parent.Name}.{Name}" : Name; + _host is { } parent ? $"{parent.Name}.{Name}" : Name; public string CanonicalName => - (mActualParent ?? mParent) is { } parent + (_parent ?? _host) is { } parent ? $"{parent.CanonicalName}.{Name}" : _name ?? $"(unnamed {GetType().GetName(qualified: true)})"; @@ -911,13 +1014,22 @@ public bool ShouldCacheToTexture public string Name { get => _name ?? $"(unnamed {GetType().Name})"; - set => _name = value; + set + { + if (_name == value) + { + return; + } + + _name = value; + _cachedToString = null; + } } /// /// Control's size and position relative to the parent. /// - public Rectangle Bounds => mBounds; + public Rectangle Bounds => _bounds; public Rectangle OuterBounds => _outerBounds; @@ -926,12 +1038,12 @@ public string Name /// /// Bounds for the renderer. /// - public Rectangle RenderBounds => mRenderBounds; + public Rectangle RenderBounds => _renderBounds; /// /// Bounds adjusted by padding. /// - public Rectangle InnerBounds => mInnerBounds; + public Rectangle InnerBounds => _innerBounds; /// /// Size restriction. @@ -942,12 +1054,18 @@ public Point MinimumSize set { var oldValue = _minimumSize; + if (oldValue == value) + { + return; + } + _minimumSize = value; if (_innerPanel != null) { _innerPanel.MinimumSize = InnerPanelSizeFrom(value); } OnMinimumSizeChanged(oldValue, value); + Invalidate(alsoInvalidateParent: true); } } @@ -960,12 +1078,18 @@ public Point MaximumSize set { var oldValue = _maximumSize; + if (oldValue == value) + { + return; + } + _maximumSize = value; if (_innerPanel != null) { _innerPanel.MaximumSize = InnerPanelSizeFrom(value); } OnMaximumSizeChanged(oldValue, value); + Invalidate(alsoInvalidateParent: true); } } @@ -981,24 +1105,24 @@ public Point MaximumSize /// /// Indicates whether the control and its parents are visible. /// - public bool IsVisible + public bool IsVisibleInTree { get { - if (IsHidden) + if (!_visible) { return false; } - return Parent is not { } parent || parent.IsVisible || ToolTip.IsActiveTooltip(parent); + return Parent is not { } parent || parent.IsVisibleInTree || ToolTip.IsActiveTooltip(parent); } - set => IsHidden = !value; + set => RunOnMainThread(SetVisible, value); } public bool IsVisibleInParent { get => !IsHidden || Parent is not { } parent || ToolTip.IsActiveTooltip(parent); - set => IsHidden = false; + set => RunOnMainThread(SetVisible, value); } /// @@ -1006,7 +1130,7 @@ public bool IsVisibleInParent /// public int X { - get => mBounds.X; + get => _bounds.X; set => SetPosition(value, Y); } @@ -1015,7 +1139,7 @@ public int X /// public int Y { - get => mBounds.Y; + get => _bounds.Y; set => SetPosition(X, value); } @@ -1023,13 +1147,13 @@ public int Y public int Width { - get => mBounds.Width; + get => _bounds.Width; set => SetSize(value, Height); } public int Height { - get => mBounds.Height; + get => _bounds.Height; set => SetSize(Width, value); } @@ -1039,21 +1163,21 @@ public int Height public Point Size { - get => mBounds.Size; + get => _bounds.Size; set => SetSize(value.X, value.Y); } - public int InnerWidth => mBounds.Width - (mPadding.Left + mPadding.Right); + public int InnerWidth => _bounds.Width - (_padding.Left + _padding.Right); - public int MaximumInnerWidth => _maximumSize.X - (mPadding.Left + mPadding.Right); + public int MaximumInnerWidth => _maximumSize.X - (_padding.Left + _padding.Right); - public int InnerHeight => mBounds.Height - (mPadding.Top + mPadding.Bottom); + public int InnerHeight => _bounds.Height - (_padding.Top + _padding.Bottom); - public int MaximumInnerHeight => _maximumSize.Y - (mPadding.Top + mPadding.Bottom); + public int MaximumInnerHeight => _maximumSize.Y - (_padding.Top + _padding.Bottom); - public int Bottom => mBounds.Bottom + mMargin.Bottom; + public int Bottom => _bounds.Bottom + _margin.Bottom; - public int Right => mBounds.Right + mMargin.Right; + public int Right => _bounds.Right + _margin.Right; /// /// Determines whether margin, padding and bounds outlines for the control will be drawn. Applied recursively to all @@ -1061,18 +1185,24 @@ public Point Size /// public bool DrawDebugOutlines { - get => mDrawDebugOutlines; + get => _drawDebugOutlines; set { - mDrawDebugOutlines = value; + _drawDebugOutlines = value; if (_innerPanel is { } innerPanel) { innerPanel.DrawDebugOutlines = value; } - foreach (var child in mChildren) - { - child.DrawDebugOutlines = value; - } + + RunOnMainThread(PropagateDrawDebugOutlinesToChildren, this, value); + } + } + + private static void PropagateDrawDebugOutlinesToChildren(Base @this, bool drawDebugOutlines) + { + foreach (var child in @this._children) + { + child.DrawDebugOutlines = drawDebugOutlines; } } @@ -1093,20 +1223,29 @@ public bool DrawDebugOutlines /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// - public virtual void Dispose() + public void Dispose() { - ////debug.print("Control.Base: Disposing {0} {1:X}", this, GetHashCode()); - if (mDisposed) + try { - return; + ObjectDisposedException.ThrowIf(_disposed, this); + } + catch + { + throw; } - ICacheToTexture cache = default; + _disposeStack = new StackTrace(fNeedFileInfo: true); + + _disposed = true; + + Dispose(disposing: true); + + ICacheToTexture? cache = default; #pragma warning disable CA1031 // Do not catch general exception types try { - cache = Skin.Renderer.Ctt; + cache = DisposeSkin?.Renderer.Ctt; } catch { @@ -1138,20 +1277,53 @@ public virtual void Dispose() ToolTip.ControlDeleted(this); Animation.Cancel(this); - // [Fix]: "InvalidOperationException: Collection was modified (during iteration); enumeration operation may not execute". - // (Creates an array copy of the children to avoid modifying the collection during iteration). - var children = mChildren.ToArray(); - foreach (var child in children) + DisposeChildrenOf(this); + + GC.SuppressFinalize(this); + + _disposeCompleted = true; + } + + protected virtual void Dispose(bool disposing) + { + } + + private void DisposeChildrenOf(Base target) + { + if (_host is not { _disposeCompleted: false } parent) { - child.Dispose(); + DisposeChildren(target); } + else + { + parent.DisposeChildrenOf(target); + } + } - mChildren?.Clear(); - - _innerPanel?.Dispose(); + private static void DisposeChildren(Base @this) + { + var children = @this._children.ToArray(); + try + { + foreach (var child in @this._children) + { + child.Dispose(); + } + } + catch + { + throw; + } - mDisposed = true; - GC.SuppressFinalize(this); + if (@this is Modal) + { + ApplicationContext.CurrentContext.Logger.LogTrace( + "Clearing {ChildCount} children of Modal {Name}", + @this._children.Count, + @this.CanonicalName + ); + } + @this._children.Clear(); } /// @@ -1188,30 +1360,48 @@ public virtual string GetJsonUI(bool isRoot = false) return JsonConvert.SerializeObject(GetJson(isRoot), Formatting.Indented); } - public virtual JObject? GetJson(bool isRoot = false, bool onlySerializeIfNotEmpty = false) + private static void AddChildrenToJson(Base @this, JObject json) { - JObject children = new(); - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var component in mChildren) + foreach (var node in @this._children) { - if (component == _innerPanel) + if (node == @this._innerPanel) { continue; } - if (component == Tooltip) + if (node == @this.Tooltip) { continue; } - if (!string.IsNullOrEmpty(component.Name) && children[component.Name] == null) + if (node._name is not { } name || string.IsNullOrWhiteSpace(name)) { - children.Add(component.Name, component.GetJson()); + continue; } + + if (json.ContainsKey(name)) + { + ApplicationContext.CurrentContext.Logger.LogWarning( + "Unable to add '{ChildName}' to the JSON of {ParentName} because a sibling shares the same name", + name, + @this.CanonicalName + ); + continue; + } + + var nodeJson = node.GetJson(); + json.Add(node.Name, nodeJson); } + } - if (onlySerializeIfNotEmpty && !children.HasValues) + public virtual JObject? GetJson(bool isRoot = false, bool onlySerializeIfNotEmpty = false) + { + JObject json = new(); + + RunOnMainThread(AddChildrenToJson, this, json); + + if (onlySerializeIfNotEmpty && !json.HasValues) { return null; } @@ -1219,26 +1409,26 @@ public virtual string GetJsonUI(bool isRoot = false) isRoot |= Parent == default; var boundsToWrite = isRoot - ? new Rectangle(mBoundsOnDisk.X, mBoundsOnDisk.Y, mBounds.Width, mBounds.Height) - : mBounds; + ? new Rectangle(_boundsOnDisk.X, _boundsOnDisk.Y, _bounds.Width, _bounds.Height) + : _bounds; var serializedProperties = new JObject( new JProperty(nameof(Bounds), Rectangle.ToString(boundsToWrite)), new JProperty(nameof(Dock), Dock.ToString()), - new JProperty(nameof(Padding), Padding.ToString(mPadding)), + new JProperty(nameof(Padding), Padding.ToString(_padding)), new JProperty("AlignmentEdgeDistances", Padding.ToString(_alignmentPadding)), new JProperty("AlignmentTransform", _alignmentTranslation.ToString()), - new JProperty(nameof(Margin), mMargin.ToString()), - new JProperty(nameof(RenderColor), Color.ToString(mColor)), + new JProperty(nameof(Margin), _margin.ToString()), + new JProperty(nameof(RenderColor), Color.ToString(_color)), new JProperty(nameof(Alignments), string.Join(",", _alignments.ToArray())), - new JProperty("DrawBackground", mDrawBackground), + new JProperty("DrawBackground", _drawBackground), new JProperty(nameof(MinimumSize), _minimumSize.ToString()), new JProperty(nameof(MaximumSize), _maximumSize.ToString()), new JProperty("Disabled", _disabled), - new JProperty("Hidden", mHidden), - new JProperty(nameof(RestrictToParent), mRestrictToParent), - new JProperty(nameof(MouseInputEnabled), mMouseInputEnabled), - new JProperty("HideToolTip", mHideToolTip), + new JProperty("Hidden", !_visible), + new JProperty(nameof(RestrictToParent), _restrictToParent), + new JProperty(nameof(MouseInputEnabled), _mouseInputEnabled), + new JProperty("HideToolTip", !_tooltipEnabled), new JProperty("ToolTipBackground", _tooltipBackgroundName), new JProperty("ToolTipFont", _tooltipFontInfo), new JProperty( @@ -1255,9 +1445,9 @@ public virtual string GetJsonUI(bool isRoot = false) } } - if (children.HasValues) + if (json.HasValues) { - serializedProperties.Add(nameof(Children), children); + serializedProperties.Add(nameof(Children), json); } return FixJson(serializedProperties); @@ -1387,15 +1577,15 @@ public virtual void LoadJson(JToken token, bool isRoot = default) if (obj[nameof(Bounds)] != null) { - mBoundsOnDisk = Rectangle.FromString((string)obj[nameof(Bounds)]); + _boundsOnDisk = Rectangle.FromString((string)obj[nameof(Bounds)]); isRoot = isRoot || Parent == default; if (isRoot) { - SetSize(mBoundsOnDisk.Width, mBoundsOnDisk.Height); + SetSize(_boundsOnDisk.Width, _boundsOnDisk.Height); } else { - SetBounds(ValidateJsonBounds(mBoundsOnDisk)); + SetBounds(ValidateJsonBounds(_boundsOnDisk)); } } @@ -1466,21 +1656,14 @@ public virtual void LoadJson(JToken token, bool isRoot = default) if (obj["HideToolTip"] != null && (bool) obj["HideToolTip"]) { - mHideToolTip = true; + _tooltipEnabled = false; SetToolTipText(null); } - if (obj["ToolTipBackground"] is JValue { Type: JTokenType.String } tooltipBackgroundName) + if (obj[nameof(TooltipBackground)] is JValue { Type: JTokenType.String } tokenTooltipBackgroundName) { - var fileName = tooltipBackgroundName.Value(); - GameTexture texture = null; - if (!string.IsNullOrWhiteSpace(fileName)) - { - texture = GameContentManager.Current?.GetTexture(Content.TextureType.Gui, fileName); - } - - _tooltipBackgroundName = fileName; - _tooltipBackground = texture; + var tooltipBackgroundName = tokenTooltipBackgroundName.Value(); + TooltipBackgroundName = tooltipBackgroundName; } if (obj.TryGetValue(nameof(TooltipFont), out var tokenTooltipFont) && tokenTooltipFont is JValue { Type: JTokenType.String } valueTooltipFont) @@ -1517,25 +1700,30 @@ public virtual void LoadJson(JToken token, bool isRoot = default) innerPanel.LoadJson(tokenInnerPanel); } - if (obj[nameof(Children)] != null) + if (obj.TryGetValue(nameof(Children), out var tokenChildren) && tokenChildren is JObject objectChildren) { - var children = obj[nameof(Children)]; - foreach (JProperty tkn in children) - { - var name = tkn.Name; - foreach (var ctrl in mChildren) - { - if (ctrl.Name == name) - { - ctrl.LoadJson(tkn.First); - } - } - } + RunOnMainThread(LoadChildrenJson, this, objectChildren); } Invalidate(alsoInvalidateParent: true); } + private static void LoadChildrenJson(Base @this, JObject objectChildren) + { + foreach (var child in @this._children) + { + if (child._name is not { } name || string.IsNullOrWhiteSpace(name)) + { + continue; + } + + if (objectChildren.TryGetValue(name, out var tokenChild)) + { + child.LoadJson(tokenChild); + } + } + } + public virtual void ProcessAlignments() { foreach (var alignment in _alignments) @@ -1582,11 +1770,6 @@ public virtual void ProcessAlignments() } } - private bool HasNamedChildren() - { - return mChildren?.Any(ctrl => !string.IsNullOrEmpty(ctrl?.Name)) ?? false; - } - public event GwenEventHandler? VisibilityChanged; /// @@ -1635,33 +1818,7 @@ public void DelayedDelete() Canvas?.AddDelayedDelete(this); } - private readonly ConcurrentQueue _deferredActions = []; - - public void Defer(Action action) - { - _deferredActions.Enqueue(action); - Invalidate(); - } - - public override string ToString() - { - if (this is MenuItem) - { - return "[MenuItem: " + (this as MenuItem).Text + "]"; - } - - if (this is Label) - { - return "[Label: " + (this as Label).Text + "]"; - } - - if (this is Text) - { - return "[Text: " + (this as Text).DisplayedText + "]"; - } - - return GetType().ToString(); - } + public override string ToString() => _cachedToString ??= InternalToString(); /// /// Enables the control. @@ -1726,7 +1883,7 @@ public virtual void SetToolTipText(string? text) { var tooltip = Tooltip; - if (mHideToolTip || string.IsNullOrWhiteSpace(text)) + if (!_tooltipEnabled || string.IsNullOrWhiteSpace(text)) { if (Tooltip is { Parent: not null }) { @@ -1785,24 +1942,20 @@ protected virtual void UpdateToolTipProperties() /// Determines whether the operation should be carried recursively. protected virtual void InvalidateChildren(bool recursive = false) { - for (int i = 0; i < mChildren.Count; i++) - { - mChildren[i]?.Invalidate(); - if (recursive) - { - mChildren[i]?.InvalidateChildren(true); - } - } + RunOnMainThread(InvalidateChildren, this, recursive); + + _innerPanel?.Invalidate(); + _innerPanel?.InvalidateChildren(recursive: true); + } - if (_innerPanel != null) + private static void InvalidateChildren(Base @this, bool recursive) + { + foreach (var node in @this._children) { - foreach (var child in _innerPanel.mChildren) + node.Invalidate(); + if (recursive) { - child?.Invalidate(); - if (recursive) - { - child?.InvalidateChildren(true); - } + node.InvalidateChildren(true); } } } @@ -1815,17 +1968,19 @@ protected virtual void InvalidateChildren(bool recursive = false) /// public virtual void Invalidate() { - if (!_needsLayout) + if (!_layoutDirty) { - _needsLayout = true; + _layoutDirty = true; } - if (!mCacheTextureDirty) + if (!_cacheTextureDirty) { - mCacheTextureDirty = true; + _cacheTextureDirty = true; } } + protected static void Invalidate(Base @this) => @this.Invalidate(); + public void Invalidate(bool alsoInvalidateParent) { Invalidate(); @@ -1835,200 +1990,318 @@ public void Invalidate(bool alsoInvalidateParent) } } - public virtual void MoveBefore(Base other) + public void MoveBefore(Base other) { - if (other == this) + if (ReferenceEquals(this, other)) { return; } - if (other.Parent is not {} otherParent) + if (_host is not { } parent) { - // If the other component has no parent we can't move before it return; } - if (Parent is { } parent && parent != otherParent) + parent.RunOnMainThread(MoveChildBeforeOther, this, other); + } + + private static void MoveChildBeforeOther(Base @this, Base childToMove, Base other) + { + if (other.Parent != @this) { - // If we have a parent and the parent is not the same as the other component, we should not move - // This won't be hit if we have no parent, which will be treated as an insertion return; } - var parentChildren = otherParent.Children; - var otherIndex = parentChildren.IndexOf(other); + var children = @this.HostedChildren; + var otherIndex = children.IndexOf(other); if (otherIndex < 0) { ApplicationContext.Context.Value?.Logger.LogError( - "Possible race condition detected: Component '{Other}' is not in its parent's ({Parent}) list of children", - string.IsNullOrWhiteSpace(other.Name) ? other.GetType().GetName(qualified: true) : other.Name, - string.IsNullOrWhiteSpace(otherParent.Name) ? otherParent.GetType().GetName(qualified: true) : otherParent.Name + "Possible race condition detected: '{Other}' is not a child of '{Parent}'", + other.CanonicalName, + @this.CanonicalName ); return; } - var ownIndex = parentChildren.IndexOf(this); + var ownIndex = children.IndexOf(childToMove); + + if (ownIndex < 0) { - // If we have no parent yet, add it - ownIndex = otherParent.AddChild(this); + children.Insert(otherIndex, childToMove); + return; } var insertionIndex = otherIndex; - if (ownIndex < insertionIndex) + if (ownIndex < otherIndex) { - // If the component being moved is already ordered before the other component the insertion - // index will decrease by one after we Remove() below, so we need to decrement it --insertionIndex; + } - if (ownIndex == insertionIndex) - { - // Skip this since we're not actually moving it - return; - } + if (ownIndex == insertionIndex) + { + return; } - _ = parentChildren.Remove(this); - parentChildren.Insert(insertionIndex, this); + children.Remove(childToMove); + children.Insert(insertionIndex, childToMove); } - public virtual void MoveAfter(Base other) + public void MoveAfter(Base other) { - if (other == this) + if (ReferenceEquals(this, other)) { return; } - if (other.Parent is not {} otherParent) + if (_host is not { } parent) { - // If the other component has no parent we can't move before it return; } - if (Parent is { } parent && parent != otherParent) + parent.RunOnMainThread(MoveChildAfterOther, this, other); + } + + private List HostedChildren => _innerPanel?.HostedChildren ?? _children; + + private static void MoveChildAfterOther(Base @this, Base childToMove, Base other) + { + if (other.Parent != @this) { - // If we have a parent and the parent is not the same as the other component, we should not move - // This won't be hit if we have no parent, which will be treated as an insertion return; } - var parentChildren = otherParent.Children; - var otherIndex = parentChildren.IndexOf(other); + var children = @this.HostedChildren; + var otherIndex = children.IndexOf(other); if (otherIndex < 0) { ApplicationContext.Context.Value?.Logger.LogError( - "Possible race condition detected: Component '{Other}' is not in its parent's ({Parent}) list of children", - string.IsNullOrWhiteSpace(other.Name) ? other.GetType().GetName(qualified: true) : other.Name, - string.IsNullOrWhiteSpace(otherParent.Name) ? otherParent.GetType().GetName(qualified: true) : otherParent.Name + "Possible race condition detected: '{Other}' is not a child of '{Parent}'", + other.CanonicalName, + @this.CanonicalName ); return; } - var ownIndex = parentChildren.IndexOf(this); + var ownIndex = children.IndexOf(childToMove); + if (ownIndex < 0) { - // If we have no parent yet, add it - ownIndex = otherParent.AddChild(this); + // If we have no parent yet, insert it + children.Insert(otherIndex + 1, childToMove); + return; } var insertionIndex = otherIndex; if (ownIndex > insertionIndex) { - // If the component being moved is already ordered after the other component the insertion - // index will decrease by one after we Remove() below, so we need to decrement it ++insertionIndex; + } + + if (ownIndex == insertionIndex) + { + return; + } + + children.Remove(childToMove); + children.Insert(insertionIndex, childToMove); + } - if (ownIndex == insertionIndex) + public void SortChildrenBy(Func keySelector) where TSortKey : IComparable + { + _children.Sort( + (a, b) => { - // Skip this since we're not actually moving it - return; + TSortKey keyB = keySelector(b); + + if (keySelector(a) is { } keyA) + { + return keyA.CompareTo(keyB); + } + + // Not sure why no matter what I do that the static analysis refuses to acknowledge that it can be null, + // so I'm just disabling it instead so it doesn't get hit by auto-formatting + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + return -keyB?.CompareTo(default) ?? 0; + } + ); + } + + public void ClearChildren(bool dispose = false) => RunOnMainThread(ClearChildren, this, dispose); + + private static void ClearChildren(Base @this, bool dispose) + { + if (@this is Modal) + { + ApplicationContext.CurrentContext.Logger.LogTrace( + "Clearing {ChildCount} children of Modal {Name}", + @this._children.Count, + @this.CanonicalName + ); + } + + if (dispose) + { + foreach (var child in @this.HostedChildren) + { + child.Dispose(); } } - _ = parentChildren.Remove(this); - parentChildren.Insert(insertionIndex, this); + @this.HostedChildren.Clear(); + @this.Invalidate(); } - /// - /// Sends the control to the bottom of paren't visibility stack. - /// - public virtual void SendToBack() + public void Insert(int index, Base node) => RunOnMainThread(Insert, this, index, node); + + private static void Insert(Base @this, int index, Base node) { - if (mActualParent == null) + @this._children.Insert(index, node); + } + + public void Remove(Base node) => RunOnMainThread(Remove, this, node); + + private static void Remove(Base @this, Base node) + { + var index = @this._children.BinarySearch(node); + if (index < 0) { return; } - if (mActualParent.mChildren.Count == 0) + RemoveAt(@this, index); + } + + public void RemoveAt(int index) => RunOnMainThread(RemoveAt, this, index); + + private static void RemoveAt(Base @this, int index) + { + if (@this is Modal) { - return; + ApplicationContext.CurrentContext.Logger.LogTrace( + "Removing child at {Index} of Modal {Name}", + index, + @this.CanonicalName + ); } + @this._children.RemoveAt(index); + } - if (mActualParent.mChildren.First() == this) + public int IndexOf(Base? node) + { + if (node?.Parent != this) { - return; + return -1; } - mActualParent.mChildren.Remove(this); - mActualParent.mChildren.Insert(0, this); + return RunOnMainThread(IndexOf, node); + } + + private static int IndexOf(Base @this, Base node) => @this._children.IndexOf(node); + + public int IndexOf(Func predicate) => RunOnMainThread(IndexOf, predicate); + + private static int IndexOf(Base @this, Func predicate) + { + for (var index = 0; index < @this._children.Count; ++index) + { + if (predicate(@this._children[index])) + { + return index; + } + } - InvalidateParent(); + return -1; } - /// - /// Brings the control to the top of paren't visibility stack. - /// - public virtual void BringToFront() + public int LastIndexOf(Func predicate) => RunOnMainThread(LastIndexOf, predicate); + + private static int LastIndexOf(Base @this, Func predicate) { - if (mParent != null && mParent is Modal modal) + for (var index = @this._children.Count - 1; index >= 0; ++index) { - modal.BringToFront(); + if (predicate(@this._children[index])) + { + return index; + } } - var last = mActualParent?.mChildren.LastOrDefault(); - if (last == default || last == this) + return -1; + } + + public void ResortChild(Base child, Func keySelector) where TKey : IComparable + { + if (child.Parent != this) { return; } - mActualParent.mChildren.Remove(this); - mActualParent.mChildren.Add(this); - InvalidateParent(); - Redraw(); + RunOnMainThread(ResortChild, this, child, keySelector); } - public virtual void BringNextToControl(Base child, bool behind) + private static void ResortChild(Base @this, Base child, Func keySelector) + where TKey : IComparable => + @this._children.Resort(child, keySelector); + + /// + /// Sends the control to the bottom of paren't visibility stack. + /// + public void SendToBack() => RunOnMainThread(SendToBack, this); + + private static void SendToBack(Base @this) { - if (null == mActualParent) + if (@this._parent == null) { return; } - mActualParent.mChildren.Remove(this); - - // todo: validate - var idx = mActualParent.mChildren.IndexOf(child); - if (idx == mActualParent.mChildren.Count - 1) + if (@this._parent._children.Count == 0) { - BringToFront(); + return; + } + if (@this._parent._children.First() == @this) + { return; } - if (behind) + @this._parent._children.Remove(@this); + @this._parent._children.Insert(0, @this); + + @this.InvalidateParent(); + } + + /// + /// Brings the control to the top of paren't visibility stack. + /// + public void BringToFront() => RunOnMainThread(BringToFront, this); + + private static void BringToFront(Base @this) + { + if (@this._host is Modal modal) { - ++idx; + modal.BringToFront(); + } - if (idx == mActualParent.mChildren.Count - 1) - { - BringToFront(); + var actualParent = @this._parent; + // Using null propagation somehow breaks the static analysis after the return + // ReSharper disable once UseNullPropagation + if (actualParent is null) + { + return; + } - return; - } + var last = actualParent._children.LastOrDefault(); + if (last == default || last == @this) + { + return; } - mActualParent.mChildren.Insert(idx, this); - InvalidateParent(); + actualParent._children.Remove(@this); + actualParent._children.Add(@this); + @this.InvalidateParent(); + @this.Redraw(); } /// @@ -2039,14 +2312,14 @@ public virtual void BringNextToControl(Base child, bool behind) /// The first element that matches the conditions defined by the specified predicate, if found; otherwise, the default value for type . public virtual Base Find(Predicate predicate, bool recurse = false) { - var child = mChildren.Find(predicate); + var child = _children.Find(predicate); if (child != null) { return child; } return recurse - ? mChildren.Select(selectChild => selectChild?.Find(predicate, true)).FirstOrDefault() + ? _children.Select(selectChild => selectChild?.Find(predicate, true)).FirstOrDefault() : default; } @@ -2080,11 +2353,11 @@ public virtual IEnumerable FindAll(Predicate predicate, bool recurse { var children = new List(); - children.AddRange(mChildren.FindAll(predicate)); + children.AddRange(_children.FindAll(predicate)); if (recurse) { - children.AddRange(mChildren.SelectMany(selectChild => selectChild?.FindAll(predicate, true))); + children.AddRange(_children.SelectMany(selectChild => selectChild?.FindAll(predicate, true))); } return children; @@ -2133,30 +2406,30 @@ public virtual IEnumerable FindAll(Predicate predicate, bool recurse /// Determines whether all the background should be dimmed. public void MakeModal(bool dim = false) { - if (mModal != null) + if (_modal != null) { return; } - mModal = new Modal(Canvas) + _modal = new Modal(Canvas) { ShouldDrawBackground = dim, }; - mOldParent = Parent; - Parent = mModal; + _previousParent = Parent; + Parent = _modal; } public void RemoveModal() { - if (mModal == null) + if (_modal is not { } modal) { return; } - Parent = mOldParent; - Canvas?.RemoveChild(mModal, false); - mModal = null; + Parent = _previousParent; + Canvas?.RemoveChild(modal, false); + _modal = null; } /// @@ -2165,28 +2438,30 @@ public void RemoveModal() /// /// If InnerPanel is not null, it will become the parent. /// - /// Control to be added as a child. + /// Control to be added as a child. /// - public virtual int AddChild(Base child) - { - int childIndex; + public void AddChild(Base node) => RunOnMainThread(AddChild, this, node); - if (_innerPanel == null) + private static void AddChild(Base @this, Base node) + { + if (@this._innerPanel == null) { - childIndex = mChildren.Count; - mChildren.Add(child); - child.mActualParent = this; + @this._children.Add(node); + node._parent = @this; } else { - childIndex = _innerPanel.AddChild(child); + @this._innerPanel.AddChild(node); + } + + if (node._dock != default) + { + @this.InvalidateDock(); } - child.DrawDebugOutlines = DrawDebugOutlines; + node.DrawDebugOutlines = @this.DrawDebugOutlines; - OnChildAdded(child); - - return childIndex; + @this.OnChildAdded(node); } /// @@ -2194,28 +2469,55 @@ public virtual int AddChild(Base child) /// /// Child to be removed. /// Determines whether the child should be disposed (added to delayed delete queue). - public virtual void RemoveChild(Base child, bool dispose) + public virtual void RemoveChild(Base child, bool dispose) => RunOnMainThread(RemoveChild, this, child, dispose); + + private static void RemoveChild(Base @this, Base child, bool dispose) { - // If we removed our innerpanel + // If we removed our inner panel // remove our pointer to it - if (_innerPanel == child) + if (@this._innerPanel == child) { - mChildren.Remove(_innerPanel); - _innerPanel.DelayedDelete(); - _innerPanel = null; + try + { + if (@this is Modal) + { + ApplicationContext.CurrentContext.Logger.LogTrace( + "Removing {ChildName} (inner panel) from Modal {Name} (Thread {ThreadId})", + child.CanonicalName, + @this.CanonicalName, + Environment.CurrentManagedThreadId + ); + } + @this._children.Remove(child); + child.DelayedDelete(); + @this._innerPanel = null; + } + catch (NullReferenceException) + { + throw; + } return; } - if (_innerPanel != null && _innerPanel.Children.Contains(child)) + if (@this._innerPanel is { } innerPanel && innerPanel.Children.Contains(child)) { - _innerPanel.RemoveChild(child, dispose); + innerPanel.RemoveChild(child, dispose); return; } - mChildren.Remove(child); - OnChildRemoved(child); + if (@this is Modal) + { + ApplicationContext.CurrentContext.Logger.LogTrace( + "Removing {ChildName} (normal child) from Modal {Name} (Thread {ThreadId})", + child.CanonicalName, + @this.CanonicalName, + Environment.CurrentManagedThreadId + ); + } + @this._children.Remove(child); + @this.OnChildRemoved(child); if (dispose) { @@ -2229,9 +2531,9 @@ public virtual void RemoveChild(Base child, bool dispose) public virtual void DeleteAllChildren() { // todo: probably shouldn't invalidate after each removal - while (mChildren.Count > 0) + while (_children.Count > 0) { - RemoveChild(mChildren[0], true); + RemoveChild(_children[0], true); } } @@ -2420,14 +2722,14 @@ public virtual bool SetBounds(int x, int y, int width, int height) width = Math.Max(0, width); height = Math.Max(0, height); - if (mBounds.X == x && mBounds.Y == y && mBounds.Width == width && mBounds.Height == height) + if (_bounds.X == x && _bounds.Y == y && _bounds.Width == width && _bounds.Height == height) { return false; } var oldBounds = Bounds; - var newBounds = mBounds with + var newBounds = _bounds with { X = x, Y = y, @@ -2448,9 +2750,9 @@ public virtual bool SetBounds(int x, int y, int width, int height) ); } - mBounds = newBounds; + _bounds = newBounds; - var margin = mMargin; + var margin = _margin; Rectangle outerBounds = new(newBounds); outerBounds.X -= margin.Left; outerBounds.Y -= margin.Top; @@ -2475,6 +2777,12 @@ public virtual bool SetBounds(int x, int y, int width, int height) if (oldBounds.Size != newBounds.Size) { + if (_cacheToTexture) + { + Skin.Renderer.Ctt.DisposeCachedTexture(this); + Redraw(); + } + OnSizeChanged(oldBounds.Size, newBounds.Size); SizeChanged?.Invoke( this, @@ -2487,6 +2795,12 @@ public virtual bool SetBounds(int x, int y, int width, int height) ProcessAlignments(); } + if (_dock != Pos.None) + { + InvalidateDock(); + InvalidateParentDock(); + } + BoundsChanged?.Invoke( this, new ValueChangedEventArgs @@ -2499,6 +2813,18 @@ public virtual bool SetBounds(int x, int y, int width, int height) return true; } + public void InvalidateDock() + { + if (!_dockDirty) + { + _dockDirty = true; + } + + Invalidate(); + } + + protected void InvalidateParentDock() => _parent?.InvalidateDock(); + /// /// Positions the control inside its parent. /// @@ -2556,11 +2882,9 @@ protected virtual void OnPositionChanged(Point oldPosition, Point newPosition) /// protected virtual void OnBoundsChanged(Rectangle oldBounds, Rectangle newBounds) { - //Anything that needs to update on size changes - //Iterate my children and tell them I've changed Parent?.OnChildBoundsChanged(this, oldBounds, newBounds); - if (mBounds.Width != oldBounds.Width || mBounds.Height != oldBounds.Height) + if (_bounds.Width != oldBounds.Width || _bounds.Height != oldBounds.Height) { Invalidate(); } @@ -2574,9 +2898,9 @@ protected virtual void OnBoundsChanged(Rectangle oldBounds, Rectangle newBounds) /// protected virtual void OnScaleChanged() { - for (int i = 0; i < mChildren.Count; i++) + for (int i = 0; i < _children.Count; i++) { - mChildren[i].OnScaleChanged(); + _children[i].OnScaleChanged(); } } @@ -2585,6 +2909,10 @@ protected virtual void OnScaleChanged() /// protected virtual void OnChildBoundsChanged(Base child, Rectangle oldChildBounds, Rectangle newChildBounds) { + if ((child.Dock & ~Pos.Fill) != 0) + { + InvalidateDock(); + } } /// @@ -2635,7 +2963,7 @@ protected virtual void DoCacheRender(Skin.Base skin, Base master) } // Render the control and its children if the cache is dirty and the clip region is visible - if (mCacheTextureDirty && renderer.ClipRegionVisible) + if (_cacheTextureDirty && renderer.ClipRegionVisible) { if (ClipContents) { @@ -2655,8 +2983,7 @@ protected virtual void DoCacheRender(Skin.Base skin, Base master) // Render the control Render(skin); - var childrenToRender = mChildren.ToArray(); - childrenToRender = OrderChildrenForRendering(childrenToRender.Where(child => child.IsVisible)); + var childrenToRender = OrderChildrenForRendering(_children.Where(IsNodeVisible)); foreach (var child in childrenToRender) { @@ -2667,7 +2994,7 @@ protected virtual void DoCacheRender(Skin.Base skin, Base master) if (ShouldCacheToTexture) { cache.FinishCacheTexture(this); - mCacheTextureDirty = false; + _cacheTextureDirty = false; } } @@ -2683,6 +3010,8 @@ protected virtual void DoCacheRender(Skin.Base skin, Base master) } } + private static bool IsNodeVisible(Base node) => node.IsVisibleInTree; + /// /// Rendering logic implementation. /// @@ -2691,14 +3020,16 @@ internal virtual void DoRender(Skin.Base skin) { // If this control has a different skin, // then so does its children. - if (mSkin != null) + if (_skin != null) { - skin = mSkin; + skin = _skin; } // Do think Think(); + UpdateColors(); + var render = skin.Renderer; if (render.Ctt != null && ShouldCacheToTexture) @@ -2723,7 +3054,7 @@ internal virtual void RenderDebugOutlinesRecursive(Skin.Base skin) { var oldRenderOffset = skin.Renderer.RenderOffset; skin.Renderer.AddRenderOffset(Bounds); - foreach (var child in mChildren) + foreach (var child in _children) { if (child.IsHidden) { @@ -2795,11 +3126,15 @@ protected virtual void RenderRecursive(Skin.Base skin, Rectangle clipRect) //Render myself first Render(skin); - var childrenToRender = mChildren.ToArray(); - childrenToRender = OrderChildrenForRendering(childrenToRender.Where(child => child.IsVisible)); + var childrenToRender = OrderChildrenForRendering(_children.Where(IsNodeVisible)); foreach (var child in childrenToRender) { + if (child is Label label && (label.Text?.Contains("resources/gui/mainmenu_buttonregister.png") ?? false)) + { + label.ToString(); + } + child.DoRender(skin); } @@ -2822,21 +3157,21 @@ protected virtual void RenderRecursive(Skin.Base skin, Rectangle clipRect) /// Deterines whether to change children skin. public virtual void SetSkin(Skin.Base skin, bool doChildren = false) { - if (mSkin == skin) + if (_skin == skin) { return; } - mSkin = skin; + _skin = skin; Invalidate(); Redraw(); OnSkinChanged(skin); if (doChildren) { - for (int i = 0; i < mChildren.Count; i++) + for (int i = 0; i < _children.Count; i++) { - mChildren[i].SetSkin(skin, true); + _children[i].SetSkin(skin, true); } } } @@ -2855,9 +3190,9 @@ protected virtual void OnSkinChanged(Skin.Base newSkin) /// Scroll delta. protected virtual bool OnMouseWheeled(int delta) { - if (mActualParent != null) + if (_parent != null) { - return mActualParent.OnMouseWheeled(delta); + return _parent.OnMouseWheeled(delta); } return false; @@ -2882,9 +3217,9 @@ internal bool InputMouseWheeled(int delta) /// Scroll delta. protected virtual bool OnMouseHWheeled(int delta) { - if (mActualParent != null) + if (_parent != null) { - return mActualParent.OnMouseHWheeled(delta); + return _parent.OnMouseHWheeled(delta); } return false; @@ -3225,15 +3560,10 @@ protected virtual void OnChildTouched(Base control) } } - var children = mChildren.ToArray(); - // Check children in reverse order (last added first). - for (int childIndex = children.Length - 1; childIndex >= 0; childIndex--) + var possibleNode = RunOnMainThread(FindComponentAt, new ComponentAtParams(new Point(x, y), filters), invalidate: false); + if (possibleNode is not null) { - var child = children[childIndex]; - if (child.GetComponentAt(x - child.X, y - child.Y, filters) is { } descendant) - { - return descendant; - } + return possibleNode; } if (this is Text) @@ -3251,6 +3581,24 @@ protected virtual void OnChildTouched(Base control) return null; } + private record struct ComponentAtParams(Point Position, NodeFilter Filters); + + private static Base? FindComponentAt(Base @this, ComponentAtParams componentAtParams) + { + var ((x, y), filters) = componentAtParams; + // Check children in reverse order (last added first). + for (int childIndex = @this._children.Count - 1; childIndex >= 0; childIndex--) + { + var child = @this._children[childIndex]; + if (child.GetComponentAt(x - child.X, y - child.Y, filters) is { } descendant) + { + return descendant; + } + } + + return null; + } + public Base? GetComponentAt(Point point, NodeFilter filters = default) => GetComponentAt(point.X, point.Y, filters); /// @@ -3267,37 +3615,57 @@ protected virtual void Layout(Skin.Base skin) } } - protected virtual bool ShouldSkipLayout => IsHidden && !ToolTip.IsActiveTooltip(this); + protected virtual bool ShouldSkipLayout => !(_visible || ToolTip.IsActiveTooltip(this)); - public int NodeCount => 1 + mChildren.Sum(child => child.NodeCount); + public int NodeCount => 1 + _children.Sum(child => child.NodeCount); - protected virtual void Prelayout(Skin.Base skin) + protected virtual void DoPrelayout(Skin.Base skin) { } protected void DoLayoutIfNeeded(Skin.Base skin) { - if (!_needsLayout) + if (!_layoutDirty) { return; } - _needsLayout = false; + _layoutDirty = false; Layout(skin); } private Point _requiredSizeForDockFillNodes; + protected void InvokeThreadQueue() + { + if (_pendingThreadQueues < 1) + { + return; + } + + if (_threadQueue.InvokePending()) + { + UpdatePendingThreadQueues(-1); + } + + var count = _children.Count; + for (var index = 0; index < count; index++) + { + var child = _children[index]; + child.InvokeThreadQueue(); + } + } + /// /// Recursively lays out the control's interior according to alignment, margin, padding, dock etc. /// /// Skin to use. - protected virtual void RecurseLayout(Skin.Base skin) + protected void RecurseLayout(Skin.Base skin) { - if (mSkin != null) + if (_skin != null) { - skin = mSkin; + skin = _skin; } if (ShouldSkipLayout) @@ -3305,10 +3673,14 @@ protected virtual void RecurseLayout(Skin.Base skin) return; } - var children = mChildren.ToArray(); - foreach (var child in children) + foreach (var child in _children) { - child.Prelayout(skin); + child.DoPrelayout(skin); + + _preLayoutActionsParent.IsExecuting = true; + PreLayout.InvokePending(); + _preLayoutActionsParent.IsExecuting = false; + child.BeforeLayout?.Invoke(child, EventArgs.Empty); } @@ -3326,277 +3698,367 @@ protected virtual void RecurseLayout(Skin.Base skin) ProcessAlignments(); } - var remainingBounds = RenderBounds; + if (_dockDirty) + { + _dockDirty = false; - // Adjust bounds for padding - remainingBounds.X += mPadding.Left; - remainingBounds.Width -= mPadding.Left + mPadding.Right; - remainingBounds.Y += mPadding.Top; - remainingBounds.Height -= mPadding.Top + mPadding.Bottom; + var remainingBounds = RenderBounds; - var dockChildSpacing = DockChildSpacing; + // Adjust bounds for padding + remainingBounds.X += _padding.Left; + remainingBounds.Width -= _padding.Left + _padding.Right; + remainingBounds.Y += _padding.Top; + remainingBounds.Height -= _padding.Top + _padding.Bottom; - var directionalDockChildren = - children.Where(child => !child.ShouldSkipLayout && !child.Dock.HasFlag(Pos.Fill)).ToArray(); - var dockFillChildren = - children.Where(child => !child.ShouldSkipLayout && child.Dock.HasFlag(Pos.Fill)).ToArray(); + var dockChildSpacing = DockChildSpacing; - foreach (var child in directionalDockChildren) - { - var childDock = child.Dock; + var directionalDockChildren = + _children.Where(child => !child.ShouldSkipLayout && !child.Dock.HasFlag(Pos.Fill)).ToArray(); + var dockFillChildren = + _children.Where(child => !child.ShouldSkipLayout && child.Dock.HasFlag(Pos.Fill)).ToArray(); - var childMargin = child.Margin; - var childMarginH = childMargin.Left + childMargin.Right; - var childMarginV = childMargin.Top + childMargin.Bottom; + foreach (var child in directionalDockChildren) + { + var childDock = child.Dock; - var childOuterWidth = childMarginH + child.Width; - var childOuterHeight = childMarginV + child.Height; + var childMargin = child.Margin; + var childMarginH = childMargin.Left + childMargin.Right; + var childMarginV = childMargin.Top + childMargin.Bottom; - var availableWidth = remainingBounds.Width - childMarginH; - var availableHeight = remainingBounds.Height - childMarginV; + var childOuterWidth = childMarginH + child.Width; + var childOuterHeight = childMarginV + child.Height; - var childFitsContentWidth = false; - var childFitsContentHeight = false; + var availableWidth = remainingBounds.Width - childMarginH; + var availableHeight = remainingBounds.Height - childMarginV; - if (child is ISmartAutoSizeToContents smartAutoSizeToContents) - { - childFitsContentWidth = smartAutoSizeToContents.AutoSizeToContentWidth; - childFitsContentHeight = smartAutoSizeToContents.AutoSizeToContentHeight; - } else if (child is IAutoSizeToContents { AutoSizeToContents: true }) - { - childFitsContentWidth = true; - childFitsContentHeight = true; - } + var childFitsContentWidth = false; + var childFitsContentHeight = false; - if (childDock.HasFlag(Pos.Left)) - { - var height = childFitsContentHeight - ? child.Height - : availableHeight; + switch (child) + { + case ISmartAutoSizeToContents smartAutoSizeToContents: + childFitsContentWidth = smartAutoSizeToContents.AutoSizeToContentWidth; + childFitsContentHeight = smartAutoSizeToContents.AutoSizeToContentHeight; + break; + case IAutoSizeToContents { AutoSizeToContents: true }: + childFitsContentWidth = true; + childFitsContentHeight = true; + break; + case IFitHeightToContents { FitHeightToContents: true }: + childFitsContentHeight = true; + break; + } - var y = remainingBounds.Y + childMargin.Top; - if (childDock.HasFlag(Pos.CenterV)) + if (childDock.HasFlag(Pos.Left)) { - height = child.Height; - var extraY = Math.Max(0, availableHeight - height) / 2; - if (extraY != 0) + var height = childFitsContentHeight + ? child.Height + : availableHeight; + + var y = remainingBounds.Y + childMargin.Top; + if (childDock.HasFlag(Pos.CenterV)) { - y += extraY; + height = child.Height; + var extraY = Math.Max(0, availableHeight - height) / 2; + if (extraY != 0) + { + y += extraY; + } } - } - else if (childDock.HasFlag(Pos.Bottom)) - { - y = remainingBounds.Bottom - (childMargin.Bottom + child.Height); - } - else if (!childDock.HasFlag(Pos.Top)) - { - var extraY = Math.Max(0, availableHeight - height) / 2; - if (extraY != 0) + else if (childDock.HasFlag(Pos.Bottom)) { - y += extraY; + y = remainingBounds.Bottom - (childMargin.Bottom + child.Height); + } + else if (!childDock.HasFlag(Pos.Top)) + { + var extraY = Math.Max(0, availableHeight - height) / 2; + if (extraY != 0) + { + y += extraY; + } } - } - - child.SetBounds( - remainingBounds.X + childMargin.Left, - y, - child.Width, - height - ); - var boundsDeltaX = childOuterWidth + dockChildSpacing.Left; - remainingBounds.X += boundsDeltaX; - remainingBounds.Width -= boundsDeltaX; - } + child.SetBounds( + remainingBounds.X + childMargin.Left, + y, + child.Width, + height + ); - if (childDock.HasFlag(Pos.Right)) - { - var height = childFitsContentHeight - ? child.Height - : availableHeight; + var boundsDeltaX = childOuterWidth + dockChildSpacing.Left; + remainingBounds.X += boundsDeltaX; + remainingBounds.Width -= boundsDeltaX; + } - var y = remainingBounds.Y + childMargin.Top; - if (childDock.HasFlag(Pos.CenterV)) + if (childDock.HasFlag(Pos.Right)) { - height = child.Height; - var extraY = Math.Max(0, availableHeight - height) / 2; - if (extraY != 0) + var height = childFitsContentHeight + ? child.Height + : availableHeight; + + var y = remainingBounds.Y + childMargin.Top; + if (childDock.HasFlag(Pos.CenterV)) { - y += extraY; + height = child.Height; + var extraY = Math.Max(0, availableHeight - height) / 2; + if (extraY != 0) + { + y += extraY; + } } + else if (childDock.HasFlag(Pos.Bottom)) + { + y = remainingBounds.Bottom - (childMargin.Bottom + child.Height); + } + else if (!childDock.HasFlag(Pos.Top)) + { + var extraY = Math.Max(0, availableHeight - height) / 2; + if (extraY != 0) + { + y += extraY; + } + } + + var offsetFromRight = child.Width + childMargin.Right; + child.SetBounds( + remainingBounds.X + remainingBounds.Width - offsetFromRight, + y, + child.Width, + height + ); + + var boundsDeltaX = childOuterWidth + dockChildSpacing.Right; + remainingBounds.Width -= boundsDeltaX; } - else if (childDock.HasFlag(Pos.Bottom)) + + if (childDock.HasFlag(Pos.Top) && !childDock.HasFlag(Pos.Left) && !childDock.HasFlag(Pos.Right)) { - y = remainingBounds.Bottom - (childMargin.Bottom + child.Height); + var width = childFitsContentWidth + ? child.Width + : availableWidth; + + var x = remainingBounds.Left + childMargin.Left; + + if (childDock.HasFlag(Pos.CenterH)) + { + x = remainingBounds.Left + (remainingBounds.Width - child.OuterWidth) / 2; + width = child.Width; + } + + child.SetBounds( + x, + remainingBounds.Top + childMargin.Top, + width, + child.Height + ); + + var boundsDeltaY = childOuterHeight + dockChildSpacing.Top; + remainingBounds.Y += boundsDeltaY; + remainingBounds.Height -= boundsDeltaY; } - else if (!childDock.HasFlag(Pos.Top)) + + if (childDock.HasFlag(Pos.Bottom) && !childDock.HasFlag(Pos.Left) && !childDock.HasFlag(Pos.Right)) { - var extraY = Math.Max(0, availableHeight - height) / 2; - if (extraY != 0) + var width = childFitsContentWidth + ? child.Width + : availableWidth; + + var offsetFromBottom = child.Height + childMargin.Bottom; + var x = remainingBounds.Left + childMargin.Left; + + if (childDock.HasFlag(Pos.CenterH)) { - y += extraY; + x = remainingBounds.Left + (remainingBounds.Width - child.OuterWidth) / 2; + width = child.Width; } - } - var offsetFromRight = child.Width + childMargin.Right; - child.SetBounds( - remainingBounds.X + remainingBounds.Width - offsetFromRight, - y, - child.Width, - height - ); + child.SetBounds( + x, + remainingBounds.Bottom - offsetFromBottom, + width, + child.Height + ); + + remainingBounds.Height -= childOuterHeight + dockChildSpacing.Bottom; + } - var boundsDeltaX = childOuterWidth + dockChildSpacing.Right; - remainingBounds.Width -= boundsDeltaX; + child.RecurseLayout(skin); } - if (childDock.HasFlag(Pos.Top) && !childDock.HasFlag(Pos.Left) && !childDock.HasFlag(Pos.Right)) - { - var width = childFitsContentWidth - ? child.Width - : availableWidth; + var boundsForFillNodes = remainingBounds; + _innerBounds = remainingBounds; - var x = remainingBounds.Left + childMargin.Left; + Point sizeToFitDockFillNodes = default; - if (childDock.HasFlag(Pos.CenterH)) + var largestDockFillSize = dockFillChildren.Aggregate( + default(Point), + (size, node) => + new Point(Math.Max(size.X, node.Width), Math.Max(size.Y, node.Height)) + ); + + int suggestedWidth, suggestedHeight; + if (dockFillChildren.Length < 2) + { + suggestedWidth = remainingBounds.Width; + suggestedHeight = remainingBounds.Height; + } + else if (largestDockFillSize.Y > largestDockFillSize.X) + { + if (largestDockFillSize.Y > remainingBounds.Height) { - x = remainingBounds.Left + (remainingBounds.Width - child.OuterWidth) / 2; - width = child.Width; + suggestedWidth = Math.Max(largestDockFillSize.X, remainingBounds.Width); + suggestedHeight = remainingBounds.Height / dockFillChildren.Length; } - - child.SetBounds( - x, - remainingBounds.Top + childMargin.Top, - width, - child.Height - ); - - var boundsDeltaY = childOuterHeight + dockChildSpacing.Top; - remainingBounds.Y += boundsDeltaY; - remainingBounds.Height -= boundsDeltaY; + else + { + suggestedWidth = remainingBounds.Width / dockFillChildren.Length; + suggestedHeight = Math.Max(largestDockFillSize.Y, remainingBounds.Height); + } + } + else if (largestDockFillSize.X > remainingBounds.Width) + { + suggestedWidth = remainingBounds.Width / dockFillChildren.Length; + suggestedHeight = Math.Max(largestDockFillSize.Y, remainingBounds.Height); + } + else + { + suggestedWidth = Math.Max(largestDockFillSize.X, remainingBounds.Width); + suggestedHeight = remainingBounds.Height / dockFillChildren.Length; } - if (childDock.HasFlag(Pos.Bottom) && !childDock.HasFlag(Pos.Left) && !childDock.HasFlag(Pos.Right)) + var fillHorizontal = suggestedHeight == remainingBounds.Height; + + // + // Fill uses the left over space, so do that now. + // + foreach (var child in dockFillChildren) { - var width = childFitsContentWidth - ? child.Width - : availableWidth; + var dock = child.Dock; - var offsetFromBottom = child.Height + childMargin.Bottom; - var x = remainingBounds.Left + childMargin.Left; + var childMargin = child.Margin; + var childMarginH = childMargin.Left + childMargin.Right; + var childMarginV = childMargin.Top + childMargin.Bottom; - if (childDock.HasFlag(Pos.CenterH)) - { - x = remainingBounds.Left + (remainingBounds.Width - child.OuterWidth) / 2; - width = child.Width; - } + Point newPosition = new( + remainingBounds.X + childMargin.Left, + remainingBounds.Y + childMargin.Top + ); - child.SetBounds( - x, - remainingBounds.Bottom - offsetFromBottom, - width, - child.Height + Point newSize = new( + suggestedWidth - childMarginH, + suggestedHeight - childMarginV ); - remainingBounds.Height -= childOuterHeight + dockChildSpacing.Bottom; - } + var childMinimumSize = child.MinimumSize; + var neededX = Math.Max(0, childMinimumSize.X - newSize.X); + var neededY = Math.Max(0, childMinimumSize.Y - newSize.Y); - child.RecurseLayout(skin); - } + bool exhaustSize = false; + if (neededX > 0 || neededY > 0) + { + exhaustSize = true; - mInnerBounds = remainingBounds; + if (sizeToFitDockFillNodes == default) + { + sizeToFitDockFillNodes = Size; + } - Point sizeToFitDockFillNodes = default; + sizeToFitDockFillNodes.X += neededX; + sizeToFitDockFillNodes.Y += neededY; + } + else if (remainingBounds.Width < 1 || remainingBounds.Height < 1) + { + if (sizeToFitDockFillNodes == default) + { + sizeToFitDockFillNodes = Size; + } - // - // Fill uses the left over space, so do that now. - // - foreach (var child in dockFillChildren) - { - var dock = child.Dock; + sizeToFitDockFillNodes.X += Math.Max(10, boundsForFillNodes.Width / dockFillChildren.Length); + sizeToFitDockFillNodes.Y += Math.Max(10, boundsForFillNodes.Height / dockFillChildren.Length); + } - var childMargin = child.Margin; - var childMarginH = childMargin.Left + childMargin.Right; - var childMarginV = childMargin.Top + childMargin.Bottom; + newSize.X = Math.Max(childMinimumSize.X, newSize.X); + newSize.Y = Math.Max(childMinimumSize.Y, newSize.Y); - Point newPosition = new( - remainingBounds.X + childMargin.Left, - remainingBounds.Y + childMargin.Top - ); + if (child is IAutoSizeToContents { AutoSizeToContents: true }) + { + if (Pos.Right == (dock & (Pos.Right | Pos.Left))) + { + var offsetFromRight = child.Width + childMargin.Right + dockChildSpacing.Right; + newPosition.X = remainingBounds.Right - offsetFromRight; + } - Point newSize = new( - remainingBounds.Width - childMarginH, - remainingBounds.Height - childMarginV - ); + if (Pos.Bottom == (dock & (Pos.Bottom | Pos.Top))) + { + var offsetFromBottom = child.Height + childMargin.Bottom + dockChildSpacing.Bottom; + newPosition.Y = remainingBounds.Bottom - offsetFromBottom; + } - var childMinimumSize = child.MinimumSize; - var neededX = Math.Max(0, childMinimumSize.X - newSize.X); - var neededY = Math.Max(0, childMinimumSize.Y - newSize.Y); + if (dock.HasFlag(Pos.CenterH)) + { + newPosition.X = remainingBounds.X + (remainingBounds.Width - (childMarginH + child.Width)) / 2; + } - bool exhaustSize = false; - if (neededX > 0 || neededY > 0) - { - exhaustSize = true; - _requiredSizeForDockFillNodes = new Point(Width + neededX, Height + neededY); - } + if (dock.HasFlag(Pos.CenterV)) + { + newPosition.Y = remainingBounds.Y + + (remainingBounds.Height - (childMarginV + child.Height)) / 2; + } - newSize.X = Math.Max(childMinimumSize.X, newSize.X); - newSize.Y = Math.Max(childMinimumSize.Y, newSize.Y); + child.SetPosition(newPosition); - if (child is IAutoSizeToContents { AutoSizeToContents: true }) - { - if (Pos.Right == (dock & (Pos.Right | Pos.Left))) - { - var offsetFromRight = child.Width + childMargin.Right + dockChildSpacing.Right; - newPosition.X = remainingBounds.Right - offsetFromRight; + // TODO: Figure out how to adjust remaining bounds in the autosize case } - - if (Pos.Bottom == (dock & (Pos.Bottom | Pos.Top))) + else { - var offsetFromBottom = child.Height + childMargin.Bottom + dockChildSpacing.Bottom; - newPosition.Y = remainingBounds.Bottom - offsetFromBottom; - } + ApplyDockFill(child, newPosition, newSize); - if (dock.HasFlag(Pos.CenterH)) - { - newPosition.X = remainingBounds.X + (remainingBounds.Width - (childMarginH + child.Width)) / 2; + var childOuterBounds = child.OuterBounds; + if (fillHorizontal) + { + var delta = childOuterBounds.Right - remainingBounds.X; + remainingBounds.X = childOuterBounds.Right; + remainingBounds.Width -= delta; + } + else + { + var delta = childOuterBounds.Bottom - remainingBounds.Y; + remainingBounds.Y = childOuterBounds.Bottom; + remainingBounds.Height -= delta; + } } - if (dock.HasFlag(Pos.CenterV)) + if (exhaustSize) { - newPosition.Y = remainingBounds.Y + (remainingBounds.Height - (childMarginV + child.Height)) / 2; + remainingBounds.X += remainingBounds.Width; + remainingBounds.Width = 0; + remainingBounds.Y += remainingBounds.Height; + remainingBounds.Height = 0; } - child.SetPosition(newPosition); + child.RecurseLayout(skin); } - else + } + else + { + foreach (var child in _children) { - ApplyDockFill(child, newPosition, newSize); - } + if (child.ShouldSkipLayout) + { + continue; + } - if (exhaustSize) - { - remainingBounds.X += remainingBounds.Width; - remainingBounds.Width = 0; - remainingBounds.Y += remainingBounds.Height; - remainingBounds.Height = 0; + child.RecurseLayout(skin); } - - child.RecurseLayout(skin); } - PostLayout(skin); - AfterLayout?.Invoke(this, EventArgs.Empty); - - while (_deferredActions.TryDequeue(out var deferredAction)) - { - deferredAction(); - } + DoPostlayout(skin); - if (sizeToFitDockFillNodes != default) - { + _postLayoutActionsParent.IsExecuting = true; + PostLayout.InvokePending(); + _postLayoutActionsParent.IsExecuting = false; - } + AfterLayout?.Invoke(this, EventArgs.Empty); // ReSharper disable once InvertIf if (_canvas is { } canvas) @@ -3628,12 +4090,12 @@ protected virtual void ApplyDockFill(Base child, Point position, Point size) /// True if the control is out child. public bool IsChild(Base child) { - return mChildren.Contains(child); + return _children.Contains(child); } public Point ToCanvas(int x, int y) { - if (mParent is not { } parent) + if (_host is not { } parent) { return new Point(x, y); } @@ -3648,7 +4110,7 @@ public Point ToCanvas(int x, int y) y += innerPanel.Y; } - return mParent.ToCanvas(x, y); + return _host.ToCanvas(x, y); } /// @@ -3671,7 +4133,7 @@ public Point ToCanvas(int x, int y) public virtual Point ToGlobal(int x, int y) { - if (mParent is not { } parent) + if (_host is not { } parent) { return new Point(x, y); } @@ -3691,7 +4153,7 @@ public virtual Point ToGlobal(int x, int y) public virtual Point ToLocal(int x, int y) { - if (mParent is not {} parent) + if (_host is not {} parent) { return new Point(x, y); } @@ -3715,7 +4177,7 @@ public virtual void CloseMenus() ////debug.print("Base.CloseMenus: {0}", this); // todo: not very efficient with the copying and recursive closing, maybe store currently open menus somewhere (canvas)? - var childrenCopy = mChildren.FindAll(x => true); + var childrenCopy = _children.FindAll(x => true); foreach (var child in childrenCopy) { child.CloseMenus(); @@ -3727,11 +4189,11 @@ public virtual void CloseMenus() /// protected virtual void UpdateRenderBounds() { - mRenderBounds.X = 0; - mRenderBounds.Y = 0; + _renderBounds.X = 0; + _renderBounds.Y = 0; - mRenderBounds.Width = mBounds.Width; - mRenderBounds.Height = mBounds.Height; + _renderBounds.Width = _bounds.Width; + _renderBounds.Height = _bounds.Height; } /// @@ -3739,35 +4201,35 @@ protected virtual void UpdateRenderBounds() /// public virtual void UpdateCursor() { - Platform.Neutral.SetCursor(mCursor); + Platform.Neutral.SetCursor(_cursor); } // giver public virtual Package DragAndDrop_GetPackage(int x, int y) { - return mDragAndDrop_package; + return _dragPayload; } // giver public virtual bool DragAndDrop_Draggable() { - if (mDragAndDrop_package == null) + if (_dragPayload == null) { return false; } - return mDragAndDrop_package.IsDraggable; + return _dragPayload.IsDraggable; } // giver public virtual void DragAndDrop_SetPackage(bool draggable, string name = "", object userData = null) { - if (mDragAndDrop_package == null) + if (_dragPayload == null) { - mDragAndDrop_package = new Package(); - mDragAndDrop_package.IsDraggable = draggable; - mDragAndDrop_package.Name = name; - mDragAndDrop_package.UserData = userData; + _dragPayload = new Package(); + _dragPayload.IsDraggable = draggable; + _dragPayload.Name = name; + _dragPayload.UserData = userData; } } @@ -3818,35 +4280,50 @@ public virtual bool DragAndDrop_CanAcceptPackage(Package p) return false; } + public record struct SizeToChildrenArgs( + bool X = true, + bool Y = true, + bool Recurse = false, + Point MinimumSize = default + ); + + private static void SizeChildrenToChildren(Base @this, SizeToChildrenArgs args) + { + foreach (var child in @this._children) + { + if (!child._visible) + { + continue; + } + + child.SizeToChildren(args); + } + } + + public bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) => + SizeToChildren(new SizeToChildrenArgs(resizeX, resizeY, recursive)); + /// /// Resizes the control to fit its children. /// - /// Determines whether to change control's width. - /// Determines whether to change control's height. - /// + /// /// True if bounds changed. - public virtual bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) + public virtual bool SizeToChildren(SizeToChildrenArgs args) { - if (!resizeX && !resizeY) + if (args is { X: false, Y: false }) { return false; } - if (recursive) + if (args.Recurse) { - var children = mChildren.ToArray(); - foreach (var child in children) - { - if (child.mHidden) - { - continue; - } - - child.SizeToChildren(resizeX: resizeX, resizeY: resizeY, recursive: recursive); - } + RunOnMainThread(SizeChildrenToChildren, this, args); } var childrenSize = GetChildrenSize(); + childrenSize.X = Math.Max(childrenSize.X, args.MinimumSize.X); + childrenSize.Y = Math.Max(childrenSize.Y, args.MinimumSize.Y); + var padding = Padding; var size = childrenSize; var paddingH = padding.Right + padding.Left; @@ -3865,7 +4342,7 @@ public virtual bool SizeToChildren(bool resizeX = true, bool resizeY = true, boo size.X = Math.Max(Math.Min(size.X, _maximumSize.X < 1 ? size.X : _maximumSize.X), _minimumSize.X); size.Y = Math.Max(Math.Min(size.Y, _maximumSize.Y < 1 ? size.Y : _maximumSize.Y), _minimumSize.Y); - var newSize = new Point(resizeX ? size.X : Width, resizeY ? size.Y : Height); + var newSize = new Point(args.X ? size.X : Width, args.Y ? size.Y : Height); if (Dock.HasFlag(Pos.Fill)) { @@ -3899,15 +4376,16 @@ protected virtual Point ApplyDockFillOnSizeToChildren(Point size, Point internal /// Implement this in derived compound controls to properly return their size. /// /// - public virtual Point GetChildrenSize() + public virtual Point GetChildrenSize() => RunOnMainThread(GetChildrenSize); + + private static Point GetChildrenSize(Base @this) { Point min = new(int.MaxValue, int.MaxValue); Point max = default; - var children = mChildren.ToArray(); - foreach (var child in children) + foreach (var child in @this._children) { - if (!child.IsVisible) + if (!child._visible) { continue; } @@ -3948,15 +4426,15 @@ internal virtual bool HandleAccelerator(string accelerator) { if (InputHandler.KeyboardFocus == this || !AccelOnlyFocus) { - if (mAccelerators.ContainsKey(accelerator)) + if (_accelerators.ContainsKey(accelerator)) { - mAccelerators[accelerator].Invoke(this, EventArgs.Empty); + _accelerators[accelerator].Invoke(this, EventArgs.Empty); return true; } } - return mChildren.Any(child => child.HandleAccelerator(accelerator)); + return _children.Any(child => child.HandleAccelerator(accelerator)); } /// @@ -3972,7 +4450,7 @@ public void AddAccelerator(string? accelerator, GwenEventHandler hand return; } - mAccelerators[accelerator] = handler; + _accelerators[accelerator] = handler; } /// @@ -3981,14 +4459,14 @@ public void AddAccelerator(string? accelerator, GwenEventHandler hand /// Accelerator text. public void AddAccelerator(string accelerator) { - mAccelerators[accelerator] = DefaultAcceleratorHandler; + _accelerators[accelerator] = DefaultAcceleratorHandler; } /// /// Function invoked after layout. /// /// Skin to use. - protected virtual void PostLayout(Skin.Base skin) + protected virtual void DoPostlayout(Skin.Base skin) { } @@ -3997,9 +4475,8 @@ protected virtual void PostLayout(Skin.Base skin) /// public virtual void Redraw() { - UpdateColors(); - mCacheTextureDirty = true; - mParent?.Redraw(); + _cacheTextureDirty = true; + _host?.Redraw(); } /// @@ -4015,7 +4492,7 @@ public virtual void UpdateColors() /// /// Invalidates control's parent. /// - public void InvalidateParent() => mParent?.Invalidate(alsoInvalidateParent: true); + public void InvalidateParent() => _host?.Invalidate(alsoInvalidateParent: true); /// /// Handler for keyboard events. @@ -4535,7 +5012,7 @@ protected bool SetIfChanged(ref string? field, string? value, StringComparison s return true; } - protected bool SetAndDoIfChanged(ref T field, T value, Action action) + protected bool SetAndDoIfChanged(ref TValue field, TValue value, Action action) { ArgumentNullException.ThrowIfNull(action, nameof(action)); @@ -4544,7 +5021,75 @@ protected bool SetAndDoIfChanged(ref T field, T value, Action action) return false; } - action(); + action(this); + return true; + } + + protected static bool SetAndDoIfChanged( + ref TValue field, + TValue value, + Action action, + TNode @this + ) where TNode : Base + { + ArgumentNullException.ThrowIfNull(action, nameof(action)); + + if (!@this.SetIfChanged(ref field, value)) + { + return false; + } + + action(@this); + return true; + } + + protected static bool SetAndDoIfChanged( + ref TValue field, + TValue value, + Action action, + Base? parent, + TNode @this + ) where TNode : Base + { + ArgumentNullException.ThrowIfNull(action, nameof(action)); + + if (!@this.SetIfChanged(ref field, value)) + { + return false; + } + + action(parent, @this); + return true; + } + + protected static bool SetAndDoIfChanged( + ref TValue field, + TValue value, + Action action, + TNode @this + ) where TNode : Base + { + ArgumentNullException.ThrowIfNull(action, nameof(action)); + + if (!@this.SetIfChanged(ref field, value)) + { + return false; + } + + action(@this, value); + return true; + } + + protected bool SetAndDoIfChanged(ref TValue field, TValue value, Action action) + { + ArgumentNullException.ThrowIfNull(action, nameof(action)); + + if (!SetIfChanged(ref field, value)) + { + return false; + } + + action(this, value); return true; } diff --git a/Intersect.Client.Framework/Gwen/Control/Button.cs b/Intersect.Client.Framework/Gwen/Control/Button.cs index ff935664bb..874e22349e 100644 --- a/Intersect.Client.Framework/Gwen/Control/Button.cs +++ b/Intersect.Client.Framework/Gwen/Control/Button.cs @@ -19,7 +19,7 @@ public partial class Button : Label private bool mCenterImage; private readonly Dictionary _stateSoundNames = []; - private readonly Dictionary _stateTextures = []; + private readonly Dictionary _stateTextures = []; private readonly Dictionary _stateTextureNames = []; private bool mToggle; @@ -418,7 +418,7 @@ public void SetStateTexture(ComponentState componentState, string textureName) /// /// /// - public void SetStateTexture(GameTexture? texture, string? name, ComponentState state) + public void SetStateTexture(IGameTexture? texture, string? name, ComponentState state) { if (texture == null && !string.IsNullOrWhiteSpace(name)) { @@ -444,7 +444,7 @@ public void SetStateTexture(GameTexture? texture, string? name, ComponentState s } } - public GameTexture? GetStateTexture(ComponentState state) => _stateTextures.GetValueOrDefault(state); + public IGameTexture? GetStateTexture(ComponentState state) => _stateTextures.GetValueOrDefault(state); public string? GetStateTextureName(ComponentState state) => _stateTextureNames.GetValueOrDefault(state); diff --git a/Intersect.Client.Framework/Gwen/Control/Canvas.cs b/Intersect.Client.Framework/Gwen/Control/Canvas.cs index d1597c705d..4993211c88 100644 --- a/Intersect.Client.Framework/Gwen/Control/Canvas.cs +++ b/Intersect.Client.Framework/Gwen/Control/Canvas.cs @@ -97,10 +97,10 @@ public bool NeedsRedraw set => mNeedsRedraw = value; } - public override void Dispose() + protected override void Dispose(bool disposing) { ProcessDelayedDeletes(); - base.Dispose(); + base.Dispose(disposing); } /// @@ -132,8 +132,6 @@ public void RenderCanvas(TimeSpan elapsed, TimeSpan total) render.Begin(); - RecurseLayout(Skin); - render.ClipRegion = Bounds; //render.RenderOffset = new Point(X,Y); @@ -223,7 +221,8 @@ private void DoThink() ProcessDelayedDeletes(); - // Check has focus etc.. + InvokeThreadQueue(); + RecurseLayout(Skin); // If we didn't have a next tab, cycle to the start. @@ -410,7 +409,7 @@ public bool Input_Character(char chr) return false; } - if (!InputHandler.KeyboardFocus.IsVisible) + if (!InputHandler.KeyboardFocus.IsVisibleInTree) { return false; } diff --git a/Intersect.Client.Framework/Gwen/Control/Checkbox.cs b/Intersect.Client.Framework/Gwen/Control/Checkbox.cs index 65d27221eb..1c51598755 100644 --- a/Intersect.Client.Framework/Gwen/Control/Checkbox.cs +++ b/Intersect.Client.Framework/Gwen/Control/Checkbox.cs @@ -1,6 +1,8 @@ using Intersect.Client.Framework.File_Management; using Intersect.Client.Framework.Graphics; +using Intersect.Client.Framework.Gwen.Control.EventArguments; using Intersect.Client.Framework.Input; +using Intersect.Framework.Eventing; using Newtonsoft.Json.Linq; namespace Intersect.Client.Framework.Gwen.Control; @@ -9,7 +11,7 @@ namespace Intersect.Client.Framework.Gwen.Control; /// /// CheckBox control. /// -public partial class Checkbox : Button +public partial class Checkbox : Button, ICheckbox { public enum ControlState @@ -27,22 +29,22 @@ public enum ControlState private bool mChecked; - private GameTexture mCheckedDisabledImage; + private IGameTexture mCheckedDisabledImage; private string mCheckedDisabledImageFilename; - private GameTexture mCheckedNormalImage; + private IGameTexture mCheckedNormalImage; private string mCheckedNormalImageFilename; //Sound Effects private string mCheckSound; - private GameTexture mDisabledImage; + private IGameTexture mDisabledImage; private string mDisabledImageFilename; - private GameTexture mNormalImage; + private IGameTexture mNormalImage; private string mNormalImageFilename; @@ -75,7 +77,7 @@ public bool IsChecked } mChecked = value; - OnCheckChanged(); + OnCheckChanged(value); } } @@ -172,42 +174,39 @@ public override void Toggle() /// /// Invoked when the checkbox has been checked. /// - public event GwenEventHandler Checked; + public event EventHandler? Checked; /// /// Invoked when the checkbox has been unchecked. /// - public event GwenEventHandler UnChecked; + public event EventHandler? Unchecked; /// /// Invoked when the checkbox state has been changed. /// - public event GwenEventHandler CheckChanged; + public event EventHandler>? CheckChanged; /// /// Handler for CheckChanged event. /// - protected virtual void OnCheckChanged() + protected virtual void OnCheckChanged(bool isChecked) { - if (IsChecked) + if (isChecked) { - if (Checked != null) - { - Checked.Invoke(this, EventArgs.Empty); - } + Checked?.Invoke(this, EventArgs.Empty); } else { - if (UnChecked != null) - { - UnChecked.Invoke(this, EventArgs.Empty); - } + Unchecked?.Invoke(this, EventArgs.Empty); } - if (CheckChanged != null) - { - CheckChanged.Invoke(this, EventArgs.Empty); - } + CheckChanged?.Invoke( + this, + new ValueChangedEventArgs + { + Value = isChecked, OldValue = !isChecked + } + ); } /// @@ -242,7 +241,7 @@ protected override void OnMouseClicked(MouseButton mouseButton, Point mousePosit base.OnMouseClicked(mouseButton, mousePosition, userAction); } - public void SetImage(GameTexture texture, string fileName, ControlState state) + public void SetImage(IGameTexture texture, string fileName, ControlState state) { switch (state) { @@ -271,7 +270,7 @@ public void SetImage(GameTexture texture, string fileName, ControlState state) } } - public GameTexture GetImage(ControlState state) + public IGameTexture GetImage(ControlState state) { switch (state) { diff --git a/Intersect.Client.Framework/Gwen/Control/CollapsibleCategory.cs b/Intersect.Client.Framework/Gwen/Control/CollapsibleCategory.cs index e4f2fc99f3..13f1265f5f 100644 --- a/Intersect.Client.Framework/Gwen/Control/CollapsibleCategory.cs +++ b/Intersect.Client.Framework/Gwen/Control/CollapsibleCategory.cs @@ -172,7 +172,7 @@ public void UnselectAll() /// Function invoked after layout. /// /// Skin to use. - protected override void PostLayout(Skin.Base skin) + protected override void DoPostlayout(Skin.Base skin) { if (IsCollapsed) { diff --git a/Intersect.Client.Framework/Gwen/Control/ColorLerpBox.cs b/Intersect.Client.Framework/Gwen/Control/ColorLerpBox.cs index 285483a416..5049a2c407 100644 --- a/Intersect.Client.Framework/Gwen/Control/ColorLerpBox.cs +++ b/Intersect.Client.Framework/Gwen/Control/ColorLerpBox.cs @@ -41,14 +41,6 @@ public ColorLerpBox(Base parent) : base(parent) /// public event GwenEventHandler ColorChanged; - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public override void Dispose() - { - base.Dispose(); - } - /// /// Linear color interpolation. /// diff --git a/Intersect.Client.Framework/Gwen/Control/ColorSlider.cs b/Intersect.Client.Framework/Gwen/Control/ColorSlider.cs index d7a4499c42..b3c37c6a00 100644 --- a/Intersect.Client.Framework/Gwen/Control/ColorSlider.cs +++ b/Intersect.Client.Framework/Gwen/Control/ColorSlider.cs @@ -16,7 +16,7 @@ public partial class ColorSlider : Base private int mSelectedDist; - private GameTexture mTexture; + private IGameTexture mTexture; /// /// Initializes a new instance of the class. @@ -43,14 +43,6 @@ public Color SelectedColor /// public event GwenEventHandler ColorChanged; - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public override void Dispose() - { - base.Dispose(); - } - /// /// Renders the control using specified skin. /// diff --git a/Intersect.Client.Framework/Gwen/Control/ComboBox.cs b/Intersect.Client.Framework/Gwen/Control/ComboBox.cs index e9e6cc82e7..d5b6c8909d 100644 --- a/Intersect.Client.Framework/Gwen/Control/ComboBox.cs +++ b/Intersect.Client.Framework/Gwen/Control/ComboBox.cs @@ -67,7 +67,7 @@ public ComboBox(Base parent, string? name = default) : base(parent, name) /// /// Indicates whether the combo menu is open. /// - public bool IsOpen => _menu.IsVisible; + public bool IsOpen => _menu.IsVisibleInTree; /// /// Selected item. @@ -119,17 +119,17 @@ public MenuItem? SelectedItem public bool OpenMenuAbove { get => _positionMenuAbove; - set => SetAndDoIfChanged(ref _positionMenuAbove, value, UpdatePositionIfOpen); + set => SetAndDoIfChanged(ref _positionMenuAbove, value, UpdatePositionIfOpen, Parent, this); } - private void UpdatePositionIfOpen() + private static void UpdatePositionIfOpen(Base? parent, ComboBox @this) { - if (!IsOpen) + if (!@this.IsOpen) { return; } - Open(); + Open(@this, parent); } public override JObject? GetJson(bool isRoot = false, bool onlySerializeIfNotEmpty = false) @@ -384,27 +384,41 @@ protected override void OnKeyboardFocus() /// /// Opens the combo. /// - public virtual void Open() + public void Open() => Open(this, Parent); + + private static void Open(ComboBox @this, Base? parent) { - if (IsDisabledByTree) + if (parent == null) + { + Open(@this); + } + else + { + parent.RunOnMainThread(Open, @this); + } + } + + private static void Open(ComboBox @this) + { + if (@this.IsDisabledByTree) { return; } - _menu.Parent = Canvas; - _menu.IsHidden = false; - _menu.BringToFront(); + @this._menu.Parent = @this.Canvas; + @this._menu.IsHidden = false; + @this._menu.BringToFront(); - var menuPadding = _menu.Padding; + var menuPadding = @this._menu.Padding; var menuPaddingH = menuPadding.Left + menuPadding.Right; var menuPaddingV = menuPadding.Top + menuPadding.Bottom; - var menuMargin = _menu.Margin; + var menuMargin = @this._menu.Margin; var menuMarginV = menuMargin.Top + menuMargin.Bottom; - var width = Width; + var width = @this.Width; var totalChildHeight = 0; - var menuItems = _menu.Children.OfType().ToArray(); + var menuItems = @this._menu.Children.OfType().ToArray(); foreach (var menuItem in menuItems) { menuItem.SizeToContents(); @@ -413,13 +427,13 @@ public virtual void Open() // width = Math.Max(width, menuItem.OuterWidth + menuPaddingH); } - var offset = ToCanvas(default); - _menu.MaximumSize = _menu.MaximumSize with { X = width }; + var offset = @this.ToCanvas(default); + @this._menu.MaximumSize = @this._menu.MaximumSize with { X = width }; - var canvasBounds = Canvas?.Bounds ?? new Rectangle(0, 0, int.MaxValue, int.MinValue); + var canvasBounds = @this.Canvas?.Bounds ?? new Rectangle(0, 0, int.MaxValue, int.MinValue); var expectedMenuHeight = totalChildHeight + menuPaddingV; - var maximumSize = _menu.MaximumSize; + var maximumSize = @this._menu.MaximumSize; if (maximumSize.Y > 0) { expectedMenuHeight = Math.Min(expectedMenuHeight, maximumSize.Y); @@ -431,7 +445,7 @@ public virtual void Open() var positionAbove = canvasBounds.Bottom < newBounds.Bottom; if (!positionAbove) { - positionAbove = _positionMenuAbove && canvasBounds.Top + newBounds.Height < newBounds.Top; + positionAbove = @this._positionMenuAbove && canvasBounds.Top + newBounds.Height < newBounds.Top; } if (positionAbove) @@ -440,16 +454,18 @@ public virtual void Open() } else { - newBounds.Y += Height; + newBounds.Y += @this.Height; } - _menu.RestrictToParent = false; - _menu.SetBounds(newBounds); - _menu.RestrictToParent = true; + @this._menu.RestrictToParent = false; + @this._menu.SetBounds(newBounds); + @this._menu.RestrictToParent = true; - base.PlaySound(mOpenMenuSound); + @this.BasePlaySound(@this.mOpenMenuSound); } + private bool BasePlaySound(string? sound) => base.PlaySound(sound); + /// /// Closes the combo. /// @@ -474,7 +490,7 @@ protected override bool OnKeyDown(bool down) return true; } - var it = _menu.Children.FindIndex(x => x == mSelectedItem); + var it = _menu.IndexOf(x => x == mSelectedItem); if (it + 1 >= _menu.Children.Count) { return true; @@ -500,7 +516,7 @@ protected override bool OnKeyUp(bool down) return true; } - var it = _menu.Children.FindLastIndex(x => x == mSelectedItem); + var it = _menu.LastIndexOf(x => x == mSelectedItem); if (it - 1 < 0) { return true; diff --git a/Intersect.Client.Framework/Gwen/Control/ContextMenu.cs b/Intersect.Client.Framework/Gwen/Control/ContextMenu.cs new file mode 100644 index 0000000000..7a04e6188e --- /dev/null +++ b/Intersect.Client.Framework/Gwen/Control/ContextMenu.cs @@ -0,0 +1,23 @@ +namespace Intersect.Client.Framework.Gwen.Control; + +public class ContextMenu : Menu +{ + public ContextMenu(Base parent, string? name = default) : base(parent, name) + { + + } + + protected override void OnPositioningBeforeOpen() + { + base.OnPositioningBeforeOpen(); + + SizeToChildren(recursive: true); + } + + protected override void OnOpen() + { + base.OnOpen(); + + PostLayout.Enqueue(contextMenu => contextMenu.SizeToChildren(recursive: true), this); + } +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/DockBase.cs b/Intersect.Client.Framework/Gwen/Control/DockBase.cs index 225111ae5a..bd68797d7b 100644 --- a/Intersect.Client.Framework/Gwen/Control/DockBase.cs +++ b/Intersect.Client.Framework/Gwen/Control/DockBase.cs @@ -491,13 +491,13 @@ public override void DragAndDrop_Hover(Package p, int x, int y) if ((dir == Pos.Top || dir == Pos.Bottom) && !mDropFar) { - if (mLeft != null && mLeft.IsVisible) + if (mLeft != null && mLeft.IsVisibleInTree) { mHoverRect.X += mLeft.Width; mHoverRect.Width -= mLeft.Width; } - if (mRight != null && mRight.IsVisible) + if (mRight != null && mRight.IsVisibleInTree) { mHoverRect.Width -= mRight.Width; } @@ -505,13 +505,13 @@ public override void DragAndDrop_Hover(Package p, int x, int y) if ((dir == Pos.Left || dir == Pos.Right) && !mDropFar) { - if (mTop != null && mTop.IsVisible) + if (mTop != null && mTop.IsVisibleInTree) { mHoverRect.Y += mTop.Height; mHoverRect.Height -= mTop.Height; } - if (mBottom != null && mBottom.IsVisible) + if (mBottom != null && mBottom.IsVisibleInTree) { mHoverRect.Height -= mBottom.Height; } diff --git a/Intersect.Client.Framework/Gwen/Control/EventArguments/VisibilityChangedEventArgs.cs b/Intersect.Client.Framework/Gwen/Control/EventArguments/VisibilityChangedEventArgs.cs index 2bfdad7fde..608677cca5 100644 --- a/Intersect.Client.Framework/Gwen/Control/EventArguments/VisibilityChangedEventArgs.cs +++ b/Intersect.Client.Framework/Gwen/Control/EventArguments/VisibilityChangedEventArgs.cs @@ -1,6 +1,13 @@ namespace Intersect.Client.Framework.Gwen.Control.EventArguments; -public class VisibilityChangedEventArgs : EventArgs +public class VisibilityChangedEventArgs(bool isVisible) : EventArgs { - public bool IsVisible { get; init; } + public VisibilityChangedEventArgs(bool isVisibleInParent, bool isVisibleInTree) : this(isVisibleInParent) + { + IsVisibleInTree = isVisibleInTree; + } + + public bool IsVisible { get; init; } = isVisible; + + public bool IsVisibleInTree { get; init; } } \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/ICheckbox.cs b/Intersect.Client.Framework/Gwen/Control/ICheckbox.cs new file mode 100644 index 0000000000..6aecd1e483 --- /dev/null +++ b/Intersect.Client.Framework/Gwen/Control/ICheckbox.cs @@ -0,0 +1,24 @@ +using Intersect.Client.Framework.Gwen.Control.EventArguments; +using Intersect.Framework.Eventing; + +namespace Intersect.Client.Framework.Gwen.Control; + +public interface ICheckbox +{ + /// + /// Invoked when the checkbox has been checked. + /// + event EventHandler? Checked; + + /// + /// Invoked when the checkbox has been unchecked. + /// + event EventHandler? Unchecked; + + /// + /// Invoked when the checkbox state has been changed. + /// + event EventHandler>? CheckChanged; + + bool IsChecked { get; set; } +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/ImagePanel.cs b/Intersect.Client.Framework/Gwen/Control/ImagePanel.cs index 1e63f6e35d..0f56ff0adc 100644 --- a/Intersect.Client.Framework/Gwen/Control/ImagePanel.cs +++ b/Intersect.Client.Framework/Gwen/Control/ImagePanel.cs @@ -25,7 +25,7 @@ public partial class ImagePanel : Base protected string mRightMouseClickSound; - private GameTexture? _texture { get; set; } + private IGameTexture? _texture { get; set; } private string? _textureName; private Rectangle _textureSourceBounds; private float _textureAspectRatio; @@ -70,7 +70,7 @@ public Margin? TextureNinePatchMargin /// /// Assign Existing Texture /// - public GameTexture? Texture + public IGameTexture? Texture { get => _texture; set @@ -166,12 +166,9 @@ protected override void OnPositionChanged(Point oldPosition, Point newPosition) base.OnPositionChanged(oldPosition, newPosition); } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public override void Dispose() + protected override void Dispose(bool disposing) { - base.Dispose(); + base.Dispose(disposing); } public override JObject? GetJson(bool isRoot = false, bool onlySerializeIfNotEmpty = false) @@ -312,9 +309,9 @@ protected override void Layout(Skin.Base skin) EnsureTextureLoaded(); } - protected override void Prelayout(Skin.Base skin) + protected override void DoPrelayout(Skin.Base skin) { - base.Prelayout(skin); + base.DoPrelayout(skin); EnsureTextureLoaded(); } @@ -403,11 +400,6 @@ protected override void Render(Skin.Base skin) /// public virtual void SizeToContents() => SizeToChildren(); - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) - { - return base.SizeToChildren(resizeX, resizeY, recursive); - } - public override bool SetBounds(int x, int y, int width, int height) { var updatedX = x; diff --git a/Intersect.Client.Framework/Gwen/Control/Label.cs b/Intersect.Client.Framework/Gwen/Control/Label.cs index d0f1002ad6..ebf22ac475 100644 --- a/Intersect.Client.Framework/Gwen/Control/Label.cs +++ b/Intersect.Client.Framework/Gwen/Control/Label.cs @@ -27,7 +27,7 @@ public partial class Label : Base, ILabel private string? mBackgroundTemplateFilename; - private GameTexture? mBackgroundTemplateTex; + private IGameTexture? mBackgroundTemplateTex; protected Color? mClickedTextColor; @@ -171,6 +171,19 @@ private static Range GetRangeForFormatArgument(string? format, int argumentIndex private static bool IsArgument(string format, Range range, int argumentIndex) => int.TryParse(format[range], out var index) && index == argumentIndex; + public bool IsEmpty + { + get + { + if (Children.Count > 1) + { + return false; + } + + return Children.FirstOrDefault() is null or Text { IsVisibleInParent: false }; + } + } + public WrappingBehavior WrappingBehavior { get => _wrappingBehavior; @@ -186,7 +199,7 @@ public WrappingBehavior WrappingBehavior } } - public GameTexture? ToolTipBackground + public IGameTexture? ToolTipBackground { get => _tooltipBackground; set @@ -324,7 +337,7 @@ public Color? TextColorOverride private string? _textOverride; private WrappingBehavior _wrappingBehavior; - private GameTexture? _tooltipBackground; + private IGameTexture? _tooltipBackground; /// /// Text override - used to display different string. @@ -479,7 +492,7 @@ public override void LoadJson(JToken obj, bool isRoot = default) } } - public GameTexture GetTemplate() + public IGameTexture GetTemplate() { return mBackgroundTemplateTex; } @@ -502,7 +515,7 @@ public string? BackgroundTemplateName } } - public void SetBackgroundTemplate(GameTexture texture, string fileName) + public void SetBackgroundTemplate(IGameTexture texture, string fileName) { if (texture == null && !string.IsNullOrWhiteSpace(fileName)) { @@ -796,11 +809,25 @@ public virtual Point MeasureShrinkToContents() var contentSize = GetContentSize(); + var minimumSize = MinimumSize; + var newWidth = contentSize.X + contentPadding.Left + contentPadding.Right; - newWidth = Math.Max(newWidth, MinimumSize.X); + newWidth = Math.Max(newWidth, minimumSize.X); var newHeight = contentSize.Y + contentPadding.Top + contentPadding.Bottom; - newHeight = Math.Max(newHeight, MinimumSize.Y); + newHeight = Math.Max(newHeight, minimumSize.Y); + + var maximumSize = MaximumSize; + + if (maximumSize.X > 0) + { + newWidth = Math.Min(maximumSize.X, newWidth); + } + + if (maximumSize.Y > 0) + { + newHeight = Math.Min(maximumSize.Y, newHeight); + } return new Point(newWidth, newHeight); } diff --git a/Intersect.Client.Framework/Gwen/Control/LabeledCheckBox.cs b/Intersect.Client.Framework/Gwen/Control/LabeledCheckBox.cs index 6dd59d1f97..1e2b3a5da2 100644 --- a/Intersect.Client.Framework/Gwen/Control/LabeledCheckBox.cs +++ b/Intersect.Client.Framework/Gwen/Control/LabeledCheckBox.cs @@ -1,14 +1,14 @@ using Intersect.Client.Framework.Graphics; using Intersect.Client.Framework.Gwen.Control.EventArguments; +using Intersect.Framework.Eventing; using Newtonsoft.Json.Linq; namespace Intersect.Client.Framework.Gwen.Control; - /// /// CheckBox with label. /// -public partial class LabeledCheckBox : Base, IAutoSizeToContents, ITextContainer +public partial class LabeledCheckBox : Base, IAutoSizeToContents, ICheckbox, ITextContainer { private readonly Checkbox _checkbox; @@ -135,22 +135,22 @@ public string? Text /// /// Invoked when the control has been checked. /// - public event GwenEventHandler? Checked; + public event EventHandler? Checked; /// /// Invoked when the control has been unchecked. /// - public event GwenEventHandler? Unchecked; + public event EventHandler? Unchecked; /// /// Invoked when the control's check has been changed. /// - public event GwenEventHandler? CheckChanged; + public event EventHandler>? CheckChanged; /// /// Handler for CheckChanged event. /// - protected virtual void OnCheckChanged(Base control, EventArgs args) + protected virtual void OnCheckChanged(ICheckbox sender, ValueChangedEventArgs args) { if (_checkbox.IsChecked) { @@ -161,7 +161,7 @@ protected virtual void OnCheckChanged(Base control, EventArgs args) Unchecked?.Invoke(this, EventArgs.Empty); } - CheckChanged?.Invoke(this, EventArgs.Empty); + CheckChanged?.Invoke(this, args); } public override Point GetChildrenSize() @@ -170,17 +170,12 @@ public override Point GetChildrenSize() return childrenSize; } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) - { - return base.SizeToChildren(resizeX, resizeY, recursive); - } - public void SetCheckSize(int w, int h) { _checkbox.SetSize(w, h); } - public void SetImage(GameTexture texture, string fileName, Checkbox.ControlState state) + public void SetImage(IGameTexture texture, string fileName, Checkbox.ControlState state) { _checkbox.SetImage(texture, fileName, state); } diff --git a/Intersect.Client.Framework/Gwen/Control/LabeledSlider.cs b/Intersect.Client.Framework/Gwen/Control/LabeledSlider.cs index 152b1585f7..595ab5b61f 100644 --- a/Intersect.Client.Framework/Gwen/Control/LabeledSlider.cs +++ b/Intersect.Client.Framework/Gwen/Control/LabeledSlider.cs @@ -99,7 +99,7 @@ public LabeledSlider(Base parent, string? name = default) : base(parent: parent, IsTabable = true; } - public GameTexture? BackgroundImage + public IGameTexture? BackgroundImage { get => _slider.BackgroundImage; set => _slider.BackgroundImage = value; @@ -136,8 +136,8 @@ public Point SliderSize public bool IsValueInputEnabled { - get => _sliderValue.IsVisible; - set => _sliderValue.IsVisible = value; + get => _sliderValue.IsVisibleInTree; + set => _sliderValue.IsVisibleInTree = value; } public string? Label @@ -259,12 +259,12 @@ public Point LabelMinimumSize set => _label.MinimumSize = value; } - public void SetDraggerImage(GameTexture? texture, ComponentState state) + public void SetDraggerImage(IGameTexture? texture, ComponentState state) { _slider.SetDraggerImage(texture, state); } - public GameTexture? GetDraggerImage(ComponentState state) + public IGameTexture? GetDraggerImage(ComponentState state) { return _slider.GetDraggerImage(state); } diff --git a/Intersect.Client.Framework/Gwen/Control/Layout/IFitHeightToContents.cs b/Intersect.Client.Framework/Gwen/Control/Layout/IFitHeightToContents.cs new file mode 100644 index 0000000000..7b4345be3f --- /dev/null +++ b/Intersect.Client.Framework/Gwen/Control/Layout/IFitHeightToContents.cs @@ -0,0 +1,6 @@ +namespace Intersect.Client.Framework.Gwen.Control.Layout; + +public interface IFitHeightToContents +{ + bool FitHeightToContents { get; set; } +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/Layout/Positioner.cs b/Intersect.Client.Framework/Gwen/Control/Layout/Positioner.cs index 7d9d917853..a0bb501a98 100644 --- a/Intersect.Client.Framework/Gwen/Control/Layout/Positioner.cs +++ b/Intersect.Client.Framework/Gwen/Control/Layout/Positioner.cs @@ -31,7 +31,7 @@ public Pos Pos /// Function invoked after layout. /// /// Skin to use. - protected override void PostLayout(Skin.Base skin) + protected override void DoPostlayout(Skin.Base skin) { foreach (var child in Children) // ok? { diff --git a/Intersect.Client.Framework/Gwen/Control/Layout/Table.cs b/Intersect.Client.Framework/Gwen/Control/Layout/Table.cs index 7da87e49fb..ae503ba0a0 100644 --- a/Intersect.Client.Framework/Gwen/Control/Layout/Table.cs +++ b/Intersect.Client.Framework/Gwen/Control/Layout/Table.cs @@ -5,6 +5,7 @@ using Intersect.Client.Framework.Gwen.Control.Data; using Intersect.Configuration; using Intersect.Core; +using Intersect.Framework.Collections; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; @@ -140,13 +141,15 @@ public ITableDataProvider? DataProvider public GameFont? Font { get => _font; - set => SetAndDoIfChanged(ref _font, value, () => + set => SetAndDoIfChanged(ref _font, value, SetFont); + } + + private static void SetFont(Base @this, GameFont? value) + { + foreach (var row in @this.Children.OfType()) { - foreach (var row in Children.OfType()) - { - row.Font = value; - } - }); + row.Font = value; + } } /// @@ -157,25 +160,29 @@ public GameFont? Font public Color? TextColor { get => _textColor; - set => SetAndDoIfChanged(ref _textColor, value, () => + set => SetAndDoIfChanged(ref _textColor, value, SetTextColor); + } + + private static void SetTextColor(Base @this, Color? value) + { + foreach (var colorableText in @this.Children.OfType()) { - foreach (IColorableText colorableText in Children) - { - colorableText.TextColor = value; - } - }); + colorableText.TextColor = value; + } } public Color? TextColorOverride { get => _textColorOverride; - set => SetAndDoIfChanged(ref _textColorOverride, value, () => + set => SetAndDoIfChanged(ref _textColorOverride, value, SetTextColorOverride); + } + + private static void SetTextColorOverride(Base @this, Color? value) + { + foreach (var colorableText in @this.Children.OfType()) { - foreach (IColorableText colorableText in Children) - { - colorableText.TextColorOverride = value; - } - }); + colorableText.TextColorOverride = value; + } } /// @@ -429,6 +436,19 @@ public TableRow AddRow(string text, string? name = null) return row; } + public TableRow InsertRowSorted( + string text, + Func keySelector, + string? name = null, + object? userData = null + ) where TKey : IComparable + { + var row = AddRow(text, name: name); + row.UserData = userData; + ResortChild(row, keySelector); + return row; + } + public TableRow AddRow(string text, int columnCount, string? name = null, int columnIndex = 0) { var row = AddRow(columnCount, name: name); @@ -473,10 +493,7 @@ public void RemoveAll() /// /// Row to search for. /// Row index if found, -1 otherwise. - public int GetRowIndex(TableRow row) - { - return Children.IndexOf(row); - } + public int GetRowIndex(TableRow row) => IndexOf(row); /// /// Lays out the control's interior according to alignment, padding, dock etc. @@ -518,9 +535,9 @@ protected override void Layout(Skin.Base skin) } } - protected override void PostLayout(Skin.Base skin) + protected override void DoPostlayout(Skin.Base skin) { - base.PostLayout(skin); + base.DoPostlayout(skin); } public bool FitRowHeightToContents @@ -580,7 +597,8 @@ protected virtual Point ComputeColumnWidths(TableRow[]? rows = null) .ToArray() ); - var availableWidth = InnerWidth - CellSpacing.X * Math.Max(0, _columnCount - 1); + var cellSpacingX = CellSpacing.X; + var availableWidth = InnerWidth; var requestedWidths = _columnWidths.ToArray(); requestedWidths = measuredContentWidths.Select( @@ -680,6 +698,51 @@ protected virtual Point ComputeColumnWidths(TableRow[]? rows = null) ) .ToArray(); + var computedFlexColumnWidthSum = flexColumnWidths.Sum(); + if (computedFlexColumnWidthSum > availableWidth) + { + var negativePixelsToRedistribute = computedFlexColumnWidthSum - availableWidth; + if (negativePixelsToRedistribute > columnCount) + { + var leftoverPixels = negativePixelsToRedistribute % columnCount; + var pixelsPerColumn = (negativePixelsToRedistribute - leftoverPixels) / columnCount; + flexColumnWidths = flexColumnWidths.Select(fcw => fcw - pixelsPerColumn).ToArray(); + negativePixelsToRedistribute = leftoverPixels; + } + + var distinctColumnWidths = flexColumnWidths.Distinct().ToArray(); + if (distinctColumnWidths.Length == columnCount) + { + for (var index = 0; index < columnCount && negativePixelsToRedistribute > 0; ++index, --negativePixelsToRedistribute) + { + --flexColumnWidths[index]; + } + } + else + { + var distinctColumnWidthsSortedByCountAscending = distinctColumnWidths.ToDictionary( + dw => dw, + dw => flexColumnWidths.Count(fcw => fcw == dw) + ) + .OrderBy(p => p.Value) + .Select(p => p.Key) + .ToArray(); + + foreach (var distinctColumnWidth in distinctColumnWidthsSortedByCountAscending) + { + for (var index = 0; + index < columnCount && negativePixelsToRedistribute > 0; + ++index, --negativePixelsToRedistribute) + { + if (flexColumnWidths[index] == distinctColumnWidth) + { + --flexColumnWidths[index]; + } + } + } + } + } + var actualWidth = 0; var actualHeight = 0; var columnLimit = Math.Min(columnCount, requestedWidths.Length); @@ -692,12 +755,43 @@ protected virtual Point ComputeColumnWidths(TableRow[]? rows = null) foreach (var row in rows) { - var rowWidth = 0; + var rowCellWidths = new int[columnLimit]; for (var columnIndex = 0; columnIndex < columnLimit; ++columnIndex) { var requestedWidth = requestedWidths[columnIndex]; var flexColumnWidth = flexColumnWidths[columnIndex]; var cellWidth = requestedWidth ?? flexColumnWidth; + if (columnIndex > 0) + { + cellWidth -= cellSpacingX; + } + + rowCellWidths[columnIndex] = cellWidth; + } + + var rowPadding = row.Padding; + var rowPaddingH = rowPadding.Left + rowPadding.Right; + if (rowPaddingH > 0) + { + var negativePixelsToRedistribute = rowPaddingH; + if (negativePixelsToRedistribute % columnLimit == 0) + { + var pixelsToRemovePerCell = negativePixelsToRedistribute / columnLimit; + rowCellWidths = rowCellWidths.Select(rcw => rcw - pixelsToRemovePerCell).ToArray(); + } + else + { + for (var columnIndex = 0; columnIndex < columnLimit && negativePixelsToRedistribute > 0; ++columnIndex, --negativePixelsToRedistribute) + { + --rowCellWidths[columnIndex]; + } + } + } + + var rowWidth = 0; + for (var columnIndex = 0; columnIndex < columnLimit; ++columnIndex) + { + var cellWidth = rowCellWidths[columnIndex]; var cell = row.GetColumn(columnIndex); if (cell is not null) { @@ -769,16 +863,14 @@ public override Point GetChildrenSize() return childrenSize; } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) + public override bool SizeToChildren(SizeToChildrenArgs args) { ApplicationContext.CurrentContext.Logger.LogTrace( - "Resizing Table {TableName} to children (X={ResizeX}, Y={ResizeY}, Recursive={Recursive})...", + "Resizing Table {TableName} to children ({Args})...", ParentQualifiedName, - resizeX, - resizeY, - recursive + args ); - return base.SizeToChildren(resizeX, resizeY, recursive); + return base.SizeToChildren(args); } public void Invalidate(bool invalidateChildren, bool invalidateRecursive = true) diff --git a/Intersect.Client.Framework/Gwen/Control/Layout/TableCell.cs b/Intersect.Client.Framework/Gwen/Control/Layout/TableCell.cs index 759bc40ed1..a48436f4ce 100644 --- a/Intersect.Client.Framework/Gwen/Control/Layout/TableCell.cs +++ b/Intersect.Client.Framework/Gwen/Control/Layout/TableCell.cs @@ -6,6 +6,14 @@ public class TableCell : Label { public TableCell(TableRow row, string? name = nameof(TableCell)) : base(parent: row, name: name) { + _textElement.IsVisibleInParent = false; + } + + protected override void OnTextChanged() + { + base.OnTextChanged(); + + _textElement.IsVisibleInParent = !string.IsNullOrEmpty(Text); } protected override void OnDockChanged(Pos oldDock, Pos newDock) @@ -18,11 +26,6 @@ protected override void Layout(Skin.Base skin) base.Layout(skin); } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) - { - return base.SizeToChildren(resizeX, resizeY, recursive); - } - public override bool SizeToContents(out Point contentSize) { return base.SizeToContents(out contentSize); diff --git a/Intersect.Client.Framework/Gwen/Control/Layout/TableRow.cs b/Intersect.Client.Framework/Gwen/Control/Layout/TableRow.cs index c377a50a1d..4e0dee7de6 100644 --- a/Intersect.Client.Framework/Gwen/Control/Layout/TableRow.cs +++ b/Intersect.Client.Framework/Gwen/Control/Layout/TableRow.cs @@ -13,7 +13,7 @@ namespace Intersect.Client.Framework.Gwen.Control.Layout; /// /// Single table row. /// -public partial class TableRow : Base, IColorableText +public partial class TableRow : Base, IColorableText, IFitHeightToContents { private readonly List mDisposalActions = []; private readonly List _columns = []; @@ -164,25 +164,29 @@ public string Text public Color? TextColor { get => _textColor; - set => SetAndDoIfChanged(ref _textColor, value, () => + set => SetAndDoIfChanged(ref _textColor, value, SetTextColor, this); + } + + private static void SetTextColor(TableRow @this, Color? value) + { + foreach (var column in @this._columns) { - foreach (var column in _columns) - { - column.TextColor = _textColor; - } - }); + column.TextColor = value; + } } public Color? TextColorOverride { get => _textColorOverride; - set => SetAndDoIfChanged(ref _textColorOverride, value, () => + set => SetAndDoIfChanged(ref _textColorOverride, value, SetTextColorOverride, this); + } + + private static void SetTextColorOverride(TableRow @this, Color? value) + { + foreach (var column in @this._columns) { - foreach (var column in _columns) - { - column.TextColorOverride = TextColorOverride; - } - }); + column.TextColorOverride = value; + } } public IEnumerable TextColumns @@ -276,27 +280,55 @@ protected override void OnMouseEntered() /// public event GwenEventHandler Selected; - public bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) + public override bool SizeToChildren(SizeToChildrenArgs args) { - var columns = _columns.ToArray(); - foreach (var column in columns) + RunOnMainThread(SizeColumnsToChildren, this, args); + return base.SizeToChildren(args); + } + + private void SizeColumnsToChildren(TableRow @this, SizeToChildrenArgs args) + { + var padding = Padding; + var paddingV = padding.Bottom + padding.Top; + var minimumHeight = Math.Max(0, MinimumSize.Y - paddingV); + foreach (var cell in this._columns) { - column.SizeToChildren(resizeX: resizeX, resizeY: resizeY, recursive: recursive); + var shrunkCellSize = cell.MeasureShrinkToContents(); + minimumHeight = Math.Max(minimumHeight, shrunkCellSize.Y); } - return base.SizeToChildren(resizeX: resizeX, resizeY: resizeY, recursive: recursive); + foreach (var cell in this._columns) + { + if (cell.IsEmpty) + { + continue; + } + + var childArgs = args with { MinimumSize = cell.MinimumSize with { Y = minimumHeight }}; + cell.SizeToChildren(childArgs); + } } protected override void OnSizeChanged(Point oldSize, Point newSize) { base.OnSizeChanged(oldSize, newSize); - ApplicationContext.CurrentContext.Logger.LogTrace( - "Table row {CanonicalName} resized from {OldSize} to {NewSize}", - ParentQualifiedName, - oldSize, - newSize - ); + if (!string.IsNullOrWhiteSpace(Name)) + { + ApplicationContext.CurrentContext.Logger.LogTrace( + "Table row {CanonicalName} resized from {OldSize} to {NewSize}", + ParentQualifiedName, + oldSize, + newSize + ); + + switch (Name) + { + case "SectionGPU": + case "SectionUI": + break; + } + } if (oldSize.X == newSize.X) { @@ -335,11 +367,18 @@ protected override void OnChildSizeChanged(Base child, Point oldChildSize, Point return; } - if (!IsVisible) + if (!IsVisibleInTree) { return; } + switch (childCanonicalName) + { + case "SectionGPU.Column0": + case "SectionUI.Column0": + break; + } + ApplicationContext.CurrentContext.Logger.LogTrace( "Table row child {CanonicalName} resized from {OldChildSize} to {NewChildSize}", childCanonicalName, @@ -523,16 +562,16 @@ void dataChanged(object sender, CellDataChangedEventArgs args) mDisposalActions.Add(() => tableCellDataProvider.DataChanged -= dataChanged); } - public override void Dispose() + protected override void Dispose(bool disposing) { - base.Dispose(); - while (mDisposalActions.Count > 0) { var lastIndex = mDisposalActions.Count - 1; mDisposalActions[lastIndex]?.Invoke(); mDisposalActions.RemoveAt(lastIndex); } + + base.Dispose(disposing); } /// @@ -596,7 +635,7 @@ public void SetCellContents(int columnIndex, Base? control, bool enableMouseInpu var textElement = column.Children.OfType().FirstOrDefault(); if (textElement is not null) { - textElement.IsVisible = control is null; + textElement.IsVisibleInTree = control is null; } var controlsToRemove = column.Children.Where(child => child is not ControlInternal.Text && child != control) @@ -754,4 +793,4 @@ public void SetComputedColumnWidths(int[] computedColumnWidths) } public static new bool IsInstance(Base? component) => component is TableRow; -} +} \ No newline at end of file diff --git a/Intersect.Client.Framework/Gwen/Control/ListBox.cs b/Intersect.Client.Framework/Gwen/Control/ListBox.cs index 70aaf6d120..ef891db1fd 100644 --- a/Intersect.Client.Framework/Gwen/Control/ListBox.cs +++ b/Intersect.Client.Framework/Gwen/Control/ListBox.cs @@ -38,9 +38,8 @@ public partial class ListBox : ScrollControl private bool mSizeToContents; - private Color mTextColor; - - private Color mTextColorOverride; + private Color? _textColor; + private Color? _textColorOverride; /// /// Initializes a new instance of the class. @@ -52,8 +51,8 @@ public ListBox(Base parent, string? name = default) : base(parent: parent, name: Margin = Margin.One; MouseInputEnabled = true; - mTextColor = Color.White; - mTextColorOverride = Color.Transparent; + _textColor = Color.White; + _textColorOverride = Color.Transparent; _table = new Table(this) { @@ -196,28 +195,32 @@ public int ColumnCount /// public event GwenEventHandler RowUnselected; - public Color TextColor + public Color? TextColor + { + get => _textColor; + set => SetAndDoIfChanged(ref _textColor, value, SetTextColor); + } + + private static void SetTextColor(Base @this, Color? value) { - get => mTextColor; - set => SetAndDoIfChanged(ref mTextColor, value, () => + foreach (var colorableText in @this.Children.OfType()) { - foreach (IColorableText colorableText in Children) - { - colorableText.TextColor = value; - } - }); + colorableText.TextColor = value; + } } - public Color TextColorOverride + public Color? TextColorOverride { - get => mTextColorOverride; - set => SetAndDoIfChanged(ref mTextColorOverride, value, () => + get => _textColorOverride; + set => SetAndDoIfChanged(ref _textColorOverride, value, SetTextColorOverride); + } + + private static void SetTextColorOverride(Base @this, Color? value) + { + foreach (var colorableText in @this.Children.OfType()) { - foreach (IColorableText colorableText in Children) - { - colorableText.TextColorOverride = value; - } - }); + colorableText.TextColorOverride = value; + } } public Point CellSpacing diff --git a/Intersect.Client.Framework/Gwen/Control/Menu.cs b/Intersect.Client.Framework/Gwen/Control/Menu.cs index ae39ed1d63..4237cc18ea 100644 --- a/Intersect.Client.Framework/Gwen/Control/Menu.cs +++ b/Intersect.Client.Framework/Gwen/Control/Menu.cs @@ -6,7 +6,6 @@ namespace Intersect.Client.Framework.Gwen.Control; - /// /// Popup menu. /// @@ -15,7 +14,7 @@ public partial class Menu : ScrollControl private string mBackgroundTemplateFilename; - private GameTexture mBackgroundTemplateTex; + private IGameTexture mBackgroundTemplateTex; private bool mDeleteOnClose; @@ -93,25 +92,42 @@ protected override void RenderUnder(Skin.Base skin) /// Opens the menu. /// /// Unused. - public void Open(Pos pos) + public void Open(Pos pos) => RunOnMainThread(Open, this, pos); + + private static void Open(Menu @this, Pos position) { - IsHidden = false; - BringToFront(); + @this.IsVisibleInParent = true; + @this.BringToFront(); + var mouse = Input.InputHandler.MousePosition; - var x = mouse.X; - var y = mouse.Y; - if (x + Width > Canvas.Width) - { - x -= Width; - } + // Subtract a few pixels to it's absolutely clear the mouse is on a menu item + var x = mouse.X - 4; + var y = mouse.Y - 4; + + @this.OnPositioningBeforeOpen(); - if (y + Height > Canvas.Height) + if (@this.Canvas is { } canvas) { - y -= Height; + var canvasSize = canvas.Size; + var size = @this.Size; + x = Math.Min(x, Math.Max(0, canvasSize.X - size.X)); + y = Math.Min(y, Math.Max(0, canvasSize.Y - size.Y)); } - SetPosition(x, y); + @this.SetPosition(x, y); + + @this.OnOpen(); + } + + protected virtual void OnPositioningBeforeOpen() + { + + } + + protected virtual void OnOpen() + { + } /// @@ -164,7 +180,7 @@ public virtual MenuItem AddItem(string text) /// Newly created control. public virtual MenuItem AddItem( string text, - GameTexture? iconTexture, + IGameTexture? iconTexture, string? textureFilename = default, string? accelerator = default, GameFont? font = default @@ -294,24 +310,25 @@ public virtual void AddDivider() divider.Margin = new Margin(IconMarginDisabled ? 0 : 24, 0, 4, 0); } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) + public override bool SizeToChildren(SizeToChildrenArgs args) { - base.SizeToChildren(resizeX: resizeX, resizeY: resizeY, recursive: recursive); - if (resizeX) + var resized = base.SizeToChildren(args); + + if (!args.X) { - var maxWidth = 0; - foreach (var child in Children) + return resized; + } + + var maxWidth = 0; + foreach (var child in Children) + { + if (child.Width > maxWidth) { - if (child.Width > maxWidth) - { - maxWidth = child.Width; - } + maxWidth = child.Width; } - - this.SetSize(maxWidth, Height); } - return true; + return this.SetSize(maxWidth, Height); } public override JObject? GetJson(bool isRoot = false, bool onlySerializeIfNotEmpty = false) @@ -378,12 +395,12 @@ private void UpdateItemStyles() } } - public GameTexture GetTemplate() + public IGameTexture GetTemplate() { return mBackgroundTemplateTex; } - public void SetBackgroundTemplate(GameTexture texture, string fileName) + public void SetBackgroundTemplate(IGameTexture texture, string fileName) { if (texture == null && !string.IsNullOrWhiteSpace(fileName)) { diff --git a/Intersect.Client.Framework/Gwen/Control/Properties.cs b/Intersect.Client.Framework/Gwen/Control/Properties.cs index fafa777dcc..d71807f07f 100644 --- a/Intersect.Client.Framework/Gwen/Control/Properties.cs +++ b/Intersect.Client.Framework/Gwen/Control/Properties.cs @@ -42,7 +42,7 @@ public int SplitWidth /// Function invoked after layout. /// /// Skin to use. - protected override void PostLayout(Skin.Base skin) + protected override void DoPostlayout(Skin.Base skin) { mSplitterBar.Height = 0; diff --git a/Intersect.Client.Framework/Gwen/Control/Property/Check.cs b/Intersect.Client.Framework/Gwen/Control/Property/Check.cs index cb35c4d6ca..212cdefb53 100644 --- a/Intersect.Client.Framework/Gwen/Control/Property/Check.cs +++ b/Intersect.Client.Framework/Gwen/Control/Property/Check.cs @@ -1,4 +1,6 @@ -namespace Intersect.Client.Framework.Gwen.Control.Property; +using Intersect.Client.Framework.Gwen.Control.EventArguments; + +namespace Intersect.Client.Framework.Gwen.Control.Property; /// @@ -17,7 +19,7 @@ public Check(Control.Base parent) : base(parent) { Checkbox = new Checkbox(this); Checkbox.ShouldDrawBackground = false; - Checkbox.CheckChanged += OnValueChanged; + Checkbox.CheckChanged += OnCheckChanged; Checkbox.IsTabable = true; Checkbox.KeyboardInputEnabled = true; Checkbox.SetPosition(2, 1); @@ -25,6 +27,11 @@ public Check(Control.Base parent) : base(parent) Height = 18; } + private void OnCheckChanged(ICheckbox checkbox, ValueChangedEventArgs args) + { + OnValueChanged((checkbox as Control.Base)!, args); + } + /// /// Property value. /// diff --git a/Intersect.Client.Framework/Gwen/Control/RadioButtonGroup.cs b/Intersect.Client.Framework/Gwen/Control/RadioButtonGroup.cs index da579f6aee..2a9ec0d924 100644 --- a/Intersect.Client.Framework/Gwen/Control/RadioButtonGroup.cs +++ b/Intersect.Client.Framework/Gwen/Control/RadioButtonGroup.cs @@ -42,7 +42,7 @@ public RadioButtonGroup(Base parent) : base(parent) /// /// Index of the selected radio button. /// - public int SelectedIndex => Children.IndexOf(mSelected); + public int SelectedIndex => IndexOf(mSelected); /// /// Invoked when the selected option has changed. @@ -70,7 +70,7 @@ public virtual LabeledRadioButton AddOption(string text, string optionName) var lrb = new LabeledRadioButton(this); lrb.Name = optionName; lrb.Text = text; - lrb.RadioButton.Checked += OnRadioClicked; + lrb.RadioButton.Checked += OnCheckChanged; lrb.Dock = Pos.Top; lrb.Margin = new Margin(0, 0, 0, 1); // 1 bottom lrb.KeyboardInputEnabled = false; // todo: true? @@ -81,6 +81,11 @@ public virtual LabeledRadioButton AddOption(string text, string optionName) return lrb; } + private void OnCheckChanged(ICheckbox checkbox, EventArgs args) + { + OnRadioClicked((checkbox as Base)!, args); + } + /// /// Handler for the option change. /// diff --git a/Intersect.Client.Framework/Gwen/Control/RichLabel.cs b/Intersect.Client.Framework/Gwen/Control/RichLabel.cs index b559caf2a7..413dff19c6 100644 --- a/Intersect.Client.Framework/Gwen/Control/RichLabel.cs +++ b/Intersect.Client.Framework/Gwen/Control/RichLabel.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Intersect.Client.Framework.File_Management; using Intersect.Client.Framework.GenericClasses; using Intersect.Client.Framework.Graphics; @@ -253,22 +254,6 @@ public void InvalidateRebuild() Invalidate(); } - /// - /// Resizes the control to fit its children. - /// - /// Determines whether to change control's width. - /// Determines whether to change control's height. - /// - /// - /// True if bounds changed. - /// - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) - { - InvalidateRebuild(); - - return base.SizeToChildren(resizeX: resizeX, resizeY: resizeY, recursive: recursive); - } - public void ForceImmediateRebuild() => Rebuild(); protected override Point ApplyDockFillOnSizeToChildren(Point size, Point internalSize) diff --git a/Intersect.Client.Framework/Gwen/Control/ScrollBar.cs b/Intersect.Client.Framework/Gwen/Control/ScrollBar.cs index c859d911b8..6b02ee5fa6 100644 --- a/Intersect.Client.Framework/Gwen/Control/ScrollBar.cs +++ b/Intersect.Client.Framework/Gwen/Control/ScrollBar.cs @@ -21,7 +21,7 @@ public partial class ScrollBar : Base private string mBackgroundTemplateFilename; - private GameTexture mBackgroundTemplateTex; + private IGameTexture mBackgroundTemplateTex; protected float mContentSize; @@ -172,12 +172,12 @@ public override void LoadJson(JToken obj, bool isRoot = default) } } - public GameTexture GetTemplate() + public IGameTexture GetTemplate() { return mBackgroundTemplateTex; } - public void SetBackgroundTemplate(GameTexture texture, string fileName) + public void SetBackgroundTemplate(IGameTexture texture, string fileName) { if (texture == null && !string.IsNullOrWhiteSpace(fileName)) { @@ -286,12 +286,12 @@ public ScrollBarButton GetScrollBarButton(Pos direction) return null; } - public void SetScrollBarImage(GameTexture texture, string fileName, ComponentState state) + public void SetScrollBarImage(IGameTexture texture, string fileName, ComponentState state) { mBar.SetImage(texture, fileName, state); } - public GameTexture GetScrollBarImage(ComponentState state) + public IGameTexture GetScrollBarImage(ComponentState state) { return mBar.GetImage(state); } diff --git a/Intersect.Client.Framework/Gwen/Control/ScrollControl.cs b/Intersect.Client.Framework/Gwen/Control/ScrollControl.cs index d747f96949..ab856a152f 100644 --- a/Intersect.Client.Framework/Gwen/Control/ScrollControl.cs +++ b/Intersect.Client.Framework/Gwen/Control/ScrollControl.cs @@ -234,7 +234,7 @@ protected override void Layout(Skin.Base skin) /// protected override bool OnMouseWheeled(int delta) { - if (CanScrollV && VerticalScrollBar.IsVisible) + if (CanScrollV && VerticalScrollBar.IsVisibleInTree) { var scrollAmount = VerticalScrollBar.ScrollAmount - VerticalScrollBar.NudgeAmount * (delta / 60.0f); if (VerticalScrollBar.SetScrollAmount(scrollAmount)) @@ -243,7 +243,7 @@ protected override bool OnMouseWheeled(int delta) } } - if (!CanScrollH || !HorizontalScrollBar.IsVisible) + if (!CanScrollH || !HorizontalScrollBar.IsVisibleInTree) { return false; } @@ -260,7 +260,7 @@ protected override bool OnMouseWheeled(int delta) /// protected override bool OnMouseHWheeled(int delta) { - if (!CanScrollH || !HorizontalScrollBar.IsVisible) + if (!CanScrollH || !HorizontalScrollBar.IsVisibleInTree) { return false; } @@ -294,17 +294,17 @@ private static void UpdateScrollbar(ScrollBar scrollBar, bool show, float ratio) if (show) { scrollBar.IsDisabled = ratio > 1; - if (scrollBar.IsVisible) + if (scrollBar.IsVisibleInTree) { return; } - scrollBar.IsVisible = true; + scrollBar.IsVisibleInTree = true; scrollBar.SetScrollAmount(0, forceUpdate: true); } else { - scrollBar.IsVisible = false; + scrollBar.IsVisibleInTree = false; scrollBar.IsDisabled = true; } } diff --git a/Intersect.Client.Framework/Gwen/Control/ScrollPanel.cs b/Intersect.Client.Framework/Gwen/Control/ScrollPanel.cs index 565e120b8b..c8eb5f6c3d 100644 --- a/Intersect.Client.Framework/Gwen/Control/ScrollPanel.cs +++ b/Intersect.Client.Framework/Gwen/Control/ScrollPanel.cs @@ -36,11 +36,6 @@ public override Point GetChildrenSize() return childrenSize; } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) - { - return base.SizeToChildren(resizeX, resizeY, recursive); - } - protected override void OnPositionChanged(Point oldPosition, Point newPosition) { base.OnPositionChanged(oldPosition, newPosition); diff --git a/Intersect.Client.Framework/Gwen/Control/Slider.cs b/Intersect.Client.Framework/Gwen/Control/Slider.cs index 061dc10437..4a6a597180 100644 --- a/Intersect.Client.Framework/Gwen/Control/Slider.cs +++ b/Intersect.Client.Framework/Gwen/Control/Slider.cs @@ -17,7 +17,7 @@ public partial class Slider : Base { private readonly SliderBar _sliderBar; - private GameTexture? _backgroundImage; + private IGameTexture? _backgroundImage; private string? _backgroundImageName; private double _maximumValue; private double _minimumValue; @@ -132,7 +132,7 @@ private double ScaleValueToExternal(double value) return minimumValue + value * (_maximumValue - minimumValue); } - public GameTexture? BackgroundImage + public IGameTexture? BackgroundImage { get => _backgroundImage; set @@ -170,7 +170,7 @@ public Point DraggerSize set => _sliderBar.Size = value; } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) + public override bool SizeToChildren(SizeToChildrenArgs args) { return false; } @@ -480,12 +480,12 @@ protected override void RenderFocus(Skin.Base skin) //skin.DrawKeyboardHighlight(this, RenderBounds, 0); } - public void SetDraggerImage(GameTexture? texture, ComponentState state) + public void SetDraggerImage(IGameTexture? texture, ComponentState state) { _sliderBar.SetImage(texture, texture?.Name, state); } - public GameTexture? GetDraggerImage(ComponentState state) + public IGameTexture? GetDraggerImage(ComponentState state) { return _sliderBar.GetImage(state); } diff --git a/Intersect.Client.Framework/Gwen/Control/TabButton.cs b/Intersect.Client.Framework/Gwen/Control/TabButton.cs index 77bd41add1..36913780ec 100644 --- a/Intersect.Client.Framework/Gwen/Control/TabButton.cs +++ b/Intersect.Client.Framework/Gwen/Control/TabButton.cs @@ -36,7 +36,7 @@ public TabButton(Base parent, string? name = null) : base(parent: parent, name: /// /// Indicates whether the tab is active. /// - public bool IsTabActive => _page is { IsVisible: true }; + public bool IsTabActive => _page is { IsVisibleInTree: true }; // todo: remove public access public TabControl? TabControl @@ -132,7 +132,7 @@ protected override bool OnKeyRight(bool down) if (down) { var count = Parent.Children.Count; - var me = Parent.Children.IndexOf(this); + var me = Parent.IndexOf(this); if (me + 1 < count) { var nextTab = Parent.Children[me + 1]; @@ -156,7 +156,7 @@ protected override bool OnKeyLeft(bool down) if (down) { var count = Parent.Children.Count; - var me = Parent.Children.IndexOf(this); + var me = Parent.IndexOf(this); if (me - 1 >= 0) { var prevTab = Parent.Children[me - 1]; diff --git a/Intersect.Client.Framework/Gwen/Control/TabControl.cs b/Intersect.Client.Framework/Gwen/Control/TabControl.cs index d9f78b1e06..942cdfe242 100644 --- a/Intersect.Client.Framework/Gwen/Control/TabControl.cs +++ b/Intersect.Client.Framework/Gwen/Control/TabControl.cs @@ -21,6 +21,7 @@ public partial class TabControl : Base /// Initializes a new instance of the class. /// /// Parent control. + /// public TabControl(Base parent, string? name = default) : base(parent, name: name) { _scrollbarButtons = new ScrollBarButton[2]; @@ -178,7 +179,7 @@ public TabButton AddPage(TabButton button) if (_activeButton is null) { _activeButton = button; - button.Page.IsVisible = true; + button.Page.IsVisibleInTree = true; } TabAdded?.Invoke(this, EventArgs.Empty); @@ -214,7 +215,7 @@ internal virtual void OnTabPressed(Base control, EventArgs args) { if (_activeButton.Page is {} previousTabPage) { - previousTabPage.IsVisible = false; + previousTabPage.IsVisibleInTree = false; } _activeButton.Redraw(); @@ -225,15 +226,18 @@ internal virtual void OnTabPressed(Base control, EventArgs args) } _activeButton = nextTab; + nextTab.InvalidateDock(); nextTab.Redraw(); - page.IsVisible = true; + page.IsVisibleInTree = true; - TabChanged?.Invoke(control, new TabChangeEventArgs - { - PreviousTab = previousTab, - ActiveTab = nextTab, - }); + TabChanged?.Invoke( + control, + new TabChangeEventArgs + { + PreviousTab = previousTab, ActiveTab = nextTab, + } + ); _tabStrip.Invalidate(); Invalidate(); @@ -243,9 +247,9 @@ internal virtual void OnTabPressed(Base control, EventArgs args) /// Function invoked after layout. /// /// Skin to use. - protected override void PostLayout(Skin.Base skin) + protected override void DoPostlayout(Skin.Base skin) { - base.PostLayout(skin); + base.DoPostlayout(skin); HandleOverflow(); } diff --git a/Intersect.Client.Framework/Gwen/Control/TabStrip.cs b/Intersect.Client.Framework/Gwen/Control/TabStrip.cs index a98898202c..1a2aef989c 100644 --- a/Intersect.Client.Framework/Gwen/Control/TabStrip.cs +++ b/Intersect.Client.Framework/Gwen/Control/TabStrip.cs @@ -88,7 +88,15 @@ public override bool DragAndDrop_HandleDrop(Package p, int x, int y) if (droppedOn != null) { var dropPos = droppedOn.CanvasPosToLocal(new Point(x, y)); - DragAndDrop.SourceControl.BringNextToControl(droppedOn, dropPos.X > droppedOn.Width / 2); + var behind = dropPos.X > droppedOn.Width / 2; + if (behind) + { + DragAndDrop.SourceControl.MoveBefore(droppedOn); + } + else + { + DragAndDrop.SourceControl.MoveAfter(droppedOn); + } } else { diff --git a/Intersect.Client.Framework/Gwen/Control/TextBox.cs b/Intersect.Client.Framework/Gwen/Control/TextBox.cs index 2849ce91b1..f18ea36b39 100644 --- a/Intersect.Client.Framework/Gwen/Control/TextBox.cs +++ b/Intersect.Client.Framework/Gwen/Control/TextBox.cs @@ -83,7 +83,7 @@ public TextBox(Base parent, string? name = default) : base(parent: parent, name: _placeholder = new Text(this) { ColorOverride = new Color(255, 143, 143, 143), - IsVisible = false, + IsVisibleInTree = false, }; } @@ -882,7 +882,7 @@ protected override void Layout(Skin.Base skin) private void UpdatePlaceholder() { - _placeholder.IsVisible = string.IsNullOrEmpty(Text) && !string.IsNullOrWhiteSpace(PlaceholderText); + _placeholder.IsVisibleInTree = string.IsNullOrEmpty(Text) && !string.IsNullOrWhiteSpace(PlaceholderText); AlignTextElement(_placeholder); } diff --git a/Intersect.Client.Framework/Gwen/Control/Titlebar.cs b/Intersect.Client.Framework/Gwen/Control/Titlebar.cs index 9e9a7439c4..5a1c78f0c0 100644 --- a/Intersect.Client.Framework/Gwen/Control/Titlebar.cs +++ b/Intersect.Client.Framework/Gwen/Control/Titlebar.cs @@ -32,7 +32,7 @@ internal Titlebar( _icon = new ImagePanel(this, name: nameof(_icon)) { Dock = Pos.Left, - IsVisible = false, + IsVisibleInTree = false, Margin = new Margin(0, 4, 0, 4), MaximumSize = new Point(24, 24), RestrictToParent = false, @@ -61,7 +61,7 @@ internal Titlebar( private void IconOnTextureLoaded(Base @base, EventArgs eventArgs) { var iconContainerTexture = _icon.Texture; - _icon.IsVisible = iconContainerTexture != null; + _icon.IsVisibleInTree = iconContainerTexture != null; if (iconContainerTexture is null) { return; diff --git a/Intersect.Client.Framework/Gwen/Control/TreeNode.cs b/Intersect.Client.Framework/Gwen/Control/TreeNode.cs index 0b2b5af1ec..08901336e6 100644 --- a/Intersect.Client.Framework/Gwen/Control/TreeNode.cs +++ b/Intersect.Client.Framework/Gwen/Control/TreeNode.cs @@ -51,7 +51,7 @@ public TreeNode(Base parent, string? name = default) : base(parent, name) { Dock = Pos.Top, Height = 100, - IsVisible = false, + IsVisibleInTree = false, Margin = new Margin(TREE_INDENTATION, 1, 0, 0), }; @@ -128,40 +128,18 @@ public bool IsSelected _trigger.ToggleState = value; } - if (SelectionChanged != null) - { - SelectionChanged.Invoke(this, EventArgs.Empty); - } - - // propagate to root parent (tree) - if (_treeControl != null && _treeControl.SelectionChanged != null) - { - _treeControl.SelectionChanged.Invoke(this, EventArgs.Empty); - } + SelectionChanged?.Invoke(this, EventArgs.Empty); + _treeControl.SelectionChanged?.Invoke(this, EventArgs.Empty); if (value) { - if (Selected != null) - { - Selected.Invoke(this, EventArgs.Empty); - } - - if (_treeControl != null && _treeControl.Selected != null) - { - _treeControl.Selected.Invoke(this, EventArgs.Empty); - } + Selected?.Invoke(this, EventArgs.Empty); + _treeControl.Selected?.Invoke(this, EventArgs.Empty); } else { - if (Unselected != null) - { - Unselected.Invoke(this, EventArgs.Empty); - } - - if (_treeControl != null && _treeControl.Unselected != null) - { - _treeControl.Unselected.Invoke(this, EventArgs.Empty); - } + Unselected?.Invoke(this, EventArgs.Empty); + _treeControl.Unselected?.Invoke(this, EventArgs.Empty); } } } @@ -235,32 +213,32 @@ public IEnumerable SelectedChildren /// /// Invoked when the node label has been pressed. /// - public event GwenEventHandler LabelPressed; + public event GwenEventHandler? LabelPressed; /// /// Invoked when the node's selected state has changed. /// - public event GwenEventHandler SelectionChanged; + public event GwenEventHandler? SelectionChanged; /// /// Invoked when the node has been selected. /// - public event GwenEventHandler Selected; + public event GwenEventHandler? Selected; /// /// Invoked when the node has been unselected. /// - public event GwenEventHandler Unselected; + public event GwenEventHandler? Unselected; /// /// Invoked when the node has been expanded. /// - public event GwenEventHandler Expanded; + public event GwenEventHandler? Expanded; /// /// Invoked when the node has been collapsed. /// - public event GwenEventHandler Collapsed; + public event GwenEventHandler? Collapsed; /// /// Renders the control using the specified skin. @@ -269,7 +247,7 @@ public IEnumerable SelectedChildren protected override void Render(Skin.Base skin) { // Calculate the height of the tree node - var isOpen = _innerPanel?.IsVisible ?? false; + var isOpen = _innerPanel?.IsVisibleInTree ?? false; var treeNodeHeight = CalculateTreeNodeHeight(isOpen); // Draw the tree node using the specified skin. @@ -309,7 +287,7 @@ private int CalculateTreeNodeHeight(bool isOpen) // ReSharper disable once InvertIf if (isOpen) { - if (innerPanel.Children.OfType().LastOrDefault(child => child.IsVisible) is { } lastVisibleChild) + if (innerPanel.Children.OfType().LastOrDefault(child => child.IsVisibleInTree) is { } lastVisibleChild) { return height + lastVisibleChild.Y; } @@ -331,16 +309,16 @@ protected override void Layout(Skin.Base skin) _toggleButton.SetPosition(0, (_trigger.Height - _toggleButton.Height) * 0.5f); } - if (_innerPanel.Children.Count == 0) + if (_innerPanel is not { Children.Count: >0 } innerPanel) { _toggleButton.Hide(); _toggleButton.ToggleState = false; - _innerPanel.Hide(); + _innerPanel?.Hide(); } else { _toggleButton.Show(); - _innerPanel.SizeToChildren(false); + innerPanel.SizeToChildren(false); } } @@ -351,11 +329,11 @@ protected override void Layout(Skin.Base skin) /// Function invoked after layout. /// /// Skin to use. - protected override void PostLayout(Skin.Base skin) + protected override void DoPostlayout(Skin.Base skin) { if (SizeToChildren(false)) { - InvalidateParent(); + InvalidateParentDock(); } } @@ -384,21 +362,17 @@ public TreeNode AddNode(string label, object? userData = null) /// public void Open() { - _innerPanel.Show(); + _innerPanel?.Show(); + if (_toggleButton != null) { _toggleButton.ToggleState = true; } - if (Expanded != null) - { - Expanded.Invoke(this, EventArgs.Empty); - } + Expanded?.Invoke(this, EventArgs.Empty); + _treeControl.Expanded?.Invoke(this, EventArgs.Empty); - if (_treeControl != null && _treeControl.Expanded != null) - { - _treeControl.Expanded.Invoke(this, EventArgs.Empty); - } + InvalidateParentDock(); Invalidate(); } @@ -408,21 +382,15 @@ public void Open() /// public void Close() { - _innerPanel.Hide(); + _innerPanel?.Hide(); + if (_toggleButton != null) { _toggleButton.ToggleState = false; } - if (Collapsed != null) - { - Collapsed.Invoke(this, EventArgs.Empty); - } - - if (_treeControl != null && _treeControl.Collapsed != null) - { - _treeControl.Collapsed.Invoke(this, EventArgs.Empty); - } + Collapsed?.Invoke(this, EventArgs.Empty); + _treeControl.Collapsed?.Invoke(this, EventArgs.Empty); Invalidate(); } @@ -433,15 +401,17 @@ public void Close() public void ExpandAll() { Open(); - foreach (var child in Children) + RunOnMainThread(ExpandAllChildren, this); + } + + private static void ExpandAllChildren(TreeNode @this) + { + foreach (var child in @this.Children) { - var node = child as TreeNode; - if (node == null) + if (child is TreeNode treeNode) { - continue; + treeNode.ExpandAll(); } - - node.ExpandAll(); } } @@ -456,15 +426,17 @@ public void UnselectAll() _trigger.ToggleState = false; } - foreach (var child in Children) + RunOnMainThread(UnselectChildren, this); + } + + private static void UnselectChildren(TreeNode @this) + { + foreach (var child in @this.Children) { - var node = child as TreeNode; - if (node == null) + if (child is TreeNode treeNode) { - continue; + treeNode.UnselectAll(); } - - node.UnselectAll(); } } @@ -490,7 +462,7 @@ protected virtual void OnToggleButtonPress(Base control, EventArgs args) /// Event source. protected virtual void OnDoubleClickName(Base control, EventArgs args) { - if (!_toggleButton.IsVisible) + if (!_toggleButton.IsVisibleInTree) { return; } @@ -512,7 +484,7 @@ protected virtual void OnClickName(Base control, EventArgs args) IsSelected = !IsSelected; } - public void SetImage(GameTexture texture, string fileName = "") + public void SetImage(IGameTexture texture, string fileName = "") { _trigger.SetStateTexture(texture, fileName, ComponentState.Normal); } @@ -524,10 +496,7 @@ protected override void OnChildAdded(Base child) { node.TreeControl = _treeControl; - if (_treeControl != null) - { - _treeControl.OnNodeAdded(node); - } + _treeControl?.OnNodeAdded(node); } base.OnChildAdded(child); diff --git a/Intersect.Client.Framework/Gwen/Control/Utility/SearchableTree.cs b/Intersect.Client.Framework/Gwen/Control/Utility/SearchableTree.cs index ddca7788a6..5e95ea0a8c 100644 --- a/Intersect.Client.Framework/Gwen/Control/Utility/SearchableTree.cs +++ b/Intersect.Client.Framework/Gwen/Control/Utility/SearchableTree.cs @@ -167,11 +167,11 @@ private void FetchEntries( { if (node.UserData is not SearchableTreeDataEntry nodeEntry) { - node.IsVisible = false; + node.IsVisibleInTree = false; continue; } - node.IsVisible = visibleEntryIds.Contains(nodeEntry.Id); + node.IsVisibleInTree = visibleEntryIds.Contains(nodeEntry.Id); } foreach (var entry in entries) diff --git a/Intersect.Client.Framework/Gwen/Control/VerticalScrollBar.cs b/Intersect.Client.Framework/Gwen/Control/VerticalScrollBar.cs index a874994dd4..2b85404dc2 100644 --- a/Intersect.Client.Framework/Gwen/Control/VerticalScrollBar.cs +++ b/Intersect.Client.Framework/Gwen/Control/VerticalScrollBar.cs @@ -1,4 +1,5 @@ -using Intersect.Client.Framework.Gwen.Input; +using Intersect.Client.Framework.GenericClasses; +using Intersect.Client.Framework.Gwen.Input; using Intersect.Client.Framework.Input; namespace Intersect.Client.Framework.Gwen.Control; @@ -52,6 +53,11 @@ public override float NudgeAmount set => base.NudgeAmount = value; } + protected override void OnBoundsChanged(Rectangle oldBounds, Rectangle newBounds) + { + base.OnBoundsChanged(oldBounds, newBounds); + } + /// /// Lays out the control's interior according to alignment, padding, dock etc. /// diff --git a/Intersect.Client.Framework/Gwen/Control/WindowControl.cs b/Intersect.Client.Framework/Gwen/Control/WindowControl.cs index 9c1b225dbb..516e39d922 100644 --- a/Intersect.Client.Framework/Gwen/Control/WindowControl.cs +++ b/Intersect.Client.Framework/Gwen/Control/WindowControl.cs @@ -30,9 +30,9 @@ public enum ControlState private Color? mInactiveColor; - private GameTexture? mActiveImage; + private IGameTexture? mActiveImage; - private GameTexture? mInactiveImage; + private IGameTexture? mInactiveImage; private string? mActiveImageFilename; @@ -123,10 +123,10 @@ public string? Title public bool IsClosable { get => !_titlebar.CloseButton.IsHidden; - set => _titlebar.CloseButton.IsVisible = value; + set => _titlebar.CloseButton.IsVisibleInTree = value; } - public GameTexture? Icon + public IGameTexture? Icon { get => _titlebar.Icon.Texture; set => _titlebar.Icon.Texture = value; @@ -157,14 +157,6 @@ protected override void OnVisibilityChanged(object? sender, VisibilityChangedEve } } - /// - /// Indicates whether the control is on top of its parent's children. - /// - public override bool IsOnTop - { - get { return Parent.Children.Where(x => x is WindowControl).Last() == this; } - } - /// /// If the shadow under the window should be drawn. /// @@ -287,10 +279,10 @@ private void Close(Base sender, EventArgs args) IsHidden = true; - if (mModal != null) + if (_modal != null) { - mModal.DelayedDelete(); - mModal = null; + _modal.DelayedDelete(); + _modal = null; } if (mDeleteOnClose) @@ -389,7 +381,7 @@ public void SetTextColor(Color clr, ControlState state) /// Sets the button's image. /// /// Texture name. Null to remove. - public void SetImage(GameTexture texture, string fileName, ControlState state) + public void SetImage(IGameTexture texture, string fileName, ControlState state) { switch (state) { @@ -408,7 +400,7 @@ public void SetImage(GameTexture texture, string fileName, ControlState state) } } - public GameTexture? GetImage(ControlState state) + public IGameTexture? GetImage(ControlState state) { switch (state) { @@ -421,7 +413,7 @@ public void SetImage(GameTexture texture, string fileName, ControlState state) } } - public bool TryGetTexture(ControlState controlState, [NotNullWhen(true)] out GameTexture? texture) + public bool TryGetTexture(ControlState controlState, [NotNullWhen(true)] out IGameTexture? texture) { texture = GetImage(controlState); return texture != default; diff --git a/Intersect.Client.Framework/Gwen/ControlInternal/Dragger.cs b/Intersect.Client.Framework/Gwen/ControlInternal/Dragger.cs index 6859e5db7a..383d18fadd 100644 --- a/Intersect.Client.Framework/Gwen/ControlInternal/Dragger.cs +++ b/Intersect.Client.Framework/Gwen/ControlInternal/Dragger.cs @@ -21,13 +21,13 @@ public partial class Dragger : Base private string? mMouseDownSound; private string? mMouseUpSound; - private GameTexture? mClickedImage; + private IGameTexture? mClickedImage; private string? mClickedImageFilename; - private GameTexture? mDisabledImage; + private IGameTexture? mDisabledImage; private string? mDisabledImageFilename; - private GameTexture? mHoverImage; + private IGameTexture? mHoverImage; private string? mHoverImageFilename; - private GameTexture? mNormalImage; + private IGameTexture? mNormalImage; private string? mNormalImageFilename; protected Base? _target; @@ -240,7 +240,7 @@ public string GetMouseUpSound() /// Sets the button's image. /// /// Texture name. Null to remove. - public virtual void SetImage(GameTexture? texture, string? name, ComponentState state) + public virtual void SetImage(IGameTexture? texture, string? name, ComponentState state) { switch (state) { @@ -269,7 +269,7 @@ public virtual void SetImage(GameTexture? texture, string? name, ComponentState } } - public virtual GameTexture? GetImage(ComponentState state) + public virtual IGameTexture? GetImage(ComponentState state) { switch (state) { diff --git a/Intersect.Client.Framework/Gwen/ControlInternal/Text.cs b/Intersect.Client.Framework/Gwen/ControlInternal/Text.cs index 66317956c4..b3e24836ec 100644 --- a/Intersect.Client.Framework/Gwen/ControlInternal/Text.cs +++ b/Intersect.Client.Framework/Gwen/ControlInternal/Text.cs @@ -33,8 +33,17 @@ public partial class Text : Base /// The name of the element. public Text(Base parent, string? name = default) : base(parent, name) { - _font = Skin.DefaultFont; - Color = Skin.Colors.Label.Normal; + if (SafeSkin is { } skin) + { + InitializeFromSkin(this); + _font = skin.DefaultFont; + Color = skin.Colors.Label.Normal; + } + else + { + PreLayout.Enqueue(InitializeFromSkin, this); + } + MouseInputEnabled = false; ColorOverride = Color.FromArgb(0, 255, 255, 255); // A==0, override disabled @@ -45,6 +54,12 @@ public Text(Base parent, string? name = default) : base(parent, name) parent.SizeChanged += ParentOnSizeChanged; } + private static void InitializeFromSkin(Text @this) + { + @this._font = @this.Skin.DefaultFont; + @this.Color = @this.Skin.Colors.Label.Normal; + } + protected override void OnSizeChanged(Point oldSize, Point newSize) { base.OnSizeChanged(oldSize, newSize); @@ -84,7 +99,7 @@ public GameFont? Font public string? DisplayedText { get => _displayedText; - set => SetAndDoIfChanged(ref _displayedText, value, Invalidate); + set => SetAndDoIfChanged(ref _displayedText, value, Invalidate, this); } public WrappingBehavior WrappingBehavior @@ -247,7 +262,7 @@ protected override void Layout(Skin.Base skin) base.Layout(skin); } - public override bool SizeToChildren(bool resizeX = true, bool resizeY = true, bool recursive = false) => SizeToContents(); + public override bool SizeToChildren(SizeToChildrenArgs args) => SizeToContents(); /// /// Handler invoked when control's scale changes. diff --git a/Intersect.Client.Framework/Gwen/Input/InputHandler.cs b/Intersect.Client.Framework/Gwen/Input/InputHandler.cs index 86b6dd5267..e538cfc396 100644 --- a/Intersect.Client.Framework/Gwen/Input/InputHandler.cs +++ b/Intersect.Client.Framework/Gwen/Input/InputHandler.cs @@ -219,7 +219,7 @@ public static bool DoSpecialKeys(Base canvas, char chr) return false; } - if (!KeyboardFocus.IsVisible) + if (!KeyboardFocus.IsVisibleInTree) { return false; } @@ -328,12 +328,12 @@ public static void OnMouseMoved(Canvas canvas, int x, int y, int dx, int dy) /// Unused. public static void OnCanvasThink(Canvas canvas) { - if (MouseFocus is { IsVisible: false }) + if (MouseFocus is { IsVisibleInTree: false }) { MouseFocus = null; } - if (KeyboardFocus != null && (!KeyboardFocus.IsVisible || !KeyboardFocus.KeyboardInputEnabled)) + if (KeyboardFocus != null && (!KeyboardFocus.IsVisibleInTree || !KeyboardFocus.KeyboardInputEnabled)) { // KeyboardFocus = null; } @@ -404,7 +404,7 @@ public static bool OnMouseButtonStateChanged(Base canvas, MouseButton mouseButto return false; } - if (!hoveredControl.IsVisible) + if (!hoveredControl.IsVisibleInTree) { return false; } @@ -489,7 +489,7 @@ public static bool OnMouseScroll(Base? canvas, int deltaX, int deltaY) if (canvas == null || HoveredControl == null || HoveredControl.Canvas != canvas || - !canvas.IsVisible + !canvas.IsVisibleInTree ) { return false; @@ -527,7 +527,7 @@ public static bool OnKeyEvent(Base canvas, Key key, bool down) return false; } - if (!KeyboardFocus.IsVisible) + if (!KeyboardFocus.IsVisibleInTree) { return false; } diff --git a/Intersect.Client.Framework/Gwen/Renderer/Base.cs b/Intersect.Client.Framework/Gwen/Renderer/Base.cs index ee56a2bf5c..aa44d588d1 100644 --- a/Intersect.Client.Framework/Gwen/Renderer/Base.cs +++ b/Intersect.Client.Framework/Gwen/Renderer/Base.cs @@ -126,7 +126,7 @@ public virtual void End() { } - public virtual GameTexture GetWhiteTexture() + public virtual IGameTexture GetWhiteTexture() { return null; } @@ -168,7 +168,7 @@ public virtual void EndClip() /// Loads the specified texture. /// /// - public virtual void LoadTexture(GameTexture t) + public virtual void LoadTexture(IGameTexture t) { } @@ -176,7 +176,7 @@ public virtual void LoadTexture(GameTexture t) /// Frees the specified texture. /// /// Texture to free. - public virtual void FreeTexture(GameTexture t) + public virtual void FreeTexture(IGameTexture t) { } @@ -190,7 +190,7 @@ public virtual void FreeTexture(GameTexture t) /// Texture coordinate u2. /// Texture coordinate v2. public virtual void DrawTexturedRect( - GameTexture? texture, + IGameTexture? texture, Rectangle targetBounds, Color color, float u1 = 0, @@ -293,7 +293,7 @@ public virtual void DrawPixel(int x, int y) /// X. /// Y. /// Pixel color. - public virtual Color PixelColor(GameTexture texture, uint x, uint y) + public virtual Color PixelColor(IGameTexture texture, uint x, uint y) { return PixelColor(texture, x, y, Color.White); } @@ -306,7 +306,7 @@ public virtual Color PixelColor(GameTexture texture, uint x, uint y) /// Y. /// Color to return on failure. /// Pixel color. - public virtual Color PixelColor(GameTexture texture, uint x, uint y, Color defaultColor) + public virtual Color PixelColor(IGameTexture texture, uint x, uint y, Color defaultColor) { return defaultColor; } diff --git a/Intersect.Client.Framework/Gwen/Renderer/IntersectRenderer.cs b/Intersect.Client.Framework/Gwen/Renderer/IntersectRenderer.cs index 5033447e21..549281aa77 100644 --- a/Intersect.Client.Framework/Gwen/Renderer/IntersectRenderer.cs +++ b/Intersect.Client.Framework/Gwen/Renderer/IntersectRenderer.cs @@ -19,13 +19,13 @@ public partial class IntersectRenderer : Base, ICacheToTexture private GameRenderer mRenderer; - private GameRenderTexture? mRenderTarget; + private IGameRenderTexture? mRenderTarget; /// /// Initializes a new instance of the class. /// /// Intersect render target. - public IntersectRenderer(GameRenderTexture renderTarget, GameRenderer renderer) + public IntersectRenderer(IGameRenderTexture renderTarget, GameRenderer renderer) { mRenderer = renderer; mRenderTarget = renderTarget; @@ -40,7 +40,7 @@ public override Color DrawColor set => mColor = new Color(value.A, value.R, value.G, value.B); } - public override Color PixelColor(GameTexture texture, uint x, uint y, Color defaultColor) + public override Color PixelColor(IGameTexture texture, uint x, uint y, Color defaultColor) { var x1 = (int) x; var y1 = (int) y; @@ -156,7 +156,7 @@ public override void DrawFilledRect(Rectangle targetRect) if (mRenderTarget == null) { mRenderer.DrawTexture( - mRenderer.GetWhiteTexture(), + mRenderer.WhitePixel, 0, 0, 1, @@ -176,7 +176,7 @@ public override void DrawFilledRect(Rectangle targetRect) else { mRenderer.DrawTexture( - mRenderer.GetWhiteTexture(), + mRenderer.WhitePixel, 0, 0, 1, @@ -196,7 +196,7 @@ public override void DrawFilledRect(Rectangle targetRect) } public override void DrawTexturedRect( - GameTexture? tex, + IGameTexture? tex, Rectangle targetRect, Color clr, float u1 = 0, @@ -341,16 +341,16 @@ private string RemoveResourcesSlash(string fileName) /// public override ICacheToTexture Ctt => this; - private Dictionary m_RT; + private Dictionary m_RT; - private Stack m_Stack; + private Stack m_Stack; - private GameRenderTexture m_RealRT; + private IGameRenderTexture m_RealRT; public void Initialize() { - m_RT = new Dictionary(); - m_Stack = new Stack(); + m_RT = new Dictionary(); + m_Stack = new Stack(); } public void ShutDown() diff --git a/Intersect.Client.Framework/Gwen/Skin/Intersect2021.cs b/Intersect.Client.Framework/Gwen/Skin/Intersect2021.cs index c619a5aab9..3294534414 100644 --- a/Intersect.Client.Framework/Gwen/Skin/Intersect2021.cs +++ b/Intersect.Client.Framework/Gwen/Skin/Intersect2021.cs @@ -164,7 +164,7 @@ public override void DrawWindow(Control.Base control, int topHeight, bool inFocu return; } - GameTexture? renderTexture = null; + IGameTexture? renderTexture = null; if (windowControl.TryGetTexture(WindowControl.ControlState.Active, out var activeTexture)) { renderTexture = activeTexture; diff --git a/Intersect.Client.Framework/Gwen/Skin/IntersectSkin.cs b/Intersect.Client.Framework/Gwen/Skin/IntersectSkin.cs index 5b37c19994..052b086d8e 100644 --- a/Intersect.Client.Framework/Gwen/Skin/IntersectSkin.cs +++ b/Intersect.Client.Framework/Gwen/Skin/IntersectSkin.cs @@ -17,7 +17,7 @@ namespace Intersect.Client.Framework.Gwen.Skin; /// public class IntersectSkin : TexturedBase { - private static GameTexture LoadEmbeddedSkinTexture(GameContentManager contentManager) + private static IGameTexture LoadEmbeddedSkinTexture(GameContentManager contentManager) { const string skinTextureName = "skin-intersect.png"; var skinResourceName = $"{typeof(IntersectSkin).Namespace}.{skinTextureName}"; @@ -41,7 +41,7 @@ private static GameTexture LoadEmbeddedSkinTexture(GameContentManager contentMan skinTextureName ); - return contentManager.Load( + return contentManager.Load( ContentType.Interface, skinTextureName, () => typeof(IntersectSkin).Assembly.GetManifestResourceStream(skinResourceName) ?? @@ -295,7 +295,7 @@ public override void DrawWindow(Control.Base control, int topHeight, bool inFocu return; } - GameTexture? renderTexture = null; + IGameTexture? renderTexture = null; if (windowControl.TryGetTexture(WindowControl.ControlState.Active, out var activeTexture)) { renderTexture = activeTexture; diff --git a/Intersect.Client.Framework/Gwen/Skin/TexturedBase.cs b/Intersect.Client.Framework/Gwen/Skin/TexturedBase.cs index 3d708d025b..3a0780d7de 100644 --- a/Intersect.Client.Framework/Gwen/Skin/TexturedBase.cs +++ b/Intersect.Client.Framework/Gwen/Skin/TexturedBase.cs @@ -451,7 +451,7 @@ public static TexturedBase FindSkin(Renderer.Base renderer, GameContentManager c return new TexturedBase(renderer, contentManager, skinName); } - protected readonly GameTexture _texture; + protected readonly IGameTexture _texture; protected SkinTextures mTextures; @@ -462,7 +462,7 @@ public static TexturedBase FindSkin(Renderer.Base renderer, GameContentManager c /// /// Renderer to use. /// - public TexturedBase(Renderer.Base renderer, GameTexture texture) : base(renderer) + public TexturedBase(Renderer.Base renderer, IGameTexture texture) : base(renderer) { _texture = texture ?? throw new ArgumentNullException(nameof(texture)); texture.Loaded += _ => InitializeColors(); @@ -960,7 +960,7 @@ public override void DrawRadioButton(Control.Base control, bool selected, bool d } } - protected bool TryGetOverrideTexture(Checkbox control, bool selected, bool pressed, out GameTexture overrideTexture) + protected bool TryGetOverrideTexture(Checkbox control, bool selected, bool pressed, out IGameTexture overrideTexture) { Checkbox.ControlState controlState = Checkbox.ControlState.Normal; if (selected) @@ -1153,7 +1153,7 @@ public override void DrawTabTitleBar(Control.Base control) public override void DrawWindow(Control.Base control, int topHeight, bool inFocus) { - GameTexture renderImg = null; + IGameTexture renderImg = null; if (((WindowControl) control).GetImage(Control.WindowControl.ControlState.Active) != null) { renderImg = ((WindowControl) control).GetImage(Control.WindowControl.ControlState.Active); @@ -1334,7 +1334,7 @@ public override void DrawScrollBar(Control.Base control, bool horizontal, bool d public override void DrawScrollBarBar(ScrollBarBar scrollBarBar) { - GameTexture? renderImg = scrollBarBar.GetImage(ComponentState.Normal); + IGameTexture? renderImg = scrollBarBar.GetImage(ComponentState.Normal); if (scrollBarBar.IsDisabledByTree) { renderImg = scrollBarBar.GetImage(ComponentState.Disabled); @@ -2109,7 +2109,7 @@ bool disabled i = 3; } - GameTexture renderImg = null; + IGameTexture renderImg = null; if (disabled && button.GetStateTexture(ComponentState.Disabled) != null) { @@ -2329,7 +2329,7 @@ public override void DrawMenuDivider(Control.Base control) public override void DrawWindowCloseButton(CloseButton closeButton, bool depressed, bool hovered, bool disabled) { - GameTexture renderImg = null; + IGameTexture renderImg = null; if (disabled && closeButton.GetStateTexture(ComponentState.Disabled) != null) { renderImg = closeButton.GetStateTexture(ComponentState.Disabled); diff --git a/Intersect.Client.Framework/Gwen/Skin/Texturing/Bordered.cs b/Intersect.Client.Framework/Gwen/Skin/Texturing/Bordered.cs index 86d543708e..d0ef128f95 100644 --- a/Intersect.Client.Framework/Gwen/Skin/Texturing/Bordered.cs +++ b/Intersect.Client.Framework/Gwen/Skin/Texturing/Bordered.cs @@ -32,7 +32,7 @@ public static implicit operator FivePatch(Bordered bordered) ); } - private GameTexture mTexture; + private IGameTexture mTexture; private readonly SubRect[] mRects; @@ -43,7 +43,7 @@ public static implicit operator FivePatch(Bordered bordered) private float mHeight; public Bordered( - GameTexture texture, + IGameTexture texture, float x, float y, float w, @@ -94,7 +94,7 @@ void SetRect(int num, float x, float y, float w, float h) } private void Init( - GameTexture texture, + IGameTexture texture, float x, float y, float w, diff --git a/Intersect.Client.Framework/Gwen/Skin/Texturing/FivePatch.cs b/Intersect.Client.Framework/Gwen/Skin/Texturing/FivePatch.cs index 92dcaa5e82..72a82a4916 100644 --- a/Intersect.Client.Framework/Gwen/Skin/Texturing/FivePatch.cs +++ b/Intersect.Client.Framework/Gwen/Skin/Texturing/FivePatch.cs @@ -6,7 +6,7 @@ namespace Intersect.Client.Framework.Gwen.Skin.Texturing; public readonly record struct FivePatch { - private readonly GameTexture _texture; + private readonly IGameTexture _texture; private readonly float _textureWidth; private readonly float _textureHeight; private readonly UVSquare[] _uvSquares; @@ -21,7 +21,7 @@ public readonly record struct FivePatch private readonly int _interiorHeight; public FivePatch( - GameTexture texture, + IGameTexture texture, int x, int y, int width, diff --git a/Intersect.Client.Framework/Gwen/Skin/Texturing/Single.cs b/Intersect.Client.Framework/Gwen/Skin/Texturing/Single.cs index a2d4114755..ad38a222f3 100644 --- a/Intersect.Client.Framework/Gwen/Skin/Texturing/Single.cs +++ b/Intersect.Client.Framework/Gwen/Skin/Texturing/Single.cs @@ -10,7 +10,7 @@ namespace Intersect.Client.Framework.Gwen.Skin.Texturing; public partial struct Single { - private readonly GameTexture mTexture; + private readonly IGameTexture mTexture; private readonly float[] mUv; @@ -18,7 +18,7 @@ public partial struct Single private readonly int mHeight; - public Single(GameTexture texture, float x, float y, float w, float h) + public Single(IGameTexture texture, float x, float y, float w, float h) { mTexture = texture; float texw = 1; diff --git a/Intersect.Client.Core/Core/IClientContext.cs b/Intersect.Client.Framework/IClientContext.cs similarity index 79% rename from Intersect.Client.Core/Core/IClientContext.cs rename to Intersect.Client.Framework/IClientContext.cs index b7f9bf5f53..6eddefbf70 100644 --- a/Intersect.Client.Core/Core/IClientContext.cs +++ b/Intersect.Client.Framework/IClientContext.cs @@ -5,7 +5,7 @@ namespace Intersect.Client.Core; /// /// Declares the API surface of client contexts. /// -internal interface IClientContext : IApplicationContext +public interface IClientContext : IApplicationContext { /// /// The platform-specific runner that initializes the actual user-visible client. diff --git a/Intersect.Client.Core/Core/IPlatformRunner.cs b/Intersect.Client.Framework/IPlatformRunner.cs similarity index 93% rename from Intersect.Client.Core/Core/IPlatformRunner.cs rename to Intersect.Client.Framework/IPlatformRunner.cs index fe498f2bbe..e390eac372 100644 --- a/Intersect.Client.Core/Core/IPlatformRunner.cs +++ b/Intersect.Client.Framework/IPlatformRunner.cs @@ -3,7 +3,7 @@ /// /// Declares the API surface to launch instances of platform-specific (e.g. MonoGame, Unity) runners. /// -internal interface IPlatformRunner +public interface IPlatformRunner { /// /// Starts the platform-specific runner for the provided context and post-startup action. diff --git a/Intersect.Client.Framework/Input/ControlMapping.cs b/Intersect.Client.Framework/Input/ControlMapping.cs index 655a06048d..5a4d70689f 100644 --- a/Intersect.Client.Framework/Input/ControlMapping.cs +++ b/Intersect.Client.Framework/Input/ControlMapping.cs @@ -71,6 +71,11 @@ public bool IsActive([NotNullWhen(true)] out ControlBinding? activeBinding) return true; } + if (!GameInput.Current.IsMouseInBounds) + { + continue; + } + if (GameInput.Current.MouseHitInterface) { continue; diff --git a/Intersect.Client.Framework/Input/GameInput.cs b/Intersect.Client.Framework/Input/GameInput.cs index e3fab35dce..6789826cc8 100644 --- a/Intersect.Client.Framework/Input/GameInput.cs +++ b/Intersect.Client.Framework/Input/GameInput.cs @@ -44,7 +44,7 @@ private void OnOptionsOnOptionsLoaded(Options options) public abstract IControlSet ControlSet { get; set; } - public IReadOnlySet ControlsProviders => _controlsProviders.ToImmutableHashSet(); + public IReadOnlySet ControlsProviders => _controlsProviders; public Control[] AllControls => ControlsProviders.SelectMany(provider => provider.Controls) .Distinct() @@ -52,6 +52,8 @@ private void OnOptionsOnOptionsLoaded(Options options) public abstract bool MouseHitInterface { get; } + public abstract bool IsMouseInBounds { get; } + public bool AddControlsProviders(params IControlsProvider[] controlsProviders) { var success = true; diff --git a/Intersect.Client.Framework/Interface/Data/DataProvider.cs b/Intersect.Client.Framework/Interface/Data/DataProvider.cs index 88df1a54af..750e8ac7f5 100644 --- a/Intersect.Client.Framework/Interface/Data/DataProvider.cs +++ b/Intersect.Client.Framework/Interface/Data/DataProvider.cs @@ -8,6 +8,8 @@ public abstract partial class DataProvider : IDataProvider, IUpd { public event DataProviderEventHandler>? ValueChanged; + public object? UserData { get; set; } + public TValue Value { get; private set; } = default!; protected bool TrySetValue(TValue value) diff --git a/Intersect.Client.Framework/Interface/IMutableInterface.cs b/Intersect.Client.Framework/Interface/IMutableInterface.cs index e23f8e1184..055a386e62 100644 --- a/Intersect.Client.Framework/Interface/IMutableInterface.cs +++ b/Intersect.Client.Framework/Interface/IMutableInterface.cs @@ -4,7 +4,7 @@ namespace Intersect.Client.Interface; public interface IMutableInterface { - List Children { get; } + IReadOnlyList Children { get; } TElement Create(params object[] parameters) where TElement : Base; diff --git a/Intersect.Client/Program.cs b/Intersect.Client/Program.cs index 743f207261..c5a3071549 100644 --- a/Intersect.Client/Program.cs +++ b/Intersect.Client/Program.cs @@ -3,4 +3,4 @@ Console.WriteLine($"Starting {Assembly.GetExecutingAssembly().GetMetadataName()}..."); -Intersect.Client.Core.Program.Main(args); \ No newline at end of file +Intersect.Client.Core.Program.Main(Assembly.GetExecutingAssembly(), args); \ No newline at end of file diff --git a/Intersect.Editor/Content/Texture.cs b/Intersect.Editor/Content/Texture.cs index 258f59c1dc..badebb1f20 100644 --- a/Intersect.Editor/Content/Texture.cs +++ b/Intersect.Editor/Content/Texture.cs @@ -1,4 +1,5 @@ -using Intersect.IO.Files; +using Intersect.Framework.Core; +using Intersect.IO.Files; using Intersect.Utilities; using Microsoft.Extensions.Logging; using Microsoft.Xna.Framework.Graphics; diff --git a/Intersect.Editor/Core/FakeStartupOptionsThatDoNotEverGetUsedForThisGarbageWinFormsEditorStartupOptions.cs b/Intersect.Editor/Core/DummyStartupOptions.cs similarity index 59% rename from Intersect.Editor/Core/FakeStartupOptionsThatDoNotEverGetUsedForThisGarbageWinFormsEditorStartupOptions.cs rename to Intersect.Editor/Core/DummyStartupOptions.cs index 6ea29c4a61..bdb73de0c2 100644 --- a/Intersect.Editor/Core/FakeStartupOptionsThatDoNotEverGetUsedForThisGarbageWinFormsEditorStartupOptions.cs +++ b/Intersect.Editor/Core/DummyStartupOptions.cs @@ -2,8 +2,7 @@ namespace Intersect.Editor.Core; -public sealed class - FakeStartupOptionsThatDoNotEverGetUsedForThisGarbageWinFormsEditorStartupOptions : ICommandLineOptions +public sealed class DummyStartupOptions : ICommandLineOptions { public string WorkingDirectory => Environment.CurrentDirectory; public IEnumerable PluginDirectories => []; diff --git a/Intersect.Editor/Core/EditorContext.cs b/Intersect.Editor/Core/EditorContext.cs new file mode 100644 index 0000000000..115448ab87 --- /dev/null +++ b/Intersect.Editor/Core/EditorContext.cs @@ -0,0 +1,54 @@ +using System.Reflection; +using Intersect.Core; +using Intersect.Plugins.Helpers; +using Intersect.Plugins.Interfaces; +using Intersect.Threading; +using Microsoft.Extensions.Logging; +using ApplicationContext = Intersect.Core.ApplicationContext; + +namespace Intersect.Editor.Core; + +public class EditorContext : IApplicationContext +{ + private bool _disposed; + + public EditorContext(Assembly entryAssembly, PacketHelper packetHelper, ILogger logger) + { + ApplicationContext.Context.Value = this; + + Name = entryAssembly.GetName().Name ?? "Intersect Editor"; + PacketHelper = packetHelper; + Logger = logger; + StartupOptions = new DummyStartupOptions(); + } + + public void Dispose() + { + ObjectDisposedException.ThrowIf(_disposed, this); + _disposed = true; + GC.SuppressFinalize(this); + } + + public bool HasErrors { get; } + + // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter + public bool IsDisposed => _disposed; + + public bool IsStarted => IsRunning || Networking.Network.Connecting; + public bool IsRunning => Networking.Network.Connected; + public string Name { get; } + public DummyStartupOptions StartupOptions { get; } + + ICommandLineOptions IApplicationContext.StartupOptions => StartupOptions; + + public ILogger Logger { get; } + public IPacketHelper PacketHelper { get; } + public List Services => throw new NotSupportedException(); + + public TApplicationService GetService() where TApplicationService : IApplicationService => + throw new NotSupportedException(); + + public void Start(bool lockUntilShutdown = true) => throw new NotSupportedException(); + + public ILockingActionQueue StartWithActionQueue() => throw new NotSupportedException(); +} \ No newline at end of file diff --git a/Intersect.Editor/Core/FakeApplicationContextForThisGarbageWinFormsEditorThatIHateAndWishItWouldBurnInAFireContext.cs b/Intersect.Editor/Core/FakeApplicationContextForThisGarbageWinFormsEditorThatIHateAndWishItWouldBurnInAFireContext.cs deleted file mode 100644 index 88df6d1465..0000000000 --- a/Intersect.Editor/Core/FakeApplicationContextForThisGarbageWinFormsEditorThatIHateAndWishItWouldBurnInAFireContext.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Intersect.Core; -using Intersect.Plugins.Interfaces; -using Intersect.Threading; -using Microsoft.Extensions.Logging; - -namespace Intersect.Editor.Core; - -public sealed class FakeApplicationContextForThisGarbageWinFormsEditorThatIHateAndWishItWouldBurnInAFireContext : IApplicationContext -{ - public void Dispose() - { - IsDisposed = true; - } - - public bool HasErrors { get; set; } - public bool IsDisposed { get; private set; } - public bool IsStarted { get; set; } - public bool IsRunning { get; set; } - ICommandLineOptions IApplicationContext.StartupOptions => StartupOptions; - - public FakeStartupOptionsThatDoNotEverGetUsedForThisGarbageWinFormsEditorStartupOptions StartupOptions { get; } - public ILogger Logger { get; } - public IPacketHelper PacketHelper => throw new NotSupportedException(); - public List Services { get; } - - public TApplicationService GetService() where TApplicationService : IApplicationService - { - throw new NotImplementedException(); - } - - public void Start(bool lockUntilShutdown = true) - { - throw new NotImplementedException(); - } - - public ILockingActionQueue StartWithActionQueue() - { - throw new NotImplementedException(); - } - - public FakeApplicationContextForThisGarbageWinFormsEditorThatIHateAndWishItWouldBurnInAFireContext(ILogger logger) - { - Logger = logger; - StartupOptions = new FakeStartupOptionsThatDoNotEverGetUsedForThisGarbageWinFormsEditorStartupOptions(); - Services = []; - } -} \ No newline at end of file diff --git a/Intersect.Editor/Core/Graphics.cs b/Intersect.Editor/Core/Graphics.cs index 4cef4c2d46..d3398f3eb9 100644 --- a/Intersect.Editor/Core/Graphics.cs +++ b/Intersect.Editor/Core/Graphics.cs @@ -7,6 +7,7 @@ using Intersect.Editor.General; using Intersect.Editor.Maps; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Maps; using Intersect.Utilities; diff --git a/Intersect.Editor/Core/Main.cs b/Intersect.Editor/Core/Main.cs index f2649b7e1c..5c65cd0271 100644 --- a/Intersect.Editor/Core/Main.cs +++ b/Intersect.Editor/Core/Main.cs @@ -3,6 +3,7 @@ using Intersect.Editor.General; using Intersect.Editor.Localization; using Intersect.Editor.Maps; +using Intersect.Framework.Core; using Intersect.Utilities; using Microsoft.Extensions.Logging; diff --git a/Intersect.Editor/Core/Program.cs b/Intersect.Editor/Core/Program.cs index b76a1dab04..703c67b208 100644 --- a/Intersect.Editor/Core/Program.cs +++ b/Intersect.Editor/Core/Program.cs @@ -3,6 +3,8 @@ using Intersect.Editor.Forms; using Intersect.Editor.General; using Intersect.Framework.Logging; +using Intersect.Network; +using Intersect.Plugins.Helpers; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Core; @@ -21,19 +23,6 @@ public static partial class Program static Program() { - LoggingLevelSwitch loggingLevelSwitch = - new(Debugger.IsAttached ? LogEventLevel.Debug : LogEventLevel.Information); - - var executingAssembly = Assembly.GetExecutingAssembly(); - var (_, logger) = new LoggerConfiguration().CreateLoggerForIntersect( - executingAssembly, - "Editor", - loggingLevelSwitch - ); - - ApplicationContext.Context.Value = - new FakeApplicationContextForThisGarbageWinFormsEditorThatIHateAndWishItWouldBurnInAFireContext(logger); - var iconStream = typeof(Program).Assembly.GetManifestResourceStream(IconManifestResourceName); if (iconStream == default) { @@ -52,7 +41,34 @@ static Program() [STAThread] public static void Main() { - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogTrace("Starting editor..."); + LoggingLevelSwitch loggingLevelSwitch = + new(Debugger.IsAttached ? LogEventLevel.Debug : LogEventLevel.Information); + + var executingAssembly = Assembly.GetExecutingAssembly(); + var (loggerFactory, logger) = new LoggerConfiguration().CreateLoggerForIntersect( + executingAssembly, + "Editor", + loggingLevelSwitch + ); + + var packetTypeRegistry = new PacketTypeRegistry( + loggerFactory.CreateLogger(), + typeof(SharedConstants).Assembly + ); + if (!packetTypeRegistry.TryRegisterBuiltIn()) + { + throw new Exception("Failed to register built-in packets."); + } + + var packetHandlerRegistry = new PacketHandlerRegistry( + packetTypeRegistry, + loggerFactory.CreateLogger() + ); + var packetHelper = new PacketHelper(packetTypeRegistry, packetHandlerRegistry); + PackedIntersectPacket.AddKnownTypes(packetHelper.AvailablePacketTypes); + EditorContext editorContext = new(executingAssembly, packetHelper, logger); + + ApplicationContext.CurrentContext.Logger.LogTrace("Starting editor..."); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; Application.ThreadException += Application_ThreadException; @@ -60,7 +76,7 @@ public static void Main() Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogTrace("Unpacking libraries..."); + ApplicationContext.CurrentContext.Logger.LogTrace("Unpacking libraries..."); //Place sqlite3.dll where it's needed. var dllname = Environment.Is64BitProcess ? "sqlite3x64.dll" : "sqlite3x86.dll"; @@ -76,15 +92,15 @@ public static void Main() } } - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogTrace("Libraries unpacked."); + ApplicationContext.CurrentContext.Logger.LogTrace("Libraries unpacked."); - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogTrace("Creating forms..."); + ApplicationContext.CurrentContext.Logger.LogTrace("Creating forms..."); Globals.UpdateForm = new FrmUpdate(); Globals.LoginForm = new FrmLogin(); Globals.MainForm = new FrmMain(); - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogTrace("Forms created."); + ApplicationContext.CurrentContext.Logger.LogTrace("Forms created."); - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogTrace("Starting application."); + ApplicationContext.CurrentContext.Logger.LogTrace("Starting application."); Application.Run(Globals.UpdateForm); } @@ -96,7 +112,7 @@ private static void Application_ThreadException(object sender, ThreadExceptionEv //Really basic error handler for debugging purposes public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs exception) { - Intersect.Core.ApplicationContext.Context.Value?.Logger.LogError( + ApplicationContext.CurrentContext.Logger.LogError( (Exception)exception?.ExceptionObject, "Unhandled exception" ); diff --git a/Intersect.Editor/Entities/Animation.cs b/Intersect.Editor/Entities/Animation.cs index 46525dbeac..44ecd25fc4 100644 --- a/Intersect.Editor/Entities/Animation.cs +++ b/Intersect.Editor/Entities/Animation.cs @@ -1,4 +1,5 @@ using Intersect.Editor.Content; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; using Microsoft.Xna.Framework.Graphics; diff --git a/Intersect.Editor/Forms/frmLogin.cs b/Intersect.Editor/Forms/frmLogin.cs index d57d91ae9c..ae8fbefa03 100644 --- a/Intersect.Editor/Forms/frmLogin.cs +++ b/Intersect.Editor/Forms/frmLogin.cs @@ -7,6 +7,7 @@ using Intersect.Editor.General; using Intersect.Editor.Localization; using Intersect.Editor.Networking; +using Intersect.Framework.Core; using Intersect.Network; using Intersect.Utilities; using Microsoft.Extensions.Logging; diff --git a/Intersect.Editor/Networking/Network.cs b/Intersect.Editor/Networking/Network.cs index 0458fedae5..a9bbe82f30 100644 --- a/Intersect.Editor/Networking/Network.cs +++ b/Intersect.Editor/Networking/Network.cs @@ -1,68 +1,24 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Reflection; using Intersect.Configuration; using Intersect.Editor.General; using Intersect.Network; -using Intersect.Network.Events; using Intersect.Core; +using Intersect.Editor.Core; +using Intersect.Framework.Core; +using Intersect.Network.Events; using Intersect.Threading; using Intersect.Plugins.Helpers; -using Intersect.Plugins.Interfaces; -using Intersect.Rsa; -using System.Diagnostics.CodeAnalysis; -using System.Net; using Intersect.Utilities; using Intersect.Network.Packets.Unconnected.Client; +using Intersect.Rsa; using Microsoft.Extensions.Logging; +using ApplicationContext = Intersect.Core.ApplicationContext; namespace Intersect.Editor.Networking; - -internal sealed class VirtualApplicationContext : IApplicationContext -{ - public VirtualApplicationContext(IPacketHelper packetHelper) - { - PacketHelper = packetHelper; - } - - public bool HasErrors => throw new NotImplementedException(); - - public bool IsDisposed => throw new NotImplementedException(); - - public bool IsStarted => throw new NotImplementedException(); - - public bool IsRunning => throw new NotImplementedException(); - - public ICommandLineOptions StartupOptions => throw new NotImplementedException(); - - public ILogger Logger => Intersect.Core.ApplicationContext.Context.Value?.Logger ?? - throw new InvalidOperationException("Application context not yet initialized"); - - public IPacketHelper PacketHelper { get; } - - public List Services => throw new NotImplementedException(); - - public void Dispose() - { - throw new NotImplementedException(); - } - - public TApplicationService GetService() where TApplicationService : IApplicationService - { - throw new NotImplementedException(); - } - - public void Start(bool lockUntilShutdown = true) - { - throw new NotImplementedException(); - } - - public ILockingActionQueue StartWithActionQueue() - { - throw new NotImplementedException(); - } -} - internal static partial class Network { @@ -90,33 +46,21 @@ public static void InitNetwork() { if (EditorLidgrenNetwork == null) { - var packetTypeRegistry = new PacketTypeRegistry( - Intersect.Core.ApplicationContext.Context.Value?.Logger, - typeof(SharedConstants).Assembly - ); - if (!packetTypeRegistry.TryRegisterBuiltIn()) - { - throw new Exception("Failed to register built-in packets."); - } - - var packetHandlerRegistry = new PacketHandlerRegistry(packetTypeRegistry, Intersect.Core.ApplicationContext.Context.Value?.Logger); - packetHandlerRegistry.TryRegisterAvailableTypeHandlers(typeof(Network).Assembly, requireAttribute: true); - var packetHelper = new PacketHelper(packetTypeRegistry, packetHandlerRegistry); - PackedIntersectPacket.AddKnownTypes(packetHelper.AvailablePacketTypes); - var virtualEditorContext = new VirtualEditorContext(packetHelper, Intersect.Core.ApplicationContext.Context.Value?.Logger); - PacketHandler = new PacketHandler(virtualEditorContext, packetHandlerRegistry); + var editorContext = ApplicationContext.GetCurrentContext(); + var packetHandlerRegistry = editorContext.PacketHelper.HandlerRegistry; + packetHandlerRegistry.TryRegisterAvailableTypeHandlers(typeof(Intersect.Editor.Networking.Network).Assembly, requireAttribute: true); + PacketHandler = new PacketHandler(editorContext, packetHandlerRegistry); var config = new NetworkConfiguration( ClientConfiguration.Instance.Host, ClientConfiguration.Instance.Port ); - var virtualApplicationContext = new VirtualApplicationContext(packetHelper); - var assembly = Assembly.GetExecutingAssembly(); - using (var stream = assembly.GetManifestResourceStream("Intersect.Editor.network.handshake.bkey.pub")) + var executingAssembly = Assembly.GetExecutingAssembly(); + using (var stream = executingAssembly.GetManifestResourceStream("Intersect.Editor.network.handshake.bkey.pub")) { var rsaKey = new RsaKey(stream); Debug.Assert(rsaKey != null, "rsaKey != null"); - EditorLidgrenNetwork = new ClientNetwork(virtualApplicationContext, config, rsaKey.Parameters); + EditorLidgrenNetwork = new ClientNetwork(editorContext, config, rsaKey.Parameters); } EditorLidgrenNetwork.Handler = PacketHandler.HandlePacket; @@ -276,37 +220,4 @@ public static void SendPacket(IntersectPacket packet) } } -} - -internal sealed partial class VirtualEditorContext : IApplicationContext -{ - internal VirtualEditorContext(PacketHelper packetHelper, ILogger logger) - { - PacketHelper = packetHelper; - Logger = logger; - } - - public bool HasErrors => Network.ConnectionDenied; - - public bool IsDisposed { get; private set; } - - public bool IsStarted => IsRunning || Network.Connecting; - - public bool IsRunning => Network.Connected; - - public ICommandLineOptions StartupOptions => default; - - public ILogger Logger { get; } - - public List Services { get; } = new List(); - - public IPacketHelper PacketHelper { get; } - - public void Dispose() => IsDisposed = true; - - public TApplicationService GetService() where TApplicationService : IApplicationService => default; - - public void Start(bool lockUntilShutdown = true) { } - - public ILockingActionQueue StartWithActionQueue() => default; -} +} \ No newline at end of file diff --git a/Intersect.Network/LiteNetLib/LiteNetLibConnection.cs b/Intersect.Network/LiteNetLib/LiteNetLibConnection.cs index 95b67a55d2..0470541897 100644 --- a/Intersect.Network/LiteNetLib/LiteNetLibConnection.cs +++ b/Intersect.Network/LiteNetLib/LiteNetLibConnection.cs @@ -93,7 +93,19 @@ internal bool TryProcessInboundMessage( { buffer = default; - var cipherdata = reader.GetRemainingBytes(); + var cipherdata = reader.GetBytesWithLength(); + +#if DEBUG + byte[]? debugPlaindata = null; + if (Debugger.IsAttached) + { + if (!reader.EndOfData) + { + debugPlaindata = reader.GetBytesWithLength(); + } + } +#endif + if (cipherdata == default) { return false; @@ -115,7 +127,8 @@ internal bool TryProcessInboundMessage( case EncryptionResult.EmptyInput: case EncryptionResult.SizeMismatch: case EncryptionResult.Error: - ApplicationContext.Context.Value?.Logger.LogWarning($"RIEP: {Guid} {decryptionResult}"); + // Symmetric Decryption Error Result + ApplicationContext.Context.Value?.Logger.LogWarning($"SDER: {Guid} {decryptionResult}"); return false; default: throw new UnreachableException(); @@ -141,7 +154,8 @@ public override bool Send(IPacket packet, TransmissionMode transmissionMode = Tr case EncryptionResult.EmptyInput: case EncryptionResult.SizeMismatch: case EncryptionResult.Error: - ApplicationContext.Context.Value?.Logger.LogWarning($"RIEP: {Guid} {encryptionResult}"); + // Symmetric Encryption Error Result + ApplicationContext.Context.Value?.Logger.LogWarning($"SEER: {Guid} {encryptionResult}"); return false; default: @@ -151,9 +165,16 @@ public override bool Send(IPacket packet, TransmissionMode transmissionMode = Tr #if DIAGNOSTIC ApplicationContext.Context.Value?.Logger.LogDebug($"Send({transmissionMode}) cipherdata({cipherdata.Length})={Convert.ToHexString(cipherdata)}"); #endif - NetDataWriter data = new(false, cipherdata.Length + sizeof(byte)); + + NetDataWriter data = new(true, cipherdata.Length + sizeof(byte)); data.Put((byte)1); - data.Put(cipherdata.ToArray()); + data.PutBytesWithLength(cipherdata.ToArray()); +#if DEBUG + if (Debugger.IsAttached) + { + data.PutBytesWithLength(packetData); + } +#endif return Send(data, transmissionMode); } diff --git a/Intersect.Network/LiteNetLib/LiteNetLibInterface.cs b/Intersect.Network/LiteNetLib/LiteNetLibInterface.cs index 9962e31cf4..cc8bb458d3 100644 --- a/Intersect.Network/LiteNetLib/LiteNetLibInterface.cs +++ b/Intersect.Network/LiteNetLib/LiteNetLibInterface.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography; using System.Text; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Framework.Reflection; using Intersect.Memory; using Intersect.Network.Events; diff --git a/Intersect.Server.Core/Core/Bootstrapper.cs b/Intersect.Server.Core/Core/Bootstrapper.cs index 62bc6b069d..e5f3891409 100644 --- a/Intersect.Server.Core/Core/Bootstrapper.cs +++ b/Intersect.Server.Core/Core/Bootstrapper.cs @@ -7,7 +7,7 @@ using Intersect.Core; using Intersect.Factories; using Intersect.Framework.Logging; -using Intersect.Framework.Reflection; +using Intersect.Framework.SystemInformation; using Intersect.GameObjects; using Intersect.GameObjects.Events; using Intersect.GameObjects.Maps; @@ -26,9 +26,6 @@ using Intersect.Utilities; using Microsoft.Extensions.Logging; using Serilog; -using Serilog.Core; -using Serilog.Events; -using Serilog.Extensions.Logging; namespace Intersect.Server.Core; @@ -45,7 +42,7 @@ static Bootstrapper() public static ILockingActionQueue MainThread { get; private set; } - public static void Start(params string[] args) + public static void Start(Assembly entryAssembly, params string[] args) { (string[] Args, Parser Parser, ServerCommandLineOptions CommandLineOptions) parsedArguments = ParseCommandLineArgs(args); @@ -66,13 +63,14 @@ public static void Start(params string[] args) Console.WriteLine("Pre-context setup finished."); - var executingAssembly = Assembly.GetExecutingAssembly(); - var (_, logger) = new LoggerConfiguration().CreateLoggerForIntersect( - executingAssembly, + var (loggerFactory, logger) = new LoggerConfiguration().CreateLoggerForIntersect( + entryAssembly, "Server", LoggingOptions.LoggingLevelSwitch ); + PlatformStatistics.Logger = loggerFactory.CreateLogger(); + var packetTypeRegistry = new PacketTypeRegistry(logger, typeof(SharedConstants).Assembly); if (!packetTypeRegistry.TryRegisterBuiltIn()) { diff --git a/Intersect.Server.Core/Core/LogicService.LogicThread.cs b/Intersect.Server.Core/Core/LogicService.LogicThread.cs index a26760a0c8..1a40c4ff75 100644 --- a/Intersect.Server.Core/Core/LogicService.LogicThread.cs +++ b/Intersect.Server.Core/Core/LogicService.LogicThread.cs @@ -7,6 +7,7 @@ using Amib.Threading; using System.Collections.Concurrent; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Server.Metrics; using Intersect.Server.Networking; using Intersect.Server.Database.PlayerData.Players; diff --git a/Intersect.Server.Core/Core/ServerContext.cs b/Intersect.Server.Core/Core/ServerContext.cs index 0a0d06a93c..fc020e3591 100644 --- a/Intersect.Server.Core/Core/ServerContext.cs +++ b/Intersect.Server.Core/Core/ServerContext.cs @@ -5,6 +5,7 @@ using Intersect.Server.Localization; using Intersect.Server.Networking; using System.Diagnostics; +using System.Reflection; using Intersect.Factories; using Intersect.Plugins; using Intersect.Server.Plugins; @@ -31,14 +32,19 @@ internal partial class ServerContext : ApplicationContext - new ServerContext(options, logger, packetHelper); + new ServerContext(Assembly.GetExecutingAssembly(), options, logger, packetHelper); protected ServerContext( + Assembly entryAssembly, ServerCommandLineOptions startupOptions, ILogger logger, IPacketHelper packetHelper ) : base( - startupOptions, logger, packetHelper + entryAssembly, + "Intersect Server", + startupOptions, + logger, + packetHelper ) { // Register the factory for creating service plugin contexts diff --git a/Intersect.Server.Core/Database/PlayerData/Players/Guild.cs b/Intersect.Server.Core/Database/PlayerData/Players/Guild.cs index eb567a3c73..1281d29eab 100644 --- a/Intersect.Server.Core/Database/PlayerData/Players/Guild.cs +++ b/Intersect.Server.Core/Database/PlayerData/Players/Guild.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using Intersect.Collections.Slotting; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Variables; using Microsoft.EntityFrameworkCore; using Intersect.Network.Packets.Server; diff --git a/Intersect.Server.Core/Database/PlayerData/User.cs b/Intersect.Server.Core/Database/PlayerData/User.cs index b3ea8ecf31..ae518ec6b4 100644 --- a/Intersect.Server.Core/Database/PlayerData/User.cs +++ b/Intersect.Server.Core/Database/PlayerData/User.cs @@ -7,6 +7,7 @@ using System.Text; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Variables; using Intersect.Framework.Reflection; using Intersect.Security; diff --git a/Intersect.Server.Core/Entities/Combat/Dash.cs b/Intersect.Server.Core/Entities/Combat/Dash.cs index 7f3dc9db70..9a2fd60d89 100644 --- a/Intersect.Server.Core/Entities/Combat/Dash.cs +++ b/Intersect.Server.Core/Entities/Combat/Dash.cs @@ -1,5 +1,6 @@ using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Server.Networking; using Intersect.Utilities; using Microsoft.Extensions.Logging; diff --git a/Intersect.Server.Core/Entities/Combat/DoT.cs b/Intersect.Server.Core/Entities/Combat/DoT.cs index 03d40dbafd..f7352f6c6d 100644 --- a/Intersect.Server.Core/Entities/Combat/DoT.cs +++ b/Intersect.Server.Core/Entities/Combat/DoT.cs @@ -1,4 +1,5 @@ using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Utilities; diff --git a/Intersect.Server.Core/Entities/Combat/Status.cs b/Intersect.Server.Core/Entities/Combat/Status.cs index 932d4942c8..3f51f387ad 100644 --- a/Intersect.Server.Core/Entities/Combat/Status.cs +++ b/Intersect.Server.Core/Entities/Combat/Status.cs @@ -1,4 +1,5 @@ using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Server.General; using Intersect.Server.Networking; diff --git a/Intersect.Server.Core/Entities/Entity.cs b/Intersect.Server.Core/Entities/Entity.cs index 62f0841b0d..6ff63d96df 100644 --- a/Intersect.Server.Core/Entities/Entity.cs +++ b/Intersect.Server.Core/Entities/Entity.cs @@ -4,6 +4,7 @@ using Intersect.Collections.Slotting; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Events; using Intersect.GameObjects.Maps; diff --git a/Intersect.Server.Core/Entities/Events/CommandProcessing.cs b/Intersect.Server.Core/Entities/Events/CommandProcessing.cs index 4cde991d92..0cdd5e7e43 100644 --- a/Intersect.Server.Core/Entities/Events/CommandProcessing.cs +++ b/Intersect.Server.Core/Entities/Events/CommandProcessing.cs @@ -1,5 +1,6 @@ using System.Text; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Variables; using Intersect.GameObjects; using Intersect.GameObjects.Events; diff --git a/Intersect.Server.Core/Entities/Events/Event.cs b/Intersect.Server.Core/Entities/Events/Event.cs index 2c97399430..6a9e06fa49 100644 --- a/Intersect.Server.Core/Entities/Events/Event.cs +++ b/Intersect.Server.Core/Entities/Events/Event.cs @@ -1,5 +1,6 @@ using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects.Events; using Intersect.GameObjects.Events.Commands; using Intersect.Server.Localization; diff --git a/Intersect.Server.Core/Entities/Events/EventPageInstance.cs b/Intersect.Server.Core/Entities/Events/EventPageInstance.cs index 93930b95cd..9cb8577642 100644 --- a/Intersect.Server.Core/Entities/Events/EventPageInstance.cs +++ b/Intersect.Server.Core/Entities/Events/EventPageInstance.cs @@ -1,4 +1,5 @@ using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Events; using Intersect.Network.Packets.Server; diff --git a/Intersect.Server.Core/Entities/Npc.cs b/Intersect.Server.Core/Entities/Npc.cs index cb0cf84491..e078d6401d 100644 --- a/Intersect.Server.Core/Entities/Npc.cs +++ b/Intersect.Server.Core/Entities/Npc.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Animations; using Intersect.Network.Packets.Server; diff --git a/Intersect.Server.Core/Entities/Player.Database.cs b/Intersect.Server.Core/Entities/Player.Database.cs index f5a064971f..0da9093115 100644 --- a/Intersect.Server.Core/Entities/Player.Database.cs +++ b/Intersect.Server.Core/Entities/Player.Database.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Server.Database; using Intersect.Server.Database.PlayerData; using Intersect.Server.Networking; diff --git a/Intersect.Server.Core/Entities/Player.cs b/Intersect.Server.Core/Entities/Player.cs index 18a0755c53..2732f64ab6 100644 --- a/Intersect.Server.Core/Entities/Player.cs +++ b/Intersect.Server.Core/Entities/Player.cs @@ -6,6 +6,7 @@ using Intersect.Collections.Slotting; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Variables; using Intersect.GameObjects; using Intersect.GameObjects.Animations; diff --git a/Intersect.Server.Core/Entities/Projectile.cs b/Intersect.Server.Core/Entities/Projectile.cs index 596dc22b37..24e876ee52 100644 --- a/Intersect.Server.Core/Entities/Projectile.cs +++ b/Intersect.Server.Core/Entities/Projectile.cs @@ -1,4 +1,5 @@ using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Maps; using Intersect.Network.Packets.Server; diff --git a/Intersect.Server.Core/Entities/ProjectileSpawn.cs b/Intersect.Server.Core/Entities/ProjectileSpawn.cs index 227502bd79..93f624a6ac 100644 --- a/Intersect.Server.Core/Entities/ProjectileSpawn.cs +++ b/Intersect.Server.Core/Entities/ProjectileSpawn.cs @@ -1,4 +1,5 @@ using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Server.Entities.Combat; using Intersect.Server.Entities.Events; diff --git a/Intersect.Server.Core/General/Time.cs b/Intersect.Server.Core/General/Time.cs index f24ad50b47..f8c76e3ccd 100644 --- a/Intersect.Server.Core/General/Time.cs +++ b/Intersect.Server.Core/General/Time.cs @@ -1,4 +1,5 @@ using Intersect.Core; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Server.Networking; using Intersect.Utilities; diff --git a/Intersect.Server.Core/Maps/MapInstance.cs b/Intersect.Server.Core/Maps/MapInstance.cs index 10815e2a73..255fd6fcf2 100644 --- a/Intersect.Server.Core/Maps/MapInstance.cs +++ b/Intersect.Server.Core/Maps/MapInstance.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.GameObjects.Events; using Intersect.GameObjects.Maps; diff --git a/Intersect.Server.Core/Maps/MapTrapInstance.cs b/Intersect.Server.Core/Maps/MapTrapInstance.cs index 65ac48b0ad..6c9f33d627 100644 --- a/Intersect.Server.Core/Maps/MapTrapInstance.cs +++ b/Intersect.Server.Core/Maps/MapTrapInstance.cs @@ -1,3 +1,4 @@ +using Intersect.Framework.Core; using Intersect.GameObjects; using Intersect.Server.Entities; using Intersect.Server.Entities.Events; diff --git a/Intersect.Server.Core/Metrics/MetricsRoot.cs b/Intersect.Server.Core/Metrics/MetricsRoot.cs index a7cacabf85..9e15ad7e71 100644 --- a/Intersect.Server.Core/Metrics/MetricsRoot.cs +++ b/Intersect.Server.Core/Metrics/MetricsRoot.cs @@ -1,3 +1,4 @@ +using Intersect.Framework.Core; using Intersect.Server.Metrics.Controllers; using Intersect.Utilities; using Newtonsoft.Json; diff --git a/Intersect.Server.Core/Networking/Client.cs b/Intersect.Server.Core/Networking/Client.cs index db653470df..4de69929d3 100644 --- a/Intersect.Server.Core/Networking/Client.cs +++ b/Intersect.Server.Core/Networking/Client.cs @@ -2,6 +2,7 @@ using Intersect.Config; using Intersect.ErrorHandling; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Network; using Intersect.Network.Packets; using Intersect.Server.Database.Logging.Entities; diff --git a/Intersect.Server.Core/Networking/PacketHandler.cs b/Intersect.Server.Core/Networking/PacketHandler.cs index cb822f362d..afec4de177 100644 --- a/Intersect.Server.Core/Networking/PacketHandler.cs +++ b/Intersect.Server.Core/Networking/PacketHandler.cs @@ -20,6 +20,7 @@ using System.Diagnostics; using System.Text; using Intersect.Core; +using Intersect.Framework.Core; using Intersect.Network.Packets.Server; using Intersect.Server.Core; using Microsoft.Extensions.Logging; diff --git a/Intersect.Server.Core/Networking/PacketSender.cs b/Intersect.Server.Core/Networking/PacketSender.cs index a6f528937e..9c2dde5695 100644 --- a/Intersect.Server.Core/Networking/PacketSender.cs +++ b/Intersect.Server.Core/Networking/PacketSender.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Variables; using Intersect.GameObjects; using Intersect.GameObjects.Animations; diff --git a/Intersect.Server/Core/FullServerContext.cs b/Intersect.Server/Core/FullServerContext.cs index 5050e128b2..703bf42ea8 100644 --- a/Intersect.Server/Core/FullServerContext.cs +++ b/Intersect.Server/Core/FullServerContext.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Net; +using System.Reflection; using System.Resources; using Intersect.Core; using Intersect.Framework.Net; @@ -24,12 +25,17 @@ internal class FullServerContext : ServerContext, IFullServerContext private const string AsymmetricKeyManifestResourceName = "Intersect.Server.network.handshake.bkey"; internal FullServerContext( + Assembly entryAssembly, ServerCommandLineOptions startupOptions, ILogger logger, IPacketHelper packetHelper - ) : base(startupOptions, logger, packetHelper) + ) : base(entryAssembly, startupOptions, logger, packetHelper) { - packetHelper.HandlerRegistry.TryRegisterAvailableMethodHandlers(typeof(NetworkedPacketHandler), new NetworkedPacketHandler(), false); + packetHelper.HandlerRegistry.TryRegisterAvailableMethodHandlers( + typeof(NetworkedPacketHandler), + new NetworkedPacketHandler(), + false + ); packetHelper.HandlerRegistry.TryRegisterAvailableTypeHandlers(typeof(FullServerContext).Assembly); } diff --git a/Intersect.Server/Networking/NetworkedPacketHandler.cs b/Intersect.Server/Networking/NetworkedPacketHandler.cs index 686ed8e1f3..ceb93d27db 100644 --- a/Intersect.Server/Networking/NetworkedPacketHandler.cs +++ b/Intersect.Server/Networking/NetworkedPacketHandler.cs @@ -1,5 +1,6 @@ using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Framework.Core.GameObjects.Variables; using Intersect.GameObjects; using Intersect.GameObjects.Crafting; diff --git a/Intersect.Server/Program.cs b/Intersect.Server/Program.cs index 2376e16e61..bae7a1c9f5 100644 --- a/Intersect.Server/Program.cs +++ b/Intersect.Server/Program.cs @@ -22,10 +22,11 @@ public static void Main(string[] args) try { - Console.WriteLine($"Starting {Assembly.GetExecutingAssembly().GetMetadataName()}..."); + var executingAssembly = Assembly.GetExecutingAssembly(); + Console.WriteLine($"Starting {executingAssembly.GetMetadataName()}..."); ServerContext.ServerContextFactory = (options, logger, packetHelper) => - new FullServerContext(options, logger, packetHelper); + new FullServerContext(executingAssembly, options, logger, packetHelper); ServerContext.NetworkFactory = (context, parameters, handlePacket, shouldProcessPacket) => { @@ -39,7 +40,7 @@ public static void Main(string[] args) Client.EnqueueNetworkTask = action => ServerNetwork.Pool.QueueWorkItem(action); - Bootstrapper.Start(args); + Bootstrapper.Start(executingAssembly, args); } catch (Exception exception) { diff --git a/Intersect.Server/Web/Controllers/Api/V1/InfoController.cs b/Intersect.Server/Web/Controllers/Api/V1/InfoController.cs index 49c44e6b8a..c776f16851 100644 --- a/Intersect.Server/Web/Controllers/Api/V1/InfoController.cs +++ b/Intersect.Server/Web/Controllers/Api/V1/InfoController.cs @@ -1,6 +1,7 @@ using System.Net; using Intersect.Core; using Intersect.Enums; +using Intersect.Framework.Core; using Intersect.Server.Core; using Intersect.Server.Entities; using Intersect.Server.General; diff --git a/Intersect.SinglePlayer/Program.cs b/Intersect.SinglePlayer/Program.cs index 7ace96f3a7..b3bcda5338 100644 --- a/Intersect.SinglePlayer/Program.cs +++ b/Intersect.SinglePlayer/Program.cs @@ -13,7 +13,8 @@ using Bootstrapper = Intersect.Server.Core.Bootstrapper; using Console = System.Console; -Console.WriteLine($"Starting {Assembly.GetExecutingAssembly().GetMetadataName()}..."); +var executingAssembly = Assembly.GetExecutingAssembly(); +Console.WriteLine($"Starting {executingAssembly.GetMetadataName()}..."); const string singleplayer = "singleplayer"; var singleplayerPassword = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(singleplayer))); @@ -59,10 +60,10 @@ } }; - Thread serverThread = new(args => Bootstrapper.Start(args as string[])); + Thread serverThread = new(args => Bootstrapper.Start(executingAssembly, args as string[])); serverThread.Start(args.Append("--migrate-automatically").Distinct().ToArray()); - Intersect.Client.Core.Program.Main(args); + Intersect.Client.Core.Program.Main(executingAssembly, args); } finally { diff --git a/Intersect.Tests/Utilities/TimingTests.cs b/Intersect.Tests/Utilities/TimingTests.cs index 77cb79ae63..ecba598399 100644 --- a/Intersect.Tests/Utilities/TimingTests.cs +++ b/Intersect.Tests/Utilities/TimingTests.cs @@ -1,6 +1,7 @@ using Intersect.Utilities; using NUnit.Framework; using System.Diagnostics; +using Intersect.Framework.Core; namespace Intersect.Tests.Server { diff --git a/Intersect.sln.DotSettings b/Intersect.sln.DotSettings index d08674b0f0..6887f56783 100644 --- a/Intersect.sln.DotSettings +++ b/Intersect.sln.DotSettings @@ -1,13 +1,34 @@  + FPS + GPU IP UI UTF UV VBO - <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Functions"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="gl" Suffix="" Style="AaBb_AaBb" /></Policy> - <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Delegates"><ElementKinds><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="gl" Suffix="_d" Style="AaBb" /></Policy> - <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Types"><ElementKinds><Kind Name="STRUCT" /><Kind Name="ENUM" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="GL" Suffix="" Style="aa_bb" /></Policy> - <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Macros"><ElementKinds><Kind Name="ENUM" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="GL_" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="_NVX" Style="AA_BB" /><ExtraRule Prefix="" Suffix="_ATI" Style="AA_BB" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Events"><ElementKinds><Kind Name="EVENT" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + + True + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Generic Type Parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="T" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + + True + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"><ElementKinds><Kind Name="LOCAL_VARIABLE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Any" Description="Read-only Fields"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="_f" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Delegates"><ElementKinds><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="gl" Suffix="_d" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="Value Types"><ElementKinds><Kind Name="STRUCT" /><Kind Name="ENUM" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="GL" Suffix="" Style="aa_bb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="gl" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"><ElementKinds><Kind Name="PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and Namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="DELEGATE" /><Kind Name="ENUM" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"><ElementKinds><Kind Name="INTERFACE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="I" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Macros"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="GL_" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="_NVX" Style="AA_BB" /><ExtraRule Prefix="" Suffix="_ATI" Style="AA_BB" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> True True True