Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d8180a0
feat: product quality
omar-akermi Apr 29, 2025
adf3cca
fix: usings
omar-akermi Apr 29, 2025
2051d7f
fix: abstract quality
omar-akermi Apr 29, 2025
f134b62
feat: get product properties
omar-akermi Apr 29, 2025
492a4ba
fix: references
omar-akermi Apr 29, 2025
0a3c70c
more fixes
omar-akermi Apr 29, 2025
36e845c
changes for max
omar-akermi May 10, 2025
394c586
Merge branch 'bleeding-edge' into products
omar-akermi May 10, 2025
f0cbcd7
fixes
omar-akermi May 10, 2025
63899d1
Merge remote-tracking branch 'origin/products' into products
omar-akermi May 10, 2025
373744d
should work
omar-akermi May 11, 2025
fcee074
fix: Fix NPC constraint for NPC.Get and fix csproj for latest rider
MaxtorCoder May 12, 2025
e68f0a8
quest patches fix
omar-akermi May 14, 2025
7cdce5c
Merge branch 'bleeding-edge' into questpatches
omar-akermi May 14, 2025
78bec8b
fix for mono
omar-akermi May 14, 2025
79b610d
Merge remote-tracking branch 'origin/questpatches' into questpatches
omar-akermi May 14, 2025
41d3b05
changes for max
omar-akermi May 14, 2025
fd280c4
fix: using logger
omar-akermi May 15, 2025
e7df9c0
change exception handling
omar-akermi May 15, 2025
f115cdd
fix: save under Modded/Quests
omar-akermi May 16, 2025
25daf94
fix: load path
omar-akermi May 16, 2025
eaeb339
fix: no singleton usage savemanager.instance
omar-akermi May 16, 2025
e84c658
fix: singelton in load
omar-akermi May 16, 2025
7a2328e
fix: codestyling
omar-akermi May 16, 2025
d0f02d9
Get rid of the preprocessord,
omar-akermi May 16, 2025
641f48f
fix: build for melon
omar-akermi May 16, 2025
84bd747
fix: directory creation instead of log
omar-akermi May 17, 2025
0de38fe
Merge pull request #70 from KaBooMa/products
MaxtorCoder May 17, 2025
a608613
Merge branch 'bleeding-edge' into questpatches
omar-akermi May 17, 2025
62bff73
Merge pull request #86 from KaBooMa/questpatches
MaxtorCoder May 17, 2025
ca364bb
feat: simple vehicle spawning
k073l May 19, 2025
000b93d
fix: don't spawn if not host
k073l May 19, 2025
d804cb3
reorder fields and methods, rename private members
k073l May 20, 2025
a84dffb
throw exception if user tries to spawn null vehicle
k073l May 20, 2025
1c91ed9
removed NetworkSingleton generics
k073l May 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion S1API/Entities/NPC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ public bool ConversationCanBeHidden
/// </summary>
/// <typeparam name="T">The NPC class to get the instance of.</typeparam>
/// <returns></returns>
public static NPC? Get<T>() =>
public static NPC? Get<T>() where T : NPC =>
All.FirstOrDefault(npc => npc.GetType() == typeof(T));

#endregion
Expand Down
112 changes: 72 additions & 40 deletions S1API/Internal/Patches/QuestPatches.cs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
{
/// <summary>
/// INTERNAL: All patches related to quests.
/// INTERNAL: Contains patches related to quest processing and custom modifications.
/// </summary>
[HarmonyPatch]
internal class QuestPatches
{
/// <summary>
/// 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.
/// </summary>
protected static readonly Logging.Log Logger = new Logging.Log("QuestPatches");

/// <summary>
/// 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="__instance">Instance of the quest manager.</param>
/// <param name="parentFolderPath">Path to the base Quest folder.</param>
/// <param name="__result">List of extra saveable data. The game uses this for cleanup later.</param>
[HarmonyPatch(typeof(S1Quests.QuestManager), "WriteData")]
/// <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 QuestManagerWriteData(S1Quests.QuestManager __instance, string parentFolderPath, ref List<string> __result)
private static void SaveManager_Save_Postfix(string saveFolderPath)
{
string questsPath = Path.Combine(parentFolderPath, "Quests");
try
{
var saveManager = S1Persistence.SaveManager.Instance;

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<string> dummy = new List<string>();
quest.SaveInternal(questsPath, ref dummy);
}
}

foreach (Quest quest in QuestManager.Quests)
quest.SaveInternal(questsPath, ref __result);
}
catch (Exception ex)
{
Logger.Error("[S1API] ❌ Failed to save modded quests:\n" + ex);
}
}


