Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Example Implementation: MioHuntressPlugin.cs
Updated a week agoThis 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;
}
}
}
}