diff --git a/NeoModLoader.csproj b/NeoModLoader.csproj index 7d821d5..52d9cc3 100644 --- a/NeoModLoader.csproj +++ b/NeoModLoader.csproj @@ -217,14 +217,14 @@ - - - + + + - + diff --git a/WorldBoxMod.cs b/WorldBoxMod.cs index cc75fd0..0db1b89 100644 --- a/WorldBoxMod.cs +++ b/WorldBoxMod.cs @@ -9,6 +9,7 @@ using NeoModLoader.services; using NeoModLoader.ui; using NeoModLoader.utils; +using NeoModLoader.utils.Builders; using UnityEngine; namespace NeoModLoader; @@ -63,7 +64,7 @@ private void Update() Harmony.CreateAndPatchAll(typeof(LM), Others.harmony_id); Harmony.CreateAndPatchAll(typeof(ResourcesPatch), Others.harmony_id); Harmony.CreateAndPatchAll(typeof(CustomAudioManager), Others.harmony_id); - + Harmony.CreateAndPatchAll(typeof(AssetPatches), Others.harmony_id); if (!SmoothLoader.isLoading()) SmoothLoader.prepare(); SmoothLoader.add(() => @@ -90,8 +91,6 @@ private void Update() ModCompileLoadService.prepareCompile(mod_nodes); }, "Load Mods Info And Prepare Mods"); - - SmoothLoader.add(() => { var mods_to_load = new List(); @@ -109,7 +108,7 @@ private void Update() } }, "Compile Mod " + mod.mod_decl.Name); } - + MasterBuilder Builder = new(); foreach (var mod in mod_nodes) { SmoothLoader.add(() => @@ -117,9 +116,11 @@ private void Update() if (mods_to_load.Contains(mod.mod_decl)) { ResourcesPatch.LoadResourceFromFolder(Path.Combine(mod.mod_decl.FolderPath, - Paths.ModResourceFolderName)); + Paths.ModResourceFolderName), out List builders); + Builder.AddBuilders(builders); ResourcesPatch.LoadResourceFromFolder(Path.Combine(mod.mod_decl.FolderPath, - Paths.NCMSAdditionModResourceFolderName)); + Paths.NCMSAdditionModResourceFolderName), out List builders2); + Builder.AddBuilders(builders2); ResourcesPatch.LoadAssetBundlesFromFolder(Path.Combine(mod.mod_decl.FolderPath, Paths.ModAssetBundleFolderName)); } @@ -129,6 +130,7 @@ private void Update() SmoothLoader.add(() => { ModCompileLoadService.loadMods(mods_to_load); + Builder.BuildAll(); ModInfoUtils.SaveModRecords(); NCMSCompatibleLayer.Init(); var successfulInit = new Dictionary(); @@ -150,7 +152,6 @@ private void Update() }, "Post-Init Mod " + mod.GetDeclaration().Name); } }, "Load Mods"); - SmoothLoader.add(ResourcesPatch.PatchSomeResources, "Patch part of Resources into game"); SmoothLoader.add(() => diff --git a/general/game/ItemAssetCreator.cs b/general/game/ItemAssetCreator.cs deleted file mode 100644 index 1f629de..0000000 --- a/general/game/ItemAssetCreator.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System.Text; -using NeoModLoader.services; - -namespace NeoModLoader.General.Game; - -/// -/// This class is used to create item assets to avoid useless coding and avoid necessary code to be forgotten. -/// -public static class ItemAssetCreator -{ - /// - /// Create material for weapon - /// - /// You should add it to manually - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static ItemAsset CreateWeaponMaterial( - string id, - BaseStats base_stats = null, - int cost_gold = 0, - KeyValuePair[] cost_resources = null, - int equipment_value = 0, - bool metallic = false, - int minimum_city_storage_resource_1 = 0, - int mod_rank = 0, - - Rarity quality = Rarity.R0_Normal, - string tech_needed = null - ) - { - ItemAsset asset = CreateAccessoryOrArmorMaterial(id, base_stats, cost_gold, cost_resources, equipment_value, - minimum_city_storage_resource_1, mod_rank, quality, tech_needed); - asset.metallic = metallic; - return asset; - } - /// - /// Create material for accessory or armor. - /// - /// You should add it to or manually - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static ItemAsset CreateAccessoryOrArmorMaterial( - string id, - BaseStats base_stats = null, - int cost_gold = 0, - KeyValuePair[] cost_resources = null, - int equipment_value = 0, - int minimum_city_storage_resource_1 = 0, - int mod_rank = 0, - Rarity quality = Rarity.R0_Normal, - string tech_needed = null - ) - { - ItemAsset asset = new ItemAsset(); - asset.id = id; - asset.base_stats = base_stats ?? asset.base_stats; - asset.cost_gold = cost_gold; - asset.equipment_value = equipment_value; - asset.minimum_city_storage_resource_1 = minimum_city_storage_resource_1; - asset.mod_rank = mod_rank; - asset.quality = quality; - //asset.tech_needed = tech_needed; TODO - - asset.cost_resource_id_1 = "none"; - asset.cost_resource_id_2 = "none"; - if (cost_resources != null) - { - switch (cost_resources.Length) - { - case 0: - break; - case 1: - asset.cost_resource_1 = cost_resources[0].Value; - asset.cost_resource_id_1 = cost_resources[0].Key; - break; - case >= 2: - asset.cost_resource_1 = cost_resources[0].Value; - asset.cost_resource_id_1 = cost_resources[0].Key; - asset.cost_resource_2 = cost_resources[1].Value; - asset.cost_resource_id_2 = cost_resources[1].Key; - break; - } - } - return asset; - } - /// - /// Create and add an item modifier - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static ItemAsset CreateAndAddModifier( - string id, - string mod_type, - int mod_rank, - string translation_key, - string[] pools, - int rarity = 1, - int equipment_value = 0, - Rarity quality = Rarity.R0_Normal, - BaseStats base_stats = null, - AttackAction action_attack_target = null, - WorldAction action_special_effect = null, - float special_effect_interval = 0.1f - ) - { - ItemAsset asset = new ItemAsset(); - asset.id = id; - asset.mod_type = mod_type; - asset.mod_rank = mod_rank; - asset.translation_key = translation_key; - asset.rarity = Math.Min(100, rarity); - asset.equipment_value = equipment_value; - asset.quality = quality; - asset.base_stats = base_stats; - asset.action_attack_target = action_attack_target; - asset.action_special_effect = action_special_effect; - asset.special_effect_interval = special_effect_interval; - - StringBuilder pool_builder = new StringBuilder(); - foreach (string pool in pools) - { - pool_builder.Append(pool); - pool_builder.Append(','); - } - pool_builder.Remove(pool_builder.Length - 1, 1); - asset.pool = pool_builder.ToString(); - - foreach (string pool_id in pools) - { - if (!AssetManager.items_modifiers.pools.ContainsKey(pool_id)) - { - LogService.LogWarning($"Invalid pool id {pool_id} for modifier {id}"); - continue; - } - - for (int i = 0; i < rarity; i++) - { - AssetManager.items_modifiers.pools[pool_id].Add(asset); - } - } - - AssetManager.items_modifiers.add(asset); - - return asset; - } - /// - /// Create and add a melee weapon - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static ItemAsset CreateMeleeWeapon( - string id, - BaseStats base_stats = null, - string material = null, - List item_modifiers = null, - string name_class = null, - List name_templates = null, - string tech_needed = null, - AttackAction action_attack_target = null, - WorldAction action_special_effect = null, - float special_effect_interval = 1f, - int equipment_value = 0, - string path_slash_animation = "effects/slashes/slash_base" - ) - { - ItemAsset asset = AssetManager.items.clone(id, "_melee"); - - asset.base_stats = base_stats ?? asset.base_stats; - asset.material = material ?? asset.material; - asset.item_modifier_ids = item_modifiers != null ? item_modifiers.ToArray() : asset.item_modifier_ids; - asset.name_class = string.IsNullOrEmpty(name_class) ? asset.name_class : name_class; - asset.name_templates = name_templates ?? asset.name_templates; - //asset.tech_needed = tech_needed; TODO - asset.action_attack_target = action_attack_target; - asset.action_special_effect = action_special_effect; - asset.special_effect_interval = special_effect_interval; - asset.equipment_value = equipment_value; - asset.path_slash_animation = path_slash_animation; - - asset.attack_type = WeaponType.Melee; - asset.equipment_type = EquipmentType.Weapon; - return asset; - } - /// - /// Create and add a range weapon - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static ItemAsset CreateRangeWeapon( - string id, - string projectile, - BaseStats base_stats = null, - string material = null, - List item_modifiers = null, - string name_class = null, - List name_templates = null, - string tech_needed = null, - AttackAction action_attack_target = null, - WorldAction action_special_effect = null, - float special_effect_interval = 1f, - int equipment_value = 0, - string path_slash_animation = "effects/slashes/slash_punch" - ) - { - ItemAsset asset = AssetManager.items.clone(id, "_range"); - - asset.base_stats = base_stats ?? asset.base_stats; - asset.material = material ?? asset.material; - asset.item_modifier_ids = item_modifiers != null ? item_modifiers.ToArray() : asset.item_modifier_ids; - asset.name_class = string.IsNullOrEmpty(name_class) ? asset.name_class : name_class; - asset.name_templates = name_templates ?? asset.name_templates; - //asset.tech_needed = tech_needed; TODO - asset.action_attack_target = action_attack_target; - asset.action_special_effect = action_special_effect; - asset.special_effect_interval = special_effect_interval; - asset.equipment_value = equipment_value; - asset.path_slash_animation = path_slash_animation; - asset.projectile = string.IsNullOrEmpty(projectile) ? "snowball" : projectile; - - StringBuilder warning_builder = new StringBuilder(); - warning_builder.AppendLine($"Some unexpected for {id} as a range weapon:"); - if (string.IsNullOrEmpty(projectile)) - { - warning_builder.AppendLine("\t projectile is null or empty. "); - } - - asset.attack_type = WeaponType.Range; - asset.equipment_type = EquipmentType.Weapon; - - return asset; - } - /// - /// Create and add an armor or accessory item. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static ItemAsset CreateArmorOrAccessory( - string id, - EquipmentType equipmentType, - BaseStats base_stats = null, - string material = null, - List item_modifiers = null, - string name_class = null, - List name_templates = null, - string tech_needed = null, - AttackAction action_attack_target = null, - WorldAction action_special_effect = null, - float special_effect_interval = 1f, - int equipment_value = 0 - ) - { - string template = equipmentType switch - { - EquipmentType.Armor => "armor", - EquipmentType.Boots => "boots", - EquipmentType.Helmet => "helmet", - EquipmentType.Ring => "ring", - EquipmentType.Amulet => "amulet", - _ => throw new ArgumentOutOfRangeException(nameof(equipmentType), equipmentType, null) - }; - ItemAsset asset = AssetManager.items.clone(id, template); - - asset.base_stats = base_stats ?? asset.base_stats; - asset.material = material ?? asset.material; - asset.item_modifier_ids = item_modifiers != null ? item_modifiers.ToArray() : asset.item_modifier_ids; - asset.name_class = string.IsNullOrEmpty(name_class) ? asset.name_class : name_class; - asset.name_templates = name_templates ?? asset.name_templates; - //asset.tech_needed = tech_needed; TODO - asset.action_attack_target = action_attack_target; - asset.action_special_effect = action_special_effect; - asset.special_effect_interval = special_effect_interval; - asset.equipment_value = equipment_value; - - asset.equipment_type = equipmentType; - return asset; - } -} \ No newline at end of file diff --git a/services/ModCompileLoadService.cs b/services/ModCompileLoadService.cs index 22e3b41..9b8aaa3 100644 --- a/services/ModCompileLoadService.cs +++ b/services/ModCompileLoadService.cs @@ -11,6 +11,7 @@ using NeoModLoader.General; using NeoModLoader.ncms_compatible_layer; using NeoModLoader.utils; +using NeoModLoader.utils.Builders; using UnityEngine; namespace NeoModLoader.services; @@ -627,13 +628,15 @@ public static bool TryCompileAndLoadModAtRuntime(ModDeclare mod_declare) bool compile_success = TryCompileModAtRuntime(mod_declare); if (!compile_success) return false; - - ResourcesPatch.LoadResourceFromFolder(Path.Combine(mod_declare.FolderPath, Paths.ModResourceFolderName)); + MasterBuilder Builder = new MasterBuilder(); + ResourcesPatch.LoadResourceFromFolder(Path.Combine(mod_declare.FolderPath, Paths.ModResourceFolderName), out List builders); ResourcesPatch.LoadResourceFromFolder(Path.Combine(mod_declare.FolderPath, - Paths.NCMSAdditionModResourceFolderName)); + Paths.NCMSAdditionModResourceFolderName), out List builders2); LoadMod(mod_declare); - + Builder.AddBuilders(builders); + Builder.AddBuilders(builders2); + Builder.BuildAll(); ResourcesPatch.PatchSomeResources(); return true; } diff --git a/services/ModReloadService.cs b/services/ModReloadService.cs index b632ff2..db83c1c 100644 --- a/services/ModReloadService.cs +++ b/services/ModReloadService.cs @@ -2,6 +2,7 @@ using NeoModLoader.constants; using NeoModLoader.General; using NeoModLoader.utils; +using NeoModLoader.utils.Builders; namespace NeoModLoader.services; @@ -17,11 +18,14 @@ public static bool HotfixMethods(IReloadable pMod, ModDeclare pModDeclare) public static bool ReloadResources(IMod pMod) { + MasterBuilder Builder = new(); ResourcesPatch.LoadResourceFromFolder(Path.Combine(pMod.GetDeclaration().FolderPath, - Paths.ModResourceFolderName)); + Paths.ModResourceFolderName), out List builders); ResourcesPatch.LoadResourceFromFolder(Path.Combine(pMod.GetDeclaration().FolderPath, - Paths.NCMSAdditionModResourceFolderName)); - + Paths.NCMSAdditionModResourceFolderName), out List builders2); + Builder.AddBuilders(builders); + Builder.AddBuilders(builders2); + Builder.BuildAll(); return false; } diff --git a/utils/Builders/ActorAssetBuilder.cs b/utils/Builders/ActorAssetBuilder.cs new file mode 100644 index 0000000..9e0e0c9 --- /dev/null +++ b/utils/Builders/ActorAssetBuilder.cs @@ -0,0 +1,16 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder to create Actor Assets + /// NOT FINUSHED!!!!!!!!!!!!!!!!!!!! + /// + public sealed class ActorAssetBuilder : UnlockableAssetBuilder + { + /// + public ActorAssetBuilder(string ID) : base(ID) { } + /// + public ActorAssetBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public ActorAssetBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + } +} diff --git a/utils/Builders/ActorTraitBuilder.cs b/utils/Builders/ActorTraitBuilder.cs new file mode 100644 index 0000000..6e20874 --- /dev/null +++ b/utils/Builders/ActorTraitBuilder.cs @@ -0,0 +1,145 @@ +using NeoModLoader.utils.SerializedAssets; +using Newtonsoft.Json; +using System.Collections.Concurrent; + +namespace NeoModLoader.utils.Builders +{ + /// + /// A Method to get Additional custom stats depending on the actor + /// + public delegate BaseStats GetAdditionalBaseStatsMethod(Actor Actor); + /// + /// A Builder for creating Actor Traits + /// + public sealed class ActorTraitBuilder : BaseTraitBuilder + { + internal static ConcurrentDictionary AdditionalBaseStatMethods = new(); + /// + public ActorTraitBuilder(string ID) : base(ID) + { + Group = S_TraitGroup.miscellaneous; + } + /// + protected override void LoadFromPath(string FilePathToBuild) + { + SerializedActorTrait assetSerialized = JsonConvert.DeserializeObject(File.ReadAllText(FilePathToBuild)); + Asset = SerializedActorTrait.ToAsset(assetSerialized); + } + /// + public ActorTraitBuilder(string ID, bool LoadImmediately) : base(ID, LoadImmediately) { } + /// + public ActorTraitBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + void LinkWithLibrary() + { + if (Asset.combat) + { + Library.pot_traits_combat.Add(Asset); + } + if (Asset.is_mutation_box_allowed) + { + Library.pot_traits_mutation_box.Add(Asset); + } + if (Asset.rate_acquire_grow_up != 0) + { + for (int j = 0; j < Asset.rate_acquire_grow_up; j++) + { + Library.pot_traits_growup.Add(Asset); + } + } + if (Asset.rate_birth != 0) + { + for (int i = 0; i < Asset.rate_birth; i++) + { + Library.pot_traits_birth.Add(Asset); + } + } + } + /// + public override void Build(bool SetRarityAutomatically = false, bool AutoLocalize = true, bool LinkWithOtherAssets = false) + { + base.Build(SetRarityAutomatically, AutoLocalize, LinkWithOtherAssets); + LinkWithLibrary(); + Library.checkDefault(Asset); + Asset.only_active_on_era_flag = Asset.era_active_moon || Asset.era_active_night; + } + /// + /// (Optional) creates a method which gives custom stats to a actor who has this trait + /// + public GetAdditionalBaseStatsMethod AdditionalBaseStatsMethod { set + { + if (!AdditionalBaseStatMethods.TryAdd(Asset.id, value)) + { + AdditionalBaseStatMethods[Asset.id] = value; + } + } + } + /// + /// if true, actors cannot have this trait if they have the strong minded trait + /// + public bool AffectsMind { get { return Asset.affects_mind; } set { Asset.affects_mind = value; } } + /// + /// if true, actors can cure this trait and remove it + /// + public bool CanBeCured { get { return Asset.can_be_cured; } set { Asset.can_be_cured = value; } } + /// + /// when actors with the subspecies trait accelerated healing age, they can remove traits which have this set to true + /// + public bool RemovedByAcceleratedHealing { get { return Asset.can_be_removed_by_accelerated_healing; } set { Asset.can_be_removed_by_accelerated_healing = value; } } + /// + /// if true, Divine Light can remove this trait from actors + /// + public bool RemovedByDevineLight { get { return Asset.can_be_removed_by_accelerated_healing; } set { Asset.can_be_removed_by_divine_light = value; } } + /// + /// if true, this actor trait represents a combat skill, and actors could try to gain this trait + /// + public bool IsCombatSkill { get { return Asset.combat; } set { Asset.combat = value; } } + /// + /// if true, the base stats of this trait will only be applied to the actor in the age of dark + /// + public bool ActiveInDarkEra { get { return Asset.era_active_night; } set { Asset.era_active_night = value;} } + /// + /// if true, the base stats of this trait will only be applied to the actor in the age of moon + /// + public bool ActiveInMoonEra { get { return Asset.era_active_moon; } set { Asset.era_active_moon = value; } } + /// + /// The ID of a WILD kingdom (Default, Non Civ) that this trait forces the actor to, the trait must add the forcedkingdomadd effect to its ActionOnAdd, you should also add it to ActionOnLoad + /// + public string ForcedKingdomID { get { return Asset.forced_kingdom; } set { Asset.forced_kingdom = value; } } + /// + /// if true, actors can get this trait from mutation box + /// + public bool UsedInMutationBox { get { return Asset.is_mutation_box_allowed; } set { Asset.is_mutation_box_allowed = value; } } + /// + /// when actors try to make best friends, this goes into account, with increased likeability increasing the chance they become friends + /// + public float ActorsLikeability { get { return Asset.likeability; } set { Asset.likeability = value; } } + /// + /// used for when actors try to make best friends, relations between kings, and kings with city leaders, if the other actor has a trait which is an opposite of this trait, the likeability factor is increased by this devided by 100 + /// + public int OppositeTraitLikeability { get { return Asset.opposite_trait_mod; } set { Asset.opposite_trait_mod = value; } } + /// + /// When an actor turns into an adult, he can get new traits, the chance of getting this trait is determined by its rate + /// + public int RateAcquireWhenGrownUp { get { return Asset.rate_acquire_grow_up; } set { Asset.rate_acquire_grow_up = value; } } + /// + /// the chance of a child inheriting this trait from a parent, also if it is above 0 cloned actors will get this trait from their original + /// + public int RateBirth { get { return Asset.rate_birth; } set { Asset.rate_birth = value; } } + /// + /// same thing as rate birth, if this is 0 it will become rate inherit * 10 + /// + public int RateInherit { get { return Asset.rate_inherit; } set { Asset.rate_inherit = value; } } + /// + /// When creating the zombie varient of a actor asset, the game removes any default traits of the actor asset which have this enabled + /// + public bool RemoveForZombies { get { return Asset.remove_for_zombie_actor_asset; } set { Asset.remove_for_zombie_actor_asset = value; } } + /// + /// basically the same as OppositeTraitLikeability but applied when both actors have this trait + /// + public int SameTraitLikeability { get { return Asset.same_trait_mod; } set { Asset.same_trait_mod = value; } } + /// + /// doesnt do anything + /// + public TraitType Type { get { return Asset.type; } set { Asset.type = value; } } + } +} \ No newline at end of file diff --git a/utils/Builders/AssetBuilder.cs b/utils/Builders/AssetBuilder.cs new file mode 100644 index 0000000..3ada04e --- /dev/null +++ b/utils/Builders/AssetBuilder.cs @@ -0,0 +1,112 @@ +using NeoModLoader.services; +using NeoModLoader.utils.SerializedAssets; +using Newtonsoft.Json; +namespace NeoModLoader.utils.Builders +{ + /// + /// The Base Class for building assets, you only use this if your mod has custom assets, otherwise use its derived types!!!!!!!!!!! + /// + public class AssetBuilder : Builder where A : Asset, new() where AL : AssetLibrary + { + private AssetBuilder() { Library = GetLibrary(); } + /// + /// The Asset being built + /// + public A Asset { get; protected set; } + /// + /// the Library to add the asset to + /// + public readonly AL Library; + /// + /// Used so the child classes can create their asset before the builder inititates + /// + protected virtual A CreateAsset(string ID) { return new A() { id = ID }; } + /// + /// Initiates the builder + /// + protected virtual void Init() { } + /// + /// Loads the asset from FilePathToBuild + /// + protected virtual void LoadFromPath(string FilePathToBuild) + { + SerializableAsset assetSerialized = JsonConvert.DeserializeObject>(File.ReadAllText(FilePathToBuild)); + Asset = SerializableAsset.ToAsset(assetSerialized); + } + void LoadAssetFromPath(string FilePathToBuild) + { + try + { + LoadFromPath(FilePathToBuild); + } + catch + { + LogService.LogError($"the asset {Path.GetFileName(FilePathToBuild)} is outdated or corrupted!, make sure to serialize it on the latest version and use default serialization settings"); + } + } + /// + /// Creates a builder with a new asset with Id ID, other variables are default + /// + public AssetBuilder(string ID) : this() + { + Asset = CreateAsset(ID); + Init(); + } + internal string FilePathToBuild = null; + /// + /// Deserializes a Asset loaded from a file path + /// + /// + /// if LoadImmediatly is false, the asset will be loaded when built + /// + /// this path starts from the operating system root + /// the reason for this is when NML automatically loads file assets, it loads the assets before the mod is compiled, and so it then has to deserialize the assets after the mod is compiled because the assets could have delegates which point to the mod, and deserialization will produce an error if the delegates point to nothing + public AssetBuilder(string FilePath, bool LoadImmediately) : this() + { + if (LoadImmediately) + { + LoadAssetFromPath(FilePath); + } + else + { + FilePathToBuild = FilePath; + } + } + + /// + /// Creates a builder, and the asset being built is copied off a asset with ID CopyFrom + /// + public AssetBuilder(string ID, string CopyFrom) : this() + { + bool Cloned = CopyFrom != null; + if (Cloned) + { + Library.clone(out A Asset, Library.get(CopyFrom)); + Asset.id = ID; + this.Asset = Asset; + } + else + { + Asset = CreateAsset(ID); + } + Init(); + } + AL GetLibrary() { + return AssetManager._instance._list.OfType().FirstOrDefault() ?? throw new NotImplementedException($"No library found for {typeof(A).Name}!"); + } + /// + /// Builds The Asset + /// + public override void Build(bool LinkWithOtherAssets) + { + if (FilePathToBuild != null) + { + LoadAssetFromPath(FilePathToBuild); + } + Library.add(Asset); + base.Build(LinkWithOtherAssets); + } + /// + public override void LinkAssets() { } + } +} \ No newline at end of file diff --git a/utils/Builders/AssetPatches.cs b/utils/Builders/AssetPatches.cs new file mode 100644 index 0000000..0dc69e7 --- /dev/null +++ b/utils/Builders/AssetPatches.cs @@ -0,0 +1,65 @@ +using HarmonyLib; +using NeoModLoader.utils.Builders; +using System.Reflection.Emit; +using static NeoModLoader.utils.Builders.ActorTraitBuilder; + +namespace NeoModLoader.utils +{ + internal class AssetPatches + { + [HarmonyPatch(typeof(Actor), "updateStats")] + [HarmonyTranspiler] + static IEnumerable MergeWithCustomStats(IEnumerable instructions) + { + CodeMatcher Matcher = new CodeMatcher(instructions); + Matcher.MatchForward(false, new CodeMatch[] + { + new CodeMatch(OpCodes.Callvirt, AccessTools.Method(typeof(BaseStats), nameof(BaseStats.clear))) + }); + Matcher.Advance(1); + Matcher.Insert(new CodeInstruction[] + { + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AssetPatches), nameof(MergeCustomStats))) + }); + return Matcher.Instructions(); + } + static void MergeCustomStats(Actor __instance) + { + foreach(ActorTrait trait in __instance.traits) + { + if(AdditionalBaseStatMethods.TryGetValue(trait.id, out GetAdditionalBaseStatsMethod method)) + { + __instance.stats.mergeStats(method(__instance)); + } + } + } + static BaseStats[] GetCustomStats(ActorTrait trait) + { + if(SelectedUnit.unit == null || !SelectedUnit.unit.hasTrait(trait)) + { + return Array.Empty(); + } + if(!AdditionalBaseStatMethods.TryGetValue(trait.id, out GetAdditionalBaseStatsMethod method)) + { + return Array.Empty(); + } + return new BaseStats[] { method(SelectedUnit.unit) }; + } + [HarmonyPatch(typeof(TooltipLibrary), "showTrait")] + [HarmonyTranspiler] + static IEnumerable ShowCustomStats(IEnumerable instructions) + { + CodeMatcher Matcher = new CodeMatcher(instructions); + Matcher.MatchForward(false, new CodeMatch[] + { + new CodeMatch(OpCodes.Call, AccessTools.Field(typeof(Array), nameof(Array.Empty))) + }); + Matcher.RemoveInstruction(); + Matcher.Insert(new CodeInstruction[] { + new CodeInstruction(OpCodes.Ldloc_0), + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AssetPatches), nameof(GetCustomStats))) }); + return Matcher.Instructions(); + } + } +} diff --git a/utils/Builders/AugmentationAssetBuilder.cs b/utils/Builders/AugmentationAssetBuilder.cs new file mode 100644 index 0000000..176b132 --- /dev/null +++ b/utils/Builders/AugmentationAssetBuilder.cs @@ -0,0 +1,115 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder for building augmentation assets + /// + public class AugmentationAssetBuilder : UnlockableAssetBuilder where A : BaseAugmentationAsset, new() where AL : BaseLibraryWithUnlockables + { + /// + public AugmentationAssetBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public AugmentationAssetBuilder(string ID) : base(ID) { } + /// + public AugmentationAssetBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + /// + /// the combat actions in this Asset, any actors with this Asset will have this actiom, and if this Asset is for a clan/subspecies/etc, any actors in that group will also have it + /// + public IEnumerable CombatActions { get + { + return Asset.combat_actions_ids; + } + set + { + foreach(string action in value) + { + Asset.addCombatAction(action); + } + } + } + void LinkDecisions() + { + if (Asset.decision_ids != null) + { + Asset.decisions_assets = new DecisionAsset[Asset.decision_ids.Count]; + for (int i = 0; i < Asset.decision_ids.Count; i++) + { + string tDecisionID = Asset.decision_ids[i]; + DecisionAsset tDecisionAsset = AssetManager.decisions_library.get(tDecisionID); + Asset.decisions_assets[i] = tDecisionAsset; + } + } + } + /// + public override void LinkAssets() + { + LinkDecisions(); + Asset.linkCombatActions(); + Asset.linkSpells(); + base.LinkAssets(); + } + /// + /// the Decisions (Neurons) in the Object with this Asset, if the object is a group like clan or subspecies, all actors in the group get this decision + /// + public IEnumerable Decisions { get { return Asset.decision_ids; } set + { + foreach (string action in value) + { + Asset.addDecision(action); + } + } } + /// + /// the spells in the Asset, which an actor can use + /// + public IEnumerable Spells { get { return Asset.spells_ids; } set + { + foreach(string action in value) + { + Asset.addSpell(action); + } + } } + /// + /// an action performed on something hit by this object or an object apart of a group with this trait + /// + public AttackAction AttackAction { get { return Asset.action_attack_target; } set { Asset.action_attack_target = value; } } + /// + /// The Action Performed on something, when this asset is added to it! + /// + public WorldActionTrait ActionWhenAdded { get { return Asset.action_on_add; } set { Asset.action_on_add = value; } } + /// + /// The Action Performed on something, when this asset is removed from it! + /// + public WorldActionTrait ActionWhenRemoved { get { return Asset.action_on_remove; } set { Asset.action_on_remove = value; } } + /// + /// The Action Performed on something with this asset, when that something is loaded from a save file + /// + public WorldActionTrait ActionOnLoad { get { return Asset.action_on_load; } set { Asset.action_on_load = value; } } + /// + /// The Action Performed on something, this keeps happening until that something is destroyed or this is removed + /// + public WorldAction ActonSpecialEffect { get { return Asset.action_special_effect; } set { Asset.action_special_effect = value; } } + /// + /// the cooldown for the ActionSpecialEffect + /// + public float SpecialEffectCoolDown { get { return Asset.special_effect_interval; } set { Asset.special_effect_interval = value; } } + /// + /// if false, it cant be removed from something + /// + public bool CanBeRemoved { get { return Asset.can_be_removed; } set { Asset.can_be_removed = value; } } + /// + /// if false, this asset cant be given to something, AND it cant be removed + /// + public bool CanBeGiven { get { return Asset.can_be_given; } set { Asset.can_be_given = value; } } + /// + /// The ID of the group this asset is in, you can find them in S_TraitGroup, S_EquipmentGroup, etc + /// + public string Group { get { return Asset.group_id; } set { Asset.group_id = value; } } + /// + /// The Priority used for when this asset is being displayed next to other assets in its Group + /// + public int Priority { get { return Asset.priority; } set { Asset.priority = value; } } + /// + /// If true, Meta Editors like the Plots Editor and Equipment Editor will show this asset + /// + public bool ShowInMetaEditor { get { return Asset.show_in_meta_editor; } set { Asset.show_in_meta_editor = value; } } + } +} diff --git a/utils/Builders/BaseTraitBuilder.cs b/utils/Builders/BaseTraitBuilder.cs new file mode 100644 index 0000000..3417931 --- /dev/null +++ b/utils/Builders/BaseTraitBuilder.cs @@ -0,0 +1,355 @@ +using FMOD; +using HarmonyLib; +using NeoModLoader.General; + +namespace NeoModLoader.utils.Builders +{ + /// + /// The Base Builder for Trait builders + /// + public class BaseTraitBuilder : AugmentationAssetBuilder where A : BaseTrait, new() where AL : BaseTraitLibrary + { + /// + public BaseTraitBuilder(string ID) : base(ID) { } + /// + public BaseTraitBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public BaseTraitBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + + /// + protected override void Init() + { + Description1ID = null; + Description2ID = null; + NameID = (null); + } + /// + /// the traits in the asset's list of opposite traits, if a object has one of these traits it cannot add this trait + /// + public IEnumerable OppositeTraits + { + set + { + foreach (string trait in value) + { + Asset.addOpposite(trait); + } + } + get + { + return Asset.opposite_list; + } + } + /// + /// the tags in assets base stats meta + /// + public IEnumerable MetaTags + { + get + { + return Asset.base_stats_meta._tags; + } + set + { + foreach (string tag in value) + { + Asset.base_stats_meta.addTag(tag); + } + } + } + /// + /// sets traits which this trait automatically removes when it is added to a object + /// + public IEnumerable TraitsToRemove + { + set + { + Asset.traits_to_remove_ids = TraitsToRemove.ToArray(); + } + get + { + return Asset.traits_to_remove_ids; + } + } + /// + public override void Build(bool LinkWithOtherAssets) + { + Build(false, true, LinkWithOtherAssets); + } + /// + /// all other traits in the library will be added to the opposites list if the ShouldOppose function returns true + /// + /// a function which has a trait as input and outputs true if added to opposites list + public IEnumerable> OpposeAllOtherTraits { set { + foreach(Func func in value) + { + foreach (A asset in Library.list) + { + if (asset.id != Asset.id && func(asset)) + { + Asset.addOpposite(asset.id); + } + } + } + } } + void LinkWithActors() + { + foreach (ActorAsset tActorAsset in AssetManager.actor_library.list) + { + List traits = Library.getDefaultTraitsForMeta(tActorAsset); + if (traits != null && traits.Contains(Asset.id)) + { + Asset.default_for_actor_assets ??= new List(); + Asset.default_for_actor_assets.Add(tActorAsset); + } + } + } + void LinkWithTraits() + { + if (Asset.opposite_list != null && Asset.opposite_list.Count > 0) + { + Asset.opposite_traits = new HashSet(Asset.opposite_list.Count); + foreach (string tID in Asset.opposite_list) + { + A tOppositeTrait = Library.get(tID); + Asset.opposite_traits.Add(tOppositeTrait); + } + } + if (Asset.traits_to_remove_ids != null) + { + int tCount = Asset.traits_to_remove_ids.Length; + Asset.traits_to_remove = new A[tCount]; + for (int i = 0; i < tCount; i++) + { + string ID = Asset.traits_to_remove_ids[i]; + A tTraitToAdd = Library.get(ID); + Asset.traits_to_remove[i] = tTraitToAdd; + } + } + } + void CheckIcon() + { + if (string.IsNullOrEmpty(Asset.path_icon)) + { + Asset.path_icon = Library.icon_path + Asset.getLocaleID(); + } + } + void LinkWithBaseLibrary() + { + if (Asset.spawn_random_trait_allowed) + { + Library._pot_allowed_to_be_given_randomly.AddTimes(Asset.spawn_random_rate, Asset); + } + } + void SetRarityAutomatically() + { + if (Asset.unlocked_with_achievement) + { + Asset.rarity = Rarity.R3_Legendary; + } + else + { + bool tHasDecisions = Asset.decision_ids != null; + bool tHasSpells = Asset.spells_ids != null; + bool tHasCombatActions = Asset.combat_actions_ids != null; + bool tHasTag = Asset.base_stats.hasTags(); + bool tHasPlot = !string.IsNullOrEmpty(Asset.plot_id); + int tCount = 0; + if (Asset.action_death != null || Asset.action_special_effect != null || Asset.action_get_hit != null || Asset.action_birth != null || Asset.action_attack_target != null || Asset.action_on_add != null || Asset.action_on_remove != null || Asset.action_on_load != null) + { + tCount++; + } + if (tHasDecisions) + { + tCount++; + } + if (tHasSpells) + { + tCount++; + } + if (tHasCombatActions) + { + tCount++; + } + if (tHasTag) + { + tCount++; + } + if (tHasPlot) + { + tCount++; + } + if (tCount > 0) + { + if (tCount == 1) + { + Asset.rarity = Rarity.R1_Rare; + } + else + { + Asset.rarity = Rarity.R2_Epic; + } + } + } + } + /// + /// when a creature/group is spawned, this is the chance they get the trait, this is not out of 100 + /// + public int ChanceToGetOnCreation { set { + Asset.spawn_random_rate = value; + Asset.spawn_random_trait_allowed = value > 0; + } + get + { + return Asset.spawn_random_rate; + } + } + /// + public override void LinkAssets() + { + LinkWithTraits(); + LinkWithActors(); + base.LinkAssets(); + } + /// + /// Builds the Trait, if autolocalize is on it will use the ID'S as the translated text + /// + /// + /// if you have opposite traits/traits to remove which you build after you build this, link it with the other traits after you build them! + /// + public virtual void Build(bool SetRarityAutomatically = false, bool AutoLocalize = true, bool LinkWithOtherAssets = false) + { + base.Build(LinkWithOtherAssets); + if (AutoLocalize) + { + Localize(Asset.special_locale_id, Asset.special_locale_description, Asset.special_locale_description_2); + } + if (SetRarityAutomatically) + { + this.SetRarityAutomatically(); + } + CheckIcon(); + LinkWithBaseLibrary(); + } + /// + /// the ID of the Localized Description, setting this does not fully localize the asset, you must either call Localize() or have a localization folder + /// + public string Description1ID { set + { + Asset.special_locale_description = value; + if (value == null) + { + Asset.has_description_1 = false; + } + else + { + Asset.has_description_1 = true; + } + } + get + { + return Asset.special_locale_description; + } + } + /// + /// the ID of the Localized 2nd Description, setting this does not fully localize the asset, you must either call Localize() or have a localization folder + /// + public string Description2ID + { + set + { + Asset.special_locale_description_2 = value; + if (value == null) + { + Asset.has_description_2 = false; + } + else + { + Asset.has_description_2 = true; + } + } + get + { + return Asset.special_locale_description_2; + } + } + /// + /// Localizes the Asset, you must set the ID's of the descriptions and name first + /// + public void Localize(string Name = null, string Description = null, string Description2 = null) + { + if (Name != null) { + LM.AddToCurrentLocale(Asset.special_locale_id, Name); + } + if (Description != null) + { + LM.AddToCurrentLocale(Asset.special_locale_description, Description); + } + if (Description2 != null) + { + LM.AddToCurrentLocale(Asset.special_locale_description_2, Description2); + } + } + /// + /// the ID of the Localized Name, setting this does not fully localize the asset, you must either call Localize() or have a localization folder + /// + public string NameID + { + set + { + Asset.special_locale_id = value; + if (value == null) + { + Asset.has_localized_id = false; + } + else + { + Asset.has_localized_id = true; + } + } + get + { + return Asset.special_locale_id; + } + } + /// + /// The Displayed Rarity of the Trait + /// + /// + /// also, sometimes this controls how common it is like in subspecies traits + /// + public Rarity Rarity { get { return Asset.rarity; } set { Asset.rarity = value; } } + /// + /// just like base stats, but mainly used to add Tags, not stats + /// + public BaseStats BaseStatsMeta { get { return Asset.base_stats_meta; } set { Asset.base_stats_meta = value; } } + /// + /// Not used for actor traits, but for kingdoms, subspecies, clans, etc. any actor born in that group will perform this action + /// + public WorldAction ActionOnBirth { get { return Asset.action_birth; } set { Asset.action_birth = value; } } + /// + /// used for actor traits and groups (kingdoms, clans, etc), any actor with the trait or in the group will perform this action on death + /// + public WorldAction ActionOnDeath { get { return Asset.action_death; } set { Asset.action_death = value; } } + /// + /// Used for groups (subspecies, clan, etc) whenever a actor in this group reaches their birthday (age goes up by one) they perform this action + /// + public WorldAction ActionOnGrowth { get { return Asset.action_growth; } set { Asset.action_growth = value;} } + /// + /// used for actor traits and group traits (traits for kingdoms, clans, etc) any actor with the trait or in the group with this trait perform this when they are hit + /// + public GetHitAction ActionGetHit { get { return Asset.action_get_hit; } set { Asset.action_get_hit = value; } } + /// + /// when an actor writes a book, any trait from his language, culture, his traits, and religion can be in the book and can be transfered to those who read it, a trait must have this set to true to be able to be written + /// + public bool CanBeInBook { get { return Asset.can_be_in_book; } set { Asset.can_be_in_book = value; } } + /// + /// Used for languages and cultures, for languages it is the chance (out of 100) that the book written in this language does its action, and for cultures it controls experience for actors, spread of culture, etc. + /// + public float CustomValue { get { return Asset.value; } set { Asset.value = value;} } + /// + /// this stores an ID of a plot asset, used for religions, if a religion trait has a plotID, the plot can be done by the religion with the trait + /// + public string PlotID { get { return Asset.plot_id; } set { Asset.plot_id = value; } } + } +} \ No newline at end of file diff --git a/utils/Builders/Builder.cs b/utils/Builders/Builder.cs new file mode 100644 index 0000000..0d25652 --- /dev/null +++ b/utils/Builders/Builder.cs @@ -0,0 +1,17 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// The Base Class For All Builders + /// + public abstract class Builder + { + /// + /// Builds Something + /// + public virtual void Build(bool LinkWithOtherAssets) { if(LinkWithOtherAssets) { LinkAssets(); } } + /// + /// links this builder with other assets + /// + public abstract void LinkAssets(); + } +} \ No newline at end of file diff --git a/utils/Builders/ClanTraitBuilder.cs b/utils/Builders/ClanTraitBuilder.cs new file mode 100644 index 0000000..4b68bfb --- /dev/null +++ b/utils/Builders/ClanTraitBuilder.cs @@ -0,0 +1,32 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder to create clan traits + /// + public sealed class ClanTraitBuilder : BaseTraitBuilder + { + /// + public ClanTraitBuilder(string ID) : base(ID) { } + /// + public ClanTraitBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public ClanTraitBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + + /// + /// Stats which are applied to Males in this clan + /// + public BaseStats BaseStatsMale + { + get { return Asset.base_stats_male; } + set { Asset.base_stats_male = value; } + } + /// + /// Stats which are applied to Females in this clan + /// + public BaseStats BaseStatsFemale + { + get { return Asset.base_stats_female; } + set { Asset.base_stats_female = value; } + } + } +} diff --git a/utils/Builders/CultureTraitBuilder.cs b/utils/Builders/CultureTraitBuilder.cs new file mode 100644 index 0000000..c9ac926 --- /dev/null +++ b/utils/Builders/CultureTraitBuilder.cs @@ -0,0 +1,43 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder to build culture traits + /// + public sealed class CultureTraitBuilder : BaseTraitBuilder + { + /// + public CultureTraitBuilder(string ID) : base(ID) { } + /// + public CultureTraitBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public CultureTraitBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + /// + /// the weapons which this culture produces + /// + public IEnumerable Weapons { get { return Asset.related_weapons_ids; } set { foreach (string weapon in value) { Asset.addWeaponSpecial(weapon); } } } + /// + /// the weapon sub types which this culture produces + /// + public IEnumerable WeaponSubTypes { get { return Asset.related_weapon_subtype_ids; } set { foreach (string weapon in value) { Asset.addWeaponSubtype(weapon); } } } + /// + /// used for a cultures building layout plan, the zone checker determines weather buildings can be placed in the target zone or not + /// + public PassableZoneChecker TownLayoutPlan + { + get { return Asset.passable_zone_checker; } + set + { + Asset.setTownLayoutPlan(value); + } + } + /// + public override void LinkAssets() + { + if (Asset.town_layout_plan) + { + OpposeAllOtherTraits = new[] { (CultureTrait trait) => trait.town_layout_plan }; + } + base.LinkAssets(); + } + } +} \ No newline at end of file diff --git a/utils/Builders/GroupAssetBuilder.cs b/utils/Builders/GroupAssetBuilder.cs new file mode 100644 index 0000000..e194d3f --- /dev/null +++ b/utils/Builders/GroupAssetBuilder.cs @@ -0,0 +1,47 @@ +using NeoModLoader.General; +using UnityEngine; + +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder to create group/category assets + /// + public sealed class GroupAssetBuilder : AssetBuilder> where A : BaseCategoryAsset, new() + { + /// + public GroupAssetBuilder(string ID) : base(ID) { } + /// + public GroupAssetBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public GroupAssetBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + /// + public override void Build(bool LinkWithOtherAssets) + { + Localize(); + Build(LinkWithOtherAssets); + } + /// + /// Localizes the name + /// + public void Localize(string LocalName = null) + { + LocalName ??= Asset.getLocaleID(); + LM.AddToCurrentLocale(Asset.getLocaleID(), LocalName); + } + /// + /// The ID used for localization + /// + public string Name { get { return Asset.name; } set { Asset.name = value; } } + /// + /// A Hex Value that represents the color + /// + public string ColorHexCode { get { return Asset.color; } set { Asset.color = value; } } + /// + /// Sets the color using a Color + /// + public void SetColor(Color color) + { + ColorHexCode = Toolbox.colorToHex(color); + } + } +} \ No newline at end of file diff --git a/utils/Builders/ItemBuilder.cs b/utils/Builders/ItemBuilder.cs new file mode 100644 index 0000000..d3fcbee --- /dev/null +++ b/utils/Builders/ItemBuilder.cs @@ -0,0 +1,265 @@ +using NeoModLoader.General; +using NeoModLoader.utils.SerializedAssets; +using Newtonsoft.Json; +using UnityEngine; + +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder to build items + /// + public sealed class ItemBuilder : AugmentationAssetBuilder + { + static string GetEquipmentType(EquipmentType pType) => pType switch + { + EquipmentType.Weapon => "$melee", + EquipmentType.Helmet => "$helmet", + EquipmentType.Armor => "$armor", + EquipmentType.Boots => "$boots", + EquipmentType.Ring => "$ring", + EquipmentType.Amulet => "$amulet", + _ => "$equipment" + }; + /// + public ItemBuilder(string Path, bool LoadImmediately) : base(Path, LoadImmediately) { } + /// + public ItemBuilder(string ID, EquipmentType Type) : base(ID, GetEquipmentType(Type)) { } + /// + /// creates a weapon, and if projectileifranged is not null, a ranged weapon with projectileifranged being the ID of the projectile + /// + public ItemBuilder(string WeaponID, string ProjectileIfRanged = null) : base(WeaponID, "$melee") { + if (ProjectileIfRanged != null) + { + ConvertIntoRangedWeapon(ProjectileIfRanged); + } + } + /// + protected override void LoadFromPath(string FilePathToBuild) + { + SerializedItemAsset assetSerialized = JsonConvert.DeserializeObject(File.ReadAllText(FilePathToBuild)); + Asset = SerializedItemAsset.ToAsset(assetSerialized); + CultureTraitsThisWeaponIsIn = assetSerialized.CultureTraitsThisItemIsIn; + CultureTraitsThisWeaponsTypeIsIn = assetSerialized.CultureTraitsThisItemsTypeIsIn; + } + /// + protected override ItemAsset CreateAsset(string ID) + { + ItemAsset asset = new() + { + id = ID, + group_id = "sword", + pool = "equipment", + name_templates = new List() { "armor_name"} + }; + asset.setCost(0); + return asset; + } + /// + /// converts the Asset into a ranged weapon, its type must be Weapon to do this + /// + public void ConvertIntoRangedWeapon(string ProjectileID) + { + Asset.pool = "range"; + Asset.attack_type = WeaponType.Range; + Asset.projectile = ProjectileID; + Asset.base_stats["projectiles"] = 1f; + Asset.base_stats["damage_range"] = 0.6f; + } + /// + public override void LinkAssets() + { + foreach(string CultureTraitID in CultureTraitsThisWeaponIsIn) + { + AssetManager.culture_traits.get(CultureTraitID).addWeaponSpecial(Asset.id); + } + foreach (string CultureTraitID in CultureTraitsThisWeaponsTypeIsIn) + { + AssetManager.culture_traits.get(CultureTraitID).addWeaponSubtype(WeaponSubType); + } + if (Asset.item_modifier_ids != null) + { + Asset.item_modifiers = new ItemAsset[Asset.item_modifier_ids.Length]; + for (int i = 0; i < Asset.item_modifier_ids.Length; i++) + { + string tModID = Asset.item_modifier_ids[i]; + ItemAsset tModData = AssetManager.items_modifiers.get(tModID); + if (tModData == null) + { + BaseAssetLibrary.logAssetError("ItemLibrary: Item Modifier Asset not found", tModID); + } + else + { + Asset.item_modifiers[i] = tModData; + } + } + } + base.LinkAssets(); + } + /// + public override void Build(bool LinkWithOtherAssets) + { + Build(null, true, LinkWithOtherAssets); + } + /// + /// Builds the Item, if description is not null it will automatically localize + /// + /// The Description of the item, if null, localization will have to take place from a localize json file + /// is unlocked by default + /// links with other assets + public void Build(string Description = null, bool UnlockedByDefault = true, bool LinkWithOtherAssets = false) + { + if(Description != null) + { + Localize(Asset.getLocaleID(), Description); + } + AddWeaponsSprite(); + LinkWithLibrary(); + if (UnlockedByDefault) + { + UnlockByDefault(); + } + base.Build(LinkWithOtherAssets); + } + void LinkWithLibrary() + { + if (!Library.equipment_by_subtypes.ContainsKey(Asset.equipment_subtype)) + { + Library.equipment_by_subtypes.Add(Asset.equipment_subtype, new List()); + } + Library.equipment_by_subtypes[Asset.equipment_subtype].Add(Asset); + if (Asset.is_pool_weapon) + { + Library.pool_weapon_assets_all.Add(Asset); + } + if (!Asset.is_pool_weapon) + { + string tGroupId = Asset.group_id; + if (!Library.equipment_by_groups_all.ContainsKey(tGroupId)) + { + Library.equipment_by_groups_all.Add(tGroupId, new List()); + } + Library.equipment_by_groups_all[tGroupId].Add(Asset); + } + if (Asset.isUnlocked()) + { + if (Asset.is_pool_weapon && !Library.pool_weapon_assets_unlocked.Contains(Asset)) + { + Library.pool_weapon_assets_unlocked.Add(Asset); + } + if (!Asset.is_pool_weapon) + { + string tGroupId = Asset.group_id; + if (!Library.equipment_by_groups_unlocked.ContainsKey(tGroupId)) + { + Library.equipment_by_groups_unlocked.Add(tGroupId, new List()); + } + List tList = Library.equipment_by_groups_unlocked[tGroupId]; + if (!tList.Contains(Asset)) + { + tList.Add(Asset); + } + } + } + } + /// + /// the name templates of this item, a random template is chosen when the game uses them + /// + public IEnumerable NameTemplates + { + get { return Asset.name_templates; } + set { Asset.name_templates = value.ToList(); } + } + void AddWeaponsSprite() + { + var dictItems = ActorAnimationLoader._dict_items; + var sprite = Resources.Load("weapons/" + Asset.id); + dictItems.Add("w_" + Asset.id, new List() { sprite }); + } + /// + /// Localizes the Items name and description the current language + /// + public void Localize(string Name, string Description) + { + LM.AddToCurrentLocale(Asset.getLocaleID(), Name); + LM.AddToCurrentLocale(Asset.getDescriptionID(), Description); + } + /// + /// The ID of the Name, doesnt have to be set + /// + public string NameID { get { return Asset.translation_key; } set { Asset.translation_key = value; } } + /// + /// The Displayed Rarity of the Asset + /// + public Rarity Rarity { get { return Asset.quality; } set { Asset.quality = value; } } + /// + /// The Value of this equipment, used for when cities want to craft something, they prefer equipment with higher values + /// + public int EquipmentValue { get { return Asset.equipment_value; } set { Asset.equipment_value = value; } } + /// + /// the texture path to the animation that gets played when a actor holding this weapon attacks something + /// + public string SlashAnimationPath { get { return Asset.path_slash_animation; } set { Asset.path_slash_animation = value;} } + /// + /// if true, the weapon displayed in the actors HAND is animated + /// + public bool Animated { get { return Asset.animated; } set { Asset.animated = value; } } + /// + /// The ID of a group of items (subtype) this item is apart of, different cultures have different prefered sub types, cultures only create weapons apart of their prefered subtypes + /// + public string WeaponSubType { get { return Asset.equipment_subtype; } set { Asset.equipment_subtype = value; } } + /// + /// culture traits who have this items subtype added, MUST be a weapon + /// + /// + /// the builder must link its assets so the cultures actually add it + /// + public IEnumerable CultureTraitsThisWeaponsTypeIsIn; + /// + /// the cultures who have this item in their preferred weapons, it must be a weapon + /// + /// + /// the builder must link its assets so the cultures actually add it + /// + public IEnumerable CultureTraitsThisWeaponIsIn; + /// + /// the amount of coins a city has to spend to craft this item + /// + public int CoinCost { get { return Asset.cost_coins_resources; } set { Asset.cost_coins_resources = value; } } + /// + /// the amount of coins a city has to spend to craft/repair this time + /// + public int GoldCost { get { return Asset.cost_gold; } set { Asset.cost_gold = value; } } + /// + /// The ID of the first material and its amount a city needs to craft this item, and also the Minimum amount required + /// + public ValueTuple Resource1 { get { return new(Asset.cost_resource_id_1, Asset.cost_resource_1, Asset.minimum_city_storage_resource_1); } set { Asset.cost_resource_1 = value.Item2; Asset.minimum_city_storage_resource_1 = value.Item3; Asset.cost_resource_id_1 = value.Item1; } } + /// + /// The ID of the secound material and its amount a city needs to craft this item + /// + public ValueTuple Resource2 { get { return new(Asset.cost_resource_id_2, Asset.cost_resource_2); } set { Asset.cost_resource_id_2 = value.Item1; Asset.cost_resource_2 = value.Item2; } } + /// + /// the durability of the item, by default its 100 + /// + public int Durability { get { return Asset.durability; } set { Asset.durability = value; } } + /// + /// if true, it must also have a SubType + /// + public bool CanBeCraftedByCities { get { return Asset.is_pool_weapon; } set { Asset.is_pool_weapon = value; } } + /// + /// the modifiers in this item's list, when it is created all of them are applied + /// + public IEnumerable ItemModifiers { get { return Asset.item_modifier_ids; } set { Asset.item_modifier_ids = value.ToArray(); } } + /// + /// the material of the item, doesnt change any properties, just for displaying + /// + public string Material { get { return Asset.material; } set { Asset.material = value; } } + /// + /// only used for SFX, if a weapon is metallic and a actor hits someone with it, it produces noise + /// + public bool Metallic { get { return Asset.metallic; } set { Asset.metallic = value; } } + /// + /// not used by the game at the moment! + /// + public int Rate { get { return Asset.pool_rate; } set { Asset.pool_rate = value; } } + } +} \ No newline at end of file diff --git a/utils/Builders/ItemModifierBuilder.cs b/utils/Builders/ItemModifierBuilder.cs new file mode 100644 index 0000000..a9df1a3 --- /dev/null +++ b/utils/Builders/ItemModifierBuilder.cs @@ -0,0 +1,92 @@ +using NeoModLoader.General; + +namespace NeoModLoader.utils.Builders +{ + /// + /// A Builder which creates item modifiers! + /// + public sealed class ItemModifierBuilder : AugmentationAssetBuilder + { + /// + public ItemModifierBuilder(string FilePath, bool LoadImmediately) :base(FilePath, LoadImmediately) { } + /// + /// A Modifier Builder + /// + public ItemModifierBuilder(string ID, int Tier = 1) : base(ID + Tier, ID+1) { Asset.mod_rank = Tier; Asset.mod_type = ID; } + void LinkWithLibrary() + { + for (int i = 0; i < Asset.rarity; i++) + { + if (Asset.pool.Contains("weapon")) + { + Library.pools["weapon"].Add(Asset); + } + if (Asset.pool.Contains("armor")) + { + Library.pools["armor"].Add(Asset); + } + if (Asset.pool.Contains("accessory")) + { + Library.pools["accessory"].Add(Asset); + } + } + } + /// + public override void Build(bool LinkWithOtherAssets) + { + Build(true, LinkWithOtherAssets); + } + /// + /// Builds the modifier + /// + public void Build(bool Localize = true, bool LinkWithOtherAssets = false) + { + LinkWithLibrary(); + if (Localize) + { + LM.AddToCurrentLocale(Asset.getLocaleID(), Asset.getLocaleID()); + } + base.Build(LinkWithOtherAssets); + } + /// + /// The Displayed Rarity of the Asset + /// + public Rarity Rarity { get { return Asset.quality; } set { Asset.quality = value; } } + /// + /// if true, the player can give this modifier to items + /// + public bool CanModifiersBeGiven { get { return Asset.mod_can_be_given; } set { Asset.mod_can_be_given = value; } } + /// + /// the pools of items (like armor, accessory, etc) this mod is in, any item pools with this added can have it added to an item created + /// + public IEnumerable ItemPools + { + set + { + foreach (string PoolID in value) + { + if (Asset.pool.Length > 0) + { + Asset.pool += "," + PoolID; + } + else + { + Asset.pool = PoolID; + } + } + } + get + { + return Asset.pool.Split(','); + } + } + /// + /// how common this modifier is, in the pools it is in. + /// + public int PoolAmount { get { return Asset.rarity; } set { Asset.rarity = value; } } + /// + /// The ID of the Name, doesnt have to be set + /// + public string NameID { get { return Asset.translation_key; } set { Asset.translation_key = value; } } + } +} diff --git a/utils/Builders/MasterBuilder.cs b/utils/Builders/MasterBuilder.cs new file mode 100644 index 0000000..94eb3ed --- /dev/null +++ b/utils/Builders/MasterBuilder.cs @@ -0,0 +1,43 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// A manager for your builders, meant so you can link all of your assets together + /// + public sealed class MasterBuilder + { + readonly List Builders = new(); + /// + /// Adds a builder + /// + public B AddBuilder(B Builder) where B : Builder + { + Builders.Add(Builder); + return Builder; + } + /// + /// Adds's builders + /// + public void AddBuilders(IEnumerable Builders) + { + if(Builders == null) + { + return; + } + this.Builders.AddRange(Builders); + } + /// + /// Builds all of the builders and links their assets together + /// + public void BuildAll() + { + foreach (var builder in Builders) + { + builder.Build(false); + } + foreach (var builder in Builders) + { + builder.LinkAssets(); + } + } + } +} \ No newline at end of file diff --git a/utils/Builders/SubspeciesTraitBuilder.cs b/utils/Builders/SubspeciesTraitBuilder.cs new file mode 100644 index 0000000..7d430b2 --- /dev/null +++ b/utils/Builders/SubspeciesTraitBuilder.cs @@ -0,0 +1,197 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// used for when building a subspecies trait + /// + public enum SubSpeciesTrait + { + /// + /// A Normal Subspecies trait + /// + Trait, + /// + /// this subspecies trait is a phenotype, linking it with a phenotype asset + /// + PhenoType, + /// + /// this subspecies trait is an egg, it controls the eggs produced from this subspecies (their shape, colors, hatch time) + /// + Egg, + /// + /// this trait is a skin mutation + /// + SkinMutation + } + /// + /// a builder which creates subspecies traits + /// + public sealed class SubspeciesTraitBuilder : BaseTraitBuilder + { + static string TraitToDerive(SubSpeciesTrait trait) => trait switch + { + SubSpeciesTrait.Trait => null, + SubSpeciesTrait.Egg => SubspeciesTraitLibrary.TEMPLATE_EGG, + SubSpeciesTrait.SkinMutation => SubspeciesTraitLibrary.TEMPLATE_SKIN_MUTATION, + _ => null + }; + /// + /// creates a subspecies trait (type egg) with a afterhatchfromeggaction + /// + /// + /// the afterhatchfromeggaction is performed when actors from this subspecies hatch from an egg + /// + public SubspeciesTraitBuilder(string ID, AfterHatchFromEggAction afterHatchFromEggAction) : this(ID, SubSpeciesTrait.Egg) + { + Asset.after_hatch_from_egg_action = afterHatchFromEggAction; + } + /// + /// creates a subspecies trait (type skin mutation), the overridepath species the path to the sprites which overrides the normal sprites + /// + /// + /// an example would be actors/species/mutations/mutation_skin_light_orb + /// any actors in this subspecies who are babys will not render their head if false + public SubspeciesTraitBuilder(string ID, string OverridePath, bool RenderChildHeads) : this(ID, SubSpeciesTrait.SkinMutation) + { + Asset.render_heads_for_children = RenderChildHeads; + Asset.sprite_path = OverridePath; + } + /// + /// builds an asset depending on the Type + /// + public SubspeciesTraitBuilder(string ID, SubSpeciesTrait Type) : base(ID, TraitToDerive(Type)) { + if (Type == SubSpeciesTrait.PhenoType) + { + UsesSpecialIconLogic = true; + PathIcon = "ui/Icons/iconPhenotype"; + Asset.id = "phenotype_skin" + "_" + ID; + Asset.phenotype_skin = true; + Asset.id_phenotype = ID; + Asset.group_id = "phenotypes"; + NameID = "subspecies_trait_phenotype"; + Description1ID = ("subspecies_trait_phenotype_info"); + Asset.spawn_random_trait_allowed = false; + } + else if (Type == SubSpeciesTrait.Egg) + { + Asset.id_egg = Asset.id; + Asset.sprite_path = "eggs/" + Asset.id_egg; + } + } + void LinkWithLibrary() + { + if (Asset.spawn_random_trait_allowed) + { + Library._pot_allowed_to_be_given_randomly.Add(Asset); + } + if (Asset.in_mutation_pot_add) + { + int tRate = Asset.rarity.GetRate(); + Library._pot_mutation_traits_add.AddTimes(tRate, Asset); + } + if (Asset.in_mutation_pot_remove) + { + int tRate2 = Asset.rarity.GetRate(); + Library._pot_mutation_traits_remove.AddTimes(tRate2, Asset); + } + if (Asset.phenotype_egg && Asset.after_hatch_from_egg_action != null) + { + Asset.has_after_hatch_from_egg_action = true; + } + } + /// + public override void Build(bool SetRarityAutomatically = false, bool AutoLocalize = true, bool LinkWithOtherAssets = false) + { + base.Build(SetRarityAutomatically, AutoLocalize, LinkWithOtherAssets); + Library.loadSpritesPaths(Asset); + LinkWithLibrary(); + } + /// + public override void LinkAssets() + { + if (Asset.id_phenotype != null) + { + PhenotypeAsset Phenotype = AssetManager.phenotype_library.get(Asset.id_phenotype); + Phenotype.subspecies_trait_id = Asset.id; + Asset.priority = Phenotype.priority; + } + if (Asset.is_mutation_skin) + { + OpposeAllOtherTraits = new[] { (SubspeciesTrait trait) => trait.is_mutation_skin }; + } + if (Asset.phenotype_skin) + { + OpposeAllOtherTraits = new[] { (SubspeciesTrait trait) => trait.phenotype_skin }; + } + if (Asset.phenotype_egg) + { + OpposeAllOtherTraits = new[] { (SubspeciesTrait trait) => trait.phenotype_egg }; + } + base.LinkAssets(); + + } + /// + public SubspeciesTraitBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public SubspeciesTraitBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + /// + /// Used for phenotypes if true the icon will not use the path icon, instead it will use the phenotype color attached to the subspecies trait + /// + public bool UsesSpecialIconLogic { get { return Asset.special_icon_logic; } set { Asset.special_icon_logic = value; } } + /// + /// the file names used for actors in this subspecies's swim animations, along with its speed (default 10) + /// + /// + /// use ActorAnimationSequences as it already has lists of these for you + /// + public ValueTuple SwimAnimation { get { return new(Asset.animation_swim, Asset.animation_swim_speed); } set { Asset.animation_swim = value.Item1; Asset.animation_swim_speed = value.Item2; } } + /// + /// the file names used for actors in this subspecies's walk animations, along with its speed (default 10) + /// + /// + /// use ActorAnimationSequences as it already has lists of these for you + /// + public ValueTuple WalkAnimation { get { return new(Asset.animation_walk, Asset.animation_walk_speed); } set { Asset.animation_walk = value.Item1; Asset.animation_walk_speed = value.Item2; } } + /// + /// the file names used for actors in this subspecies's idle animations, along with its speed (default 10) + /// + /// + /// use ActorAnimationSequences as it already has lists of these for you + /// + public ValueTuple IdleAnimation { get { return new(Asset.animation_idle, Asset.animation_idle_speed); } set { Asset.animation_idle = value.Item1; Asset.animation_idle_speed = value.Item2; } } + /// + /// if true, when subspecies's mutate, they can get this triat, the less its rarity the higher the chance + /// + public bool CanBeAddedFromMutations { get { return Asset.in_mutation_pot_add; } set { Asset.in_mutation_pot_add = value; } } + /// + /// if true, when subspecies's mutate, they can get this triat removed, the less its rarity the higher the chance + /// + public bool CanbeRemovedFromMutations { get { return Asset.in_mutation_pot_remove; } set { Asset.in_mutation_pot_remove = value; } } + /// + /// used if the trait is a skin mutation, this chooses the female skins used + /// + /// names used are like "female_1" for example + public List FemaleSkins { get { return Asset.skin_citizen_female; } set { Asset.skin_citizen_female = value; } } + /// + /// used if the trait is a skin mutation, this chooses the male skins used + /// + /// names used are like "male_1" for example + public List MaleSkins { get { return Asset.skin_citizen_male; } set { Asset.skin_citizen_male = value; } } + /// + /// used if the trait is a skin mutation, this chooses the warrior skins used + /// + /// names used are like "warrior_1" for example + public List WarriorSkins { get { return Asset.skin_warrior; } set { Asset.skin_warrior = value; } } + /// + /// if true, any resources whose diet list has a tag apart of this assets meta tags, any subspecies's with this trait will consume that resource + /// + public bool DietRelated { get { return Asset.is_diet_related; } set { Asset.is_diet_related = value; } } + /// + /// when a zombie subspecies is created they will remove remove any traits with this set to true + /// + public bool RemoveIfZombieSubSpecies { get { return Asset.remove_for_zombies; } set { Asset.remove_for_zombies = value; } } + /// + /// if true, any actors in this subspecies will not rotate when unconscious + /// + public bool DontRotateWhenUnconscious { get { return Asset.prevent_unconscious_rotation; } set { Asset.prevent_unconscious_rotation = value; } } + } +} \ No newline at end of file diff --git a/utils/Builders/UnlockableAssetBuilder.cs b/utils/Builders/UnlockableAssetBuilder.cs new file mode 100644 index 0000000..ebcf978 --- /dev/null +++ b/utils/Builders/UnlockableAssetBuilder.cs @@ -0,0 +1,80 @@ +namespace NeoModLoader.utils.Builders +{ + /// + /// A builder for building unlockable assets + /// + public class UnlockableAssetBuilder : AssetBuilder where A : BaseUnlockableAsset, new() where AL : BaseLibraryWithUnlockables + { + /// + public UnlockableAssetBuilder(string ID) : base(ID) { BaseStats = new BaseStats(); } + /// + public UnlockableAssetBuilder(string FilePath, bool LoadImmediately) : base(FilePath, LoadImmediately) { } + /// + public UnlockableAssetBuilder(string ID, string CopyFrom) : base(ID, CopyFrom) { } + /// + /// if true, this asset should be discovered by the player to be used + /// + public bool NeedsToBeExplored { set { Asset.needs_to_be_explored = value; } get { return Asset.needs_to_be_explored; } } + void LinkWithAchievment() + { + if (Asset.unlocked_with_achievement) + { + Achievement pAchievement = AssetManager.achievements.get(Asset.achievement_id); + if (pAchievement.unlock_assets == null) + { + pAchievement.unlock_assets = new List(); + pAchievement.unlocks_something = true; + } + pAchievement.unlock_assets.Add(Asset); + } + } + /// + public override void LinkAssets() + { + LinkWithAchievment(); + } + /// + /// the asset unlocked if this achievment has been unlocked + /// + public string AchievmentToUnlockThis { set { Asset.unlocked_with_achievement = value != null; Asset.achievement_id = value; } + get { return Asset.achievement_id; } + } + /// + /// makes the asset available by default + /// + public void UnlockByDefault() + { + Asset.unlocked_with_achievement = false; + Asset.achievement_id = null; + Asset.needs_to_be_explored = false; + } + /// + /// the stats of this asset + /// + /// + /// an example would be Stats = new(){ {"health", 2}, {"armor", 2} }; + /// + public Dictionary Stats + { + set + { + foreach (KeyValuePair valueTuple in value) + { + BaseStats[valueTuple.Key] = valueTuple.Value; + } + } + } + /// + /// The Stats that are applied to the thing that has this asset, like a actor or a Clan + /// + public BaseStats BaseStats { get { return Asset.base_stats; } set { Asset.base_stats = value; } } + /// + /// the path to the icon, starting from the root directory (GameResources) + /// + public string PathIcon { get { return Asset.path_icon; } set { Asset.path_icon = value;} } + /// + /// if true, the Knowledge Window will display this asset + /// + public bool ShowInKnowledgeWindow { get { return Asset.show_in_knowledge_window; } set { Asset.show_in_knowledge_window = value; } } + } +} diff --git a/utils/DelegateExtentions.cs b/utils/DelegateExtentions.cs new file mode 100644 index 0000000..7fe2b0e --- /dev/null +++ b/utils/DelegateExtentions.cs @@ -0,0 +1,88 @@ +using System.Reflection; +namespace NeoModLoader.utils +{ + /// + /// extentions for delegates, meant for serializing them + /// + public static class DelegateExtentions + { + /// + /// gets all of the parameters from a delegate type + /// + public static Type[] GetDelegateParameters(this Type delegateType) + { + MethodInfo method = delegateType.GetMethod("Invoke"); + ParameterInfo[] info = method.GetParameters(); + Type[] types = new Type[info.Length]; + for (int i = 0; i < info.Length; i++) { + types[i] = info[i].ParameterType; + } + return types; + } + /// + /// converts a string to a single delegate + /// + /// + /// An Example would be "Randy:randomInt+Unity.Mathematics.Random:NextInt" + /// + /// a list of objects split by '+' with each object being a class:methodname + /// the delegate type, like worldaction + /// if String is null + public static D AsDelegate(this string String) where D : Delegate + { + return (D)String.AsDelegate(typeof(D)); + } + /// + /// converts a string to a single delegate + /// + /// + /// An Example would be "Randy:randomInt+Unity.Mathematics.Random:NextInt" + /// + /// a list of objects split by '+' with each object being a class:methodname + /// if null, the AsString function must have includetype set to true + /// if String is null + /// if the delegatetype is null and the string doesnt include the type + public static Delegate AsDelegate(this string String, Type DelegateType = null) + { + if(String?.Contains("&") ?? throw new ArgumentNullException("The String is null!")) + { + string[] TypeAndDelegate = String.Split('&'); + DelegateType ??= Type.GetType(TypeAndDelegate[0]); + String = TypeAndDelegate[1]; + } + string[] DelegateIDS = String.Split('+'); + Delegate[] Delegates = new Delegate[DelegateIDS.Length]; + Type[] Parameters = DelegateType?.GetDelegateParameters() ?? throw new ArgumentException("The String Does Not Contain the delegate type!"); + for (int i =0; i < DelegateIDS.Length; i++) + { + string[] MethodPath = DelegateIDS[i].Split(':'); + var m = Type.GetType(MethodPath[0]).GetMethod(MethodPath[1], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, Parameters, null); + Delegates[i] = m.CreateDelegate(DelegateType); + } + return Delegate.Combine(Delegates); + } + /// + /// converts a delegate to a string which is a list of method paths split by '+' with each method path being a class:methodname + /// + /// + /// An Example would be delegate Randy.randomInt and Unity.Mathematics.Random.NextInt would become "Randy:randomInt+Unity.Mathematics.Random:NextInt" + /// + /// if Delegate is null + public static string AsString(this Delegate pDelegate, bool IncludeType = false) + { + Delegate[] Delegates = pDelegate?.GetInvocationList() ?? throw new ArgumentNullException("The Delegate is null!"); + string[] MethodPaths = new string[Delegates.Length]; + for (int i = 0; i < Delegates.Length; i++) + { + MethodInfo Method = Delegates[i].Method; + MethodPaths[i] = $"{Method.DeclaringType.AssemblyQualifiedName}:{Method.Name}"; + } + string String = string.Join("+", MethodPaths); + if (IncludeType) + { + String = string.Join("&", pDelegate.GetType().AssemblyQualifiedName, String); + } + return String; + } + } +} \ No newline at end of file diff --git a/utils/ResourcesPatch.cs b/utils/ResourcesPatch.cs index fd4933b..541b38b 100644 --- a/utils/ResourcesPatch.cs +++ b/utils/ResourcesPatch.cs @@ -1,9 +1,8 @@ using System.Globalization; -using FMOD; -using FMODUnity; using HarmonyLib; using NeoModLoader.api.exceptions; using NeoModLoader.services; +using NeoModLoader.utils.Builders; using Newtonsoft.Json; using UnityEngine; using UnityEngine.U2D; @@ -88,16 +87,33 @@ public static Object[] LoadResourceFile(ref string path, ref string pLowerPath) { if (pLowerPath.EndsWith(".png") || pLowerPath.EndsWith(".jpg") || pLowerPath.EndsWith(".jpeg")) return SpriteLoadUtils.LoadSprites(path); - if (pLowerPath.EndsWith(".wav")) - { - LoadWavFile(path); - } - return new Object[] { LoadTextAsset(path) }; } + static Builder LoadAsset(string Path, string Extention) => Extention switch + { + //"actorasset" => new ActorAssetBuilder(Path, false), + ".actortraitasset" => new ActorTraitBuilder(Path, false), + ".subspeciestraitasset" => new SubspeciesTraitBuilder(Path, false), + ".itemasset" => new ItemBuilder(Path, false), + ".itemmodifierasset" => new ItemModifierBuilder(Path, false), + ".clantraitasset" => new ClanTraitBuilder(Path, false), + ".culturetraitasset" => new CultureTraitBuilder(Path, false), + ".actortraitgroupasset" => new GroupAssetBuilder(Path, false), + ".achievementgroupasset" => new GroupAssetBuilder(Path, false), + ".clantraitgroupasset" => new GroupAssetBuilder(Path, false), + ".culturetraitgroupasset" => new GroupAssetBuilder(Path, false), + ".itemgroupasset" => new GroupAssetBuilder(Path, false), + ".kingdomtraitgroupasset" => new GroupAssetBuilder(Path, false), + ".languagetraitgroupasset" => new GroupAssetBuilder(Path, false), + ".plotcategoryasset" => new GroupAssetBuilder(Path, false), + ".religiontraitgroupasset" => new GroupAssetBuilder(Path, false), + ".subspeciestraitgroupasset" => new GroupAssetBuilder(Path, false), + ".worldlawgroupasset" => new GroupAssetBuilder(Path, false), + _ => throw new NotSupportedException($"the asset {Extention} has not been supported yet!"), + }; /// /// doesnt return anything, simply adds the wav to the wav library that contains all the custom sounds /// @@ -118,30 +134,10 @@ private static void LoadWavFile(string path) } catch (Exception) { - container = new WavContainer(path, true, 50f); + container = new WavContainer(path, SoundMode.Stereo3D, 50f); } - CustomAudioManager.AudioWavLibrary.Add(Name, container); } - [HarmonyPrefix] - [HarmonyPatch(typeof(MusicBox), nameof(MusicBox.playSound), typeof(string), typeof(float), typeof(float), - typeof(bool), typeof(bool))] - static bool LoadCustomSound(string pSoundPath, float pX, float pY, bool pGameViewOnly) - { - if (!MusicBox.sounds_on) - { - return false; - } - - if (pGameViewOnly && World.world.quality_changer.isLowRes()) - { - return false; - } - if (!CustomAudioManager.AudioWavLibrary.ContainsKey(pSoundPath)) return true; - - CustomAudioManager.LoadCustomSound(pX, pY, pSoundPath); - return false; - } private static TextAsset LoadTextAsset(string path) { @@ -150,15 +146,20 @@ private static TextAsset LoadTextAsset(string path) return textAsset; } - internal static void LoadResourceFromFolder(string pFolder) + internal static void LoadResourceFromFolder(string pFolder, out List Builders) { + Builders = null; if (!Directory.Exists(pFolder)) return; - var files = SystemUtils.SearchFileRecursive(pFolder, filename => !filename.StartsWith("."), dirname => !dirname.StartsWith(".")); foreach (var file in files) { - tree.AddFromFile(file.Replace(pFolder, "").Replace('\\', '/').Substring(1), file); + tree.AddFromFile(file.Replace(pFolder, "").Replace('\\', '/').Substring(1), file, out Builder builder); + if(builder != null) + { + Builders ??= new List(); + Builders.Add(builder); + } } } @@ -360,11 +361,22 @@ public void Add(string path, Object obj) /// /// Path to resource in tree /// Path to resource in actual filesystem - public void AddFromFile(string path, string absPath) + /// if your folder contains asset files (.actorasset, .itemasset) pass a master builder to load these assets, then make it buildall + public void AddFromFile(string path, string absPath, out Builder Builder) { + Builder = null; string lower_path = path.ToLower(); if (lower_path.EndsWith(".meta") || lower_path.EndsWith("sprites.json")) return; - + if (lower_path.EndsWith(".wav")) + { + LoadWavFile(absPath); + return; + } + if (lower_path.EndsWith("asset")) + { + Builder = LoadAsset(absPath, Path.GetExtension(lower_path)); + return; + } string parent_path = Path.GetDirectoryName(lower_path); Object[] objs; try diff --git a/utils/SerializedAssets/SerializableAsset.cs b/utils/SerializedAssets/SerializableAsset.cs new file mode 100644 index 0000000..1656d6a --- /dev/null +++ b/utils/SerializedAssets/SerializableAsset.cs @@ -0,0 +1,96 @@ +using Newtonsoft.Json.Linq; +using System.Reflection; + +namespace NeoModLoader.utils.SerializedAssets +{ + /// + /// Because delegates like worldaction cannot be serialized, this is used so you can serialize them + /// + [Serializable] + public class SerializableAsset where A : Asset, new() + { + /// + /// the variables of the asset + /// + public Dictionary Variables = new(); + /// + /// the delegates of the asset + /// + /// + /// the way it stores delegates is that it stores their name, the path to their class, and then searches the assembly for a matching delegate + /// + public Dictionary Delegates = new(); + /// + /// takes delegates and variables from an asset and takes them to a serializable asset + /// + public static void Serialize(A Asset, SerializableAsset asset) + { + foreach (FieldInfo field in typeof(A).GetFields()) + { + object Value = field.GetValue(Asset); + if (Value is Delegate value) + { + asset.Delegates.Add(field.Name, value.AsString(false)); + } + else + { + asset.Variables.Add(field.Name, Value); + } + } + } + /// + /// Converts the augmentation asset to a serializable version + /// + public static SerializableAsset FromAsset(A Asset) + { + SerializableAsset asset = new(); + Serialize(Asset, asset); + return asset; + } + /// + /// takes delegates and variables from a serializable asset and takes them to a asset + /// + public static void Deserialize(SerializableAsset Asset, A asset) + { + static object GetRealValueOfObject(object Value, Type Type) + { + if (Type == typeof(int)) + { + return Convert.ToInt32(Value); + } + else if (Type == typeof(float)) + { + return Convert.ToSingle(Value); + } + else if (Value is JObject JObject) + { + return JObject.ToObject(Type); + } + return Value; + } + foreach (FieldInfo field in typeof(A).GetFields()) + { + if (typeof(Delegate).IsAssignableFrom(field.FieldType)) + { + if (Asset.Delegates.TryGetValue(field.Name, out string Delegate)) + { + field.SetValue(asset, Delegate.AsDelegate(field.FieldType)); + } + } + else if (Asset.Variables.TryGetValue(field.Name, out object Value)) + { + field.SetValue(asset, GetRealValueOfObject(Value, field.FieldType)); + } + } + } + /// + /// converts the serializable version to its asset + /// + public static A ToAsset(SerializableAsset Asset) + { + A asset = new(); + Deserialize(Asset, asset); + return asset; + } + } +} \ No newline at end of file diff --git a/utils/SerializedAssets/SerializedActorTrait.cs b/utils/SerializedAssets/SerializedActorTrait.cs new file mode 100644 index 0000000..7ba39c0 --- /dev/null +++ b/utils/SerializedAssets/SerializedActorTrait.cs @@ -0,0 +1,41 @@ +using NeoModLoader.utils.Builders; + +namespace NeoModLoader.utils.SerializedAssets +{ + /// + /// a serializable actor trait, including its custom additional base stats method + /// + public class SerializedActorTrait : SerializableAsset + { + /// + /// the class and the method of the additionalbasestatsmethod + /// + public string AdditionalBaseStatsMethod; + /// + /// Converts the actor trait asset to a serializable version + /// + public static SerializedActorTrait FromAsset(ActorTrait Asset, GetAdditionalBaseStatsMethod Method = null) + { + SerializedActorTrait asset = new SerializedActorTrait(); + Serialize(Asset, asset); + if (Method != null) + { + asset.AdditionalBaseStatsMethod = Method.AsString(false); + } + return asset; + } + /// + /// Converts the serializable version to a actor trait asset + /// + public static ActorTrait ToAsset(SerializedActorTrait Asset) + { + ActorTrait asset = new(); + Deserialize(Asset, asset); + if (Asset.AdditionalBaseStatsMethod != null) + { + ActorTraitBuilder.AdditionalBaseStatMethods.TryAdd(asset.id, Asset.AdditionalBaseStatsMethod.AsDelegate()); + } + return asset; + } + } +} diff --git a/utils/SerializedAssets/SerializedItemAsset.cs b/utils/SerializedAssets/SerializedItemAsset.cs new file mode 100644 index 0000000..1d4e16b --- /dev/null +++ b/utils/SerializedAssets/SerializedItemAsset.cs @@ -0,0 +1,37 @@ +namespace NeoModLoader.utils.SerializedAssets +{ + /// + /// a serializable item asset, including culture traits which have it and its type + /// + public class SerializedItemAsset : SerializableAsset + { + internal string[] CultureTraitsThisItemIsIn; + internal string[] CultureTraitsThisItemsTypeIsIn; + /// + /// Converts the item asset to a serializable version + /// + public static SerializedItemAsset FromAsset(ItemAsset Asset, IEnumerable cultureTraitsItem = null, IEnumerable cultureTraitsType = null) + { + SerializedItemAsset asset = new(); + Serialize(Asset, asset); + if (cultureTraitsItem != null) + { + asset.CultureTraitsThisItemIsIn = cultureTraitsItem.ToArray(); + } + if (cultureTraitsType != null) + { + asset.CultureTraitsThisItemsTypeIsIn = cultureTraitsType.ToArray(); + } + return asset; + } + /// + /// Converts the serializable version to a actor trait asset + /// + public static ItemAsset ToAsset(SerializedItemAsset Asset) + { + ItemAsset asset = new(); + Deserialize(Asset, asset); + return asset; + } + } +} \ No newline at end of file diff --git a/utils/SoundUtils.cs b/utils/SoundUtils.cs index 111f57c..7143f3c 100644 --- a/utils/SoundUtils.cs +++ b/utils/SoundUtils.cs @@ -3,44 +3,100 @@ using HarmonyLib; using NeoModLoader.services; using Newtonsoft.Json; -using System.Collections.ObjectModel; using UnityEngine; namespace NeoModLoader.utils; +/// +/// The Type used for which sound group this sound goes into, UI, Music, and SFX which control its volume +/// public enum SoundType { + /// + /// Apart of the Music Group + /// Music, + /// + /// Apart of the Sound Group (SFX) + /// Sound, + /// + /// Apart of the UI Group + /// UI } +/// +/// The Mode which controls if and how the volume changes depending on your distance to it +/// +public enum SoundMode +{ + /// + /// 2D sound, volume doesnt change + /// + Basic, + /// + /// 3D sound (volume changes depending on distance) which uses 2 audio channels, is quiet + /// + Stereo3D, + /// + /// 3D sound which uses 1 channel, can be loud + /// + Mono3D +} internal struct WavContainer { [JsonIgnore]public string Path; - [JsonProperty("3D")] public bool _3D; + public SoundMode Mode; public float Volume; public SoundType Type; public int LoopCount; public bool Ramp; - public WavContainer(string Path, bool _3D, float Volume, int LoopCount = 0, bool Ramp = false, SoundType Type = SoundType.Sound) + public WavContainer(string Path, SoundMode Mode, float Volume, int LoopCount = 0, bool Ramp = false, SoundType Type = SoundType.Sound) { this.Ramp = Ramp; this.Path = Path; - this._3D = _3D; + this.Mode = Mode; this.Volume = Volume; this.Type = Type; this.LoopCount = LoopCount; } } +/// +/// A container to manage the Sound +/// public struct ChannelContainer { + /// + /// A FMOD Audio Channel which plays the sound + /// public Channel Channel { get; internal set; } + /// + /// to be used for Mono3D + /// + /// + /// X and Y represent position, Z represents volume + /// + public Vector3 PosAndVolume = default; + /// + /// The Transform or gameobject which this Sound is attached two. sounds whose mode is BASIC must not use this + /// public Transform AttachedTo; - internal ChannelContainer(Channel channel, Transform attachedTo = null) + internal ChannelContainer(Channel channel, Transform attachedTo = null, Vector3 PosAndVolume = default) { Channel = channel; + this.PosAndVolume = PosAndVolume; AttachedTo = attachedTo; } + /// + /// returns true if the channel has stopped playing or something wrong happened + /// + public readonly bool Finushed + { + get { return Channel.isPlaying(out bool IsPlaying) != RESULT.OK || !IsPlaying; } + } } +/// +/// A Manager for managing your custom sounds! +/// public class CustomAudioManager { static FMOD.System fmodSystem; @@ -54,29 +110,83 @@ static void Update() SFXGroup.setVolume(GetVolume(SoundType.Sound)); MusicGroup.setVolume(GetVolume(SoundType.Music)); UIGroup.setVolume(GetVolume(SoundType.UI)); - for (int i =0; i < channels.Count; i++) + for (int i =0; i < Channels.Count; i++) { - ChannelContainer C = channels[i]; + ChannelContainer C = Channels[i]; if (!UpdateChannel(C)) { - channels.Remove(C); + Channels.Remove(C); i--; } } } + [HarmonyPrefix] + [HarmonyPatch(typeof(MusicBox), nameof(MusicBox.playSound), typeof(string), typeof(float), typeof(float), + typeof(bool), typeof(bool))] + [HarmonyPriority(Priority.Last)] + static bool PlaySoundPatch(string pSoundPath, float pX, float pY, bool pGameViewOnly) + { + if (!MusicBox.sounds_on) + { + return true; + } + if (pGameViewOnly && World.world.quality_changer.isLowRes()) + { + return true; + } + if (!AudioWavLibrary.ContainsKey(pSoundPath)) return true; + LoadCustomSound(pSoundPath, pX, pY); + return false; + } + [HarmonyPrefix] + [HarmonyPatch(typeof(MusicBox), nameof(MusicBox.playDrawingSound))] + [HarmonyPriority(Priority.Last)] + static bool PlayDrawingSoundPatch(string pSoundPath, float pX, float pY) + { + if (!MusicBox.sounds_on) + { + return true; + } + if (!AudioWavLibrary.ContainsKey(pSoundPath)) return true; + LoadDrawingSound(pSoundPath, pX, pY); + return false; + } + /// + /// plays a sound at a location unless another sound with the same path is playing, then the other sound is set to that position + /// + public static ChannelContainer LoadDrawingSound(string pSoundPath, float pX, float pY) + { + if(DrawingSounds.TryGetValue(pSoundPath, out ChannelContainer container) && !container.Finushed) + { + SetChannelPosition(container, pX, pY); + } + else + { + DrawingSounds.Remove(pSoundPath); + container = LoadCustomSound(pSoundPath, pX, pY); + DrawingSounds.Add(pSoundPath, container); + } + return container; + } /// /// Loads a custom sound from the wav library /// - /// the ID of the wav file, aka its file name + /// the X position + /// the Y position + /// the ID of the wav file, aka its file name /// The transform to attach the sound to - /// The ID of the channel that the sound is playing in, -1 if failed - public static int LoadCustomSound(float pX, float pY, string pSoundPath, Transform AttachedTo = null) + /// The ID of the channel that the sound is playing in, default if failed + public static ChannelContainer LoadCustomSound(string WAVName, float pX, float pY, Transform AttachedTo = null) { - WavContainer WAV = AudioWavLibrary[pSoundPath]; - if (fmodSystem.createSound(WAV.Path, WAV._3D ? MODE.LOOP_NORMAL | MODE._3D : MODE.LOOP_NORMAL, out var sound) != RESULT.OK) + WavContainer WAV = AudioWavLibrary[WAVName]; + if(WAV.Mode == SoundMode.Basic) + { + AttachedTo = null; + } + if (fmodSystem.createSound(WAV.Path, WAV.Mode == SoundMode.Stereo3D ? MODE.LOOP_NORMAL | MODE._3D : MODE.LOOP_NORMAL, out var sound) != RESULT.OK) { - UnityEngine.Debug.Log($"Unable to play sound {pSoundPath}!"); - return -1; + LogService.LogError($"Unable to play sound {WAVName}!"); + return default; } sound.setLoopCount(WAV.LoopCount); Channel channel = default; @@ -87,9 +197,11 @@ public static int LoadCustomSound(float pX, float pY, string pSoundPath, Transfo } channel.setVolumeRamp(WAV.Ramp); channel.setVolume(WAV.Volume/100); - AddChannel(channel, AttachedTo); - SetChannelPosition(channel, pX, pY); - return channels.Count-1; + AddChannel(channel, AttachedTo, WAV.Mode == SoundMode.Mono3D ? new Vector3(pX, pY, WAV.Volume) : default); + if (WAV.Mode == SoundMode.Stereo3D) { + SetChannelPosition(channel, pX, pY); + } + return Channels[Channels.Count-1]; } internal static void Initialize() { @@ -111,55 +223,84 @@ internal static void Initialize() LogService.LogError("Failed to create UIGroup!"); } } - internal static void AddChannel(Channel channel, Transform AttachedTo = null) + internal static void AddChannel(Channel channel, Transform AttachedTo = null, Vector3 PosAndVolume = default) { - ChannelContainer Container = new ChannelContainer(channel, AttachedTo); - channels.Add(Container); + Channels.Add(new(channel, AttachedTo, PosAndVolume)); } /// /// Allows the Modder to modify the data of the wav file at runtime /// - public static void ModifyWavData(string ID, float Volume, bool _3D, int LoopCount = 0, bool Ramp = false, SoundType Type = SoundType.Sound) + public static void ModifyWavData(string ID, float Volume, SoundMode Mode, int LoopCount = 0, bool Ramp = false, SoundType Type = SoundType.Sound) { if (!AudioWavLibrary.ContainsKey(ID)) { return; } - AudioWavLibrary[ID] = new WavContainer(AudioWavLibrary[ID].Path, _3D, Volume, LoopCount, Ramp, Type); + AudioWavLibrary[ID] = new WavContainer(AudioWavLibrary[ID].Path, Mode, Volume, LoopCount, Ramp, Type); } static bool UpdateChannel(ChannelContainer channel) { - channel.Channel.isPlaying(out bool isPlaying); - if (!isPlaying) + if (channel.Finushed) { return false; } if (channel.AttachedTo != null) { - SetChannelPosition(channel.Channel, channel.AttachedTo.position.x, channel.AttachedTo.position.y); + SetChannelPosition(channel, channel.AttachedTo.position.x, channel.AttachedTo.position.y); + } + if(channel.PosAndVolume != default) + { + UpdateMonoVolume(channel); } return true; } + static void UpdateMonoVolume(ChannelContainer Channel) + { + Vector3 CameraPos = Camera.main.transform.position; + float Distance = Vector3.Distance(new Vector3(CameraPos.x, CameraPos.y, Camera.main.orthographicSize), new Vector2(Channel.PosAndVolume.x, Channel.PosAndVolume.y)); + Channel.Channel.setVolume(Mathf.Clamp01(Channel.PosAndVolume.z/Distance)); + } + /// + /// Clears All Custom Sounds + /// [HarmonyPrefix] [HarmonyPatch(typeof(MapBox), nameof(MapBox.clearWorld))] public static void ClearAllCustomSounds() { - foreach (var channel in channels) + foreach (var channel in Channels) { channel.Channel.stop(); } - channels.Clear(); + Channels.Clear(); + } + /// + /// Sets the position of a channel, channel must be 3D + /// + public static void SetChannelPosition(ChannelContainer channel, float pX, float pY) + { + if (channel.PosAndVolume == default) + { + SetChannelPosition(channel.Channel, pX, pY); + } + else + { + channel.PosAndVolume.x = pX; + channel.PosAndVolume.y = pY; + } } + /// + /// Sets the position of a channel, channel must be Stereo-3D + /// public static void SetChannelPosition(Channel channel, float pX, float pY) { channel.get3DAttributes(out VECTOR Pos, out VECTOR vel); if (Pos.x != pX || Pos.y != pY) { - VECTOR pos = new VECTOR() { x = pX, y = pY, z = 0 }; + VECTOR pos = new() { x = pX, y = pY, z = 0 }; channel.set3DAttributes(ref pos, ref vel); } } - public static float GetVolume(SoundType soundType) + static float GetVolume(SoundType soundType) { float Volume = 1; if (soundType == SoundType.Music) @@ -177,7 +318,7 @@ public static float GetVolume(SoundType soundType) Volume *= PlayerConfig.getIntValue("volume_master_sound") / 100f; return Volume; } - internal static readonly Dictionary AudioWavLibrary = new Dictionary(); - static readonly List channels = new List(); - public static ReadOnlyCollection ChannelList { get { return channels.AsReadOnly(); } } -} + internal static readonly Dictionary AudioWavLibrary = new(); + static readonly List Channels = new(); + static readonly Dictionary DrawingSounds = new(); +} \ No newline at end of file