/// <summary>
/// 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.
/// </summary>
/// <param name="__instance">Instance of the quest loader.</param>
/// <param name="mainPath">Path to the base Quest folder.</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)
{
// 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<S1Datas.QuestData>(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;
Expand All @@ -93,26 +139,12 @@ private static void QuestsLoaderLoad(S1Loaders.QuestsLoader __instance, string m
}
}


/// <summary>
/// Patching performed for when stale files are deleted.
/// Executes logic prior to the start of a quest.
/// Ensures that linked modded quest data is properly initialized.
/// </summary>
/// <param name="__instance">Instance of the quest manager.</param>
/// <param name="parentFolderPath">Path to the base Quest folder.</param>
[HarmonyPatch(typeof(S1Quests.QuestManager), "DeleteUnapprovedFiles")]
[HarmonyPostfix]
private static void QuestManagerDeleteUnapprovedFiles(S1Quests.QuestManager __instance, string parentFolderPath)
{
string questFolder = Path.Combine(parentFolderPath, "Quests");
string?[] existingQuests = QuestManager.Quests.Select(quest => quest.SaveFolder).ToArray();

string[] unapprovedQuestDirectories = Directory.GetDirectories(questFolder)
.Where(directory => directory.StartsWith("Quest_") && !existingQuests.Contains(directory))
.ToArray();

foreach (string unapprovedQuestDirectory in unapprovedQuestDirectories)
Directory.Delete(unapprovedQuestDirectory, true);
}

/// <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
23 changes: 18 additions & 5 deletions S1API/Items/ItemDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if (IL2CPPMELON)
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
Expand Down Expand Up @@ -153,16 +153,29 @@ public override bool Equals(object? obj) =>
/// <param name="a">The first <see cref="ItemDefinition"/> to compare.</param>
/// <param name="b">The second <see cref="ItemDefinition"/> to compare.</param>
/// <returns><c>true</c> if both instances are equal or have the same S1ItemDefinition; otherwise, <c>false</c>.</returns>
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);
}
/// <summary>
/// Determines whether two <see cref="ItemDefinition"/> instances are not equal.
/// </summary>
/// <param name="a">The first <see cref="ItemDefinition"/> to compare.</param>
/// <param name="b">The second <see cref="ItemDefinition"/> to compare.</param>
/// <returns><c>true</c> if the instances are not equal; otherwise, <c>false</c>.</returns>
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);
}

}

/// <summary>
Expand Down
34 changes: 28 additions & 6 deletions S1API/Products/CocaineDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,61 @@
#if (IL2CPPMELON)
using Il2CppScheduleOne.Product;
using S1CocaineDefinition = Il2CppScheduleOne.Product.CocaineDefinition;
using S1Properties = Il2CppScheduleOne.Properties;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using ScheduleOne.Product;
using S1CocaineDefinition = ScheduleOne.Product.CocaineDefinition;
using S1Properties = ScheduleOne.Properties;
#endif

using System.Collections.Generic;
using S1API.Internal.Utils;
using S1API.Items;

