From bec5a5cd8a3d3e75b0125d55c9fb16f749d51e9d Mon Sep 17 00:00:00 2001 From: Robert Jordan Date: Thu, 21 Jul 2022 18:01:36 -0400 Subject: [PATCH] Main Menu Playlist Mode & Rate this level button (v1.1.3.0) Refactors: * Moved some common duplicate code to new extensions class for LevelSelectMenuLogic. * Cleaned up base color handling for Personal playlists to reference a single readonly field. Also added `GetBaseColor` for use outside of the extensions class. * Moved where LevelPlaylistCompoundData was created for LevelSelectMenuLogic's tempPlaylist, so that it's done right after the tempPlaylist is created. Fixes: * Fixed PlaylistFileDeleted event assigning wrong value to name field (field is currently unused, so no functional difference). * Fixed LevelSelectMenuLogic's tempPlaylist state so that it'll always be reset when entering the Playlist Mode menu. * Also fixed the file name input field for saving/loading in Playlist Mode, so that the field doesn't remember *echoes of a past playlsit*. New Options: * Finally added options for previously always-on settings: * Enable Playlist Options menu * Enable *Visit Workshop page* button for Main Menus * Enable Playlist Mode when in the Choose Main Menu display type. * Re-introduce the *Rate this level* button in the Advanced level select menu. * Hide unused buttons in the Advanced level select menu for the Choose Main Menu display type. --- .../ConfigurationLogic.cs | 60 ++++- .../Distance.LevelSelectAdditions.csproj | 7 + .../Events/PlaylistFileDeleted.cs | 2 +- .../Assembly-CSharp/LevelPlaylist.cs | 50 +++- .../Assembly-CSharp/LevelSelectMenuLogic.cs | 49 ++++ .../LevelGridMenu/PlaylistEntry/get_Color_.cs | 14 +- .../LevelSelectMenuLogic/ClearTempPlaylist.cs | 13 +- .../LevelSelectMenuLogic/Initialize.cs | 50 ++-- .../OnLevelButtonClicked.cs | 73 ++++++ .../OnLevelPlaylistMenuClickLoad.cs | 14 +- .../OnLevelPlaylistMenuClickSave.cs | 9 +- .../OnRateLevelPanelPop.cs | 44 ++++ .../OpenLevelPlaylistMenu.cs | 87 +++++++ .../SetupLevelPlaylistVisuals.cs | 32 ++- .../LevelSelectMenuLogic/Start.cs | 7 + .../LevelSelectMenuLogic/StartPlaylist.cs | 22 ++ .../LevelSelectMenuLogic/UpdateInput.cs | 139 +++++++++++ Distance.LevelSelectAdditions/Mod.cs | 37 +++ .../Properties/AssemblyInfo.cs | 4 +- .../Scripts/LevelPlaylistSelectRenameLogic.cs | 11 +- .../LevelSelectWorkshopRateButtonLogic.cs | 232 ++++++++++++++++++ .../Scripts/Menus/LevelSetOptionsMenu.cs | 19 +- README.md | 9 +- 23 files changed, 873 insertions(+), 111 deletions(-) create mode 100644 Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelSelectMenuLogic.cs create mode 100644 Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelButtonClicked.cs create mode 100644 Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnRateLevelPanelPop.cs create mode 100644 Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OpenLevelPlaylistMenu.cs create mode 100644 Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/StartPlaylist.cs create mode 100644 Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/UpdateInput.cs create mode 100644 Distance.LevelSelectAdditions/Scripts/LevelSelectWorkshopRateButtonLogic.cs diff --git a/Distance.LevelSelectAdditions/ConfigurationLogic.cs b/Distance.LevelSelectAdditions/ConfigurationLogic.cs index 9dbecdc..f8d4736 100644 --- a/Distance.LevelSelectAdditions/ConfigurationLogic.cs +++ b/Distance.LevelSelectAdditions/ConfigurationLogic.cs @@ -2,7 +2,6 @@ using Distance.LevelSelectAdditions.Sorting; using Reactor.API.Configuration; using System; -using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -108,6 +107,49 @@ public bool EnableTheOtherSideSprintCampaign set => Set(EnableTheOtherSideSprintCampaign_ID, value); } + + private const string EnableLevelSetOptionsMenu_ID = "levelsets.enable_levelsets_options_menu"; + public bool EnableLevelSetOptionsMenu + { + get => Get(EnableLevelSetOptionsMenu_ID); + set => Set(EnableLevelSetOptionsMenu_ID, value); + } + + private const string EnableChooseMainMenuQuickPlaylist_ID = "levelsets.enable_choose_mainmenu_quick_playlist"; + public bool EnableChooseMainMenuQuickPlaylist + { + get => Get(EnableChooseMainMenuQuickPlaylist_ID); + set => Set(EnableChooseMainMenuQuickPlaylist_ID, value); + } + + private const string EnableChooseMainMenuVisitWorkshopButton_ID = "gui.enable_choose_mainmenu_workshop_button"; + public bool EnableChooseMainMenuVisitWorkshopButton + { + get => Get(EnableChooseMainMenuVisitWorkshopButton_ID); + set => Set(EnableChooseMainMenuVisitWorkshopButton_ID, value); + } + + private const string EnableRateWorkshopLevelButton_ID = "gui.enable_rate_workshop_level_button"; + public bool EnableRateWorkshopLevelButton + { + get => Get(EnableRateWorkshopLevelButton_ID); + set => Set(EnableRateWorkshopLevelButton_ID, value); + } + + private const string HideChooseMainMenuUnusedButtons_ID = "gui.hide_choose_mainmenu_unused_buttons"; + public bool HideChooseMainMenuUnusedButtons + { + get => Get(HideChooseMainMenuUnusedButtons_ID); + set => Set(HideChooseMainMenuUnusedButtons_ID, value); + } + + /*private const string FixLevelSelectScrollBug_ID = "gui.fix_level_select_scroll_bug"; + public bool FixLevelSelectScrollBug + { + get => Get(FixLevelSelectScrollBug_ID); + set => Set(FixLevelSelectScrollBug_ID, value); + }*/ + /*private const string LevelSetSettingsTable_ID = "levelsets.options"; public Dictionary> LevelSetSettingsTable { @@ -122,15 +164,6 @@ public bool EnableTheOtherSideSprintCampaign set => Set(State_LastLevelSets_ID, value); } - // No config option for these yet - public bool EnableLevelSetOptionsMenu => true; - - public bool FixLevelSelectScrollBug => true; - - public bool EnableChooseMainMenuVisitWorkshopButton => true; - - public bool HideChooseMainMenuUnusedButtons => true; - #endregion #region Helpers @@ -244,6 +277,13 @@ public void Awake() Get(WorkshopReverseSortingMethod3_ID, false); Get(EnableTheOtherSideSprintCampaign_ID, false); + Get(EnableLevelSetOptionsMenu_ID, true); + Get(EnableChooseMainMenuQuickPlaylist_ID, true); + Get(EnableChooseMainMenuVisitWorkshopButton_ID, true); + Get(EnableRateWorkshopLevelButton_ID, true); + Get(HideChooseMainMenuUnusedButtons_ID, true); + //Get(FixLevelSelectScrollBug_ID, true); // Always enable this fix + // Save settings, and any defaults that may have been added. Save(); } diff --git a/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj b/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj index 3d82923..035b2f3 100644 --- a/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj +++ b/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj @@ -92,6 +92,7 @@ + @@ -106,15 +107,21 @@ + + + + + + diff --git a/Distance.LevelSelectAdditions/Events/PlaylistFileDeleted.cs b/Distance.LevelSelectAdditions/Events/PlaylistFileDeleted.cs index 17a9318..4938414 100644 --- a/Distance.LevelSelectAdditions/Events/PlaylistFileDeleted.cs +++ b/Distance.LevelSelectAdditions/Events/PlaylistFileDeleted.cs @@ -16,7 +16,7 @@ public Data(string oldFilePath, string oldLevelSetID, string name) { this.filePath = oldFilePath; this.levelSetID = oldLevelSetID; - this.name = oldLevelSetID; + this.name = name; } } } diff --git a/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelPlaylist.cs b/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelPlaylist.cs index 44220f6..a92b3e4 100644 --- a/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelPlaylist.cs +++ b/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelPlaylist.cs @@ -12,6 +12,12 @@ namespace Distance.LevelSelectAdditions.Extensions { public static class LevelPlaylistExtensions { + #region Constants + + public static readonly Color BasePersonalColor = GConstants.myLevelColor_; + + #endregion + #region Playlist Attributes public static bool IsResourcesPlaylist(this LevelPlaylist playlist) @@ -136,7 +142,7 @@ public static string GetLevelSetIDPrefix(string levelSetID) #endregion - #region Display Name + #region Personal Display Name public static string GetUncoloredName(this LevelPlaylist playlist) { @@ -150,7 +156,7 @@ public static bool GetColor(this LevelPlaylist playlist, out bool colorTag, out bool hasColor = playlist.Name_.DecodeNGUIColor(out _, out colorTag, out color); if (multiplyBaseColor && hasColor && !colorTag) { - color *= GConstants.myLevelColor_; + color *= BasePersonalColor; } return hasColor; } @@ -161,7 +167,25 @@ public static bool GetNameAndColor(this LevelPlaylist playlist, out string name, bool hasColor = playlist.Name_.DecodeNGUIColor(out name, out colorTag, out color); if (multiplyBaseColor && hasColor && !colorTag) { - color *= GConstants.myLevelColor_; + color *= BasePersonalColor; + } + return hasColor; + } + + // Gets the base color to use for the label (this base color is used to preserve vanilla playlist colors that don't use the `[c][/c]` tag). + // If alwaysUseBaseColor is true, then the base playlist color is always returned, even if the name is uncolored. + // (AKA `BasePersonalColor` will always be output if the `[c][/c]` tag is not used.) + // Returns true if the playlist name is colored. + public static bool GetBaseColor(this LevelPlaylist playlist, out Color baseColor, bool alwaysUseBaseColor) + { + bool hasColor = playlist.GetColor(out bool colorTag, out _, false); + if ((hasColor || alwaysUseBaseColor) && !colorTag) + { + baseColor = BasePersonalColor; + } + else + { + baseColor = Color.white; // Default color with `[c][/c]` tag, or when no color is used. } return hasColor; } @@ -230,11 +254,11 @@ public static bool Rename(this LevelPlaylist playlist, string newName, bool auto if (hasColor && colorTag) { - /*if (!hasSymbols && color.Color32Equals(GConstants.myLevelColor_)) + /*if (!hasSymbols && color.Color32Equals(BasePersonalColor)) { playlist.Name_ = newName;//.EncodeNGUIColorHex(Color.white); // [FFFFFF]{newName}[-] } - else if (color.TryGetBaseColorFromMultiplier(GConstants.myLevelColor_, out Color baseColor)) + else if (color.TryGetBaseColorFromMultiplier(BasePersonalColor, out Color baseColor)) { // We can lose the color tag, because the current name color naturally supports the myLevelColor_ multiplier. playlist.Name_ = newName.EncodeNGUIColorHex(baseColor); // [RRGGBB(AA)]{newName}[-] @@ -295,7 +319,7 @@ public static bool Recolor(this LevelPlaylist playlist, Color? optNewColor, bool bool hasColor = playlist.GetNameAndColor(out string name, out bool colorTag, out Color oldColor, false); - Color newColor = (optNewColor ?? GConstants.myLevelColor_); + Color newColor = (optNewColor ?? BasePersonalColor); // These bools state whether the new and old colors can support *not* using the [c] color tag. @@ -303,14 +327,14 @@ public static bool Recolor(this LevelPlaylist playlist, Color? optNewColor, bool bool useOldBase = !colorTag; if (colorTag) // If color tag is used, then oldColor is not the baseColor { - useOldBase = oldColor.TryGetBaseColorFromMultiplier(GConstants.myLevelColor_, out oldBaseColor); + useOldBase = oldColor.TryGetBaseColorFromMultiplier(BasePersonalColor, out oldBaseColor); } else { - oldColor *= GConstants.myLevelColor_; + oldColor *= BasePersonalColor; } - //bool useOldBase = oldColor.TryGetBaseColorFromMultiplier(GConstants.myLevelColor_, out Color oldBaseColor); - bool useNewBase = newColor.TryGetBaseColorFromMultiplier(GConstants.myLevelColor_, out Color newBaseColor); + //bool useOldBase = oldColor.TryGetBaseColorFromMultiplier(BasePersonalColor, out Color oldBaseColor); + bool useNewBase = newColor.TryGetBaseColorFromMultiplier(BasePersonalColor, out Color newBaseColor); // If the user has put other formatting/color symbols inside the name, @@ -327,7 +351,7 @@ public static bool Recolor(this LevelPlaylist playlist, Color? optNewColor, bool // The above comparison needs to check `colorTag`, because the extracted color would be a base color otherwise. // Normal color is the same, don't change anything. } - else if (!optNewColor.HasValue || newColor.Color32Equals(GConstants.myLevelColor_)) + else if (!optNewColor.HasValue || newColor.Color32Equals(BasePersonalColor)) { if (hasSymbols && colorTag) { @@ -545,11 +569,11 @@ public static void PromptRecolor(this LevelPlaylist playlist, Action onSub { if (!playlist.GetColor(out bool colorTag, out Color color, true)) { - color = GConstants.myLevelColor_; + color = BasePersonalColor; } /*else if (!colorTag) // handled by true parameter { - color *= GConstants.myLevelColor_; + color *= BasePersonalColor; }*/ string hex = (color.a < 1f) ? NGUIText.EncodeColor32(color) : NGUIText.EncodeColor24(color); diff --git a/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelSelectMenuLogic.cs b/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelSelectMenuLogic.cs new file mode 100644 index 0000000..ac0c017 --- /dev/null +++ b/Distance.LevelSelectAdditions/Extensions/Assembly-CSharp/LevelSelectMenuLogic.cs @@ -0,0 +1,49 @@ +using Distance.LevelSelectAdditions.Scripts; +using System; +using UnityEngine; + +namespace Distance.LevelSelectAdditions.Extensions +{ + public static class LevelSelectMenuLogicExtensions + { + public static void UpdateQuickPlaylistText(this LevelSelectMenuLogic levelSelectMenu) + { + if (!levelSelectMenu.tempPlaylist_.Name_.IsEmptyPlaylistName()) + { + levelSelectMenu.quickPlaylistLabel_.text = levelSelectMenu.tempPlaylist_.Name_; // Update the label showing the playlist name + } + + // Preserve playlist name color (when not using `[c][/c] tag). + levelSelectMenu.tempPlaylist_.GetBaseColor(out Color baseColor, false); + levelSelectMenu.quickPlaylistLabel_.color = baseColor; + } + + public static void UpdateBottomLeftButtonVisibility(this LevelSelectMenuLogic levelSelectMenu) + { + // Make sure to always display these buttons when in Playlist Mode. + if (Mod.Instance.Config.HideChooseMainMenuUnusedButtons && !levelSelectMenu.showingLevelPlaylist_) + { + // Hide unused buttons when in the Choose Main Menu display type. + bool isMainMenu = levelSelectMenu.displayType_ == LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel; + levelSelectMenu.createPlaylistButton_.SetActive(!isMainMenu || Mod.Instance.Config.EnableChooseMainMenuQuickPlaylist); + levelSelectMenu.showLeaderboardsButton_.SetActive(!isMainMenu); + } + else + { + levelSelectMenu.createPlaylistButton_.SetActive(true); + levelSelectMenu.showLeaderboardsButton_.SetActive(true); + } + } + + public static void ResetTempPlaylistState(this LevelSelectMenuLogic levelSelectMenu) + { + levelSelectMenu.tempPlaylist_.Name_ = nameof(LevelPlaylist); // restore default uninitialized name + + var playlistData = levelSelectMenu.tempPlaylist_.GetComponent(); + if (playlistData) + { + playlistData.FilePath = null; + } + } + } +} diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/PlaylistEntry/get_Color_.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/PlaylistEntry/get_Color_.cs index 26fe874..0cae0b4 100644 --- a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/PlaylistEntry/get_Color_.cs +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/PlaylistEntry/get_Color_.cs @@ -21,19 +21,9 @@ internal static void Postfix(LevelGridMenu.PlaylistEntry __instance, ref Color _ { if (__instance.type_ == LevelGridMenu.PlaylistEntry.Type.Personal) { - if (__instance.playlist_.GetColor(out bool colorTag, out _, false)) + if (__instance.playlist_.GetBaseColor(out Color baseColor, false)) { - if (colorTag) - { - // Otherwise we'll return the normal color, which is multiplied against the base color tags. - // This will keep color tags used without this mod consistent. - __result = Color.white; - } - else - { - // Color functions in vanilla by multiplying against the personal playlist color. - __result = GConstants.myLevelColor_; - } + __result = baseColor; } } } diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/ClearTempPlaylist.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/ClearTempPlaylist.cs index 0fa91c0..8589f1d 100644 --- a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/ClearTempPlaylist.cs +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/ClearTempPlaylist.cs @@ -1,4 +1,4 @@ -using Distance.LevelSelectAdditions.Scripts; +using Distance.LevelSelectAdditions.Extensions; using HarmonyLib; namespace Distance.LevelSelectAdditions.Harmony @@ -6,6 +6,8 @@ namespace Distance.LevelSelectAdditions.Harmony /// /// Patch to stop tempPlaylist_ from preserving its previous name. This can effect using the Rename button, /// as well as the initial "QUICK PLAYLIST..." text due to the SetupLevelPlaylistVisuals patch. + /// + /// Also includes patch to hide unused Leaderboards (and optionally Playlist Mode) buttons when in the Choose Main Menu display type. /// [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.ClearTempPlaylist))] internal static class LevelSelectMenuLogic__ClearTempPlaylist @@ -13,13 +15,10 @@ internal static class LevelSelectMenuLogic__ClearTempPlaylist [HarmonyPostfix] internal static void Postfix(LevelSelectMenuLogic __instance) { - __instance.tempPlaylist_.Name_ = nameof(LevelPlaylist); // restore default uninitialized name + __instance.ResetTempPlaylistState(); - var playlistData = __instance.tempPlaylist_.GetComponent(); - if (playlistData) - { - playlistData.FilePath = null; - } + // We're exiting Playlist Mode, so we need to re-evaluate bottom left button visibility. + __instance.UpdateBottomLeftButtonVisibility(); } } } \ No newline at end of file diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/Initialize.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/Initialize.cs index 62c7693..ee98401 100644 --- a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/Initialize.cs +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/Initialize.cs @@ -1,4 +1,6 @@ -using HarmonyLib; +using Distance.LevelSelectAdditions.Extensions; +using Distance.LevelSelectAdditions.Scripts; +using HarmonyLib; namespace Distance.LevelSelectAdditions.Harmony { @@ -14,6 +16,10 @@ namespace Distance.LevelSelectAdditions.Harmony /// Now that the menu has initialized with the search bar selected, you can only scroll to the top and bottom entries. /// /// The in-game fix for this bug is to manually select the search bar with your mouse, then scroll off of the search bar. + /// + /// Also includes patch to hide unused Leaderboards (and optionally Playlist Mode) buttons when in the Choose Main Menu display type. + /// + /// Also includes patch to reset and update various states required when entering the Advanced level select menu. /// [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.Initialize))] internal static class LevelSelectMenuLogic__Initialize @@ -21,28 +27,34 @@ internal static class LevelSelectMenuLogic__Initialize [HarmonyPrefix] internal static void Prefix(LevelSelectMenuLogic __instance) { - if (Mod.Instance.Config.FixLevelSelectScrollBug) + // Reset search bar selected state, if needed. + if (__instance.searchInput_.isSelected) { - // Reset search bar selected state, if needed. - if (__instance.searchInput_.isSelected) - { - // NOTE: isSelected alone won't be enough to deselect the input, because UIInput.selection is still - // assigned to the control (and determines the state of `isSelected`). - // So use the 'built-in' protected method `OnSelect` to change selection instead. - //__instance.searchInput_.isSelected = false; - //UIInput.selection = null; - __instance.searchInput_.OnSelect(false); - } + // NOTE: isSelected alone won't be enough to deselect the input, because UIInput.selection is still + // assigned to the control (and determines the state of `isSelected`). + // So use the 'built-in' protected method `OnSelect` to change selection instead. + //__instance.searchInput_.isSelected = false; + //UIInput.selection = null; + __instance.searchInput_.OnSelect(false); } - // Don't implement this until Playlist Mode is ready, - // since we'll want to make the this visible for the "REMOVE LEVEL" button. - /*if (Mod.Instance.Config.HideChooseMainMenuUnusedButtons) + // Reflect the current `EnableRateWorkshopLevelButton` setting. + var workshopRateButtonLogic = __instance.GetOrAddComponent(); + if (workshopRateButtonLogic) { - // Keep the Playlist Mode button, since there are plans to restore that functionality for Choose Main Menu levels. - bool isMainMenu = __instance.displayType_ == LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel; - __instance.showLeaderboardsButton_.SetActive(!isMainMenu); - }*/ + workshopRateButtonLogic.Initialize(); + } + + // Cleanup Playlist Mode states that aren't reset when leaving via MenuCancel. + __instance.ResetTempPlaylistState(); + } + + [HarmonyPostfix] + internal static void Postfix(LevelSelectMenuLogic __instance) + { + // This needs to be handled in the Postfix, because something inside `Initialize` + // indirectly changes the active state of these buttons. + __instance.UpdateBottomLeftButtonVisibility(); } } } diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelButtonClicked.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelButtonClicked.cs new file mode 100644 index 0000000..f0b3572 --- /dev/null +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelButtonClicked.cs @@ -0,0 +1,73 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Distance.LevelSelectAdditions.Harmony +{ + /// + /// Patch to enable adding a selected level in Playlist Mode while in the Choose Main Menu level display type. + /// + [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.OnLevelButtonClicked))] + internal static class LevelSelectMenuLogic__OnLevelButtonClicked + { + [HarmonyTranspiler] + internal static IEnumerable Transpiler(IEnumerable instructions) + { + var codes = new List(instructions); + + Mod.Instance.Logger.Info("Transpiling..."); + // VISUAL: + //if (this.displayType_ != LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) + // -to- + //if (AllowMainMenuDisplayTypeForQuickPlaylist_(this)) + + for (int i = 3; i < codes.Count; i++) + { + if ((codes[i - 3].opcode == OpCodes.Ldarg_0) && + (codes[i - 2].opcode == OpCodes.Ldfld && ((FieldInfo)codes[i - 2].operand).Name == "displayType_") && + (codes[i - 1].opcode == OpCodes.Ldc_I4_2) && // (LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) + (codes[i ].opcode == OpCodes.Bne_Un)) + { + Mod.Instance.Logger.Info($"ldfld displayType_ @ {i-2}"); + Mod.Instance.Logger.Info($"ldc.i4.2 @ {i-1}"); + + // Replace: ldarg.0 + // Replace: ldfld displayType_ + // Replace: ldc.i4.2 + // Replace: bne.un (preserve jump label) + // With: ldarg.0 + // With: call AllowMainMenuDisplayTypeForQuickPlaylist_ + // With: brtrue (preserve jump label) + codes.RemoveRange(i - 2, 2); + codes.InsertRange(i - 2, new CodeInstruction[] + { + //new CodeInstruction(OpCodes.Ldarg_0, null), + new CodeInstruction(OpCodes.Call, typeof(LevelSelectMenuLogic__OnLevelButtonClicked).GetMethod(nameof(AllowMainMenuDisplayTypeForQuickPlaylist_))), + }); + i -= 1; // instruction offset + + codes[i].opcode = OpCodes.Brtrue; // Preserve other instruction info, just change the opcode + + break; + } + } + return codes.AsEnumerable(); + } + + #region Helper Functions + + public static bool AllowMainMenuDisplayTypeForQuickPlaylist_(LevelSelectMenuLogic levelSelectMenu) + { + // We need to ensure logic for ChooseMainMenu ONLY passes through if we're currently in Quick Playlist mode. + if (levelSelectMenu.showingLevelPlaylist_ && Mod.Instance.Config.EnableChooseMainMenuQuickPlaylist) + { + return true; + } + return levelSelectMenu.displayType_ != LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel; + } + + #endregion + } +} diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickLoad.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickLoad.cs index b83dfa0..d8bf93c 100644 --- a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickLoad.cs +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickLoad.cs @@ -1,4 +1,5 @@ -using Distance.LevelSelectAdditions.Scripts; +using Distance.LevelSelectAdditions.Extensions; +using Distance.LevelSelectAdditions.Scripts; using HarmonyLib; using UnityEngine; @@ -20,14 +21,17 @@ internal static bool Prefix(LevelSelectMenuLogic __instance, string absolutePath __instance.tempPlaylist_.Clear(); __instance.tempPlaylist_.CopyFrom(loadedPlaylist); - var playlistData = __instance.tempPlaylist_.gameObject.GetOrAddComponent(); - playlistData.FilePath = absolutePath; - playlistData.Playlist = __instance.tempPlaylist_; + var playlistData = __instance.tempPlaylist_.GetComponent(); + if (playlistData) + { + playlistData.FilePath = absolutePath; + playlistData.Playlist = __instance.tempPlaylist_; + } __instance.SetupLevelPlaylistVisuals(); __instance.tempPlaylist_.Name_ = loadedPlaylist.Name_; - __instance.quickPlaylistLabel_.text = loadedPlaylist.Name_; + __instance.UpdateQuickPlaylistText(); G.Sys.MenuPanelManager_.Pop(false); } diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickSave.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickSave.cs index 815e3cb..8a9af69 100644 --- a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickSave.cs +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnLevelPlaylistMenuClickSave.cs @@ -25,18 +25,17 @@ internal static bool Prefix(LevelSelectMenuLogic __instance, string absolutePath // If our playlist is unnamed, then assign a display name based on the file name. - string newName = __instance.tempPlaylist_.Name_; - if (newName.IsEmptyPlaylistName()) + if (__instance.tempPlaylist_.Name_.IsEmptyPlaylistName()) { - newName = Resource.GetFileNameWithoutExtension(absolutePath); + string newName = Resource.GetFileNameWithoutExtension(absolutePath); __instance.tempPlaylist_.Name_ = newName; - __instance.quickPlaylistLabel_.text = newName; // Update the label showing the playlist name + __instance.UpdateQuickPlaylistText(); Mod.Instance.Logger.Debug($"Saving playlist New Name: " + (newName != null ? $"\"{newName}\"" : "null")); } - var playlistData = __instance.tempPlaylist_.gameObject.GetOrAddComponent(); + var playlistData = __instance.tempPlaylist_.GetComponent(); if (playlistData) { playlistData.FilePath = absolutePath; diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnRateLevelPanelPop.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnRateLevelPanelPop.cs new file mode 100644 index 0000000..dd1a859 --- /dev/null +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OnRateLevelPanelPop.cs @@ -0,0 +1,44 @@ +using HarmonyLib; + +namespace Distance.LevelSelectAdditions.Harmony +{ + /// + /// Patch to update the field after rating a level. + /// This is needed to ensure the level select list updates it's displayed vote, and so that we can grab the current vote + /// for the level and display it on our rate button. + /// + [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.OnRateLevelPanelPop))] + internal static class LevelSelectMenuLogic__OnRateLevelPanelPop + { + [HarmonyPrefix] + internal static void Prefix(LevelSelectMenuLogic __instance) + { + // This needs to be a prefix so that the displayed info is updated before `ReportEntryChanged` is called. + + var entry = __instance.selectedEntry_; + if (entry != null && entry.ugcLevelData_ != null) + { + // IT'S REFLECTION TIME! + // Performance isn't a concern for this usage, since this function is only called after user input from the rate level prompt. + + // Steamworks is located in `Assembly-CSharp-firstpass.dll`, so we don't have access to its types and methods/properties using those types. + // So our goal is to update entry.myWorkshopVoteIndex_ to match the vote change performed by the Rate Level prompt. + // TODO: In the future, the GSL may add `Assembly-CSharp-firstpass.dll` as a dependency. So eventually we could change this + // (assuming all other mods follow suit and update to the latest GSL version, which sounds like a nightmare to achieve). + + //public static int WorkshopVoteIndex(this EWorkshopVote vote); + var method_WorkshopVoteIndex = typeof(EWorkshopVoteEx).GetMethod("WorkshopVoteIndex"); + //public EWorkshopVote WorkshopVote_ { get; set; } + var getter_WorkshopVote_ = typeof(WorkshopLevelInfo).GetProperty("WorkshopVote_").GetGetMethod(); + + int newVoteIndex = (int)method_WorkshopVoteIndex.Invoke(null, new object[] { getter_WorkshopVote_.Invoke(entry.ugcLevelData_, null) }); + + if (newVoteIndex != entry.myWorkshopVoteIndex_) + { + entry.myWorkshopVoteIndex_ = newVoteIndex; + // Any extra handling here if the vote was changed. + } + } + } + } +} diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OpenLevelPlaylistMenu.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OpenLevelPlaylistMenu.cs new file mode 100644 index 0000000..e5724cf --- /dev/null +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/OpenLevelPlaylistMenu.cs @@ -0,0 +1,87 @@ +using Distance.LevelSelectAdditions.Extensions; +using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Distance.LevelSelectAdditions.Harmony +{ + /// + /// Patch to enable actually opening Playlist Mode panel when in the Choose Main Menu level display type. + /// + [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.OpenLevelPlaylistMenu))] + internal static class LevelSelectMenuLogic__OpenLevelPlaylistMenu + { + [HarmonyPrefix] + internal static void Prefix(LevelSelectMenuLogic __instance) + { + // Change the input field to the current 'known' file name if this playlist was loaded, or has previously been saved. + string filePath = __instance.tempPlaylist_.GetFilePath(); + string fileName = null; + if (filePath != null) + { + fileName = Resource.GetFileNameWithoutExtension(filePath); + } + + __instance.levelPlaylistSelectMenu_.levelPathInput_.Value_ = fileName ?? string.Empty; // Should null be used here instead(?) + } + + [HarmonyTranspiler] + internal static IEnumerable Transpiler(IEnumerable instructions) + { + var codes = new List(instructions); + + Mod.Instance.Logger.Info("Transpiling..."); + // VISUAL: + //if (this.displayType_ == LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel || ...) + // -to- + //if (!AllowMainMenuDisplayTypeForQuickPlaylist_(this) || ...) + + for (int i = 3; i < codes.Count; i++) + { + if ((codes[i - 3].opcode == OpCodes.Ldarg_0) && + (codes[i - 2].opcode == OpCodes.Ldfld && ((FieldInfo)codes[i - 2].operand).Name == "displayType_") && + (codes[i - 1].opcode == OpCodes.Ldc_I4_2) && // (LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) + (codes[i ].opcode == OpCodes.Beq)) + { + Mod.Instance.Logger.Info($"ldfld displayType_ @ {i-2}"); + Mod.Instance.Logger.Info($"ldc.i4.2 @ {i-1}"); + + // Replace: ldarg.0 + // Replace: ldfld displayType_ + // Replace: ldc.i4.2 + // Replace: beq (preserve jump label) + // With: ldarg.0 + // With: call AllowMainMenuDisplayTypeForQuickPlaylist_ + // With: brfalse (preserve jump label) + codes.RemoveRange(i - 2, 2); + codes.InsertRange(i - 2, new CodeInstruction[] + { + //new CodeInstruction(OpCodes.Ldarg_0, null), + new CodeInstruction(OpCodes.Call, typeof(LevelSelectMenuLogic__OpenLevelPlaylistMenu).GetMethod(nameof(AllowMainMenuDisplayTypeForQuickPlaylist_))), + }); + i -= 1; // instruction offset + + codes[i].opcode = OpCodes.Brfalse; // Preserve other instruction info, just change the opcode + + break; + } + } + return codes.AsEnumerable(); + } + + #region Helper Functions + + public static bool AllowMainMenuDisplayTypeForQuickPlaylist_(LevelSelectMenuLogic levelSelectMenu) + { + if (Mod.Instance.Config.EnableChooseMainMenuQuickPlaylist) + { + return true; + } + return levelSelectMenu.displayType_ != LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel; + } + + #endregion + } +} diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/SetupLevelPlaylistVisuals.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/SetupLevelPlaylistVisuals.cs index b588cfb..3b83d1d 100644 --- a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/SetupLevelPlaylistVisuals.cs +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/SetupLevelPlaylistVisuals.cs @@ -1,5 +1,4 @@ using Distance.LevelSelectAdditions.Extensions; -using Distance.LevelSelectAdditions.Scripts; using HarmonyLib; using System.Collections.Generic; using System.Linq; @@ -13,20 +12,27 @@ namespace Distance.LevelSelectAdditions.Harmony /// /// Also includes patch to make the Visit Workshop page button visible for the Choose Main Menu display type /// (when first opening the level select menu). + /// + /// Also includes patch to hide the Start Playlist button when in Playlist Mode for the Choose Main Menu display type. + /// + /// Also includes patch to hide unused Leaderboards (and optionally Playlist Mode) buttons when in the Choose Main Menu display type. /// [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.SetupLevelPlaylistVisuals))] internal static class LevelSelectMenuLogic__SetupLevelPlaylistVisuals { + [HarmonyPrefix] + internal static void Prefix(LevelSelectMenuLogic __instance) + { + // We *might* be entering Playlist Mode, so we need to re-evaluate bottom left button visibility. + __instance.UpdateBottomLeftButtonVisibility(); + } + [HarmonyPostfix] internal static void Postfix(LevelSelectMenuLogic __instance) { - // Ensure our compound data component is attached. - __instance.tempPlaylist_.gameObject.GetOrAddComponent(); - - if (!__instance.tempPlaylist_.Name_.IsEmptyPlaylistName()) - { - __instance.quickPlaylistLabel_.text = __instance.tempPlaylist_.Name_; - } + // We need to update the Quick Playlist label because every call to this function assigns its + // text value to QUICK PLAYLIST... which we don't want when we have a named playlist. + __instance.UpdateQuickPlaylistText(); } [HarmonyTranspiler] @@ -34,7 +40,7 @@ internal static IEnumerable Transpiler(IEnumerable(instructions); - Mod.Instance.Logger.Info("Transpiling..."); + Mod.Instance.Logger.Info("Transpiling (1/2)..."); // VISUAL: //if (... && this.displayType_ != LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel && ...) // -to- @@ -69,7 +75,7 @@ internal static IEnumerable Transpiler(IEnumerable 0; //this.startPlaylistButton_.SetActive(active); @@ -103,7 +109,7 @@ internal static IEnumerable Transpiler(IEnumerable /// Patch to create the manager for the QUICK PLAYLIST rename button. + /// + /// And patch to create the manager for the Workshop rate this level button. /// [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.Start))] internal static class LevelSelectMenuLogic__Start @@ -12,7 +14,12 @@ internal static class LevelSelectMenuLogic__Start [HarmonyPostfix] internal static void Postfix(LevelSelectMenuLogic __instance) { + // Ensure our compound data component is attached. + // `tempPlaylist_` is created in `LevelSelectMenuLogic.Start`, so this is the best point to ensure the data is added. + __instance.tempPlaylist_.gameObject.GetOrAddComponent(); + __instance.GetOrAddComponent(); + __instance.GetOrAddComponent(); } } } \ No newline at end of file diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/StartPlaylist.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/StartPlaylist.cs new file mode 100644 index 0000000..58bb29e --- /dev/null +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/StartPlaylist.cs @@ -0,0 +1,22 @@ +using HarmonyLib; + +namespace Distance.LevelSelectAdditions.Harmony +{ + /// + /// Patch to prevent starting a loaded Quick Playlist when in the Choose Main Menu display type. + /// + [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.StartPlaylist))] + internal static class LevelSelectMenuLogic__StartPlaylist + { + [HarmonyPrefix] + internal static bool Prefix(LevelSelectMenuLogic __instance) + { + if (__instance.displayType_ == LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) + { + return false; // Prevent starting a playlist in Choose Main Menu level display. + } + + return true; // Fallthrough to normal method behavior. + } + } +} diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/UpdateInput.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/UpdateInput.cs new file mode 100644 index 0000000..c8eda6d --- /dev/null +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelSelectMenuLogic/UpdateInput.cs @@ -0,0 +1,139 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Distance.LevelSelectAdditions.Harmony +{ + /// + /// Patch to enable Playlist Mode button inputs for the Choose Main Menu display type. + /// + [HarmonyPatch(typeof(LevelSelectMenuLogic), nameof(LevelSelectMenuLogic.UpdateInput))] + internal static class LevelSelectMenuLogic__UpdateInput + { + // Prefix patch version: + /*[HarmonyPrefix] + internal static void Prefix(LevelSelectMenuLogic __instance) + { + if (__instance.ignoreMenuInputForOneFrame_ || !G.Sys.MenuPanelManager_.MenuInputEnabled_ || __instance.SearchButtonSelected_) + { + return; + } + + if (__instance.displayType_ == LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) + { + // Enable PLAYLIST MODE button in the Choose Main Menu level display type. + if (__instance.inputManager_.GetKeyUp(InputAction.MenuSpecial_2, -2)) + { + if (__instance.showingLevelPlaylist_) + { + if (__instance.tempPlaylist_.Count_ > 0) + { + __instance.menuPanelManager_.ShowOkCancel("Closing the playlist will clear it. Are you sure that you want to continue?", + "Closing Playlist", + new MessagePanelLogic.OnButtonClicked(__instance.ClearTempPlaylist), + new MessagePanelLogic.OnButtonClicked(__instance.ClosingPlaylistPop), + UIWidget.Pivot.Center); + } + else + { + __instance.ClearTempPlaylist(); + } + } + else + { + __instance.showingLevelPlaylist_ = true; + __instance.SetupLevelPlaylistVisuals(); + } + } + + // Enable REMOVE LEVEL button in the Choose Main Menu level display type. + if (__instance.showingLevelPlaylist_ && __instance.inputManager_.GetKeyUp(InputAction.MenuSpecial_1, -2)) + { + if (__instance.tempPlaylist_.Count_ > 0) + { + __instance.tempPlaylist_.Remove(__instance.tempPlaylist_.Count_ - 1); + } + __instance.SetupLevelPlaylistVisuals(); + } + } + }*/ + + [HarmonyTranspiler] + internal static IEnumerable Transpiler(IEnumerable instructions) + { + var codes = new List(instructions); + + Mod.Instance.Logger.Info("Transpiling..."); + // VISUAL: + //if (this.displayType_ != LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel && ...) + // -to- + //if (AllowMainMenuDisplayTypeForQuickPlaylist_(this) && ...) + + // 1st appearance: block to handle toggling Playlist Mode on/off + // 2nd appearance: block to handle the REMOVE LEVEL input in Playlist Mode. + for (int i = 3; i < codes.Count; i++) + { + if ((codes[i - 3].opcode == OpCodes.Ldarg_0) && + (codes[i - 2].opcode == OpCodes.Ldfld && ((FieldInfo)codes[i - 2].operand).Name == "displayType_") && + (codes[i - 1].opcode == OpCodes.Ldc_I4_2) && // (LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) + (codes[i ].opcode == OpCodes.Beq)) + { + if (i >= 7) + { + if ((codes[i - 7].opcode == OpCodes.Ldc_I4_S && ((sbyte) codes[i - 7].operand) == 44) && + (codes[i - 6].opcode == OpCodes.Ldc_I4_S && ((sbyte) codes[i - 6].operand) == -2) && + (codes[i - 5].opcode == OpCodes.Callvirt && ((MethodInfo)codes[i - 5].operand).Name == "GetKeyUp") && + (codes[i - 4].opcode == OpCodes.Brfalse)) + { + Mod.Instance.Logger.Info($"ldc.i4.s 44 @ {i-7}"); + Mod.Instance.Logger.Info($"ldc.i4.s -2 @ {i-6}"); + Mod.Instance.Logger.Info($"callvirt GetKeyUp @ {i-5}"); + + // This is the condition for OpenLeaderboards, which we don't want to enable for main menus. + continue; + } + } + + Mod.Instance.Logger.Info($"ldfld displayType_ @ {i-2}"); + Mod.Instance.Logger.Info($"ldc.i4.2 @ {i-1}"); + + // Replace: ldarg.0 + // Replace: ldfld displayType_ + // Replace: ldc.i4.2 + // Replace: beq (preserve jump label) + // With: ldarg.0 + // With: call AllowMainMenuDisplayTypeForQuickPlaylist_ + // With: brfalse (preserve jump label) + codes.RemoveRange(i - 2, 2); + codes.InsertRange(i - 2, new CodeInstruction[] + { + //new CodeInstruction(OpCodes.Ldarg_0, null), + new CodeInstruction(OpCodes.Call, typeof(LevelSelectMenuLogic__UpdateInput).GetMethod(nameof(AllowMainMenuDisplayTypeForQuickPlaylist_))), + }); + i -= 1; // instruction offset + + codes[i].opcode = OpCodes.Brfalse; // Preserve other instruction info, just change the opcode + + + // Don't break after first appearance, we need to patch this instruction pattern 2 times. + } + } + return codes.AsEnumerable(); + } + + #region Helper Functions + + public static bool AllowMainMenuDisplayTypeForQuickPlaylist_(LevelSelectMenuLogic levelSelectMenu) + { + if (Mod.Instance.Config.EnableChooseMainMenuQuickPlaylist) + { + return true; + } + return levelSelectMenu.displayType_ != LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel; + } + + #endregion + } +} diff --git a/Distance.LevelSelectAdditions/Mod.cs b/Distance.LevelSelectAdditions/Mod.cs index 6493b8c..b19af83 100644 --- a/Distance.LevelSelectAdditions/Mod.cs +++ b/Distance.LevelSelectAdditions/Mod.cs @@ -109,6 +109,7 @@ private void CreateSettingsMenu() MenuTree settingsMenu = new MenuTree("menu.mod." + Mod.Name.ToLower(), Mod.FriendlyName); + // Page 1 settingsMenu.CheckBox(MenuDisplayMode.MainMenu, "setting:workshop_hide_levels_in_playlists", "HIDE WORKSHOP LEVELS IN PLAYLISTS", @@ -198,6 +199,42 @@ private void CreateSettingsMenu() description: "Shows extra sprint campaign level sets that aren't normally available (requires unlock)."); + // Page 2 + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:levelsets_enable_levelsets_options_menu", + "ENABLE PLAYLIST OPTIONS MENU", + () => Config.EnableLevelSetOptionsMenu, + (value) => Config.EnableLevelSetOptionsMenu = value, + description: "Enables the Options menu in the Level Set grid view for customizing personal playlists."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:levelsets_enable_choose_mainmenu_quick_playlist", + "ENABLE PLAYLIST MODE FOR MAIN MENUS", + () => Config.EnableChooseMainMenuQuickPlaylist, + (value) => Config.EnableChooseMainMenuQuickPlaylist = value, + description: "Allows creating playlists when choosing a Main Menu level (does not allow selecting multiple levels)."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:gui_enable_choose_mainmenu_workshop_button", + "ENABLE WORKSHOP BUTTON FOR MAIN MENUS", + () => Config.EnableChooseMainMenuVisitWorkshopButton, + (value) => Config.EnableChooseMainMenuVisitWorkshopButton = value, + description: "Enables the 'Visit Workshop page' button in the Advanced level select menu when choosing a Main Menu level."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:gui_enable_rate_workshop_level_button", + "ENABLE RATE WORKSHOP LEVEL BUTTON", + () => Config.EnableRateWorkshopLevelButton, + (value) => Config.EnableRateWorkshopLevelButton = value, + description: "Re-introduces the 'Rate this level' button in the Advanced level select menu."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:gui_hide_choose_mainmenu_unused_buttons", + "HIDE UNUSED BUTTONS FOR MAIN MENUS", + () => Config.HideChooseMainMenuUnusedButtons, + (value) => Config.HideChooseMainMenuUnusedButtons = value, + description: "Hides unused buttons in the Advanced level select menu when choosing a Main Menu level."); + Menus.AddNew(MenuDisplayMode.MainMenu, settingsMenu, Mod.FriendlyName.ToUpper(), "Settings for level selection limits, filtering, sorting, and organization."); diff --git a/Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs b/Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs index a9c0f16..128572d 100644 --- a/Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs +++ b/Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs @@ -32,8 +32,8 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.2.0")] -[assembly: AssemblyFileVersion("1.1.2.0")] +[assembly: AssemblyVersion("1.1.3.0")] +[assembly: AssemblyFileVersion("1.1.3.0")] diff --git a/Distance.LevelSelectAdditions/Scripts/LevelPlaylistSelectRenameLogic.cs b/Distance.LevelSelectAdditions/Scripts/LevelPlaylistSelectRenameLogic.cs index 3688b63..78173a3 100644 --- a/Distance.LevelSelectAdditions/Scripts/LevelPlaylistSelectRenameLogic.cs +++ b/Distance.LevelSelectAdditions/Scripts/LevelPlaylistSelectRenameLogic.cs @@ -1,9 +1,4 @@ -using Centrifuge.Distance.Data; -using Centrifuge.Distance.Game; -using Distance.LevelSelectAdditions.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; +using Distance.LevelSelectAdditions.Extensions; using UnityEngine; namespace Distance.LevelSelectAdditions.Scripts @@ -46,7 +41,7 @@ private void SetupPlaylistRenameButton() GameObject playlistButtonGroup2 = this.transform.Find("Panel - Level Select/Anchor - Center/Left Panel/PlaylistButtonGroup2")?.gameObject; if (!playlistButtonGroup2) { - Mod.Instance.Logger.Error("\"PlaylistButtonGroup2\" component not found"); + Mod.Instance.Logger.Error("\"PlaylistButtonGroup2\" game object not found"); return; } @@ -133,7 +128,7 @@ private void OnPlaylistRenameButtonClicked() private void OnPlaylistRenameSubmit(bool changed) { - this.levelSelectMenu_.quickPlaylistLabel_.text = this.levelSelectMenu_.tempPlaylist_.Name_; + this.levelSelectMenu_.UpdateQuickPlaylistText(); } } } diff --git a/Distance.LevelSelectAdditions/Scripts/LevelSelectWorkshopRateButtonLogic.cs b/Distance.LevelSelectAdditions/Scripts/LevelSelectWorkshopRateButtonLogic.cs new file mode 100644 index 0000000..a65a01c --- /dev/null +++ b/Distance.LevelSelectAdditions/Scripts/LevelSelectWorkshopRateButtonLogic.cs @@ -0,0 +1,232 @@ +using UnityEngine; + +namespace Distance.LevelSelectAdditions.Scripts +{ + public class LevelSelectWorkshopRateButtonLogic : MonoBehaviour + { + private LevelSelectMenuLogic levelSelectMenu_; + private GameObject visitButton_; + private GameObject rateButton_; + private UILabel votingLabel_; + + // Preserve this information if the user has the rate button disabled in the config. + private float visitButtonNewPositionX_; + private int visitButtonNewWidth_; + private float visitButtonOldPositionX_; + private int visitButtonOldWidth_; + + + // Call during LevelSelectMenuLogic.Initialize to match the current `EnableRateWorkshopLevelButton` setting. + public void Initialize() + { + if (!this.levelSelectMenu_) + { + return; // Failed initialization, don't do anything. + } + + if (this.visitButton_ != null && this.rateButton_ != null) + { + UIWidget visitWidget = this.visitButton_.GetComponent(); + + if (Mod.Instance.Config.EnableRateWorkshopLevelButton) + { + this.visitButton_.transform.SetGlobalPosX(this.visitButtonNewPositionX_); + visitWidget.width = this.visitButtonNewWidth_; + this.rateButton_.SetActive(true); + } + else + { + this.visitButton_.transform.SetGlobalPosX(this.visitButtonOldPositionX_); + visitWidget.width = this.visitButtonOldWidth_; + this.rateButton_.SetActive(false); + } + } + } + + + private void Awake() + { + this.levelSelectMenu_ = this.GetComponentInParent(); + if (!this.levelSelectMenu_) + { + Mod.Instance.Logger.Error(nameof(LevelSelectMenuLogic) + " component not found"); + return; + } + + SetupWorkshopRateButton(); + Initialize(); // Reflect the current `EnableRateWorkshopLevelButton` setting. + } + + private void Update() + { + if (this.levelSelectMenu_ == null) + { + return; // Failed initialization, don't do anything. + } + + // LAZY MODE: + // We *could* choose to only update the label when the rating is changed by handling OnRateLevelPanelPop, + // (and when choosing a new entry with SelectEntry)... But that's extra hooking that isn't necessary, + // so just update the label every cycle. + this.SetRatingText(); + + + if (this.levelSelectMenu_.IsMenuActiveAndTop_) + { + this.UpdateInput(); + } + } + + private void UpdateInput() + { + if (this.levelSelectMenu_.ignoreMenuInputForOneFrame_ || !G.Sys.MenuPanelManager_.MenuInputEnabled_ || + this.levelSelectMenu_.SearchButtonSelected_) + { + return; + } + + // TODO: If we want a keybind bound to rating the level, handle input for it here. + } + + // | --------------------------------------------- | + // | Personal Best: N/A | + // | Updated: 3/2/2021 unplayed | + // | Difficulty: Advanced | + // | Workshop Rating: # # # # * | + // | Votes: 456 (87.65% positive) | + // |_______________________________________________| + // _______________________________________________ + // |___________| Visit Workshop page |___________| + // + // V change bottom bar into V + // _______________________________________________ + // |_| My Rating: None || Visit Workshop page |_| + // + private void SetupWorkshopRateButton() + { + // Full path: "LevelSelectRoot/Panel - Level Select/Anchor - Center/Left Panel/WorkshopItemControlsPanel/VisitWorkshopButton" + GameObject visitButton = this.transform.Find("Panel - Level Select/Anchor - Center/Left Panel/WorkshopItemControlsPanel/VisitWorkshopButton")?.gameObject; + if (!visitButton) + { + Mod.Instance.Logger.Error("\"VisitWorkshopButton\" game object not found"); + return; + } + this.visitButton_ = visitButton; + + // Create a copy of VisitWorkshopButton to be our new rate button. + GameObject rateButton = UnityEngine.Object.Instantiate(visitButton, visitButton.transform.parent); + rateButton.name = "RateWorkshopLevelButton"; + this.rateButton_ = rateButton; + + // Reposition the button (to the right) to make room for our new button. + // NOTE: Unlike with the Quick Playlist Rename Button, the positions used here are global, and not tied to the parent panel shape. + // Additionally, the width field is an int, so we have less flexibility with resizing. So use precalculated values. + // _______________________________________________ + // |___________| Visit Workshop page |___________| + // + // V change bottom bar into V + // _______________________________________________ + // |_| My Rating: None || Visit Workshop page |_| + + Vector3 visitPos = visitButton.transform.position; + Vector3 ratePos = visitPos;// rateButton.transform.position; + // initial x pos: -0.56f + + this.visitButtonOldPositionX_ = visitPos.x; + + visitPos.x -= (-0.56f - -0.365f); // (+= 0.195f) panel center => far right + ratePos.x -= (-0.56f - -0.795f); // (-= 0.235f) panel center => far left + + this.visitButtonNewPositionX_ = visitPos.x; + + visitButton.transform.position = visitPos; + rateButton.transform.position = ratePos; + + + // Shrink the rate button width so that both buttons fit inside the panel area. + // NGUI widgets handle all aspects of resizing button components (...thankfully). + UIWidget visitWidget = visitButton.GetComponent(); + UIWidget rateWidget = rateButton.GetComponent(); + // initial width: 184 (~247px) + // initial height: 24 ( ~28px) + + this.visitButtonOldWidth_ = visitWidget.width; + + rateWidget.width = 157; // (~209px) + + this.visitButtonNewWidth_ = visitWidget.width; // Unchanged at the moment + + + // What does this actually do? Is it necessary? + // (This is coppied from Gsl.Centrifuge.Distance's `VersionNumber`) + rateButton.ForEachChildObjectDepthFirstRecursive((obj) => { + obj.SetActive(true); + }); + + // Change the label text for our button. + // This function does the same thing as the 2 lines below it. + //LevelSelectMenuLogic.SetChildLabelText(rateButton, "My Rating: None"); + UILabel rateLabel = rateButton.GetComponentInChildren(); + // Use this as dummy text, so that SetRatingText can choose to only function while the rate button setting is enabled. + rateLabel.text = "Rate this level"; // OLD BUTTON TEXT that was used before this feature was removed. + this.votingLabel_ = rateLabel; // Store the label so that we can update its text at any time. + this.SetRatingText(); // Set the initial text for the rate button label. + + + // Change the `onClick` event for our button. + // There are two UIButton components in here (for foreground and background). + // We want to find the one with the `onClick` event assigned. + foreach (var button in rateButton.GetComponents()) + { + if (button.onClick != null && button.onClick.Count > 0) + { + button.onClick.Clear(); + button.onClick.Add(new EventDelegate(OnWorkshopRateButtonClicked)); + + break; + } + } + } + + private void OnWorkshopRateButtonClicked() + { + // Rating is already handled by this *previously-unused* level select function. + // The `RateLevel` function handles all safety checks like whether this is really + // a workshop level or not. So we don't have to do anything extra. + this.levelSelectMenu_.RateLevel(); + } + + /// + /// Code duplication: + /// + private void SetRatingText() + { + // Make sure to honor the Workshop Rating Privacy Mode setting. + // Note that this is actually handled much earlier on by `WorkshopLevelInfo.WorkshopVote_`, + // which returns 'No vote' when in privacy mode, but we want to follow convention with `FinishMenuLogic`. + if (G.Sys.OptionsManager_.General_.WorkshopRatingPrivacyMode_) + { + this.votingLabel_.text = "Rating: Hidden"; // Interestingly, the "My" is excluded only for this. + return; + } + + var entry = this.levelSelectMenu_.selectedEntry_; + // CHANGE: Use `[c][/c]` tags to ensure we get the exact color we want. + switch (entry?.myWorkshopVoteIndex_ ?? 2) // Fallback to 'No vote' if no entry is selected. + { + case 0: // Vote for + this.votingLabel_.text = "My Rating: [c][96FFB1]+[-][/c]"; + break; + + case 1: // Vote against + this.votingLabel_.text = "My Rating: [c][FF9796]-[-][/c]"; + break; + + //case 2: // No vote + default: + this.votingLabel_.text = "My Rating: None"; + break; + } + } + } +} diff --git a/Distance.LevelSelectAdditions/Scripts/Menus/LevelSetOptionsMenu.cs b/Distance.LevelSelectAdditions/Scripts/Menus/LevelSetOptionsMenu.cs index f360f12..1266ab4 100644 --- a/Distance.LevelSelectAdditions/Scripts/Menus/LevelSetOptionsMenu.cs +++ b/Distance.LevelSelectAdditions/Scripts/Menus/LevelSetOptionsMenu.cs @@ -92,20 +92,11 @@ protected void UpdateMenuTitle() // Displays the playlist name below the menu title. protected void UpdateMenuDescription() { - Color color = Color.white; - bool colorTag = this.playlist_.Name_.DecodeNGUIColorTag(out _); - if (!colorTag) + Color baseColor = Color.white; + var playlistData = this.playlist_.GetComponent(); + if (playlistData?.PlaylistEntry != null) { - // No color tag, so we need to use the multiplier color. - var playlistData = this.playlist_.GetComponent(); - if (playlistData?.PlaylistEntry != null) - { - color = playlistData.PlaylistEntry.Color_; - } - else - { - color = GConstants.myLevelColor_; - } + baseColor = playlistData.PlaylistEntry.Color_; } UILabel titleLabelObject = this.TitleLabel.GetComponent(); @@ -114,7 +105,7 @@ protected void UpdateMenuDescription() this.DescriptionLabel?.SetActive(true); descriptionLabelObject.text = this.playlist_.Name_; - descriptionLabelObject.color = color; + descriptionLabelObject.color = baseColor; // Use same font as playlist entries in Level Sets menu. descriptionLabelObject.fontStyle = titleLabelObject.fontStyle; // FontStyle.Normal; diff --git a/README.md b/README.md index abc0982..8aa5cae 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Distance mod that extends the level selection UI by adding more options. * Change Display Color * Rename Playlist File * Delete Playlist File +* Saving a playlist no longer overwrites its display name with the file name. Instead, the display name can be changed with the *Name* button when in Playlist Mode. * Remember the last-accessed level set/playlist, so that the game will return to your original place after playing a level appearing in multiple playlists. -* Fix scrolling bug in level list where you could only scroll to the very top or bottom entry. -* Make the Visit Workshop page button visible when Choosing Main Menu levels. +* Fix scrolling bug in Advanced level select menu where you could only scroll to the very top or bottom entry. ## Current Options @@ -25,6 +25,11 @@ Distance mod that extends the level selection UI by adding more options. * Finish Status * Medals Earned * Show hidden sprint campaign level sets. +* Enable or disable the Playlist Options menu. +* Allow creating playlists when Choosing Main Menu levels. +* Make the *Visit Workshop page* button visible in the Advanced level select menu when Choosing Main Menu levels. +* Re-introduce the *Rate this level* button in the Advanced level select menu. +* Hide unused buttons that appear in the Advanced level select menu when Choosing Main Menu levels. ## Known Bugs