diff --git a/S1API/Internal/Patches/QuestPatches.cs b/S1API/Internal/Patches/QuestPatches.cs index dc0dd12a..bc7d1e6a 100644 --- a/S1API/Internal/Patches/QuestPatches.cs +++ b/S1API/Internal/Patches/QuestPatches.cs @@ -1,13 +1,14 @@ -#if (IL2CPPMELON) +#if (IL2CPPMELON) using S1Loaders = Il2CppScheduleOne.Persistence.Loaders; using S1Datas = Il2CppScheduleOne.Persistence.Datas; using S1Quests = Il2CppScheduleOne.Quests; +using S1Persistence = Il2CppScheduleOne.Persistence; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1Loaders = ScheduleOne.Persistence.Loaders; using S1Datas = ScheduleOne.Persistence.Datas; using S1Quests = ScheduleOne.Quests; +using S1Persistence = ScheduleOne.Persistence; #endif - #if (IL2CPPMELON || IL2CPPBEPINEX) using Il2CppSystem.Collections.Generic; #elif (MONOMELON || MONOBEPINEX) @@ -19,63 +20,108 @@ using System.Linq; using HarmonyLib; using Newtonsoft.Json; -using S1API.Internal.Abstraction; using S1API.Internal.Utils; using S1API.Quests; using UnityEngine; +using ISaveable = S1API.Internal.Abstraction.ISaveable; namespace S1API.Internal.Patches { /// - /// INTERNAL: All patches related to quests. + /// INTERNAL: Contains patches related to quest processing and custom modifications. /// [HarmonyPatch] internal class QuestPatches { /// - /// Patching performed when all quests are saved. + /// Provides a centralized logging mechanism to capture and output messages, warnings, + /// and errors during runtime, using underlying logging frameworks like BepInEx or MelonLoader. /// - /// Instance of the quest manager. - /// Path to the base Quest folder. - /// List of extra saveable data. The game uses this for cleanup later. - [HarmonyPatch(typeof(S1Quests.QuestManager), "WriteData")] + protected static readonly Logging.Log Logger = new Logging.Log("QuestPatches"); + + /// + /// Executes additional logic after quests are saved by the SaveManager. + /// Ensures that directories for modded quests are properly created and that + /// only non-vanilla modded quests are saved into the specified folder. + /// + /// The path to the save folder where quests are being stored. + [HarmonyPatch(typeof(S1Persistence.SaveManager), nameof(S1Persistence.SaveManager.Save), typeof(string))] [HarmonyPostfix] - private static void QuestManagerWriteData(S1Quests.QuestManager __instance, string parentFolderPath, ref List __result) + private static void SaveManager_Save_Postfix(string saveFolderPath) { - string questsPath = Path.Combine(parentFolderPath, "Quests"); + try + { + var saveManager = S1Persistence.SaveManager.Instance; - foreach (Quest quest in QuestManager.Quests) - quest.SaveInternal(questsPath, ref __result); + string[] approved = { + "Modded", + Path.Combine("Modded", "Quests") + }; + + foreach (var path in approved) + { + if (!saveManager.ApprovedBaseLevelPaths.Contains(path)) + saveManager.ApprovedBaseLevelPaths.Add(path); + } + + // ✅ Create the directory structure + string questsPath = Path.Combine(saveFolderPath, "Modded", "Quests"); + Directory.CreateDirectory(questsPath); + + // ✅ Save only non-vanilla modded quests + foreach (Quest quest in QuestManager.Quests) + { + if (!quest.GetType().Namespace.StartsWith("ScheduleOne")) + { + List dummy = new List(); + quest.SaveInternal(questsPath, ref dummy); + } + } + + } + catch (Exception ex) + { + Logger.Error("[S1API] ❌ Failed to save modded quests:\n" + ex); + } } + /// - /// Patching performed for when all quests are loaded. + /// Invoked after all base quests are loaded to handle modded quest loading. + /// Loads modded quests from a specific "Modded/Quests" directory and integrates them into the game. /// - /// Instance of the quest loader. - /// Path to the base Quest folder. + /// The quest loader instance responsible for managing quest load operations. + /// The path to the primary quest directory in the base game. [HarmonyPatch(typeof(S1Loaders.QuestsLoader), "Load")] [HarmonyPostfix] private static void QuestsLoaderLoad(S1Loaders.QuestsLoader __instance, string mainPath) { - // Make sure we have a quests directory (fresh saves don't at this point in runtime) - if (!Directory.Exists(mainPath)) + string moddedQuestsPath = Path.Combine( + S1Persistence.LoadManager.Instance.LoadedGameFolderPath, + "Modded", "Quests" + ); + + if (!Directory.Exists(moddedQuestsPath)) + { + Directory.CreateDirectory(moddedQuestsPath); return; + } - string[] questDirectories = Directory.GetDirectories(mainPath) + string[] questDirectories = Directory.GetDirectories(moddedQuestsPath) .Select(Path.GetFileName) .Where(directory => directory != null && directory.StartsWith("Quest_")) - .ToArray()!; + .ToArray(); foreach (string questDirectory in questDirectories) { - string baseQuestPath = Path.Combine(mainPath, questDirectory); + string baseQuestPath = Path.Combine(moddedQuestsPath, questDirectory); __instance.TryLoadFile(baseQuestPath, out string questDataText); if (questDataText == null) continue; S1Datas.QuestData baseQuestData = JsonUtility.FromJson(questDataText); - string questDirectoryPath = Path.Combine(mainPath, questDirectory); + string questDirectoryPath = Path.Combine(moddedQuestsPath, questDirectory); string questDataPath = Path.Combine(questDirectoryPath, "QuestData"); if (!__instance.TryLoadFile(questDataPath, out string questText)) continue; @@ -93,6 +139,12 @@ private static void QuestsLoaderLoad(S1Loaders.QuestsLoader __instance, string m } } + + /// + /// Executes logic prior to the start of a quest. + /// Ensures that linked modded quest data is properly initialized. + /// + /// The instance of the quest that is being started. [HarmonyPatch(typeof(S1Quests.Quest), "Start")] [HarmonyPrefix] private static void QuestStart(S1Quests.Quest __instance) => diff --git a/S1API/Items/ItemDefinition.cs b/S1API/Items/ItemDefinition.cs index 24b6e213..7cffc920 100644 --- a/S1API/Items/ItemDefinition.cs +++ b/S1API/Items/ItemDefinition.cs @@ -1,4 +1,4 @@ -#if (IL2CPPMELON) +#if (IL2CPPMELON) using S1ItemFramework = Il2CppScheduleOne.ItemFramework; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1ItemFramework = ScheduleOne.ItemFramework; @@ -153,16 +153,29 @@ public override bool Equals(object? obj) => /// The first to compare. /// The second to compare. /// true if both instances are equal or have the same S1ItemDefinition; otherwise, false. - public static bool operator ==(ItemDefinition? a, ItemDefinition? b) => - ReferenceEquals(a, b) || a != null && b != null && a.S1ItemDefinition == b.S1ItemDefinition; - + public static bool operator ==(ItemDefinition? a, ItemDefinition? b) + { + if (ReferenceEquals(a, b)) + return true; + if (a is null || b is null) + return false; + return ReferenceEquals(a.S1ItemDefinition, b.S1ItemDefinition); + } /// /// Determines whether two instances are not equal. /// /// The first to compare. /// The second to compare. /// true if the instances are not equal; otherwise, false. - public static bool operator !=(ItemDefinition? a, ItemDefinition? b) => !(a == b); + public static bool operator !=(ItemDefinition? a, ItemDefinition? b) + { + if (ReferenceEquals(a, b)) + return false; + if (a is null || b is null) + return true; + return !ReferenceEquals(a.S1ItemDefinition, b.S1ItemDefinition); + } + } ///