From ca5888e540df03246ac27ac153613a29d7739c0f Mon Sep 17 00:00:00 2001 From: Nils Penndorf Date: Wed, 30 Apr 2025 16:46:42 +0200 Subject: [PATCH 1/4] Added possibility to change the number of levels to be voteable --- MapVote.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/MapVote.cs b/MapVote.cs index 65cccbe..81a7116 100644 --- a/MapVote.cs +++ b/MapVote.cs @@ -18,6 +18,7 @@ using Unity.VisualScripting; using TMPro; using UnityEngine.UI; +using Random = System.Random; namespace MapVote { [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] @@ -51,6 +52,7 @@ internal sealed class MapVote : BaseUnityPlugin public static ConfigEntry VotingTime; public static ConfigEntry HideInMenu; public static ConfigEntry NoRepeatedMaps; + public static ConfigEntry VoteableLevelNumber; // Vote Data public static VotesDictionary CurrentVotes = new() { }; @@ -126,6 +128,7 @@ public void Awake() VotingTime = Config.Bind("General", "Voting Time", 10, new ConfigDescription("The amount of seconds until the voting ends, after the first player voted.", new AcceptableValueRange(3, 30))); HideInMenu = Config.Bind("General", "Hide in Menu", false, new ConfigDescription("When true - hides the Menu in the lobby menu and randomly selects a random map - Voting is still enabled in the truck")); NoRepeatedMaps = Config.Bind("General", "No Repeated Maps", false, new ConfigDescription("When true - disallows votes for the most recently played map - You won't play the same map twice in a row")); + VoteableLevelNumber = Config.Bind("General", "Number of voteable levels", 0, new ConfigDescription("The number of levels that can be voted for. 0 = all levels", new AcceptableValueRange(0, 10))); CompatibilityPatches.RunPatches(Chainloader.PluginInfos.Select(x => (x.Key)).ToList()); @@ -323,8 +326,26 @@ public static void CreateVotePopup(bool isInMenu = false) VotePopup = MenuAPI.CreateREPOPopupPage("Next map", true, !isInMenu, 0f, isInMenu ? new Vector2(40f, 0f) : new Vector2(-100f,0f)); var runManger = FindObjectOfType(); - var levels = runManger.levels; - + var actualLevelNumber = runManger.levels.Count < VoteableLevelNumber.Value ? runManger.levels.Count : VoteableLevelNumber.Value; + var levelIndexes = new List(); + var totalLevels = runManger.levels.Count; + for (var i = 0; i < totalLevels; i++) + { + levelIndexes.Add(i); + } + + var random = new Random(); + for (var i = levelIndexes.Count - 1; i > 0; i--) + { + var randomIndex = random.Next(0, i + 1); + (levelIndexes[i], levelIndexes[randomIndex]) = (levelIndexes[randomIndex], levelIndexes[i]); + } + + var selectedIndexes = levelIndexes.Take(actualLevelNumber).ToArray(); + var levelNumber = VoteableLevelNumber.Value > 0 ? VoteableLevelNumber.Value.ToString() : "all"; + Logger.LogMessage($"Starting a vote for {levelNumber} levels"); + var levels = VoteableLevelNumber.Value > 0 ? runManger.levels.Where(l => runManger.levels.IndexOf(l) >= 0 && selectedIndexes.Contains(runManger.levels.IndexOf(l))).ToList() : runManger.levels; + // Generate Vote Options from Levels foreach (var (level, index) in levels.Select((level, index) => (level, index))) { From 544f740b0c1f2fc3c3919bcb5b2e8cf8acad0cb0 Mon Sep 17 00:00:00 2001 From: Nils Penndorf Date: Wed, 30 Apr 2025 22:04:26 +0200 Subject: [PATCH 2/4] Added synchronization of available levels. Known bugs: host can't close the voting page, updating the number in the config doesn't work when in vote/game right now --- MapVote.cs | 118 ++++++++++++++++++++++++++--------- Patches/SteamManagerPatch.cs | 4 ++ 2 files changed, 94 insertions(+), 28 deletions(-) diff --git a/MapVote.cs b/MapVote.cs index 81a7116..4fc5631 100644 --- a/MapVote.cs +++ b/MapVote.cs @@ -47,6 +47,8 @@ internal sealed class MapVote : BaseUnityPlugin public static NetworkedEvent? OnSyncVotes; public static NetworkedEvent? OnSyncLastMapPlayed; public static NetworkedEvent? OnStartCountdown; + public static NetworkedEvent? OnMapsRandomized; + public static NetworkedEvent? OnPlayerConnected; // Configs public static ConfigEntry VotingTime; @@ -69,6 +71,7 @@ internal sealed class MapVote : BaseUnityPlugin public static bool ShouldHookRunMangerSetRunLevel = false; public static string? LastMapPlayed; + public static List CurrentVoteLevels = new(); public static MapVote Instance; @@ -160,6 +163,8 @@ internal static void Initialize() OnSyncVotes = new NetworkedEvent("OnSyncVotes", HandleOnSyncVotes); OnSyncLastMapPlayed = new NetworkedEvent("OnSyncLastMapPlayed", HandleOnSyncLastMapPlayed); OnStartCountdown = new NetworkedEvent("OnStartCountdown", HandleOnStartCountdown); + OnMapsRandomized = new NetworkedEvent("OnMapsRandomized", HandleOnMapsRandomized); + OnPlayerConnected = new NetworkedEvent("OnPlayerConnected", HandleOnPlayerConnected); if(!HideInMenu.Value) { @@ -181,6 +186,27 @@ public static void Reset() OwnVoteLevel = null; UpdateButtonLabels(); } + + private static void HandleOnPlayerConnected(EventData data) + { + if (SemiFunc.IsMasterClientOrSingleplayer()) + { + Logger.LogMessage("Received player connected event, sending level data"); + OnMapsRandomized?.RaiseEvent(CurrentVoteLevels.ToArray(), NetworkingEvents.RaiseOthers, SendOptions.SendReliable); + } + } + + private static void HandleOnMapsRandomized(EventData data) + { + if (SemiFunc.IsMasterClientOrSingleplayer()) + { + return; + } + CurrentVoteLevels = ((string[])data.CustomData).ToList(); + Logger.LogMessage($"Randomized maps received, generating {CurrentVoteLevels.Count} vote options"); + CreateVotePopup(true); + } + private static void HandleOnSyncLastMapPlayed(EventData data) { string lastMap = (string)data.CustomData; @@ -307,28 +333,18 @@ public static IEnumerator StartCountdown() } } - public static void CreateVotePopup(bool isInMenu = false) + public static List GetLevels(bool reRandomize = true) { - MenuAPI.CloseAllPagesAddedOnTop(); - VoteOptionButtons.Clear(); - - if(VotePopup != null) - { - VotePopup.ClosePage(true); - VotePopup = null; - } - - if (RunManager.instance.levelCurrent.name == TRUCK_LEVEL_NAME) + var runManager = FindObjectOfType(); + if (!reRandomize) { - GameDirector.instance.DisableInput = true; + var levels = runManager.levels.Where(l => CurrentVoteLevels.Contains(l.name)).ToList(); + return levels; } - - VotePopup = MenuAPI.CreateREPOPopupPage("Next map", true, !isInMenu, 0f, isInMenu ? new Vector2(40f, 0f) : new Vector2(-100f,0f)); - var runManger = FindObjectOfType(); - - var actualLevelNumber = runManger.levels.Count < VoteableLevelNumber.Value ? runManger.levels.Count : VoteableLevelNumber.Value; + + var actualLevelNumber = runManager.levels.Count < VoteableLevelNumber.Value ? runManager.levels.Count : VoteableLevelNumber.Value; var levelIndexes = new List(); - var totalLevels = runManger.levels.Count; + var totalLevels = runManager.levels.Count; for (var i = 0; i < totalLevels; i++) { levelIndexes.Add(i); @@ -344,8 +360,62 @@ public static void CreateVotePopup(bool isInMenu = false) var selectedIndexes = levelIndexes.Take(actualLevelNumber).ToArray(); var levelNumber = VoteableLevelNumber.Value > 0 ? VoteableLevelNumber.Value.ToString() : "all"; Logger.LogMessage($"Starting a vote for {levelNumber} levels"); - var levels = VoteableLevelNumber.Value > 0 ? runManger.levels.Where(l => runManger.levels.IndexOf(l) >= 0 && selectedIndexes.Contains(runManger.levels.IndexOf(l))).ToList() : runManger.levels; + return VoteableLevelNumber.Value > 0 ? runManager.levels.Where(l => runManager.levels.IndexOf(l) >= 0 && selectedIndexes.Contains(runManager.levels.IndexOf(l))).ToList() : runManager.levels; + } + + public static void CreateVotePopup(bool isInMenu = false) + { + MenuAPI.CloseAllPagesAddedOnTop(); + VoteOptionButtons.Clear(); + + if(VotePopup != null) + { + VotePopup.ClosePage(true); + VotePopup = null; + } + + if (RunManager.instance.levelCurrent.name == TRUCK_LEVEL_NAME) + { + GameDirector.instance.DisableInput = true; + } + var runManager = FindObjectOfType(); + if (SemiFunc.IsMasterClientOrSingleplayer()) + { + if (CurrentVoteLevels.Count <= 0) + { + var levels = GetLevels(); + CurrentVoteLevels = levels.Select(x => x.name).ToList(); + Logger.LogMessage($"{CurrentVoteLevels.Count} random maps selected, sending to clients"); + } + else + { + var levels = GetLevels(false); + CurrentVoteLevels = levels.Select(x => x.name).ToList(); + } + + OnMapsRandomized?.RaiseEvent(CurrentVoteLevels.ToArray(), NetworkingEvents.RaiseOthers, SendOptions.SendReliable); + GenerateVoteOptions(isInMenu); + } + else + { + GenerateVoteOptions(isInMenu); + } + + VotePopup!.AddElement(parent => + { + VotingTimeLabel = MenuAPI.CreateREPOLabel(null, parent, new Vector2(isInMenu ? 394f : 254f, 30f)); + }); + + VotePopup.OpenPage(true); + UpdateButtonLabels(); + VotePopup.GetComponent().PageStateSet(MenuPage.PageState.Active); + } + + public static void GenerateVoteOptions(bool isInMenu = false) + { + var levels = GetLevels(false); + VotePopup = MenuAPI.CreateREPOPopupPage("Next map", true, !isInMenu, 0f, isInMenu ? new Vector2(40f, 0f) : new Vector2(-100f,0f)); // Generate Vote Options from Levels foreach (var (level, index) in levels.Select((level, index) => (level, index))) { @@ -400,16 +470,8 @@ public static void CreateVotePopup(bool isInMenu = false) VoteOptionButtons.Add(new VoteOptionButton(VOTE_RANDOM_LABEL, 0, btn, true)); return btn.rectTransform; }); - - VotePopup.AddElement(parent => - { - VotingTimeLabel = MenuAPI.CreateREPOLabel(null, parent, new Vector2(isInMenu ? 394f : 254f, 30f)); - }); - - VotePopup.OpenPage(true); - UpdateButtonLabels(); - VotePopup.GetComponent().PageStateSet(MenuPage.PageState.Active); } + public static void UpdateButtonLabels() { VoteOptionButtons.ForEach(b => diff --git a/Patches/SteamManagerPatch.cs b/Patches/SteamManagerPatch.cs index 2383537..56efbf3 100644 --- a/Patches/SteamManagerPatch.cs +++ b/Patches/SteamManagerPatch.cs @@ -16,6 +16,10 @@ public static void PostfixJoiningPlayer() MapVote.OnSyncVotes?.RaiseEvent(MapVote.CurrentVotes.Values, NetworkingEvents.RaiseAll, SendOptions.SendReliable); MapVote.OnSyncLastMapPlayed?.RaiseEvent(MapVote.LastMapPlayed, NetworkingEvents.RaiseOthers, SendOptions.SendReliable); } + else + { + MapVote.OnPlayerConnected?.RaiseEvent("", NetworkingEvents.RaiseAll, SendOptions.SendReliable); + } } } } From 30d2542fc305a49da7d0bf067810ec067d463d35 Mon Sep 17 00:00:00 2001 From: Nils Penndorf Date: Thu, 1 May 2025 12:11:29 +0200 Subject: [PATCH 3/4] Multiple fixes: Non-votable maps are now displayed but disabled. Votable maps (and the random option) are displayed at the top of the list. Player connection event handler gets called directly, without the unnecessary event. VoteOptionButton now has a "Disabled" property with extension methods to change the disabled state. Changing the number of votable maps in the config will re-randomize votable maps according to the number that is configured, even while in game/lobby. --- MapVote.cs | 69 +++++++++++++++++++----------------- Patches/SteamManagerPatch.cs | 5 +-- VoteOptionButton.cs | 29 +++++++++++---- 3 files changed, 60 insertions(+), 43 deletions(-) diff --git a/MapVote.cs b/MapVote.cs index 4fc5631..26b168b 100644 --- a/MapVote.cs +++ b/MapVote.cs @@ -187,7 +187,7 @@ public static void Reset() UpdateButtonLabels(); } - private static void HandleOnPlayerConnected(EventData data) + public static void HandleOnPlayerConnected(EventData data) { if (SemiFunc.IsMasterClientOrSingleplayer()) { @@ -379,19 +379,16 @@ public static void CreateVotePopup(bool isInMenu = false) GameDirector.instance.DisableInput = true; } - var runManager = FindObjectOfType(); if (SemiFunc.IsMasterClientOrSingleplayer()) { - if (CurrentVoteLevels.Count <= 0) + if (CurrentVoteLevels.Count != VoteableLevelNumber.Value) { - var levels = GetLevels(); - CurrentVoteLevels = levels.Select(x => x.name).ToList(); + CurrentVoteLevels = GetLevels().Select(x => x.name).ToList(); Logger.LogMessage($"{CurrentVoteLevels.Count} random maps selected, sending to clients"); } else { - var levels = GetLevels(false); - CurrentVoteLevels = levels.Select(x => x.name).ToList(); + CurrentVoteLevels = GetLevels(false).Select(x => x.name).ToList(); } OnMapsRandomized?.RaiseEvent(CurrentVoteLevels.ToArray(), NetworkingEvents.RaiseOthers, SendOptions.SendReliable); @@ -414,9 +411,12 @@ public static void CreateVotePopup(bool isInMenu = false) public static void GenerateVoteOptions(bool isInMenu = false) { + var runManager = FindObjectOfType(); var levels = GetLevels(false); + levels.AddRange(runManager.levels.Where(l => !CurrentVoteLevels.Contains(l.name)));; VotePopup = MenuAPI.CreateREPOPopupPage("Next map", true, !isInMenu, 0f, isInMenu ? new Vector2(40f, 0f) : new Vector2(-100f,0f)); // Generate Vote Options from Levels + var counter = 0; foreach (var (level, index) in levels.Select((level, index) => (level, index))) { var name = level.name; @@ -431,7 +431,7 @@ public static void GenerateVoteOptions(bool isInMenu = false) OnVoteEvent?.RaiseEvent(name, NetworkingEvents.RaiseAll, SendOptions.SendReliable); }, parent); - if(HasBeenLastPlayed(name)) + if(HasBeenLastPlayed(name) || !CurrentVoteLevels.Contains(name)) { btn.gameObject.GetComponent().disabled = true; } @@ -446,37 +446,42 @@ public static void GenerateVoteOptions(bool isInMenu = false) VoteOptionButtons.Add(new VoteOptionButton(name, 0, btn)); return btn.rectTransform; }); - } - - // Generate "Random" Vote Option - VotePopup.AddElementToScrollView(parent => - { - var btn = MenuAPI.CreateREPOButton(null, () => { - if (DisableInput) + if (counter == CurrentVoteLevels.Count - 1 || (VoteableLevelNumber.Value == 0 && counter == levels.Count - 1)) + { + // Generate "Random" Vote Option + VotePopup.AddElementToScrollView(parent => { - return; - } - OwnVoteLevel = VOTE_RANDOM_LABEL; - OnVoteEvent?.RaiseEvent(VOTE_RANDOM_LABEL, NetworkingEvents.RaiseAll, SendOptions.SendReliable); - }, parent); - - var layoutGroup = btn.AddComponent(); - layoutGroup.spacing = 235f; - - var votesLabel = GameObject.Instantiate(btn.labelTMP.gameObject, btn.transform); - var lbl = votesLabel.GetComponent(); - lbl.horizontalAlignment = HorizontalAlignmentOptions.Right; - - VoteOptionButtons.Add(new VoteOptionButton(VOTE_RANDOM_LABEL, 0, btn, true)); - return btn.rectTransform; - }); + var btn = MenuAPI.CreateREPOButton(null, () => { + if (DisableInput) + { + return; + } + OwnVoteLevel = VOTE_RANDOM_LABEL; + OnVoteEvent?.RaiseEvent(VOTE_RANDOM_LABEL, NetworkingEvents.RaiseAll, SendOptions.SendReliable); + }, parent); + + var layoutGroup = btn.AddComponent(); + layoutGroup.spacing = 235f; + + var votesLabel = GameObject.Instantiate(btn.labelTMP.gameObject, btn.transform); + var lbl = votesLabel.GetComponent(); + lbl.horizontalAlignment = HorizontalAlignmentOptions.Right; + + VoteOptionButtons.Add(new VoteOptionButton(VOTE_RANDOM_LABEL, 0, btn, true)); + return btn.rectTransform; + }); + } + counter++; + } + } public static void UpdateButtonLabels() { VoteOptionButtons.ForEach(b => { - b.UpdateLabel(false, HasBeenLastPlayed(b.Level)); + if (HasBeenLastPlayed(b.Level) || (VoteableLevelNumber.Value > 0 && !CurrentVoteLevels.Contains(b.Level) && !b.IsRandomButton)) b.Disable(); + b.UpdateLabel(); }); } diff --git a/Patches/SteamManagerPatch.cs b/Patches/SteamManagerPatch.cs index 56efbf3..ec85861 100644 --- a/Patches/SteamManagerPatch.cs +++ b/Patches/SteamManagerPatch.cs @@ -15,10 +15,7 @@ public static void PostfixJoiningPlayer() { MapVote.OnSyncVotes?.RaiseEvent(MapVote.CurrentVotes.Values, NetworkingEvents.RaiseAll, SendOptions.SendReliable); MapVote.OnSyncLastMapPlayed?.RaiseEvent(MapVote.LastMapPlayed, NetworkingEvents.RaiseOthers, SendOptions.SendReliable); - } - else - { - MapVote.OnPlayerConnected?.RaiseEvent("", NetworkingEvents.RaiseAll, SendOptions.SendReliable); + MapVote.HandleOnPlayerConnected(new EventData(){customData = MapVote.CurrentVoteLevels.ToArray()}); } } } diff --git a/VoteOptionButton.cs b/VoteOptionButton.cs index ea8c9f4..ebf90f5 100644 --- a/VoteOptionButton.cs +++ b/VoteOptionButton.cs @@ -12,11 +12,13 @@ internal sealed class VoteOptionButton public string Level { get; set; } public REPOButton Button { get; set; } public bool IsRandomButton { get; set; } - public VoteOptionButton(string _level, int _votes, REPOButton _button, bool _isRandomButton = false) + public bool Disabled { get; set; } + public VoteOptionButton(string _level, int _votes, REPOButton _button, bool _isRandomButton = false, bool _isDisabled = false) { Level = _level; Button = _button; IsRandomButton = _isRandomButton; + Disabled = _isDisabled; } public int GetVotes(Dictionary votes) @@ -34,28 +36,28 @@ public int GetVotes(Dictionary votes) return votesNum; } - public void UpdateLabel(bool _highlight = false, bool _disabled = false) + public void UpdateLabel(bool _highlight = false) { var votes = MapVote.CurrentVotes.Values; var ownVote = MapVote.OwnVoteLevel == Level; var playerCount = Math.Max(Math.Min(GameDirector.instance.PlayerList.Count, 12), 4); var votesCount = GetVotes(votes); - Color mainColor = _disabled ? Color.gray : (_highlight == true ? Color.green : ownVote ? Color.yellow : Color.white); + Color mainColor = Disabled ? Color.gray : (_highlight == true ? Color.green : ownVote ? Color.yellow : Color.white); StringBuilder sb = new(); - if (_disabled) sb.Append(""); + if (Disabled) sb.Append(""); sb.Append($"[{Utilities.ColorString((ownVote || _highlight ? "X" : " "), mainColor)}] "); - if(!_disabled) sb.Append($""); + if(!Disabled) sb.Append($""); sb.Append($"{(IsRandomButton ? MapVote.VOTE_RANDOM_LABEL : Utilities.RemoveLevelPrefix(Level))}"); - if (!_disabled) + if (!Disabled) { sb.Append(""); } - if (_disabled) sb.Append(""); + if (Disabled) sb.Append(""); var votesLabel = Button.transform.GetChild(1); votesLabel.GetComponent().text = $"{Utilities.ColorString(new string('I', votesCount), Color.green)}{Utilities.ColorString(new string('I', playerCount - votesCount), Color.white)}"; @@ -64,4 +66,17 @@ public void UpdateLabel(bool _highlight = false, bool _disabled = false) $"{sb.ToString()}"; } } + + public static class VoteOptionButtonExtensions + { + internal static void Disable(this VoteOptionButton button) + { + button.Disabled = true; + } + + internal static void Enable(this VoteOptionButton button) + { + button.Disabled = false; + } + } } From 818dc7b73a44f2af042433feada0135e80f33b97 Mon Sep 17 00:00:00 2001 From: Nils Penndorf Date: Thu, 1 May 2025 22:22:09 +0200 Subject: [PATCH 4/4] Multiple fixes: Client wasn't able to interact with anything except the vote popup in lobby - caused by directly calling HandleOnPlayerConnected instead of letting the client inform the host. Probably because the time differential between the player joining and the host sending the data Votes weren't cleared when re-randomizing the levels on config change, potentially causing disabled levels to be voted Fixed last played level being included in random levels (if config option to disable last played level was true) --- MapVote.cs | 5 +++-- Patches/SteamManagerPatch.cs | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/MapVote.cs b/MapVote.cs index 26b168b..8807abe 100644 --- a/MapVote.cs +++ b/MapVote.cs @@ -347,7 +347,7 @@ public static List GetLevels(bool reRandomize = true) var totalLevels = runManager.levels.Count; for (var i = 0; i < totalLevels; i++) { - levelIndexes.Add(i); + if (!HasBeenLastPlayed(runManager.levels[i].name)) levelIndexes.Add(i); } var random = new Random(); @@ -383,6 +383,7 @@ public static void CreateVotePopup(bool isInMenu = false) { if (CurrentVoteLevels.Count != VoteableLevelNumber.Value) { + CurrentVotes.Values.Clear(); CurrentVoteLevels = GetLevels().Select(x => x.name).ToList(); Logger.LogMessage($"{CurrentVoteLevels.Count} random maps selected, sending to clients"); } @@ -413,7 +414,7 @@ public static void GenerateVoteOptions(bool isInMenu = false) { var runManager = FindObjectOfType(); var levels = GetLevels(false); - levels.AddRange(runManager.levels.Where(l => !CurrentVoteLevels.Contains(l.name)));; + levels.AddRange(runManager.levels.Where(l => !CurrentVoteLevels.Contains(l.name))); VotePopup = MenuAPI.CreateREPOPopupPage("Next map", true, !isInMenu, 0f, isInMenu ? new Vector2(40f, 0f) : new Vector2(-100f,0f)); // Generate Vote Options from Levels var counter = 0; diff --git a/Patches/SteamManagerPatch.cs b/Patches/SteamManagerPatch.cs index ec85861..384f9a8 100644 --- a/Patches/SteamManagerPatch.cs +++ b/Patches/SteamManagerPatch.cs @@ -15,7 +15,10 @@ public static void PostfixJoiningPlayer() { MapVote.OnSyncVotes?.RaiseEvent(MapVote.CurrentVotes.Values, NetworkingEvents.RaiseAll, SendOptions.SendReliable); MapVote.OnSyncLastMapPlayed?.RaiseEvent(MapVote.LastMapPlayed, NetworkingEvents.RaiseOthers, SendOptions.SendReliable); - MapVote.HandleOnPlayerConnected(new EventData(){customData = MapVote.CurrentVoteLevels.ToArray()}); + } + else + { + MapVote.OnPlayerConnected?.RaiseEvent("", NetworkingEvents.RaiseAll, SendOptions.SendReliable); } } }