Skip to content
72 changes: 51 additions & 21 deletions S1API/Internal/Patches/QuestPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
using S1Datas = Il2CppScheduleOne.Persistence.Datas;
using S1Quests = Il2CppScheduleOne.Quests;
using S1Persistence = Il2CppScheduleOne.Persistence;
using Il2CppScheduleOne.DevUtilities;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1Loaders = ScheduleOne.Persistence.Loaders;
using S1Datas = ScheduleOne.Persistence.Datas;
using S1Quests = ScheduleOne.Quests;
using S1Persistence = ScheduleOne.Persistence;

using ScheduleOne.DevUtilities;
#endif
#if (IL2CPPMELON || IL2CPPBEPINEX)
using Il2CppSystem.Collections.Generic;
Expand All @@ -29,62 +30,90 @@
namespace S1API.Internal.Patches
{
/// <summary>
/// INTERNAL: Contains patches specific to quest handling and modification.
/// INTERNAL: Contains patches related to quest processing and custom modifications.
/// </summary>
[HarmonyPatch]
internal class QuestPatches
{
/// <summary>
/// Provides a centralized logging mechanism to capture and output messages, warnings,
/// and errors during runtime, using underlying logging frameworks like BepInEx or MelonLoader.
/// </summary>
protected static readonly Logging.Log Logger = new Logging.Log("QuestPatches");

/// <summary>
/// Invoked after all quests are saved.
/// Ensures that modded quest data is correctly saved to a designated folder.
/// 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.
/// </summary>
/// <param name="saveFolderPath">The path to the primary save folder where quest data will be stored.</param>
/// <param name="saveFolderPath">The path to the save folder where quests are being stored.</param>
[HarmonyPatch(typeof(S1Persistence.SaveManager), nameof(S1Persistence.SaveManager.Save), typeof(string))]
[HarmonyPostfix]
private static void SaveManager_Save_Postfix(string saveFolderPath)
{
try
{
string moddedQuestsPath = Path.Combine(saveFolderPath, "..\\Players\\ModdedQuests");
var saveManager = S1Persistence.SaveManager.Instance;

if (!Directory.Exists(moddedQuestsPath))
Directory.CreateDirectory(moddedQuestsPath);
string[] approved = {
"Modded",
Path.Combine("Modded", "Quests")
};

foreach (Quest quest in QuestManager.Quests)
foreach (var path in approved)
{
if (!saveManager.ApprovedBaseLevelPaths.Contains(path))
saveManager.ApprovedBaseLevelPaths.Add(path);
}

List<string> dummy = new List<string>();

quest.SaveInternal(moddedQuestsPath, ref dummy);
// ✅ 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"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Since the foreach loop is only doing this one thing and only against one type of thing, you could simplify it with foreach (Quest quest in QuestManager.Quests.Where(q => !q.GetType().Namespace.StartsWith("ScheduleOne")) to remove the if. This would also mean that you're looping over fewer things.

Nit in this case because I'm sure it's a very small collection, but good to keep in mind for future.

{
List<string> dummy = new List<string>();
quest.SaveInternal(questsPath, ref dummy);
}
}

}
catch (Exception ex)
{
Logger.Error("Failed during SaveManager_Save_Postfix execution.\n" + ex);
throw;
Logger.Error("[S1API] ❌ Failed to save modded quests:\n" + ex);
}
}


/// <summary>
/// Patching performed for when all quests are loaded from the modded quests directory.
/// 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.
/// </summary>
/// <param name="__instance">Instance of the quest loader responsible for loading quests.</param>
/// <param name="mainPath">Path to the base Quest folder where quests are located.</param>
/// <param name="__instance">The quest loader instance responsible for managing quest load operations.</param>
/// <param name="mainPath">The path to the primary quest directory in the base game.</param>
[HarmonyPatch(typeof(S1Loaders.QuestsLoader), "Load")]
[HarmonyPostfix]
private static void QuestsLoaderLoad(S1Loaders.QuestsLoader __instance, string mainPath)
{
string moddedQuestsPath = Path.Combine(mainPath, "..\\Players\\ModdedQuests");
string moddedQuestsPath = Path.Combine(
S1Persistence.LoadManager.Instance.LoadedGameFolderPath,
"Modded", "Quests"
);

if (!Directory.Exists(moddedQuestsPath))
{
Logger.Warning("[S1API] No Modded/Quests folder found: " + moddedQuestsPath);
return;
}

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(moddedQuestsPath, questDirectory);
Expand Down Expand Up @@ -114,9 +143,10 @@ private static void QuestsLoaderLoad(S1Loaders.QuestsLoader __instance, string m


/// <summary>
/// Executes custom initialization logic whenever a quest starts.
/// Executes logic prior to the start of a quest.
/// Ensures that linked modded quest data is properly initialized.
/// </summary>
/// <param name="__instance">The instance of the quest that is starting.</param>
/// <param name="__instance">The instance of the quest that is being started.</param>
[HarmonyPatch(typeof(S1Quests.Quest), "Start")]
[HarmonyPrefix]
private static void QuestStart(S1Quests.Quest __instance) =>
Expand Down
Loading