diff --git a/EvoSC.sln b/EvoSC.sln index e68f57b82..d4eb28d4b 100644 --- a/EvoSC.sln +++ b/EvoSC.sln @@ -118,11 +118,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalRecordsModule.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForceTeamModule", "src/Modules/ForceTeamModule/ForceTeamModule.csproj", "{ACFF2B23-0518-493C-93A0-08CF9FD4F8E6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForceTeamModule.Tests", "tests\Modules\ForceTeamModule.Tests\ForceTeamModule.Tests.csproj", "{DE9532E8-F0ED-400B-9592-AF8F74BC83EB}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamSettingsModule", "src\Modules\TeamSettingsModule\TeamSettingsModule.csproj", "{C10A11E5-4AD8-4229-81F1-57D7DC364E27}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamSettingsModule.Tests", "tests\Modules\TeamSettingsModule.Tests\TeamSettingsModule.Tests.csproj", "{B1745099-0081-4443-BF79-26A888765E16}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ForceTeamModule.Tests", "tests\Modules\ForceTeamModule.Tests\ForceTeamModule.Tests.csproj", "{DE9532E8-F0ED-400B-9592-AF8F74BC83EB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamInfoModule", "src\Modules\TeamInfoModule\TeamInfoModule.csproj", "{5E178B0D-5F29-4990-8DCF-DC7FF113C4F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamInfoModule.Tests", "tests\Modules\TeamInfoModule.Tests\TeamInfoModule.Tests.csproj", "{71D8BDA5-6787-471A-8064-CEEE4203EBEF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerManagementModule", "src/Modules/ServerManagementModule/ServerManagementModule.csproj", "{AAE1AC39-4C4B-48A9-8B7B-08D2CD461FC1}" EndProject @@ -379,6 +383,14 @@ Global {F8C7FE5E-B389-4BA8-B0DF-6D9D0A9B0949}.Debug|Any CPU.Build.0 = Debug|Any CPU {F8C7FE5E-B389-4BA8-B0DF-6D9D0A9B0949}.Release|Any CPU.ActiveCfg = Release|Any CPU {F8C7FE5E-B389-4BA8-B0DF-6D9D0A9B0949}.Release|Any CPU.Build.0 = Release|Any CPU + {5E178B0D-5F29-4990-8DCF-DC7FF113C4F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E178B0D-5F29-4990-8DCF-DC7FF113C4F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E178B0D-5F29-4990-8DCF-DC7FF113C4F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E178B0D-5F29-4990-8DCF-DC7FF113C4F0}.Release|Any CPU.Build.0 = Release|Any CPU + {71D8BDA5-6787-471A-8064-CEEE4203EBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71D8BDA5-6787-471A-8064-CEEE4203EBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71D8BDA5-6787-471A-8064-CEEE4203EBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71D8BDA5-6787-471A-8064-CEEE4203EBEF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -433,9 +445,11 @@ Global {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} - {ACFF2B23-0518-493C-93A0-08CF9FD4F8E6} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {C10A11E5-4AD8-4229-81F1-57D7DC364E27} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {B1745099-0081-4443-BF79-26A888765E16} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} + {5E178B0D-5F29-4990-8DCF-DC7FF113C4F0} = {DC47658A-F421-4BA4-B617-090A7DFB3900} + {71D8BDA5-6787-471A-8064-CEEE4203EBEF} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} + {ACFF2B23-0518-493C-93A0-08CF9FD4F8E6} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {DE9532E8-F0ED-400B-9592-AF8F74BC83EB} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} {AAE1AC39-4C4B-48A9-8B7B-08D2CD461FC1} = {DC47658A-F421-4BA4-B617-090A7DFB3900} {F8C7FE5E-B389-4BA8-B0DF-6D9D0A9B0949} = {6D75D6A2-6ECD-4DE4-96C5-CAD7D134407A} diff --git a/src/EvoSC/EvoSC.csproj b/src/EvoSC/EvoSC.csproj index f805432f5..89c709a3e 100644 --- a/src/EvoSC/EvoSC.csproj +++ b/src/EvoSC/EvoSC.csproj @@ -50,6 +50,7 @@ + @@ -69,5 +70,4 @@ - diff --git a/src/EvoSC/InternalModules.cs b/src/EvoSC/InternalModules.cs index 524043756..b0ee64b76 100644 --- a/src/EvoSC/InternalModules.cs +++ b/src/EvoSC/InternalModules.cs @@ -23,6 +23,7 @@ using EvoSC.Modules.Official.ServerManagementModule; using EvoSC.Modules.Official.SetName; using EvoSC.Modules.Official.SpectatorTargetInfoModule; +using EvoSC.Modules.Official.TeamInfoModule; using EvoSC.Modules.Official.TeamSettingsModule; using EvoSC.Modules.Official.WorldRecordModule; using FluentMigrator.Runner.Exceptions; @@ -57,7 +58,8 @@ public static class InternalModules typeof(LocalRecordsModule), typeof(ForceTeamModule), typeof(TeamSettingsModule), - typeof(ServerManagementModule) + typeof(ServerManagementModule), + typeof(TeamInfoModule) ]; /// diff --git a/src/Modules/TeamInfoModule/Config/ITeamInfoSettings.cs b/src/Modules/TeamInfoModule/Config/ITeamInfoSettings.cs new file mode 100644 index 000000000..2e9293ed7 --- /dev/null +++ b/src/Modules/TeamInfoModule/Config/ITeamInfoSettings.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using Config.Net; +using EvoSC.Modules.Attributes; + +namespace EvoSC.Modules.Official.TeamInfoModule.Config; + +[Settings] +public interface ITeamInfoSettings +{ + [Option(DefaultValue = 0.0), Description("Specifies the horizontal center position of the widget.")] + public double X { get; set; } + + [Option(DefaultValue = 80.0), Description("Specifies the vertical top edge position of the wigdet. Values from -90 to 90 allowed.")] + public double Y { get; set; } + + [Option(DefaultValue = false), Description("If enabled a smaller version of the widget is shown, without the large team name.")] + public bool CompactMode { get; set; } +} diff --git a/src/Modules/TeamInfoModule/Controllers/TeamInfoEventController.cs b/src/Modules/TeamInfoModule/Controllers/TeamInfoEventController.cs new file mode 100644 index 000000000..cd0032f07 --- /dev/null +++ b/src/Modules/TeamInfoModule/Controllers/TeamInfoEventController.cs @@ -0,0 +1,81 @@ +using EvoSC.Common.Controllers; +using EvoSC.Common.Controllers.Attributes; +using EvoSC.Common.Events.Attributes; +using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Models; +using EvoSC.Common.Remote; +using EvoSC.Common.Remote.EventArgsModels; +using EvoSC.Modules.Official.TeamInfoModule.Interfaces; +using GbxRemoteNet.Events; + +namespace EvoSC.Modules.Official.TeamInfoModule.Controllers; + +[Controller] +public class TeamInfoEventController(ITeamInfoService teamInfoService) : EvoScController +{ + [Subscribe(ModeScriptEvent.Scores)] + public async Task OnScoresAsync(object sender, ScoresEventArgs eventArgs) + { + var isTeamsModeActive = await teamInfoService.GetModeIsTeamsAsync(); + + if (!eventArgs.UseTeams) + { + if (!isTeamsModeActive) + { + return; + } + + await teamInfoService.SetModeIsTeamsAsync(false); + await teamInfoService.HideTeamInfoWidgetEveryoneAsync(); + + return; + } + + if (!isTeamsModeActive) + { + await teamInfoService.SetModeIsTeamsAsync(true); + } + + var teamInfos = eventArgs.Teams.ToList(); + var team1Points = teamInfos[0]!.MatchPoints; + var team2Points = teamInfos[1]!.MatchPoints; + + if (eventArgs.Section is ModeScriptSection.EndRound or ModeScriptSection.Undefined) + { + await teamInfoService.UpdatePointsAsync(team1Points, team2Points); + } + } + + [Subscribe(ModeScriptEvent.StartRoundStart)] + public async Task OnRoundStartAsync(object sender, RoundEventArgs args) + { + if (!await teamInfoService.GetModeIsTeamsAsync()) + { + return; + } + + await teamInfoService.UpdateRoundNumberAsync(args.Count); + } + + [Subscribe(ModeScriptEvent.PodiumStart)] + public async Task OnPodiumStartAsync(object sender, PodiumEventArgs args) + { + if (!await teamInfoService.GetModeIsTeamsAsync()) + { + return; + } + + await teamInfoService.HideTeamInfoWidgetEveryoneAsync(); + } + + [Subscribe(GbxRemoteEvent.EndMap)] + public async Task OnEndMapAsync(object sender, MapGbxEventArgs args) + { + if (!await teamInfoService.GetModeIsTeamsAsync()) + { + return; + } + + await teamInfoService.HideTeamInfoWidgetEveryoneAsync(); + } +} diff --git a/src/Modules/TeamInfoModule/Interfaces/ITeamInfoService.cs b/src/Modules/TeamInfoModule/Interfaces/ITeamInfoService.cs new file mode 100644 index 000000000..7c9dd524b --- /dev/null +++ b/src/Modules/TeamInfoModule/Interfaces/ITeamInfoService.cs @@ -0,0 +1,81 @@ +using EvoSC.Common.Util.MatchSettings.Models.ModeScriptSettingsModels; + +namespace EvoSC.Modules.Official.TeamInfoModule.Interfaces; + +public interface ITeamInfoService +{ + /// + /// Triggers detection if team mode is active. + /// + /// + public Task InitializeModuleAsync(); + + /// + /// Gets the current mode script settings for teams mode. + /// + /// + public Task GetModeScriptTeamSettingsAsync(); + + /// + /// Get the text displayed at the bottom of the team info widget. + /// + /// + /// + public Task GetInfoBoxTextAsync(TeamsModeScriptSettings modeScriptTeamSettings); + + /// + /// Gets all necessary data for the widget. + /// + /// + public Task GetWidgetDataAsync(); + + /// + /// Determines whether a team has match point. + /// + /// + /// + /// + /// + /// + public Task DoesTeamHaveMatchPointAsync(int teamPoints, int opponentPoints, int? pointsLimit, int? pointsGap); + + /// + /// Sends the team info widget to all players. + /// + /// + public Task SendTeamInfoWidgetEveryoneAsync(); + + /// + /// Hides the team info widget for all players. + /// + /// + public Task HideTeamInfoWidgetEveryoneAsync(); + + /// + /// Sets the current round number. + /// + /// + /// + public Task UpdateRoundNumberAsync(int round); + + /// + /// Updates the points for both teams. + /// + /// + /// + /// + public Task UpdatePointsAsync(int team1Points, int team2Points); + + /// + /// Tells whether the service is currently active for teams mode. + /// + /// + public Task GetModeIsTeamsAsync(); + + /// + /// Sets whether the service is active for teams mode or not. + /// + /// + /// + public Task SetModeIsTeamsAsync(bool modeIsTeams); +} diff --git a/src/Modules/TeamInfoModule/Localization.resx b/src/Modules/TeamInfoModule/Localization.resx new file mode 100644 index 000000000..02cf34b4b --- /dev/null +++ b/src/Modules/TeamInfoModule/Localization.resx @@ -0,0 +1,19 @@ + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Services/TeamInfoService.cs b/src/Modules/TeamInfoModule/Services/TeamInfoService.cs new file mode 100644 index 000000000..9bc9d02f4 --- /dev/null +++ b/src/Modules/TeamInfoModule/Services/TeamInfoService.cs @@ -0,0 +1,146 @@ +using EvoSC.Common.Interfaces; +using EvoSC.Common.Services.Attributes; +using EvoSC.Common.Services.Models; +using EvoSC.Common.Util.MatchSettings.Models.ModeScriptSettingsModels; +using EvoSC.Manialinks.Interfaces; +using EvoSC.Modules.Official.TeamInfoModule.Config; +using EvoSC.Modules.Official.TeamInfoModule.Interfaces; +using LinqToDB.Common; + +namespace EvoSC.Modules.Official.TeamInfoModule.Services; + +[Service(LifeStyle = ServiceLifeStyle.Singleton)] +public class TeamInfoService( + IServerClient server, + IManialinkManager manialinks, + ITeamInfoSettings settings +) : ITeamInfoService +{ + private const string WidgetTemplate = "TeamInfoModule.TeamInfoWidget"; + + private bool _modeIsTeams; + private int _currentRound; + private int _team1Points; + private int _team2Points; + + public async Task InitializeModuleAsync() + { + await server.Remote.TriggerModeScriptEventArrayAsync("Trackmania.GetScores"); + } + + public async Task GetWidgetDataAsync() + { + var modeScriptSettings = await GetModeScriptTeamSettingsAsync(); + var infoBoxText = await GetInfoBoxTextAsync(modeScriptSettings); + var team1 = await server.Remote.GetTeamInfoAsync(1); + var team2 = await server.Remote.GetTeamInfoAsync(2); + var team1MatchPoint = await DoesTeamHaveMatchPointAsync(_team1Points, _team2Points, + modeScriptSettings.PointsLimit, + modeScriptSettings.PointsGap); + var team2MatchPoint = await DoesTeamHaveMatchPointAsync(_team2Points, _team1Points, + modeScriptSettings.PointsLimit, + modeScriptSettings.PointsGap); + + return new + { + team1, + team2, + infoBoxText, + team1MatchPoint, + team2MatchPoint, + settings, + roundNumber = _currentRound, + team1Points = _team1Points, + team2Points = _team2Points, + neutralEmblemUrl = modeScriptSettings.NeutralEmblemUrl + }; + } + + public async Task GetModeScriptTeamSettingsAsync() + { + var modeScriptSettings = await server.Remote.GetModeScriptSettingsAsync(); + + return new TeamsModeScriptSettings + { + PointsLimit = (int)modeScriptSettings.GetValueOrDefault("S_PointsLimit", 5), + FinishTimeout = (int)modeScriptSettings.GetValueOrDefault("S_FinishTimeout", -1), + MaxPointsPerRound = (int)modeScriptSettings.GetValueOrDefault("S_MaxPointsPerRound", 6), + PointsGap = (int)modeScriptSettings.GetValueOrDefault("S_PointsGap", 1), + RoundsPerMap = (int)modeScriptSettings.GetValueOrDefault("S_RoundsPerMap", -1), + MapsPerMatch = (int)modeScriptSettings.GetValueOrDefault("S_MapsPerMatch", -1), + NeutralEmblemUrl = (string)modeScriptSettings.GetValueOrDefault("S_NeutralEmblemUrl", "") + }; + } + + public Task GetInfoBoxTextAsync(TeamsModeScriptSettings modeScriptTeamSettings) + { + var infoBoxText = new List(); + + //Add point limit and gap + if (modeScriptTeamSettings.PointsLimit > 0) + { + infoBoxText.Add("FIRST TO " + modeScriptTeamSettings.PointsLimit); + + if (modeScriptTeamSettings.PointsGap > 1) + { + infoBoxText.Add($"GAP {modeScriptTeamSettings.PointsGap}"); + } + } + + //Add rounds per map + if (modeScriptTeamSettings.RoundsPerMap > 0) + { + infoBoxText.Add($"{modeScriptTeamSettings.RoundsPerMap} ROUNDS/MAP"); + } + + return Task.FromResult(infoBoxText.IsNullOrEmpty() ? null : string.Join(" | ", infoBoxText)); + } + + public Task DoesTeamHaveMatchPointAsync(int teamPoints, int opponentPoints, int? pointsLimit, int? pointsGap) + { + if (pointsLimit == null) + { + return Task.FromResult(false); + } + + pointsGap ??= 1; + + return Task.FromResult(teamPoints >= pointsLimit - 1 && + teamPoints - (pointsGap - 1) >= opponentPoints); + } + + public async Task SendTeamInfoWidgetEveryoneAsync() + { + await manialinks.SendPersistentManialinkAsync(WidgetTemplate, await GetWidgetDataAsync()); + } + + public async Task HideTeamInfoWidgetEveryoneAsync() + { + await manialinks.HideManialinkAsync(WidgetTemplate); + } + + public async Task UpdateRoundNumberAsync(int round) + { + _currentRound = round; + await SendTeamInfoWidgetEveryoneAsync(); + } + + public async Task UpdatePointsAsync(int team1Points, int team2Points) + { + _team1Points = team1Points; + _team2Points = team2Points; + await SendTeamInfoWidgetEveryoneAsync(); + } + + public Task GetModeIsTeamsAsync() + { + return Task.FromResult(_modeIsTeams); + } + + public Task SetModeIsTeamsAsync(bool modeIsTeams) + { + _modeIsTeams = modeIsTeams; + + return Task.CompletedTask; + } +} diff --git a/src/Modules/TeamInfoModule/TeamInfoModule.cs b/src/Modules/TeamInfoModule/TeamInfoModule.cs new file mode 100644 index 000000000..6a068e297 --- /dev/null +++ b/src/Modules/TeamInfoModule/TeamInfoModule.cs @@ -0,0 +1,13 @@ +using EvoSC.Modules.Attributes; +using EvoSC.Modules.Interfaces; +using EvoSC.Modules.Official.TeamInfoModule.Interfaces; + +namespace EvoSC.Modules.Official.TeamInfoModule; + +[Module(IsInternal = true)] +public class TeamInfoModule(ITeamInfoService teamInfoService) : EvoScModule, IToggleable +{ + public Task EnableAsync() => teamInfoService.InitializeModuleAsync(); + + public Task DisableAsync() => Task.CompletedTask; +} diff --git a/src/Modules/TeamInfoModule/TeamInfoModule.csproj b/src/Modules/TeamInfoModule/TeamInfoModule.csproj new file mode 100644 index 000000000..2d4a34b53 --- /dev/null +++ b/src/Modules/TeamInfoModule/TeamInfoModule.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + EvoSC.Modules.Official.TeamInfoModule + + + + + + + + + ResXFileCodeGenerator + Localization.Designer.cs + + + diff --git a/src/Modules/TeamInfoModule/Templates/Components/BottomInfoBox.mt b/src/Modules/TeamInfoModule/Templates/Components/BottomInfoBox.mt new file mode 100644 index 000000000..5006b9c54 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/BottomInfoBox.mt @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/Components/EmblemBox.mt b/src/Modules/TeamInfoModule/Templates/Components/EmblemBox.mt new file mode 100644 index 000000000..ca0ed89ad --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/EmblemBox.mt @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/Components/GainedPoints.mt b/src/Modules/TeamInfoModule/Templates/Components/GainedPoints.mt new file mode 100644 index 000000000..3c5c2a287 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/GainedPoints.mt @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/Components/MatchPointBox.mt b/src/Modules/TeamInfoModule/Templates/Components/MatchPointBox.mt new file mode 100644 index 000000000..b8ea8a044 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/MatchPointBox.mt @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/Components/PointsBox.mt b/src/Modules/TeamInfoModule/Templates/Components/PointsBox.mt new file mode 100644 index 000000000..d6534cbf8 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/PointsBox.mt @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/Components/RoundCounter.mt b/src/Modules/TeamInfoModule/Templates/Components/RoundCounter.mt new file mode 100644 index 000000000..78d7e3ca4 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/RoundCounter.mt @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/Components/TeamFullName.mt b/src/Modules/TeamInfoModule/Templates/Components/TeamFullName.mt new file mode 100644 index 000000000..8c64165a1 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/Components/TeamFullName.mt @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Modules/TeamInfoModule/Templates/TeamInfoWidget.mt b/src/Modules/TeamInfoModule/Templates/TeamInfoWidget.mt new file mode 100644 index 000000000..1207bd0b6 --- /dev/null +++ b/src/Modules/TeamInfoModule/Templates/TeamInfoWidget.mt @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/TeamInfoModule/info.toml b/src/Modules/TeamInfoModule/info.toml new file mode 100644 index 000000000..cb09a0b8b --- /dev/null +++ b/src/Modules/TeamInfoModule/info.toml @@ -0,0 +1,11 @@ +[info] +# A unique name for this module, this is used as a identifier +name = "TeamInfoModule" +# The title of the module +title = "TeamInfo Module" +# A short description of what the module is and does +summary = "Displays teams and their points in a widget on the screen." +# The current version of this module, using SEMVER +version = "1.0.0" +# The name of the author that created this module +author = "Evo" diff --git a/src/Modules/TeamSettingsModule/Templates/Scripts/ColorInput.ms b/src/Modules/TeamSettingsModule/Templates/Scripts/ColorInput.ms index 52d8b309a..63e446ab9 100644 --- a/src/Modules/TeamSettingsModule/Templates/Scripts/ColorInput.ms +++ b/src/Modules/TeamSettingsModule/Templates/Scripts/ColorInput.ms @@ -35,7 +35,7 @@ if (Event.Control.ControlId == "picker") { declare entryFrame <=> (colorInputFrame.Controls[3] as CMlFrame); declare colorEntry <=> (entryFrame.Controls[1] as CMlEntry); - colorEntry.Value = ColorLib::RgbToHex(picker.Color); + colorEntry.Value = ColorLib::RgbToHex6(picker.Color); previewLabel.ModulateColor = picker.Color; } *** diff --git a/tests/Modules/TeamInfoModule.Tests/Controllers/TeamInfoEventControllerTests.cs b/tests/Modules/TeamInfoModule.Tests/Controllers/TeamInfoEventControllerTests.cs new file mode 100644 index 000000000..e62dfa126 --- /dev/null +++ b/tests/Modules/TeamInfoModule.Tests/Controllers/TeamInfoEventControllerTests.cs @@ -0,0 +1,142 @@ +using EvoSC.Common.Interfaces.Controllers; +using EvoSC.Common.Models; +using EvoSC.Common.Models.Callbacks; +using EvoSC.Common.Remote.EventArgsModels; +using EvoSC.Modules.Official.TeamInfoModule.Controllers; +using EvoSC.Modules.Official.TeamInfoModule.Interfaces; +using EvoSC.Testing.Controllers; +using GbxRemoteNet.Events; +using Moq; +using Xunit; + +namespace EvoSC.Modules.Official.TeamInfoModule.Tests.Controllers; + +public class TeamInfoEventControllerTests : ControllerMock +{ + private Mock _teamInfoService = new(); + + public TeamInfoEventControllerTests() + { + InitMock(_teamInfoService); + } + + [Fact] + public async Task Sets_Team_Mode_Disabled_If_UseTeams_Is_False() + { + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(true)); + + await Controller.OnScoresAsync(null, + new ScoresEventArgs + { + Section = ModeScriptSection.Undefined, + UseTeams = false, + WinnerTeam = 0, + WinnerPlayer = null, + Teams = new List(), + Players = new List() + }); + + _teamInfoService.Verify(s => s.SetModeIsTeamsAsync(false), Times.Once); + _teamInfoService.Verify(s => s.SetModeIsTeamsAsync(true), Times.Never); + _teamInfoService.Verify(s => s.UpdatePointsAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Sets_Team_Mode_Enabled_If_Required_And_Updates_Points() + { + var team1Points = 4; + var team2Points = 7; + + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(false)); + + await Controller.OnScoresAsync(null, + new ScoresEventArgs + { + Section = ModeScriptSection.Undefined, + UseTeams = true, + WinnerTeam = 0, + WinnerPlayer = null, + Teams = new List + { + new TeamScore { MatchPoints = team1Points }, + new TeamScore { MatchPoints = team2Points }, + }, + Players = new List() + }); + + _teamInfoService.Verify(s => s.SetModeIsTeamsAsync(true), Times.Once); + _teamInfoService.Verify(s => s.UpdatePointsAsync(team1Points, team2Points), Times.Once); + } + + [Fact] + public async Task Updates_Points_On_New_Scores_While_Already_In_Teams_Mode() + { + var team1Points = 45; + var team2Points = 56; + + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(true)); + + await Controller.OnScoresAsync(null, + new ScoresEventArgs + { + Section = ModeScriptSection.EndRound, + UseTeams = true, + WinnerTeam = 0, + WinnerPlayer = null, + Teams = new List + { + new TeamScore { MatchPoints = team1Points }, + new TeamScore { MatchPoints = team2Points }, + }, + Players = new List() + }); + + _teamInfoService.Verify(s => s.SetModeIsTeamsAsync(false), Times.Never); + _teamInfoService.Verify(s => s.SetModeIsTeamsAsync(true), Times.Never); + _teamInfoService.Verify(s => s.UpdatePointsAsync(team1Points, team2Points), Times.Once); + } + + [Fact] + public async Task Updates_Round_Number_On_New_Round() + { + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(true)); + + var roundNumber = 777; + await Controller.OnRoundStartAsync(null, new RoundEventArgs { Count = roundNumber, Time = 0 }); + _teamInfoService.Verify(s => s.UpdateRoundNumberAsync(roundNumber), Times.Once); + } + + [Fact] + public async Task Doesnt_Update_Round_Number_On_New_Round_If_Mode_Is_Not_Teams() + { + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(false)); + + await Controller.OnRoundStartAsync(null, new RoundEventArgs { Count = 0, Time = 0 }); + _teamInfoService.Verify(s => s.UpdateRoundNumberAsync(0), Times.Never); + } + + [Fact] + public async Task Hides_Widget_On_Podium_Start() + { + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(true)); + + await Controller.OnPodiumStartAsync(null, new PodiumEventArgs { Time = 0 }); + _teamInfoService.Verify(s => s.HideTeamInfoWidgetEveryoneAsync(), Times.Once); + } + + [Fact] + public async Task Hides_Widget_On_End_Map() + { + _teamInfoService.Setup(s => s.GetModeIsTeamsAsync()) + .Returns(Task.FromResult(true)); + + await Controller.OnEndMapAsync(null, new MapGbxEventArgs()); + _teamInfoService.Verify(s => s.HideTeamInfoWidgetEveryoneAsync(), Times.Once); + } +} diff --git a/tests/Modules/TeamInfoModule.Tests/Services/TeamInfoServiceTests.cs b/tests/Modules/TeamInfoModule.Tests/Services/TeamInfoServiceTests.cs new file mode 100644 index 000000000..4c289db07 --- /dev/null +++ b/tests/Modules/TeamInfoModule.Tests/Services/TeamInfoServiceTests.cs @@ -0,0 +1,262 @@ +using EvoSC.Common.Interfaces; +using EvoSC.Common.Util.MatchSettings.Models.ModeScriptSettingsModels; +using EvoSC.Manialinks.Interfaces; +using EvoSC.Modules.Official.TeamInfoModule.Config; +using EvoSC.Modules.Official.TeamInfoModule.Interfaces; +using EvoSC.Modules.Official.TeamInfoModule.Services; +using EvoSC.Testing; +using GbxRemoteNet.Interfaces; +using GbxRemoteNet.Structs; +using GbxRemoteNet.XmlRpc.ExtraTypes; +using Moq; +using Xunit; + +namespace EvoSC.Modules.Official.TeamInfoModule.Tests.Services; + +public class TeamInfoServiceTests +{ + private readonly Mock _manialinkManager = new(); + private readonly Mock _settings = new(); + + private readonly (Mock Client, Mock Remote) + _server = Mocking.NewServerClientMock(); + + private ITeamInfoService TeamInfoServiceMock() + { + return new TeamInfoService(_server.Client.Object, _manialinkManager.Object, _settings.Object); + } + + [Fact] + public async Task Initializes_Module_Service() + { + await TeamInfoServiceMock().InitializeModuleAsync(); + + _server.Remote.Verify(remote => remote.TriggerModeScriptEventArrayAsync("Trackmania.GetScores"), Times.Once); + } + + [Theory] + [InlineData(7, 6, 0, 1, true)] + [InlineData(7, 6, 5, 1, true)] + [InlineData(7, 6, 6, 1, true)] + [InlineData(7, 0, 0, 1, false)] + [InlineData(7, 0, 0, null, false)] + [InlineData(7, 5, 0, 1, false)] + [InlineData(7, 6, 5, 2, true)] + [InlineData(7, 7, 6, 2, true)] + [InlineData(7, 6, 6, 2, false)] + [InlineData(7, 7, 7, 2, false)] + [InlineData(null, 7, 7, 2, false)] + public async Task Detects_Map_Point(int? pointsLimit, int teamPoints, int opponentPoints, int? pointsGap, + bool shouldHaveMapPoint) + { + Assert.Equal( + shouldHaveMapPoint, + await TeamInfoServiceMock().DoesTeamHaveMatchPointAsync(teamPoints, opponentPoints, pointsLimit, pointsGap) + ); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Sets_And_Gets_Mode_Is_Teams(bool isTeamsMode) + { + var teamInfoService = TeamInfoServiceMock(); + await teamInfoService.SetModeIsTeamsAsync(isTeamsMode); + + Assert.Equal(isTeamsMode, await teamInfoService.GetModeIsTeamsAsync()); + } + + [Fact] + public async Task Parses_Mode_Script_Team_Settings() + { + var teamInfoService = TeamInfoServiceMock(); + var mockModeSettings = new GbxDynamicObject + { + ["S_PointsLimit"] = 1, + ["S_FinishTimeout"] = 2, + ["S_MaxPointsPerRound"] = 3, + ["S_PointsGap"] = 4, + ["S_RoundsPerMap"] = 5, + ["S_MapsPerMatch"] = 6, + ["S_NeutralEmblemUrl"] = "https://domain.tld/image.png", + }; + + _server.Remote.Setup(s => s.GetModeScriptSettingsAsync()) + .Returns(Task.FromResult(mockModeSettings)); + + var modeScriptTeamSettings = await teamInfoService.GetModeScriptTeamSettingsAsync(); + + _server.Remote.Verify(remote => remote.GetModeScriptSettingsAsync(), Times.Once); + + Assert.Equal(1, modeScriptTeamSettings.PointsLimit); + Assert.Equal(2, modeScriptTeamSettings.FinishTimeout); + Assert.Equal(3, modeScriptTeamSettings.MaxPointsPerRound); + Assert.Equal(4, modeScriptTeamSettings.PointsGap); + Assert.Equal(5, modeScriptTeamSettings.RoundsPerMap); + Assert.Equal(6, modeScriptTeamSettings.MapsPerMatch); + Assert.Equal("https://domain.tld/image.png", modeScriptTeamSettings.NeutralEmblemUrl); + } + + [Fact] + public async Task Returns_Default_Mode_Script_Team_Settings_For_Unset_Settings() + { + var teamInfoService = TeamInfoServiceMock(); + var mockModeSettings = new GbxDynamicObject(); + + _server.Remote.Setup(s => s.GetModeScriptSettingsAsync()) + .Returns(Task.FromResult(mockModeSettings)); + + var modeScriptTeamSettings = await teamInfoService.GetModeScriptTeamSettingsAsync(); + + _server.Remote.Verify(remote => remote.GetModeScriptSettingsAsync(), Times.Once); + + Assert.Equal(5, modeScriptTeamSettings.PointsLimit); + Assert.Equal(-1, modeScriptTeamSettings.FinishTimeout); + Assert.Equal(6, modeScriptTeamSettings.MaxPointsPerRound); + Assert.Equal(1, modeScriptTeamSettings.PointsGap); + Assert.Equal(-1, modeScriptTeamSettings.RoundsPerMap); + Assert.Equal(-1, modeScriptTeamSettings.MapsPerMatch); + Assert.Equal("", modeScriptTeamSettings.NeutralEmblemUrl); + } + + [Fact] + public async Task Sends_Widget_To_Everyone() + { + var teamInfoService = TeamInfoServiceMock(); + + _server.Remote.Setup(s => s.GetModeScriptSettingsAsync()) + .Returns(Task.FromResult(new GbxDynamicObject())); + + await teamInfoService.SendTeamInfoWidgetEveryoneAsync(); + + _server.Remote.Verify(remote => remote.GetModeScriptSettingsAsync(), Times.Once); + _manialinkManager.Verify( + m => m.SendPersistentManialinkAsync("TeamInfoModule.TeamInfoWidget", It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Hides_Widget_For_Everyone() + { + await TeamInfoServiceMock().HideTeamInfoWidgetEveryoneAsync(); + + _manialinkManager.Verify( + m => m.HideManialinkAsync("TeamInfoModule.TeamInfoWidget"), + Times.Once); + } + + [Fact] + public async Task Updates_Round_Number() + { + var teamInfoService = TeamInfoServiceMock(); + + _server.Remote.Setup(s => s.GetModeScriptSettingsAsync()) + .Returns(Task.FromResult(new GbxDynamicObject())); + + await teamInfoService.UpdateRoundNumberAsync(777); + + _manialinkManager.Verify( + m => m.SendPersistentManialinkAsync("TeamInfoModule.TeamInfoWidget", It.IsAny()), + Times.Once); + } + + [Fact] + public async Task Updates_Teams_Points() + { + var teamInfoService = TeamInfoServiceMock(); + + _server.Remote.Setup(s => s.GetModeScriptSettingsAsync()) + .Returns(Task.FromResult(new GbxDynamicObject())); + + await teamInfoService.UpdatePointsAsync(4, 5); + + _manialinkManager.Verify( + m => m.SendPersistentManialinkAsync("TeamInfoModule.TeamInfoWidget", It.IsAny()), + Times.Once); + } + + [Theory] + [InlineData(5, 1, 0, "FIRST TO 5")] + [InlineData(6, 2, 0, "FIRST TO 6 | GAP 2")] + [InlineData(7, 3, 0, "FIRST TO 7 | GAP 3")] + [InlineData(0, 1, 3, "3 ROUNDS/MAP")] + [InlineData(9, 1, 4, "FIRST TO 9 | 4 ROUNDS/MAP")] + [InlineData(9, 5, 4, "FIRST TO 9 | GAP 5 | 4 ROUNDS/MAP")] + [InlineData(0, 1, 0, null)] + public async Task Generates_Info_Box_Text(int pointsLimit, int pointsGap, int roundsPerMap, string? expected) + { + var teamInfoService = TeamInfoServiceMock(); + var modeScriptTeamSettings = new TeamsModeScriptSettings + { + PointsLimit = pointsLimit, PointsGap = pointsGap, RoundsPerMap = roundsPerMap + }; + + Assert.Equal(expected, await teamInfoService.GetInfoBoxTextAsync(modeScriptTeamSettings)); + } + + [Fact] + public async Task Gets_Widget_Data() + { + var teamInfoService = TeamInfoServiceMock(); + var team1Info = new TmTeamInfo { Name = "unit1" }; + var team2Info = new TmTeamInfo { Name = "unit2" }; + var expectedTeam1Points = 4; + var expectedTeam2Points = 0; + var expectedRoundNumber = 777; + + _server.Remote.Setup(s => s.GetModeScriptSettingsAsync()) + .Returns(Task.FromResult(new GbxDynamicObject())); + _server.Remote.Setup(s => s.GetTeamInfoAsync(1)) + .Returns(Task.FromResult(team1Info)); + _server.Remote.Setup(s => s.GetTeamInfoAsync(2)) + .Returns(Task.FromResult(team2Info)); + + await teamInfoService.UpdateRoundNumberAsync(expectedRoundNumber); + await teamInfoService.UpdatePointsAsync(expectedTeam1Points, expectedTeam2Points); + var widgetData = await teamInfoService.GetWidgetDataAsync(); + + _server.Remote.Verify(remote => remote.GetModeScriptSettingsAsync(), Times.Exactly(3)); + _server.Remote.Verify(remote => remote.GetTeamInfoAsync(1), Times.Exactly(3)); + _server.Remote.Verify(remote => remote.GetTeamInfoAsync(2), Times.Exactly(3)); + + var team1Property = widgetData.GetType().GetProperty("team1"); + var returnedTeam1Data = team1Property.GetValue(widgetData, null); + Assert.IsType(returnedTeam1Data); + Assert.Equal("unit1", returnedTeam1Data.Name); + + var team2Property = widgetData.GetType().GetProperty("team2"); + var returnedTeam2Data = team2Property.GetValue(widgetData, null); + Assert.IsType(returnedTeam2Data); + Assert.Equal("unit2", returnedTeam2Data.Name); + + var infoBoxText = widgetData.GetType().GetProperty("infoBoxText"); + var returnedInfoBoxText = infoBoxText.GetValue(widgetData, null); + Assert.Equal("FIRST TO 5", returnedInfoBoxText); + + var team1MatchPointProperty = widgetData.GetType().GetProperty("team1MatchPoint"); + var returnedTeam1MatchPoint = team1MatchPointProperty.GetValue(widgetData, null); + Assert.IsType(returnedTeam1MatchPoint); + Assert.True(returnedTeam1MatchPoint); + + var team2MatchPointProperty = widgetData.GetType().GetProperty("team2MatchPoint"); + var returnedTeam2MatchPoint = team2MatchPointProperty.GetValue(widgetData, null); + Assert.IsType(returnedTeam2MatchPoint); + Assert.False(returnedTeam2MatchPoint); + + var roundNumberProperty = widgetData.GetType().GetProperty("roundNumber"); + var returnedRoundNumber = roundNumberProperty.GetValue(widgetData, null); + Assert.Equal(expectedRoundNumber, returnedRoundNumber); + + var team1PointsProperty = widgetData.GetType().GetProperty("team1Points"); + var returnedTeam1Points = team1PointsProperty.GetValue(widgetData, null); + Assert.Equal(expectedTeam1Points, returnedTeam1Points); + + var team2PointsProperty = widgetData.GetType().GetProperty("team2Points"); + var returnedTeam2Points = team2PointsProperty.GetValue(widgetData, null); + Assert.Equal(expectedTeam2Points, returnedTeam2Points); + + var neutralEmblemUrlProperty = widgetData.GetType().GetProperty("neutralEmblemUrl"); + var returnedNeutralEmblemUrl = neutralEmblemUrlProperty.GetValue(widgetData, null); + Assert.Empty(returnedNeutralEmblemUrl); + } +} diff --git a/tests/Modules/TeamInfoModule.Tests/TeamInfoModule.Tests.csproj b/tests/Modules/TeamInfoModule.Tests/TeamInfoModule.Tests.csproj new file mode 100644 index 000000000..c5b5aa077 --- /dev/null +++ b/tests/Modules/TeamInfoModule.Tests/TeamInfoModule.Tests.csproj @@ -0,0 +1,37 @@ + + + + net8.0 + enable + enable + + false + true + EvoSC.Modules.Official.TeamInfoModule.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + ..\..\..\..\..\.nuget\packages\moq\4.20.70\lib\net6.0\Moq.dll + + + +