diff --git a/NLog.config b/NLog.config index b951b8d..1025b68 100644 --- a/NLog.config +++ b/NLog.config @@ -1,7 +1,6 @@ - diff --git a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs index 05ebf69..091fd16 100644 --- a/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs +++ b/Torch.Server/ViewModels/ConfigDedicatedViewModel.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using NLog; using Sandbox.Engine.Utils; +using Torch.Collections; using VRage.Game; using VRage.Game.ModAPI; @@ -58,7 +59,7 @@ namespace Torch.Server.ViewModels private SessionSettingsViewModel _sessionSettings; public SessionSettingsViewModel SessionSettings { get => _sessionSettings; set { _sessionSettings = value; OnPropertyChanged(); } } - public ObservableList WorldPaths { get; } = new ObservableList(); + public MtObservableList WorldPaths { get; } = new MtObservableList(); private string _administrators; public string Administrators { get => _administrators; set { _administrators = value; OnPropertyChanged(); } } private string _banned; diff --git a/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs b/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs index 1b90cdb..5575fcc 100644 --- a/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs +++ b/Torch.Server/ViewModels/Entities/Blocks/BlockViewModel.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Sandbox.Game.Entities.Cube; using Sandbox.ModAPI; using Sandbox.ModAPI.Interfaces; +using Torch.Collections; using Torch.Server.ViewModels.Entities; namespace Torch.Server.ViewModels.Blocks @@ -15,7 +16,7 @@ namespace Torch.Server.ViewModels.Blocks public class BlockViewModel : EntityViewModel { public IMyTerminalBlock Block { get; } - public ObservableList Properties { get; } = new ObservableList(); + public MtObservableList Properties { get; } = new MtObservableList(); public string FullName => $"{Block.CubeGrid.CustomName} - {Block.CustomName}"; diff --git a/Torch.Server/ViewModels/Entities/GridViewModel.cs b/Torch.Server/ViewModels/Entities/GridViewModel.cs index b34875e..b591c99 100644 --- a/Torch.Server/ViewModels/Entities/GridViewModel.cs +++ b/Torch.Server/ViewModels/Entities/GridViewModel.cs @@ -2,6 +2,7 @@ using System.Linq; using Sandbox.Game.Entities; using Sandbox.ModAPI; +using Torch.Collections; using Torch.Server.ViewModels.Blocks; namespace Torch.Server.ViewModels.Entities @@ -9,7 +10,7 @@ namespace Torch.Server.ViewModels.Entities public class GridViewModel : EntityViewModel, ILazyLoad { private MyCubeGrid Grid => (MyCubeGrid)Entity; - public ObservableList Blocks { get; } = new ObservableList(); + public MtObservableList Blocks { get; } = new MtObservableList(); /// public string DescriptiveName { get; } @@ -34,7 +35,7 @@ namespace Torch.Server.ViewModels.Entities { var block = obj.FatBlock as IMyTerminalBlock; if (block != null) - Blocks.Insert(new BlockViewModel(block, Tree), b => b.Name); + Blocks.Add(new BlockViewModel(block, Tree)); OnPropertyChanged(nameof(Name)); } diff --git a/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs b/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs index 36a6c1c..e9b76aa 100644 --- a/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs +++ b/Torch.Server/ViewModels/Entities/VoxelMapViewModel.cs @@ -4,6 +4,7 @@ using Sandbox.Game.Entities; using VRage.Game.Entity; using VRage.Game.ModAPI; using System.Threading.Tasks; +using Torch.Collections; namespace Torch.Server.ViewModels.Entities { @@ -15,7 +16,7 @@ namespace Torch.Server.ViewModels.Entities public override bool CanStop => false; - public ObservableList AttachedGrids { get; } = new ObservableList(); + public MtObservableList AttachedGrids { get; } = new MtObservableList(); public async Task UpdateAttachedGrids() { diff --git a/Torch.Server/ViewModels/EntityTreeViewModel.cs b/Torch.Server/ViewModels/EntityTreeViewModel.cs index 0bfd279..87b00cf 100644 --- a/Torch.Server/ViewModels/EntityTreeViewModel.cs +++ b/Torch.Server/ViewModels/EntityTreeViewModel.cs @@ -11,16 +11,17 @@ using VRage.Game.ModAPI; using VRage.ModAPI; using System.Windows.Threading; using NLog; +using Torch.Collections; namespace Torch.Server.ViewModels { public class EntityTreeViewModel : ViewModel { //TODO: these should be sorted sets for speed - public ObservableList Grids { get; set; } = new ObservableList(); - public ObservableList Characters { get; set; } = new ObservableList(); - public ObservableList FloatingObjects { get; set; } = new ObservableList(); - public ObservableList VoxelMaps { get; set; } = new ObservableList(); + public MtObservableList Grids { get; set; } = new MtObservableList(); + public MtObservableList Characters { get; set; } = new MtObservableList(); + public MtObservableList FloatingObjects { get; set; } = new MtObservableList(); + public MtObservableList VoxelMaps { get; set; } = new MtObservableList(); public Dispatcher ControlDispatcher => _control.Dispatcher; private EntityViewModel _currentEntity; @@ -29,7 +30,7 @@ namespace Torch.Server.ViewModels public EntityViewModel CurrentEntity { get => _currentEntity; - set { _currentEntity = value; OnPropertyChanged(); } + set { _currentEntity = value; OnPropertyChanged(nameof(CurrentEntity)); } } public EntityTreeViewModel(UserControl control) @@ -67,16 +68,16 @@ namespace Torch.Server.ViewModels switch (obj) { case MyCubeGrid grid: - Grids.Insert(new GridViewModel(grid, this), g => g.Name); + Grids.Add(new GridViewModel(grid, this)); break; case MyCharacter character: - Characters.Insert(new CharacterViewModel(character, this), c => c.Name); + Characters.Add(new CharacterViewModel(character, this)); break; case MyFloatingObject floating: - FloatingObjects.Insert(new FloatingObjectViewModel(floating, this), f => f.Name); + FloatingObjects.Add(new FloatingObjectViewModel(floating, this)); break; case MyVoxelBase voxel: - VoxelMaps.Insert(new VoxelMapViewModel(voxel, this), v => v.Name); + VoxelMaps.Add(new VoxelMapViewModel(voxel, this)); break; } } diff --git a/Torch.Server/ViewModels/PluginManagerViewModel.cs b/Torch.Server/ViewModels/PluginManagerViewModel.cs index d910cf8..52533e9 100644 --- a/Torch.Server/ViewModels/PluginManagerViewModel.cs +++ b/Torch.Server/ViewModels/PluginManagerViewModel.cs @@ -6,18 +6,19 @@ using System.Threading.Tasks; using Torch.API; using Torch.API.Managers; using Torch.API.Plugins; +using Torch.Collections; namespace Torch.Server.ViewModels { public class PluginManagerViewModel : ViewModel { - public ObservableList Plugins { get; } = new ObservableList(); + public MtObservableList Plugins { get; } = new MtObservableList(); private PluginViewModel _selectedPlugin; public PluginViewModel SelectedPlugin { get => _selectedPlugin; - set { _selectedPlugin = value; OnPropertyChanged(); } + set { _selectedPlugin = value; OnPropertyChanged(nameof(SelectedPlugin)); } } public PluginManagerViewModel() { } diff --git a/Torch.Server/ViewModels/SessionSettingsViewModel.cs b/Torch.Server/ViewModels/SessionSettingsViewModel.cs index f354dd5..35d569d 100644 --- a/Torch.Server/ViewModels/SessionSettingsViewModel.cs +++ b/Torch.Server/ViewModels/SessionSettingsViewModel.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using SharpDX.Toolkit.Collections; +using Torch.Collections; using VRage.Game; using VRage.Library.Utils; @@ -35,7 +36,7 @@ namespace Torch.Server.ViewModels BlockLimits.Add(new BlockLimitViewModel(this, limit.Key, limit.Value)); } - public ObservableList BlockLimits { get; } = new ObservableList(); + public MtObservableList BlockLimits { get; } = new MtObservableList(); #region Multipliers diff --git a/Torch/Collections/MTObservableCollection.cs b/Torch/Collections/MTObservableCollection.cs index fa513aa..b93a3d1 100644 --- a/Torch/Collections/MTObservableCollection.cs +++ b/Torch/Collections/MTObservableCollection.cs @@ -1,77 +1,358 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Windows.Threading; +using Torch.Utils; -namespace Torch +namespace Torch.Collections { - [Obsolete("Use ObservableList.")] - public class MTObservableCollection : ObservableCollection + /// + /// Multithread safe, observable collection + /// + /// Collection type + /// Value type + public abstract class MtObservableCollection : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable where TC : class, ICollection { - public override event NotifyCollectionChangedEventHandler CollectionChanged; + protected readonly ReaderWriterLockSlim Lock; + protected readonly TC Backing; + private int _version; + private readonly ThreadLocal _threadViews; - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + protected MtObservableCollection(TC backing) { - NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged; - if (collectionChanged != null) - foreach (var del in collectionChanged.GetInvocationList()) - { - var nh = (NotifyCollectionChangedEventHandler)del; - var dispObj = nh.Target as DispatcherObject; + Backing = backing; + // recursion so the events can read snapshots. + Lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + _version = 0; + _threadViews = new ThreadLocal(() => new ThreadView(this)); + } - var dispatcher = dispObj?.Dispatcher; - if (dispatcher != null && !dispatcher.CheckAccess()) - { - dispatcher.BeginInvoke( - (Action)(() => nh.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))), - DispatcherPriority.DataBind); - continue; - } + /// + /// Takes a snapshot of this collection. Note: This call is only done when a read lock is acquired. + /// + /// Collection to clear and reuse, or null if none + /// The snapshot + protected abstract TC Snapshot(TC old); - nh.Invoke(this, e); + /// + /// Marks all snapshots taken of this collection as dirty. + /// + protected void MarkSnapshotsDirty() + { + _version++; + } + + #region ICollection + /// + public void Add(TV item) + { + using(Lock.WriteUsing()) + { + Backing.Add(item); + MarkSnapshotsDirty(); + OnPropertyChanged(nameof(Count)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Backing.Count - 1)); + } + } + + /// + public void Clear() + { + using(Lock.WriteUsing()) + { + Backing.Clear(); + MarkSnapshotsDirty(); + OnPropertyChanged(nameof(Count)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } + + /// + public bool Contains(TV item) + { + using (Lock.ReadUsing()) + return Backing.Contains(item); + } + + /// + public void CopyTo(TV[] array, int arrayIndex) + { + using (Lock.ReadUsing()) + Backing.CopyTo(array, arrayIndex); + } + + /// + public bool Remove(TV item) + { + using(Lock.UpgradableReadUsing()) { + int? oldIndex = (Backing as IList)?.IndexOf(item); + if (oldIndex == -1) + return false; + using(Lock.WriteUsing()) { + if (!Backing.Remove(item)) + return false; + MarkSnapshotsDirty(); + + OnPropertyChanged(nameof(Count)); + OnCollectionChanged(oldIndex != null + ? new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, oldIndex) + : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item)); + return true; } - } - - public void Insert(T item, Func selector, IComparer comparer) - { - var key = selector(item); - for (var i = 0; i < Count; i++) - { - var key2 = selector(Items[i]); - if (comparer.Compare(key, key2) < 1) - continue; - - Insert(i + 1, item); - return; - } - - Add(item); - } - - public void Sort(Func selector, IComparer comparer = null) - { - List sortedItems; - if (comparer != null) - sortedItems = Items.OrderBy(selector, comparer).ToList(); - else - sortedItems = Items.OrderBy(selector).ToList(); - - Items.Clear(); - foreach (var item in sortedItems) - Add(item); - } - - public void RemoveWhere(Func condition) - { - for (var i = Items.Count - 1; i > 0; i--) - { - if (condition(Items[i])) - RemoveAt(i); } } + + /// + public int Count + { + get + { + using (Lock.ReadUsing()) + return Backing.Count; + } + } + + /// + public bool IsReadOnly => Backing.IsReadOnly; + #endregion + + #region Event Wrappers + protected void OnPropertyChanged(string propName) + { + NotifyEvent(this, new PropertyChangedEventArgs(propName)); + } + + protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + NotifyEvent(this, e); + } + + protected void NotifyEvent(object sender, PropertyChangedEventArgs args) + { + _propertyChangedEvent.Raise(sender, args); + } + + protected void NotifyEvent(object sender, NotifyCollectionChangedEventArgs args) + { + _collectionChangedEvent.Raise(sender, args); + } + + private readonly DispatcherEvent _propertyChangedEvent = + new DispatcherEvent(); + /// + public event PropertyChangedEventHandler PropertyChanged + { + add => _propertyChangedEvent.Add(value); + remove => _propertyChangedEvent.Remove(value); + } + + private readonly DispatcherEvent _collectionChangedEvent = + new DispatcherEvent(); + /// + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => _collectionChangedEvent.Add(value); + remove => _collectionChangedEvent.Remove(value); + } + /// + /// Event that invokes handlers registered by dispatchers on dispatchers. + /// + /// Event argument type + /// Event handler delegate type + private sealed class DispatcherEvent where TEvtArgs : EventArgs + { + private delegate void DelInvokeHandler(TEvtHandle handler, object sender, TEvtArgs args); + + private static readonly DelInvokeHandler _invokeDirectly; + static DispatcherEvent() + { + MethodInfo invoke = typeof(TEvtHandle).GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); + Debug.Assert(invoke != null, "No invoke method on handler type"); + _invokeDirectly = (DelInvokeHandler)Delegate.CreateDelegate(typeof(DelInvokeHandler), invoke); + } + + private static Dispatcher CurrentDispatcher => Dispatcher.FromThread(Thread.CurrentThread); + + + private event EventHandler _event; + + internal void Raise(object sender, TEvtArgs args) + { + _event?.Invoke(sender, args); + } + + internal void Add(TEvtHandle evt) + { + if (evt == null) + return; + _event += new DispatcherDelegate(evt).Invoke; + } + + internal void Remove(TEvtHandle evt) + { + if (_event == null || evt == null) + return; + Delegate[] invokeList = _event.GetInvocationList(); + for (int i = invokeList.Length - 1; i >= 0; i--) + { + var wrapper = (DispatcherDelegate)invokeList[i].Target; + Debug.Assert(wrapper._dispatcher == CurrentDispatcher, "Adding and removing should be done from the same dispatcher"); + if (wrapper._delegate.Equals(evt)) + { + _event -= wrapper.Invoke; + return; + } + } + } + + private struct DispatcherDelegate + { + internal readonly Dispatcher _dispatcher; + internal readonly TEvtHandle _delegate; + + internal DispatcherDelegate(TEvtHandle del) + { + _dispatcher = CurrentDispatcher; + _delegate = del; + } + + public void Invoke(object sender, TEvtArgs args) + { + if (_dispatcher == null || _dispatcher == CurrentDispatcher) + _invokeDirectly(_delegate, sender, args); + else + // (Delegate) (object) == dual cast so that the compiler likes it + _dispatcher.BeginInvoke((Delegate)(object)_delegate, DispatcherPriority.DataBind, sender, args); + } + } + } + + #endregion + + #region Enumeration + /// + /// Manages a snapshot to a collection and dispatches enumerators from that snapshot. + /// + private sealed class ThreadView + { + private readonly MtObservableCollection _owner; + private readonly WeakReference _snapshot; + /// + /// The of the + /// + private int _snapshotVersion; + /// + /// Number of strong references to the value pointed to be + /// + private int _snapshotRefCount; + + internal ThreadView(MtObservableCollection owner) + { + _owner = owner; + _snapshot = new WeakReference(null); + _snapshotVersion = 0; + _snapshotRefCount = 0; + } + + private TC GetSnapshot() + { + // reading the version number + snapshots + using (_owner.Lock.ReadUsing()) + { + if (!_snapshot.TryGetTarget(out TC currentSnapshot) || _snapshotVersion != _owner._version) + { + // Update the snapshot, using the old one if it isn't referenced. + currentSnapshot = _owner.Snapshot(_snapshotRefCount == 0 ? currentSnapshot : null); + _snapshotVersion = _owner._version; + _snapshotRefCount = 0; + _snapshot.SetTarget(currentSnapshot); + } + return currentSnapshot; + } + } + + /// + /// Borrows a snapshot from a and provides an enumerator. + /// Once is called the read lock is released. + /// + internal sealed class Enumerator : IEnumerator + { + private readonly IEnumerator _backing; + private readonly ThreadView _owner; + private bool _disposed; + + internal Enumerator(ThreadView owner) + { + _owner = owner; + // Lock required since destructors run MT + lock (_owner) + { + _owner._snapshotRefCount++; + _backing = owner.GetSnapshot().GetEnumerator(); + } + _disposed = false; + } + + ~Enumerator() + { + // Lock required since destructors run MT + if (!_disposed && _owner != null) + lock (_owner) + Dispose(); + } + + public void Dispose() + { + // safe deref so finalizer can clean up + _backing?.Dispose(); + _owner._snapshotRefCount--; + _disposed = true; + } + + public bool MoveNext() + { + if (_disposed) + throw new ObjectDisposedException(nameof(Enumerator)); + return _backing.MoveNext(); + } + + public void Reset() + { + if (_disposed) + throw new ObjectDisposedException(nameof(Enumerator)); + _backing.Reset(); + } + + public TV Current + { + get + { + if (_disposed) + throw new ObjectDisposedException(nameof(Enumerator)); + return _backing.Current; + } + } + + object IEnumerator.Current => Current; + } + } + + /// + public IEnumerator GetEnumerator() + { + return new ThreadView.Enumerator(_threadViews.Value); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion } } diff --git a/Torch/Collections/MtObservableDictionary.cs b/Torch/Collections/MtObservableDictionary.cs new file mode 100644 index 0000000..3aea46a --- /dev/null +++ b/Torch/Collections/MtObservableDictionary.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Torch.Utils; + +namespace Torch.Collections +{ + /// + /// Multithread safe observable dictionary + /// + /// Key type + /// Value type + public class MtObservableDictionary : MtObservableCollection, KeyValuePair>, IDictionary + { + /// + /// Creates an empty observable dictionary + /// + public MtObservableDictionary() : base(new Dictionary()) + { + Keys = new ProxyCollection(this, Backing.Keys, (x) => x.Key); + Values = new ProxyCollection(this, Backing.Values, (x) => x.Value); + } + + protected override IDictionary Snapshot(IDictionary old) + { + if (old == null) + return new Dictionary(Backing); + old.Clear(); + foreach (KeyValuePair k in Backing) + old.Add(k); + return old; + } + + /// + public bool ContainsKey(TK key) + { + using (Lock.ReadUsing()) + return Backing.ContainsKey(key); + } + + /// + public void Add(TK key, TV value) + { + Add(new KeyValuePair(key, value)); + } + + /// + public bool Remove(TK key) + { + return TryGetValue(key, out TV result) && Remove(new KeyValuePair(key, result)); + } + + /// + public bool TryGetValue(TK key, out TV value) + { + using (Lock.ReadUsing()) + return Backing.TryGetValue(key, out value); + } + + /// + public TV this[TK key] + { + get + { + using (Lock.ReadUsing()) + return Backing[key]; + } + set + { + using (Lock.WriteUsing()) + { + var oldKv = new KeyValuePair(key, Backing[key]); + var newKv = new KeyValuePair(key, value); + Backing[key] = value; + MarkSnapshotsDirty(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newKv, oldKv)); + } + } + } + + /// + public ICollection Keys { get; } + + /// + public ICollection Values { get; } + + internal void RaiseFullReset() + { + OnPropertyChanged(nameof(Count)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + private class ProxyCollection : ICollection + { + private readonly MtObservableDictionary _owner; + private readonly ICollection _backing; + private readonly Func, TP> _selector; + + internal ProxyCollection(MtObservableDictionary owner, ICollection backing, Func, TP> selector) + { + _owner = owner; + _backing = backing; + _selector = selector; + } + + /// + public IEnumerator GetEnumerator() => new TransformEnumerator, TP>(_owner.GetEnumerator(), _selector); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public void Add(TP item) + { + using (_owner.Lock.WriteUsing()) + { + _backing.Add(item); + _owner.RaiseFullReset(); + } + } + + /// + public void Clear() + { + _owner.Clear(); + } + + /// + public bool Contains(TP item) + { + using (_owner.Lock.ReadUsing()) + return _backing.Contains(item); + } + + /// + public void CopyTo(TP[] array, int arrayIndex) + { + using (_owner.Lock.ReadUsing()) + _backing.CopyTo(array, arrayIndex); + } + + /// + public bool Remove(TP item) + { + using (_owner.Lock.WriteUsing()) + { + if (!_backing.Remove(item)) + return false; + _owner.RaiseFullReset(); + return true; + } + } + + /// + public int Count + { + get + { + using (_owner.Lock.ReadUsing()) + return _backing.Count; + } + } + + public bool IsReadOnly => _backing.IsReadOnly; + } + } +} diff --git a/Torch/Collections/MtObservableList.cs b/Torch/Collections/MtObservableList.cs new file mode 100644 index 0000000..76d2075 --- /dev/null +++ b/Torch/Collections/MtObservableList.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Torch.Utils; + +namespace Torch.Collections +{ + /// + /// Multithread safe, observable list + /// + /// Value type + public class MtObservableList : MtObservableCollection, T>, IList + { + /// + /// Initializes a new instance of the MtObservableList class that is empty and has the default initial capacity. + /// + public MtObservableList() : base(new List()) + { + } + + /// + /// Initializes a new instance of the MtObservableList class that is empty and has the specified initial capacity. + /// + /// + public MtObservableList(int capacity) : base(new List(capacity)) + { + } + + protected override IList Snapshot(IList old) + { + if (old == null) + { + var list = new List(Backing); + return list; + } + old.Clear(); + if (old is List tmp) + tmp.AddRange(Backing); + else + foreach (T k in Backing) + old.Add(k); + return old; + } + + /// + public int IndexOf(T item) + { + using (Lock.ReadUsing()) + return Backing.IndexOf(item); + } + + /// + public void Insert(int index, T item) + { + using(Lock.WriteUsing()) + { + Backing.Insert(index, item); + MarkSnapshotsDirty(); + OnPropertyChanged(nameof(Count)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + } + } + + /// + public void RemoveAt(int index) + { + using (Lock.WriteUsing()) + { + T old = Backing[index]; + Backing.RemoveAt(index); + MarkSnapshotsDirty(); + OnPropertyChanged(nameof(Count)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old, index)); + } + } + + /// + public T this[int index] + { + get + { + using(Lock.ReadUsing()) + return Backing[index]; + } + set + { + using(Lock.ReadUsing()) { + T old = Backing[index]; + Backing[index] = value; + MarkSnapshotsDirty(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old, index)); + } + } + } + + /// + public void RemoveWhere(Func predicate) + { + for (int i = Count - 1; i >= 0; i--) + if (predicate(this[i])) + RemoveAt(i); + } + + /// + /// Sorts the list using the given selector and comparer./> + /// + public void Sort(Func selector, IComparer comparer = null) + { + using (Lock.ReadUsing()) { + comparer = comparer ?? Comparer.Default; + if (Backing is List lst) + lst.Sort(new TransformComparer(selector, comparer)); + else + { + List sortedItems = Backing.OrderBy(selector, comparer).ToList(); + Backing.Clear(); + foreach (T v in sortedItems) + Backing.Add(v); + } + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } + } +} diff --git a/Torch/Collections/ObservableDictionary.cs b/Torch/Collections/ObservableDictionary.cs deleted file mode 100644 index 0b0a3ea..0000000 --- a/Torch/Collections/ObservableDictionary.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Threading; - -namespace Torch.Collections -{ - [Serializable] - public class ObservableDictionary : ViewModel, IDictionary - { - private IDictionary _internalDict; - - public ObservableDictionary() - { - _internalDict = new Dictionary(); - } - - public ObservableDictionary(IDictionary dictionary) - { - _internalDict = new Dictionary(dictionary); - } - - /// - /// Create a using the given dictionary by reference. The original dictionary should not be used after calling this. - /// - public static ObservableDictionary ByReference(IDictionary dictionary) - { - return new ObservableDictionary - { - _internalDict = dictionary - }; - } - - /// - public IEnumerator> GetEnumerator() - { - return _internalDict.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)_internalDict).GetEnumerator(); - } - - /// - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - /// - public bool Remove(KeyValuePair item) - { - return Remove(item.Key); - } - - /// - public void Clear() - { - _internalDict.Clear(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - OnPropertyChanged(nameof(Count)); - } - - /// - public bool Contains(KeyValuePair item) - { - return _internalDict.Contains(item); - } - - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - foreach (var kv in _internalDict) - { - array[arrayIndex] = kv; - arrayIndex++; - } - } - - /// - public int Count => _internalDict.Count; - - /// - public bool IsReadOnly => false; - - /// - public bool ContainsKey(TKey key) - { - return _internalDict.ContainsKey(key); - } - - /// - public void Add(TKey key, TValue value) - { - _internalDict.Add(key, value); - var kv = new KeyValuePair(key, value); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, kv)); - OnPropertyChanged(nameof(Count)); - } - - /// - public bool Remove(TKey key) - { - if (!_internalDict.ContainsKey(key)) - return false; - - var kv = new KeyValuePair(key, this[key]); - if (!_internalDict.Remove(key)) - return false; - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, kv)); - OnPropertyChanged(nameof(Count)); - return true; - } - - /// - public bool TryGetValue(TKey key, out TValue value) - { - return _internalDict.TryGetValue(key, out value); - } - - /// - public TValue this[TKey key] - { - get => _internalDict[key]; - set - { - var oldKv = new KeyValuePair(key, _internalDict[key]); - var newKv = new KeyValuePair(key, value); - _internalDict[key] = value; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newKv, oldKv)); - } - - - } - - /// - public ICollection Keys => _internalDict.Keys; - - /// - public ICollection Values => _internalDict.Values; - } -} diff --git a/Torch/Collections/ObservableList.cs b/Torch/Collections/ObservableList.cs deleted file mode 100644 index bb8e480..0000000 --- a/Torch/Collections/ObservableList.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Windows.Threading; - -namespace Torch -{ - /// - /// An observable version of . - /// - public class ObservableList : ViewModel, IList - { - private List _internalList = new List(); - - /// - public void Clear() - { - _internalList.Clear(); - OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - public bool Contains(T item) - { - return _internalList.Contains(item); - } - - /// - public void CopyTo(T[] array, int arrayIndex) - { - _internalList.CopyTo(array, arrayIndex); - } - - /// - public bool Remove(T item) - { - var oldIndex = _internalList.IndexOf(item); - if (!_internalList.Remove(item)) - return false; - - OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, oldIndex)); - return true; - } - - /// - public int Count => _internalList.Count; - - /// - public bool IsReadOnly => false; - - /// - public void Add(T item) - { - _internalList.Add(item); - OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1)); - } - - /// - public int IndexOf(T item) => _internalList.IndexOf(item); - - /// - public void Insert(int index, T item) - { - _internalList.Insert(index, item); - OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); - } - - /// - /// Inserts an item in order based on the provided selector and comparer. This will only work properly on a pre-sorted list. - /// - public void Insert(T item, Func selector, IComparer comparer = null) - { - comparer = comparer ?? Comparer.Default; - var key1 = selector(item); - for (var i = 0; i < _internalList.Count; i++) - { - var key2 = selector(_internalList[i]); - if (comparer.Compare(key1, key2) < 1) - { - Insert(i, item); - return; - } - } - - Add(item); - } - - /// - public void RemoveAt(int index) - { - var old = this[index]; - _internalList.RemoveAt(index); - OnPropertyChanged(nameof(Count)); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, old, index)); - } - - public T this[int index] - { - get => _internalList[index]; - set - { - var old = _internalList[index]; - if (old.Equals(value)) - return; - - _internalList[index] = value; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old, index)); - } - } - - /// - /// Sorts the list using the given selector and comparer./> - /// - public void Sort(Func selector, IComparer comparer = null) - { - comparer = comparer ?? Comparer.Default; - var sortedItems = _internalList.OrderBy(selector, comparer).ToList(); - - _internalList = sortedItems; - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - /// Removes all items that satisfy the given condition. - /// - public void RemoveWhere(Func condition) - { - for (var i = Count - 1; i > 0; i--) - { - if (condition?.Invoke(this[i]) ?? false) - RemoveAt(i); - } - } - - /// - public IEnumerator GetEnumerator() - { - return _internalList.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)_internalList).GetEnumerator(); - } - } -} diff --git a/Torch/Collections/TransformComparer.cs b/Torch/Collections/TransformComparer.cs new file mode 100644 index 0000000..893fa17 --- /dev/null +++ b/Torch/Collections/TransformComparer.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.Collections +{ + /// + /// Comparer that uses a delegate to select the key to compare on. + /// + /// Input to this comparer + /// Type of comparison key + public class TransformComparer : IComparer + { + private readonly IComparer _comparer; + private readonly Func _selector; + + /// + /// Creates a new transforming comparer that uses the given key selector, and the given key comparer. + /// + /// Key selector + /// Key comparer + public TransformComparer(Func transform, IComparer comparer = null) + { + _selector = transform; + _comparer = comparer ?? Comparer.Default; + } + + /// + public int Compare(TIn x, TIn y) + { + return _comparer.Compare(_selector(x), _selector(y)); + } + } +} diff --git a/Torch/Collections/TransformEnumerator.cs b/Torch/Collections/TransformEnumerator.cs new file mode 100644 index 0000000..5a3a431 --- /dev/null +++ b/Torch/Collections/TransformEnumerator.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Torch.Collections +{ + /// + /// Enumerator that transforms from one enumeration into another. + /// + /// Input type + /// Output type + public class TransformEnumerator : IEnumerator + { + private readonly IEnumerator _input; + private readonly Func _transform; + + /// + /// Creates a new transform enumerator with the given transform function + /// + /// Input to proxy enumerator + /// Transform function + public TransformEnumerator(IEnumerator input, Func transform) + { + _input = input; + _transform = transform; + } + + /// + public void Dispose() + { + _input.Dispose(); + } + + /// + public bool MoveNext() + { + return _input.MoveNext(); + } + + /// + public void Reset() + { + _input.Reset(); + } + + /// + public TOut Current => _transform(_input.Current); + + /// + object IEnumerator.Current => Current; + } +} diff --git a/Torch/Managers/KeenLogManager.cs b/Torch/Managers/KeenLogPatch.cs similarity index 97% rename from Torch/Managers/KeenLogManager.cs rename to Torch/Managers/KeenLogPatch.cs index 41734f7..dfa9c2a 100644 --- a/Torch/Managers/KeenLogManager.cs +++ b/Torch/Managers/KeenLogPatch.cs @@ -14,7 +14,7 @@ using VRage.Utils; namespace Torch.Managers { [PatchShim] - internal class KeenLogManager + internal static class KeenLogPatch { private static readonly Logger _log = LogManager.GetLogger("Keen"); @@ -63,7 +63,7 @@ namespace Torch.Managers private static MethodInfo Method(string name) { - return typeof(KeenLogManager).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + return typeof(KeenLogPatch).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); } [ReflectedMethod(Name = "GetThreadId")] diff --git a/Torch/Managers/MultiplayerManagerBase.cs b/Torch/Managers/MultiplayerManagerBase.cs index 4825ac2..0533e61 100644 --- a/Torch/Managers/MultiplayerManagerBase.cs +++ b/Torch/Managers/MultiplayerManagerBase.cs @@ -48,7 +48,7 @@ namespace Torch.Managers /// public event Action PlayerLeft; - public ObservableDictionary Players { get; } = new ObservableDictionary(); + public MtObservableDictionary Players { get; } = new MtObservableDictionary(); #pragma warning disable 649 [ReflectedGetter(Name = "m_players")] diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs index 4b0e953..848a2da 100644 --- a/Torch/Plugins/PluginManager.cs +++ b/Torch/Plugins/PluginManager.cs @@ -27,7 +27,7 @@ namespace Torch.Managers private static Logger _log = LogManager.GetLogger(nameof(PluginManager)); private const string MANIFEST_NAME = "manifest.xml"; public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); - private readonly ObservableDictionary _plugins = new ObservableDictionary(); + private readonly MtObservableDictionary _plugins = new MtObservableDictionary(); #pragma warning disable 649 [Dependency] private ITorchSessionManager _sessionManager; diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index 7ba8cc0..4994736 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -154,7 +154,11 @@ Properties\AssemblyVersion.cs - + + + + + @@ -164,7 +168,7 @@ - + @@ -190,7 +194,6 @@ - @@ -215,6 +218,7 @@ + @@ -224,7 +228,6 @@ - diff --git a/Torch/Utils/SynchronizationExtensions.cs b/Torch/Utils/SynchronizationExtensions.cs new file mode 100644 index 0000000..0364cab --- /dev/null +++ b/Torch/Utils/SynchronizationExtensions.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Torch.Utils +{ + /// + /// Extension functions related to synchronization + /// + public static class SynchronizationExtensions + { + + /// + /// Acquires a RAII view of the lock in read mode. + /// + /// Lock + /// RAII token + public static IDisposable ReadUsing(this ReaderWriterLockSlim lck) + { + return new ReaderWriterLockSlimReadToken(lck); + } + + /// + /// Acquires a RAII view of the lock in upgradable read mode. + /// + /// Lock + /// RAII token + public static IDisposable UpgradableReadUsing(this ReaderWriterLockSlim lck) + { + return new ReaderWriterLockSlimUpgradableToken(lck); + } + + /// + /// Acquires a RAII view of the lock in write mode. + /// + /// Lock + /// RAII token + public static IDisposable WriteUsing(this ReaderWriterLockSlim lck) + { + return new ReaderWriterLockSlimWriteToken(lck); + } + + #region Support Structs + private struct ReaderWriterLockSlimUpgradableToken : IDisposable + { + private ReaderWriterLockSlim _lock; + + public ReaderWriterLockSlimUpgradableToken(ReaderWriterLockSlim lc) + { + _lock = lc; + lc.EnterUpgradeableReadLock(); + } + + public void Dispose() + { + _lock.ExitUpgradeableReadLock(); + } + } + + private struct ReaderWriterLockSlimWriteToken : IDisposable + { + private ReaderWriterLockSlim _lock; + + public ReaderWriterLockSlimWriteToken(ReaderWriterLockSlim lc) + { + _lock = lc; + lc.EnterWriteLock(); + } + + public void Dispose() + { + _lock.ExitWriteLock(); + } + } + + private struct ReaderWriterLockSlimReadToken : IDisposable + { + private ReaderWriterLockSlim _lock; + + public ReaderWriterLockSlimReadToken(ReaderWriterLockSlim lc) + { + _lock = lc; + lc.EnterReadLock(); + } + + public void Dispose() + { + _lock.ExitReadLock(); + } + } + #endregion + } +}