From f8a8021b5ab2687798c37581d8b30a2b4a745793 Mon Sep 17 00:00:00 2001 From: Robert Jordan Date: Tue, 28 Jun 2022 20:32:25 -0400 Subject: [PATCH] Initial codebase commit (v1.0.0.0) Initial codebase commit with original working goal for the mod: * Support for changing Workshop LevelSet. * Remove or change level limit. * Optionally filter out levels in personal playlists. * Up to 3 layers of sorting methods with option to reverse individual methods. --- .env | 8 +- ...nce.LevelSelectAdditions.Content.projitems | 6 +- ...stance.LevelSelectAdditions.Content.shproj | 8 +- .../Mod/mod.json | 14 + ...e.sln => Distance.LevelSelectAdditions.sln | 9 +- .../ConfigurationLogic.cs | 169 +++++++++ .../Distance.LevelSelectAdditions.csproj | 11 +- .../Distance.LevelSelectAdditions.targets | 6 +- .../LevelGridMenu/CreateAndAddLevelSet.cs | 60 +++ Distance.LevelSelectAdditions/LevelFilter.cs | 103 +++++ Distance.LevelSelectAdditions/LevelSort.cs | 354 ++++++++++++++++++ Distance.LevelSelectAdditions/Mod.cs | 185 +++++++++ .../Properties/AssemblyInfo.cs | 9 +- .../packages.config | 0 .../LevelGridMenu/CreateAndAddLevelSet.cs | 187 +++++++++ Mod.Template.Content/Mod/mod.json | 12 - Mod.Template/Mod.cs | 57 --- README.md | 24 +- setup.ps1 | 97 ----- 19 files changed, 1122 insertions(+), 197 deletions(-) rename Mod.Template.Content/Mod.Template.Content.projitems => Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.projitems (88%) rename Mod.Template.Content/Mod.Template.Content.shproj => Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.shproj (86%) create mode 100644 Distance.LevelSelectAdditions.Content/Mod/mod.json rename Mod.Template.sln => Distance.LevelSelectAdditions.sln (70%) create mode 100644 Distance.LevelSelectAdditions/ConfigurationLogic.cs rename Mod.Template/Mod.Template.csproj => Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj (93%) rename Mod.Template/Mod.Template.targets => Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.targets (62%) create mode 100644 Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs create mode 100644 Distance.LevelSelectAdditions/LevelFilter.cs create mode 100644 Distance.LevelSelectAdditions/LevelSort.cs create mode 100644 Distance.LevelSelectAdditions/Mod.cs rename {Mod.Template => Distance.LevelSelectAdditions}/Properties/AssemblyInfo.cs (86%) rename {Mod.Template => Distance.LevelSelectAdditions}/packages.config (100%) create mode 100644 Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs delete mode 100644 Mod.Template.Content/Mod/mod.json delete mode 100644 Mod.Template/Mod.cs delete mode 100644 setup.ps1 diff --git a/.env b/.env index 13efeba..2379411 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ -SOLUTION=Mod.Template.sln -ARTIFACTS=Build/Distance Mod Template.zip -RELEASE_BODY=This release was automatically generated as part of a GitHub workflow. Please read the repository README for more info. \ No newline at end of file +SOLUTION=Distance.LevelSelectAdditions.sln +ARTIFACTS=Build/Distance Level Select Additions.zip +RELEASE_BODY=This release was automatically generated as part of a GitHub workflow. Please read the repository README for more info. + + diff --git a/Mod.Template.Content/Mod.Template.Content.projitems b/Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.projitems similarity index 88% rename from Mod.Template.Content/Mod.Template.Content.projitems rename to Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.projitems index 8289aae..4250ef9 100644 --- a/Mod.Template.Content/Mod.Template.Content.projitems +++ b/Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.projitems @@ -6,7 +6,7 @@ 4e7d37c7-8afe-4f79-baf6-4ce82d05e091 - Mod.Template.Content + Distance.LevelSelectAdditions.Content @@ -16,4 +16,6 @@ - \ No newline at end of file + + + diff --git a/Mod.Template.Content/Mod.Template.Content.shproj b/Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.shproj similarity index 86% rename from Mod.Template.Content/Mod.Template.Content.shproj rename to Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.shproj index d4502a8..53b8666 100644 --- a/Mod.Template.Content/Mod.Template.Content.shproj +++ b/Distance.LevelSelectAdditions.Content/Distance.LevelSelectAdditions.Content.shproj @@ -1,4 +1,4 @@ - + 4e7d37c7-8afe-4f79-baf6-4ce82d05e091 @@ -8,6 +8,8 @@ - + - \ No newline at end of file + + + diff --git a/Distance.LevelSelectAdditions.Content/Mod/mod.json b/Distance.LevelSelectAdditions.Content/Mod/mod.json new file mode 100644 index 0000000..f053137 --- /dev/null +++ b/Distance.LevelSelectAdditions.Content/Mod/mod.json @@ -0,0 +1,14 @@ +{ + "FriendlyName": "Level Select Additions", + "Description": "Extends the level selection UI by adding more options.", + "Author": "trigger_segfault", + "Contact": "Github: @trigger-segfault; Discord: trigger_segfault#7281", + "ModuleFileName": "Distance.LevelSelectAdditions.dll", + "RequiredGSLs": [ + "com.github.reherc/Centrifuge.Distance" + ], + "SkipLoad": false, + "Priority": 10 +} + + diff --git a/Mod.Template.sln b/Distance.LevelSelectAdditions.sln similarity index 70% rename from Mod.Template.sln rename to Distance.LevelSelectAdditions.sln index 5ea1c6d..cd29429 100644 --- a/Mod.Template.sln +++ b/Distance.LevelSelectAdditions.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31515.178 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Mod.Template.Content", "Mod.Template.Content\Mod.Template.Content.shproj", "{4E7D37C7-8AFE-4F79-BAF6-4CE82D05E091}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Distance.LevelSelectAdditions.Content", "Distance.LevelSelectAdditions.Content\Distance.LevelSelectAdditions.Content.shproj", "{4E7D37C7-8AFE-4F79-BAF6-4CE82D05E091}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mod.Template", "Mod.Template\Mod.Template.csproj", "{7BCB2908-B003-45D9-BE68-50CBA5217603}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Distance.LevelSelectAdditions", "Distance.LevelSelectAdditions\Distance.LevelSelectAdditions.csproj", "{7BCB2908-B003-45D9-BE68-50CBA5217603}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{08A1C9FA-7DC2-4A60-B9A6-AC0EB7132893}" ProjectSection(SolutionItems) = preProject @@ -17,7 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution - Mod.Template.Content\Mod.Template.Content.projitems*{4e7d37c7-8afe-4f79-baf6-4ce82d05e091}*SharedItemsImports = 13 + Distance.LevelSelectAdditions.Content\Distance.LevelSelectAdditions.Content.projitems*{4e7d37c7-8afe-4f79-baf6-4ce82d05e091}*SharedItemsImports = 13 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,3 +36,6 @@ Global SolutionGuid = {E6970D48-CD23-4427-951B-448B144F30DA} EndGlobalSection EndGlobal + + + diff --git a/Distance.LevelSelectAdditions/ConfigurationLogic.cs b/Distance.LevelSelectAdditions/ConfigurationLogic.cs new file mode 100644 index 0000000..3aa1c99 --- /dev/null +++ b/Distance.LevelSelectAdditions/ConfigurationLogic.cs @@ -0,0 +1,169 @@ +using Reactor.API.Configuration; +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using SortingMethod = LevelSelectMenuLogic.SortingMethod; + +namespace Distance.LevelSelectAdditions +{ + public class ConfigurationLogic : MonoBehaviour + { + #region Properties + + private const string HideWorkshopLevelsInPlaylists_ID = "workshop.hide_levels_in_playlists"; + public bool HideWorkshopLevelsInPlaylists + { + get => Get(HideWorkshopLevelsInPlaylists_ID); + set => Set(HideWorkshopLevelsInPlaylists_ID, value); + } + + private const string WorkshopLevelLimit_ID = "workshop.level_limit"; + public int WorkshopLevelLimit + { + get => Get(WorkshopLevelLimit_ID); + set => Set(WorkshopLevelLimit_ID, value); + } + + private const string WorkshopSortingMethod_ID = "workshop.sorting_method"; + public SortingMethod WorkshopSortingMethod + { + get => Get(WorkshopSortingMethod_ID); + set => Set(WorkshopSortingMethod_ID, value); + } + + private const string WorkshopSortingMethod2_ID = "workshop.sorting_method2"; + public SortingMethod WorkshopSortingMethod2 + { + get => Get(WorkshopSortingMethod2_ID); + set => Set(WorkshopSortingMethod2_ID, value); + } + + private const string WorkshopSortingMethod3_ID = "workshop.sorting_method3"; + public SortingMethod WorkshopSortingMethod3 + { + get => Get(WorkshopSortingMethod3_ID); + set => Set(WorkshopSortingMethod3_ID, value); + } + + private const string WorkshopReverseSortingMethod_ID = "workshop.reverse_sorting_method"; + public bool WorkshopReverseSortingMethod + { + get => Get(WorkshopReverseSortingMethod_ID); + set => Set(WorkshopReverseSortingMethod_ID, value); + } + + private const string WorkshopReverseSortingMethod2_ID = "workshop.reverse_sorting_method2"; + public bool WorkshopReverseSortingMethod2 + { + get => Get(WorkshopReverseSortingMethod2_ID); + set => Set(WorkshopReverseSortingMethod2_ID, value); + } + + private const string WorkshopReverseSortingMethod3_ID = "workshop.reverse_sorting_method3"; + public bool WorkshopReverseSortingMethod3 + { + get => Get(WorkshopReverseSortingMethod3_ID); + set => Set(WorkshopReverseSortingMethod3_ID, value); + } + + // Future plans: + /*private const string EnableRecentlDownloadsLevelSet_ID = "recentdownloads.enable"; + public bool EnableRecentlDownloadsLevelSet + { + get => Get(EnableRecentlDownloadsLevelSet_ID); + set => Set(EnableRecentlDownloadsLevelSet_ID, value); + } + + private const string RecentDownloadedsTimeInDays_ID = "recentdownloads.time_in_days"; + public double RecentDownloadedsTimeInDays + { + get => Get(RecentDownloadedsTimeInDays_ID); + set => Set(RecentDownloadedsTimeInDays_ID, value); + } + + private const string HideRecentDownloadsLevelsInPlaylists_ID = "recentdownloads.hide_levels_in_playlists"; + public bool HideRecentDownloadsLevelsInPlaylists + { + get => Get(HideRecentDownloadsLevelsInPlaylists_ID); + set => Set(HideRecentDownloadsLevelsInPlaylists_ID, value); + } + + private const string RecentDownloadsLevelLimit_ID = "recentdownloads.level_limit"; + public int RecentDownloadsLevelLimit + { + get => Get(RecentDownloadsLevelLimit_ID); + set => Set(RecentDownloadsLevelLimit_ID, value); + }*/ + + #endregion + + #region Helpers + + public SortingMethod[] GetWorkshopSortingMethods() + { + return new SortingMethod[] + { + WorkshopSortingMethod, + WorkshopSortingMethod2, + WorkshopSortingMethod3, + }; + } + public bool[] GetWorkshopReverseSortingMethods() + { + return new bool[] + { + WorkshopReverseSortingMethod, + WorkshopReverseSortingMethod2, + WorkshopReverseSortingMethod3, + }; + } + + #endregion + + internal Settings Config; + + public event Action OnChanged; + + private void Load() + { + Config = new Settings("Config");// Mod.FullName); + } + + public void Awake() + { + Load(); + + // Assign default settings (if not already assigned). + Get(HideWorkshopLevelsInPlaylists_ID, false); + Get(WorkshopLevelLimit_ID, 1000); + Get(WorkshopSortingMethod_ID, LevelSort.Recently_Downloaded); + Get(WorkshopSortingMethod2_ID, LevelSort.None); + Get(WorkshopSortingMethod3_ID, LevelSort.None); + Get(WorkshopReverseSortingMethod_ID, false); + Get(WorkshopReverseSortingMethod2_ID, false); + Get(WorkshopReverseSortingMethod3_ID, false); + + // Save settings, and any defaults that may have been added. + Save(); + } + + public T Get(string key, T @default = default) + { + return Config.GetOrCreate(key, @default); + } + + public void Set(string key, T value) + { + Config[key] = value; + Save(); + } + + public void Save() + { + Config?.Save(); + OnChanged?.Invoke(this); + } + } +} diff --git a/Mod.Template/Mod.Template.csproj b/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj similarity index 93% rename from Mod.Template/Mod.Template.csproj rename to Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj index 186296c..3c58a95 100644 --- a/Mod.Template/Mod.Template.csproj +++ b/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.csproj @@ -7,8 +7,8 @@ {7BCB2908-B003-45D9-BE68-50CBA5217603} Library Properties - Mod.Template - Mod.Template + Distance.LevelSelectAdditions + Distance.LevelSelectAdditions v3.5 512 true @@ -86,13 +86,18 @@ + + + + - + + diff --git a/Mod.Template/Mod.Template.targets b/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.targets similarity index 62% rename from Mod.Template/Mod.Template.targets rename to Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.targets index fc29990..22f3431 100644 --- a/Mod.Template/Mod.Template.targets +++ b/Distance.LevelSelectAdditions/Distance.LevelSelectAdditions.targets @@ -1,5 +1,7 @@  - Distance Mod Template + Distance Level Select Additions - \ No newline at end of file + + + diff --git a/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs new file mode 100644 index 0000000..d7e9ace --- /dev/null +++ b/Distance.LevelSelectAdditions/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs @@ -0,0 +1,60 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; + +//using SortingMethod = LevelSelectMenuLogic.SortingMethod; + +namespace Distance.LevelSelectAdditions.Harmony +{ + /// + /// A patch applied to the method that handles preparing and adding LevelSets to the list. + /// + /// Here we need to add our own handling for Workshop LevelSets and remove the original levelPlaylist.LimitCountTo(100). + /// + [HarmonyPatch(typeof(LevelGridMenu), nameof(LevelGridMenu.CreateAndAddLevelSet))] + internal static class LevelGridMenu__CreateAndAddLevelSet + { + + internal static bool Prefix(LevelGridMenu __instance, LevelSet set, string name, LevelGridMenu.PlaylistEntry.Type type, LevelGroupFlags flags) + { + // Only perform special handling for Workshop LevelSets (since that's all that's currently supported). + if (type == LevelGridMenu.PlaylistEntry.Type.Workshop) + { + LevelPlaylist levelPlaylist = LevelPlaylist.Create(set, name, flags); + LevelGridMenu.PlaylistEntry.UnlockStyle unlockStyle = LevelGridMenu.PlaylistEntry.UnlockStyle.None; + + // Filter out levels in personal playlists, if the user has specified this. + if (Mod.Instance.Config.HideWorkshopLevelsInPlaylists) + { + LevelFilter.ExcludeLevelsInPersonalPlaylists(levelPlaylist, __instance.modeID_); + } + + // Apply custom sorting. + { + var sorter = new LevelSort(Mod.Instance.Config.GetWorkshopSortingMethods(), + Mod.Instance.Config.GetWorkshopReverseSortingMethods()); + sorter.SortPlaylist(__instance, levelPlaylist); + } + + // Finally apply limit after sorting and potentially removing levels that were in playlists. + if (Mod.Instance.Config.WorkshopLevelLimit != LevelFilter.Infinite) + { + LevelFilter.LimitLevels(levelPlaylist, Mod.Instance.Config.WorkshopLevelLimit); + } + + + // All done! Add the organized LevelPlaylist entry. + __instance.CreateAndAddEntry(levelPlaylist, type, true, unlockStyle, string.Empty); + + + // Skip the original method, since we've fully handled this level set. + return false; + } + + // Fallback to the original method for anything that's not a Workshop LevelSet. + return true; + } + + } +} diff --git a/Distance.LevelSelectAdditions/LevelFilter.cs b/Distance.LevelSelectAdditions/LevelFilter.cs new file mode 100644 index 0000000..9e713b6 --- /dev/null +++ b/Distance.LevelSelectAdditions/LevelFilter.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Distance.LevelSelectAdditions +{ + public class LevelFilter + { + #region Limit Constants + + public const int Infinite = -1; + + public static int[] GetArbitraryLimitsList() + { + return new int[] + { + Infinite, + 25, + 50, + 100, + 250, + 500, + 750, + 1000, + 1500, + 2000, + // Anything higher may as well be infinite... + }; + } + + public static string GetLimitName(int limit) + { + switch (limit) + { + case Infinite: + return nameof(Infinite); + + default: + return limit.ToString(); + } + } + + #endregion + + public static void LimitLevels(LevelPlaylist levelPlaylist, int limit) + { + if (limit != Infinite && limit >= 0) + { + levelPlaylist.LimitCountTo(limit); + } + } + + public static void ExcludeLevelsInPersonalPlaylists(LevelPlaylist levelPlaylist, GameModeID modeID) + { + if (DirectoryEx.Exists(Resource.PersonalLevelPlaylistsDirPath_)) + { + // Create a list of all levels found in playlists. + // NOTE: This does not perform any normalization. This is expected to already be handled by the LevelPlaylist xml file. + HashSet levelsInPersonalPlaylists = new HashSet(); + + List filePathsInDirWithPattern = Resource.GetFilePathsInDirWithPattern(Resource.PersonalLevelPlaylistsDirPath_, "*.xml", null); + filePathsInDirWithPattern.RemoveAll((string s) => !Resource.FileExist(s)); + foreach (string absolutePath in filePathsInDirWithPattern) + { + LevelPlaylist personalPlaylist = LevelGridMenu.LoadPlaylist(absolutePath); + if (!levelPlaylist) + { + Mod.Instance.Logger.Info("Failed to load: " + absolutePath); + continue; + } + if (personalPlaylist.Count_ == 0) + { + continue; + } + if (personalPlaylist.FirstModeID_ != modeID) + { + continue; + } + + for (int i = 0; i < personalPlaylist.Count_; i++) + { + levelsInPersonalPlaylists.Add(personalPlaylist.GetLevelNameAndPathPairAtIndex(i).levelPath_); + } + } + + for (int i = 0; i < levelPlaylist.Count_; i++) + { + LevelNameAndPathPair levelNameAndPathPair = levelPlaylist.GetLevelNameAndPathPairAtIndex(i); + + // TODO: Is there any possibility that the Workshop playlist could mess up and have duplicates? + // If so, then we shouldn't remove items from the hashset. + if (levelsInPersonalPlaylists.Remove(levelNameAndPathPair.levelPath_)) + //if (levelsInPersonalPlaylists.Contains(levelNameAndPathPair.levelPath_)) + { + levelPlaylist.Remove(i); + i--; + } + } + } + } + + } +} diff --git a/Distance.LevelSelectAdditions/LevelSort.cs b/Distance.LevelSelectAdditions/LevelSort.cs new file mode 100644 index 0000000..9c13bcc --- /dev/null +++ b/Distance.LevelSelectAdditions/LevelSort.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using SortingMethod = LevelSelectMenuLogic.SortingMethod; + +namespace Distance.LevelSelectAdditions +{ + public class LevelSort + { + #region Sorting Method Constants + + public const SortingMethod None = (SortingMethod)(-1); + //public const SortingMethod Shuffle = (SortingMethod)(-2); + + public const SortingMethod Level_Name = SortingMethod.Level_Name; + public const SortingMethod Author = SortingMethod.Author; + public const SortingMethod Recently_Downloaded = SortingMethod.Recently_Downloaded; + //public const SortingMethod Recently_Published = SortingMethod.Recently_Published; + //public const SortingMethod Recently_Updated = SortingMethod.Recently_Updated; + //public const SortingMethod Recently_Played = SortingMethod.Recently_Played; + public const SortingMethod Difficulty = SortingMethod.Difficulty; + public const SortingMethod Finish_Status = SortingMethod.Finish_Status; + public const SortingMethod Medal_Earned = SortingMethod.Medal_Earned; + //public const SortingMethod Workshop_Rating = SortingMethod.Workshop_Rating; + //public const SortingMethod My_Rating = SortingMethod.My_Rating; + //public const SortingMethod Positive_Votes = SortingMethod.Positive_Votes; + + public static SortingMethod[] GetSupportedMethodsList() + { + return new SortingMethod[] + { + None, + //Shuffle, + + Level_Name, + Author, + Recently_Downloaded, + //Recently_Played, + //Recently_Published, + //Recently_Updated, + Difficulty, + Finish_Status, + Medal_Earned, + //Workshop_Rating, + //My_Rating, + //Positive_Votes, + }; + } + + public static string GetMethodName(SortingMethod sortingMethod) + { + switch (sortingMethod) + { + case LevelSort.None: + return nameof(None); + + //case LevelSort.Shuffle: + // return nameof(Shuffle); + + default: + return sortingMethod.ToString().Replace('_', ' '); + } + } + + #endregion + + #region Fields + + // Condensed array of comparisons determined by SortingMethod[] and bool[] constructor arguments. + private readonly Comparison[] comparisonMethods; + private readonly bool[] reverseMethods; + + // Additional level info needed by sorting methods, used by SortPlaylist() when in LevelGridMenu. + private Dictionary playlistEntries; + + #endregion + + /// + /// Create a sorter with layers of sorting methods. Where if an earlier method returns 0, the next will be used to sort. + /// + public LevelSort(SortingMethod[] sortingMethods, bool[] reverseMethods) + { + Debug.Assert(sortingMethods != null); + Debug.Assert(reverseMethods == null || sortingMethods.Length == reverseMethods.Length); + + List> comparisonList = sortingMethods.Select(x => GetComparison(x)).ToList(); + List reverseList = (reverseMethods ?? new bool[sortingMethods.Length]).ToList(); + + for (int i = 0; i < comparisonList.Count; i++) + { + if (comparisonList[i] == null) + { + // Remove None/unsupported comparisons + comparisonList.RemoveAt(i); + reverseList.RemoveAt(i); + i--; + } + else if (i > 0 && comparisonList[i - 1] == comparisonList[i] && reverseList[i - 1] == reverseList[i]) + { + // Condense repeated comparison types + comparisonList.RemoveAt(i); + reverseList.RemoveAt(i); + i--; + } + } + + this.comparisonMethods = comparisonList.ToArray(); + this.reverseMethods = reverseList.ToArray(); + } + + /// + /// Code duplication: We need YET ANOTHER LevelEntry type, so that we can perform sorting without creating UI elements. + /// + public class LevelSortEntry + { + public LevelNameAndPathPair LevelNameAndPath { get; } + public GameModeID ModeID { get; } + public LevelInfo LevelInfo { get; } + public string AuthorName { get; } + public ProfileProgress.LevelProgress LevelProgress { get; } + public MedalStatus MedalStatus { get; } + + /// + /// Construct a sort entry using information from the . + /// + public LevelSortEntry(LevelSelectMenuLogic levelSelectMenu, LevelNameAndPathPair levelNameAndPath) + { + this.LevelNameAndPath = levelNameAndPath; + this.ModeID = levelSelectMenu.modeID_; + this.LevelInfo = levelSelectMenu.levelSets_.GetLevelInfo(this.LevelNameAndPath.levelPath_); + this.AuthorName = GetAuthorName(this.LevelInfo); + + this.LevelProgress = levelSelectMenu.currentProfile_?.progress_?.GetLevelProgress(this.LevelNameAndPath.levelPath_); + this.MedalStatus = MedalStatus.None; + if (this.LevelProgress != null) + { + this.MedalStatus = this.LevelProgress.GetMedal(this.ModeID); + } + } + + /// + /// Construct a sort entry using information from the . + /// + public LevelSortEntry(LevelGridMenu levelGridMenu, LevelPlaylist.ModeAndLevelInfo modeAndLevelInfo) + { + this.LevelNameAndPath = modeAndLevelInfo.levelNameAndPath_; + this.ModeID = levelGridMenu.modeID_; // modeAndLevelInfo.mode_; + this.LevelInfo = levelGridMenu.levelSets_.GetLevelInfo(this.LevelNameAndPath.levelPath_); + this.AuthorName = GetAuthorName(this.LevelInfo); + + this.LevelProgress = levelGridMenu.profileProgress_?.GetLevelProgress(this.LevelNameAndPath.levelPath_); + this.MedalStatus = MedalStatus.None; + if (this.LevelProgress != null) + { + this.MedalStatus = this.LevelProgress.GetMedal(this.ModeID); + } + } + } + + private static Comparison GetComparison(SortingMethod sortingMethod) + { + switch (sortingMethod) + { + case SortingMethod.Level_Name: + return CompareLevelName; + + case SortingMethod.Author: + return CompareAuthor; + + case SortingMethod.Difficulty: + return CompareDifficulty; + + case SortingMethod.Finish_Status: + return CompareFinishStatus; + + case SortingMethod.Medal_Earned: + return CompareMedalEarned; + + case SortingMethod.Recently_Downloaded: + return CompareMostRecentlyDownloaded; + + // Not supported yet + //case LevelSort.Shuffle: + // return CompareShuffle; + + case LevelSort.None: + return null; + + default: + return null; // Not supported + } + } + + /// + /// Code duplication: + /// + internal static string GetAuthorName(LevelInfo levelInfo) + { + string authorName = "Unknown"; + if (levelInfo.levelType_ == LevelType.Official) + { + authorName = "Refract"; + } + if (levelInfo.levelType_ == LevelType.Community) + { + authorName = (string.IsNullOrEmpty(levelInfo.levelCreatorName_) ? "Community Creator" : levelInfo.levelCreatorName_); + } + else if (levelInfo.levelType_ == LevelType.Workshop) + { + authorName = SteamworksManager.GetSteamName(levelInfo.workshopCreatorID_); + } + return authorName; + } + + #region Sort + + /// + /// Perform sorting for playlists by grabbing additional information from the . + /// + public void SortPlaylist(LevelGridMenu levelGridMenu, LevelPlaylist playlist) + { + // Map levels to entries with extra information needed for most sorting methods. + // This info could also be grabbed on the fly, but it would be slower if you have + // thousands of workshop levels or something. + this.playlistEntries = new Dictionary(); + foreach (LevelPlaylist.ModeAndLevelInfo modeAndLevelInfo in playlist.playlist_) + { + this.playlistEntries.Add(modeAndLevelInfo, new LevelSortEntry(levelGridMenu, modeAndLevelInfo)); + } + /*this.entries = new Dictionary(); + foreach (LevelPlaylist.ModeAndLevelInfo modeAndLevelInfo in playlist.playlist_) + { + this.entries.Add(modeAndLevelInfo.levelNameAndPath_.levelPath_, new LevelSortEntry(levelGridMenu, modeAndLevelInfo)); + //this.entries.Add(modeAndLevelInfo, new LevelSortEntry(levelGridMenu, modeAndLevelInfo)); + }*/ + + playlist.Sort(this.ComparePlaylist); + } + + /// + /// Perform comparisons for playlists. + /// + private int ComparePlaylist(LevelPlaylist.ModeAndLevelInfo x, LevelPlaylist.ModeAndLevelInfo y) + { + // Get our entries containing needed additional information. + LevelSortEntry x_entry = this.playlistEntries[x]; + LevelSortEntry y_entry = this.playlistEntries[y]; + //LevelSortEntry x_entry = this.entries[x.levelNameAndPath_.levelPath_]; + //LevelSortEntry y_entry = this.entries[y.levelNameAndPath_.levelPath_]; + + // Keep applying comparisons down the list, until one returns a difference. + int result = 0; + for (int i = 0; result == 0 && i < this.comparisonMethods.Length; i++) + { + result = this.comparisonMethods[i](x_entry, y_entry); + if (this.reverseMethods[i]) + { + result = -result; + } + } + + return result; + } + + #endregion + + #region Comparisons + + /// + /// Code duplication: + /// + public static int CompareLevelName(LevelSortEntry x, LevelSortEntry y) + { + return LevelSelectMenuLogic.CompareAlphabeticalWithSpecialCharLast(x.LevelInfo.levelName_, y.LevelInfo.levelName_); + } + + /// + /// Code duplication: + /// + /// + public static int CompareAuthor(LevelSortEntry x, LevelSortEntry y) + { + return LevelSelectMenuLogic.CompareAlphabeticalWithSpecialCharLast(x.AuthorName, y.AuthorName); + } + + /// + /// Code duplication: + /// + public static int CompareDifficulty(LevelSortEntry x, LevelSortEntry y) + { + return x.LevelInfo.difficulty_.CompareTo(y.LevelInfo.difficulty_); + } + + /// + /// Code duplication: + /// + /// CHANGE: is treated as . + /// + public static int CompareFinishStatus(LevelSortEntry x, LevelSortEntry y) + { + // Treat MedalStatus.None as MedalStatus.Did_Not_Finish. + bool x_notFinished = x.MedalStatus <= MedalStatus.Did_Not_Finish; + bool y_notFinished = y.MedalStatus <= MedalStatus.Did_Not_Finish; + + if (x_notFinished == y_notFinished) + { + return 0; + } + else if (!x_notFinished) + { + return -1; + } + else //if (!y_notFinished) + { + return 1; + } + } + + /// + /// Code duplication: + /// + /// CHANGE: is treated as the lower than . + /// + public static int CompareMedalEarned(LevelSortEntry x, LevelSortEntry y) + { + MedalStatus x_medalStatus = x.MedalStatus; + MedalStatus y_medalStatus = y.MedalStatus; + + // Treat MedalStatus.None as MedalStatus.Did_Not_Finish. + if (x_medalStatus < MedalStatus.Did_Not_Finish) + { + x_medalStatus = MedalStatus.Did_Not_Finish; + } + if (y_medalStatus < MedalStatus.Did_Not_Finish) + { + y_medalStatus = MedalStatus.Did_Not_Finish; + } + + return y_medalStatus.CompareTo(x_medalStatus); + } + + /// + /// Code duplication: + /// + public static int CompareMostRecentlyDownloaded(LevelSortEntry x, LevelSortEntry y) + { + return y.LevelInfo.fileLastWriteDateTime_.CompareTo(x.LevelInfo.fileLastWriteDateTime_); + } + + #endregion + } +} diff --git a/Distance.LevelSelectAdditions/Mod.cs b/Distance.LevelSelectAdditions/Mod.cs new file mode 100644 index 0000000..3757d6b --- /dev/null +++ b/Distance.LevelSelectAdditions/Mod.cs @@ -0,0 +1,185 @@ +using Centrifuge.Distance.Game; +using Centrifuge.Distance.GUI.Controls; +using Centrifuge.Distance.GUI.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using Reactor.API.Attributes; +using Reactor.API.Interfaces.Systems; +using Reactor.API.Logging; +using Reactor.API.Runtime.Patching; +using UnityEngine; + +using SortingMethod = LevelSelectMenuLogic.SortingMethod; + +namespace Distance.LevelSelectAdditions +{ + /// + /// The mod's main class containing its entry point + /// + [ModEntryPoint("com.github.trigger-segfault/Distance.LevelSelectAdditions")] + public sealed class Mod : MonoBehaviour + { + public const string Name = "LevelSelectAdditions"; + public const string FullName = "Distance." + Name; + public const string FriendlyName = "Level Select Additions"; + + + public static Mod Instance { get; private set; } + + public IManager Manager { get; private set; } + + public Log Logger { get; private set; } + + public ConfigurationLogic Config { get; private set; } + + /// + /// Method called as soon as the mod is loaded. + /// WARNING: Do not load asset bundles/textures in this function + /// The unity assets systems are not yet loaded when this + /// function is called. Loading assets here can lead to + /// unpredictable behaviour and crashes! + /// + public void Initialize(IManager manager) + { + // Do not destroy the current game object when loading a new scene + DontDestroyOnLoad(this); + + Instance = this; + Manager = manager; + + Logger = LogManager.GetForCurrentAssembly(); + Config = gameObject.AddComponent(); + + Logger.Info(Mod.Name + ": Initializing..."); + + try + { + CreateSettingsMenu(); + } + catch (Exception ex) + { + Logger.Error(Mod.Name + ": Error during CreateSettingsMenu()"); + Logger.Exception(ex); + throw; + } + + try + { + RuntimePatcher.AutoPatch(); + } + catch (Exception ex) + { + Logger.Error(Mod.Name + ": Error during RuntimePatcher.AutoPatch()"); + Logger.Exception(ex); + throw; + } + + Logger.Info(Mod.Name + ": Initialized!"); + } + + private void CreateSettingsMenu() + { + // Arbitrary values for limits, since the Input Prompt is kind of a pain. + // You can always change them for more precise values in the config file anyways. + // Maybe the Input Prompt can *also* be added, for more precise changes... + // hopefully that would update the other setting in the menu. + Dictionary limitDictionary = LevelFilter.GetArbitraryLimitsList() + .ToDictionary(l => LevelFilter.GetLimitName(l)); + + Dictionary sortDictionary = LevelSort.GetSupportedMethodsList() + .ToDictionary(s => LevelSort.GetMethodName(s)); + + + MenuTree settingsMenu = new MenuTree("menu.mod." + Mod.Name.ToLower(), Mod.FriendlyName); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:workshop_hide_levels_in_playlists", + "HIDE WORKSHOP LEVELS IN PLAYLISTS", + () => Config.HideWorkshopLevelsInPlaylists, + (value) => Config.HideWorkshopLevelsInPlaylists = value, + description: "Exclude levels from the Workshop Level Set that appear in personal playlists."); + + settingsMenu.ListBox(MenuDisplayMode.MainMenu, + "setting:workshop_level_limit", + "WORKSHOP LEVEL LIMIT", + () => Config.WorkshopLevelLimit, + (value) => Config.WorkshopLevelLimit = value, + limitDictionary, + description: "Set maximum number of levels shown in Workshop Level Set."); + + // Unfortunately, ListBoxes don't support displaying unknown entries, so using this would be ugly :( + /*settingsMenu.InputPrompt(MenuDisplayMode.MainMenu, + "setting:workshop_level_limit", + "CHANGE WORKSHOP LEVEL LIMIT...", + (string x) => { + if (int.TryParse(x, out int result)) + { + Config.WorkshopLevelLimit = Math.Max(//-1 + 0, + result); + } + else + { + Logger.Warning("Failed to parse user input."); + } + }, + title: "NEW LEVEL LIMIT", + defaultValue: null, //"1000", + description: "Manually set the maximum number of levels shown in Workshop Level Set.");*/ + + settingsMenu.ListBox(MenuDisplayMode.MainMenu, + "setting:workshop_sorting_method", + "WORKSHOP SORTING (1ST)", + () => Config.WorkshopSortingMethod, + (value) => Config.WorkshopSortingMethod = value, + sortDictionary, + description: "Choose how Workshop Level Set levels are sorted."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:workshop_reverse_sorting_method", + "REVERSE WORKSHOP SORTING (1ST)", + () => Config.WorkshopReverseSortingMethod, + (value) => Config.WorkshopReverseSortingMethod = value, + description: "Reverse the order of the first Workshop sorting method."); + + settingsMenu.ListBox(MenuDisplayMode.MainMenu, + "setting:workshop_sorting_method2", + "WORKSHOP SORTING (2ND)", + () => Config.WorkshopSortingMethod2, + (value) => Config.WorkshopSortingMethod2 = value, + sortDictionary, + description: "Second fallback method for how Workshop Level Set levels are sorted."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:workshop_reverse_sorting_method2", + "REVERSE WORKSHOP SORTING (2ND)", + () => Config.WorkshopReverseSortingMethod2, + (value) => Config.WorkshopReverseSortingMethod2 = value, + description: "Reverse the order of the second Workshop sorting method."); + + settingsMenu.ListBox(MenuDisplayMode.MainMenu, + "setting:workshop_sorting_method3", + "WORKSHOP SORTING (3RD)", + () => Config.WorkshopSortingMethod3, + (value) => Config.WorkshopSortingMethod3 = value, + sortDictionary, + description: "Third fallback method for how Workshop Level Set levels are sorted."); + + settingsMenu.CheckBox(MenuDisplayMode.MainMenu, + "setting:workshop_reverse_sorting_method3", + "REVERSE WORKSHOP SORTING (3RD)", + () => Config.WorkshopReverseSortingMethod3, + (value) => Config.WorkshopReverseSortingMethod3 = value, + description: "Reverse the order of the third Workshop sorting method."); + + + Menus.AddNew(MenuDisplayMode.MainMenu, settingsMenu, + Mod.FriendlyName.ToUpper(), + "Settings for level selection limits, filtering, sorting, and organization."); + } + } +} + + + diff --git a/Mod.Template/Properties/AssemblyInfo.cs b/Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs similarity index 86% rename from Mod.Template/Properties/AssemblyInfo.cs rename to Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs index 2ff0ed0..24ad219 100644 --- a/Mod.Template/Properties/AssemblyInfo.cs +++ b/Distance.LevelSelectAdditions/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("Distance.ModTemplate")] +[assembly: AssemblyTitle("Distance.LevelSelectAdditions")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Distance.ModTemplate")] -[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyProduct("Distance.LevelSelectAdditions")] +[assembly: AssemblyCopyright("Copyright © Robert Jordan 2022")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -34,3 +34,6 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + + + diff --git a/Mod.Template/packages.config b/Distance.LevelSelectAdditions/packages.config similarity index 100% rename from Mod.Template/packages.config rename to Distance.LevelSelectAdditions/packages.config diff --git a/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs b/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs new file mode 100644 index 0000000..b15120a --- /dev/null +++ b/Harmony/Assembly-CSharp/LevelGridMenu/CreateAndAddLevelSet.cs @@ -0,0 +1,187 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using UnityEngine; + +//using SortingMethod = LevelSelectMenuLogic.SortingMethod; + +namespace Distance.NoWorkshopGridLimit.Harmony +{ + [HarmonyPatch(typeof(LevelGridMenu), nameof(LevelGridMenu.CreateAndAddLevelSet))] + internal static class LevelGridMenu__CreateAndAddLevelSet + { + internal static bool Prefix(LevelGridMenu __instance, LevelSet set, string name, LevelGridMenu.PlaylistEntry.Type type, LevelGroupFlags flags) + { + if (type == LevelGridMenu.PlaylistEntry.Type.Workshop) + { + Mod.Instance.Logger.Info($"No Workshop Grid Limit: LevelGridMenu__CreateAndAddLevelSet.Prefix(): type = {type}");//, __runOriginal = {__runOriginal}"); + + try + { + LevelPlaylist levelPlaylist = LevelPlaylist.Create(set, name, flags); + LevelGridMenu.PlaylistEntry.UnlockStyle unlockStyle = LevelGridMenu.PlaylistEntry.UnlockStyle.None; + //levelPlaylist.Playlist_.Sort() + + // Filter out levels in personal playlists, if the user has specified this. + if (Mod.Instance.Config.HideWorkshopLevelsInPlaylist) + { + Mod.Instance.Logger.Info("Filtering with HideWorkshopLevelsInPlaylist..."); + + if (DirectoryEx.Exists(Resource.PersonalLevelPlaylistsDirPath_)) + { + + HashSet levelsInPersonalPlaylists = new HashSet(); + + List filePathsInDirWithPattern = Resource.GetFilePathsInDirWithPattern(Resource.PersonalLevelPlaylistsDirPath_, "*.xml", null); + filePathsInDirWithPattern.RemoveAll((string s) => !Resource.FileExist(s)); + foreach (string absolutePath in filePathsInDirWithPattern) + { + LevelPlaylist personalPlaylist = LevelGridMenu.LoadPlaylist(absolutePath); + if (!levelPlaylist) + { + Mod.Instance.Logger.Info("Failed to load: " + absolutePath); + continue; + } + if (personalPlaylist.Count_ == 0) + { + continue; + } + if (personalPlaylist.FirstModeID_ != __instance.modeID_) + { + continue; + } + + for (int i = 0; i < personalPlaylist.Count_; i++) + { + levelsInPersonalPlaylists.Add(personalPlaylist.GetLevelNameAndPathPairAtIndex(i).levelPath_); + } + /*foreach (LevelPlaylist.ModeAndLevelInfo modeAndLevelInfo in personalPlaylist.playlist_) { + levelsInPersonalPlaylists.Add(modeAndLevelInfo.levelNameAndPath_.levelPath_); + }*/ + } + + for (int i = 0; i < levelPlaylist.Count_; i++) + { + LevelNameAndPathPair levelNameAndPathPair = levelPlaylist.GetLevelNameAndPathPairAtIndex(i); + + // TODO: Is there any possibility that the Workshop playlist could mess up and have duplicates? + // If so, then we shouldn't remove items from the hashset. + //if (levelsInPersonalPlaylists.Remove(levelNameAndPathPair.levelPath_)) { + if (levelsInPersonalPlaylists.Contains(levelNameAndPathPair.levelPath_)) + { + levelPlaylist.Remove(i); + i--; + } + } + } + + } + + // Apply limit after potentially removing levels that were in playlists. + int limit = Mod.Instance.Config.WorkshopLevelLimit; + if (limit != ConfigurationLogic.LevelLimit_Infinite && limit >= 0) + { + Mod.Instance.Logger.Info("Limiting with WorkshopLevelLimit..."); + levelPlaylist.LimitCountTo(limit); // original: 100 + } + + // Finally apply custom sorting. + /*if (Mod.Instance.Config.WorkshopSortingMethod == SortingMethod.Recently_Downloaded) { + Mod.Instance.Logger.Info("Sorting with with SortByMostRecentlyDownloaded..."); + levelPlaylist.Sort(new Comparison(__instance.SortByMostRecentlyDownloaded)); + } + else + {*/ + LevelSelectMenuLogic.SortingMethod defaultMethod = LevelSelectMenuLogic.SortingMethod.Level_Name; + if (Mod.Instance.Config.WorkshopSortingMethod == LevelSelectMenuLogic.SortingMethod.Level_Name) + { + defaultMethod = LevelSelectMenuLogic.SortingMethod.Recently_Downloaded; + } + Mod.Instance.Logger.Info($"Sorting with with LevelSortHelper {Mod.Instance.Config.WorkshopSortingMethod}..."); + var helper = new LevelSortHelper(new LevelSelectMenuLogic.SortingMethod[] + { + Mod.Instance.Config.WorkshopSortingMethod, + //defaultMethod, + Mod.Instance.Config.WorkshopSortingMethod2, + Mod.Instance.Config.WorkshopSortingMethod3, + }, + new bool[] + { + //false, false,// false, + Mod.Instance.Config.WorkshopReversedSortingMethod, + Mod.Instance.Config.WorkshopReversedSortingMethod2, + Mod.Instance.Config.WorkshopReversedSortingMethod3, + }); + //var helper = new LevelSortHelper(Mod.Instance.Config.GetWorkshopSortingMethods(), + // Mod.Instance.Config.GetWorkshopReversedSortingMethods()); + // // Mod.Instance.Config.WorkshopSortingMethod0, defaultMethod); + helper.SortPlaylist(__instance, levelPlaylist); + //} + /*switch (Mod.Instance.Config.WorkshopSortingMethod) { + case SortingMethod.Level_Name: + levelPlaylist.Sort(new Comparison(helper.SortByLevelName)); + break; + case SortingMethod.Author: + levelPlaylist.Sort(new Comparison(helper.SortByAuthor)); + break; + case SortingMethod.Difficulty: + levelPlaylist.Sort(new Comparison(helper.SortByDifficulty)); + break; + case SortingMethod.Finish_Status: + levelPlaylist.Sort(new Comparison(helper.SortByFinishStatus)); + break; + case SortingMethod.Medal_Earned: + levelPlaylist.Sort(new Comparison(helper.SortByMedalEarned)); + break; + + case SortingMethod.Recently_Downloaded: + levelPlaylist.Sort(new Comparison(__instance.SortByMostRecentlyDownloaded)); + break; + + default: + goto case SortingMethod.Recently_Downloaded; + } + LevelSelectMenuLogic.LevelEntry.Create() + __instance.levelSets_.GetLevelInfo + LevelPlaylist.ModeAndLevelInfo m; + m. + if (sorting == SortingMethod.De) + switch (Mod.Instance.Config.WorkshopSortingMethod) { + case WorkshopSortMethod.Recent: + levelPlaylist.Sort(new Comparison(__instance.SortByMostRecentlyDownloaded)); + break; + }*/ + + //levelPlaylist.Sort(new Comparison(__instance.SortByMostRecentlyDownloaded)); + + Mod.Instance.Logger.Info("CreateAndAddEntry..."); + __instance.CreateAndAddEntry(levelPlaylist, type, true, unlockStyle, string.Empty); + + Mod.Instance.Logger.Info("Done!"); + return false; + + } + catch (Exception ex) + { + Mod.Instance.Logger.Info("LevelGridMenu__CreateAndAddLevelSet.Prefix(): EXCEPTION!!"); + Mod.Instance.Logger.Exception(ex); + } + } + return true; + + /*LevelPlaylist levelPlaylist = LevelPlaylist.Create(set, name, flags); + LevelGridMenu.PlaylistEntry.UnlockStyle unlockStyle = LevelGridMenu.PlaylistEntry.UnlockStyle.None; + if (type == LevelGridMenu.PlaylistEntry.Type.Official && __instance.displayType_ == LevelSelectMenuAbstract.DisplayType.ChooseMainMenuLevel) { + unlockStyle = LevelGridMenu.PlaylistEntry.UnlockStyle.ChooseMainMenu; + } + else if (type == LevelGridMenu.PlaylistEntry.Type.Workshop) { + levelPlaylist.Sort(new Comparison(__instance.SortByMostRecentlyDownloaded)); + //levelPlaylist.LimitCountTo(100); + } + __instance.CreateAndAddEntry(levelPlaylist, type, true, unlockStyle, string.Empty); + return false;*/ + + } + + } +} diff --git a/Mod.Template.Content/Mod/mod.json b/Mod.Template.Content/Mod/mod.json deleted file mode 100644 index 6fe0e0f..0000000 --- a/Mod.Template.Content/Mod/mod.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "FriendlyName": "Mod Template", - "Author": "You", - "Contact": "N/A", - "ModuleFileName": "Mod.Template.dll", - "Dependencies": [], - "RequiredGSLs": [ - "com.github.reherc/Centrifuge.Distance" - ], - "SkipLoad": false, - "Priority": 10 -} \ No newline at end of file diff --git a/Mod.Template/Mod.cs b/Mod.Template/Mod.cs deleted file mode 100644 index a08b398..0000000 --- a/Mod.Template/Mod.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Reactor.API.Attributes; -using Reactor.API.Interfaces.Systems; -using Reactor.API.Logging; -using Reactor.API.Runtime.Patching; -using UnityEngine; - -namespace Distance.ModTemplate -{ - /// - /// The mod's main class containing its entry point - /// - [ModEntryPoint("")] - public sealed class Mod : MonoBehaviour - { - public static Mod Instance { get; private set; } - - public IManager Manager { get; private set; } - - public Log Logger { get; private set; } - - /// - /// Method called as soon as the mod is loaded. - /// WARNING: Do not load asset bundles/textures in this function - /// The unity assets systems are not yet loaded when this - /// function is called. Loading assets here can lead to - /// unpredictable behaviour and crashes! - /// - public void Initialize(IManager manager) - { - // Do not destroy the current game object when loading a new scene - DontDestroyOnLoad(this); - - Instance = this; - - Manager = manager; - - // Create a log file - Logger = LogManager.GetForCurrentAssembly(); - - Logger.Info("Hello World!"); - - RuntimePatcher.AutoPatch(); - } - - /// - /// Method called after - /// GameManager.Start() - /// This initialisation method is the same as - /// the Spectrum mod loader initialisation procedure. - /// - public void LateInitialize(IManager manager) - { - // Code here... - Initialize(manager); - } - } -} diff --git a/README.md b/README.md index 89f2589..4fbebf8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Distance mod template +# Distance Level Select Additions -Use this repository as a base to create your own Distance mods. +Distance mod that extends the level selection UI by adding more options. -# Using the template -On github, click the `Use this template` button to create a new repository based on this one +## Current Options -Once the repository is created, be sure to clone it locally **with submodules**: -```sh -git clone --recurse-submodules -j8 -``` +* Exclude levels in personal playlists from the Workshop Level Set. +* Remove or change the limit to levels in the Workshop Level Set (original limit was 100). +* Apply up to 3 layers of sorting to the Workshop Level Set (each method can be reversed). + * Recently Downloaded (default) + * Level Name + * Author Name + * Difficulty + * Finish Status + * Medals Earned -# Setting up the project -After cloning the repository on your local drive, run the [setup.ps1](setup.ps1) script using [PowerShell 7 or higher](https://github.com/PowerShell/PowerShell#get-powershell). - -This script will rename the files correctly and replace their content to match your mod name. \ No newline at end of file diff --git a/setup.ps1 b/setup.ps1 deleted file mode 100644 index 0343983..0000000 --- a/setup.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -Set-Variable Source -Option Constant -Value $MyInvocation.MyCommand.Definition; -Set-Variable ValidationRegex -Option Constant -Value "^([a-zA-Z]+([ ]?[a-zA-Z0-9])*)\r?$"; - -$Extensions = ".txt", ".json", ".env", ".sln", ".proj", ".csproj", ".vbproj", ".shproj", ".projitems", ".cs", ".vb", ".targets", ".props"; - -Class ProjectInfo { - [string]$Namespace; - [string]$Title; - - ProjectInfo([string]$namespace, [string]$title) { - $this.Namespace = $namespace; - $this.Title = $title; - } -} - -Function Read-ProjectInfo { - [OutputType([ProjectInfo])] - Param () - - Write-Host "Enter a new project name to setup your mod (Example: 'My First Mod')."; - Write-Host "In order to be valid, a name must respect the following rules:"; - Write-Host "- The project name can't begin with a number"; - Write-Host "- The project name can't start or end with one or more spaces"; - Write-Host "- The name can't contain a series of more than 1 consecutive space character"; - Write-Host "- Words must be composed only of (latin) letters and numbers (a-zA-Z0-9)"; - Write-Host "- Any special characters are not allowed (@#-.;:/!§,?%=+* ...)"; - Write-Host ""; - Write-Host "The program will prompt you to type a name until it is a valid one."; - Write-Host "To exit it, press CTRL + C"; - Write-Host ""; - - Do { - $ProjectName = Read-Host -Prompt "Enter project name"; - } - Until ($ProjectName -match $ValidationRegex); - - Return [ProjectInfo]::new("Distance." + $ProjectName.Replace(" ", ""), "Distance " + $ProjectName); -} - -Function Rename-Files { - Param ([string]$Find, [string]$Replace) - - Rename-Items -Find $Find -Replace $Replace -Attributes "Directory"; - Rename-Items -Find $Find -Replace $Replace -Attributes "!Directory"; - Write-FilesContent -Find $Find -Replace $Replace -} - -Function Rename-Items { - Param ([string]$Find, [string]$Replace, [string]$Attributes) - $Items = Get-ChildItem -Attributes $Attributes -Recurse | Where-Object -FilterScript {$_.FullName -Ne $Source}; - - ForEach ($Item in $Items) { - If ($Item.Name.Contains($Find)) { - Rename-Item -Path $Item.PSPath -NewName $Item.Name.Replace($Find, $Replace); - } - } -} - -Function Write-FilesContent { - Param ([string]$Find, [string]$Replace) - - $Files = Get-ChildItem -File -Recurse | Where-Object -FilterScript {$_.FullName -Ne $Source -And $Extensions.Contains($_.Extension.ToLower())}; - - ForEach ($File in $Files) { - $Content = (Get-Content -Path $File.FullName -Encoding utf8 -Raw).Replace($Find, $Replace); - Set-Content -Path $File.FullName -Value $Content -Encoding utf8; - } -} - -Function Remove-Script { - [OutputType([bool])] - Param () - - $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Remove this powershell script from the disk."; - $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Keep this powershell script on the disk."; - - $Options = [System.Management.Automation.Host.ChoiceDescription[]]($Yes, $No); - - $ChoiceResult = $Host.Ui.PromptForChoice("Setup complete!", "Do you want to delete this powershell script?", $Options, 0); - - Return $ChoiceResult -Eq 0; -} - -Function Init { - [ProjectInfo]$Info = Read-ProjectInfo; - - Rename-Files -Find "Mod.Template" -Replace $Info.Namespace; - Rename-Files -Find "Distance.ModTemplate" -Replace $Info.Namespace; - Rename-Files -Find "Mod Template" -Replace $Info.Title; - - [bool]$RemoveFiles = Remove-Script; - If ($RemoveFiles) { - Remove-Item -Path "$Source" -Force - } -} - -Init; \ No newline at end of file