diff --git a/EvoSC.sln b/EvoSC.sln index 6d42c6c99..e1ff0f73c 100644 --- a/EvoSC.sln +++ b/EvoSC.sln @@ -112,6 +112,11 @@ 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalRecordsModule.Tests", "tests\Modules\LocalRecordsModule.Tests\LocalRecordsModule.Tests.csproj", "{7401429B-B842-4316-B7A2-B77E9AD966CB}" +EndProject + @@ -330,6 +335,14 @@ 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 + {7401429B-B842-4316-B7A2-B77E9AD966CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7401429B-B842-4316-B7A2-B77E9AD966CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7401429B-B842-4316-B7A2-B77E9AD966CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7401429B-B842-4316-B7A2-B77E9AD966CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -382,5 +395,7 @@ 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} + {1D8DBFC8-EC21-4DDA-9D88-DE7CA04C8449} = {DC47658A-F421-4BA4-B617-090A7DFB3900} + {7401429B-B842-4316-B7A2-B77E9AD966CB} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} EndGlobalSection EndGlobal diff --git a/src/EvoSC.Common/Database/Repository/Maps/MapRepository.cs b/src/EvoSC.Common/Database/Repository/Maps/MapRepository.cs index 6fabd5801..a1eea86f5 100644 --- a/src/EvoSC.Common/Database/Repository/Maps/MapRepository.cs +++ b/src/EvoSC.Common/Database/Repository/Maps/MapRepository.cs @@ -19,6 +19,11 @@ public class MapRepository(IDbConnectionFactory dbConnFactory, ILogger t.DbDetails) .SingleOrDefaultAsync(m => m.Id == id); + public async Task GetMapsAsync() => await Table() + .LoadWith(m => m.DbAuthor) + .LoadWith(m => m.DbDetails) + .ToArrayAsync(); + public async Task GetMapByUidAsync(string uid) => await Table() .LoadWith(t => t.DbAuthor) .LoadWith(t => t.DbDetails) diff --git a/src/EvoSC.Common/Interfaces/Database/Repository/IMapRepository.cs b/src/EvoSC.Common/Interfaces/Database/Repository/IMapRepository.cs index baca8ad32..84769c3a2 100644 --- a/src/EvoSC.Common/Interfaces/Database/Repository/IMapRepository.cs +++ b/src/EvoSC.Common/Interfaces/Database/Repository/IMapRepository.cs @@ -14,6 +14,12 @@ public interface IMapRepository /// The maps database ID. /// a map if it exists in the database. public Task GetMapByIdAsync(long id); + + /// + /// Get all maps in the database. + /// + /// + public Task GetMapsAsync(); /// /// Gets a map from the database based on the provided UID. diff --git a/src/EvoSC.Common/Interfaces/Services/IMapService.cs b/src/EvoSC.Common/Interfaces/Services/IMapService.cs index f058d16d3..72929f840 100644 --- a/src/EvoSC.Common/Interfaces/Services/IMapService.cs +++ b/src/EvoSC.Common/Interfaces/Services/IMapService.cs @@ -86,4 +86,10 @@ public interface IMapService /// Map to get details of. /// public Task FetchMapDetailsAsync(IMap map); + + /// + /// Get all maps in the database. + /// + /// + public Task GetCurrentMapListAsync(); } diff --git a/src/EvoSC.Common/Services/MapService.cs b/src/EvoSC.Common/Services/MapService.cs index 22165b840..05cfad607 100644 --- a/src/EvoSC.Common/Services/MapService.cs +++ b/src/EvoSC.Common/Services/MapService.cs @@ -198,6 +198,8 @@ public async Task FetchMapDetailsAsync(IMap map) } } + public Task GetCurrentMapListAsync() => mapRepository.GetMapsAsync(); + private static bool MapVersionExistsInDb(IMap map, MapMetadata mapMetadata) { return map.ExternalVersion == mapMetadata.ExternalVersion; diff --git a/src/EvoSC.Common/Util/FormattingUtils.cs b/src/EvoSC.Common/Util/FormattingUtils.cs index 095abd297..475c4113f 100644 --- a/src/EvoSC.Common/Util/FormattingUtils.cs +++ b/src/EvoSC.Common/Util/FormattingUtils.cs @@ -49,9 +49,9 @@ public static TextFormatter FormatPlayerChatMessage(string nickname, string mess { var formattedMessage = new TextFormatter() .AddText("[") - .AddText(text => text.AsIsolated().AddText(nickname)) + .AddText(text => text.AddText(nickname)) .AddText("] ") - .AddText(text => text.AsIsolated().AddText(message)); + .AddText(text => text.AddText(message)); return formattedMessage; } diff --git a/src/EvoSC.Common/Util/TextFormatting/FormattedText.cs b/src/EvoSC.Common/Util/TextFormatting/FormattedText.cs index cf1e16423..f9d1c3720 100644 --- a/src/EvoSC.Common/Util/TextFormatting/FormattedText.cs +++ b/src/EvoSC.Common/Util/TextFormatting/FormattedText.cs @@ -9,7 +9,7 @@ namespace EvoSC.Common.Util.TextFormatting; public class FormattedText { public TextStyling? Style { get; set; } - public bool IsIsolated { get; set; } + public bool IsIsolated { get; set; } = true; public StringBuilder Text { get; set; } = new StringBuilder(); public FormattedText(){} @@ -127,9 +127,9 @@ public FormattedText WithStyle(Action styleAction) /// interfere with later text. /// /// - public FormattedText AsIsolated() + public FormattedText AsNotIsolated() { - IsIsolated = true; + IsIsolated = false; return this; } diff --git a/src/EvoSC.Common/Util/TextFormatting/TextColor.cs b/src/EvoSC.Common/Util/TextFormatting/TextColor.cs index a5c5973d0..97e69a5f7 100644 --- a/src/EvoSC.Common/Util/TextFormatting/TextColor.cs +++ b/src/EvoSC.Common/Util/TextFormatting/TextColor.cs @@ -29,9 +29,26 @@ public TextColor(Color color) public TextColor(string hex) { - this._r = byte.Parse(hex[0].ToString(), System.Globalization.NumberStyles.HexNumber); - this._g = byte.Parse(hex[1].ToString(), System.Globalization.NumberStyles.HexNumber); - this._b = byte.Parse(hex[2].ToString(), System.Globalization.NumberStyles.HexNumber); + if (hex.Length == 3) + { + this._r = byte.Parse(hex[0].ToString(), System.Globalization.NumberStyles.HexNumber); + this._g = byte.Parse(hex[1].ToString(), System.Globalization.NumberStyles.HexNumber); + this._b = byte.Parse(hex[2].ToString(), System.Globalization.NumberStyles.HexNumber); + } + else if (hex.Length >= 6) + { + var r = Math.Floor(byte.Parse(hex[0..2], NumberStyles.HexNumber) / 255.0 * 0xF); + var g = Math.Floor(byte.Parse(hex[2..4], NumberStyles.HexNumber) / 255.0 * 0xF); + var b = Math.Floor(byte.Parse(hex[4..6], NumberStyles.HexNumber) / 255.0 * 0xF); + + this._r = (byte)r; + this._g = (byte)g; + this._b = (byte)b; + } + else + { + throw new FormatException("Invalid color code"); + } } /// diff --git a/src/EvoSC.Common/Util/TextFormatting/TextStyling.cs b/src/EvoSC.Common/Util/TextFormatting/TextStyling.cs index 76330bd10..14f135415 100644 --- a/src/EvoSC.Common/Util/TextFormatting/TextStyling.cs +++ b/src/EvoSC.Common/Util/TextFormatting/TextStyling.cs @@ -31,6 +31,17 @@ public TextStyling WithColor(TextColor color) return this; } + /// + /// Set the color of this style. + /// + /// The color to use. + /// + public TextStyling WithColor(string color) + { + _color = new TextColor(color); + return this; + } + /// /// Set the color of this style. /// diff --git a/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs b/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs index fc6a33586..2ef28abf6 100644 --- a/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs +++ b/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs @@ -1,12 +1,11 @@ -using EvoSC.Common.Interfaces.Models; -using EvoSC.Manialinks.Interfaces.Models; +using EvoSC.Manialinks.Interfaces.Models; namespace EvoSC.Manialinks.Interfaces; /// /// Manialink template & ManiaScript registry, and can render, display and hide Manialink to players. /// -public interface IManialinkManager +public interface IManialinkManager : IManialinkOperations { /// /// Add all the default templates from EvoSC. @@ -54,27 +53,10 @@ public interface IManialinkManager public void RemoveManiaScript(string name); /// - /// Render a template and send it to all players. - /// - /// The name of the template. - /// Any kind of data the template uses. - /// - public Task SendManialinkAsync(string name, IDictionary data); - - /// - /// Render a template and send it to all players. - /// - /// The name of the template. - /// Any kind of data the template uses. - /// - public Task SendManialinkAsync(string name, dynamic data); - - /// - /// Render a template and send it to all players without data. + /// Pre-process all current templates registered. /// - /// The name of the template. /// - public Task SendManialinkAsync(string name); + public Task PreprocessAllAsync(); /// /// Send a manialink to all players which persists even if a player re-connects. @@ -103,105 +85,82 @@ public interface IManialinkManager public Task SendPersistentManialinkAsync(string name); /// - /// Render a template and send it to a specific player. - /// - /// The player to send to. - /// The name of the template. - /// Data which the template uses. - /// - public Task SendManialinkAsync(IPlayer player, string name, IDictionary data); - - /// - /// Render a template and send it to a specific player. + /// 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. /// - /// The player to send to. - /// The name of the template. - /// Data which the template uses + /// Name of the template to show. + /// Method that returns data to be sent. /// - public Task SendManialinkAsync(IPlayer player, string name, dynamic data); - + public Task SendPersistentManialinkAsync(string name, Func> setupData); + /// - /// Render a template and send it to a specific player. + /// 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. /// - /// The login to send to. - /// The name of the template. - /// Data which the template uses + /// Name of the template to show. + /// Method that returns data to be sent. /// - public Task SendManialinkAsync(string playerLogin, string name, dynamic data); + public Task SendPersistentManialinkAsync(string name, Func>> setupData); /// - /// Render a template and send it to a specific player without data. + /// Hides and removes a persistent mainalink. /// - /// The player to send to. - /// The name of the template. + /// Name of the template to hide. /// - public Task SendManialinkAsync(IPlayer player, string name) => SendManialinkAsync(player, name, new { }); + public Task RemovePersistentManialinkAsync(string name); /// - /// Render a template and send it to a set of players. + /// Add a global variable that is accessible from all templates. /// - /// The players to send to - /// The name of the template. - /// Data which the template uses - /// - public Task SendManialinkAsync(IEnumerable players, string name, IDictionary data); + /// Name of the variable. + /// Value of the variable. + /// Variable type. + public void AddGlobalVariable(string name, T value); /// - /// Render a template and send it to a set of players. + /// Remove a global variable. /// - /// The players to send to - /// The name of the template. - /// Data which the template uses - /// - public Task SendManialinkAsync(IEnumerable players, string name, dynamic data); + /// Name of the variable to remove. + public void RemoveGlobalVariable(string name); /// - /// Render a template and send it to a set of players without data. + /// Remove all global variables. /// - /// The players to send to - /// The name of the template. - /// - public Task SendManialinkAsync(IEnumerable players, string name) => - SendManialinkAsync(players, name, new { }); + public void ClearGlobalVariables(); /// - /// Hide a manialink from all players. + /// Get the rendered contents of a template. /// - /// Name of the manialink to hide. + /// Name of the template. + /// Data to send to the template. /// - public Task HideManialinkAsync(string name); + public Task PrepareAndRenderAsync(string name, IDictionary data); /// - /// Hide a manialink from a player. + /// Get the rendered contents of a template. /// - /// The player to hide the manialink from. - /// Name of the manialink to hide. + /// Name of the template. + /// Data to send to the template. /// - public Task HideManialinkAsync(IPlayer player, string name); + public Task PrepareAndRenderAsync(string name, dynamic data); /// - /// Hide a manialink from a player. + /// Get the effective name of a template's name. This may change + /// depending on the theme. /// - /// The player to hide the manialink from. - /// Name of the manialink to hide. + /// Original name of the template. /// - public Task HideManialinkAsync(string playerLogin, string name); - - /// - /// Hide a manialink from a set of players. - /// - /// The players to hide the manialink from. - /// Name of the manialink to hide. - /// - public Task HideManialinkAsync(IEnumerable players, string name); - + public string GetEffectiveName(string name); + /// - /// Pre-process all current templates registered. + /// Create a new transaction for manialink operations. + /// Nothing is done until the transaction is committed, + /// and all operations are sent at once for speed. /// /// - public Task PreprocessAllAsync(); - - public void AddGlobalVariable(string name, T value); - public void RemoveGlobalVariable(string name); - public void ClearGlobalVariables(); + public IManialinkTransaction CreateTransaction(); } diff --git a/src/EvoSC.Manialinks/Interfaces/IManialinkOperations.cs b/src/EvoSC.Manialinks/Interfaces/IManialinkOperations.cs new file mode 100644 index 000000000..b27be90db --- /dev/null +++ b/src/EvoSC.Manialinks/Interfaces/IManialinkOperations.cs @@ -0,0 +1,122 @@ +using EvoSC.Common.Interfaces.Models; + +namespace EvoSC.Manialinks.Interfaces; + +public interface IManialinkOperations +{ + /// + /// Render a template and send it to all players. + /// + /// The name of the template. + /// Any kind of data the template uses. + /// + public Task SendManialinkAsync(string name, IDictionary data); + + /// + /// Render a template and send it to all players. + /// + /// The name of the template. + /// Any kind of data the template uses. + /// + public Task SendManialinkAsync(string name, dynamic data); + + /// + /// Render a template and send it to all players without data. + /// + /// The name of the template. + /// + + public Task SendManialinkAsync(string name); + /// + /// Render a template and send it to a specific player. + /// + /// The player to send to. + /// The name of the template. + /// Data which the template uses. + /// + public Task SendManialinkAsync(IPlayer player, string name, IDictionary data); + + /// + /// Render a template and send it to a specific player. + /// + /// The player to send to. + /// The name of the template. + /// Data which the template uses + /// + public Task SendManialinkAsync(IPlayer player, string name, dynamic data); + + /// + /// Render a template and send it to a specific player. + /// + /// The login to send to. + /// The name of the template. + /// Data which the template uses + /// + public Task SendManialinkAsync(string playerLogin, string name, dynamic data); + + /// + /// Render a template and send it to a specific player without data. + /// + /// The player to send to. + /// The name of the template. + /// + public Task SendManialinkAsync(IPlayer player, string name) => SendManialinkAsync(player, name, new { }); + + /// + /// Render a template and send it to a set of players. + /// + /// The players to send to + /// The name of the template. + /// Data which the template uses + /// + public Task SendManialinkAsync(IEnumerable players, string name, IDictionary data); + + /// + /// Render a template and send it to a set of players. + /// + /// The players to send to + /// The name of the template. + /// Data which the template uses + /// + public Task SendManialinkAsync(IEnumerable players, string name, dynamic data); + + /// + /// Render a template and send it to a set of players without data. + /// + /// The players to send to + /// The name of the template. + /// + public Task SendManialinkAsync(IEnumerable players, string name) => + SendManialinkAsync(players, name, new { }); + + /// + /// Hide a manialink from all players. + /// + /// Name of the manialink to hide. + /// + public Task HideManialinkAsync(string name); + + /// + /// Hide a manialink from a player. + /// + /// The player to hide the manialink from. + /// Name of the manialink to hide. + /// + public Task HideManialinkAsync(IPlayer player, string name); + + /// + /// Hide a manialink from a player. + /// + /// The player to hide the manialink from. + /// Name of the manialink to hide. + /// + public Task HideManialinkAsync(string playerLogin, string name); + + /// + /// Hide a manialink from a set of players. + /// + /// The players to hide the manialink from. + /// Name of the manialink to hide. + /// + public Task HideManialinkAsync(IEnumerable players, string name); +} diff --git a/src/EvoSC.Manialinks/Interfaces/IManialinkTransaction.cs b/src/EvoSC.Manialinks/Interfaces/IManialinkTransaction.cs new file mode 100644 index 000000000..6a85012ec --- /dev/null +++ b/src/EvoSC.Manialinks/Interfaces/IManialinkTransaction.cs @@ -0,0 +1,6 @@ +namespace EvoSC.Manialinks.Interfaces; + +public interface IManialinkTransaction : IManialinkOperations +{ + public Task CommitAsync(); +} 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/ManialinkController.cs b/src/EvoSC.Manialinks/ManialinkController.cs index ac67a0459..8e6e055bd 100644 --- a/src/EvoSC.Manialinks/ManialinkController.cs +++ b/src/EvoSC.Manialinks/ManialinkController.cs @@ -30,7 +30,7 @@ public Task ShowAsync(IEnumerable players, string maniaLink) => public Task ShowAsync(IEnumerable players, string maniaLink, object data) => Context.ManialinkManager.SendManialinkAsync(players, maniaLink, PrepareManialinkData(data)); - + public Task ShowPersistentAsync(string name) => Context.ManialinkManager.SendPersistentManialinkAsync(name, PrepareManialinkData(new object())); public Task ShowPersistentAsync(string name, object data) => Context.ManialinkManager.SendPersistentManialinkAsync(name, PrepareManialinkData(data)); diff --git a/src/EvoSC.Manialinks/ManialinkManager.cs b/src/EvoSC.Manialinks/ManialinkManager.cs index ccb6c6d97..87928429b 100644 --- a/src/EvoSC.Manialinks/ManialinkManager.cs +++ b/src/EvoSC.Manialinks/ManialinkManager.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; +using System.ComponentModel; +using System.Dynamic; using System.Reflection; -using System.Text; using EvoSC.Common.Events; using EvoSC.Common.Interfaces; using EvoSC.Common.Interfaces.Models; @@ -19,7 +20,6 @@ using GbxRemoteNet; using GbxRemoteNet.Events; using ManiaTemplates; -using ManiaTemplates.Lib; using Microsoft.Extensions.Logging; namespace EvoSC.Manialinks; @@ -33,7 +33,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 +207,12 @@ public async Task SendPersistentManialinkAsync(string name, IDictionary SendPersistentManialinkAsync(name, new { }); + public Task SendPersistentManialinkAsync(string name, Func> setupData) => + SendPersistentManialinkAsync(name, async Task> () => + { + 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) + { + 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 Task RemovePersistentManialinkAsync(string name) + { + name = GetEffectiveName(name); + _persistentManialinks.TryRemove(name, out _); + return Task.CompletedTask; + } + public async Task SendManialinkAsync(IPlayer player, string name, IDictionary data) { name = GetEffectiveName(name); @@ -261,7 +308,7 @@ public Task HideManialinkAsync(string name) { name = GetEffectiveName(name); _persistentManialinks.TryRemove(name, out _); - var manialinkOutput = CreateHideManialink(name); + var manialinkOutput = ManialinkUtils.CreateHideManialink(name); return _server.Remote.SendDisplayManialinkPageAsync(manialinkOutput, 3, true); } @@ -269,21 +316,21 @@ public Task HideManialinkAsync(IPlayer player, string name) { name = GetEffectiveName(name); _persistentManialinks.TryRemove(name, out _); - var manialinkOutput = CreateHideManialink(name); + var manialinkOutput = ManialinkUtils.CreateHideManialink(name); return _server.Remote.SendDisplayManialinkPageToLoginAsync(player.GetLogin(), manialinkOutput, 3, true); } public Task HideManialinkAsync(string playerLogin, string name) { name = GetEffectiveName(name); - var manialinkOutput = CreateHideManialink(name); + var manialinkOutput = ManialinkUtils.CreateHideManialink(name); return _server.Remote.SendDisplayManialinkPageToLoginAsync(playerLogin, manialinkOutput, 3, true); } public Task HideManialinkAsync(IEnumerable players, string name) { name = GetEffectiveName(name); - var manialinkOutput = CreateHideManialink(name); + var manialinkOutput = ManialinkUtils.CreateHideManialink(name); var multiCall = new MultiCall(); foreach (var player in players) @@ -330,9 +377,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) @@ -393,32 +468,22 @@ private IEnumerable PrepareRender(string name) return assemblies; } - private async Task PrepareAndRenderAsync(string name, IDictionary data) + public async Task PrepareAndRenderAsync(string name, IDictionary data) { var assemblies = PrepareRender(name); return await _engine.RenderAsync(name, data, assemblies); } - private async Task PrepareAndRenderAsync(string name, dynamic data) + public async Task PrepareAndRenderAsync(string name, dynamic data) { var assemblies = PrepareRender(name); return await _engine.RenderAsync(name, data, assemblies); } - - private string CreateHideManialink(string name) - { - var sb = new StringBuilder() - .Append("\n") - .Append("\n") - .Append("\n"); - return sb.ToString(); - } - - private string GetEffectiveName(string name) => + public string GetEffectiveName(string name) => _themeManager.ComponentReplacements.TryGetValue(name, out var effectiveName) ? effectiveName : name; + + public IManialinkTransaction CreateTransaction() => new ManialinkTransaction(this, _server); } diff --git a/src/EvoSC.Manialinks/ManialinkTransaction.cs b/src/EvoSC.Manialinks/ManialinkTransaction.cs new file mode 100644 index 000000000..9970f5012 --- /dev/null +++ b/src/EvoSC.Manialinks/ManialinkTransaction.cs @@ -0,0 +1,122 @@ +using EvoSC.Common.Interfaces; +using EvoSC.Common.Interfaces.Models; +using EvoSC.Common.Util; +using EvoSC.Manialinks.Interfaces; +using EvoSC.Manialinks.Util; +using GbxRemoteNet; +using GbxRemoteNet.Interfaces; + +namespace EvoSC.Manialinks; + +public class ManialinkTransaction(IManialinkManager manialinkManager, IServerClient server) : IManialinkTransaction +{ + private readonly MultiCall _serverCalls = new MultiCall(); + private readonly HashSet _persistentRemovals = []; + + public Task SendManialinkAsync(string name, IDictionary data) => AddSendCallAsync(name, data); + + public Task SendManialinkAsync(string name, dynamic data) => AddSendCallAsync(name, data); + + public Task SendManialinkAsync(string name) => AddSendCallAsync(name, new { }); + + public Task SendManialinkAsync(IPlayer player, string name, IDictionary data) => AddSendCallAsync(player, name, data); + + public Task SendManialinkAsync(IPlayer player, string name, dynamic data) => AddSendCallAsync(player, name, data); + + public Task SendManialinkAsync(string playerLogin, string name, dynamic data) => AddSendCallAsync(playerLogin, name, data); + + public async Task SendManialinkAsync(IEnumerable players, string name, IDictionary data) + { + foreach (var player in players) + { + await AddSendCallAsync(player, name, data); + } + } + + public async Task SendManialinkAsync(IEnumerable players, string name, dynamic data) + { + foreach (var player in players) + { + await AddSendCallAsync(player, name, data); + } + } + + public Task HideManialinkAsync(string name) => AddHideCallAsync(name); + + public Task HideManialinkAsync(IPlayer player, string name) => AddHideCallAsync(player, name); + + public Task HideManialinkAsync(string playerLogin, string name) => AddHideCallAsync(playerLogin, name); + + public async Task HideManialinkAsync(IEnumerable players, string name) + { + foreach (var player in players) + { + await AddHideCallAsync(player, name); + } + } + + private async Task AddSendCallAsync(string name, IDictionary data) + { + var output = await GetSendOutputAsync(name, data); + _serverCalls.Add(nameof(IGbxRemoteClient.SendDisplayManialinkPageAsync), output, 0, false); + } + + private async Task AddSendCallAsync(string name, dynamic data) + { + var output = await GetSendOutputAsync(name, data); + _serverCalls.Add(nameof(IGbxRemoteClient.SendDisplayManialinkPageAsync), output, 0, false); + } + + private async Task AddSendCallAsync(string playerLogin, string name, IDictionary data) + { + var output = await GetSendOutputAsync(name, data); + _serverCalls.Add(nameof(IGbxRemoteClient.SendDisplayManialinkPageToLoginAsync), playerLogin, output, 0, false); + } + + private async Task AddSendCallAsync(string playerLogin, string name, dynamic data) + { + var output = await GetSendOutputAsync(name, data); + _serverCalls.Add(nameof(IGbxRemoteClient.SendDisplayManialinkPageToLoginAsync), playerLogin, output, 0, false); + } + + private Task AddSendCallAsync(IPlayer player, string name, IDictionary data) => + AddSendCallAsync(player.GetLogin(), name, data); + + private Task AddSendCallAsync(IPlayer player, string name, dynamic data) => + AddSendCallAsync(player.GetLogin(), name, data); + + private Task AddHideCallAsync(string name) + { + var output = PrepareAndGetHideCall(name); + _serverCalls.Add(nameof(IGbxRemoteClient.SendDisplayManialinkPageAsync), output, 3, true); + return Task.CompletedTask; + } + + private Task AddHideCallAsync(string playerLogin, string name) + { + var output = PrepareAndGetHideCall(name); + _serverCalls.Add(nameof(IGbxRemoteClient.SendDisplayManialinkPageToLoginAsync), playerLogin, output, 3, true); + return Task.CompletedTask; + } + + private Task AddHideCallAsync(IPlayer player, string name) => AddHideCallAsync(player.GetLogin(), name); + + private async Task GetSendOutputAsync(string name, IDictionary data) + { + return await manialinkManager.PrepareAndRenderAsync(manialinkManager.GetEffectiveName(name), data); + } + + private async Task GetSendOutputAsync(string name, dynamic data) + { + return await manialinkManager.PrepareAndRenderAsync(manialinkManager.GetEffectiveName(name), data); + } + + private string PrepareAndGetHideCall(string name) + { + name = manialinkManager.GetEffectiveName(name); + _persistentRemovals.Add(name); + return ManialinkUtils.CreateHideManialink(name); + } + + public Task CommitAsync() => server.Remote.MultiCallAsync(_serverCalls); +} 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.Manialinks/Templates/Containers/Widget.mt b/src/EvoSC.Manialinks/Templates/Containers/Widget.mt index 0eb24922a..d6401cc93 100644 --- a/src/EvoSC.Manialinks/Templates/Containers/Widget.mt +++ b/src/EvoSC.Manialinks/Templates/Containers/Widget.mt @@ -2,53 +2,56 @@ - - - + + + + + +