Example Implementation: MioHuntressPlugin.cs

Updated a week ago

This file demonstrates an edited version of the main plugin file generated by skinbuilder. The 3 significant changes that are relevant to MwSkinAdditions are split up and highlighted with comments.

using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using MonoMod.RuntimeDetour.HookGen;
using RoR2;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Permissions;
using UnityEngine;
using UnityEngine.AddressableAssets;


#pragma warning disable CS0618
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
#pragma warning restore CS0618
namespace MioHuntress {
    [BepInDependency("mwmw.MwSkinAdditions", BepInDependency.DependencyFlags.HardDependency)] // SIGNIFICANT DIFFERENCE #1: Ensuring MwSkinAdditions is present and loaded before this mod
    [BepInPlugin("com.Miyowi.MioHuntress", "MioHuntress", "1.0.0")]
    public partial class MioHuntressPlugin : BaseUnityPlugin {
        internal static MioHuntressPlugin Instance { get; private set; }
        internal static ManualLogSource InstanceLogger => Instance?.Logger;

        public static AssetBundle assetBundle;

        public static ConfigFile config;

        private static readonly List<Material> materialsWithRoRShader = new List<Material>();

        private void Awake() {
            config = Config;
            MwSkinEvents.CreateVoiceGroups(); // SIGNIFICANT DIFFERENCE #2: Creating voice groups on awake to ensure they are loaded into the ContentPack in time
            Assets.Init();
        }

        private void Start() {
            Instance = this;

            BeforeStart();

            using (var assetStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MioHuntress.miyowimiohuntress")) {
                assetBundle = AssetBundle.LoadFromStream(assetStream);
            }

            BodyCatalog.availability.CallWhenAvailable(BodyCatalogInit);
            HookEndpointManager.Add(typeof(Language).GetMethod(nameof(Language.LoadStrings)), (Action<Action<Language>, Language>)LanguageLoadStrings);

            ReplaceShaders();

            Options.Init();

            AfterStart();
        }

        partial void BeforeStart();
        partial void AfterStart();
        static partial void BeforeBodyCatalogInit();
        static partial void AfterBodyCatalogInit();

        private static void ReplaceShaders() {
            LoadMaterialsWithReplacedShader(@"RoR2/Base/Shaders/HGStandard.shader"
                , @"Assets/Skins/MioHuntress/Assets/matMio.mat", @"Assets/Skins/MioHuntress/Assets/matMioBow.mat");
        }

        private static void LoadMaterialsWithReplacedShader(string shaderPath, params string[] materialPaths) {
            var shader = Addressables.LoadAssetAsync<Shader>(shaderPath).WaitForCompletion();
            foreach (var materialPath in materialPaths) {
                var material = assetBundle.LoadAsset<Material>(materialPath);
                material.shader = shader;
                materialsWithRoRShader.Add(material);
            }
        }

        private static void LanguageLoadStrings(Action<Language> orig, Language self) {
            orig(self);

            self.SetStringByToken("MIYOWI_SKIN_DEFMIOHUNTRESS_NAME", "Mio");
        }

        private static void BodyCatalogInit() {
            BeforeBodyCatalogInit();

            AddHuntressBodyDefMioHuntressSkin();

            AfterBodyCatalogInit();
        }

        static partial void HuntressBodyDefMioHuntressSkinAdded(SkinDef skinDef, GameObject bodyPrefab);

