diff --git a/S1API/Entities/NPC.cs b/S1API/Entities/NPC.cs index a758395f..6c24deac 100644 --- a/S1API/Entities/NPC.cs +++ b/S1API/Entities/NPC.cs @@ -592,7 +592,7 @@ public bool ConversationCanBeHidden /// /// The NPC class to get the instance of. /// - public static NPC? Get() => + public static NPC? Get() where T : NPC => All.FirstOrDefault(npc => npc.GetType() == typeof(T)); #endregion diff --git a/S1API/Internal/Patches/QuestPatches.cs b/S1API/Internal/Patches/QuestPatches.cs index b5e062dc..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. + /// + 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. /// - /// 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")] + /// 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; + + 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); + } + } - foreach (Quest quest in QuestManager.Quests) - quest.SaveInternal(questsPath, ref __result); + } + 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,26 +139,12 @@ private static void QuestsLoaderLoad(S1Loaders.QuestsLoader __instance, string m } } + /// - /// 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. /// - /// Instance of the quest manager. - /// Path to the base Quest folder. - [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); - } - + /// 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); + } + } /// diff --git a/S1API/Products/CocaineDefinition.cs b/S1API/Products/CocaineDefinition.cs index 24feb99f..0ee5e2f5 100644 --- a/S1API/Products/CocaineDefinition.cs +++ b/S1API/Products/CocaineDefinition.cs @@ -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 { /// - /// Represents the definition of a Cocaine product. + /// Defines the characteristics and behaviors of a cocaine product within the system. /// public class CocaineDefinition : ProductDefinition { /// - /// INTERNAL: Strongly typed access to the CocaineDefinition. + /// Provides internal access to the CocaineDefinition type within the Schedule One system. /// internal S1CocaineDefinition S1CocaineDefinition => CrossType.As(S1ItemDefinition); /// - /// Creates a new cocaine product definition. + /// Represents the definition of a cocaine product within the system. /// - /// The original in-game cocaine definition. internal CocaineDefinition(S1CocaineDefinition definition) - : base(definition) { } + : base(definition) + { + } /// - /// Creates an instance of this cocaine product. + /// Creates an instance of this product definition with the specified quantity. /// + /// The quantity of the product to instantiate. Defaults to 1 if not specified. + /// An representing the instantiated product with the specified quantity. public override ItemInstance CreateInstance(int quantity = 1) => new ProductInstance(CrossType.As( S1CocaineDefinition.GetDefaultInstance(quantity))); + + /// + /// Retrieves a list of properties associated with this product definition. + /// + /// A list of properties for the product. + public List GetProperties() + { + var result = new List(); + var list = S1CocaineDefinition?.Properties; + if (list != null) + { + for (int i = 0; i < list.Count; i++) + result.Add(list[i]); + } + return result; + } } } diff --git a/S1API/Products/MethDefinition.cs b/S1API/Products/MethDefinition.cs index ee8e16d1..87689244 100644 --- a/S1API/Products/MethDefinition.cs +++ b/S1API/Products/MethDefinition.cs @@ -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 { /// - /// Represents the definition of a Meth product. + /// Represents the definition of a meth product within the ScheduleOne product framework. /// public class MethDefinition : ProductDefinition { /// - /// INTERNAL: Strongly typed access to the MethDefinition. + /// INTERNAL: Strongly typed access to S1MethDefinition. /// internal S1MethDefinition S1MethDefinition => CrossType.As(S1ItemDefinition); /// - /// Creates a new meth product definition. + /// Represents the definition of a Meth product in the product framework. /// - /// The original in-game meth definition. internal MethDefinition(S1MethDefinition definition) - : base(definition) { } + : base(definition) + { + } /// - /// Creates an instance of this meth product. + /// Creates an instance of this meth product with the specified quantity. /// + /// The quantity of the product instance to create. Defaults to 1 if not specified. + /// An instance of representing the created meth product. public override ItemInstance CreateInstance(int quantity = 1) => new ProductInstance(CrossType.As( S1MethDefinition.GetDefaultInstance(quantity))); + + /// + /// Retrieves the list of properties associated with the meth product definition. + /// + /// A list of properties defined for the meth product. + public List GetProperties() + { + var result = new List(); + var list = S1MethDefinition?.Properties; + if (list != null) + { + for (int i = 0; i < list.Count; i++) + result.Add(list[i]); + } + return result; + } } } diff --git a/S1API/Products/ProductDefinition.cs b/S1API/Products/ProductDefinition.cs index 09751899..7ba72fb6 100644 --- a/S1API/Products/ProductDefinition.cs +++ b/S1API/Products/ProductDefinition.cs @@ -1,10 +1,15 @@ #if (IL2CPPMELON) using Il2CppInterop.Runtime.InteropTypes; using S1Product = Il2CppScheduleOne.Product; +using ItemFramework = Il2CppScheduleOne.ItemFramework; +using Properties = Il2CppScheduleOne.Properties; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1Product = ScheduleOne.Product; +using ItemFramework = ScheduleOne.ItemFramework; +using Properties = ScheduleOne.Properties; #endif +using System.Collections.Generic; using S1API.Internal.Utils; using S1API.Items; using UnityEngine; @@ -26,8 +31,11 @@ public class ProductDefinition : ItemDefinition /// INTERNAL: Creates a product definition from the in-game product definition. /// /// - internal ProductDefinition(S1Product.ProductDefinition productDefinition) : base(productDefinition) { } - +#if IL2CPPMELON + internal ProductDefinition(ItemFramework.ItemDefinition productDefinition) : base(productDefinition) { } +#else + internal ProductDefinition(ItemFramework.ItemDefinition productDefinition) : base(productDefinition) { } +#endif /// /// The price associated with this product. /// @@ -49,6 +57,14 @@ public Sprite Icon { get { return S1ProductDefinition.Icon; } } +#if IL2CPPMELON + private List properties; // or however properties are stored + public List Properties; // or however properties are stored +#else + private List properties; // or however properties are stored + public IReadOnlyList Properties => properties.AsReadOnly(); +#endif - } + +} } diff --git a/S1API/Products/ProductDefinitionWrapper.cs b/S1API/Products/ProductDefinitionWrapper.cs index 14e904b8..923744cc 100644 --- a/S1API/Products/ProductDefinitionWrapper.cs +++ b/S1API/Products/ProductDefinitionWrapper.cs @@ -1,27 +1,34 @@ using S1API.Internal.Utils; - #if (IL2CPPMELON) using S1Product = Il2CppScheduleOne.Product; -#else +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1Product = ScheduleOne.Product; #endif namespace S1API.Products { /// - /// INTERNAL: A wrapper class for converting a product definition to its proper dedicated class. + /// Provides functionality to wrap and convert generic product definitions into their specific type-derived definitions. /// - internal static class ProductDefinitionWrapper + public static class ProductDefinitionWrapper { - internal static ProductDefinition Wrap(ProductDefinition def) + /// + /// Converts a generic into its corresponding typed wrapper. + /// + /// The raw product definition to be processed and converted. + /// A wrapped instance of with type-specific methods and properties, or the input definition if no specific wrapper applies. + public static ProductDefinition Wrap(ProductDefinition def) { var item = def.S1ItemDefinition; if (CrossType.Is(item, out var weed)) return new WeedDefinition(weed); + if (CrossType.Is(item, out var meth)) return new MethDefinition(meth); + if (CrossType.Is(item, out var coke)) return new CocaineDefinition(coke); + return def; } } diff --git a/S1API/Products/ProductInstance.cs b/S1API/Products/ProductInstance.cs index f3fd7f4b..150c6e72 100644 --- a/S1API/Products/ProductInstance.cs +++ b/S1API/Products/ProductInstance.cs @@ -1,41 +1,72 @@ -#if (IL2CPPMELON) +#if (IL2CPPMELON ) using S1Product = Il2CppScheduleOne.Product; +using S1Properties = Il2CppScheduleOne.Properties; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1Product = ScheduleOne.Product; +using S1Properties = ScheduleOne.Properties; #endif - +using System.Collections.Generic; using S1API.Internal.Utils; -using S1API.Items; - +using S1ItemInstance = S1API.Items.ItemInstance; namespace S1API.Products { /// /// Represents an instance of a product in the game. /// - public class ProductInstance : ItemInstance + /// + /// This class defines specific properties and behaviors for a product instance, + /// such as quality, packaging, and definition, derived from the S1API's item instance structure. + /// + public class ProductInstance : S1ItemInstance { /// - /// INTERNAL: The stored reference to the in-game product instance. + /// INTERNAL: Provides access to the underlying in-game product item instance. /// internal S1Product.ProductItemInstance S1ProductInstance => CrossType.As(S1ItemInstance); /// - /// INTERNAL: Creates a product instance from the in-game product instance. + /// Represents an instance of a product, derived from a specific in-game product item instance, + /// with additional properties for packaging, quality, and product definition. /// - /// - internal ProductInstance(S1Product.ProductItemInstance productInstance) : base(productInstance) { } + internal ProductInstance(S1Product.ProductItemInstance productInstance) + : base(productInstance) + { + } /// - /// Whether this product is currently packaged or not. + /// Indicates whether the product instance has applied packaging. /// - public bool IsPackaged => - S1ProductInstance.AppliedPackaging; + public bool IsPackaged => S1ProductInstance.AppliedPackaging; /// - /// The type of packaging applied to this product. + /// Provides access to the packaging information applied to the product, + /// represented as a specific packaging definition instance. /// public PackagingDefinition AppliedPackaging => new PackagingDefinition(S1ProductInstance.AppliedPackaging); + + /// + /// Represents the quality level of the product instance. + /// + /// + /// Quality levels provide a measure of the product's grading, ranging from "Trash" to "Heavenly". + /// + public Quality Quality => S1ProductInstance.Quality.ToAPI(); + + /// + /// Gets the definition of the product associated with this instance. + /// + public ProductDefinition Definition => new ProductDefinition(S1ProductInstance.Definition); + + /// + /// Gets the list of properties associated with the product definition. + /// + /// + /// This property provides an unmodifiable list of properties associated + /// with the underlying product definition. Each property represents + /// a specific characteristic or behavior of the corresponding product. + /// + public IReadOnlyList Properties => Definition.Properties; } } diff --git a/S1API/Products/Quality.cs b/S1API/Products/Quality.cs new file mode 100644 index 00000000..d95d2313 --- /dev/null +++ b/S1API/Products/Quality.cs @@ -0,0 +1,87 @@ +#if IL2CPPMELON +using InternalQuality = Il2CppScheduleOne.ItemFramework.EQuality; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using InternalQuality = ScheduleOne.ItemFramework.EQuality; +#endif +namespace S1API.Products +{ + /// + /// Represents the quality levels for items. + /// + /// + /// This enumeration defines various quality tiers that items can belong to. Each tier represents a specific + /// standard or grade, ranging from the lowest to the highest. + /// + public enum Quality + { + /// + /// Represents "Trash" Quality + /// + Trash = 0, + + /// + /// Represents "Poor" Quality + /// + Poor = 1, + + /// + /// Represents "Standard" Quality + /// + Standard = 2, + + /// + /// Represents "Premium" quality + /// + Premium = 3, + + /// + /// Represents "Heavenly" quality + /// + Heavenly = 4 + } + + /// + /// Provides extension methods for converting between and + /// enumerations. + /// + internal static class QualityExtensions + { + /// + /// Converts an instance of to its corresponding + /// representation. + /// + /// The instance to convert. + /// A value that represents the converted quality. + internal static Quality ToAPI(this InternalQuality quality) + { + return quality switch + { + InternalQuality.Trash => Quality.Trash, + InternalQuality.Poor => Quality.Poor, + InternalQuality.Standard => Quality.Standard, + InternalQuality.Premium => Quality.Premium, + InternalQuality.Heavenly => Quality.Heavenly, + _ => Quality.Trash, + }; + } + + /// + /// Converts an instance of the enum to its corresponding + /// enum representation. + /// + /// The enum value to convert. + /// The corresponding enum value. + internal static InternalQuality ToInternal(this Quality quality) + { + return quality switch + { + Quality.Trash => InternalQuality.Trash, + Quality.Poor => InternalQuality.Poor, + Quality.Standard => InternalQuality.Standard, + Quality.Premium => InternalQuality.Premium, + Quality.Heavenly => InternalQuality.Heavenly, + _ => InternalQuality.Trash, + }; + } + } +} diff --git a/S1API/Products/WeedDefinition.cs b/S1API/Products/WeedDefinition.cs index 8f334711..fb4c0164 100644 --- a/S1API/Products/WeedDefinition.cs +++ b/S1API/Products/WeedDefinition.cs @@ -1,40 +1,63 @@ #if (IL2CPPMELON) using Il2CppScheduleOne.Product; using S1WeedDefinition = Il2CppScheduleOne.Product.WeedDefinition; +using S1Properties = Il2CppScheduleOne.Properties; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using ScheduleOne.Product; using S1WeedDefinition = ScheduleOne.Product.WeedDefinition; +using S1Properties = ScheduleOne.Properties; #endif using S1API.Internal.Utils; using S1API.Items; +using System.Collections.Generic; namespace S1API.Products { /// - /// Represents the definition of a Weed product. + /// Represents a specific type of weed product definition. /// public class WeedDefinition : ProductDefinition { /// - /// INTERNAL: Strongly typed access to the WeedDefinition. + /// INTERNAL: Strongly typed reference to Schedule One's WeedDefinition. /// internal S1WeedDefinition S1WeedDefinition => CrossType.As(S1ItemDefinition); - /// - /// Creates a new weed product definition. + /// Represents a specific type of weed product definition. /// - /// The original in-game weed definition. internal WeedDefinition(S1WeedDefinition definition) - : base(definition) { } + : base(definition) + { + } /// - /// Creates an instance of this weed product. + /// Creates an instance of the product with the specified quantity. /// + /// The quantity of the product to create. Defaults to 1 if not specified. + /// An representing the created product instance with the specified quantity. public override ItemInstance CreateInstance(int quantity = 1) => new ProductInstance(CrossType.As( S1WeedDefinition.GetDefaultInstance(quantity))); + + /// + /// Retrieves a list of properties associated with this weed definition. + /// + /// A list of properties of type S1Properties.Property that are linked with the current weed definition, or an empty list if none are found. + public List GetProperties() + { + var result = new List(); + var list = S1WeedDefinition?.Properties; + + if (list != null) + { + for (int i = 0; i < list.Count; i++) + result.Add(list[i]); + } + + return result; + } } } diff --git a/S1API/S1API.csproj b/S1API/S1API.csproj index 85a23c63..418bcc38 100644 --- a/S1API/S1API.csproj +++ b/S1API/S1API.csproj @@ -17,6 +17,7 @@ AnyCPU S1API true + latest diff --git a/S1API/Vehicles/LandVehicle.cs b/S1API/Vehicles/LandVehicle.cs new file mode 100644 index 00000000..3f91bdd7 --- /dev/null +++ b/S1API/Vehicles/LandVehicle.cs @@ -0,0 +1,198 @@ +#if (IL2CPPMELON) +using S1Vehicles = Il2CppScheduleOne.Vehicles; +using Il2Cpp; +using Il2CppFishNet; +using Il2CppFishNet.Connection; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1Vehicles = ScheduleOne.Vehicles; +using FishNet; +using FishNet.Connection; +#endif +using System; +using System.Reflection; +using UnityEngine; +using S1API.Logging; + +namespace S1API.Vehicles +{ + /// + /// Represents a land vehicle in the game. + /// + public class LandVehicle + { + // Public members intended to be used by modders + #region Public Members + /// + /// Creates a new LandVehicle instance. + /// + public LandVehicle(string vehicleCode) + { + var vehiclePrefab = S1Vehicles.VehicleManager.Instance.GetVehiclePrefab(vehicleCode); + if (vehiclePrefab == null) + { + _logger.Error($"SpawnVehicle: '{vehicleCode}' is not a valid vehicle code!"); + return; + } + + var component = UnityEngine.Object.Instantiate(vehiclePrefab.gameObject) + .GetComponent(); + + component.SetGUID(GUIDManager.GenerateUniqueGUID()); + S1Vehicles.VehicleManager.Instance.AllVehicles.Add(component); + + S1LandVehicle = component; + SetConnection(); + } + + /// + /// Vehicle price. + /// + public float VehiclePrice + { + get => S1LandVehicle.VehiclePrice; + set => VehiclePriceField?.SetValue(S1LandVehicle, value); + } + + /// + /// Vehicle's top speed. + /// + public float TopSpeed + { + get => S1LandVehicle.TopSpeed; + set => S1LandVehicle.TopSpeed = value; + } + + /// + /// If the vehicle is owned by the player. + /// + public bool IsPlayerOwned + { + get => S1LandVehicle.IsPlayerOwned; + set => SetIsPlayerOwned(value); + } + + /// + /// Vehicle's color. + /// + public VehicleColor Color + { + get => (VehicleColor)S1LandVehicle.OwnedColor; + set => SetColor(value); + } + + /// + /// Spawns the vehicle in the game world. + /// + /// Position in the world + /// Rotation of the vehicle + public void Spawn(Vector3 position, Quaternion rotation) + { + if (!InstanceFinder.IsServer) + { + _logger.Warning("Spawn can only be called on the server!"); + return; + } + + if (S1LandVehicle == null) + throw new Exception("Unable to spawn vehicle, S1LandVehicle is null!"); + + S1LandVehicle.transform.position = position; + S1LandVehicle.transform.rotation = rotation; + S1Vehicles.VehicleManager.Instance.Spawn(S1LandVehicle.gameObject); + } + + #endregion + + // Internal members used by S1API + #region Internal Members + + /// + /// INTERNAL: The stored reference to the land vehicle in-game (see ). + /// + internal S1Vehicles.LandVehicle S1LandVehicle = null!; + + /// + /// INTERNAL: Creates a LandVehicle instance from an in-game land vehicle instance. + /// + /// The in-game land vehicle instance. + internal LandVehicle(S1Vehicles.LandVehicle landVehicle) + { + S1LandVehicle = landVehicle; + SetConnection(); + } + + #endregion + + // Private members used by LandVehicle class + #region Private Members + + /// + /// Logger for the LandVehicle class. + /// + private static readonly Log _logger = new Log("S1API.LandVehicle"); + + /// + /// The stored reference to protected vehiclePrice field in the land vehicle in-game. + /// + private static readonly FieldInfo? VehiclePriceField = + typeof(S1Vehicles.LandVehicle).GetField("vehiclePrice", BindingFlags.NonPublic); + + /// + /// Connection to the player that owns the vehicle. + /// + private NetworkConnection? _conn; + + /// + /// Sets the connection to the player that owns the vehicle. + /// + private void SetConnection() + { + var nm = InstanceFinder.NetworkManager; + if (nm.IsClientOnly) + { + var tempConn = InstanceFinder.ClientManager.Connection; + if (tempConn != null && tempConn.IsValid) + _conn = tempConn; + } + else if (nm.IsServerOnly || (nm.IsServer && !nm.IsClient)) + { + var owner = S1LandVehicle.Owner; + if (owner != null && owner.IsValid) + _conn = owner; + } + } + + /// + /// Helper method to set the vehicle as player owned. + /// + /// If true, sets vehicle as player owned + private void SetIsPlayerOwned(bool isPlayerOwned) + { + S1LandVehicle.SetIsPlayerOwned(_conn, isPlayerOwned); + // make sure to add/remove the vehicle from the player owned vehicles list + if (isPlayerOwned) + S1Vehicles.VehicleManager.Instance.PlayerOwnedVehicles.Add(S1LandVehicle); + else + S1Vehicles.VehicleManager.Instance.PlayerOwnedVehicles.Remove(S1LandVehicle); + } + + /// + /// Helper method to set the vehicle color. + /// + /// Vehicle's color + private void SetColor(VehicleColor color) + { + var setOwnedColorMethod = + typeof(S1Vehicles.LandVehicle).GetMethod("SetOwnedColor", + BindingFlags.Instance | BindingFlags.NonPublic); + if (setOwnedColorMethod == null) + { + _logger.Error("SetOwnedColor method not found!"); + return; + } + + setOwnedColorMethod.Invoke(S1LandVehicle, [_conn, (S1Vehicles.Modification.EVehicleColor)color]); + } + #endregion + } +} \ No newline at end of file diff --git a/S1API/Vehicles/VehicleColor.cs b/S1API/Vehicles/VehicleColor.cs new file mode 100644 index 00000000..4694d3a5 --- /dev/null +++ b/S1API/Vehicles/VehicleColor.cs @@ -0,0 +1,26 @@ +namespace S1API.Vehicles +{ + /// + /// Represents available colors for vehicles. + /// + public enum VehicleColor + { + Black, + DarkGrey, + LightGrey, + White, + Yellow, + Orange, + Red, + DullRed, + Pink, + Purple, + Navy, + DarkBlue, + LightBlue, + Cyan, + LightGreen, + DarkGreen, + Custom + } +} \ No newline at end of file