diff --git a/Src/RandomizerTMF.Logic/RandomizerEngine.cs b/Src/RandomizerTMF.Logic/RandomizerEngine.cs index b4f807a..4cf2548 100644 --- a/Src/RandomizerTMF.Logic/RandomizerEngine.cs +++ b/Src/RandomizerTMF.Logic/RandomizerEngine.cs @@ -1,17 +1,19 @@ using GBX.NET; using GBX.NET.Engines.Game; +using GBX.NET.Exceptions; using Microsoft.Extensions.Logging; using RandomizerTMF.Logic.Exceptions; using RandomizerTMF.Logic.TypeConverters; using System.Collections.Concurrent; using System.Diagnostics; using System.Net; +using System.Text.RegularExpressions; using TmEssentials; using YamlDotNet.Serialization; namespace RandomizerTMF.Logic; -public static class RandomizerEngine +public static partial class RandomizerEngine { private static readonly int requestMaxAttempts = 10; private static int requestAttempt; @@ -109,6 +111,9 @@ private set public static StreamWriter LogWriter { get; private set; } public static StreamWriter? CurrentSessionLogWriter { get; private set; } public static bool SessionEnding { get; private set; } + + [GeneratedRegex("[^a-zA-Z0-9_.]+")] + private static partial Regex SpecialCharRegex(); static RandomizerEngine() { @@ -292,8 +297,8 @@ private static void UpdateSessionDataFromAutosave(string fullPath, SessionMap ma _ => "" } + replay.Time.ToTmString(useHundredths: true, useApostrophe: true); - var mapName = TextFormatter.Deformat(map.Map.MapName).Trim(); - + var mapName = SpecialCharRegex().Replace(TextFormatter.Deformat(map.Map.MapName).Trim(), "_"); + var replayFileFormat = string.IsNullOrWhiteSpace(Config.ReplayFileFormat) ? Constants.DefaultReplayFileFormat : Config.ReplayFileFormat; @@ -541,9 +546,13 @@ public static bool ScanAutosaves() anythingChanged = true; } - catch + catch (NotAGbxException) { - // Errors get lost currently + // do nothing + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Gbx error found in the Autosaves folder when reading the header."); } } @@ -775,22 +784,57 @@ public static void ValidateRules() throw new RuleValidationException("Time limit cannot be above 9:59:59"); } - if (Config.Rules.RequestRules.PrimaryType is EPrimaryType.Platform - && (Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) || Config.Rules.RequestRules.Site.HasFlag(ESite.Nations))) + foreach (var primaryType in Enum.GetValues()) { - throw new RuleValidationException("Platform is not valid with TMNF or Nations Exchange"); + if (primaryType is EPrimaryType.Race) + { + continue; + } + + if (Config.Rules.RequestRules.PrimaryType == primaryType + && (Config.Rules.RequestRules.Site == ESite.Any + || Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) || Config.Rules.RequestRules.Site.HasFlag(ESite.Nations))) + { + throw new RuleValidationException($"{primaryType} is not valid with TMNF or Nations Exchange"); + } } - if (Config.Rules.RequestRules.PrimaryType is EPrimaryType.Stunts - && (Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) || Config.Rules.RequestRules.Site.HasFlag(ESite.Nations))) + if (Config.Rules.RequestRules.Environment is not null || Config.Rules.RequestRules.Vehicle is not null) { - throw new RuleValidationException("Stunts is not valid with TMNF or Nations Exchange"); + foreach (var env in Enum.GetValues()) + { + if (env is EEnvironment.Stadium) + { + continue; + } + + if (Config.Rules.RequestRules.Site != ESite.Any && !Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) && !Config.Rules.RequestRules.Site.HasFlag(ESite.Nations)) + { + continue; + } + + if (Config.Rules.RequestRules.Environment?.Contains(env) == true) + { + throw new RuleValidationException($"{env} is not valid with TMNF or Nations Exchange"); + } + + if (Config.Rules.RequestRules.Vehicle?.Contains(env) == true) + { + throw new RuleValidationException($"{env}Car is not valid with TMNF or Nations Exchange"); + } + } + } + + if (Config.Rules.RequestRules.EqualEnvironmentDistribution + && Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) || Config.Rules.RequestRules.Site.HasFlag(ESite.Nations)) + { + throw new RuleValidationException($"Equal environment distribution is not valid with TMNF or Nations Exchange"); } - if (Config.Rules.RequestRules.PrimaryType is EPrimaryType.Puzzle - && (Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) || Config.Rules.RequestRules.Site.HasFlag(ESite.Nations))) + if (Config.Rules.RequestRules.EqualVehicleDistribution + && Config.Rules.RequestRules.Site.HasFlag(ESite.TMNF) || Config.Rules.RequestRules.Site.HasFlag(ESite.Nations)) { - throw new RuleValidationException("Puzzle is not valid with TMNF or Nations Exchange"); + throw new RuleValidationException($"Equal vehicle distribution is not valid with TMNF or Nations Exchange"); } } @@ -848,7 +892,7 @@ private static void SetReadOnlySessionYml() private static async Task PrepareNewMapAsync(CancellationToken cancellationToken) { Status("Fetching random track..."); - + // Randomized URL is constructed with the ToUrl() method. var requestUrl = Config.Rules.RequestRules.ToUrl(); @@ -993,6 +1037,8 @@ private static async Task PrepareNewMapAsync(CancellationToken cancellationToken SaveSessionData(); // May not be super necessary? } + + /// /// Handles the play loop of a map. Throws cancellation exception on session end (not the map end). /// diff --git a/Src/RandomizerTMF.Logic/RandomizerTMF.Logic.csproj b/Src/RandomizerTMF.Logic/RandomizerTMF.Logic.csproj index 38a6e01..2979829 100644 --- a/Src/RandomizerTMF.Logic/RandomizerTMF.Logic.csproj +++ b/Src/RandomizerTMF.Logic/RandomizerTMF.Logic.csproj @@ -1,7 +1,7 @@ - 1.0.1 + 1.0.2 net7.0 enable enable diff --git a/Src/RandomizerTMF.Logic/RequestRules.cs b/Src/RandomizerTMF.Logic/RequestRules.cs index bb63bf8..926c820 100644 --- a/Src/RandomizerTMF.Logic/RequestRules.cs +++ b/Src/RandomizerTMF.Logic/RequestRules.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Diagnostics; +using System.Reflection; using System.Text; using TmEssentials; @@ -7,7 +8,14 @@ namespace RandomizerTMF.Logic; public class RequestRules { + private static readonly ESite[] siteValues = Enum.GetValues(); + private static readonly EEnvironment[] envValues = Enum.GetValues(); + + // Custom rules that are not part of the official API + public required ESite Site { get; set; } + public bool EqualEnvironmentDistribution { get; set; } + public bool EqualVehicleDistribution { get; set; } public string? Author { get; set; } public HashSet? Environment { get; set; } @@ -40,12 +48,12 @@ public class RequestRules { var b = new StringBuilder("https://"); - var matchingSites = Enum.GetValues() + var matchingSites = siteValues .Where(x => x != ESite.Any && (Site & x) == x) .ToArray(); var siteUrl = GetSiteUrl(matchingSites.Length == 0 - ? Enum.GetValues().Where(x => x is not ESite.Any).ToArray() + ? siteValues.Where(x => x is not ESite.Any).ToArray() : matchingSites); b.Append(siteUrl); @@ -54,9 +62,21 @@ public class RequestRules var first = true; - foreach (var prop in GetType().GetProperties().Where(x => x.Name != nameof(Site))) + foreach (var prop in GetType().GetProperties().Where(DoesNotSkip)) { - if (prop.GetValue(this) is not object val || val is null || (val is IEnumerable enumerable && !enumerable.Cast().Any())) + var val = prop.GetValue(this); + + if (EqualEnvironmentDistribution && prop.Name == nameof(Environment)) + { + val = GetRandomEnvironmentThroughSet(Environment); + } + + if (EqualVehicleDistribution && prop.Name == nameof(Vehicle)) + { + val = GetRandomEnvironmentThroughSet(Vehicle); + } + + if (val is null || (val is IEnumerable enumerable && !enumerable.Cast().Any())) { continue; } @@ -89,6 +109,28 @@ public class RequestRules return b.ToString(); } + private bool DoesNotSkip(PropertyInfo prop) + { + return prop.Name is not nameof(Site) + and not nameof(EqualEnvironmentDistribution) + and not nameof(EqualVehicleDistribution); + } + + private static EEnvironment GetRandomEnvironment(HashSet? container) + { + if (container is null || container.Count == 0) + { + return (EEnvironment)Random.Shared.Next(0, envValues.Length); // Safe in case of EEnvironment + } + + return container.ElementAt(Random.Shared.Next(0, container.Count)); + } + + private static HashSet GetRandomEnvironmentThroughSet(HashSet? container) + { + return new HashSet() { GetRandomEnvironment(container) }; + } + private static string GetSiteUrl(ESite[] matchingSites) { var randomSite = matchingSites[Random.Shared.Next(matchingSites.Length)]; diff --git a/Src/RandomizerTMF/Models/SessionDataMapModel.cs b/Src/RandomizerTMF/Models/SessionDataMapModel.cs index cccdb44..4d97347 100644 --- a/Src/RandomizerTMF/Models/SessionDataMapModel.cs +++ b/Src/RandomizerTMF/Models/SessionDataMapModel.cs @@ -32,10 +32,6 @@ public SessionDataMapModel(SessionDataMap map) public void TmxClick() { - Process.Start(new ProcessStartInfo - { - FileName = Map.TmxLink, - UseShellExecute = true - }); + ProcessUtils.OpenUrl(Map.TmxLink); } } diff --git a/Src/RandomizerTMF/Models/SessionDataReplayModel.cs b/Src/RandomizerTMF/Models/SessionDataReplayModel.cs index 3784c82..2ed8009 100644 --- a/Src/RandomizerTMF/Models/SessionDataReplayModel.cs +++ b/Src/RandomizerTMF/Models/SessionDataReplayModel.cs @@ -35,7 +35,7 @@ public SessionDataReplayModel(SessionDataReplay replay, string sessionStr, bool Replay = replay; var path = Path.Combine(Constants.Sessions, sessionStr, Constants.Replays, replay.FileName); - var node = GameBox.ParseNode(path.Replace("\uFEFF", "")); + var node = GameBox.ParseNode(path); if (first) { diff --git a/Src/RandomizerTMF/ProcessUtils.cs b/Src/RandomizerTMF/ProcessUtils.cs new file mode 100644 index 0000000..6f95d6b --- /dev/null +++ b/Src/RandomizerTMF/ProcessUtils.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +namespace RandomizerTMF; + +public static class ProcessUtils +{ + public static void OpenUrl(string url) + { + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + + public static void OpenDir(string dirPath) + { + Process.Start(new ProcessStartInfo() + { + FileName = dirPath, + UseShellExecute = true, + Verb = "open" + }); + } +} diff --git a/Src/RandomizerTMF/RandomizerTMF.csproj b/Src/RandomizerTMF/RandomizerTMF.csproj index c6a343d..d4e6849 100644 --- a/Src/RandomizerTMF/RandomizerTMF.csproj +++ b/Src/RandomizerTMF/RandomizerTMF.csproj @@ -20,7 +20,7 @@ - 1.0.1 + 1.0.2 true true diff --git a/Src/RandomizerTMF/ViewModels/AboutWindowViewModel.cs b/Src/RandomizerTMF/ViewModels/AboutWindowViewModel.cs index 9b676c7..8c35130 100644 --- a/Src/RandomizerTMF/ViewModels/AboutWindowViewModel.cs +++ b/Src/RandomizerTMF/ViewModels/AboutWindowViewModel.cs @@ -44,10 +44,6 @@ public void UpdateClick() private void OpenWeb(string site) { - Process.Start(new ProcessStartInfo - { - FileName = site, - UseShellExecute = true - }); + ProcessUtils.OpenUrl(site); } } diff --git a/Src/RandomizerTMF/ViewModels/DashboardWindowViewModel.cs b/Src/RandomizerTMF/ViewModels/DashboardWindowViewModel.cs index c922aae..314c253 100644 --- a/Src/RandomizerTMF/ViewModels/DashboardWindowViewModel.cs +++ b/Src/RandomizerTMF/ViewModels/DashboardWindowViewModel.cs @@ -274,7 +274,7 @@ public void OpenDownloadedMapsFolderClick() { if (RandomizerEngine.DownloadedDirectoryPath is not null) { - OpenFolder(RandomizerEngine.DownloadedDirectoryPath + Path.DirectorySeparatorChar); + ProcessUtils.OpenDir(RandomizerEngine.DownloadedDirectoryPath + Path.DirectorySeparatorChar); } } @@ -282,17 +282,7 @@ public void OpenSessionsFolderClick() { if (RandomizerEngine.SessionsDirectoryPath is not null) { - OpenFolder(RandomizerEngine.SessionsDirectoryPath + Path.DirectorySeparatorChar); + ProcessUtils.OpenDir(RandomizerEngine.SessionsDirectoryPath + Path.DirectorySeparatorChar); } } - - private static void OpenFolder(string folderPath) - { - Process.Start(new ProcessStartInfo() - { - FileName = folderPath, - UseShellExecute = true, - Verb = "open" - }); - } } diff --git a/Src/RandomizerTMF/ViewModels/RequestRulesControlViewModel.cs b/Src/RandomizerTMF/ViewModels/RequestRulesControlViewModel.cs index 6106d00..cd79914 100644 --- a/Src/RandomizerTMF/ViewModels/RequestRulesControlViewModel.cs +++ b/Src/RandomizerTMF/ViewModels/RequestRulesControlViewModel.cs @@ -926,5 +926,31 @@ public bool MaxATEnabled } } + public bool EqualEnvDistribution + { + get => RandomizerEngine.Config.Rules.RequestRules.EqualEnvironmentDistribution; + set + { + RandomizerEngine.Config.Rules.RequestRules.EqualEnvironmentDistribution = value; + + this.RaisePropertyChanged(nameof(EqualEnvDistribution)); + + RandomizerEngine.SaveConfig(); + } + } + + public bool EqualVehicleDistribution + { + get => RandomizerEngine.Config.Rules.RequestRules.EqualVehicleDistribution; + set + { + RandomizerEngine.Config.Rules.RequestRules.EqualVehicleDistribution = value; + + this.RaisePropertyChanged(nameof(EqualVehicleDistribution)); + + RandomizerEngine.SaveConfig(); + } + } + public void UploadedBeforeReset() => UploadedBefore = null; } diff --git a/Src/RandomizerTMF/ViewModels/SessionDataViewModel.cs b/Src/RandomizerTMF/ViewModels/SessionDataViewModel.cs index 42245c8..81e7690 100644 --- a/Src/RandomizerTMF/ViewModels/SessionDataViewModel.cs +++ b/Src/RandomizerTMF/ViewModels/SessionDataViewModel.cs @@ -160,7 +160,7 @@ public void OpenSessionFolderClick() { if (RandomizerEngine.SessionsDirectoryPath is not null) { - OpenFolder(Path.Combine(RandomizerEngine.SessionsDirectoryPath, Model.Data.StartedAtText) + Path.DirectorySeparatorChar); + ProcessUtils.OpenDir(Path.Combine(RandomizerEngine.SessionsDirectoryPath, Model.Data.StartedAtText) + Path.DirectorySeparatorChar); } } @@ -172,21 +172,11 @@ public void OpenReplaysFolderClick() if (Directory.Exists(replaysDir)) { - OpenFolder(replaysDir + Path.DirectorySeparatorChar); + ProcessUtils.OpenDir(replaysDir + Path.DirectorySeparatorChar); } } } - private static void OpenFolder(string folderPath) - { - Process.Start(new ProcessStartInfo() - { - FileName = folderPath, - UseShellExecute = true, - Verb = "open" - }); - } - [GeneratedRegex("[a-z][A-Z]")] private static partial Regex SentenceCaseRegex(); diff --git a/Src/RandomizerTMF/ViewModels/SessionMapViewModel.cs b/Src/RandomizerTMF/ViewModels/SessionMapViewModel.cs index 0d6edc2..396e9ff 100644 --- a/Src/RandomizerTMF/ViewModels/SessionMapViewModel.cs +++ b/Src/RandomizerTMF/ViewModels/SessionMapViewModel.cs @@ -25,10 +25,6 @@ public SessionMapViewModel(PlayedMapModel model) public void VisitOnTmxClick() { - Process.Start(new ProcessStartInfo - { - FileName = Model.Map.TmxLink, - UseShellExecute = true - }); + ProcessUtils.OpenUrl(Model.Map.TmxLink); } } diff --git a/Src/RandomizerTMF/ViewModels/TopBarViewModel.cs b/Src/RandomizerTMF/ViewModels/TopBarViewModel.cs index 80dd2fb..a066ab2 100644 --- a/Src/RandomizerTMF/ViewModels/TopBarViewModel.cs +++ b/Src/RandomizerTMF/ViewModels/TopBarViewModel.cs @@ -48,14 +48,10 @@ public void OnMinimizeClick() { MinimizeClick?.Invoke(); } - + public void DonateClick() { - Process.Start(new ProcessStartInfo - { - FileName = "https://paypal.me/bigbang1112", - UseShellExecute = true - }); + ProcessUtils.OpenUrl("https://paypal.me/bigbang1112"); } public void VersionClick() diff --git a/Src/RandomizerTMF/Views/DashboardWindow.axaml b/Src/RandomizerTMF/Views/DashboardWindow.axaml index 498bfb9..3486c90 100644 --- a/Src/RandomizerTMF/Views/DashboardWindow.axaml +++ b/Src/RandomizerTMF/Views/DashboardWindow.axaml @@ -9,8 +9,8 @@ Icon="/Assets/icon.ico" Width="1250" MinWidth="1180" - Height="685" - MinHeight="645" + Height="700" + MinHeight="680" WindowStartupLocation="CenterScreen" Title="Randomizer TMF - Random Map Challenge for TMNF/TMUF"> diff --git a/Src/RandomizerTMF/Views/RequestRulesControl.axaml b/Src/RandomizerTMF/Views/RequestRulesControl.axaml index 71e5e97..587f088 100644 --- a/Src/RandomizerTMF/Views/RequestRulesControl.axaml +++ b/Src/RandomizerTMF/Views/RequestRulesControl.axaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="530" + mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="580" xmlns:views="using:RandomizerTMF.Views" xmlns:vm="using:RandomizerTMF.ViewModels" x:Class="RandomizerTMF.Views.RequestRulesControl"> @@ -169,6 +169,8 @@ + + Time limit: @@ -228,6 +230,15 @@