namespace S1API.Products
{
/// <summary>
/// Represents the definition of a Cocaine product.
/// Defines the characteristics and behaviors of a cocaine product within the system.
/// </summary>
public class CocaineDefinition : ProductDefinition
{
/// <summary>
/// INTERNAL: Strongly typed access to the CocaineDefinition.
/// Provides internal access to the CocaineDefinition type within the Schedule One system.
/// </summary>
internal S1CocaineDefinition S1CocaineDefinition =>
CrossType.As<S1CocaineDefinition>(S1ItemDefinition);

/// <summary>
/// Creates a new cocaine product definition.
/// Represents the definition of a cocaine product within the system.
/// </summary>
/// <param name="definition">The original in-game cocaine definition.</param>
internal CocaineDefinition(S1CocaineDefinition definition)
: base(definition) { }
: base(definition)
{
}

/// <summary>
/// Creates an instance of this cocaine product.
/// Creates an instance of this product definition with the specified quantity.
/// </summary>
/// <param name="quantity">The quantity of the product to instantiate. Defaults to 1 if not specified.</param>
/// <returns>An <see cref="ItemInstance"/> representing the instantiated product with the specified quantity.</returns>
public override ItemInstance CreateInstance(int quantity = 1) =>
new ProductInstance(CrossType.As<ProductItemInstance>(
S1CocaineDefinition.GetDefaultInstance(quantity)));

/// <summary>
/// Retrieves a list of properties associated with this product definition.
/// </summary>
/// <returns>A list of properties for the product.</returns>
public List<S1Properties.Property> GetProperties()
{
var result = new List<S1Properties.Property>();
var list = S1CocaineDefinition?.Properties;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
result.Add(list[i]);
}
return result;
}
}
}
35 changes: 28 additions & 7 deletions S1API/Products/MethDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,60 @@
#if (IL2CPPMELON)
using Il2CppScheduleOne.Product;
using S1MethDefinition = Il2CppScheduleOne.Product.MethDefinition;
using S1Properties = Il2CppScheduleOne.Properties;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using ScheduleOne.Product;
using S1MethDefinition = ScheduleOne.Product.MethDefinition;
using S1Properties = ScheduleOne.Properties;
#endif

using System.Collections.Generic;
using S1API.Internal.Utils;
using S1API.Items;

namespace S1API.Products
{
/// <summary>
/// Represents the definition of a Meth product.
/// Represents the definition of a meth product within the ScheduleOne product framework.
/// </summary>
public class MethDefinition : ProductDefinition
{
/// <summary>
/// INTERNAL: Strongly typed access to the MethDefinition.
/// INTERNAL: Strongly typed access to S1MethDefinition.
/// </summary>
internal S1MethDefinition S1MethDefinition =>
CrossType.As<S1MethDefinition>(S1ItemDefinition);

/// <summary>
/// Creates a new meth product definition.
/// Represents the definition of a Meth product in the product framework.
/// </summary>
/// <param name="definition">The original in-game meth definition.</param>
internal MethDefinition(S1MethDefinition definition)
: base(definition) { }
: base(definition)
{
}

/// <summary>
/// Creates an instance of this meth product.
/// Creates an instance of this meth product with the specified quantity.
/// </summary>
/// <param name="quantity">The quantity of the product instance to create. Defaults to 1 if not specified.</param>
/// <returns>An instance of <see cref="ItemInstance"/> representing the created meth product.</returns>
public override ItemInstance CreateInstance(int quantity = 1) =>
new ProductInstance(CrossType.As<ProductItemInstance>(
S1MethDefinition.GetDefaultInstance(quantity)));

/// <summary>
/// Retrieves the list of properties associated with the meth product definition.
/// </summary>
/// <returns>A list of properties defined for the meth product.</returns>
public List<S1Properties.Property> GetProperties()
{
var result = new List<S1Properties.Property>();
var list = S1MethDefinition?.Properties;
if (list != null)
{
for (int i = 0; i < list.Count; i++)
result.Add(list[i]);
}
return result;
}
}
}
Loading
Loading