From 7691ecb281f74ff984af34ba7f40738336bc44d1 Mon Sep 17 00:00:00 2001 From: snixtho Date: Wed, 5 Jun 2024 11:10:35 +0200 Subject: [PATCH 01/18] begin local records --- EvoSC.sln | 10 +- .../Database/Models/DbLocalRecord.cs | 34 +++++ .../Repository/LocalRecordRepository.cs | 117 ++++++++++++++++++ .../Interfaces/ILocalRecord.cs | 12 ++ .../Interfaces/ILocalRecordRepository.cs | 14 +++ .../LocalRecordsModule/LocalRecordsModule.cs | 8 ++ .../LocalRecordsModule.csproj | 26 ++++ .../LocalRecordsModule/Localization.resx | 19 +++ .../202406050955_AddLocalRecordsTable.cs | 16 +++ .../Templates/LocalRecordsWidget.mt | 5 + src/Modules/LocalRecordsModule/info.toml | 11 ++ .../Database/Models/DbPlayerRecord.cs | 58 +++++++++ .../Interfaces/Models/IPlayerRecord.cs | 5 +- 13 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs create mode 100644 src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs create mode 100644 src/Modules/LocalRecordsModule/Interfaces/ILocalRecord.cs create mode 100644 src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs create mode 100644 src/Modules/LocalRecordsModule/LocalRecordsModule.cs create mode 100644 src/Modules/LocalRecordsModule/LocalRecordsModule.csproj create mode 100644 src/Modules/LocalRecordsModule/Localization.resx create mode 100644 src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs create mode 100644 src/Modules/LocalRecordsModule/Templates/LocalRecordsWidget.mt create mode 100644 src/Modules/LocalRecordsModule/info.toml diff --git a/EvoSC.sln b/EvoSC.sln index 6d42c6c99..c64ed88ef 100644 --- a/EvoSC.sln +++ b/EvoSC.sln @@ -112,6 +112,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapListModule", "src\Module EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapListModule.Tests", "tests\Modules\MapListModule.Tests\MapListModule.Tests.csproj", "{098D1F9B-054D-4158-BB6C-AC908C4595F6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalRecordsModule", "src/Modules/LocalRecordsModule/LocalRecordsModule.csproj", "{1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449}" +EndProject + @@ -330,6 +333,10 @@ Global {098D1F9B-054D-4158-BB6C-AC908C4595F6}.Debug|Any CPU.Build.0 = Debug|Any CPU {098D1F9B-054D-4158-BB6C-AC908C4595F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {098D1F9B-054D-4158-BB6C-AC908C4595F6}.Release|Any CPU.Build.0 = Release|Any CPU + {1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -382,5 +389,6 @@ Global {2D2D3AC6-C8BD-4615-BCC7-9A64007DB762} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} {AB316816-F301-4693-86E8-C31E78F53A59} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {098D1F9B-054D-4158-BB6C-AC908C4595F6} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} - EndGlobalSection + {1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449} = {DC47658A-F421-4BA4-B617-090A7DFB3900} +EndGlobalSection EndGlobal diff --git a/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs b/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs new file mode 100644 index 000000000..b5946b412 --- /dev/null +++ b/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs @@ -0,0 +1,34 @@ +using EvoSC.Common.Database.Models.Maps; +using EvoSC.Common.Database.Models.Player; +using EvoSC.Common.Interfaces.Models; +using EvoSC.Modules.Official.LocalRecordsModule.Interfaces; +using EvoSC.Modules.Official.PlayerRecords.Database.Models; +using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; +using LinqToDB.Mapping; + +namespace EvoSC.Modules.Official.LocalRecordsModule.Database.Models; + +public class DbLocalRecord : ILocalRecord +{ + [PrimaryKey, Identity] + public long Id { get; set; } + + [Column] + public long MapId { get; set; } + + [Column] + public long RecordId { get; set; } + + [Column] + public int Position { get; set; } + + public IMap Map => DbMap; + + public IPlayerRecord Record => DbRecord; + + [Association(ThisKey = nameof(MapId), OtherKey = nameof(DbMap.Id))] + public DbMap DbMap { get; set; } + + [Association(ThisKey = nameof(RecordId), OtherKey = nameof(DbPlayerRecord.Id))] + public DbPlayerRecord DbRecord { get; set; } +} diff --git a/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs b/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs new file mode 100644 index 000000000..154bb54d3 --- /dev/null +++ b/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs @@ -0,0 +1,117 @@ +using EvoSC.Common.Database.Models.Maps; +using EvoSC.Common.Database.Models.Player; +using EvoSC.Common.Database.Repository; +using EvoSC.Common.Interfaces.Database; +using EvoSC.Common.Interfaces.Models; +using EvoSC.Modules.Official.LocalRecordsModule.Database.Models; +using EvoSC.Modules.Official.LocalRecordsModule.Interfaces; +using EvoSC.Modules.Official.PlayerRecords.Database.Models; +using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; +using LinqToDB; + +namespace EvoSC.Modules.Official.LocalRecordsModule.Database.Repository; + +public class LocalRecordRepository : DbRepository, ILocalRecordRepository +{ + protected LocalRecordRepository(IDbConnectionFactory dbConnFactory) : base(dbConnFactory) + { + } + + public async Task> GetLocalRecordsOfMapByIdAsync(long mapId) => + await NewLoadAll() + .Where(r => r.Map.Id == mapId) + .ToArrayAsync(); + + public async Task AddOrUpdateRecordAsync(IMap map, IPlayerRecord record) + { + // check if old record is better or equal, and dont bother updating if so + var oldRecord = await GetRecordOfPlayerInMapAsync(record.Player, map); + + if (oldRecord != null && oldRecord.Record.CompareTo(record) <= 0) + { + return oldRecord; + } + + var localRecord = new DbLocalRecord + { + MapId = map.Id, + RecordId = record.Id, + Position = 0, + DbMap = new DbMap(map), + DbRecord = new DbPlayerRecord(record) + }; + + var transaction = await Database.BeginTransactionAsync(); + + try + { + // remove old record and add the new one + if (oldRecord != null) + { + await Database.DeleteAsync(oldRecord); + } + + var id = await Database.InsertWithIdentityAsync(localRecord); + localRecord.Id = Convert.ToInt64(id); + } + catch (Exception ex) + { + await transaction.RollbackTransactionAsync(); + throw; + } + + await RecalculatePositionsOfMapAsync(map); + return localRecord; + } + + public async Task RecalculatePositionsOfMapAsync(IMap map) + { + var locals = await GetLocalRecordsOfMapByIdAsync(map.Id); + var sorted = locals.OrderBy(r => r.Record.Score).ToArray(); + var updated = new List(); + + for (var i = 0; i < sorted.Length; i++) + { + var newPos = i + 1; + + // don't update records that dont need it + if (sorted[i].Position == newPos) + { + continue; + } + + sorted[i].Position = newPos; + updated.Add(sorted[i]); + } + + var transaction = await Database.BeginTransactionAsync(); + + try + { + foreach (var record in updated) + { + await Database.UpdateAsync(record); + } + + await transaction.CommitTransactionAsync(); + } + catch (Exception ex) + { + await transaction.RollbackTransactionAsync(); + } + } + + public async Task> GetRecordsByPlayerAsync(IPlayer player) => + await NewLoadAll() + .Where(r => r.Record.Player.Id == player.Id) + .ToArrayAsync(); + + public async Task GetRecordOfPlayerInMapAsync(IPlayer player, IMap map) => + await NewLoadAll() + .FirstOrDefaultAsync(r => r.Record.Player.Id == player.Id); + + private ILoadWithQueryable NewLoadAll() => Table() + .LoadWith(r => r.Map) + .LoadWith(r => r.Record) + .ThenLoad(r => r.Player); +} diff --git a/src/Modules/LocalRecordsModule/Interfaces/ILocalRecord.cs b/src/Modules/LocalRecordsModule/Interfaces/ILocalRecord.cs new file mode 100644 index 000000000..1742a30af --- /dev/null +++ b/src/Modules/LocalRecordsModule/Interfaces/ILocalRecord.cs @@ -0,0 +1,12 @@ +using EvoSC.Common.Interfaces.Models; +using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; + +namespace EvoSC.Modules.Official.LocalRecordsModule.Interfaces; + +public interface ILocalRecord +{ + public long Id { get; } + public int Position { get; } + public IMap Map { get; } + public IPlayerRecord Record { get; } +} diff --git a/src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs b/src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs new file mode 100644 index 000000000..74d32dbe2 --- /dev/null +++ b/src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs @@ -0,0 +1,14 @@ +using EvoSC.Common.Interfaces.Models; +using EvoSC.Modules.Official.LocalRecordsModule.Database.Models; +using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; + +namespace EvoSC.Modules.Official.LocalRecordsModule.Interfaces; + +public interface ILocalRecordRepository +{ + public Task> GetLocalRecordsOfMapByIdAsync(long mapId); + public Task AddOrUpdateRecordAsync(IMap map, IPlayerRecord record); + public Task RecalculatePositionsOfMapAsync(IMap map); + public Task> GetRecordsByPlayerAsync(IPlayer player); + public Task GetRecordOfPlayerInMapAsync(IPlayer player, IMap map); +} diff --git a/src/Modules/LocalRecordsModule/LocalRecordsModule.cs b/src/Modules/LocalRecordsModule/LocalRecordsModule.cs new file mode 100644 index 000000000..af409a695 --- /dev/null +++ b/src/Modules/LocalRecordsModule/LocalRecordsModule.cs @@ -0,0 +1,8 @@ +using EvoSC.Modules.Attributes; + +namespace EvoSC.Modules.Official.LocalRecordsModule; + +[Module(IsInternal = true)] +public class LocalRecordsModule : EvoScModule +{ +} diff --git a/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj b/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj new file mode 100644 index 000000000..d9b3074db --- /dev/null +++ b/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj @@ -0,0 +1,26 @@ + + + net8.0 + enable + enable + EvoSC.Modules.Official.LocalRecordsModule + false + LocalRecordsModule + Local Records + Determine and display top records of a map from locally saved records. + 1.0.0 + Evo + + + + + + + + + + + + + + diff --git a/src/Modules/LocalRecordsModule/Localization.resx b/src/Modules/LocalRecordsModule/Localization.resx new file mode 100644 index 000000000..10e3157c7 --- /dev/null +++ b/src/Modules/LocalRecordsModule/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/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs b/src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs new file mode 100644 index 000000000..f22f5e511 --- /dev/null +++ b/src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs @@ -0,0 +1,16 @@ +using FluentMigrator; + +namespace EvoSC.Modules.Official.LocalRecordsModule; + +[Tags("Production")] +[Migration(1717574116)] +public class AddLocalRecordsTable : Migration +{ + public override void Up() + { + } + + public override void Down() + { + } +} diff --git a/src/Modules/LocalRecordsModule/Templates/LocalRecordsWidget.mt b/src/Modules/LocalRecordsModule/Templates/LocalRecordsWidget.mt new file mode 100644 index 000000000..2ba8d97ee --- /dev/null +++ b/src/Modules/LocalRecordsModule/Templates/LocalRecordsWidget.mt @@ -0,0 +1,5 @@ + + + diff --git a/src/Modules/LocalRecordsModule/info.toml b/src/Modules/LocalRecordsModule/info.toml new file mode 100644 index 000000000..8c4d95a58 --- /dev/null +++ b/src/Modules/LocalRecordsModule/info.toml @@ -0,0 +1,11 @@ +[info] +# A unique name for this module, this is used as a identifier +name = "LocalRecordsModule" +# The title of the module +title = "Local Records" +# A short description of what the module is and does +summary = "Determine and display top records of a map from locally saved records." +# The current version of this module, using SEMVER +version = "1.0.0" +# The name of the author that created this module +author = "Evo" diff --git a/src/Modules/PlayerRecords/Database/Models/DbPlayerRecord.cs b/src/Modules/PlayerRecords/Database/Models/DbPlayerRecord.cs index 7751b9051..c895bf8de 100644 --- a/src/Modules/PlayerRecords/Database/Models/DbPlayerRecord.cs +++ b/src/Modules/PlayerRecords/Database/Models/DbPlayerRecord.cs @@ -42,4 +42,62 @@ public class DbPlayerRecord : IPlayerRecord public DbMap DbMap; public IMap? Map => DbMap; + + public DbPlayerRecord(){} + + public DbPlayerRecord(IPlayerRecord? record) + { + if (record == null) + { + return; + } + + Id = record.Id; + PlayerId = record.Player.Id; + MapId = record.Map.Id; + Score = record.Score; + RecordType = record.RecordType; + Checkpoints = record.Checkpoints; + CreatedAt = record.CreatedAt; + UpdatedAt = record.UpdatedAt; + DbPlayer = new DbPlayer(record.Player); + DbMap = new DbMap(record.Map); + } + + public int CompareTo(IPlayerRecord? other) + { + if (other == null) + { + // better than "nothing" + return 1; + } + + if (RecordType != other.RecordType) + { + throw new InvalidOperationException("Cannot compare records of different types"); + } + + return RecordType switch + { + PlayerRecordType.Points => ComparePoints(), + PlayerRecordType.Time => CompareTime(), + _ => CompareTime() + }; + + // Compare time record type, lower is better + int CompareTime() => Score switch + { + _ when Score < other.Score => 1, + _ when Score > other.Score => 1, + _ => 0 + }; + + // compare points record type, higher is better + int ComparePoints() => Score switch + { + _ when Score > other.Score => 1, + _ when Score < other.Score => 1, + _ => 0 + }; + } } diff --git a/src/Modules/PlayerRecords/Interfaces/Models/IPlayerRecord.cs b/src/Modules/PlayerRecords/Interfaces/Models/IPlayerRecord.cs index 90654397d..d1da8180f 100644 --- a/src/Modules/PlayerRecords/Interfaces/Models/IPlayerRecord.cs +++ b/src/Modules/PlayerRecords/Interfaces/Models/IPlayerRecord.cs @@ -2,11 +2,14 @@ namespace EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; -public interface IPlayerRecord +public interface IPlayerRecord : IComparable { + public long Id { get; } public IPlayer Player { get; } public IMap Map { get; } public int Score { get; } public PlayerRecordType RecordType { get; } public string Checkpoints { get; } + public DateTime CreatedAt { get; } + public DateTime UpdatedAt { get; } } From a02f4869daa7052128c8a84584256e6273cd89f0 Mon Sep 17 00:00:00 2001 From: snixtho Date: Wed, 5 Jun 2024 12:56:59 +0200 Subject: [PATCH 02/18] Add dynamic data changes to persistent manialinks. --- .../Interfaces/IManialinkManager.cs | 22 ++++++ .../Interfaces/Models/IPersistentManialink.cs | 9 +++ .../Models/PersistentManialinkType.cs | 7 ++ src/EvoSC.Manialinks/ManialinkManager.cs | 73 +++++++++++++++++-- .../Models/PersistentManialink.cs | 11 +++ src/EvoSC/EvoSC.csproj | 1 + src/EvoSC/InternalModules.cs | 4 +- .../Controllers/ShowWidgetController.cs | 18 +++++ .../Database/Models/DbLocalRecord.cs | 3 + .../Repository/LocalRecordRepository.cs | 45 +++++++++++- .../{ => Database}/ILocalRecordRepository.cs | 4 +- .../Services/ILocalRecordsService.cs | 7 ++ .../LocalRecordsModule/LocalRecordsModule.cs | 14 +++- .../LocalRecordsModule.csproj | 3 + .../202406050955_AddLocalRecordsTable.cs | 7 ++ src/Modules/LocalRecordsModule/info.toml | 3 + .../Repository/PlayerRecordsRepository.cs | 5 ++ .../Interfaces/IPlayerRecordsRepository.cs | 4 + 18 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 src/EvoSC.Manialinks/Interfaces/Models/IPersistentManialink.cs create mode 100644 src/EvoSC.Manialinks/Interfaces/Models/PersistentManialinkType.cs create mode 100644 src/EvoSC.Manialinks/Models/PersistentManialink.cs create mode 100644 src/Modules/LocalRecordsModule/Controllers/ShowWidgetController.cs rename src/Modules/LocalRecordsModule/Interfaces/{ => Database}/ILocalRecordRepository.cs (75%) create mode 100644 src/Modules/LocalRecordsModule/Interfaces/Services/ILocalRecordsService.cs diff --git a/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs b/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs index fc6a33586..39b91fb77 100644 --- a/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs +++ b/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs @@ -102,6 +102,28 @@ public interface IManialinkManager /// public Task SendPersistentManialinkAsync(string name); + /// + /// Send a manialink to all players which persists even if a player re-connects. + /// It will also automatically show for new players. + /// + /// This method allows for dynamic data updates by a callback method. + /// + /// Name of the template to show. + /// Method that returns data to be sent. + /// + public Task SendPersistentManialinkAsync(string name, Func> setupData); + + /// + /// Send a manialink to all players which persists even if a player re-connects. + /// It will also automatically show for new players. + /// + /// This method allows for dynamic data updates by a callback method. + /// + /// + /// Method that returns data to be sent. + /// + public Task SendPersistentManialinkAsync(string name, Func>> setupData); + /// /// Render a template and send it to a specific player. /// diff --git a/src/EvoSC.Manialinks/Interfaces/Models/IPersistentManialink.cs b/src/EvoSC.Manialinks/Interfaces/Models/IPersistentManialink.cs new file mode 100644 index 000000000..41291e4d8 --- /dev/null +++ b/src/EvoSC.Manialinks/Interfaces/Models/IPersistentManialink.cs @@ -0,0 +1,9 @@ +namespace EvoSC.Manialinks.Interfaces.Models; + +public interface IPersistentManialink +{ + public string Name { get; } + public PersistentManialinkType Type { get; } + public string CompiledOutput { get; } + public Func>>? DynamicDataCallbackAsync { get; } +} diff --git a/src/EvoSC.Manialinks/Interfaces/Models/PersistentManialinkType.cs b/src/EvoSC.Manialinks/Interfaces/Models/PersistentManialinkType.cs new file mode 100644 index 000000000..fa6e3deb6 --- /dev/null +++ b/src/EvoSC.Manialinks/Interfaces/Models/PersistentManialinkType.cs @@ -0,0 +1,7 @@ +namespace EvoSC.Manialinks.Interfaces.Models; + +public enum PersistentManialinkType +{ + Static, + Dynamic +} diff --git a/src/EvoSC.Manialinks/ManialinkManager.cs b/src/EvoSC.Manialinks/ManialinkManager.cs index ccb6c6d97..e84da3e90 100644 --- a/src/EvoSC.Manialinks/ManialinkManager.cs +++ b/src/EvoSC.Manialinks/ManialinkManager.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using System.Diagnostics; +using System.Dynamic; using System.Reflection; using System.Text; using EvoSC.Common.Events; @@ -33,7 +35,7 @@ public class ManialinkManager : IManialinkManager private readonly ManiaTemplateEngine _engine = new(); private readonly Dictionary _templates = new(); private readonly Dictionary _scripts = new(); - private readonly ConcurrentDictionary _persistentManialinks = new(); + private readonly ConcurrentDictionary _persistentManialinks = new(); private static IEnumerable s_defaultAssemblies = new[] { @@ -207,7 +209,12 @@ public async Task SendPersistentManialinkAsync(string name, IDictionary SendPersistentManialinkAsync(name, new { }); + public Task SendPersistentManialinkAsync(string name, Func> setupData) => + SendPersistentManialinkAsync(name, async () => + { + var data = await setupData(); + return (IDictionary)data; + }); + + public async Task SendPersistentManialinkAsync(string name, Func>> setupData) + { + name = GetEffectiveName(name); + + _persistentManialinks[name] = new PersistentManialink + { + Name = name, + Type = PersistentManialinkType.Static, + DynamicDataCallbackAsync = setupData + }; + + var data = await setupData(); + var manialinkOutput = await PrepareAndRenderAsync(name, data); + await _server.Remote.SendDisplayManialinkPageAsync(manialinkOutput, 0, false); + } + public async Task SendManialinkAsync(IPlayer player, string name, IDictionary data) { name = GetEffectiveName(name); @@ -330,9 +365,37 @@ private async Task HandlePlayerConnectAsync(object sender, PlayerConnectGbxEvent { try { - foreach (var (_, output) in _persistentManialinks) + foreach (var (_, manialink) in _persistentManialinks) { - await _server.Remote.SendDisplayManialinkPageToLoginAsync(e.Login, output, 0, false); + string? output = null; + + switch (manialink.Type) + { + case PersistentManialinkType.Static: + output = manialink.CompiledOutput; + break; + case PersistentManialinkType.Dynamic: + { + var data = await manialink.DynamicDataCallbackAsync?.Invoke(); + output = await PrepareAndRenderAsync(manialink.Name, data); + } + break; + } + + if (output == null) + { + _logger.LogWarning("Failed to get output of persistent ({Type}) manialink: {Name}", + manialink.Type switch + { + PersistentManialinkType.Dynamic => "Dynamic", + PersistentManialinkType.Static => "Static", + _ => "Unknown", + }, + manialink.Name); + continue; + } + + await _server.Remote.SendDisplayManialinkPageToLoginAsync(e.Login, manialink.CompiledOutput, 0, false); } } catch (Exception ex) diff --git a/src/EvoSC.Manialinks/Models/PersistentManialink.cs b/src/EvoSC.Manialinks/Models/PersistentManialink.cs new file mode 100644 index 000000000..8f11b2b91 --- /dev/null +++ b/src/EvoSC.Manialinks/Models/PersistentManialink.cs @@ -0,0 +1,11 @@ +using EvoSC.Manialinks.Interfaces.Models; + +namespace EvoSC.Manialinks.Models; + +public class PersistentManialink : IPersistentManialink +{ + public required string Name { get; init; } + public required PersistentManialinkType Type { get; init; } + public string CompiledOutput { get; init; } + public Func>>? DynamicDataCallbackAsync { get; init; } +} diff --git a/src/EvoSC/EvoSC.csproj b/src/EvoSC/EvoSC.csproj index 08d603228..25c2b72fc 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 abf3e7627..500e002c8 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.LocalRecordsModule; using EvoSC.Modules.Official.MapListModule; using EvoSC.Modules.Official.MapQueueModule; using EvoSC.Modules.Official.MapsModule; @@ -49,7 +50,8 @@ public static class InternalModules typeof(ASayModule), typeof(SpectatorTargetInfoModule), typeof(MapQueueModule), - typeof(MapListModule) + typeof(MapListModule), + typeof(LocalRecordsModule) }; /// diff --git a/src/Modules/LocalRecordsModule/Controllers/ShowWidgetController.cs b/src/Modules/LocalRecordsModule/Controllers/ShowWidgetController.cs new file mode 100644 index 000000000..d57ace0d5 --- /dev/null +++ b/src/Modules/LocalRecordsModule/Controllers/ShowWidgetController.cs @@ -0,0 +1,18 @@ +using EvoSC.Common.Controllers; +using EvoSC.Common.Controllers.Attributes; +using EvoSC.Common.Events.Attributes; +using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Remote; +using GbxRemoteNet.Events; + +namespace EvoSC.Modules.Official.LocalRecordsModule.Controllers; + +[Controller] +public class ShowWidgetController : EvoScController +{ + [Subscribe(GbxRemoteEvent.PlayerConnect)] + public async Task OnPlayerConnectAsync(object sender, PlayerConnectGbxEventArgs e) + { + + } +} diff --git a/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs b/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs index b5946b412..d61761537 100644 --- a/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs +++ b/src/Modules/LocalRecordsModule/Database/Models/DbLocalRecord.cs @@ -8,8 +8,11 @@ namespace EvoSC.Modules.Official.LocalRecordsModule.Database.Models; +[Table(TableName)] public class DbLocalRecord : ILocalRecord { + public const string TableName = "LocalRecords"; + [PrimaryKey, Identity] public long Id { get; set; } diff --git a/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs b/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs index 154bb54d3..97df17304 100644 --- a/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs +++ b/src/Modules/LocalRecordsModule/Database/Repository/LocalRecordRepository.cs @@ -3,19 +3,22 @@ using EvoSC.Common.Database.Repository; using EvoSC.Common.Interfaces.Database; using EvoSC.Common.Interfaces.Models; +using EvoSC.Common.Services.Attributes; using EvoSC.Modules.Official.LocalRecordsModule.Database.Models; using EvoSC.Modules.Official.LocalRecordsModule.Interfaces; +using EvoSC.Modules.Official.LocalRecordsModule.Interfaces.Database; using EvoSC.Modules.Official.PlayerRecords.Database.Models; +using EvoSC.Modules.Official.PlayerRecords.Interfaces; using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; using LinqToDB; namespace EvoSC.Modules.Official.LocalRecordsModule.Database.Repository; -public class LocalRecordRepository : DbRepository, ILocalRecordRepository +[Service] +public class LocalRecordRepository(IDbConnectionFactory dbConnFactory, IPlayerRecordsRepository recordsRepository) + : DbRepository(dbConnFactory), ILocalRecordRepository { - protected LocalRecordRepository(IDbConnectionFactory dbConnFactory) : base(dbConnFactory) - { - } + private readonly IPlayerRecordsRepository _recordsRepository = recordsRepository; public async Task> GetLocalRecordsOfMapByIdAsync(long mapId) => await NewLoadAll() @@ -53,6 +56,8 @@ public async Task AddOrUpdateRecordAsync(IMap map, IPlayerRecord var id = await Database.InsertWithIdentityAsync(localRecord); localRecord.Id = Convert.ToInt64(id); + + await transaction.CommitTransactionAsync(); } catch (Exception ex) { @@ -110,6 +115,38 @@ await NewLoadAll() await NewLoadAll() .FirstOrDefaultAsync(r => r.Record.Player.Id == player.Id); + public async Task DeleteRecordAsync(IPlayer player, IMap map) + { + var transaction = await Database.BeginTransactionAsync(); + + try + { + await Table().DeleteAsync(r => r.MapId == map.Id && r.Record.Player.Id == player.Id); + await recordsRepository.DeleteRecordAsync(player, map); + } + catch (Exception ex) + { + await transaction.RollbackTransactionAsync(); + throw; + } + } + + public async Task DeleteRecordAsync(ILocalRecord localRecord) + { + var transaction = await Database.BeginTransactionAsync(); + + try + { + await Database.DeleteAsync(localRecord); + await recordsRepository.DeleteRecordAsync(localRecord.Record); + } + catch (Exception ex) + { + await transaction.RollbackTransactionAsync(); + throw; + } + } + private ILoadWithQueryable NewLoadAll() => Table() .LoadWith(r => r.Map) .LoadWith(r => r.Record) diff --git a/src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs b/src/Modules/LocalRecordsModule/Interfaces/Database/ILocalRecordRepository.cs similarity index 75% rename from src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs rename to src/Modules/LocalRecordsModule/Interfaces/Database/ILocalRecordRepository.cs index 74d32dbe2..0227882f4 100644 --- a/src/Modules/LocalRecordsModule/Interfaces/ILocalRecordRepository.cs +++ b/src/Modules/LocalRecordsModule/Interfaces/Database/ILocalRecordRepository.cs @@ -2,7 +2,7 @@ using EvoSC.Modules.Official.LocalRecordsModule.Database.Models; using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; -namespace EvoSC.Modules.Official.LocalRecordsModule.Interfaces; +namespace EvoSC.Modules.Official.LocalRecordsModule.Interfaces.Database; public interface ILocalRecordRepository { @@ -11,4 +11,6 @@ public interface ILocalRecordRepository public Task RecalculatePositionsOfMapAsync(IMap map); public Task> GetRecordsByPlayerAsync(IPlayer player); public Task GetRecordOfPlayerInMapAsync(IPlayer player, IMap map); + public Task DeleteRecordAsync(IPlayer player, IMap map); + public Task DeleteRecordAsync(ILocalRecord localRecord); } diff --git a/src/Modules/LocalRecordsModule/Interfaces/Services/ILocalRecordsService.cs b/src/Modules/LocalRecordsModule/Interfaces/Services/ILocalRecordsService.cs new file mode 100644 index 000000000..02183fb76 --- /dev/null +++ b/src/Modules/LocalRecordsModule/Interfaces/Services/ILocalRecordsService.cs @@ -0,0 +1,7 @@ +namespace EvoSC.Modules.Official.LocalRecordsModule.Interfaces.Services; + +public interface ILocalRecordsService +{ + public Task ShowWidgetAsync(); + public Task> GetLocalsOfCurrentMapAsync(); +} diff --git a/src/Modules/LocalRecordsModule/LocalRecordsModule.cs b/src/Modules/LocalRecordsModule/LocalRecordsModule.cs index af409a695..2377145c1 100644 --- a/src/Modules/LocalRecordsModule/LocalRecordsModule.cs +++ b/src/Modules/LocalRecordsModule/LocalRecordsModule.cs @@ -1,8 +1,20 @@ +using EvoSC.Manialinks.Interfaces; using EvoSC.Modules.Attributes; +using EvoSC.Modules.Interfaces; +using EvoSC.Modules.Official.LocalRecordsModule.Interfaces.Services; namespace EvoSC.Modules.Official.LocalRecordsModule; [Module(IsInternal = true)] -public class LocalRecordsModule : EvoScModule +public class LocalRecordsModule(IManialinkManager manialinkManager, ILocalRecordsService localRecordsService) : EvoScModule, IToggleable { + public Task EnableAsync() => + manialinkManager.SendPersistentManialinkAsync("LocalRecordsModule.LocalRecordsWidget", + async () => + { + var records = localRecordsService.GetLocalsOfCurrentMapAsync(); + return new { records }; + }); + + public Task DisableAsync() => manialinkManager.HideManialinkAsync("LocalRecordsModule.LocalRecordsWidget"); } diff --git a/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj b/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj index d9b3074db..8d279fb72 100644 --- a/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj +++ b/src/Modules/LocalRecordsModule/LocalRecordsModule.csproj @@ -22,5 +22,8 @@ + + + diff --git a/src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs b/src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs index f22f5e511..b40bb8a9d 100644 --- a/src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs +++ b/src/Modules/LocalRecordsModule/Migrations/202406050955_AddLocalRecordsTable.cs @@ -1,3 +1,4 @@ +using EvoSC.Modules.Official.LocalRecordsModule.Database.Models; using FluentMigrator; namespace EvoSC.Modules.Official.LocalRecordsModule; @@ -8,9 +9,15 @@ public class AddLocalRecordsTable : Migration { public override void Up() { + Create.Table(DbLocalRecord.TableName) + .WithColumn(nameof(DbLocalRecord.Id)).AsInt64().PrimaryKey().Identity() + .WithColumn(nameof(DbLocalRecord.MapId)).AsInt64().Indexed() + .WithColumn(nameof(DbLocalRecord.RecordId)).AsInt64().Indexed() + .WithColumn(nameof(DbLocalRecord.Position)).AsInt32(); } public override void Down() { + Delete.Table(DbLocalRecord.TableName); } } diff --git a/src/Modules/LocalRecordsModule/info.toml b/src/Modules/LocalRecordsModule/info.toml index 8c4d95a58..fc5c8d2f7 100644 --- a/src/Modules/LocalRecordsModule/info.toml +++ b/src/Modules/LocalRecordsModule/info.toml @@ -9,3 +9,6 @@ summary = "Determine and display top records of a map from locally saved records version = "1.0.0" # The name of the author that created this module author = "Evo" + +[dependencies] +RecordsModule = "1.0.0" diff --git a/src/Modules/PlayerRecords/Database/Repository/PlayerRecordsRepository.cs b/src/Modules/PlayerRecords/Database/Repository/PlayerRecordsRepository.cs index e97979ab5..192487279 100644 --- a/src/Modules/PlayerRecords/Database/Repository/PlayerRecordsRepository.cs +++ b/src/Modules/PlayerRecords/Database/Repository/PlayerRecordsRepository.cs @@ -52,4 +52,9 @@ public async Task InsertRecordAsync(IPlayer player, IMap map, in return record; } + + public Task DeleteRecordAsync(IPlayer player, IMap map) => Table() + .DeleteAsync(r => r.MapId == map.Id && r.PlayerId == player.Id); + + public Task DeleteRecordAsync(IPlayerRecord record) => Database.DeleteAsync(record); } diff --git a/src/Modules/PlayerRecords/Interfaces/IPlayerRecordsRepository.cs b/src/Modules/PlayerRecords/Interfaces/IPlayerRecordsRepository.cs index e5a2e68a4..2c4051512 100644 --- a/src/Modules/PlayerRecords/Interfaces/IPlayerRecordsRepository.cs +++ b/src/Modules/PlayerRecords/Interfaces/IPlayerRecordsRepository.cs @@ -1,5 +1,6 @@ using EvoSC.Common.Interfaces.Models; using EvoSC.Modules.Official.PlayerRecords.Database.Models; +using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models; namespace EvoSC.Modules.Official.PlayerRecords.Interfaces; @@ -26,4 +27,7 @@ public interface IPlayerRecordsRepository /// The record to add. /// public Task InsertRecordAsync(IPlayer player, IMap map, int score, IEnumerable checkpoints); + + public Task DeleteRecordAsync(IPlayer player, IMap map); + public Task DeleteRecordAsync(IPlayerRecord record); } From f8b48e3ed861498ff7a94b2b88a13ae9fdd97f7e Mon Sep 17 00:00:00 2001 From: snixtho Date: Wed, 5 Jun 2024 14:08:40 +0200 Subject: [PATCH 03/18] fix persistent manialinks --- src/EvoSC.Manialinks/ManialinkManager.cs | 17 +++++++++++--- .../Templates/Containers/Widget.mt | 6 ++--- .../Controllers/ShowWidgetController.cs | 5 ---- .../Services/ILocalRecordsService.cs | 1 - .../LocalRecordsModule.csproj | 3 --- .../Services/LocalRecordsService.cs | 23 +++++++++++++++++++ .../Templates/LocalRecordsWidget.mt | 7 +++++- 7 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 src/Modules/LocalRecordsModule/Services/LocalRecordsService.cs diff --git a/src/EvoSC.Manialinks/ManialinkManager.cs b/src/EvoSC.Manialinks/ManialinkManager.cs index e84da3e90..a2d5e7f50 100644 --- a/src/EvoSC.Manialinks/ManialinkManager.cs +++ b/src/EvoSC.Manialinks/ManialinkManager.cs @@ -1,8 +1,10 @@ using System.Collections.Concurrent; +using System.ComponentModel; using System.Diagnostics; using System.Dynamic; using System.Reflection; using System.Text; +using System.Text.Json.Serialization; using EvoSC.Common.Events; using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Models; @@ -23,6 +25,8 @@ using ManiaTemplates; using ManiaTemplates.Lib; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace EvoSC.Manialinks; @@ -233,10 +237,17 @@ public async Task SendPersistentManialinkAsync(string name, dynamic data) public Task SendPersistentManialinkAsync(string name) => SendPersistentManialinkAsync(name, new { }); public Task SendPersistentManialinkAsync(string name, Func> setupData) => - SendPersistentManialinkAsync(name, async () => + SendPersistentManialinkAsync(name, async Task> () => { - var data = await setupData(); - return (IDictionary)data; + var rawData = await setupData(); + IDictionary data = new ExpandoObject(); + + foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(rawData.GetType())) + { + data.Add(prop.Name, prop.GetValue(rawData)); + } + + return data; }); public async Task SendPersistentManialinkAsync(string name, Func>> setupData) diff --git a/src/EvoSC.Manialinks/Templates/Containers/Widget.mt b/src/EvoSC.Manialinks/Templates/Containers/Widget.mt index 0eb24922a..34a257334 100644 --- a/src/EvoSC.Manialinks/Templates/Containers/Widget.mt +++ b/src/EvoSC.Manialinks/Templates/Containers/Widget.mt @@ -2,15 +2,15 @@ - - + +