        private static void AddHuntressBodyDefMioHuntressSkin() {
            var bodyName = "HuntressBody";
            var skinName = "DefMioHuntress";
            try {
                var bodyPrefab = BodyCatalog.FindBodyPrefab(bodyName);
                if (!bodyPrefab) {
                    InstanceLogger.LogWarning($"Failed to add \"{skinName}\" skin because \"{bodyName}\" doesn't exist");
                    return;
                }

                var modelLocator = bodyPrefab.GetComponent<ModelLocator>();
                if (!modelLocator) {
                    InstanceLogger.LogWarning($"Failed to add \"{skinName}\" skin to \"{bodyName}\" because it doesn't have \"ModelLocator\" component");
                    return;
                }

                var mdl = modelLocator.modelTransform.gameObject;
                var skinController = mdl ? mdl.GetComponent<ModelSkinController>() : null;
                if (!skinController) {
                    InstanceLogger.LogWarning($"Failed to add \"{skinName}\" skin to \"{bodyName}\" because it doesn't have \"ModelSkinController\" component");
                    return;
                }

                var renderers = mdl.GetComponentsInChildren<Renderer>(true);
                var lights = mdl.GetComponentsInChildren<Light>(true);

                var skin = ScriptableObject.CreateInstance<SkinDef>();
                var skinParams = ScriptableObject.CreateInstance<SkinDefParams>();
                skin.skinDefParams = skinParams;

                TryCatchThrow("Icon", () => {
                    skin.icon = assetBundle.LoadAsset<Sprite>(@"Assets\SkinMods\MioHuntress\Icons\DefMioHuntressIcon.png");
                });
                skin.name = skinName;
                skin.nameToken = "MIYOWI_SKIN_DEFMIOHUNTRESS_NAME";
                skin.rootObject = mdl;
                TryCatchThrow("Base Skins", () => {
                    skin.baseSkins = new SkinDef[]
                    {
                        ThrowIfOutOfBounds(0, "Index 0 is out of bounds of skins array", skinController.skins, 0),
                    };
                });
                TryCatchThrow("Unlockable Name", () => {
                    skin.unlockableDef = null;
                });
                TryCatchThrow("Game Object Activations", () => {
                    skinParams.gameObjectActivations = new SkinDefParams.GameObjectActivation[]
                    {
                        new SkinDefParams.GameObjectActivation
                        {
                            gameObject = ThrowIfNull(0, "There is no renderer with the name \"HuntressScarfMesh\"", renderers.FirstOrDefault(r => r.name == "HuntressScarfMesh")).gameObject,
                            shouldActivate = false,
                            spawnPrefabOnModelObject = false,
                        },
                    };
                });
                TryCatchThrow("Renderer Infos", () => {
                    skinParams.rendererInfos = new CharacterModel.RendererInfo[]
                    {
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = assetBundle.LoadAsset<Material>(@"Assets/Skins/MioHuntress/Assets/matMio.mat"),
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = false,
                            renderer = ThrowIfNull(0, "There is no renderer with the name \"HuntressMesh\"", renderers.FirstOrDefault(r => r.name == "HuntressMesh")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = assetBundle.LoadAsset<Material>(@"Assets/Skins/MioHuntress/Assets/matMioBow.mat"),
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = false,
                            renderer = ThrowIfNull(1, "There is no renderer with the name \"BowMesh\"", renderers.FirstOrDefault(r => r.name == "BowMesh")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.bowStringMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(2, "There is no renderer with the name \"BowString\"", renderers.FirstOrDefault(r => r.name == "BowString")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(3, "There is no renderer with the name \"Quad 1\"", renderers.FirstOrDefault(r => r.name == "Quad 1")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(4, "There is no renderer with the name \"Quad 2\"", renderers.FirstOrDefault(r => r.name == "Quad 2")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(5, "There is no renderer with the name \"Quad Cluster 1\"", renderers.FirstOrDefault(r => r.name == "Quad Cluster 1")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(6, "There is no renderer with the name \"Quad Cluster 2\"", renderers.FirstOrDefault(r => r.name == "Quad Cluster 2")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(7, "There is no renderer with the name \"Quad Cluster 3\"", renderers.FirstOrDefault(r => r.name == "Quad Cluster 3")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(8, "There is no renderer with the name \"Quad Cluster 4\"", renderers.FirstOrDefault(r => r.name == "Quad Cluster 4")),
                        },
                        new CharacterModel.RendererInfo
                        {
                            defaultMaterial = Assets.arrowMat,
                            defaultShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On,
                            ignoreOverlays = true,
                            renderer = ThrowIfNull(9, "There is no renderer with the name \"Quad Cluster 5\"", renderers.FirstOrDefault(r => r.name == "Quad Cluster 5")),
                        },
                    };
                });
                TryCatchThrow("Mesh Replacements", () => {
                    skinParams.meshReplacements = new SkinDefParams.MeshReplacement[]
                    {
                        new SkinDefParams.MeshReplacement
                        {
                            mesh = assetBundle.LoadAsset<Mesh>(@"Assets\SkinMods\MioHuntress\Meshes\Body.mesh"),
                            renderer = ThrowIfNull(0, "There is no renderer with the name \"HuntressMesh\"", renderers.FirstOrDefault(r => r.name == "HuntressMesh")),
                        },
                        new SkinDefParams.MeshReplacement
                        {
                            mesh = assetBundle.LoadAsset<Mesh>(@"Assets\SkinMods\MioHuntress\Meshes\JuniperBowMesh.mesh"),
                            renderer = ThrowIfNull(1, "There is no renderer with the name \"BowMesh\"", renderers.FirstOrDefault(r => r.name == "BowMesh")),
                        },
                    };
                });
                TryCatchThrow("Light Infos", () => {
                    skinParams.lightReplacements = new CharacterModel.LightInfo[]
                    {
                    };
                });
                TryCatchThrow("Minion Skin Replacements", () => {
                    skinParams.minionSkinReplacements = Array.Empty<SkinDefParams.MinionSkinReplacement>();
                });
                TryCatchThrow("Projectile Ghost Replacements", () => {
                    skinParams.projectileGhostReplacements = Array.Empty<SkinDefParams.ProjectileGhostReplacement>();
                });

                Array.Resize(ref skinController.skins, skinController.skins.Length + 1);
                skinController.skins[skinController.skins.Length - 1] = skin;
                MwSkinEvents.Init(skin); // SIGNIFICANT DIFFERENCE #3: Initialising the EventSub object to enable MwSkinAdditions features
                HuntressBodyDefMioHuntressSkinAdded(skin, bodyPrefab);
            } catch (FieldException e) {
                if (e.InnerException is ElementException ie) {
                    InstanceLogger.LogWarning($"Failed to add \"{skinName}\" skin to \"{bodyName}\"");
                    InstanceLogger.LogWarning($"Field causing issue: {e.Message}, element: {ie.Index}");
                    InstanceLogger.LogWarning(ie.Message);
                    InstanceLogger.LogError(e.InnerException);
                } else {
                    InstanceLogger.LogWarning($"Failed to add \"{skinName}\" skin to \"{bodyName}\"");
                    InstanceLogger.LogWarning($"Field causing issue: {e.Message}");
                    InstanceLogger.LogError(e.InnerException);
                }
            } catch (Exception e) {
                InstanceLogger.LogWarning($"Failed to add \"{skinName}\" skin to \"{bodyName}\"");
                InstanceLogger.LogError(e);
            }
        }

        private static T ThrowIfEquals<T>(int index, string message, T value, T expected) where T : Enum {
            if (value.Equals(expected)) {
                throw new ElementException(index, message);
            }

            return value;
        }

        private static T ThrowIfOutOfBounds<T>(int index, string message, T[] array, int elementIndex) where T : class {
            if (array is null || array.Length <= elementIndex) {
                throw new ElementException(index, message);
            }

            return array[elementIndex];
        }

        private static T ThrowIfNull<T>(int index, string message, T value) where T : class {
            if (value is null) {
                throw new ElementException(index, message);
            }

            return value;
        }

        private static void TryCatchThrow(string message, Action action) {
            try {
                action?.Invoke();
            } catch (Exception e) {
                throw new FieldException(message, e);
            }
        }

        private static void TryAddComponent<T>(GameObject obj) where T : Component {
            if (obj && !obj.GetComponent<T>()) {
                obj.AddComponent<T>();
            }
        }

        private class FieldException : Exception {
            public FieldException(string message, Exception innerException) : base(message, innerException) { }
        }

        private class ElementException : Exception {
            public int Index { get; }
            public ElementException(int index, string message) : base(message) {
                Index = index;
            }
        }
    }
}