From efd6b7c4feed7e8cdc263dd0f7d546019872366d Mon Sep 17 00:00:00 2001 From: snixtho Date: Tue, 2 Jan 2024 11:04:26 +0100 Subject: [PATCH 01/76] initial --- EvoSC.sln | 10 +++++++- src/Modules/MapQueueModule/Localization.resx | 19 ++++++++++++++ src/Modules/MapQueueModule/MapQueueModule.cs | 8 ++++++ .../MapQueueModule/MapQueueModule.csproj | 25 +++++++++++++++++++ src/Modules/MapQueueModule/info.toml | 11 ++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/Modules/MapQueueModule/Localization.resx create mode 100644 src/Modules/MapQueueModule/MapQueueModule.cs create mode 100644 src/Modules/MapQueueModule/MapQueueModule.csproj create mode 100644 src/Modules/MapQueueModule/info.toml diff --git a/EvoSC.sln b/EvoSC.sln index e5e0c1fab..9069d518e 100644 --- a/EvoSC.sln +++ b/EvoSC.sln @@ -104,6 +104,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModuleManagerModule", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatchTrackerModule.Tests", "tests\Modules\MatchTrackerModule.Tests\MatchTrackerModule.Tests.csproj", "{9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapQueueModule", "src/Modules/MapQueueModule/MapQueueModule.csproj", "{28C14D85-DD8A-4933-A0A0-B655682B5D02}" +EndProject + @@ -303,6 +306,10 @@ Global {9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE}.Release|Any CPU.Build.0 = Release|Any CPU + {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -351,5 +358,6 @@ Global {42BCE01C-EBEF-4DB6-A61C-35B63C915AD3} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} {65F795D5-058D-4F8D-B8E0-BE3F64E14CDF} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} - EndGlobalSection + {28C14D85-DD8A-4933-A0A0-B655682B5D02} = {DC47658A-F421-4BA4-B617-090A7DFB3900} +EndGlobalSection EndGlobal diff --git a/src/Modules/MapQueueModule/Localization.resx b/src/Modules/MapQueueModule/Localization.resx new file mode 100644 index 000000000..10e3157c7 --- /dev/null +++ b/src/Modules/MapQueueModule/Localization.resx @@ -0,0 +1,19 @@ + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/src/Modules/MapQueueModule/MapQueueModule.cs b/src/Modules/MapQueueModule/MapQueueModule.cs new file mode 100644 index 000000000..4f2d244b1 --- /dev/null +++ b/src/Modules/MapQueueModule/MapQueueModule.cs @@ -0,0 +1,8 @@ +using EvoSC.Modules.Attributes; + +namespace EvoSC.Modules.Official.MapQueueModule; + +[Module(IsInternal = true)] +public class MapQueueModule : EvoScModule +{ +} diff --git a/src/Modules/MapQueueModule/MapQueueModule.csproj b/src/Modules/MapQueueModule/MapQueueModule.csproj new file mode 100644 index 000000000..40e7f8a11 --- /dev/null +++ b/src/Modules/MapQueueModule/MapQueueModule.csproj @@ -0,0 +1,25 @@ + + + net8.0 + enable + enable + EvoSC.Modules.Official.MapQueueModule + false + MapQueueModule + Map Queue + Map priority management. + 1.0.0 + Evo + + + + + + + + + + + + + diff --git a/src/Modules/MapQueueModule/info.toml b/src/Modules/MapQueueModule/info.toml new file mode 100644 index 000000000..d69f328bc --- /dev/null +++ b/src/Modules/MapQueueModule/info.toml @@ -0,0 +1,11 @@ +[info] +# A unique name for this module, this is used as a identifier +name = "MapQueueModule" +# The title of the module +title = "Map Queue" +# A short description of what the module is and does +summary = "Map priority management." +# The current version of this module, using SEMVER +version = "1.0.0" +# The name of the author that created this module +author = "Evo" From bb7d7773e69fab6aaa6fc623d8aa078e25a7d237 Mon Sep 17 00:00:00 2001 From: snixtho Date: Wed, 3 Jan 2024 11:18:52 +0100 Subject: [PATCH 02/76] implement async deque --- EvoSC.sln | 19 ++- src/EvoSC/EvoSC.csproj | 1 + src/EvoSC/InternalModules.cs | 4 +- .../Interfaces/IMapListService.cs | 6 + .../Interfaces/Models/IMapList.cs | 11 ++ src/Modules/MapListModule/Models/MapList.cs | 29 ++++ .../MapListModule/Services/MapListService.cs | 11 ++ .../Controllers/QueueController.cs | 19 +++ .../Interfaces/IMapQueueService.cs | 13 ++ .../Utils/AsyncDeque/IAsyncDeque.cs | 12 ++ .../Interfaces/Utils/AsyncDeque/IDequeNode.cs | 9 ++ .../Services/MapQueueService.cs | 90 ++++++++++++ .../Utils/AsyncDeque/AsyncDeque.cs | 138 ++++++++++++++++++ .../Utils/AsyncDeque/DequeNode.cs | 10 ++ .../Utils/AsyncDequeTests.cs | 41 ++++++ 15 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 src/Modules/MapListModule/Interfaces/IMapListService.cs create mode 100644 src/Modules/MapListModule/Interfaces/Models/IMapList.cs create mode 100644 src/Modules/MapListModule/Models/MapList.cs create mode 100644 src/Modules/MapListModule/Services/MapListService.cs create mode 100644 src/Modules/MapQueueModule/Controllers/QueueController.cs create mode 100644 src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs create mode 100644 src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs create mode 100644 src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IDequeNode.cs create mode 100644 src/Modules/MapQueueModule/Services/MapQueueService.cs create mode 100644 src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs create mode 100644 src/Modules/MapQueueModule/Utils/AsyncDeque/DequeNode.cs create mode 100644 tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs diff --git a/EvoSC.sln b/EvoSC.sln index 9069d518e..1a1b2d114 100644 --- a/EvoSC.sln +++ b/EvoSC.sln @@ -106,6 +106,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatchTrackerModule.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapQueueModule", "src/Modules/MapQueueModule/MapQueueModule.csproj", "{28C14D85-DD8A-4933-A0A0-B655682B5D02}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapListModule", "src/Modules/MapListModule/MapListModule.csproj", "{662F9AD7-8283-4DD8-BE19-37855AD15DAD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapQueueModuleTests", "tests\Modules\MapQueueModuleTests\MapQueueModuleTests.csproj", "{2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}" +EndProject + @@ -310,6 +315,14 @@ Global {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Debug|Any CPU.Build.0 = Debug|Any CPU {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Release|Any CPU.ActiveCfg = Release|Any CPU {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Release|Any CPU.Build.0 = Release|Any CPU + {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Release|Any CPU.Build.0 = Release|Any CPU + {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -358,6 +371,8 @@ Global {42BCE01C-EBEF-4DB6-A61C-35B63C915AD3} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} {65F795D5-058D-4F8D-B8E0-BE3F64E14CDF} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} - {28C14D85-DD8A-4933-A0A0-B655682B5D02} = {DC47658A-F421-4BA4-B617-090A7DFB3900} -EndGlobalSection + {28C14D85-DD8A-4933-A0A0-B655682B5D02} = {DC47658A-F421-4BA4-B617-090A7DFB3900} + {662F9AD7-8283-4DD8-BE19-37855AD15DAD} = {DC47658A-F421-4BA4-B617-090A7DFB3900} + {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} + EndGlobalSection EndGlobal diff --git a/src/EvoSC/EvoSC.csproj b/src/EvoSC/EvoSC.csproj index 0318cfab0..5f38610f3 100644 --- a/src/EvoSC/EvoSC.csproj +++ b/src/EvoSC/EvoSC.csproj @@ -26,6 +26,7 @@ + diff --git a/src/EvoSC/InternalModules.cs b/src/EvoSC/InternalModules.cs index ff3225015..42645db99 100644 --- a/src/EvoSC/InternalModules.cs +++ b/src/EvoSC/InternalModules.cs @@ -5,6 +5,7 @@ using EvoSC.Modules.Official.ExampleModule; using EvoSC.Modules.Official.FastestCpModule; using EvoSC.Modules.Official.LiveRankingModule; +using EvoSC.Modules.Official.MapQueueModule; using EvoSC.Modules.Official.MapsModule; using EvoSC.Modules.Official.MatchManagerModule; using EvoSC.Modules.Official.MatchRankingModule; @@ -45,7 +46,8 @@ public static class InternalModules typeof(LiveRankingModule), typeof(MatchRankingModule), typeof(ASayModule), - typeof(SpectatorTargetInfoModule) + typeof(SpectatorTargetInfoModule), + typeof(MapQueueModule) }; /// diff --git a/src/Modules/MapListModule/Interfaces/IMapListService.cs b/src/Modules/MapListModule/Interfaces/IMapListService.cs new file mode 100644 index 000000000..d82c29e62 --- /dev/null +++ b/src/Modules/MapListModule/Interfaces/IMapListService.cs @@ -0,0 +1,6 @@ +namespace EvoSC.Modules.Official.MapListModule.Interfaces; + +public interface IMapListService +{ + +} diff --git a/src/Modules/MapListModule/Interfaces/Models/IMapList.cs b/src/Modules/MapListModule/Interfaces/Models/IMapList.cs new file mode 100644 index 000000000..89dc36482 --- /dev/null +++ b/src/Modules/MapListModule/Interfaces/Models/IMapList.cs @@ -0,0 +1,11 @@ +using EvoSC.Common.Interfaces.Models; + +namespace EvoSC.Modules.Official.MapListModule.Interfaces.Models; + +public interface IMapList +{ + public IEnumerable Maps { get; } + + public void Add(IMap map); + public void Remove(IMap map); +} diff --git a/src/Modules/MapListModule/Models/MapList.cs b/src/Modules/MapListModule/Models/MapList.cs new file mode 100644 index 000000000..30f8e8984 --- /dev/null +++ b/src/Modules/MapListModule/Models/MapList.cs @@ -0,0 +1,29 @@ +using EvoSC.Common.Interfaces.Models; +using EvoSC.Modules.Official.MapListModule.Interfaces.Models; + +namespace EvoSC.Modules.Official.MapListModule.Models; + +public class MapList : IMapList +{ + private readonly Dictionary _maps = []; + + public IEnumerable Maps => _maps.Values; + + public void Add(IMap map) + { + if (!_maps.TryAdd(map.Uid, map)) + { + throw new InvalidOperationException($"Map with UID '{map.Uid}' already exists map list"); + } + } + + public void Remove(IMap map) + { + if (!_maps.ContainsKey(map.Uid)) + { + throw new InvalidOperationException($"Map with UID '{map.Uid}' does not exist in the map list"); + } + + _maps.Remove(map.Uid); + } +} diff --git a/src/Modules/MapListModule/Services/MapListService.cs b/src/Modules/MapListModule/Services/MapListService.cs new file mode 100644 index 000000000..7f7475fb7 --- /dev/null +++ b/src/Modules/MapListModule/Services/MapListService.cs @@ -0,0 +1,11 @@ +using EvoSC.Common.Services.Attributes; +using EvoSC.Common.Services.Models; +using EvoSC.Modules.Official.MapListModule.Interfaces; + +namespace EvoSC.Modules.Official.MapListModule.Services; + +[Service(LifeStyle = ServiceLifeStyle.Transient)] +public class MapListService : IMapListService +{ + +} diff --git a/src/Modules/MapQueueModule/Controllers/QueueController.cs b/src/Modules/MapQueueModule/Controllers/QueueController.cs new file mode 100644 index 000000000..5c4c27a20 --- /dev/null +++ b/src/Modules/MapQueueModule/Controllers/QueueController.cs @@ -0,0 +1,19 @@ +using EvoSC.Common.Controllers; +using EvoSC.Common.Controllers.Attributes; +using EvoSC.Common.Events.Attributes; +using EvoSC.Common.Interfaces; +using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Remote; +using GbxRemoteNet.Events; + +namespace EvoSC.Modules.Official.MapQueueModule.Controllers; + +[Controller] +public class QueueController(IServerClient server) : EvoScController +{ + [Subscribe(GbxRemoteEvent.BeginMap)] + public async Task OnEndMapAsync(object sender, MapGbxEventArgs args) + { + await server.Remote.ChooseNextMapAsync("Campaigns/CurrentQuarterly/Fall 2023 - 04.Map.Gbx"); + } +} diff --git a/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs b/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs new file mode 100644 index 000000000..0b276e779 --- /dev/null +++ b/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs @@ -0,0 +1,13 @@ +using EvoSC.Common.Interfaces.Models; + +namespace EvoSC.Modules.Official.MapQueueModule.Interfaces; + +public interface IMapQueueService +{ + public IReadOnlyCollection QueuedMaps { get; } + public Task EnqueueAsync(IMap map); + public Task DequeueNextAsync(); + public Task PeekNextAsync(); + public Task DropAsync(IMap map); + public Task ClearAsync(); +} diff --git a/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs b/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs new file mode 100644 index 000000000..6c7f3b8af --- /dev/null +++ b/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs @@ -0,0 +1,12 @@ +using System.Collections; + +namespace EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; + +public interface IAsyncDeque : ICollection +{ + public void Enqueue(TItem item); + public TItem Dequeue(); + public TItem PeekFirst(); + public void Drop(TItem item); + public void Clear(); +} diff --git a/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IDequeNode.cs b/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IDequeNode.cs new file mode 100644 index 000000000..8bbca22fa --- /dev/null +++ b/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IDequeNode.cs @@ -0,0 +1,9 @@ +namespace EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; + +public interface IDequeNode +{ + public IDequeNode? Next { get; set; } + public IDequeNode? Previous { get; set; } + + public TItem Item { get; } +} diff --git a/src/Modules/MapQueueModule/Services/MapQueueService.cs b/src/Modules/MapQueueModule/Services/MapQueueService.cs new file mode 100644 index 000000000..184a35c63 --- /dev/null +++ b/src/Modules/MapQueueModule/Services/MapQueueService.cs @@ -0,0 +1,90 @@ +using System.Collections.Concurrent; +using EvoSC.Common.Interfaces; +using EvoSC.Common.Interfaces.Models; +using EvoSC.Common.Interfaces.Services; +using EvoSC.Common.Services.Attributes; +using EvoSC.Common.Services.Models; +using EvoSC.Modules.Official.MapQueueModule.Interfaces; + +namespace EvoSC.Modules.Official.MapQueueModule.Services; + +[Service(LifeStyle = ServiceLifeStyle.Singleton)] +public class MapQueueService(IEventManager events, IServerClient server, IMapService maps) : IMapQueueService +{ + private Queue _mapQueue = new(); + private object _mapQueueLock = new(); + + public IReadOnlyCollection QueuedMaps + { + get + { + lock (_mapQueueLock) + { + return _mapQueue; + } + } + } + + public async Task EnqueueAsync(IMap map) + { + lock (_mapQueueLock) + { + _mapQueue.Enqueue(map); + } + } + + public async Task DequeueNextAsync() + { + if (QueuedMaps.Count == 0) + { + throw new InvalidOperationException("There are no more maps to dequeue"); + } + + IMap? nextMap; + + lock (_mapQueueLock) + { + nextMap = _mapQueue.Dequeue(); + } + + if (nextMap == null) + { + throw new InvalidOperationException("Failed to get next map from the queue"); + } + + return nextMap; + } + + public Task PeekNextAsync() + { + if (QueuedMaps.Count == 0) + { + throw new InvalidOperationException("There are no maps in the queue"); + } + + lock (_mapQueueLock) + { + return Task.FromResult(_mapQueue.Peek()); + } + } + + public Task DropAsync(IMap map) + { + lock (_mapQueueLock) + { + + } + + return Task.CompletedTask; + } + + public Task ClearAsync() + { + lock (_mapQueueLock) + { + _mapQueue.Clear(); + } + + return Task.CompletedTask; + } +} diff --git a/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs b/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs new file mode 100644 index 000000000..6e9141704 --- /dev/null +++ b/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs @@ -0,0 +1,138 @@ +using System.Collections; +using EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; + +namespace EvoSC.Modules.Official.MapQueueModule.Utils.AsyncDeque; + +public class AsyncDeque : IAsyncDeque +{ + private IDequeNode? _last; + private IDequeNode? _first; + private readonly object _lock = new(); + private int _count = 0; + + public IEnumerator GetEnumerator() + { + var node = _first; + + while (node != null) + { + yield return node.Item; + node = node.Next; + } + } + + public void CopyTo(Array array, int index) + { + throw new NotSupportedException(); + } + + public int Count + { + get + { + lock (SyncRoot) + { + return _count; + } + } + } + + public bool IsSynchronized => true; + public object SyncRoot => _lock; + + + public void Enqueue(TItem item) + { + lock (SyncRoot) + { + if (_last == null) + { + _last = new DequeNode { Next = null, Previous = null, Item = item }; + _first = _last; + } + else + { + var newItem = new DequeNode { Next = null, Previous = _last, Item = item }; + _last.Next = newItem; + _last = newItem; + } + } + } + + public TItem Dequeue() + { + lock (SyncRoot) + { + if (_first == null) + { + throw new InvalidOperationException("There are no items in the queue"); + } + + var item = _first.Item; + + if (_first == _last) + { + _first = null; + _last = null; + } + else + { + _first = _first.Next; + } + + return item; + } + } + + public TItem PeekFirst() + { + lock (SyncRoot) + { + if (_first == null) + { + throw new InvalidOperationException("There are no items in the queue"); + } + + return _first.Item; + } + } + + public void Drop(TItem item) + { + lock (SyncRoot) + { + var node = _first; + + while (node != null) + { + if (node.Item != null && node.Item.Equals(item)) + { + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + + return; + } + + node = node.Next; + } + + throw new InvalidOperationException("Item does not exist in the queue"); + } + } + + public void Clear() + { + lock (SyncRoot) + { + _first = null; + _last = null; + } + } +} diff --git a/src/Modules/MapQueueModule/Utils/AsyncDeque/DequeNode.cs b/src/Modules/MapQueueModule/Utils/AsyncDeque/DequeNode.cs new file mode 100644 index 000000000..0d8f4e93b --- /dev/null +++ b/src/Modules/MapQueueModule/Utils/AsyncDeque/DequeNode.cs @@ -0,0 +1,10 @@ +using EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; + +namespace EvoSC.Modules.Official.MapQueueModule.Utils.AsyncDeque; + +public class DequeNode : IDequeNode +{ + public IDequeNode? Next { get; set; } + public IDequeNode? Previous { get; set; } + public TItem Item { get; init; } +} diff --git a/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs b/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs new file mode 100644 index 000000000..d378b057b --- /dev/null +++ b/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs @@ -0,0 +1,41 @@ +using EvoSC.Modules.Official.MapQueueModule.Utils.AsyncDeque; + +namespace MapQueueModuleTests.Utils; + +public class AsyncDequeTests +{ + class MyItemType + { + public string MyProp { get; set; } + } + + [Fact] + public void Item_Is_Added_And_Dequeued() + { + var queue = new AsyncDeque(); + + queue.Enqueue(new MyItemType { MyProp = "Hello" }); + + var item = queue.Dequeue(); + + Assert.Equal("Hello", item.MyProp); + } + + [Fact] + public void Multiple_Items_Are_Added_And_Dequeued_In_FIFO() + { + var queue = new AsyncDeque(); + + queue.Enqueue(new MyItemType { MyProp = "Item 1" }); + queue.Enqueue(new MyItemType { MyProp = "Item 2" }); + queue.Enqueue(new MyItemType { MyProp = "Item 3" }); + + var first = queue.Dequeue(); + var second = queue.Dequeue(); + var third = queue.Dequeue(); + + Assert.Equal("Item 1", first.MyProp); + Assert.Equal("Item 2", second.MyProp); + Assert.Equal("Item 3", third.MyProp); + } +} From a41f9f677510784edd71578ca18344bb7573297d Mon Sep 17 00:00:00 2001 From: snixtho Date: Wed, 3 Jan 2024 13:18:03 +0100 Subject: [PATCH 03/76] implement queue controller --- .../Controllers/QueueController.cs | 44 +++++- .../Events/Args/MapQueueEventArgs.cs | 8 + .../Args/MapQueueMapDroppedEventArgs.cs | 6 + .../MapQueueModule/Events/MapQueueEvents.cs | 9 ++ .../Interfaces/IMapQueueService.cs | 1 + .../Utils/AsyncDeque/IAsyncDeque.cs | 2 +- .../Services/MapQueueService.cs | 79 +++------- .../Utils/AsyncDeque/AsyncDeque.cs | 26 +++- .../Utils/AsyncDequeTests.cs | 138 +++++++++++++++++- 9 files changed, 246 insertions(+), 67 deletions(-) create mode 100644 src/Modules/MapQueueModule/Events/Args/MapQueueEventArgs.cs create mode 100644 src/Modules/MapQueueModule/Events/Args/MapQueueMapDroppedEventArgs.cs create mode 100644 src/Modules/MapQueueModule/Events/MapQueueEvents.cs diff --git a/src/Modules/MapQueueModule/Controllers/QueueController.cs b/src/Modules/MapQueueModule/Controllers/QueueController.cs index 5c4c27a20..62eba7aa8 100644 --- a/src/Modules/MapQueueModule/Controllers/QueueController.cs +++ b/src/Modules/MapQueueModule/Controllers/QueueController.cs @@ -3,17 +3,55 @@ using EvoSC.Common.Events.Attributes; using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Remote; +using EvoSC.Modules.Official.MapQueueModule.Events; +using EvoSC.Modules.Official.MapQueueModule.Events.Args; +using EvoSC.Modules.Official.MapQueueModule.Interfaces; using GbxRemoteNet.Events; namespace EvoSC.Modules.Official.MapQueueModule.Controllers; [Controller] -public class QueueController(IServerClient server) : EvoScController +public class QueueController(IMapQueueService mapQueue, IServerClient server, IMapService maps) : EvoScController { [Subscribe(GbxRemoteEvent.BeginMap)] - public async Task OnEndMapAsync(object sender, MapGbxEventArgs args) + public async Task OnBeginMapAsync(object sender, MapGbxEventArgs args) { - await server.Remote.ChooseNextMapAsync("Campaigns/CurrentQuarterly/Fall 2023 - 04.Map.Gbx"); + var currentMap = await maps.GetCurrentMapAsync(); + + try + { + await mapQueue.DropAsync(currentMap); + } + catch (Exception ex) + { + // if map isn't in the queue, we just ignore it + } + + if (mapQueue.QueuedMapsCount > 0) + { + var next = await mapQueue.PeekNextAsync(); + await server.Remote.ChooseNextMapAsync(next.FilePath); + } + } + + [Subscribe(MapQueueEvents.MapQueued)] + public async Task OnMapQueuedAsync(object sender, MapQueueEventArgs args) + { + if (mapQueue.QueuedMapsCount == 1) + { + await server.Remote.ChooseNextMapAsync(args.QueuedMap.FilePath); + } + } + + [Subscribe(MapQueueEvents.MapDropped)] + public async Task OnMapDroppedAsync(object sender, MapQueueMapDroppedEventArgs args) + { + if (args.WasNext) + { + var next = await mapQueue.PeekNextAsync(); + await server.Remote.ChooseNextMapAsync(next.FilePath); + } } } diff --git a/src/Modules/MapQueueModule/Events/Args/MapQueueEventArgs.cs b/src/Modules/MapQueueModule/Events/Args/MapQueueEventArgs.cs new file mode 100644 index 000000000..2f5830f80 --- /dev/null +++ b/src/Modules/MapQueueModule/Events/Args/MapQueueEventArgs.cs @@ -0,0 +1,8 @@ +using EvoSC.Common.Interfaces.Models; + +namespace EvoSC.Modules.Official.MapQueueModule.Events.Args; + +public class MapQueueEventArgs : EventArgs +{ + public IMap QueuedMap { get; init; } +} diff --git a/src/Modules/MapQueueModule/Events/Args/MapQueueMapDroppedEventArgs.cs b/src/Modules/MapQueueModule/Events/Args/MapQueueMapDroppedEventArgs.cs new file mode 100644 index 000000000..b36f5cdc4 --- /dev/null +++ b/src/Modules/MapQueueModule/Events/Args/MapQueueMapDroppedEventArgs.cs @@ -0,0 +1,6 @@ +namespace EvoSC.Modules.Official.MapQueueModule.Events.Args; + +public class MapQueueMapDroppedEventArgs : MapQueueEventArgs +{ + public bool WasNext { get; init; } +} diff --git a/src/Modules/MapQueueModule/Events/MapQueueEvents.cs b/src/Modules/MapQueueModule/Events/MapQueueEvents.cs new file mode 100644 index 000000000..196b9a9cd --- /dev/null +++ b/src/Modules/MapQueueModule/Events/MapQueueEvents.cs @@ -0,0 +1,9 @@ +namespace EvoSC.Modules.Official.MapQueueModule.Events; + +public enum MapQueueEvents +{ + MapQueued, + MapDequeued, + MapDropped, + QueueCleared +} diff --git a/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs b/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs index 0b276e779..8f7ae7baa 100644 --- a/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs +++ b/src/Modules/MapQueueModule/Interfaces/IMapQueueService.cs @@ -5,6 +5,7 @@ namespace EvoSC.Modules.Official.MapQueueModule.Interfaces; public interface IMapQueueService { public IReadOnlyCollection QueuedMaps { get; } + public int QueuedMapsCount { get; } public Task EnqueueAsync(IMap map); public Task DequeueNextAsync(); public Task PeekNextAsync(); diff --git a/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs b/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs index 6c7f3b8af..2866288e1 100644 --- a/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs +++ b/src/Modules/MapQueueModule/Interfaces/Utils/AsyncDeque/IAsyncDeque.cs @@ -2,7 +2,7 @@ namespace EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; -public interface IAsyncDeque : ICollection +public interface IAsyncDeque : ICollection, IEnumerable { public void Enqueue(TItem item); public TItem Dequeue(); diff --git a/src/Modules/MapQueueModule/Services/MapQueueService.cs b/src/Modules/MapQueueModule/Services/MapQueueService.cs index 184a35c63..8ce938587 100644 --- a/src/Modules/MapQueueModule/Services/MapQueueService.cs +++ b/src/Modules/MapQueueModule/Services/MapQueueService.cs @@ -1,90 +1,57 @@ -using System.Collections.Concurrent; -using EvoSC.Common.Interfaces; +using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Models; using EvoSC.Common.Interfaces.Services; using EvoSC.Common.Services.Attributes; using EvoSC.Common.Services.Models; +using EvoSC.Modules.Official.MapQueueModule.Events; +using EvoSC.Modules.Official.MapQueueModule.Events.Args; using EvoSC.Modules.Official.MapQueueModule.Interfaces; +using EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; +using EvoSC.Modules.Official.MapQueueModule.Utils.AsyncDeque; namespace EvoSC.Modules.Official.MapQueueModule.Services; [Service(LifeStyle = ServiceLifeStyle.Singleton)] public class MapQueueService(IEventManager events, IServerClient server, IMapService maps) : IMapQueueService { - private Queue _mapQueue = new(); - private object _mapQueueLock = new(); + private readonly IAsyncDeque _mapQueue = new AsyncDeque(); - public IReadOnlyCollection QueuedMaps - { - get - { - lock (_mapQueueLock) - { - return _mapQueue; - } - } - } + public IReadOnlyCollection QueuedMaps => _mapQueue.ToArray(); + public int QueuedMapsCount => _mapQueue.Count; - public async Task EnqueueAsync(IMap map) + public Task EnqueueAsync(IMap map) { - lock (_mapQueueLock) - { - _mapQueue.Enqueue(map); - } + _mapQueue.Enqueue(map); + return events.RaiseAsync(MapQueueEvents.MapQueued, new MapQueueEventArgs { QueuedMap = map }); } public async Task DequeueNextAsync() { - if (QueuedMaps.Count == 0) - { - throw new InvalidOperationException("There are no more maps to dequeue"); - } - - IMap? nextMap; - - lock (_mapQueueLock) - { - nextMap = _mapQueue.Dequeue(); - } - - if (nextMap == null) - { - throw new InvalidOperationException("Failed to get next map from the queue"); - } + var map = _mapQueue.Dequeue(); + await events.RaiseAsync(MapQueueEvents.MapDequeued, new MapQueueEventArgs { QueuedMap = map }); - return nextMap; + return map; } public Task PeekNextAsync() { - if (QueuedMaps.Count == 0) - { - throw new InvalidOperationException("There are no maps in the queue"); - } - - lock (_mapQueueLock) - { - return Task.FromResult(_mapQueue.Peek()); - } + return Task.FromResult(_mapQueue.PeekFirst()); } public Task DropAsync(IMap map) { - lock (_mapQueueLock) + var isNext = _mapQueue.PeekFirst() == map; + _mapQueue.Drop(map); + return events.RaiseAsync(MapQueueEvents.MapDropped, new MapQueueMapDroppedEventArgs { - - } - - return Task.CompletedTask; + QueuedMap = map, + WasNext = isNext + }); } public Task ClearAsync() { - lock (_mapQueueLock) - { - _mapQueue.Clear(); - } - - return Task.CompletedTask; + _mapQueue.Clear(); + return events.RaiseAsync(MapQueueEvents.QueueCleared, EventArgs.Empty); } } diff --git a/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs b/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs index 6e9141704..19734dc8d 100644 --- a/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs +++ b/src/Modules/MapQueueModule/Utils/AsyncDeque/AsyncDeque.cs @@ -9,8 +9,8 @@ public class AsyncDeque : IAsyncDeque private IDequeNode? _first; private readonly object _lock = new(); private int _count = 0; - - public IEnumerator GetEnumerator() + + IEnumerator IEnumerable.GetEnumerator() { var node = _first; @@ -21,6 +21,11 @@ public IEnumerator GetEnumerator() } } + public IEnumerator GetEnumerator() + { + return GetEnumerator(); + } + public void CopyTo(Array array, int index) { throw new NotSupportedException(); @@ -56,6 +61,8 @@ public void Enqueue(TItem item) _last.Next = newItem; _last = newItem; } + + _count++; } } @@ -80,6 +87,7 @@ public TItem Dequeue() _first = _first.Next; } + _count--; return item; } } @@ -107,16 +115,21 @@ public void Drop(TItem item) { if (node.Item != null && node.Item.Equals(item)) { - if (node.Previous != null) + if (node == _first) { - node.Previous.Next = node.Next; + _first = _first.Next; } - - if (node.Next != null) + else if (node == _last) { + _last = _last.Previous; + } + else + { + node.Previous.Next = node.Next; node.Next.Previous = node.Previous; } + _count--; return; } @@ -133,6 +146,7 @@ public void Clear() { _first = null; _last = null; + _count = 0; } } } diff --git a/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs b/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs index d378b057b..31f6d9480 100644 --- a/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs +++ b/tests/Modules/MapQueueModuleTests/Utils/AsyncDequeTests.cs @@ -1,4 +1,6 @@ -using EvoSC.Modules.Official.MapQueueModule.Utils.AsyncDeque; +using System.Runtime.CompilerServices; +using EvoSC.Modules.Official.MapQueueModule.Interfaces.Utils.AsyncDeque; +using EvoSC.Modules.Official.MapQueueModule.Utils.AsyncDeque; namespace MapQueueModuleTests.Utils; @@ -38,4 +40,138 @@ public void Multiple_Items_Are_Added_And_Dequeued_In_FIFO() Assert.Equal("Item 2", second.MyProp); Assert.Equal("Item 3", third.MyProp); } + + [Fact] + public void Count_Is_Updated_When_Enqueuing() + { + var queue = new AsyncDeque(); + + queue.Enqueue(new MyItemType { MyProp = "Item 1" }); + queue.Enqueue(new MyItemType { MyProp = "Item 2" }); + queue.Enqueue(new MyItemType { MyProp = "Item 3" }); + + Assert.Equal(3, queue.Count); + } + + [Fact] + public void Count_Is_Updated_When_Dequeuing() + { + var queue = new AsyncDeque(); + + queue.Enqueue(new MyItemType { MyProp = "Item 1" }); + queue.Enqueue(new MyItemType { MyProp = "Item 2" }); + queue.Enqueue(new MyItemType { MyProp = "Item 3" }); + + queue.Dequeue(); + queue.Dequeue(); + + Assert.Single(queue); + Assert.Equal(1, queue.Count); + } + + [Fact] + public void Clear_Removes_Everything_And_Resets_Count() + { + var queue = new AsyncDeque(); + + queue.Enqueue(new MyItemType { MyProp = "Item 1" }); + queue.Enqueue(new MyItemType { MyProp = "Item 2" }); + queue.Enqueue(new MyItemType { MyProp = "Item 3" }); + + queue.Clear(); + + Assert.Empty(queue); + Assert.Equal(0, queue.Count); + } + + [Fact] + public void Empty_Queue_Throws_Exception_On_Dequeue() + { + var queue = new AsyncDeque(); + + Assert.Throws(() => queue.Dequeue()); + } + + [Fact] + public void PeekFirst_Returns_First_In_Queue() + { + var queue = new AsyncDeque(); + + queue.Enqueue(new MyItemType { MyProp = "Item 1" }); + queue.Enqueue(new MyItemType { MyProp = "Item 2" }); + queue.Enqueue(new MyItemType { MyProp = "Item 3" }); + + var first = queue.PeekFirst(); + + Assert.Equal("Item 1", first.MyProp); + } + + [Fact] + public void PeekFirst_Throws_If_Empty() + { + var queue = new AsyncDeque(); + + Assert.Throws(() => queue.PeekFirst()); + } + + [Theory] + [InlineData(new []{0}, 2, "Item 2")] + [InlineData(new []{1}, 2, "Item 1")] + [InlineData(new []{2}, 2, "Item 1")] + [InlineData(new []{0, 2}, 1, "Item 2")] + [InlineData(new []{0, 1}, 1, "Item 3")] + [InlineData(new []{2, 1}, 1, "Item 1")] + [InlineData(new []{1, 0}, 1, "Item 3")] + [InlineData(new []{2, 0}, 1, "Item 2")] + public void Dropping_Updates_Accordingly(int[] remove, int expectedCount, string expectedFirst) + { + var queue = Create3ItemQueue(); + + foreach (var toRemove in remove) + { + queue.Queue.Drop((MyItemType)((ITuple)queue)[toRemove]); + } + + var first = queue.Queue.PeekFirst(); + + Assert.Equal(expectedCount, queue.Queue.Count); + Assert.Equal(expectedFirst, first.MyProp); + } + + [Fact] + public void Dropping_Non_Existent_Item_Throws_Exception() + { + var queue = new AsyncDeque(); + + var item = new MyItemType(); + + Assert.Throws(() => queue.Drop(item)); + } + + [Fact] + public void CopyTo_Is_Not_Supported() + { + var queue = new AsyncDeque(); + Assert.Throws(() => queue.CopyTo(Array.Empty(), 0)); + } + + private ( + MyItemType Item1, + MyItemType Item2, + MyItemType Item3, + IAsyncDeque Queue + ) Create3ItemQueue() + { + var queue = new AsyncDeque(); + + var item1 = new MyItemType { MyProp = "Item 1" }; + var item2 = new MyItemType { MyProp = "Item 2" }; + var item3 = new MyItemType { MyProp = "Item 3" }; + + queue.Enqueue(item1); + queue.Enqueue(item2); + queue.Enqueue(item3); + + return (item1, item2, item3, queue); + } } From c25dbcf1337091bda5f0a56ddc7b5b555b54c72c Mon Sep 17 00:00:00 2001 From: snixtho Date: Fri, 5 Jan 2024 14:07:48 +0100 Subject: [PATCH 04/76] Add container and panel ML components. --- EvoSC.sln | 15 ++++--- .../Util/TextFormatting/TextFormatter.cs | 11 +++++ .../Templates/Controls/Container.mt | 37 +++++++++++++++ .../Templates/Controls/Panel.mt | 37 +++++++++++++++ .../Templates/Drawing/QuarterCircle.mt | 45 +++++++++++++++++++ .../Templates/Scripts/Container.ms | 20 +++++++++ .../Util/GlobalManialinkUtils.cs | 5 +++ src/EvoSC/EvoSC.csproj | 1 + src/EvoSC/InternalModules.cs | 4 +- .../ExampleModule/ExampleController2.cs | 2 +- .../ExampleModule/Templates/MyManialink.mt | 38 +++++++--------- .../Controllers/MapListCommandController.cs | 16 +++++++ .../Interfaces/IMapListService.cs | 6 --- .../Interfaces/Models/IMapList.cs | 11 ----- src/Modules/MapListModule/MapListModule.cs | 6 +++ src/Modules/MapListModule/Models/MapList.cs | 29 ------------ .../MapListModule/Services/MapListService.cs | 11 ----- .../MapListModule/Templates/MapList.mt | 14 ++++++ src/Modules/MapListModule/info.toml | 11 +++++ .../Controllers/QueueCommandsController.cs | 45 +++++++++++++++++++ .../Controllers/QueueController.cs | 6 ++- 21 files changed, 280 insertions(+), 90 deletions(-) create mode 100644 src/EvoSC.Manialinks/Templates/Controls/Container.mt create mode 100644 src/EvoSC.Manialinks/Templates/Controls/Panel.mt create mode 100644 src/EvoSC.Manialinks/Templates/Drawing/QuarterCircle.mt create mode 100644 src/EvoSC.Manialinks/Templates/Scripts/Container.ms create mode 100644 src/Modules/MapListModule/Controllers/MapListCommandController.cs delete mode 100644 src/Modules/MapListModule/Interfaces/IMapListService.cs delete mode 100644 src/Modules/MapListModule/Interfaces/Models/IMapList.cs create mode 100644 src/Modules/MapListModule/MapListModule.cs delete mode 100644 src/Modules/MapListModule/Models/MapList.cs delete mode 100644 src/Modules/MapListModule/Services/MapListService.cs create mode 100644 src/Modules/MapListModule/Templates/MapList.mt create mode 100644 src/Modules/MapListModule/info.toml create mode 100644 src/Modules/MapQueueModule/Controllers/QueueCommandsController.cs diff --git a/EvoSC.sln b/EvoSC.sln index 1a1b2d114..bc9bbd56c 100644 --- a/EvoSC.sln +++ b/EvoSC.sln @@ -106,10 +106,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatchTrackerModule.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapQueueModule", "src/Modules/MapQueueModule/MapQueueModule.csproj", "{28C14D85-DD8A-4933-A0A0-B655682B5D02}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapListModule", "src/Modules/MapListModule/MapListModule.csproj", "{662F9AD7-8283-4DD8-BE19-37855AD15DAD}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapQueueModuleTests", "tests\Modules\MapQueueModuleTests\MapQueueModuleTests.csproj", "{2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapListModule", "src\Modules\MapListModule\MapListModule.csproj", "{AB316816-F301-4693-86E8-C31E78F53A59}" +EndProject + @@ -315,14 +316,14 @@ Global {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Debug|Any CPU.Build.0 = Debug|Any CPU {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Release|Any CPU.ActiveCfg = Release|Any CPU {28C14D85-DD8A-4933-A0A0-B655682B5D02}.Release|Any CPU.Build.0 = Release|Any CPU - {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {662F9AD7-8283-4DD8-BE19-37855AD15DAD}.Release|Any CPU.Build.0 = Release|Any CPU {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762}.Release|Any CPU.Build.0 = Release|Any CPU + {AB316816-F301-4693-86E8-C31E78F53A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB316816-F301-4693-86E8-C31E78F53A59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB316816-F301-4693-86E8-C31E78F53A59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB316816-F301-4693-86E8-C31E78F53A59}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -372,7 +373,7 @@ Global {65F795D5-058D-4F8D-B8E0-BE3F64E14CDF} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {9EF4D340-0C49-4A15-9BCF-6CD9508AA7DE} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} {28C14D85-DD8A-4933-A0A0-B655682B5D02} = {DC47658A-F421-4BA4-B617-090A7DFB3900} - {662F9AD7-8283-4DD8-BE19-37855AD15DAD} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} + {AB316816-F301-4693-86E8-C31E78F53A59} = {DC47658A-F421-4BA4-B617-090A7DFB3900} EndGlobalSection EndGlobal diff --git a/src/EvoSC.Common/Util/TextFormatting/TextFormatter.cs b/src/EvoSC.Common/Util/TextFormatting/TextFormatter.cs index f439fb082..d384e3fe4 100644 --- a/src/EvoSC.Common/Util/TextFormatting/TextFormatter.cs +++ b/src/EvoSC.Common/Util/TextFormatting/TextFormatter.cs @@ -120,6 +120,17 @@ public TextFormatter AddText(Action textAction) return this; } + /// + /// Add a formatted text object. + /// + /// Formatted text to add. + /// + public TextFormatter AddText(FormattedText formattedText) + { + _textParts.Add(formattedText); + return this; + } + /// /// Convert the complete text into the raw formatted representation. /// diff --git a/src/EvoSC.Manialinks/Templates/Controls/Container.mt b/src/EvoSC.Manialinks/Templates/Controls/Container.mt new file mode 100644 index 000000000..175a7dd9b --- /dev/null +++ b/src/EvoSC.Manialinks/Templates/Controls/Container.mt @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/EvoSC.Manialinks/Templates/Controls/Panel.mt b/src/EvoSC.Manialinks/Templates/Controls/Panel.mt index 7209dc7d4..bdfd0246d 100644 --- a/src/EvoSC.Manialinks/Templates/Controls/Panel.mt +++ b/src/EvoSC.Manialinks/Templates/Controls/Panel.mt @@ -37,7 +37,6 @@ - + diff --git a/src/EvoSC.Manialinks/Templates/Controls/Rating.mt b/src/EvoSC.Manialinks/Templates/Controls/Rating.mt new file mode 100644 index 000000000..5d1643cf8 --- /dev/null +++ b/src/EvoSC.Manialinks/Templates/Controls/Rating.mt @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/EvoSC.Manialinks/Templates/Controls/Tag.mt b/src/EvoSC.Manialinks/Templates/Controls/Tag.mt index 9cd4bfe93..eaa3eb00e 100644 --- a/src/EvoSC.Manialinks/Templates/Controls/Tag.mt +++ b/src/EvoSC.Manialinks/Templates/Controls/Tag.mt @@ -12,6 +12,7 @@ + diff --git a/src/EvoSC.Manialinks/Templates/Controls/ScrollBar.mt b/src/EvoSC.Manialinks/Templates/Controls/ScrollBar.mt new file mode 100644 index 000000000..efef12b7f --- /dev/null +++ b/src/EvoSC.Manialinks/Templates/Controls/ScrollBar.mt @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/EvoSC.Manialinks/Templates/Controls/Tag.mt b/src/EvoSC.Manialinks/Templates/Controls/Tag.mt index eaa3eb00e..2ce6ca36e 100644 --- a/src/EvoSC.Manialinks/Templates/Controls/Tag.mt +++ b/src/EvoSC.Manialinks/Templates/Controls/Tag.mt @@ -6,6 +6,7 @@ + @@ -20,7 +21,7 @@ y="{{ y }}" id="{{ id }}" width="{{ width }}" - height="2" + height="{{ height }}" cornerRadius='{{ style == "Round" ? 1 : 0 }}' bgColor="{{ bgColor == null ? Util.TypeToColorBg(severity) : bgColor }}" data="{{ closable }}" @@ -28,11 +29,11 @@ >