From 3d3f6d8c5a633e545820ff10257a1a19e87defb8 Mon Sep 17 00:00:00 2001 From: ReinMasamune Date: Fri, 29 Nov 2019 21:40:46 -0500 Subject: [PATCH 1/6] DirectorAPI --- R2API/DirectorAPIexternal.cs | 238 ++++++++++++++++++++++ R2API/DirectorAPIhelpers.cs | 373 ++++++++++++++++++++++++++++++++++ R2API/DirectorAPIinternal.cs | 379 +++++++++++++++++++++++++++++++++++ 3 files changed, 990 insertions(+) create mode 100644 R2API/DirectorAPIexternal.cs create mode 100644 R2API/DirectorAPIhelpers.cs create mode 100644 R2API/DirectorAPIinternal.cs diff --git a/R2API/DirectorAPIexternal.cs b/R2API/DirectorAPIexternal.cs new file mode 100644 index 00000000..16589373 --- /dev/null +++ b/R2API/DirectorAPIexternal.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using R2API.Utils; +using RoR2; +using UnityEngine; + +namespace R2API { + // ReSharper disable once InconsistentNaming + //[R2APISubmodule] + public static partial class DirectorAPI { + /// + /// Event used to edit stage settings. + /// + public static event Action stageSettingsActions; + /// + /// Event used to edit/add/remove the monsters spawned on a stage. + /// + public static event Action , StageInfo> monsterActions; + /// + /// Event used to edit/add/remove interactables spawned on a stage. + /// + public static event Action , StageInfo> interactableActions; + /// + /// Event used to edit/add/remove monster families on a stage. + /// + public static event Action, StageInfo> familyActions; + /// + /// If this is called then DirectorAPI will hook ClassicStageInfo.Awake and use the events to make changes + /// + + /// + /// The three categories for monsters. Support for custom categories will come later. + /// + public enum MonsterCategory { + /// + /// An invalid default value. Anything with this value is ignored when dealing with monsters. + /// + None = 0, + /// + /// Small enemies like Lemurians and Beetles. + /// + BasicMonsters = 1, + /// + /// Medium enemies like Golems and Beetle Guards. + /// + Minibosses = 2, + /// + /// Bosses like Vagrants and Titans. + /// + Champions = 3 + } + + /// + /// The categories for interactables. Support for custom categories will come later. + /// + public enum InteractableCategory { + /// + /// An invalid default value. Anything with this value is ignored when dealing with interactables. + /// + None = 0, + /// + /// Chests, such as basic chests, large chests, shops, equipment barrels, lunar pods, and category chests. NOT legendary chests or cloaked chests. + /// + Chests = 1, + /// + /// Barrels. + /// + Barrels = 2, + /// + /// Chance shrines, blood shrines, combat shrines, order shrines, mountain shrines, shrine of the woods. NOT shrine of gold. + /// + Shrines = 3, + /// + /// All types of drones such as TC-280, equipment drones, gunner drones, healing drones, and incinerator drones. NOT gunner turrets. + /// + Drones = 4, + /// + /// Gunner turrets only. + /// + Misc = 5, + /// + /// Legendary chests, cloaked chests, shrine of gold, and radio scanners. + /// + Rare = 6, + /// + /// All three tiers of printers. + /// + Duplicator = 7 + } + + /// + /// A flags enum for the vanilla stages. Custom stages are handled with a string in StageInfo. + /// + [Flags] + public enum Stage { + /// + /// When this is set to custom, check the string in StageInfo + /// + Custom = 1, + TitanicPlains = 2, + DistantRoost = 4, + WetlandAspect = 8, + AbandonedAqueduct = 16, + RallypointDelta = 32, + ScorchedAcres = 64, + AbyssalDepths = 128, + SirensCall = 256, + GildedCoast = 512, + MomentFractured = 1024, + Bazaar = 2048 + } + + public struct StageInfo { + /// + /// The current stage. If set to custom, check customStageName. + /// + public Stage stage; + /// + /// This is set to the name of the custom stage. Is left blank for vanilla stages. + /// + public String customStageName; + + /// + /// Returns true if the current stage matches any of the stages you specify. + /// To match a custom stage, include Stage.Custom in your stage input and specify names in customStageNames. + /// + /// The stages to match with + /// Names of the custom stages to match. Leave blank to match all custom stages + /// + public Boolean CheckStage( Stage stage, params String[] customStageNames ) { + if( !stage.HasFlag( this.stage ) ) return false; + if( this.stage == Stage.Custom && customStageNames.Length != 0 && !customStageNames.Contains( this.customStageName ) ) return false; + return true; + } + } + + /// + /// A class passed to everything subscribed to stageSettingsActions that contains various settings for a stage. + /// All mods will be working off the same settings, so operators like *=,+=,-=, and /= are preferred over directly setting values. + /// + public class StageSettings { + /// + /// How many credits the scene director has for monsters at the start of a stage. + /// This scales with difficulty, and thus will always be zero on the first stage. + /// + public Int32 sceneDirectorMonsterCredits; + /// + /// How many credits the scene director has for interactables at the start of a stage. + /// + public Int32 sceneDirectorInteractableCredits; + + /// + /// If the GameObject key of the dictionary is enabled, then the scene director gains the value in extra interactable credits + /// Used for things like the door in Abyssal Depths. + /// + public Dictionary bonusCreditObjects; + + /// + /// The weights for each monster category on this stage. + /// + public Dictionary monsterCategoryWeights; + + /// + /// The weights for each interactable category on this stage. + /// + public Dictionary interactableCategoryWeights; + } + /// + /// A wrapper class for DirectorCards. A list of these is passed to everything subscribed to monsterActions and interactableActions. + /// + public class DirectorCardHolder { + /// + /// The director card. This contains the majority of the information for an interactable or monster, including the prefab. + /// + public DirectorCard card; + /// + /// The monster category the card belongs to. Will be set to None for interactables. + /// + public MonsterCategory monsterCategory; + /// + /// The interactable category the card belongs to. Will be set to none for monsters. + /// + public InteractableCategory interactableCategory; + } + /// + /// A wrapper class for Monster Families. A list of these is passed to everything subscribed to familyActions. + /// + public class MonsterFamilyHolder { + /// + /// List of all basic monsters that can spawn during this family event. + /// + public List familyBasicMonsters; + /// + /// List of all minibosses that can spawn during this family event. + /// + public List familyMinibosses; + /// + /// List of all champions that can spawn during this family event. + /// + public List familyChampions; + + /// + /// The selection weight for basic monsters during the family event. + /// + public Single familyBasicMonsterWeight; + /// + /// The selection weight for minibosses during the family event. + /// + public Single familyMinibossWeight; + /// + /// The selection weight for champions during the family event. + /// + public Single familyChampionWeight; + /// + /// The minimum number of stages completed for this family event to occur. + /// + public Int32 minStageCompletion; + /// + /// The maximum number of stages for this family event to occur. + /// + public Int32 maxStageCompletion; + + /// + /// The weight of this monster family relative to other monster families. + /// Does NOT increase the chances of a family event occuring, just the chance that this will be chosen when one does occur. + /// Support for modifying the chance of family events overall will come later (and will be in StageSettings) + /// + public Single familySelectionWeight; + + /// + /// The message sent to chat when this family is selected. + /// + public String selectionChatString; + } + } +} diff --git a/R2API/DirectorAPIhelpers.cs b/R2API/DirectorAPIhelpers.cs new file mode 100644 index 00000000..46e11011 --- /dev/null +++ b/R2API/DirectorAPIhelpers.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using R2API.Utils; +using RoR2; + +namespace R2API { + // ReSharper disable once InconsistentNaming + //[R2APISubmodule] + public static partial class DirectorAPI { + /// + /// This subclass contains helper methods for use with DirectorAPI. + /// Note that there is much more flexibility by working with the API directly through its event system. + /// The primary purpose of these helpers is to serve as example code, and to assist with very simple tasks. + /// They are NOT intended to be, or ever will be, a comprehensive way to use the DirectorAPI. + /// + public static class Helpers { + /// + /// This class contains static strings for each characterspawncard in the base game. + /// These can be used for matching names. + /// + public static class MonsterNames { + public static readonly String StoneTitanDistantRoost = "csctitanblackbeach"; + public static readonly String StoneTitanAbyssalDepths = "csctitandampcaves"; + public static readonly String StoneTitanTitanicPlains = "csctitangolemplains"; + public static readonly String StoneTitanAbandonedAqueduct = "csctitangoolake"; + public static readonly String ArchaicWisp = "cscarchwisp"; + public static readonly String StrikeDrone = "cscbackupdrone"; + public static readonly String Beetle = "cscbeetle"; + public static readonly String BeetleGuard = "cscbeetleguard"; + public static readonly String BeetleGuardFriendly = "cscbeetleguardally"; + public static readonly String BeetleQueen = "cscbeetlequeen"; + public static readonly String BrassContraption = "cscbell"; + public static readonly String BighornBison = "cscbison"; + public static readonly String ClayDunestrider = "cscclayboss"; + public static readonly String ClayTemplar = "cscclaybruiser"; + public static readonly String OverloadingWorm = "cscelectricworm"; + public static readonly String StoneGolem = "cscgolem"; + public static readonly String Grovetender = "cscgravekeeper"; + public static readonly String GreaterWisp = "cscgreaterwisp"; + public static readonly String HermitCrab = "cschermitcrab"; + public static readonly String Imp = "cscimp"; + public static readonly String ImpOverlord = "cscimpboss"; + public static readonly String Jellyfish = "cscjellyfish"; + public static readonly String Lemurian = "csclemurian"; + public static readonly String ElderLemurian = "csclemurianbruiser"; + public static readonly String LesserWisp = "csclesserwisp"; + public static readonly String MagmaWorm = "cscmagmaworm"; + public static readonly String SolusControlUnit = "cscroboballboss"; + public static readonly String SolusProbe = "cscroboballmini"; + public static readonly String AlloyWorshipUnit = "cscsuperroboballboss"; + public static readonly String AlliedWarshipUnit = "cscsuperroboballboss"; + public static readonly String FriendlyBoatUnit = "cscsuperroboballboss"; + public static readonly String Aurelionite = "csctitangold"; + public static readonly String AurelioniteAlly = "csctitangoldally"; + public static readonly String WanderingVagrant = "cscvagrant"; + public static readonly String AlloyVulture = "cscvulture"; + } + + /// + /// This class contains static strings for each interactablespawncard in the base game. + /// These can be used for matching names. + /// + public static class InteractableNames { + public static readonly String Barrel = "iscbarrel1"; + public static readonly String GunnerDrone = "iscbrokendrone1"; + public static readonly String HealingDrone = "iscbrokendrone2"; + public static readonly String EquipmentDrone = "iscbrokenequipmentdrone"; + public static readonly String IncineratorDrone = "iscbrokenflamedrone"; + public static readonly String TC280 = "iscbrokenmegadrone"; + public static readonly String MissileDrone = "iscbrokenmissiledrone"; + public static readonly String GunnerTurret = "iscbrokenturret1"; + public static readonly String DamageChest = "isccategorychestdamage"; + public static readonly String HealingChest = "isccategorychesthealing"; + public static readonly String UtilityChest = "isccategorychestutility"; + public static readonly String BasicChest = "iscchest1"; + public static readonly String CloakedChest = "iscchest1stealthed"; + public static readonly String LargeChest = "iscchest2"; + public static readonly String PrinterCommon = "iscduplicator"; + public static readonly String PrinterUncommon = "iscduplicatorlarge"; + public static readonly String PrinterLegendary = "iscduplicatormilitary"; + public static readonly String EquipmentBarrel = "iscequipmentbarrel"; + public static readonly String LegendaryChest = "iscgoldchest"; + public static readonly String HalcyonBacon = "iscgoldshoresbracon"; + public static readonly String GoldPortal = "iscgoldshoresportal"; + public static readonly String Lockbox = "isclockbox"; + public static readonly String LunarBud = "isclunarchest"; + public static readonly String CelestialPortal = "iscmsportal"; + public static readonly String RadioScanner = "iscradartower"; + public static readonly String BluePortal = "iscshopportal"; + public static readonly String BloodShrine = "iscshrineblood"; + public static readonly String MountainShrine = "iscshrineboss"; + public static readonly String ChanceShrine = "iscshrinechance"; + public static readonly String CombatShrine = "iscshrinecombat"; + public static readonly String GoldShrine = "iscshrinegoldshoresaccess"; + public static readonly String WoodsShrine = "iscshrinehealing"; + public static readonly String OrderShrine = "iscshrinerestack"; + public static readonly String Teleporter = "iscteleporter"; + public static readonly String MultiShopCommon = "isctripleshop"; + public static readonly String MultiShopUncommon = "isctripleshoplarge"; + } + + + /// + /// Enables or disables elite spawns for a specific monster. + /// + /// The name of the monster to edit + /// Should elites be allowed? + public static void PreventElites( String monsterName, Boolean elitesAllowed ) { + + DirectorAPI.monsterActions += ( monsters, currentStage ) => { + foreach( DirectorCardHolder holder in monsters ) { + if( holder.card.spawnCard.name.ToLower() == monsterName.ToLower() ) { + ((CharacterSpawnCard)holder.card.spawnCard).noElites = elitesAllowed; + } + } + }; + } + + /// + /// Adds a new monster to all stages. + /// + /// The DirectorCard for the monster + /// The category to add the monster to + public static void AddNewMonster( DirectorCard monsterCard, MonsterCategory category ) { + + DirectorCardHolder card = new DirectorCardHolder + { + card = monsterCard, + interactableCategory = InteractableCategory.None, + monsterCategory = category + }; + DirectorAPI.monsterActions += ( monsters, currentStage ) => { + monsters.Add( card ); + }; + } + + /// + /// Adds a new monster to a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The DirectorCard of the monster to add + /// The category to add the monster to + /// The stage to add the monster to + /// The name of the custom stage + public static void AddNewMonsterToStage( DirectorCard monsterCard, MonsterCategory category, Stage stage, String customStageName = "" ) { + + DirectorCardHolder card = new DirectorCardHolder + { + card = monsterCard, + interactableCategory = InteractableCategory.None, + monsterCategory = category + }; + DirectorAPI.monsterActions += ( monsters, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + monsters.Add( card ); + } + } + }; + } + + /// + /// Adds a new interactable to all stages. + /// + /// The DirectorCard for the interactable + /// The category of the interactable + public static void AddNewInteractable( DirectorCard interactableCard, InteractableCategory category ) { + + DirectorCardHolder card = new DirectorCardHolder + { + card = interactableCard, + interactableCategory = category, + monsterCategory = MonsterCategory.None + }; + DirectorAPI.interactableActions += ( interactables, currentStage ) => { + interactables.Add( card ); + }; + } + + /// + /// Adds a new interactable to a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The DirectorCard of the interactable + /// The category of the interactable + /// The stage to add the interactable to + /// The name of the custom stage + public static void AddNewInteractableToStage( DirectorCard interactableCard, InteractableCategory category, Stage stage, String customStageName = "" ) { + + DirectorCardHolder card = new DirectorCardHolder + { + card = interactableCard, + interactableCategory = category, + monsterCategory = MonsterCategory.None + }; + DirectorAPI.interactableActions += ( interactables, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + interactables.Add( card ); + } + } + }; + } + + /// + /// Removes a monster from spawns on all stages. + /// + /// The name of the monster card to remove + public static void RemoveExistingMonster( String monsterName ) { + + DirectorAPI.monsterActions += ( monsters, currentStage ) => { + monsters.RemoveAll( ( card ) => (card.card.spawnCard.name.ToLower() == monsterName.ToLower()) ); + }; + } + + /// + /// Removes a monster from spawns on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The name of the monster card to remove + /// The stage to remove on + /// The name of the custom stage + public static void RemoveExistingMonsterFromStage( String monsterName, Stage stage, String customStageName = "" ) { + + DirectorAPI.monsterActions += ( monsters, currentStage ) => { + if( currentStage.stage == stage ) { + if( (stage != Stage.Custom) ^ (currentStage.customStageName == customStageName) ) { + monsters.RemoveAll( ( card ) => (card.card.spawnCard.name.ToLower() == monsterName.ToLower()) ); + } + } + }; + } + + /// + /// Remove an interactable from spawns on all stages. + /// + /// Name of the interactable to remove + public static void RemoveExistingInteractable( String interactableName ) { + + DirectorAPI.interactableActions += ( interactables, currentStage ) => { + interactables.RemoveAll( ( card ) => (card.card.spawnCard.name.ToLower() == interactableName.ToLower()) ); + }; + } + + /// + /// Remove an interactable from spawns on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The name of the interactable to remove + /// The stage to remove on + /// The name of the custom stage + public static void RemoveExistingInteractableFromStage( String interactableName, Stage stage, String customStageName = "" ) { + + DirectorAPI.interactableActions += ( interactables, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + interactables.RemoveAll( ( card ) => (card.card.spawnCard.name.ToLower() == interactableName.ToLower()) ); + } + } + }; + } + + /// + /// Adds a flat amount of monster credits to the scene director on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The quantity to add + /// The stage to add on + /// The name of the custom stage + public static void AddSceneMonsterCredits( Int32 increase, Stage stage, String customStageName = "" ) { + + DirectorAPI.stageSettingsActions += ( settings, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + settings.sceneDirectorMonsterCredits += increase; + } + } + }; + } + + /// + /// Adds a flat amount of interactable credits to the scene director on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The quantity to add + /// The stage to add on + /// The name of the custom stage + public static void AddSceneInteractableCredits( Int32 increase, Stage stage, String customStageName = "" ) { + + DirectorAPI.stageSettingsActions += ( settings, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + settings.sceneDirectorInteractableCredits += increase; + } + } + }; + } + + /// + /// Multiplies the scene director monster credits on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The number to multiply by + /// The stage to multiply on + /// The name of the custom stage + public static void MultiplySceneMonsterCredits( Int32 multiplier, Stage stage, String customStageName = "" ) { + + DirectorAPI.stageSettingsActions += ( settings, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + settings.sceneDirectorMonsterCredits *= multiplier; + } + } + }; + } + + /// + /// Multiplies the scene director interactable credits on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The number to multiply by + /// The stage to multiply on + /// The name of the custom stage + public static void MultiplySceneInteractableCredits( Int32 multiplier, Stage stage, String customStageName = "" ) { + + DirectorAPI.stageSettingsActions += ( settings, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + settings.sceneDirectorInteractableCredits *= multiplier; + } + } + }; + } + + /// + /// Divides the scene director monster credits on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The number to divide by + /// The stage to divide on + /// The name of the custom stage + public static void ReduceSceneMonsterCredits( Int32 divisor, Stage stage, String customStageName = "" ) { + + DirectorAPI.stageSettingsActions += ( settings, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + settings.sceneDirectorMonsterCredits /= divisor; + } + } + }; + } + + /// + /// Divides the scene director interactable credits on a specific stage. + /// For custom stages use Stage.Custom and enter the name of the stage in customStageName. + /// + /// The number to divide by + /// The stage to divide on + /// The name of the custom stage + public static void ReduceSceneInteractableCredits( Int32 divisor, Stage stage, String customStageName = "" ) { + + DirectorAPI.stageSettingsActions += ( settings, currentStage ) => { + if( currentStage.stage == stage ) { + if( currentStage.CheckStage( stage, customStageName ) ) { + settings.sceneDirectorInteractableCredits /= divisor; + } + } + }; + } + } + } +} diff --git a/R2API/DirectorAPIinternal.cs b/R2API/DirectorAPIinternal.cs new file mode 100644 index 00000000..1d91445c --- /dev/null +++ b/R2API/DirectorAPIinternal.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using R2API.Utils; +using RoR2; +using UnityEngine; + +namespace R2API { + // ReSharper disable once InconsistentNaming + [R2APISubmodule] + public static partial class DirectorAPI { + [R2APISubmoduleInit(Stage = InitStage.SetHooks)] + internal static void SetHooks() { + On.RoR2.ClassicStageInfo.Awake += ClassicStageInfo_Awake; + } + + [R2APISubmoduleInit(Stage = InitStage.UnsetHooks)] + internal static void UnsetHooks() { + On.RoR2.ClassicStageInfo.Awake -= ClassicStageInfo_Awake; + } + + private static void ClassicStageInfo_Awake( On.RoR2.ClassicStageInfo.orig_Awake orig, ClassicStageInfo self ) { + var stageInfo = GetStageInfo( self ); + ApplySettingsChanges( self, stageInfo ); + ApplyMonsterChanges( self, stageInfo ); + ApplyInteractableChanges( self, stageInfo ); + ApplyFamilyChanges( self, stageInfo ); + orig( self ); + } + + private static StageInfo GetStageInfo( ClassicStageInfo stage ) { + StageInfo stageInfo = new StageInfo + { + stage = Stage.Custom, + customStageName = "", + }; + SceneInfo info = stage.GetComponent(); + if( !info ) return stageInfo; + SceneDef scene = info.sceneDef; + if( !scene ) return stageInfo; + switch( scene.sceneName ) { + case "golemplains": + stageInfo.stage = Stage.TitanicPlains; + break; + case "blackbeach": + stageInfo.stage = Stage.DistantRoost; + break; + case "goolake": + stageInfo.stage = Stage.AbandonedAqueduct; + break; + case "foggyswamp": + stageInfo.stage = Stage.WetlandAspect; + break; + case "frozenwall": + stageInfo.stage = Stage.RallypointDelta; + break; + case "wispgraveyard": + stageInfo.stage = Stage.ScorchedAcres; + break; + case "dampcavesimple": + stageInfo.stage = Stage.AbyssalDepths; + break; + case "shipgraveyard": + stageInfo.stage = Stage.SirensCall; + break; + case "goldshores": + stageInfo.stage = Stage.GildedCoast; + break; + default: + stageInfo.stage = Stage.Custom; + stageInfo.customStageName = scene.sceneName; + break; + } + return stageInfo; + } + + private static void ApplySettingsChanges( ClassicStageInfo self, StageInfo stageInfo ) { + StageSettings settings = GetStageSettings( self ); + stageSettingsActions?.Invoke( settings, stageInfo ); + SetStageSettings( self, settings ); + } + + private static void ApplyMonsterChanges( ClassicStageInfo self, StageInfo stage ) { + var monsters = self.GetFieldValue("monsterCategories"); + List monsterCards = new List(); + for( Int32 i = 0; i < monsters.categories.Length; i++ ) { + DirectorCardCategorySelection.Category cat = monsters.categories[i]; + MonsterCategory monstCat = GetMonsterCategory( cat.name ); + InteractableCategory interCat = GetInteractableCategory( cat.name); + for( Int32 j = 0; j < cat.cards.Length; j++ ) { + monsterCards.Add( new DirectorCardHolder { + interactableCategory = interCat, + monsterCategory = monstCat, + card = cat.cards[j] + } ); + } + } + monsterActions?.Invoke( monsterCards, stage ); + List monsterBasic = new List(); + List monsterSub = new List(); + List monsterChamp = new List(); + for( Int32 i = 0; i < monsterCards.Count; i++ ) { + DirectorCardHolder hold = monsterCards[i]; + switch( hold.monsterCategory ) { + default: + break; + case MonsterCategory.BasicMonsters: + monsterBasic.Add( hold.card ); + break; + case MonsterCategory.Champions: + monsterChamp.Add( hold.card ); + break; + case MonsterCategory.Minibosses: + monsterSub.Add( hold.card ); + break; + } + } + for( Int32 i = 0; i < monsters.categories.Length; i++ ) { + DirectorCardCategorySelection.Category cat = monsters.categories[i]; + switch( cat.name ) { + default: + break; + case "Champions": + cat.cards = monsterChamp.ToArray(); + break; + case "Minibosses": + cat.cards = monsterSub.ToArray(); + break; + case "Basic Monsters": + cat.cards = monsterBasic.ToArray(); + break; + } + monsters.categories[i] = cat; + } + } + + private static void ApplyInteractableChanges( ClassicStageInfo self, StageInfo stage ) { + var interactables = self.GetFieldValue("interactableCategories"); + List interactableCards = new List(); + for( Int32 i = 0; i < interactables.categories.Length; i++ ) { + DirectorCardCategorySelection.Category cat = interactables.categories[i]; + MonsterCategory monstCat = GetMonsterCategory( cat.name ); + InteractableCategory interCat = GetInteractableCategory( cat.name ); + for( Int32 j = 0; j < cat.cards.Length; j++ ) { + interactableCards.Add( new DirectorCardHolder { + interactableCategory = interCat, + monsterCategory = monstCat, + card = cat.cards[j] + } ); + } + } + interactableActions?.Invoke( interactableCards, stage ); + List interChests = new List(); + List interBarrels = new List(); + List interShrines = new List(); + List interDrones = new List(); + List interMisc = new List(); + List interRare = new List(); + List interDupe = new List(); + for( Int32 i = 0; i < interactableCards.Count; i++ ) { + DirectorCardHolder hold = interactableCards[i]; + switch( hold.interactableCategory ) { + default: + Debug.Log( "Wtf are you doing..." ); + break; + case InteractableCategory.Chests: + interChests.Add( hold.card ); + break; + case InteractableCategory.Barrels: + interBarrels.Add( hold.card ); + break; + case InteractableCategory.Drones: + interDrones.Add( hold.card ); + break; + case InteractableCategory.Duplicator: + interDupe.Add( hold.card ); + break; + case InteractableCategory.Misc: + interMisc.Add( hold.card ); + break; + case InteractableCategory.Rare: + interRare.Add( hold.card ); + break; + case InteractableCategory.Shrines: + interShrines.Add( hold.card ); + break; + } + } + for( Int32 i = 0; i < interactables.categories.Length; i++ ) { + DirectorCardCategorySelection.Category cat = interactables.categories[i]; + switch( cat.name ) { + default: + break; + case "Chests": + cat.cards = interChests.ToArray(); + break; + case "Barrels": + cat.cards = interBarrels.ToArray(); + break; + case "Shrines": + cat.cards = interShrines.ToArray(); + break; + case "Drones": + cat.cards = interDrones.ToArray(); + break; + case "Misc": + cat.cards = interMisc.ToArray(); + break; + case "Rare": + cat.cards = interRare.ToArray(); + break; + case "Duplicator": + cat.cards = interDupe.ToArray(); + break; + } + interactables.categories[i] = cat; + } + } + + private static void ApplyFamilyChanges( ClassicStageInfo self, StageInfo stage ) { + List familyHolds = new List(); + for( Int32 i = 0; i < self.possibleMonsterFamilies.Length; i++ ) { + familyHolds.Add( GetMonsterFamilyHolder( self.possibleMonsterFamilies[i] ) ); + } + familyActions?.Invoke( familyHolds, stage ); + self.possibleMonsterFamilies = new ClassicStageInfo.MonsterFamily[familyHolds.Count]; + for( Int32 i = 0; i < familyHolds.Count; i++ ) { + Debug.Log( i ); + self.possibleMonsterFamilies[i] = GetMonsterFamily( familyHolds[i] ); + } + } + + private static StageSettings GetStageSettings( ClassicStageInfo self ) { + StageSettings set = new StageSettings + { + sceneDirectorInteractableCredits = self.sceneDirectorInteractibleCredits, + sceneDirectorMonsterCredits = self.sceneDirectorMonsterCredits + }; + set.bonusCreditObjects = new Dictionary(); + for( Int32 i = 0; i < self.bonusInteractibleCreditObjects.Length; i++ ) { + var bonusObj = self.bonusInteractibleCreditObjects[i]; + set.bonusCreditObjects[bonusObj.objectThatGrantsPointsIfEnabled] = bonusObj.points; + } + set.interactableCategoryWeights = new Dictionary(); + var interCats = self.GetFieldValue("interactableCategories"); + for( Int32 i = 0; i < interCats.categories.Length; i++ ) { + var cat = interCats.categories[i]; + set.interactableCategoryWeights[GetInteractableCategory( cat.name )] = cat.selectionWeight; + } + set.monsterCategoryWeights = new Dictionary(); + var monstCats = self.GetFieldValue("monsterCategories"); + for( Int32 i = 0; i < monstCats.categories.Length; i++ ) { + var cat = monstCats.categories[i]; + set.monsterCategoryWeights[GetMonsterCategory( cat.name )] = cat.selectionWeight; + } + return set; + } + + private static void SetStageSettings( ClassicStageInfo self, StageSettings set ) { + self.sceneDirectorInteractibleCredits = set.sceneDirectorInteractableCredits; + self.sceneDirectorMonsterCredits = set.sceneDirectorMonsterCredits; + var keys = set.bonusCreditObjects.Keys.ToArray(); + var bonuses = new ClassicStageInfo.BonusInteractibleCreditObject[keys.Length]; + for( Int32 i = 0; i < keys.Length; i++ ) { + bonuses[i] = new ClassicStageInfo.BonusInteractibleCreditObject { + objectThatGrantsPointsIfEnabled = keys[i], + points = set.bonusCreditObjects[keys[i]] + }; + } + self.bonusInteractibleCreditObjects = bonuses; + var interCats = self.GetFieldValue("interactableCategories"); + for( Int32 i = 0; i < interCats.categories.Length; i++ ) { + var cat = interCats.categories[i]; + InteractableCategory intCat = GetInteractableCategory( cat.name ); + cat.selectionWeight = set.interactableCategoryWeights[intCat]; + interCats.categories[i] = cat; + } + var monstCats = self.GetFieldValue("monsterCategories"); + for( Int32 i = 0; i < monstCats.categories.Length; i++ ) { + var cat = monstCats.categories[i]; + MonsterCategory monCat = GetMonsterCategory( cat.name ); + cat.selectionWeight = set.monsterCategoryWeights[monCat]; + monstCats.categories[i] = cat; + } + } + + private static MonsterCategory GetMonsterCategory( String s ) { + switch( s ) { + default: + return MonsterCategory.None; + case "Champions": + return MonsterCategory.Champions; + case "Minibosses": + return MonsterCategory.Minibosses; + case "Basic Monsters": + return MonsterCategory.BasicMonsters; + } + } + + private static InteractableCategory GetInteractableCategory( String s ) { + switch( s ) { + default: + return InteractableCategory.None; + case "Chests": + return InteractableCategory.Chests; + case "Barrels": + return InteractableCategory.Barrels; + case "Shrines": + return InteractableCategory.Shrines; + case "Drones": + return InteractableCategory.Drones; + case "Misc": + return InteractableCategory.Misc; + case "Rare": + return InteractableCategory.Rare; + case "Duplicator": + return InteractableCategory.Duplicator; + } + } + + private static MonsterFamilyHolder GetMonsterFamilyHolder( ClassicStageInfo.MonsterFamily family ) { + MonsterFamilyHolder hold = new MonsterFamilyHolder + { + maxStageCompletion = family.maximumStageCompletion, + minStageCompletion = family.minimumStageCompletion, + familySelectionWeight = family.selectionWeight, + selectionChatString = family.familySelectionChatString + }; + var cards = family.monsterFamilyCategories.categories; + for( Int32 i = 0; i < cards.Length; i++ ) { + var cat = cards[i]; + + switch( cat.name ) { + case "Basic Monsters": + hold.familyBasicMonsterWeight = cat.selectionWeight; + hold.familyBasicMonsters = cat.cards.ToList(); + break; + case "Minibosses": + hold.familyMinibossWeight = cat.selectionWeight; + hold.familyMinibosses = cat.cards.ToList(); + break; + case "Champions": + hold.familyChampionWeight = cat.selectionWeight; + hold.familyChampions = cat.cards.ToList(); + break; + } + } + return hold; + } + + private static ClassicStageInfo.MonsterFamily GetMonsterFamily( MonsterFamilyHolder holder ) { + DirectorCardCategorySelection catSel = ScriptableObject.CreateInstance(); + catSel.categories = new DirectorCardCategorySelection.Category[3]; + catSel.categories[0] = new DirectorCardCategorySelection.Category { + name = "Champions", + selectionWeight = holder.familyChampionWeight, + cards = (holder.familyChampions != null ? holder.familyChampions.ToArray() : Array.Empty()) + }; + catSel.categories[1] = new DirectorCardCategorySelection.Category { + name = "Minibosses", + selectionWeight = holder.familyMinibossWeight, + cards = (holder.familyMinibosses != null ? holder.familyMinibosses.ToArray() : Array.Empty()) + }; + catSel.categories[2] = new DirectorCardCategorySelection.Category { + name = "Basic Monsters", + selectionWeight = holder.familyBasicMonsterWeight, + cards = (holder.familyBasicMonsters != null ? holder.familyBasicMonsters.ToArray() : Array.Empty()) + }; + return new ClassicStageInfo.MonsterFamily { + familySelectionChatString = holder.selectionChatString, + maximumStageCompletion = holder.maxStageCompletion, + minimumStageCompletion = holder.minStageCompletion, + selectionWeight = holder.familySelectionWeight, + monsterFamilyCategories = catSel + }; + } + } +} From 6d9aa1c6879bb0866cb8db2d68ba792331eb9721 Mon Sep 17 00:00:00 2001 From: ReinMasamune Date: Fri, 29 Nov 2019 21:40:58 -0500 Subject: [PATCH 2/6] EffectAPI --- R2API/EffectAPI.cs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 R2API/EffectAPI.cs diff --git a/R2API/EffectAPI.cs b/R2API/EffectAPI.cs new file mode 100644 index 00000000..bb7824a3 --- /dev/null +++ b/R2API/EffectAPI.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using R2API.Utils; +using RoR2; +using UnityEngine; + +namespace R2API { + // ReSharper disable once InconsistentNaming + [R2APISubmodule] + public static class EffectAPI { + + /// + /// Adds an effect to the EffectCatalog + /// Can be called at any time. + /// + /// The prefab of the effect to be added + /// True if the effect was added + public static Boolean AddEffect(GameObject effect) { + List effects = EffectManager.instance.GetFieldValue>("effectPrefabsList"); + Dictionary effectLookup = EffectManager.instance.GetFieldValue>("effectPrefabToIndexMap"); + + if(!effect) { + return false; + } + + System.Int32 index = effects.Count; + + effects.Add( effect ); + effectLookup.Add( effect, (System.UInt32)index ); + + return true; + } + } +} From 5efd2b7167fd7c3efa59426bfdc4547530ed6bda Mon Sep 17 00:00:00 2001 From: ReinMasamune Date: Fri, 29 Nov 2019 21:41:06 -0500 Subject: [PATCH 3/6] OrbAPI --- R2API/OrbAPI.cs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 R2API/OrbAPI.cs diff --git a/R2API/OrbAPI.cs b/R2API/OrbAPI.cs new file mode 100644 index 00000000..a924f289 --- /dev/null +++ b/R2API/OrbAPI.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using R2API.Utils; +using RoR2; + +namespace R2API { + // ReSharper disable once InconsistentNaming + [R2APISubmodule] + public static class OrbAPI { + private static Boolean orbsAlreadyAdded = false; + + public static ObservableCollection OrbDefinitions = new ObservableCollection(); + + /// + /// Adds an Orb to the orb catalog. + /// This must be called during plugin Awake() or OnEnable(). + /// The type must be a subclass of RoR2.Orbs.Orb + /// + /// The type of the orb being added + /// True if orb will be added + public static bool AddOrb(Type t) { + if (orbsAlreadyAdded) { + R2API.Logger.LogError($"Tried to add Orb type: {nameof(t)} after orb catalog was generated"); + return false; + } + + if (t == null || !t.IsSubclassOf( typeof( RoR2.Orbs.Orb ) ) ) { + R2API.Logger.LogError($"Type: {nameof(t)} is null or not a subclass of RoR2.Orbs.Orb"); + return false; + } + + OrbDefinitions.Add(t); + + return true; + } + + [R2APISubmoduleInit(Stage = InitStage.SetHooks)] + internal static void SetHooks() { + On.RoR2.Orbs.OrbCatalog.GenerateCatalog += AddOrbs; + } + + [R2APISubmoduleInit(Stage = InitStage.UnsetHooks)] + internal static void UnsetHooks() { + On.RoR2.Orbs.OrbCatalog.GenerateCatalog -= AddOrbs; + } + + private static void AddOrbs( On.RoR2.Orbs.OrbCatalog.orig_GenerateCatalog orig ) { + orbsAlreadyAdded = true; + orig(); + + Type[] orbCat = typeof(RoR2.Orbs.OrbCatalog).GetFieldValue("indexToType"); + Dictionary typeToIndex = typeof(RoR2.Orbs.OrbCatalog).GetFieldValue>("typeToIndex"); + + Int32 origLength = orbCat.Length; + Int32 extraLength = OrbDefinitions.Count; + + Array.Resize( ref orbCat, origLength + extraLength ); + + Int32 temp; + + for( Int32 i = 0; i < extraLength; i++ ) { + temp = i + origLength; + orbCat[temp] = OrbDefinitions[i]; + typeToIndex.Add( OrbDefinitions[i], temp ); + } + + typeof( RoR2.Orbs.OrbCatalog ).SetFieldValue( "indexToType", orbCat ); + typeof( RoR2.Orbs.OrbCatalog ).SetFieldValue>( "typeToIndex", typeToIndex ); + } + } +} From e21a4c04c472ca0150c03869c014d72830159130 Mon Sep 17 00:00:00 2001 From: ReinMasamune Date: Fri, 29 Nov 2019 21:41:17 -0500 Subject: [PATCH 4/6] PrefabAPI --- R2API/PrefabAPI.cs | 144 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 R2API/PrefabAPI.cs diff --git a/R2API/PrefabAPI.cs b/R2API/PrefabAPI.cs new file mode 100644 index 00000000..ae5f8308 --- /dev/null +++ b/R2API/PrefabAPI.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using R2API.Utils; +using RoR2; +using UnityEngine; +using UnityEngine.Networking; + +namespace R2API { + // ReSharper disable once InconsistentNaming + [R2APISubmodule] + public static class PrefabAPI { + + private static Boolean needToRegister = false; + private static GameObject parent; + private static List thingsToHash = new List(); + + /// + /// Duplicates a GameObject and leaves it in a "sleeping" state where it is inactive, but becomes active when spawned. + /// Also registers the clone to network if registerNetwork is not set to false. + /// Do not override the file, member, and line number parameters. They are used to generate a unique hash for the network ID. + /// + /// The GameObject to clone + /// The name to give the clone (Should be unique) + /// Should the object be registered to network + /// The GameObject of the clone + public static GameObject InstantiateClone( this GameObject g, System.String nameToSet, System.Boolean registerNetwork = true, [CallerFilePath] System.String file = "", [CallerMemberName] System.String member = "", [CallerLineNumber] System.Int32 line = 0 ) { + GameObject prefab = MonoBehaviour.Instantiate(g, GetParent().transform); + prefab.name = nameToSet; + if( registerNetwork ) { + RegisterPrefabInternal( prefab, file, member, line ); + } + return prefab; + } + + /// + /// Registers a prefab so that NetworkServer.Spawn will function properly with it. + /// Only will work on prefabs with a NetworkIdentity component. + /// Is never needed for existing objects unless you have cloned them. + /// Do not override the file, member, and line number parameters. They are used to generate a unique hash for the network ID. + /// + /// The prefab to register + public static void RegisterNetworkPrefab( this GameObject g, [CallerFilePath] System.String file = "", [CallerMemberName] System.String member = "", [CallerLineNumber] System.Int32 line = 0 ) { + RegisterPrefabInternal( g, file, member, line ); + } + + [R2APISubmoduleInit(Stage = InitStage.SetHooks)] + internal static void SetHooks() { + + } + + [R2APISubmoduleInit(Stage = InitStage.UnsetHooks)] + internal static void UnsetHooks() { + + } + + private static GameObject GetParent() { + if( !parent ) { + parent = new GameObject( "ModdedPrefabs" ); + MonoBehaviour.DontDestroyOnLoad( parent ); + parent.SetActive( false ); + + On.RoR2.Util.IsPrefab += ( orig, obj ) => { + if( obj.transform.parent && obj.transform.parent.gameObject.name == "ModdedPrefabs" ) return true; + return orig( obj ); + }; + } + + return parent; + } + + private struct HashStruct { + public GameObject prefab; + public System.String goName; + public System.String callPath; + public System.String callMember; + public System.Int32 callLine; + } + + private static void RegisterPrefabInternal( GameObject prefab, System.String callPath, System.String callMember, System.Int32 callLine ) { + HashStruct h = new HashStruct + { + prefab = prefab, + goName = prefab.name, + callPath = callPath, + callMember = callMember, + callLine = callLine + }; + thingsToHash.Add( h ); + SetupRegistrationEvent(); + } + + private static void SetupRegistrationEvent() { + if( !needToRegister ) { + needToRegister = true; + On.RoR2.Networking.GameNetworkManager.OnStartClient += RegisterClientPrefabsNStuff; + } + } + + private static NetworkHash128 nullHash = new NetworkHash128 + { + i0 = 0, + i1 = 0, + i2 = 0, + i3 = 0, + i4 = 0, + i5 = 0, + i6 = 0, + i7 = 0, + i8 = 0, + i9 = 0, + i10 = 0, + i11 = 0, + i12 = 0, + i13 = 0, + i14 = 0, + i15 = 0 + }; + + private static void RegisterClientPrefabsNStuff( On.RoR2.Networking.GameNetworkManager.orig_OnStartClient orig, RoR2.Networking.GameNetworkManager self, UnityEngine.Networking.NetworkClient newClient ) { + orig( self, newClient ); + foreach( HashStruct h in thingsToHash ) { + if( (h.prefab.GetComponent() != null)) h.prefab.GetComponent().SetFieldValue( "m_AssetId", nullHash ); + ClientScene.RegisterPrefab( h.prefab, NetworkHash128.Parse( MakeHash( h.goName + h.callPath + h.callMember + h.callLine.ToString() ) ) ); + } + } + + private static System.String MakeHash( System.String s ) { + MD5 hash = MD5.Create(); + System.Byte[] prehash = hash.ComputeHash( Encoding.UTF8.GetBytes( s ) ); + + StringBuilder sb = new StringBuilder(); + + for( System.Int32 i = 0; i < prehash.Length; i++ ) { + sb.Append( prehash[i].ToString( "x2" ) ); + } + + return sb.ToString(); + } + } +} From 3449a5d970d4a9fd39e94cae286992326e5edb8f Mon Sep 17 00:00:00 2001 From: ReinMasamune Date: Fri, 29 Nov 2019 21:41:24 -0500 Subject: [PATCH 5/6] SkillAPI --- R2API/SkillAPI.cs | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 R2API/SkillAPI.cs diff --git a/R2API/SkillAPI.cs b/R2API/SkillAPI.cs new file mode 100644 index 00000000..9d2da5f8 --- /dev/null +++ b/R2API/SkillAPI.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using R2API.Utils; +using RoR2; +using RoR2.Skills; + +namespace R2API { + // ReSharper disable once InconsistentNaming + [R2APISubmodule] + public static class SkillAPI { + /// + /// Adds a type for a skill EntityState to the SkillsCatalog. + /// State must derive from EntityStates.EntityState. + /// Note that SkillDefs and SkillFamiles must also be added seperately. + /// + /// The type to add + /// True if succesfully added + public static Boolean AddSkill( Type t ) { + if( t == null || !t.IsSubclassOf( typeof( EntityStates.EntityState ) ) || t.IsAbstract ) { + return false; + } + Type stateTab = typeof(EntityStates.EntityState).Assembly.GetType("EntityStates.StateIndexTable"); + Type[] id2State = stateTab.GetFieldValue("stateIndexToType"); + String[] name2Id = stateTab.GetFieldValue("stateIndexToTypeName"); + Dictionary state2Id = stateTab.GetFieldValue>("stateTypeToIndex"); + Int32 ogNum = id2State.Length; + Array.Resize( ref id2State, ogNum + 1 ); + Array.Resize( ref name2Id, ogNum + 1 ); + id2State[ogNum] = t; + name2Id[ogNum] = t.FullName; + state2Id[t] = (Int16)ogNum; + stateTab.SetFieldValue( "stateIndexToType", id2State ); + stateTab.SetFieldValue( "stateIndexToTypeName", name2Id ); + stateTab.SetFieldValue>( "stateTypeToIndex", state2Id ); + return true; + } + + /// + /// Registers an event to add a SkillDef to the SkillDefCatalog. + /// Must be called before Catalog init (during Awake() or OnEnable()) + /// + /// The SkillDef to add + /// True if the event was registered + public static Boolean AddSkillDef( SkillDef s ) { + if( !s ) return false; + SkillCatalog.getAdditionalSkillDefs += ( list ) => { + list.Add( s ); + }; + return true; + } + + /// + /// Registers an event to add a SkillFamily to the SkillFamiliesCatalog + /// Must be called before Catalog init (during Awake() or OnEnable()) + /// + /// The skillfamily to add + /// True if the event was registered + public static Boolean AddSkillFamily( SkillFamily sf ) { + if( !sf ) return false; + SkillCatalog.getAdditionalSkillFamilies += ( list ) => { + list.Add( sf ); + }; + return true; + } + } +} From af6e1c90bf526a60d96ac91730bd28b3100abdf0 Mon Sep 17 00:00:00 2001 From: ReinMasamune Date: Fri, 29 Nov 2019 21:41:30 -0500 Subject: [PATCH 6/6] SkinAPI --- R2API/SkinAPI.cs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 R2API/SkinAPI.cs diff --git a/R2API/SkinAPI.cs b/R2API/SkinAPI.cs new file mode 100644 index 00000000..4566c7fa --- /dev/null +++ b/R2API/SkinAPI.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using R2API.Utils; +using RoR2; +using RoR2.Skills; +using UnityEngine; + +namespace R2API { + // ReSharper disable once InconsistentNaming + [R2APISubmodule] + public static class SkinAPI { + /// + /// A container struct for all SkinDef parameters. + /// Use this to set skinDef values, then call CreateNewSkinDef(). + /// + public struct SkinDefInfo { + public SkinDef[] baseSkins; + public Sprite icon; + public System.String nameToken; + public System.String unlockableName; + public GameObject rootObject; + public CharacterModel.RendererInfo[] rendererInfos; + public System.String name; + } + + /// + /// Creates a new SkinDef from a SkinDefInfo. + /// Note that this prevents null-refs by disabling SkinDef awake while the SkinDef is being created. + /// The things that occur during awake are performed when first applied to a character instead. + /// + /// + /// + public static SkinDef CreateNewSkinDef( SkinDefInfo skin ) { + On.RoR2.SkinDef.Awake += DoNothing; + + SkinDef newSkin = ScriptableObject.CreateInstance(); + + newSkin.baseSkins = skin.baseSkins; + newSkin.icon = skin.icon; + newSkin.unlockableName = skin.unlockableName; + newSkin.rootObject = skin.rootObject; + newSkin.rendererInfos = skin.rendererInfos; + newSkin.nameToken = skin.nameToken; + newSkin.name = skin.name; + + On.RoR2.SkinDef.Awake -= DoNothing; + return newSkin; + } + + private static void DoNothing( On.RoR2.SkinDef.orig_Awake orig, SkinDef self ) { + //Intentionally do nothing + } + } +}