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
+ };
+ }
+ }
+}
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;
+ }
+ }
+}
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 );
+ }
+ }
+}
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();
+ }
+ }
+}
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;
+ }
+ }
+}
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
+ }
+ }
+}