diff --git a/EvoSC.sln b/EvoSC.sln
index 63b041ee4..e5e0c1fab 100644
--- a/EvoSC.sln
+++ b/EvoSC.sln
@@ -46,7 +46,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvoSC.Manialinks.Tests", "t
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetName", "src\Modules\SetName\SetName.csproj", "{568D81FE-858A-4052-B59B-9381E0FE604C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastestCp", "src\Modules\FastestCp\FastestCp.csproj", "{9E1335F9-6C39-4B3F-9CEB-A65EEDDF798D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastestCpModule", "src\Modules\FastestCpModule\FastestCpModule.csproj", "{9E1335F9-6C39-4B3F-9CEB-A65EEDDF798D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A}"
EndProject
@@ -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
+
+
+
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/src/EvoSC.Commands/Parser/ChatCommandParser.cs b/src/EvoSC.Commands/Parser/ChatCommandParser.cs
index 910ffe70d..5234b0fa8 100644
--- a/src/EvoSC.Commands/Parser/ChatCommandParser.cs
+++ b/src/EvoSC.Commands/Parser/ChatCommandParser.cs
@@ -1,6 +1,5 @@
using EvoSC.Commands.Exceptions;
using EvoSC.Commands.Interfaces;
-using EvoSC.Common.Interfaces.Parsing;
namespace EvoSC.Commands.Parser;
diff --git a/src/EvoSC.Common/Application/AppFeature.cs b/src/EvoSC.Common/Application/AppFeature.cs
index cc04675d2..20e8a45fc 100644
--- a/src/EvoSC.Common/Application/AppFeature.cs
+++ b/src/EvoSC.Common/Application/AppFeature.cs
@@ -115,5 +115,11 @@ public enum AppFeature
/// Manage, add, remove, display manialinks and respond to actions. The manialink framework.
///
[Identifier(NoPrefix = true)]
- Manialinks
+ Manialinks,
+
+ ///
+ /// Add, remove and manage themes using the theme manager.
+ ///
+ [Identifier(NoPrefix = true)]
+ Themes
}
diff --git a/src/EvoSC.Common/Config/Configuration.cs b/src/EvoSC.Common/Config/Configuration.cs
index d396fa270..dc834026c 100644
--- a/src/EvoSC.Common/Config/Configuration.cs
+++ b/src/EvoSC.Common/Config/Configuration.cs
@@ -1,8 +1,8 @@
using Config.Net;
-using EvoSC.Common.Config.Models;
-using EvoSC.Common.Config.Stores;
using EvoSC.Common.Config.Mapping;
using EvoSC.Common.Config.Mapping.Toml;
+using EvoSC.Common.Config.Models;
+using EvoSC.Common.Config.Stores;
namespace EvoSC.Common.Config;
@@ -23,6 +23,7 @@ public static IEvoScBaseConfig GetBaseConfig(string configFile, Dictionary();
+
+ foreach (var entry in doc.Entries)
+ {
+ GetEntriesRecursive(entry.Key, entry.Value, options);
+ }
+
+ result = new DynamicThemeOptions(options);
+ return true;
+ }
+
+ public string? ToRawString(object? value)
+ {
+ return "";
+ }
+
+ public IEnumerable SupportedTypes => new[] { typeof(DynamicThemeOptions) };
+
+ private void GetEntriesRecursive(string name, TomlValue tomlValue, Dictionary options)
+ {
+ if (tomlValue is TomlTable table)
+ {
+ foreach (var entry in table.Entries)
+ {
+ GetEntriesRecursive($"{name}.{entry.Key}", entry.Value, options);
+ }
+ }
+ else
+ {
+ options[name] = tomlValue.StringValue;
+ }
+ }
+}
diff --git a/src/EvoSC.Common/Config/Mapping/Toml/ConfigMapper.cs b/src/EvoSC.Common/Config/Mapping/Toml/ConfigMapper.cs
index db005754d..2b6e46f2d 100644
--- a/src/EvoSC.Common/Config/Mapping/Toml/ConfigMapper.cs
+++ b/src/EvoSC.Common/Config/Mapping/Toml/ConfigMapper.cs
@@ -11,5 +11,6 @@ public static void AddMapper(ITomlTypeMapper mapper) =>
public static void SetupDefaultMappers()
{
AddMapper(new TextColorTomlMapper());
+ AddMapper(new ThemeConfigOptionsMapper());
}
}
diff --git a/src/EvoSC.Common/Config/Mapping/Toml/ThemeConfigOptionsMapper.cs b/src/EvoSC.Common/Config/Mapping/Toml/ThemeConfigOptionsMapper.cs
new file mode 100644
index 000000000..288d72b64
--- /dev/null
+++ b/src/EvoSC.Common/Config/Mapping/Toml/ThemeConfigOptionsMapper.cs
@@ -0,0 +1,82 @@
+using EvoSC.Common.Interfaces.Config.Mapping;
+using EvoSC.Common.Themes;
+using Tomlet;
+using Tomlet.Models;
+
+namespace EvoSC.Common.Config.Mapping.Toml;
+
+public class ThemeConfigOptionsMapper : ITomlTypeMapper
+{
+ public TomlValue Serialize(DynamicThemeOptions? typeValue)
+ {
+ var doc = TomlDocument.CreateEmpty();
+
+ foreach (var option in typeValue)
+ {
+ BuildTomlDocument(doc, option.Key.Split('.'), option.Value);
+ }
+
+ return doc;
+ }
+
+ public DynamicThemeOptions Deserialize(TomlValue tomlValue)
+ {
+ var doc = tomlValue as TomlDocument;
+
+ if (doc == null)
+ {
+ throw new InvalidOperationException("Value is not a document");
+ }
+
+ var options = new DynamicThemeOptions();
+
+ foreach (var entry in doc.Entries)
+ {
+ BuildOptionsObject(options, entry.Key, entry.Value);
+ }
+
+ return options;
+ }
+
+ private void BuildTomlDocument(TomlDocument doc, IEnumerable optionParts, object value)
+ {
+ var parts = optionParts as string[] ?? optionParts.ToArray();
+
+ if (parts.Length == 1)
+ {
+ var tomlValue = TomletMain.ValueFrom(value);
+ doc.Entries[parts.First()] = tomlValue;
+ return;
+ }
+
+ foreach (var part in parts)
+ {
+ if (doc.ContainsKey(part))
+ {
+ BuildTomlDocument((TomlDocument)doc.Entries[part], parts.Skip(1), value);
+ }
+ else
+ {
+ var newDoc = TomlDocument.CreateEmpty();
+ doc.Entries[part] = newDoc;
+ BuildTomlDocument(newDoc, parts.Skip(1), value);
+ }
+ }
+ }
+
+ private void BuildOptionsObject(DynamicThemeOptions options, string key, TomlValue tomlValue)
+ {
+ var doc = tomlValue as TomlDocument;
+
+ if (doc == null)
+ {
+ options[key] = tomlValue.StringValue;
+ return;
+ }
+
+ foreach (var entry in doc.Entries)
+ {
+ BuildOptionsObject(options, $"{key}.{entry.Key}", entry.Value);
+ }
+ }
+}
diff --git a/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs b/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs
index 84ef9f67a..9008fb823 100644
--- a/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs
+++ b/src/EvoSC.Common/Config/Models/IEvoScBaseConfig.cs
@@ -1,4 +1,6 @@
-namespace EvoSC.Common.Config.Models;
+using EvoSC.Common.Themes;
+
+namespace EvoSC.Common.Config.Models;
public interface IEvoScBaseConfig
{
@@ -6,7 +8,9 @@ public interface IEvoScBaseConfig
public ILoggingConfig Logging { get; set; }
public IServerConfig Server { get; set; }
public IPathConfig Path { get; set; }
- public IThemeConfig Theme { get; set; }
+
public IModuleConfig Modules { get; set; }
public ILocaleConfig Locale { get; set; }
+
+ public DynamicThemeOptions Theme { get; set; }
}
diff --git a/src/EvoSC.Common/Config/Stores/TomlConfigStore.cs b/src/EvoSC.Common/Config/Stores/TomlConfigStore.cs
index b8973c62c..6c70f313f 100644
--- a/src/EvoSC.Common/Config/Stores/TomlConfigStore.cs
+++ b/src/EvoSC.Common/Config/Stores/TomlConfigStore.cs
@@ -1,12 +1,11 @@
using System.ComponentModel;
-using System.Drawing;
-using System.IO;
+using System.Globalization;
using System.Reflection;
using Config.Net;
+using EvoSC.Common.Themes;
using EvoSC.Common.Util;
using EvoSC.Common.Util.TextFormatting;
using Tomlet;
-using Tomlet.Exceptions;
using Tomlet.Models;
namespace EvoSC.Common.Config.Stores;
@@ -53,6 +52,11 @@ private TomlDocument BuildSubDocument(TomlDocument document, Type type, string n
{
foreach (var property in type.GetProperties())
{
+ if (property.PropertyType == typeof(DynamicThemeOptions))
+ {
+ continue;
+ }
+
if (property.PropertyType.IsInterface)
{
document = BuildSubDocument(document, property.PropertyType, name == "" ? property.Name : $"{name}.{property.Name}");
@@ -68,7 +72,9 @@ private TomlDocument BuildSubDocument(TomlDocument document, Type type, string n
var tomlValue = optionAttr?.DefaultValue ?? property.PropertyType.GetDefaultTypeValue();
if (property.PropertyType == typeof(TextColor))
+ {
tomlValue = new TextColor(tomlValue.ToString());
+ }
// get property value
var value = TomletMain.ValueFrom(property.PropertyType,
@@ -100,13 +106,13 @@ public void Dispose()
if (lastDotIndex > 0 && key.Length > lastDotIndex + 1 && !char.IsAsciiLetterOrDigit(key[lastDotIndex + 1]))
{
var value = _document.GetValue(key[..lastDotIndex]) as TomlArray;
- return value.Count.ToString();
+ return value.Count.ToString(CultureInfo.InvariantCulture);
}
if (key.EndsWith("]", StringComparison.Ordinal))
{
var indexStart = key.IndexOf("[", StringComparison.Ordinal);
- var index = int.Parse(key[(indexStart + 1)..^1]);
+ var index = int.Parse(key[(indexStart + 1)..^1], CultureInfo.InvariantCulture);
var value = _document.GetValue(key[..indexStart]) as TomlArray;
return value?.Skip(index)?.FirstOrDefault()?.StringValue;
@@ -118,6 +124,11 @@ public void Dispose()
{
return string.Join(" ", arrayValue.Select(v => v.StringValue));
}
+
+ if (keyValue is TomlTable tableValue)
+ {
+ return tableValue.SerializeNonInlineTable("Theme", false);
+ }
return keyValue.StringValue;
}
diff --git a/src/EvoSC.Common/Database/Repository/DbRepository.cs b/src/EvoSC.Common/Database/Repository/DbRepository.cs
index 7d5901b23..9b9206d11 100644
--- a/src/EvoSC.Common/Database/Repository/DbRepository.cs
+++ b/src/EvoSC.Common/Database/Repository/DbRepository.cs
@@ -1,7 +1,6 @@
using EvoSC.Common.Interfaces.Database;
using LinqToDB;
-using LinqToDB.Data;
namespace EvoSC.Common.Database.Repository;
diff --git a/src/EvoSC.Common/EvoSC.Common.csproj b/src/EvoSC.Common/EvoSC.Common.csproj
index 782964c9f..9e99a4cd1 100644
--- a/src/EvoSC.Common/EvoSC.Common.csproj
+++ b/src/EvoSC.Common/EvoSC.Common.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/EvoSC.Common/Interfaces/Database/IDbConnectionFactory.cs b/src/EvoSC.Common/Interfaces/Database/IDbConnectionFactory.cs
index 2a7eb8039..2ebe1cbcb 100644
--- a/src/EvoSC.Common/Interfaces/Database/IDbConnectionFactory.cs
+++ b/src/EvoSC.Common/Interfaces/Database/IDbConnectionFactory.cs
@@ -1,5 +1,4 @@
using LinqToDB;
-using LinqToDB.Data;
namespace EvoSC.Common.Interfaces.Database;
diff --git a/src/EvoSC.Common/Interfaces/Services/IServiceContainerManager.cs b/src/EvoSC.Common/Interfaces/Services/IServiceContainerManager.cs
index f98c373d5..d750bc7e9 100644
--- a/src/EvoSC.Common/Interfaces/Services/IServiceContainerManager.cs
+++ b/src/EvoSC.Common/Interfaces/Services/IServiceContainerManager.cs
@@ -33,4 +33,6 @@ public interface IServiceContainerManager
/// The ID of the module that requires the dependency.
/// The ID of the dependency.
public void RegisterDependency(Guid moduleId, Guid dependencyId);
+
+ public Container GetContainer(Guid moduleId);
}
diff --git a/src/EvoSC.Common/Interfaces/Themes/Builders/IReplaceComponentBuilder.cs b/src/EvoSC.Common/Interfaces/Themes/Builders/IReplaceComponentBuilder.cs
new file mode 100644
index 000000000..884baf86d
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/Builders/IReplaceComponentBuilder.cs
@@ -0,0 +1,14 @@
+using EvoSC.Common.Themes;
+
+namespace EvoSC.Common.Interfaces.Themes.Builders;
+
+public interface IReplaceComponentBuilder
+where TTheme : Theme
+{
+ ///
+ /// Replace a component with the provided component.
+ ///
+ /// Name of the new component that will replace the old component.
+ ///
+ public TTheme With(string newComponent);
+}
diff --git a/src/EvoSC.Common/Interfaces/Themes/Builders/ISetThemeOptionBuilder.cs b/src/EvoSC.Common/Interfaces/Themes/Builders/ISetThemeOptionBuilder.cs
new file mode 100644
index 000000000..b3517a24d
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/Builders/ISetThemeOptionBuilder.cs
@@ -0,0 +1,13 @@
+using EvoSC.Common.Themes;
+
+namespace EvoSC.Common.Interfaces.Themes.Builders;
+
+public interface ISetThemeOptionBuilder where TTheme : Theme
+{
+ ///
+ /// Set a theme option value.
+ ///
+ /// Value of the theme option.
+ ///
+ public TTheme To(object value);
+}
diff --git a/src/EvoSC.Common/Interfaces/Themes/DefaultThemeOptions.cs b/src/EvoSC.Common/Interfaces/Themes/DefaultThemeOptions.cs
new file mode 100644
index 000000000..2eda4ea08
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/DefaultThemeOptions.cs
@@ -0,0 +1,120 @@
+namespace EvoSC.Common.Interfaces.Themes;
+
+public interface DefaultThemeOptions
+{
+ ///
+ /// The default UI Font.
+ ///
+ public static readonly string UIFont = "UI.Font";
+
+ ///
+ /// The default font size.
+ ///
+ public static readonly string UIFontSize = "UI.FontSize";
+
+ ///
+ /// Primary text color.
+ ///
+ public static readonly string UITextPrimary = "UI.TextPrimary";
+
+ ///
+ /// Secondary text color.
+ ///
+ public static readonly string UITextSecondary = "UI.TextSecondary";
+
+ ///
+ /// Primary background color.
+ ///
+ public static readonly string UIBgPrimary = "UI.BgPrimary";
+
+ ///
+ /// Secondary background color.
+ ///
+ public static readonly string UIBgSecondary = "UI.BgSecondary";
+
+ ///
+ /// Primary border color.
+ ///
+ public static readonly string UIBorderPrimary = "UI.BorderPrimary";
+
+ ///
+ /// Secondary border color.
+ ///
+ public static readonly string UIBorderSecondary = "UI.BorderSecondary";
+
+ ///
+ /// Dark version of the logo.
+ ///
+ public static readonly string UILogoDark = "UI.LogoDark";
+
+ ///
+ /// Light version of the logo.
+ ///
+ public static readonly string UILogoLight = "UI.LogoLight";
+
+ ///
+ /// Primary color for chat messages.
+ ///
+ public static readonly string ChatPrimary = "Chat.Primary";
+
+ ///
+ /// Secondary color for chat messages.
+ ///
+ public static readonly string ChatSecondary = "Chat.Secondary";
+
+ ///
+ /// Color of info chat messages.
+ ///
+ public static readonly string ChatInfo = "Chat.Info";
+
+ ///
+ /// Color of success chat messages.
+ ///
+ public static readonly string ChatSuccess = "Chat.Success";
+
+ ///
+ /// Color of warning chat messages.
+ ///
+ public static readonly string ChatWarning = "Chat.Warning";
+
+ ///
+ /// Color of error/danger chat messages.
+ ///
+ public static readonly string ChatDanger = "Chat.Danger";
+
+ public static readonly string Red = "Red";
+ public static readonly string Green = "Green";
+ public static readonly string Blue = "Blue";
+ public static readonly string Yellow = "Yellow";
+ public static readonly string Teal = "Teal";
+ public static readonly string Purple = "Purple";
+ public static readonly string Gold = "Gold";
+ public static readonly string Silver = "Silver";
+ public static readonly string Bronze = "Bronze";
+ public static readonly string Grass = "Grass";
+ public static readonly string Orange = "Orange";
+ public static readonly string Gray = "Gray";
+ public static readonly string Black = "Black";
+ public static readonly string White = "White";
+ public static readonly string Pink = "Pink";
+
+ ///
+ /// Default info UI color.
+ ///
+ public static readonly string Info = "Info";
+
+ ///
+ /// Default success UI color.
+ ///
+ public static readonly string Success = "Success";
+
+ ///
+ /// Default warning UI color.
+ ///
+ public static readonly string Warning = "Warning";
+
+ ///
+ /// Default error/danger UI color.
+ ///
+ public static readonly string Danger = "Danger";
+}
diff --git a/src/EvoSC.Common/Interfaces/Themes/ITheme.cs b/src/EvoSC.Common/Interfaces/Themes/ITheme.cs
new file mode 100644
index 000000000..2da756c27
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/ITheme.cs
@@ -0,0 +1,21 @@
+namespace EvoSC.Common.Interfaces.Themes;
+
+public interface ITheme
+{
+ ///
+ /// Configures the theme.
+ ///
+ ///
+ public Task ConfigureAsync();
+
+ ///
+ /// Options set by this theme.
+ ///
+ public Dictionary ThemeOptions { get; }
+
+ ///
+ /// Component replacements defined by this theme.
+ ///
+ public Dictionary ComponentReplacements { get; }
+}
+
diff --git a/src/EvoSC.Common/Interfaces/Themes/IThemeExpressions.cs b/src/EvoSC.Common/Interfaces/Themes/IThemeExpressions.cs
new file mode 100644
index 000000000..1f68689d1
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/IThemeExpressions.cs
@@ -0,0 +1,22 @@
+using EvoSC.Common.Interfaces.Themes.Builders;
+using EvoSC.Common.Themes;
+
+namespace EvoSC.Common.Interfaces.Themes;
+
+public interface IThemeExpressions
+where TTheme : Theme
+{
+ ///
+ /// Set a theme option.
+ ///
+ /// Name of the theme option.
+ ///
+ public ISetThemeOptionBuilder Set(string key);
+
+ ///
+ /// Replace a component.
+ ///
+ /// Name of the component to replace.
+ ///
+ public IReplaceComponentBuilder Replace(string component);
+}
diff --git a/src/EvoSC.Common/Interfaces/Themes/IThemeInfo.cs b/src/EvoSC.Common/Interfaces/Themes/IThemeInfo.cs
new file mode 100644
index 000000000..7aae9220c
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/IThemeInfo.cs
@@ -0,0 +1,34 @@
+namespace EvoSC.Common.Interfaces.Themes;
+
+public interface IThemeInfo
+{
+ ///
+ /// The class type of the theme.
+ ///
+ public Type ThemeType { get; }
+
+ ///
+ /// Unique name of the theme.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Short summary describing the theme.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Class of the theme which this theme overrides.
+ ///
+ public Type? OverrideTheme { get; }
+
+ ///
+ /// ID of the module this theme is part of.
+ ///
+ public Guid ModuleId { get; }
+
+ ///
+ /// The effective theme class that is used.
+ ///
+ public Type EffectiveThemeType { get; }
+}
diff --git a/src/EvoSC.Common/Interfaces/Themes/IThemeManager.cs b/src/EvoSC.Common/Interfaces/Themes/IThemeManager.cs
new file mode 100644
index 000000000..74db964f2
--- /dev/null
+++ b/src/EvoSC.Common/Interfaces/Themes/IThemeManager.cs
@@ -0,0 +1,80 @@
+using EvoSC.Common.Themes;
+
+namespace EvoSC.Common.Interfaces.Themes;
+
+public interface IThemeManager
+{
+ ///
+ /// All available themes.
+ ///
+ public IEnumerable AvailableThemes { get; }
+
+ ///
+ /// Options for the current theme.
+ ///
+ public dynamic Theme { get; }
+
+ ///
+ /// All available component replacements.
+ ///
+ public Dictionary ComponentReplacements { get; }
+
+ ///
+ /// Add a new theme.
+ ///
+ /// Class type of the theme.
+ /// ID of the module providing the theme.
+ ///
+ public Task AddThemeAsync(Type themeType, Guid moduleId);
+
+ ///
+ /// Add a new theme.
+ ///
+ /// Class type of the theme.
+ ///
+ public Task AddThemeAsync(Type themeType);
+
+ ///
+ /// Add a new theme.
+ ///
+ /// Type of the theme class.
+ ///
+ public Task AddThemeAsync() where TTheme : Theme
+ => AddThemeAsync(typeof(TTheme));
+
+ ///
+ /// Add a new theme.
+ ///
+ /// Id of the module providing the theme.
+ /// Type of the theme class.
+ ///
+ public Task AddThemeAsync(Guid moduleId) where TTheme : Theme
+ => AddThemeAsync(typeof(TTheme), moduleId);
+
+ ///
+ /// Remove a theme.
+ ///
+ /// Name of the theme to remove.
+ ///
+ public Task RemoveThemeAsync(string name);
+
+ ///
+ /// Remove all themes from a module.
+ ///
+ /// ID of the module to remove themes from.
+ ///
+ public Task RemoveThemesForModuleAsync(Guid moduleId);
+
+ ///
+ /// Activate a theme. This will replace existing themes which this theme will
+ /// potentially override.
+ ///
+ /// Name of the theme to activate.
+ ///
+ public Task ActivateThemeAsync(string name);
+
+ ///
+ /// Invalidate the theme options cache and renew option values.
+ ///
+ public void InvalidateCache();
+}
diff --git a/src/EvoSC.Common/Remote/ChatRouter/RemoteChatRouter.cs b/src/EvoSC.Common/Remote/ChatRouter/RemoteChatRouter.cs
index 29b0d966b..46e6b3904 100644
--- a/src/EvoSC.Common/Remote/ChatRouter/RemoteChatRouter.cs
+++ b/src/EvoSC.Common/Remote/ChatRouter/RemoteChatRouter.cs
@@ -5,7 +5,6 @@
using EvoSC.Common.Middleware;
using EvoSC.Common.Util;
using EvoSC.Common.Util.ServerUtils;
-using EvoSC.Common.Util.TextFormatting;
using GbxRemoteNet.Events;
using Microsoft.Extensions.Logging;
diff --git a/src/EvoSC.Common/Remote/ServerClient.cs b/src/EvoSC.Common/Remote/ServerClient.cs
index ac0eddcb9..b76f3a77b 100644
--- a/src/EvoSC.Common/Remote/ServerClient.cs
+++ b/src/EvoSC.Common/Remote/ServerClient.cs
@@ -1,6 +1,7 @@
using EvoSC.Common.Config.Models;
using EvoSC.Common.Exceptions;
using EvoSC.Common.Interfaces;
+using EvoSC.Common.Interfaces.Themes;
using GbxRemoteNet;
using GbxRemoteNet.Interfaces;
using Microsoft.Extensions.Logging;
@@ -13,17 +14,20 @@ public partial class ServerClient : IServerClient
private readonly IEvoScBaseConfig _config;
private readonly ILogger _logger;
private readonly IEvoSCApplication _app;
+ private readonly IThemeManager _themes;
private bool _connected;
public IGbxRemoteClient Remote => _gbxRemote;
public bool Connected => _connected;
- public ServerClient(IEvoScBaseConfig config, ILogger logger, IEvoSCApplication app)
+ public ServerClient(IEvoScBaseConfig config, ILogger logger, IEvoSCApplication app, IThemeManager themes)
{
_config = config;
_logger = logger;
_app = app;
+ _themes = themes;
+
_connected = false;
_gbxRemote = new GbxRemoteClient(config.Server.Host, config.Server.Port, logger);
diff --git a/src/EvoSC.Common/Remote/ServerClient_Responses.cs b/src/EvoSC.Common/Remote/ServerClient_Responses.cs
index cd4062050..77ce020dc 100644
--- a/src/EvoSC.Common/Remote/ServerClient_Responses.cs
+++ b/src/EvoSC.Common/Remote/ServerClient_Responses.cs
@@ -8,25 +8,25 @@ public partial class ServerClient
{
private TextFormatter MakeInfoMessage(string text) =>
new TextFormatter()
- .AddText("", styling => styling.WithColor(_config.Theme.Chat.InfoColor))
+ .AddText("", styling => styling.WithColor(new TextColor(_themes.Theme.Chat_Info)))
.AddText(" ")
.AddText(text);
private TextFormatter MakeSuccessMessage(string text) =>
new TextFormatter()
- .AddText("", styling => styling.WithColor(_config.Theme.Chat.SuccessColor))
+ .AddText("", styling => styling.WithColor(new TextColor(_themes.Theme.Chat_Success)))
.AddText(" ")
.AddText(text);
private TextFormatter MakeWarningMessage(string text) =>
new TextFormatter()
- .AddText("", styling => styling.WithColor(_config.Theme.Chat.WarningColor))
+ .AddText("", styling => styling.WithColor(new TextColor(_themes.Theme.Chat_Warning)))
.AddText(" ")
.AddText(text);
private TextFormatter MakeErrorMessage(string text) =>
new TextFormatter()
- .AddText("", styling => styling.WithColor(_config.Theme.Chat.ErrorColor))
+ .AddText("", styling => styling.WithColor(new TextColor(_themes.Theme.Chat_Danger)))
.AddText(" ")
.AddText(text);
diff --git a/src/EvoSC.Common/Services/ServiceContainerManager.cs b/src/EvoSC.Common/Services/ServiceContainerManager.cs
index 23b91bdc7..647b39c03 100644
--- a/src/EvoSC.Common/Services/ServiceContainerManager.cs
+++ b/src/EvoSC.Common/Services/ServiceContainerManager.cs
@@ -143,7 +143,17 @@ public void RegisterDependency(Guid moduleId, Guid dependencyId)
_dependencyServices[moduleId].Add(dependencyId);
_logger.LogDebug("Registered dependency '{DepId}' for '{ContainerId}'", dependencyId, moduleId);
}
-
+
+ public Container GetContainer(Guid moduleId)
+ {
+ if (!_containers.ContainsKey(moduleId))
+ {
+ throw new InvalidOperationException($"Container '{moduleId}' was not found to have a container.");
+ }
+
+ return _containers[moduleId];
+ }
+
private void ResolveCoreService(UnregisteredTypeEventArgs e, Guid containerId)
{
try
@@ -173,20 +183,12 @@ private void ResolveCoreService(UnregisteredTypeEventArgs e, Guid containerId)
}
}
- try
- {
- _logger.LogTrace(
- "Dependencies does not have service '{Service}' for {Container}. Will try core services",
- e.UnregisteredServiceType,
- containerId);
-
- return _app.Services.GetInstance(e.UnregisteredServiceType);
- }
- catch (ActivationException ex)
- {
- // _logger.LogError(ex, "Failed to get EvoSC core service");
- throw;
- }
+ _logger.LogTrace(
+ "Dependencies does not have service '{Service}' for {Container}. Will try core services",
+ e.UnregisteredServiceType,
+ containerId);
+
+ return _app.Services.GetInstance(e.UnregisteredServiceType);
});
}
catch (Exception ex)
diff --git a/src/EvoSC.Common/Themes/Attributes/ThemeAttribute.cs b/src/EvoSC.Common/Themes/Attributes/ThemeAttribute.cs
new file mode 100644
index 000000000..021f8ad85
--- /dev/null
+++ b/src/EvoSC.Common/Themes/Attributes/ThemeAttribute.cs
@@ -0,0 +1,20 @@
+namespace EvoSC.Common.Themes.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class ThemeAttribute : Attribute
+{
+ ///
+ /// Unique name of the theme.
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// Short summary describing the theme.
+ ///
+ public required string Description { get; init; }
+
+ ///
+ /// The class of the theme which this theme will override.
+ ///
+ public Type? OverrideTheme { get; init; }
+}
diff --git a/src/EvoSC.Common/Themes/BaseEvoScTheme.cs b/src/EvoSC.Common/Themes/BaseEvoScTheme.cs
new file mode 100644
index 000000000..b35b278cb
--- /dev/null
+++ b/src/EvoSC.Common/Themes/BaseEvoScTheme.cs
@@ -0,0 +1,95 @@
+using EvoSC.Common.Interfaces.Themes;
+using EvoSC.Common.Themes.Attributes;
+using EvoSC.Common.Util;
+
+namespace EvoSC.Common.Themes;
+
+[Theme(Name = "Default", Description = "The default theme as defined in the EvoSC# config.")]
+public class BaseEvoScTheme : Theme
+{
+ public async override Task ConfigureAsync()
+ {
+ SetDefaultUtilityColors();
+ GenerateUtilityColorShades();
+
+ SetDefaultChatColors();
+ SetDefaultThemeOptions();
+ }
+
+ protected void SetDefaultChatColors()
+ {
+ Set(DefaultThemeOptions.ChatPrimary).To("fff");
+ Set(DefaultThemeOptions.ChatSecondary).To("eee");
+ Set(DefaultThemeOptions.ChatInfo).To("29b");
+ Set(DefaultThemeOptions.ChatDanger).To("c44");
+ Set(DefaultThemeOptions.ChatWarning).To("e83");
+ Set(DefaultThemeOptions.ChatSuccess).To("5b6");
+ }
+
+ protected void SetDefaultThemeOptions()
+ {
+ Set(DefaultThemeOptions.UIFont).To("GameFontExtraBold");
+ Set(DefaultThemeOptions.UIFontSize).To(1);
+ Set(DefaultThemeOptions.UITextPrimary).To("FFFFFF");
+ Set(DefaultThemeOptions.UITextSecondary).To("EDEDEF");
+ Set(DefaultThemeOptions.UIBgPrimary).To("FF0058");
+ Set(DefaultThemeOptions.UIBgSecondary).To("47495A");
+ Set(DefaultThemeOptions.UIBorderPrimary).To("FF0058");
+ Set(DefaultThemeOptions.UIBorderSecondary).To("FFFFFF");
+ Set(DefaultThemeOptions.UILogoDark).To("");
+ Set(DefaultThemeOptions.UILogoLight).To("");
+ }
+
+ protected void SetDefaultUtilityColors()
+ {
+ Set(DefaultThemeOptions.Red).To("E22000");
+ Set(DefaultThemeOptions.Green).To("00D909");
+ Set(DefaultThemeOptions.Blue).To("3491FA");
+ Set(DefaultThemeOptions.Yellow).To("FCE100");
+ Set(DefaultThemeOptions.Teal).To("0FC6C2");
+ Set(DefaultThemeOptions.Purple).To("722ED1");
+ Set(DefaultThemeOptions.Gold).To("FFD000");
+ Set(DefaultThemeOptions.Silver).To("9e9e9e");
+ Set(DefaultThemeOptions.Bronze).To("915d29");
+ Set(DefaultThemeOptions.Grass).To("9FDB1D");
+ Set(DefaultThemeOptions.Orange).To("F77234");
+ Set(DefaultThemeOptions.Gray).To("191A21");
+ Set(DefaultThemeOptions.Black).To("000000");
+ Set(DefaultThemeOptions.White).To("FFFFFF");
+ Set(DefaultThemeOptions.Pink).To("FF0058");
+
+ Set(DefaultThemeOptions.Info).To("29b");
+ Set(DefaultThemeOptions.Success).To("c44");
+ Set(DefaultThemeOptions.Warning).To("e83");
+ Set(DefaultThemeOptions.Danger).To("5b6");
+ }
+
+ protected void GenerateUtilityColorShades()
+ {
+ GenerateShades(DefaultThemeOptions.Red);
+ GenerateShades(DefaultThemeOptions.Green);
+ GenerateShades(DefaultThemeOptions.Blue);
+ GenerateShades(DefaultThemeOptions.Yellow);
+ GenerateShades(DefaultThemeOptions.Teal);
+ GenerateShades(DefaultThemeOptions.Purple);
+ GenerateShades(DefaultThemeOptions.Gold);
+ GenerateShades(DefaultThemeOptions.Silver);
+ GenerateShades(DefaultThemeOptions.Bronze);
+ GenerateShades(DefaultThemeOptions.Grass);
+ GenerateShades(DefaultThemeOptions.Orange);
+ GenerateShades(DefaultThemeOptions.Gray);
+ GenerateShades(DefaultThemeOptions.Pink);
+ }
+
+ private void GenerateShades(string key)
+ {
+ var color = (string)ThemeOptions[key];
+
+ for (var i = 1; i < 10; i++)
+ {
+ var lightness = i * 10f;
+ var shade = ColorUtils.SetLightness(color, lightness);
+ Set($"{key}{lightness}").To(shade);
+ }
+ }
+}
diff --git a/src/EvoSC.Common/Themes/Builders/ReplaceComponentBuilder.cs b/src/EvoSC.Common/Themes/Builders/ReplaceComponentBuilder.cs
new file mode 100644
index 000000000..881344c89
--- /dev/null
+++ b/src/EvoSC.Common/Themes/Builders/ReplaceComponentBuilder.cs
@@ -0,0 +1,24 @@
+using EvoSC.Common.Interfaces.Themes.Builders;
+
+namespace EvoSC.Common.Themes.Builders;
+
+public class ReplaceComponentBuilder : IReplaceComponentBuilder
+where TTheme : Theme
+{
+ private readonly string _component;
+ private readonly Dictionary _componentReplacements;
+ private readonly TTheme _theme;
+
+ internal ReplaceComponentBuilder(string component, Dictionary componentReplacements, TTheme theme)
+ {
+ _component = component;
+ _componentReplacements = componentReplacements;
+ _theme = theme;
+ }
+
+ public TTheme With(string newComponent)
+ {
+ _componentReplacements[_component] = newComponent;
+ return _theme;
+ }
+}
diff --git a/src/EvoSC.Common/Themes/Builders/SetThemeOptionBuilder.cs b/src/EvoSC.Common/Themes/Builders/SetThemeOptionBuilder.cs
new file mode 100644
index 000000000..7ed913027
--- /dev/null
+++ b/src/EvoSC.Common/Themes/Builders/SetThemeOptionBuilder.cs
@@ -0,0 +1,24 @@
+using EvoSC.Common.Interfaces.Themes.Builders;
+
+namespace EvoSC.Common.Themes.Builders;
+
+public class SetThemeOptionBuilder : ISetThemeOptionBuilder
+where TTheme : Theme
+{
+ private readonly string _key;
+ private readonly Dictionary _themeOptions;
+ private readonly TTheme _theme;
+
+ internal SetThemeOptionBuilder(string key, Dictionary themeOptions, TTheme theme)
+ {
+ _key = key;
+ _themeOptions = themeOptions;
+ _theme = theme;
+ }
+
+ public TTheme To(object value)
+ {
+ _themeOptions[_key] = value;
+ return _theme;
+ }
+}
diff --git a/src/EvoSC.Common/Themes/DynamicThemeOptions.cs b/src/EvoSC.Common/Themes/DynamicThemeOptions.cs
new file mode 100644
index 000000000..39e523602
--- /dev/null
+++ b/src/EvoSC.Common/Themes/DynamicThemeOptions.cs
@@ -0,0 +1,56 @@
+using System.Collections;
+using System.Dynamic;
+
+namespace EvoSC.Common.Themes;
+
+public class DynamicThemeOptions : DynamicObject, IDictionary
+{
+ private readonly Dictionary _options;
+
+ public DynamicThemeOptions() => _options = new Dictionary();
+
+ public DynamicThemeOptions(Dictionary options) =>
+ _options = new Dictionary(options);
+
+ public override bool TryGetMember(GetMemberBinder binder, out object? result)
+ {
+ return _options.TryGetValue(RealKey(binder.Name), out result);
+ }
+
+ public object this[string key]
+ {
+ get => _options[RealKey(key)];
+ set => _options[RealKey(key)] = value;
+ }
+
+ public ICollection Keys => _options.Keys;
+
+ public ICollection
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs b/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs
index 3448e571e..fc6a33586 100644
--- a/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs
+++ b/src/EvoSC.Manialinks/Interfaces/IManialinkManager.cs
@@ -200,4 +200,8 @@ public Task SendManialinkAsync(IEnumerable players, string name) =>
///
///
public Task PreprocessAllAsync();
+
+ public void AddGlobalVariable(string name, T value);
+ public void RemoveGlobalVariable(string name);
+ public void ClearGlobalVariables();
}
diff --git a/src/EvoSC.Manialinks/ManialinkController.cs b/src/EvoSC.Manialinks/ManialinkController.cs
index 9ee568afa..ac67a0459 100644
--- a/src/EvoSC.Manialinks/ManialinkController.cs
+++ b/src/EvoSC.Manialinks/ManialinkController.cs
@@ -6,7 +6,6 @@
using EvoSC.Manialinks.Interfaces;
using EvoSC.Manialinks.Interfaces.Validation;
using EvoSC.Manialinks.Validation;
-using ValidationResult = System.ComponentModel.DataAnnotations.ValidationResult;
namespace EvoSC.Manialinks;
@@ -177,7 +176,7 @@ private void AddModelValidationResult(ValidationResult? validationResult)
{
Name = validationResult.MemberNames.FirstOrDefault() ?? "Invalid Value.",
IsInvalid = false,
- Message = validationResult?.ErrorMessage ?? ""
+ Message = validationResult.ErrorMessage ?? ""
});
}
else
@@ -186,7 +185,7 @@ private void AddModelValidationResult(ValidationResult? validationResult)
{
Name = validationResult.MemberNames.FirstOrDefault() ?? "",
IsInvalid = true,
- Message = validationResult?.ErrorMessage ?? "Invalid Value."
+ Message = validationResult.ErrorMessage ?? "Invalid Value."
});
}
}
diff --git a/src/EvoSC.Manialinks/ManialinkManager.cs b/src/EvoSC.Manialinks/ManialinkManager.cs
index e8de4fee7..04ea623c6 100644
--- a/src/EvoSC.Manialinks/ManialinkManager.cs
+++ b/src/EvoSC.Manialinks/ManialinkManager.cs
@@ -1,14 +1,21 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Text;
+using EvoSC.Common.Events;
using EvoSC.Common.Interfaces;
using EvoSC.Common.Interfaces.Models;
+using EvoSC.Common.Interfaces.Themes;
using EvoSC.Common.Remote;
+using EvoSC.Common.Themes;
+using EvoSC.Common.Themes.Events;
+using EvoSC.Common.Themes.Events.Args;
using EvoSC.Common.Util;
using EvoSC.Common.Util.EnumIdentifier;
using EvoSC.Manialinks.Interfaces;
using EvoSC.Manialinks.Interfaces.Models;
using EvoSC.Manialinks.Models;
+using EvoSC.Manialinks.Themes;
+using EvoSC.Manialinks.Util;
using GbxRemoteNet;
using GbxRemoteNet.Events;
using ManiaTemplates;
@@ -21,6 +28,7 @@ public class ManialinkManager : IManialinkManager
{
private readonly ILogger _logger;
private readonly IServerClient _server;
+ private readonly IThemeManager _themeManager;
private readonly ManiaTemplateEngine _engine = new();
private readonly Dictionary _templates = new();
@@ -32,11 +40,13 @@ public class ManialinkManager : IManialinkManager
typeof(IOnlinePlayer).Assembly, typeof(ManialinkManager).Assembly
};
- public ManialinkManager(ILogger logger, IServerClient server, IEventManager events)
+ public ManialinkManager(ILogger logger, IServerClient server, IEventManager events,
+ IThemeManager themeManager)
{
_logger = logger;
_server = server;
-
+ _themeManager = themeManager;
+
events.Subscribe(s => s
.WithEvent(GbxRemoteEvent.PlayerConnect)
.WithInstance(this)
@@ -44,25 +54,26 @@ public ManialinkManager(ILogger logger, IServerClient server,
.WithHandlerMethod(HandlePlayerConnectAsync)
.AsAsync()
);
- }
- ///
- /// Used to send persistent manialinks to newly connected players.
- ///
- private async Task HandlePlayerConnectAsync(object sender, PlayerConnectGbxEventArgs e)
- {
- try
- {
- foreach (var (_, output) in _persistentManialinks)
- {
- await _server.Remote.SendDisplayManialinkPageToLoginAsync(e.Login, output, 0, false);
- }
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Failed to send persistent manialink login '{Login}'. Did they leave already?",
- e.Login);
- }
+ events.Subscribe(s => s
+ .WithPriority(EventPriority.High)
+ .WithEvent(ThemeEvents.CurrentThemeChanged)
+ .WithInstance(this)
+ .WithInstanceClass()
+ .WithHandlerMethod(HandleThemeActivatedAsync));
+
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+ themeManager.AddThemeAsync();
+
+ _engine.GlobalVariables["Util"] = new GlobalManialinkUtils(themeManager);
+ _engine.GlobalVariables["Icons"] = new GameIcons();
+ _engine.GlobalVariables["Font"] = new FontManialinkHelper(themeManager);
}
public async Task AddDefaultTemplatesAsync()
@@ -124,36 +135,6 @@ public async Task AddDefaultTemplatesAsync()
}
}
- private static string GetManialinkTemplateName(string[] namespaceParts, string[] nameComponents)
- {
- var index = 0;
- while (index < namespaceParts.Length &&
- nameComponents[index].Equals(namespaceParts[index], StringComparison.Ordinal))
- {
- index++;
- }
-
- if (nameComponents[index].Equals("Templates", StringComparison.Ordinal))
- {
- index++;
- }
-
- var templateName = $"EvoSC.{string.Join(".", nameComponents[index..^1])}";
- return templateName;
- }
-
- private MultiCall CreateMultiCall(IEnumerable players, string manialinkOutput)
- {
- var multiCall = new MultiCall();
-
- foreach (var player in players)
- {
- multiCall.Add("SendDisplayManialinkPageToLogin", player.GetLogin(), manialinkOutput, 0, false);
- }
-
- return multiCall;
- }
-
public void AddTemplate(IManialinkTemplateInfo template)
{
if (_templates.ContainsKey(template.Name))
@@ -200,52 +181,16 @@ public void RemoveManiaScript(string name)
_scripts.Remove(name);
}
- private IEnumerable PrepareRender(string name)
- {
- if (!_templates.ContainsKey(name))
- {
- throw new InvalidOperationException($"Template '{name}' not found.");
- }
-
- var assemblies = new List();
- assemblies.AddRange(s_defaultAssemblies);
- assemblies.AddRange(_templates[name].Assemblies);
-
- return assemblies;
- }
-
- private 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)
- {
- 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();
- }
-
public async Task SendManialinkAsync(string name, IDictionary data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
await _server.Remote.SendDisplayManialinkPageAsync(manialinkOutput, 0, false);
}
public async Task SendManialinkAsync(string name, dynamic data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
await _server.Remote.SendDisplayManialinkPageAsync(manialinkOutput, 0, false);
}
@@ -254,6 +199,7 @@ public async Task SendManialinkAsync(string name, dynamic data)
public async Task SendPersistentManialinkAsync(string name, IDictionary data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
await _server.Remote.SendDisplayManialinkPageAsync(manialinkOutput, 0, false);
_persistentManialinks[name] = manialinkOutput;
@@ -261,6 +207,7 @@ public async Task SendPersistentManialinkAsync(string name, IDictionary data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
await _server.Remote.SendDisplayManialinkPageToLoginAsync(player.GetLogin(), manialinkOutput, 0, false);
}
public async Task SendManialinkAsync(IPlayer player, string name, dynamic data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
await _server.Remote.SendDisplayManialinkPageToLoginAsync(player.GetLogin(), manialinkOutput, 0, false);
}
public async Task SendManialinkAsync(string playerLogin, string name, dynamic data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
await _server.Remote.SendDisplayManialinkPageToLoginAsync(playerLogin, manialinkOutput, 0, false);
}
public async Task SendManialinkAsync(IEnumerable players, string name, IDictionary data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
var multiCall = CreateMultiCall(players, manialinkOutput);
await _server.Remote.MultiCallAsync(multiCall);
@@ -295,6 +246,7 @@ public async Task SendManialinkAsync(IEnumerable players, string name,
public async Task SendManialinkAsync(IEnumerable players, string name, dynamic data)
{
+ name = GetEffectiveName(name);
var manialinkOutput = await PrepareAndRenderAsync(name, data);
var multiCall = CreateMultiCall(players, manialinkOutput);
await _server.Remote.MultiCallAsync(multiCall);
@@ -302,6 +254,7 @@ public async Task SendManialinkAsync(IEnumerable players, string name,
public Task HideManialinkAsync(string name)
{
+ name = GetEffectiveName(name);
_persistentManialinks.TryRemove(name, out _);
var manialinkOutput = CreateHideManialink(name);
return _server.Remote.SendDisplayManialinkPageAsync(manialinkOutput, 3, true);
@@ -309,6 +262,7 @@ public Task HideManialinkAsync(string name)
public Task HideManialinkAsync(IPlayer player, string name)
{
+ name = GetEffectiveName(name);
_persistentManialinks.TryRemove(name, out _);
var manialinkOutput = CreateHideManialink(name);
return _server.Remote.SendDisplayManialinkPageToLoginAsync(player.GetLogin(), manialinkOutput, 3, true);
@@ -316,12 +270,14 @@ public Task HideManialinkAsync(IPlayer player, string name)
public Task HideManialinkAsync(string playerLogin, string name)
{
+ name = GetEffectiveName(name);
var manialinkOutput = 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 multiCall = new MultiCall();
@@ -346,4 +302,118 @@ public async Task PreprocessAllAsync()
await _engine.PreProcessAsync(template.Name, assembles);
}
}
+
+ public void AddGlobalVariable(string name, T value) =>
+ _engine.GlobalVariables.AddOrUpdate(name, value, (_, _) => value);
+
+ public void RemoveGlobalVariable(string name)
+ {
+ if (_engine.GlobalVariables.ContainsKey(name))
+ {
+ _engine.GlobalVariables.Remove(name, out _);
+ }
+
+ throw new KeyNotFoundException($"Did not find global variable named '{name}'.");
+ }
+
+ public void ClearGlobalVariables() => _engine.GlobalVariables.Clear();
+
+ ///
+ /// Used to send persistent manialinks to newly connected players.
+ ///
+ private async Task HandlePlayerConnectAsync(object sender, PlayerConnectGbxEventArgs e)
+ {
+ try
+ {
+ foreach (var (_, output) in _persistentManialinks)
+ {
+ await _server.Remote.SendDisplayManialinkPageToLoginAsync(e.Login, output, 0, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to send persistent manialink login '{Login}'. Did they leave already?",
+ e.Login);
+ }
+ }
+
+ private Task HandleThemeActivatedAsync(object sender, ThemeUpdatedEventArgs e)
+ {
+ _engine.GlobalVariables["Theme"] = _themeManager.Theme;
+
+ return Task.CompletedTask;
+ }
+
+ private static string GetManialinkTemplateName(string[] namespaceParts, string[] nameComponents)
+ {
+ var index = 0;
+ while (index < namespaceParts.Length &&
+ nameComponents[index].Equals(namespaceParts[index], StringComparison.Ordinal))
+ {
+ index++;
+ }
+
+ if (nameComponents[index].Equals("Templates", StringComparison.Ordinal))
+ {
+ index++;
+ }
+
+ var templateName = $"EvoSC.{string.Join(".", nameComponents[index..^1])}";
+ return templateName;
+ }
+
+ private MultiCall CreateMultiCall(IEnumerable players, string manialinkOutput)
+ {
+ var multiCall = new MultiCall();
+
+ foreach (var player in players)
+ {
+ multiCall.Add("SendDisplayManialinkPageToLogin", player.GetLogin(), manialinkOutput, 0, false);
+ }
+
+ return multiCall;
+ }
+
+ private IEnumerable PrepareRender(string name)
+ {
+ if (!_templates.ContainsKey(name))
+ {
+ throw new InvalidOperationException($"Template '{name}' not found.");
+ }
+
+ var assemblies = new List();
+ assemblies.AddRange(s_defaultAssemblies);
+ assemblies.AddRange(_templates[name].Assemblies);
+
+ return assemblies;
+ }
+
+ private 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)
+ {
+ 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) =>
+ _themeManager.ComponentReplacements.TryGetValue(name, out var effectiveName)
+ ? effectiveName
+ : name;
}
diff --git a/src/EvoSC.Manialinks/Templates/Controls/Alert.mt b/src/EvoSC.Manialinks/Templates/Controls/Alert.mt
index 0a0945901..dc8372f8c 100644
--- a/src/EvoSC.Manialinks/Templates/Controls/Alert.mt
+++ b/src/EvoSC.Manialinks/Templates/Controls/Alert.mt
@@ -14,11 +14,11 @@
-
-
-
+
+
+
@@ -42,13 +42,13 @@
-
-
+
+
@@ -60,17 +60,13 @@
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/src/EvoSC.Manialinks/Templates/Controls/Checkbox.mt b/src/EvoSC.Manialinks/Templates/Controls/Checkbox.mt
index a4414b361..8b49e5bc1 100644
--- a/src/EvoSC.Manialinks/Templates/Controls/Checkbox.mt
+++ b/src/EvoSC.Manialinks/Templates/Controls/Checkbox.mt
@@ -19,12 +19,6 @@
-
-
-
-
-
-
@@ -38,13 +32,11 @@
pos="0.1 -0.1"
scriptevents="1"
opacity="{{ isChecked ? 1 : 0 }}"
- bgcolorfocus='{{ isChecked ? "ff0058" : "00000000" }}'
+ bgcolorfocus='{{ isChecked ? Theme.UI_Checkbox_Default_Bg : "00000000" }}'
/>
+
+
@@ -27,15 +29,15 @@
textsize="1"
halign="left" valign="top"
/>
-