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.
Decompiled source of NordCastCrafting v1.0.0
BepInEx/plugins/NordCastCrafting/NordCastCrafting.dll
Decompiled 11 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Configs; using Jotunn.Entities; using Jotunn.Extensions; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Rendering; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Nord Cast Crafting")] [assembly: AssemblyDescription("Groups Nord cast recipes and offers configurable crafting stations.")] [assembly: AssemblyCompany("Andoryn")] [assembly: AssemblyProduct("Nord Cast Crafting")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: ComVisible(false)] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace NordCastCrafting { internal static class CastIcons { private static readonly Dictionary<string, string> IconResources = new Dictionary<string, string> { ["VAGold_dualaxes_cast"] = "NordCastCrafting.CustomIcons.DualAxesCastIcon.png", ["VAGold_dualaxes_mold"] = "NordCastCrafting.CustomIcons.DualAxesMoldIcon.png", ["VAknife_gold_cast"] = "NordCastCrafting.CustomIcons.DualDaggersCastIcon.png", ["VAknife_gold_mold"] = "NordCastCrafting.CustomIcons.DualDaggersMoldIcon.png", ["SwordGoldDual_cast"] = "NordCastCrafting.CustomIcons.DualSwordsCastIcon.png", ["SwordGoldDual_mold"] = "NordCastCrafting.CustomIcons.DualSwordsMoldIcon.png" }; private static readonly Dictionary<string, Sprite> LoadedIcons = new Dictionary<string, Sprite>(); internal static void Apply(IEnumerable<GameObject> itemPrefabs) { foreach (GameObject itemPrefab in itemPrefabs) { string text = ((itemPrefab != null) ? ((Object)itemPrefab).name : null); if (string.IsNullOrEmpty(text) || !IconResources.TryGetValue(text, out var value)) { continue; } ItemDrop component = itemPrefab.GetComponent<ItemDrop>(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogWarning((object)("Could not apply cast icon to " + text + ": prefab has no ItemDrop component.")); continue; } Sprite val = LoadIcon(value); if (!((Object)(object)val == (Object)null)) { component.m_itemData.m_shared.m_icons = (Sprite[])(object)new Sprite[1] { val }; Plugin.Log.LogInfo((object)("Applied custom icon to " + text + ".")); } } } private static Sprite LoadIcon(string resourceName) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) if (LoadedIcons.TryGetValue(resourceName, out var value)) { return value; } using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); if (stream == null) { Plugin.Log.LogWarning((object)("Embedded cast icon resource was not found: " + resourceName + ".")); return null; } byte[] array = new byte[stream.Length]; stream.Read(array, 0, array.Length); Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); if (!TryDecodePng(val, array)) { Object.Destroy((Object)(object)val); Plugin.Log.LogWarning((object)("Could not decode embedded cast icon: " + resourceName + ".")); return null; } ((Object)val).name = Path.GetFileNameWithoutExtension(resourceName); ((Texture)val).filterMode = (FilterMode)1; Sprite val2 = Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f), 100f); ((Object)val2).name = ((Object)val).name; LoadedIcons[resourceName] = val2; return val2; } private static bool TryDecodePng(Texture2D texture, byte[] bytes) { MethodInfo methodInfo = Type.GetType("UnityEngine.ImageConversion, UnityEngine.ImageConversionModule")?.GetMethod("LoadImage", BindingFlags.Static | BindingFlags.Public, null, new Type[3] { typeof(Texture2D), typeof(byte[]), typeof(bool) }, null); if (methodInfo != null) { return (bool)methodInfo.Invoke(null, new object[3] { texture, bytes, true }); } return false; } } internal static class DualSwordMold { internal const string MoldPrefabName = "SwordGoldDual_mold"; internal const string CastPrefabName = "SwordGoldDual_cast"; private const string SourceMoldName = "MoldSword"; private const string SourceCastName = "SwordGoldUncooked"; private const string WeaponPrefabName = "SwordGoldDual"; private const string FoundryPrefabName = "piece_FrostFoundry"; private const float FoundryCookTime = 50f; private const string CastRecipeName = "Recipe_SwordGoldDual_cast_NordCastCrafting"; private const string DualSwordsPluginGuid = "dev.alex.dualswords"; private static readonly HashSet<string> Warnings = new HashSet<string>(); private static readonly Dictionary<ObjectDB, Dictionary<Recipe, bool>> WeaponRecipeStates = new Dictionary<ObjectDB, Dictionary<Recipe, bool>>(); private static CustomItemConversion _foundryConversion; private static bool _conversionRegistered; internal static void Register() { if (HasDualSwordsMod()) { RegisterItem("SwordGoldDual_mold", "MoldSword", "Mould: Nord Dual Swords", "Filled with the right material, this mould will create a most formidable weapon."); RegisterItem("SwordGoldDual_cast", "SwordGoldUncooked", "Cast: Nord Dual Swords", "This weapon needs to be hardened with frost."); RegisterFoundryConversion(); } } private static void RegisterItem(string prefabName, string sourceName, string displayName, string description) { //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Expected O, but got Unknown //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Expected O, but got Unknown if ((Object)(object)PrefabManager.Instance.GetPrefab(prefabName) != (Object)null) { return; } GameObject prefab = PrefabManager.Instance.GetPrefab(sourceName); if ((Object)(object)prefab == (Object)null) { WarnOnce("source-" + prefabName, "Could not register " + displayName + ": source prefab " + sourceName + " is unavailable."); return; } GameObject val = PrefabManager.Instance.CreateClonedPrefab(prefabName, prefab); ItemDrop val2 = (((Object)(object)val == (Object)null) ? null : val.GetComponent<ItemDrop>()); if ((Object)(object)val2 == (Object)null) { WarnOnce("clone-" + prefabName, "Could not create " + displayName + " from " + sourceName + "."); return; } val2.m_itemData.m_dropPrefab = val; CustomItem val3 = new CustomItem(val, true, new ItemConfig { Name = displayName, Description = description }); if (!ItemManager.Instance.AddItem(val3)) { WarnOnce("register-" + prefabName, "Could not register " + displayName + " with the item manager."); return; } Plugin.Log.LogInfo((object)("Registered " + displayName + " from " + sourceName + ".")); } internal static void ApplyRecipe(ObjectDB objectDatabase) { if (!HasDualSwordsMod()) { return; } if (!IsFeatureEnabled()) { RemoveCastRecipe(objectDatabase); RemoveFoundryConversion(); RestoreDirectWeaponRecipes(objectDatabase); return; } ItemDrop item = GetItem(objectDatabase, "SwordGoldDual_mold"); ItemDrop item2 = GetItem(objectDatabase, "SwordGoldDual_cast"); ItemDrop item3 = GetItem(objectDatabase, "SwordGoldDual"); if ((Object)(object)item == (Object)null || (Object)(object)item2 == (Object)null || (Object)(object)item3 == (Object)null) { WarnOnce("items", "Could not set up the Dual Swords cast flow because its mold, cast, or weapon prefab is missing. The original weapon recipe remains available."); return; } RegisterFoundryConversion(); Recipe val = ((IEnumerable<Recipe>)objectDatabase.m_recipes).FirstOrDefault((Func<Recipe, bool>)delegate(Recipe recipe) { object obj; if (recipe == null) { obj = null; } else { ItemDrop item4 = recipe.m_item; if (item4 == null) { obj = null; } else { GameObject gameObject = ((Component)item4).gameObject; obj = ((gameObject != null) ? ((Object)gameObject).name : null); } } return (string?)obj == "SwordGoldUncooked" && !recipe.m_noCraftOnlyUpgrade; }); Recipe val2 = (((Object)(object)val == (Object)null) ? null : CreateCastRecipe(val, item2, item)); if ((Object)(object)val2 == (Object)null) { WarnOnce("cast-recipe", "Could not create the Dual Swords cast recipe from the vanilla Nord Sword cast recipe. The original weapon recipe remains available."); return; } EnsureFoundryConversion(item2, item3); HideDirectWeaponRecipes(objectDatabase); RemoveCastRecipe(objectDatabase); objectDatabase.m_recipes.Add(val2); Plugin.Log.LogInfo((object)"Added the Nord Dual Swords cast recipe and hid direct crafting while preserving weapon upgrades."); } private static Recipe CreateCastRecipe(Recipe source, ItemDrop cast, ItemDrop mold) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Expected O, but got Unknown bool flag = false; bool flag2 = false; bool flag3 = false; List<Requirement> list = new List<Requirement>(); Requirement[] array = (Requirement[])(((object)source.m_resources) ?? ((object)new Requirement[0])); foreach (Requirement val in array) { if (!((Object)(object)val?.m_resItem == (Object)null)) { string name = ((Object)((Component)val.m_resItem).gameObject).name; bool flag4 = name == "MoldSword"; bool flag5 = name == "Gold"; bool flag6 = name == "Frostwood"; flag = flag || flag4; flag2 = flag2 || flag5; flag3 = flag3 || flag6; list.Add(new Requirement { m_resItem = (flag4 ? mold : val.m_resItem), m_amount = (flag4 ? 1 : (flag5 ? 24 : (flag6 ? 12 : val.m_amount))), m_extraAmountOnlyOneIngredient = val.m_extraAmountOnlyOneIngredient, m_amountPerLevel = ((!(flag4 || flag5 || flag6)) ? val.m_amountPerLevel : 0), m_upgraderResource = val.m_upgraderResource, m_recover = val.m_recover }); } } if (!flag || !flag2 || !flag3) { return null; } Recipe obj = ScriptableObject.CreateInstance<Recipe>(); ((Object)obj).name = "Recipe_SwordGoldDual_cast_NordCastCrafting"; obj.m_item = cast; obj.m_amount = source.m_amount; obj.m_enabled = true; obj.m_qualityResultAmountMultiplier = source.m_qualityResultAmountMultiplier; obj.m_listSortWeight = source.m_listSortWeight; obj.m_noCraftOnlyUpgrade = false; obj.m_craftingStation = source.m_craftingStation; obj.m_repairStation = source.m_repairStation; obj.m_minStationLevel = source.m_minStationLevel; obj.m_requireOnlyOneIngredient = source.m_requireOnlyOneIngredient; obj.m_resources = list.ToArray(); return obj; } private static void RegisterFoundryConversion() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown if (_foundryConversion == null) { _foundryConversion = new CustomItemConversion((ConversionConfig)new CookingConversionConfig { Station = "piece_FrostFoundry", FromItem = "SwordGoldDual_cast", ToItem = "SwordGoldDual", CookTime = 50f }); } _conversionRegistered = _conversionRegistered || ItemManager.Instance.AddItemConversion(_foundryConversion); if (_conversionRegistered) { Plugin.Log.LogInfo((object)string.Format("Registered the Frost Foundry cooking conversion through Jötunn ({0} -> {1}, {2:0}s).", "SwordGoldDual_cast", "SwordGoldDual", 50f)); } else { WarnOnce("foundry-registration", "Jötunn rejected the Frost Foundry cooking conversion registration."); } } private static bool EnsureFoundryConversion(ItemDrop cast, ItemDrop weapon) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) GameObject prefab = PrefabManager.Instance.GetPrefab("piece_FrostFoundry"); CookingStation val = (((Object)(object)prefab == (Object)null) ? null : prefab.GetComponent<CookingStation>()); if ((Object)(object)val == (Object)null) { WarnOnce("foundry-cooking-station", "Frost Foundry prefab 'piece_FrostFoundry' has no CookingStation component."); return false; } if (val.m_conversion == null) { val.m_conversion = new List<ItemConversion>(); } ItemConversion val2 = ((IEnumerable<ItemConversion>)val.m_conversion).FirstOrDefault((Func<ItemConversion, bool>)delegate(ItemConversion entry) { object obj; if (entry == null) { obj = null; } else { ItemDrop obj2 = entry.m_from; if (obj2 == null) { obj = null; } else { GameObject gameObject = ((Component)obj2).gameObject; obj = ((gameObject != null) ? ((Object)gameObject).name : null); } } return (string?)obj == "SwordGoldDual_cast"; }); if (val2 != null) { val2.m_from = cast; val2.m_to = weapon; return true; } ItemConversion itemConversion = new CookingConversionConfig { Station = "piece_FrostFoundry", FromItem = "SwordGoldDual_cast", ToItem = "SwordGoldDual", CookTime = 50f }.GetItemConversion(); itemConversion.m_from = cast; itemConversion.m_to = weapon; val.m_conversion.Add(itemConversion); return true; } private static void RemoveFoundryConversion() { GameObject prefab = PrefabManager.Instance.GetPrefab("piece_FrostFoundry"); (((Object)(object)prefab == (Object)null) ? null : prefab.GetComponent<CookingStation>())?.m_conversion?.RemoveAll(delegate(ItemConversion entry) { object obj; if (entry == null) { obj = null; } else { ItemDrop obj2 = entry.m_from; if (obj2 == null) { obj = null; } else { GameObject gameObject = ((Component)obj2).gameObject; obj = ((gameObject != null) ? ((Object)gameObject).name : null); } } return (string?)obj == "SwordGoldDual_cast"; }); if (_conversionRegistered && _foundryConversion != null) { ItemManager.Instance.RemoveItemConversion(_foundryConversion); _conversionRegistered = false; } } private static void HideDirectWeaponRecipes(ObjectDB objectDatabase) { List<Recipe> list = objectDatabase.m_recipes.Where(delegate(Recipe recipe) { object obj; if (recipe == null) { obj = null; } else { ItemDrop item = recipe.m_item; if (item == null) { obj = null; } else { GameObject gameObject = ((Component)item).gameObject; obj = ((gameObject != null) ? ((Object)gameObject).name : null); } } return (string?)obj == "SwordGoldDual" && !recipe.m_noCraftOnlyUpgrade; }).ToList(); if (!WeaponRecipeStates.TryGetValue(objectDatabase, out var value)) { value = new Dictionary<Recipe, bool>(); WeaponRecipeStates[objectDatabase] = value; } foreach (Recipe item2 in list) { if (!value.ContainsKey(item2)) { value.Add(item2, item2.m_noCraftOnlyUpgrade); } item2.m_noCraftOnlyUpgrade = true; } } private static void RestoreDirectWeaponRecipes(ObjectDB objectDatabase) { if (!WeaponRecipeStates.TryGetValue(objectDatabase, out var value)) { return; } foreach (KeyValuePair<Recipe, bool> item in value) { if ((Object)(object)item.Key != (Object)null && objectDatabase.m_recipes.Contains(item.Key)) { item.Key.m_noCraftOnlyUpgrade = item.Value; } } value.Clear(); } private static void RemoveCastRecipe(ObjectDB objectDatabase) { objectDatabase.m_recipes.RemoveAll((Recipe recipe) => ((recipe != null) ? ((Object)recipe).name : null) == "Recipe_SwordGoldDual_cast_NordCastCrafting"); } private static ItemDrop GetItem(ObjectDB objectDatabase, string prefabName) { GameObject itemPrefab = objectDatabase.GetItemPrefab(prefabName); if (!((Object)(object)itemPrefab == (Object)null)) { return itemPrefab.GetComponent<ItemDrop>(); } return null; } internal static void AddMoldToLoot(DropTable table) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) if (!IsFeatureEnabled() || table?.m_drops == null || table.m_drops.Count == 0) { return; } ObjectDB instance = ObjectDB.instance; GameObject val = ((instance != null) ? instance.GetItemPrefab("SwordGoldDual_mold") : null); if (!((Object)(object)val == (Object)null) && !table.m_drops.Any(delegate(DropData drop) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) GameObject item = drop.m_item; return ((item != null) ? ((Object)item).name : null) == "SwordGoldDual_mold"; })) { DropData val2 = ((IEnumerable<DropData>)table.m_drops).FirstOrDefault((Func<DropData, bool>)delegate(DropData drop) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) GameObject item = drop.m_item; return ((item != null) ? ((Object)item).name : null) == "MoldSword" && drop.m_weight > 0f; }); if (!((Object)(object)val2.m_item == (Object)null)) { List<DropData> drops = new List<DropData>(table.m_drops) { new DropData { m_item = val, m_stackMin = val2.m_stackMin, m_stackMax = val2.m_stackMax, m_weight = val2.m_weight, m_dontScale = val2.m_dontScale } }; table.m_drops = drops; } } } internal static bool IsFeatureEnabled() { if (HasDualSwordsMod() && StationConfig.EnableDedicatedDualSwordsMold != null) { return StationConfig.EnableDedicatedDualSwordsMold.Value; } return false; } private static bool HasDualSwordsMod() { return Chainloader.PluginInfos.ContainsKey("dev.alex.dualswords"); } private static void WarnOnce(string key, string message) { if (Warnings.Add(key)) { Plugin.Log.LogWarning((object)message); } } } [HarmonyPatch(typeof(Container), "AddDefaultItems")] internal static class DualSwordMoldContainerLootPatch { [HarmonyPrefix] private static void Prefix(Container __instance) { DualSwordMold.AddMoldToLoot(__instance.m_defaultItems); } } [HarmonyPatch(typeof(DropOnDestroyed), "OnDestroyed")] internal static class DualSwordMoldDestroyedLootPatch { [HarmonyPrefix] private static void Prefix(DropOnDestroyed __instance) { DualSwordMold.AddMoldToLoot(__instance.m_dropWhenDestroyed); } } [HarmonyPatch(typeof(CookingStation), "SetSlotVisual")] internal static class DualSwordFoundryVisualPatch { [HarmonyPostfix] private static void Postfix(CookingStation __instance, object[] __args) { if (!DualSwordMold.IsFeatureEnabled() || (Object)(object)__instance == (Object)null || __args == null || __args.Length < 2) { return; } int num = ((__args[0] is int num2) ? num2 : (-1)); string text = __args[1] as string; if (text != "SwordGoldDual" || num < 0 || __instance.m_slots == null || num >= __instance.m_slots.Length) { return; } Transform val = __instance.m_slots[num]; if (!((Object)(object)val != (Object)null) || val.childCount <= 0 || !(((Object)val.GetChild(0)).name == text)) { return; } GameObject gameObject = ((Component)val.GetChild(0)).gameObject; if ((Object)(object)gameObject.GetComponent<DualSwordFoundryDisplayMarker>() != (Object)null) { return; } GameObject val2 = (((Object)(object)ObjectDB.instance == (Object)null) ? null : ObjectDB.instance.GetItemPrefab(text)); Transform val3 = (((Object)(object)val2 == (Object)null) ? null : val2.transform.Find("ground_model")); if ((Object)(object)val3 == (Object)null) { gameObject.SetActive(true); return; } GameObject obj = Object.Instantiate<GameObject>(((Component)val3).gameObject, val, false); ((Object)obj).name = text; obj.SetActive(true); obj.AddComponent<DualSwordFoundryDisplayMarker>(); Renderer[] componentsInChildren = obj.GetComponentsInChildren<Renderer>(true); for (int i = 0; i < componentsInChildren.Length; i++) { componentsInChildren[i].shadowCastingMode = (ShadowCastingMode)0; } Object.Destroy((Object)(object)gameObject); } } internal sealed class DualSwordFoundryDisplayMarker : MonoBehaviour { } [BepInPlugin("andoryn.nordcastcrafting", "Nord Cast Crafting", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "andoryn.nordcastcrafting"; public const string PluginName = "Nord Cast Crafting"; public const string PluginVersion = "1.0.0"; private Harmony _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; StationConfig.Bind(((BaseUnityPlugin)this).Config); PrefabManager.OnVanillaPrefabsAvailable += DualSwordMold.Register; SynchronizationManager.OnConfigurationSynchronized += OnConfigurationSynchronized; _harmony = new Harmony("andoryn.nordcastcrafting"); _harmony.PatchAll(); Log.LogInfo((object)"Nord Cast Crafting 1.0.0 loaded."); } private void OnConfigurationSynchronized(object sender, ConfigurationSynchronizationEventArgs args) { RecipeStationSwap.ApplyConfiguredStations(); } private void OnDestroy() { SynchronizationManager.OnConfigurationSynchronized -= OnConfigurationSynchronized; PrefabManager.OnVanillaPrefabsAvailable -= DualSwordMold.Register; Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } [HarmonyPatch(typeof(ObjectDB), "Awake")] internal static class RecipeStationSwap { private const int SortGroupCount = 16; private static readonly Dictionary<string, CraftingStation> OriginalStations = new Dictionary<string, CraftingStation>(); private static readonly Dictionary<string, int> OriginalStationLevels = new Dictionary<string, int>(); private static readonly Dictionary<string, int> OriginalSortWeights = new Dictionary<string, int>(); private static readonly HashSet<string> MissingStationWarnings = new HashSet<string>(); private static readonly Dictionary<string, int> RecipeSortGroups = new Dictionary<string, int> { ["KeysGoldUncooked"] = 0, ["ArmorGoldHeavyHelmetUncooked"] = 1, ["ArmorGoldHeavyChestUncooked"] = 2, ["ArmorGoldHeavyLegsUncooked"] = 3, ["ArmorGoldMediumHelmetUncooked"] = 4, ["ArmorGoldMediumChestUncooked"] = 5, ["ArmorGoldMediumLegsUncooked"] = 6, ["ArmorGoldMageHelmetUncooked"] = 7, ["ArmorGoldMageChestUncooked"] = 8, ["ArmorGoldMageLegsUncooked"] = 9, ["ShieldBucklerGoldUncooked"] = 10, ["ShieldRoundGoldUncooked"] = 10, ["ShieldTowerGoldUncooked"] = 10, ["StaffFrostOrbsUncooked"] = 11, ["StaffOrbofAhriUncooked"] = 11, ["StaffSpiritCallerUncooked"] = 11, ["StaffThunderbloodUncooked"] = 11, ["StaffIceShards"] = 11, ["StaffShield"] = 11, ["StaffSkeleton"] = 11, ["StaffFireball"] = 11, ["StaffLightning"] = 11, ["StaffRedTroll"] = 11, ["StaffGreenRoots"] = 11, ["StaffClusterbomb"] = 11, ["AtgeirGoldUncooked"] = 12, ["AxeGoldUncooked"] = 12, ["BattleaxeGoldUncooked"] = 12, ["BowGoldUncooked"] = 12, ["CrossbowGoldUncooked"] = 12, ["FistGoldUncooked"] = 12, ["KnifeGoldUncooked"] = 12, ["MaceGoldUncooked"] = 12, ["SledgeGoldUncooked"] = 12, ["SpearGoldUncooked"] = 12, ["SwordGoldUncooked"] = 12, ["THSwordGoldUncooked"] = 12, ["VAGold_dualaxes_cast"] = 13, ["VAknife_gold_cast"] = 14, ["SwordGoldDual_cast"] = 15 }; [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(ObjectDB __instance) { ((MonoBehaviour)__instance).StartCoroutine(ApplyAfterDatabaseSetup(__instance)); } internal static void ApplyConfiguredStations() { if ((Object)(object)ObjectDB.instance != (Object)null) { ((MonoBehaviour)ObjectDB.instance).StartCoroutine(ApplyAfterDatabaseSetup(ObjectDB.instance)); } } private static IEnumerator ApplyAfterDatabaseSetup(ObjectDB objectDatabase) { while ((Object)(object)ZNetScene.instance == (Object)null) { yield return null; } yield return null; List<Recipe> recipes = objectDatabase.m_recipes; CastIcons.Apply(objectDatabase.m_items); DualSwordMold.ApplyRecipe(objectDatabase); ApplyRecipeSortWeights(recipes); int num = 0; foreach (Recipe item2 in recipes) { object obj; if (item2 == null) { obj = null; } else { ItemDrop item = item2.m_item; if (item == null) { obj = null; } else { GameObject gameObject = ((Component)item).gameObject; obj = ((gameObject != null) ? ((Object)gameObject).name : null); } } string text = (string)obj; if (!string.IsNullOrEmpty(text) && StationConfig.TryGetSettings(text, out var station, out var level)) { RememberOriginalStation(text, item2); if (ApplyStationChoice(text, item2, station, level)) { num++; } } } Plugin.Log.LogInfo((object)$"Applied station settings to {num} supported recipes."); } private static void RememberOriginalStation(string outputName, Recipe recipe) { if (!OriginalStations.ContainsKey(outputName)) { OriginalStations[outputName] = recipe.m_craftingStation; OriginalStationLevels[outputName] = recipe.m_minStationLevel; } } private static bool ApplyStationChoice(string outputName, Recipe recipe, string choice, int configuredLevel) { if (choice == "Original") { recipe.m_craftingStation = OriginalStations[outputName]; recipe.m_minStationLevel = OriginalStationLevels[outputName]; return true; } if (!StationConfig.TryGetPrefab(choice, out var prefabName)) { return false; } ZNetScene instance = ZNetScene.instance; object obj; if (instance == null) { obj = null; } else { GameObject prefab = instance.GetPrefab(prefabName); obj = ((prefab != null) ? prefab.GetComponent<CraftingStation>() : null); } CraftingStation val = (CraftingStation)obj; if ((Object)(object)val == (Object)null) { if (MissingStationWarnings.Add(prefabName)) { Plugin.Log.LogWarning((object)("Could not find crafting station " + prefabName + "; affected recipes keep their original station.")); } RestoreOriginalStation(outputName, recipe); return false; } recipe.m_craftingStation = val; recipe.m_minStationLevel = configuredLevel; return true; } private static void RestoreOriginalStation(string outputName, Recipe recipe) { recipe.m_craftingStation = OriginalStations[outputName]; recipe.m_minStationLevel = OriginalStationLevels[outputName]; } private static void ApplyRecipeSortWeights(List<Recipe> recipes) { foreach (Recipe recipe in recipes) { object obj; if (recipe == null) { obj = null; } else { ItemDrop item = recipe.m_item; if (item == null) { obj = null; } else { GameObject gameObject = ((Component)item).gameObject; obj = ((gameObject != null) ? ((Object)gameObject).name : null); } } string text = (string)obj; if (!string.IsNullOrEmpty(text)) { if (!OriginalSortWeights.ContainsKey(text)) { OriginalSortWeights[text] = recipe.m_listSortWeight; } else { recipe.m_listSortWeight = OriginalSortWeights[text]; } } } if (StationConfig.EnableCastRecipeSorting == null || !StationConfig.EnableCastRecipeSorting.Value) { return; } int num = (from recipe in recipes where (Object)(object)recipe?.m_item != (Object)null && !RecipeSortGroups.ContainsKey(((Object)((Component)recipe.m_item).gameObject).name) select recipe.m_listSortWeight).DefaultIfEmpty(0).Min(); int num2 = ((num > -2147483632) ? (num - 16) : int.MinValue); foreach (Recipe recipe2 in recipes) { object obj2; if (recipe2 == null) { obj2 = null; } else { ItemDrop item2 = recipe2.m_item; if (item2 == null) { obj2 = null; } else { GameObject gameObject2 = ((Component)item2).gameObject; obj2 = ((gameObject2 != null) ? ((Object)gameObject2).name : null); } } string text2 = (string)obj2; if (!string.IsNullOrEmpty(text2) && RecipeSortGroups.TryGetValue(text2, out var value)) { recipe2.m_listSortWeight = num2 + value; } } } } internal static class StationConfig { private sealed class RecipeStationGroup { internal readonly string Section; internal readonly string DefaultStation; internal readonly int DefaultLevel; internal readonly string DescriptionItems; internal readonly string[] Outputs; internal ConfigEntry<string> Station; internal ConfigEntry<int> Level; internal RecipeStationGroup(string section, string defaultStation, int defaultLevel, string descriptionItems, params string[] outputs) { Section = section; DefaultStation = defaultStation; DefaultLevel = defaultLevel; DescriptionItems = descriptionItems; Outputs = outputs; } } private const string StationDescriptionFormat = "Crafting station used for the {0}."; private const string LevelDescriptionFormat = "Required crafting station level for every cast recipe in this group (1-7). Consider that not all stations have the same maximum level."; private static readonly int[] StationLevels = new int[7] { 1, 2, 3, 4, 5, 6, 7 }; private static readonly string[] StationChoices = new string[5] { "Artisan Table", "Black Forge", "Forge", "Galdr Table", "Workbench" }; private static readonly Dictionary<string, string> StationPrefabs = new Dictionary<string, string> { ["Workbench"] = "piece_workbench", ["Forge"] = "forge", ["Artisan Table"] = "piece_artisanstation", ["Black Forge"] = "blackforge", ["Galdr Table"] = "piece_magetable" }; private static readonly RecipeStationGroup[] Groups = CreateGroups(); private static readonly Dictionary<string, RecipeStationGroup> GroupsByOutput = CreateOutputMap(); internal static ConfigEntry<bool> EnableCastRecipeSorting { get; private set; } internal static ConfigEntry<bool> EnableDedicatedDualSwordsMold { get; private set; } internal static void Bind(ConfigFile config) { AcceptableValueList<string> val = new AcceptableValueList<string>(StationChoices); EnableDedicatedDualSwordsMold = ConfigFileExtensions.BindConfig<bool>(config, "10 - Dual Swords Casts (requires Dual Swords mod)", "Enable Dedicated Dual Swords Mold", true, "When enabled, this option replaces the Nord Dual Sword direct craft recipe with one that matches how other Nord weapons are created using a Mould and Cast. You first craft a Cast: Nord Dual Swords using a Mould: Nord Dual Swords (acquired from the normal mould loot sources), 24 Bloodgold, and 12 Timberwood, then harden it into the actual weapon in the Frost Foundry. This hides the Dual Swords mod's direct craft recipe but keeps its upgrade recipe.", true, (int?)null, (AcceptableValueBase)null, (Action<ConfigEntryBase>)null, (ConfigurationManagerAttributes)null); RecipeStationGroup[] groups = Groups; foreach (RecipeStationGroup recipeStationGroup in groups) { string section = recipeStationGroup.Section; string defaultStation = recipeStationGroup.DefaultStation; string text = $"Crafting station used for the {recipeStationGroup.DescriptionItems}."; AcceptableValueBase val2 = (AcceptableValueBase)(object)val; recipeStationGroup.Station = ConfigFileExtensions.BindConfig<string>(config, section, "Crafting Station", defaultStation, text, true, (int?)null, val2, (Action<ConfigEntryBase>)null, (ConfigurationManagerAttributes)null); string section2 = recipeStationGroup.Section; int defaultLevel = recipeStationGroup.DefaultLevel; val2 = (AcceptableValueBase)(object)new AcceptableValueList<int>(StationLevels); recipeStationGroup.Level = ConfigFileExtensions.BindConfig<int>(config, section2, "Required Station Level", defaultLevel, "Required crafting station level for every cast recipe in this group (1-7). Consider that not all stations have the same maximum level.", true, (int?)null, val2, (Action<ConfigEntryBase>)null, (ConfigurationManagerAttributes)null); } EnableCastRecipeSorting = ConfigFileExtensions.BindConfig<bool>(config, "01 - Cast Recipe Sorting", "Enable Cast Recipe Sorting", true, "When enabled, groups the cast recipes together at the top of the crafting bench's recipe list.", true, (int?)null, (AcceptableValueBase)null, (Action<ConfigEntryBase>)null, (ConfigurationManagerAttributes)null); config.Save(); } internal static bool TryGetSettings(string outputPrefab, out string station, out int level) { if (GroupsByOutput.TryGetValue(outputPrefab, out var value) && value.Station != null && value.Level != null) { if (value.Station.Value == value.DefaultStation && value.Level.Value == value.DefaultLevel) { station = "Original"; level = 0; return true; } station = value.Station.Value; level = value.Level.Value; return true; } station = "Original"; level = 0; return false; } internal static bool TryGetPrefab(string choice, out string prefabName) { return StationPrefabs.TryGetValue(choice, out prefabName); } private static RecipeStationGroup[] CreateGroups() { return new RecipeStationGroup[9] { new RecipeStationGroup("02 - Intricate Key Cast", "Black Forge", 4, "Intricate Key cast", "KeysGoldUncooked"), new RecipeStationGroup("03 - Heavy Armor Casts", "Black Forge", 4, "Breastplate, Trousers, and Helmet of the Protector casts", "ArmorGoldHeavyChestUncooked", "ArmorGoldHeavyLegsUncooked", "ArmorGoldHeavyHelmetUncooked"), new RecipeStationGroup("04 - Medium Armor Casts", "Black Forge", 4, "Chestpiece, Trousers, and Hood of the Vanguard casts", "ArmorGoldMediumChestUncooked", "ArmorGoldMediumLegsUncooked", "ArmorGoldMediumHelmetUncooked"), new RecipeStationGroup("05 - Caster Armor Casts", "Galdr Table", 4, "Robes, Trousers, and Headdress of the Caller casts", "ArmorGoldMageChestUncooked", "ArmorGoldMageLegsUncooked", "ArmorGoldMageHelmetUncooked"), new RecipeStationGroup("06 - Shield Casts", "Black Forge", 4, "Buckler, Round, and Tower Shield casts", "ShieldBucklerGoldUncooked", "ShieldRoundGoldUncooked", "ShieldTowerGoldUncooked"), new RecipeStationGroup("07 - Melee Weapon Casts", "Black Forge", 4, "Atgeir, Axe, Bow, Crossbow, Dagger, Greataxe, Greatsword, Knucklechains, Mace, Sledge, Spear, and Sword casts", "AtgeirGoldUncooked", "AxeGoldUncooked", "BowGoldUncooked", "CrossbowGoldUncooked", "KnifeGoldUncooked", "BattleaxeGoldUncooked", "THSwordGoldUncooked", "FistGoldUncooked", "MaceGoldUncooked", "SledgeGoldUncooked", "SpearGoldUncooked", "SwordGoldUncooked"), new RecipeStationGroup("08 - Magic Weapon Casts", "Galdr Table", 4, "Northern Vengeance, Echo Spike, Spirit Caller, and Lightning Strike casts", "StaffFrostOrbsUncooked", "StaffOrbofAhriUncooked", "StaffSpiritCallerUncooked", "StaffThunderbloodUncooked"), new RecipeStationGroup("09 - Valheim Armory Casts (requires Valheim Armory mod)", "Black Forge", 4, "Nord Dualaxes and Nord Daggers cast recipes", "VAGold_dualaxes_cast", "VAknife_gold_cast"), new RecipeStationGroup("10 - Dual Swords Casts (requires Dual Swords mod)", "Black Forge", 4, "Dual Swords cast", "SwordGoldDual_cast") }; } private static Dictionary<string, RecipeStationGroup> CreateOutputMap() { Dictionary<string, RecipeStationGroup> dictionary = new Dictionary<string, RecipeStationGroup>(); RecipeStationGroup[] groups = Groups; foreach (RecipeStationGroup recipeStationGroup in groups) { string[] outputs = recipeStationGroup.Outputs; foreach (string key in outputs) { dictionary[key] = recipeStationGroup; } } return dictionary; } } }