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 Gravegoods v0.1.1
plugins/Gravegoods.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using Gravegoods.Commands; using Gravegoods.Compat; using Gravegoods.Config; using Gravegoods.Diagnostics; using Gravegoods.Loot; using Gravegoods.Net; using HarmonyLib; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")] [assembly: IgnoresAccessChecksTo("assembly_guiutils")] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: AssemblyCompany("RAGEmedia")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("World chests get a small chance of biome-appropriate resources, ammo, and gear.")] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyInformationalVersion("0.1.1+e31b984e24372d7e13059d92fb804ec8d0bf67eb")] [assembly: AssemblyProduct("Gravegoods")] [assembly: AssemblyTitle("Gravegoods")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.1.0")] [module: UnverifiableCode] [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 Gravegoods { [BepInPlugin("com.ragemedia.gravegoods", "Gravegoods", "0.1.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public class GravegoodsPlugin : BaseUnityPlugin { public const string PluginGuid = "com.ragemedia.gravegoods"; public const string PluginName = "Gravegoods"; public const string PluginVersion = "0.1.1"; internal static GravegoodsPlugin Instance; private readonly Harmony _harmony = new Harmony("com.ragemedia.gravegoods"); private void Awake() { Instance = this; GravegoodsConfig.Bind(((BaseUnityPlugin)this).Config); LootTables.LoadLocal(); LootTableSync.Register(); _harmony.PatchAll(Assembly.GetExecutingAssembly()); VerifyPatches(); RegeneratingWorldCompat.TryPatch(_harmony); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new GravegoodsCommand()); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void VerifyPatches() { List<MethodBase> list = _harmony.GetPatchedMethods().ToList(); LogInfo($"Harmony bound {list.Count} target(s):"); foreach (MethodBase item in list) { LogInfo(" bound " + item.DeclaringType?.FullName + "." + item.Name); } int num = 0; foreach (Type item2 in from t in Assembly.GetExecutingAssembly().GetTypes() where t.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Any() select t) { List<HarmonyMethod> list2 = (from HarmonyPatch a in item2.GetCustomAttributes(typeof(HarmonyPatch), inherit: true) select ((HarmonyAttribute)a).info).ToList(); HarmonyMethod val; try { val = HarmonyMethod.Merge(list2); } catch (Exception ex) { LogError(" UNRESOLVED " + item2.Name + ": could not merge patch attributes: " + ex.Message); num++; continue; } if (!(val?.declaringType == null) && !string.IsNullOrEmpty(val.methodName) && AccessTools.Method(val.declaringType, val.methodName, val.argumentTypes, (Type[])null) == null) { num++; LogError(" UNRESOLVED " + item2.Name + " -> " + val.declaringType.Name + "." + val.methodName + " does not exist. This patch will never run. The game version likely changed; re-check the method against the decompiled source."); } } if (num > 0) { LogError(string.Format("{0}: {1} patch target(s) failed to resolve. ", "Gravegoods", num) + "This mod is loaded but not fully functional."); } else if (list.Count == 0) { LogWarning("Gravegoods: loaded, but bound zero patches. Expected if this mod only adds content via Jotunn."); } else { LogInfo("Gravegoods v0.1.1 ready."); } } internal static void LogInfo(string msg) { ((BaseUnityPlugin)Instance).Logger.LogInfo((object)("[Gravegoods] " + msg)); } internal static void LogWarning(string msg) { ((BaseUnityPlugin)Instance).Logger.LogWarning((object)("[Gravegoods] " + msg)); } internal static void LogError(string msg) { ((BaseUnityPlugin)Instance).Logger.LogError((object)("[Gravegoods] " + msg)); } internal static void LogVerbose(string msg) { if (GravegoodsConfig.VerboseLogging != null && GravegoodsConfig.VerboseLogging.Value) { ((BaseUnityPlugin)Instance).Logger.LogInfo((object)("[Gravegoods] " + msg)); } } } } namespace Gravegoods.Patches { [HarmonyPatch(typeof(Container), "AddDefaultItems")] internal static class AddDefaultItemsPatch { private static void Postfix(Container __instance) { try { ChestLoot.OnDefaultItemsAdded(__instance); } catch (Exception arg) { GravegoodsPlugin.LogError($"Bonus roll failed on {((__instance != null) ? ((Object)__instance).name : null)}: {arg}"); } } } [HarmonyPatch(typeof(Container), "RPC_RequestOpen")] internal static class RequestOpenPatch { [HarmonyPriority(800)] private static void Prefix(Container __instance, long playerID) { try { if (__instance.CheckAccess(playerID)) { ChestLoot.OnOpenRequested(__instance); } } catch (Exception arg) { GravegoodsPlugin.LogError($"Legacy roll failed on {((__instance != null) ? ((Object)__instance).name : null)}: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class SessionStartPatch { private static void Postfix() { ChestLoot.ClearPending(); LootTables.BeginWaitingForServer(); } } } namespace Gravegoods.Net { internal static class LootTableSync { [CompilerGenerated] private static class <>O { public static CoroutineHandler <0>__OnServerReceive; public static CoroutineHandler <1>__OnClientReceive; public static Func<ZPackage> <2>__BuildPackage; } private static CustomRPC _rpc; internal static void Register() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown NetworkManager instance = NetworkManager.Instance; object obj = <>O.<0>__OnServerReceive; if (obj == null) { CoroutineHandler val = OnServerReceive; <>O.<0>__OnServerReceive = val; obj = (object)val; } object obj2 = <>O.<1>__OnClientReceive; if (obj2 == null) { CoroutineHandler val2 = OnClientReceive; <>O.<1>__OnClientReceive = val2; obj2 = (object)val2; } _rpc = instance.AddRPC("Gravegoods_LootTable", (CoroutineHandler)obj, (CoroutineHandler)obj2); SynchronizationManager.Instance.AddInitialSynchronization(_rpc, (Func<ZPackage>)BuildPackage); } private static ZPackage BuildPackage() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(LootTables.LocalText); return val; } private static IEnumerator OnServerReceive(long sender, ZPackage package) { yield break; } private static IEnumerator OnClientReceive(long sender, ZPackage package) { LootTables.ReceiveServerCopy(package.ReadString()); ChestLoot.ProcessPending(); yield break; } } } namespace Gravegoods.Loot { internal static class ChestLoot { internal static readonly int RolledKey = StringExtensionMethods.GetStableHashCode("gravegoods.rolled"); internal static readonly int BonusKey = StringExtensionMethods.GetStableHashCode("gravegoods.bonus"); internal const string LegacyGlobalKey = "gravegoods_legacy"; private static readonly List<Container> Pending = new List<Container>(); internal static bool LegacyEnabled { get { if ((Object)(object)ZoneSystem.instance != (Object)null) { return ZoneSystem.instance.GetGlobalKey("gravegoods_legacy"); } return false; } } internal static bool IsWorldChest(Container container) { if (container.m_defaultItems?.m_drops == null || container.m_defaultItems.m_drops.Count == 0) { return false; } if ((Object)(object)container.m_piece != (Object)null && container.m_piece.IsPlacedByPlayer()) { return false; } return !GravegoodsConfig.IsExcludedContainer(Utils.GetPrefabName(((Component)container).gameObject)); } internal static void OnDefaultItemsAdded(Container container) { if (!GravegoodsConfig.Enabled.Value) { return; } ZDO val = OwnedZdo(container); if (val != null && !val.GetBool(RolledKey, false) && IsWorldChest(container)) { if (!LootTables.IsReady) { Pending.Add(container); GravegoodsPlugin.LogVerbose("Deferred " + ((Object)container).name + ": waiting for server loot tables."); } else { Roll(container, val, "new"); } } } internal static void OnOpenRequested(Container container) { if (GravegoodsConfig.Enabled.Value && LegacyEnabled && LootTables.IsReady) { ZDO val = OwnedZdo(container); if (val != null && !val.GetBool(RolledKey, false) && val.GetBool(ZDOVars.s_addedDefaultItems, false) && IsWorldChest(container)) { Roll(container, val, "legacy"); } } } internal static void ProcessPending() { Container[] array = Pending.ToArray(); Pending.Clear(); Container[] array2 = array; foreach (Container val in array2) { if (!((Object)(object)val == (Object)null)) { ZDO val2 = OwnedZdo(val); if (val2 != null && !val2.GetBool(RolledKey, false)) { Roll(val, val2, "deferred"); } } } } internal static void ClearPending() { Pending.Clear(); } private static ZDO OwnedZdo(Container container) { ZNetView val = (((Object)(object)container != (Object)null) ? container.m_nview : null); if ((Object)(object)val == (Object)null || !val.IsValid() || !val.IsOwner()) { return null; } return val.GetZDO(); } private static void Roll(Container container, ZDO zdo, string why) { //IL_0012: 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_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0028: 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_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: 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_022e: Unknown result type (might be due to invalid IL or missing references) zdo.Set(RolledKey, true); Vector3 position = ((Component)container).transform.position; Biome val = (Biome)((WorldGenerator.instance != null) ? ((int)WorldGenerator.instance.GetBiome(position)) : 0); if (!Tiers.TryFromBiome(val, out var tier)) { GravegoodsPlugin.LogVerbose($"{((Object)container).name} ({why}) in {val}: no rung, no bonus roll."); return; } RollResult rollResult; try { rollResult = LootRoller.Roll(tier); } catch (Exception arg) { GravegoodsPlugin.LogError($"Roll failed for {((Object)container).name}: {arg}"); return; } foreach (string miss in rollResult.Misses) { GravegoodsPlugin.LogWarning($"{((Object)container).name} in {val}: {miss}"); } if (rollResult.Items.Count == 0) { GravegoodsPlugin.LogVerbose($"{((Object)container).name} ({why}) in {val}: nothing this time."); return; } bool before = RegeneratingWorldCompat.ReadDeposit(zdo); bool cheated = zdo.GetBool(ZDOVars.s_cheated, false) && !PlayerProfile.s_bypassCheatChecks; Inventory inventory = container.GetInventory(); List<string> list = new List<string>(); foreach (ItemData item in rollResult.Items) { item.m_cheated = cheated; if (inventory.AddItem(item)) { list.Add(((Object)item.m_dropPrefab).name); } else { GravegoodsPlugin.LogVerbose(((Object)container).name + ": no room for " + ((Object)item.m_dropPrefab).name + "."); } } if (list.Count > 0) { string text = zdo.GetString(BonusKey, ""); List<string> list2 = ((text.Length == 0) ? list : text.Split(new char[1] { ',' }).Concat(list).ToList()); zdo.Set(BonusKey, string.Join(",", list2.ToArray())); } RegeneratingWorldCompat.RestoreDeposit(zdo, before); GravegoodsPlugin.LogVerbose($"{((Object)container).name} ({why}) in {val}, rung {tier}: " + string.Join("; ", rollResult.Notes.ToArray())); } } internal static class DefaultLootFile { internal const string Text = "# Gravegoods loot tables\n#\n# World chests roll three independent bonus chances (resources, ammo, gear). The chances,\n# quality odds and durability are in com.ragemedia.gravegoods.cfg. This file decides WHAT\n# can drop. On a server, the server's copy is sent to every client; restart the server\n# after editing it.\n#\n# After a world loads, BepInEx/config/Gravegoods/report.md lists every pool this file\n# produced, every item left out and why, and every name below that did not resolve.\n#\n# Biome names: Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, Ashlands, DeepNorth\n# Everything after a # is a comment.\n\n\n# ---------------------------------------------------------------------------------------\n# [materials]\n# The rung each crafting material belongs to. Weapons, armor and ammo are not listed by\n# hand: each one is placed at the rung of its highest-rung ingredient, so ValheimArmory\n# and other content mods are picked up automatically. Intermediate items (Bronze,\n# BronzeNails, ...) resolve through their own recipes if they are not listed.\n# An item with any ingredient that is neither listed here nor craftable stays OUT of the\n# pools, and the report names the ingredient. Add it here to let those items drop.\n# ---------------------------------------------------------------------------------------\n[materials]\nMeadows: Wood, Stone, Flint, Resin, LeatherScraps, DeerHide, Feathers, Raspberry, Honey\nMeadows: Mushroom, Dandelion, NeckTail, TrophyDeer, TrophyBoar, TrophyNeck\n\nBlackForest: FineWood, RoundLog, CopperOre, Copper, CopperScrap, TinOre, Tin, Bronze\nBlackForest: BronzeScrap, BronzeNails, GreydwarfEye, TrollHide, SurtlingCore, BoneFragments\nBlackForest: Thistle, Blueberries, AncientSeed, Carrot, CarrotSeeds, MushroomYellow, Amber\nBlackForest: Ruby, TrophyGreydwarf, TrophyGreydwarfBrute, TrophyGreydwarfShaman\nBlackForest: TrophySkeleton, TrophySkeletonPoison, TrophyFrostTroll, Coal\nBlackForest: BjornHide, BjornPaw, TrophyBjorn\n\nSwamp: Iron, IronOre, IronScrap, IronNails, ElderBark, Guck, Ooze, Entrails, WitheredBone\nSwamp: Chain, Bloodbag, Turnip, TurnipSeeds, AmberPearl, Chitin, SerpentScale\nSwamp: TrophyBlob, TrophyDraugr, TrophyDraugrElite, TrophyLeech, TrophySurtling\nSwamp: TrophyAbomination, TrophyWraith, TrophySerpent, Root\n\nMountain: Silver, SilverOre, Obsidian, FreezeGland, WolfPelt, WolfFang, WolfClaw, Crystal\nMountain: JuteRed, YmirRemains, Onion, OnionSeeds, TrophyWolf, TrophyFenring, TrophyUlv\nMountain: TrophyCultist, TrophyHatchling, TrophySGolem, WolfHairBundle\n\nPlains: BlackMetal, BlackMetalScrap, LinenThread, Flax, Barley, BarleyFlour, Needle\nPlains: LoxPelt, Tar, GoblinTotem, Thunderstone, Cloudberry, TrophyDeathsquito\nPlains: TrophyGoblin, TrophyGoblinBrute, TrophyGoblinShaman, TrophyLox, TrophyGrowth\n\nMistlands: YggdrasilWood, BlackMarble, Eitr, Sap, Softtissue, Carapace, ScaleHide, JuteBlue\nMistlands: Mandible, Bilebag, BlackCore, DvergrNeedle, Wisp, RoyalJelly, MushroomJotunPuffs\nMistlands: MushroomMagecap, TrophyDvergr, TrophySeeker, TrophySeekerBrute, TrophyTick\nMistlands: TrophyGjall, TrophyHare\n\nAshlands: FlametalNew, FlametalOreNew, Flametal, FlametalOre, Blackwood, Grausten\nAshlands: CharredBone, Charredskull, AskHide, AskBladder, MorgenSinew, MorgenHeart\nAshlands: CelestialFeather, GemstoneRed, GemstoneBlue, GemstoneGreen, MoltenCore\nAshlands: ProustitePowder, CharredCogwheel, MechanicalSpring, Ironpit, Pukeberries\nAshlands: Vineberry, VineberrySeeds, Fiddleheadfern, TrophyAsksvin, TrophyMorgen\nAshlands: TrophyCharredMelee, TrophyCharredArcher, TrophyCharredMage, TrophyFallenValkyrie\nAshlands: TrophyVolture, BellFragment, CharcoalResin, BonemawSerpentTooth, SulfurStone\n\nDeepNorth: Gold, GoldOre, MooseHide, MooseSinew, SealHide, Frostwood, NornThread\n# Bloodgold gear is cast from a mold per weapon or armor type.\nDeepNorth: MoldSword, MoldSword2H, MoldAxe, MoldAxe2H, MoldMace, MoldMace2H, MoldKnife\nDeepNorth: MoldSpear, MoldAtgeir, MoldBow, MoldCrossbow, MoldFistweapon\nDeepNorth: MoldShieldRound, MoldShieldBuckler, MoldShieldTower\nDeepNorth: MoldArmorGoldHelmet, MoldArmorGoldChest, MoldArmorGoldLegs\nDeepNorth: MoldArmorMediumHelmet, MoldArmormediumChest, MoldArmorMediumLegs\nDeepNorth: MoldArmorMageHelmet, MoldArmorMageChest, MoldArmorMageLegs\n\n# Valheim 2.0 materials whose biome is not confirmed are placed HIGH on purpose: a rung too\n# high only makes an item rarer, a rung too low hands it out early. Move them down once known.\nPlains: TrophyBjornUndead, UndeadBjornRibcage, WrithanRoots\nMistlands: GiantBloodSack\nDeepNorth: Ice, BarkaBranch\n\n\n# ---------------------------------------------------------------------------------------\n# [boss materials]\n# Boss trophies and boss drops. Anything whose every recipe needs one of these stays out\n# of the pools: these are the progression gates, and a chest should never hand them over.\n# ---------------------------------------------------------------------------------------\n[boss materials]\nTrophyEikthyr, HardAntler\nTrophyTheElder, CryptKey\nTrophyBonemass, Wishbone\nTrophyDragonQueen, DragonTear\nTrophyGoblinKing, YagluthDrop\nTrophySeekerQueen, QueenDrop\nTrophyFader, FaderDrop, FaderEmber\n# Unique exploration rewards, not farmable materials.\nDyrnwynHiltFragment, CrownJewel\n\n\n# ---------------------------------------------------------------------------------------\n# [never]\n# Items that never appear, however they classify. One or more names per line.\n# ---------------------------------------------------------------------------------------\n[never]\n# Novelty and tool items that happen to use a weapon or armor slot.\nTankard, Tankard_dvergr, GrapplingHook, Scythe, Shovel, HelmetFishingHat, AxeEarly\n\n\n# ---------------------------------------------------------------------------------------\n# [overrides]\n# Force an item into a rung, skipping recipe classification. This also admits items that\n# have no recipe at all (trader stock, drops). Boss materials are NOT checked here.\n# SwordIron: Swamp\n# ---------------------------------------------------------------------------------------\n[overrides]\n\n\n# ---------------------------------------------------------------------------------------\n# [resources <Biome>]\n# What a resource roll can give, per biome. Item min-max [weight, default 1]\n# ---------------------------------------------------------------------------------------\n[resources Meadows]\nFlint 3-6\nResin 3-6\nLeatherScraps 2-5\nDeerHide 2-3\nHoney 2-4\nNeckTail 2-4\nCoins 5-15 0.5\n\n[resources BlackForest]\nGreydwarfEye 3-6\nResin 4-8\nThistle 2-4\nTrollHide 1-2 0.6\nSurtlingCore 1-2 0.6\nCopperOre 2-4 0.6\nTinOre 2-4 0.6\nAncientSeed 1 0.5\nAmber 1 0.4\nRuby 1 0.3\nCoins 10-30 0.7\n\n[resources Swamp]\nGuck 2-4\nEntrails 3-6\nOoze 2-4\nElderBark 3-6\nWitheredBone 1-2 0.6\nIronScrap 2-4 0.6\nBloodbag 2-3 0.6\nAmberPearl 1 0.4\nCoins 20-40 0.7\n\n[resources Mountain]\nFreezeGland 2-4\nWolfPelt 2-3\nWolfFang 2-4\nObsidian 3-6\nOnion 2-4\nCrystal 1-2 0.5\nSilverOre 2-3 0.5\nRuby 1 0.4\nCoins 20-50 0.7\n\n[resources Plains]\nBarley 3-6\nFlax 3-6\nNeedle 3-6\nCloudberry 3-6\nTar 2-4 0.7\nLoxPelt 1-2 0.6\nBlackMetalScrap 2-4 0.5\nGoblinTotem 1 0.3\nCoins 30-60 0.7\n\n[resources Mistlands]\nSofttissue 2-4\nBlackMarble 3-6\nMushroomJotunPuffs 3-6\nMushroomMagecap 3-6\nCarapace 2-4 0.7\nScaleHide 1-2 0.6\nMandible 1-2 0.5\nBilebag 1 0.4\nCoins 40-80 0.7\n\n[resources Ashlands]\nGrausten 3-6\nBlackwood 3-6\nCharredBone 3-6\nVineberry 3-5\nAskHide 1-2 0.6\nCelestialFeather 1-2 0.5\nFlametalOreNew 2-3 0.5\nGemstoneRed 1 0.2\nGemstoneBlue 1 0.2\nGemstoneGreen 1 0.2\nCoins 50-100 0.7\n\n[resources DeepNorth]\nFrostwood 3-6\nMooseHide 1-2 0.6\nSealHide 1-2 0.6\nCoins 60-120 0.7\n"; } internal sealed class ResourceEntry { internal string Prefab; internal int Min; internal int Max; internal float Weight; } internal sealed class LootFile { internal readonly Dictionary<string, Tier> Materials = new Dictionary<string, Tier>(StringComparer.Ordinal); internal readonly HashSet<string> BossMaterials = new HashSet<string>(StringComparer.Ordinal); internal readonly HashSet<string> Never = new HashSet<string>(StringComparer.Ordinal); internal readonly Dictionary<string, Tier> Overrides = new Dictionary<string, Tier>(StringComparer.Ordinal); internal readonly List<ResourceEntry>[] Resources = NewTierLists(); internal readonly List<string> Warnings = new List<string>(); private static List<ResourceEntry>[] NewTierLists() { List<ResourceEntry>[] array = new List<ResourceEntry>[8]; for (int i = 0; i < array.Length; i++) { array[i] = new List<ResourceEntry>(); } return array; } internal static LootFile Parse(string text) { LootFile lootFile = new LootFile(); if (string.IsNullOrEmpty(text)) { lootFile.Warnings.Add("loot.txt is empty; no pools will be built."); return lootFile; } string text2 = ""; Tier tier = Tier.Meadows; string[] array = text.Replace("\r", "").Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { string text3 = StripComment(array[i]).Trim(); if (text3.Length == 0) { continue; } string text4 = $"loot.txt line {i + 1}"; if (text3.StartsWith("[") && text3.EndsWith("]")) { text2 = text3.Substring(1, text3.Length - 2).Trim().ToLowerInvariant(); if (text2.StartsWith("resources")) { string text5 = text2.Substring("resources".Length).Trim(); if (!Tiers.TryParse(text5, out tier)) { lootFile.Warnings.Add(text4 + ": unknown biome '" + text5 + "' in section header."); text2 = "ignored"; } else { text2 = "resources"; } } continue; } switch (text2) { case "materials": ParseTierList(lootFile, text3, text4); break; case "boss materials": foreach (string item in SplitNames(text3)) { lootFile.BossMaterials.Add(item); } break; case "never": foreach (string item2 in SplitNames(text3)) { lootFile.Never.Add(item2); } break; case "overrides": ParseOverride(lootFile, text3, text4); break; case "resources": ParseResource(lootFile, tier, text3, text4); break; default: lootFile.Warnings.Add(text4 + ": line outside any known section, ignored."); break; case "ignored": break; } } return lootFile; } private static string StripComment(string line) { int num = line.IndexOf('#'); if (num >= 0) { return line.Substring(0, num); } return line; } private static IEnumerable<string> SplitNames(string text) { string[] array = text.Split(new char[3] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text2 in array) { yield return text2.Trim(); } } private static void ParseTierList(LootFile file, string line, string where) { int num = line.IndexOf(':'); if (num < 0 || !Tiers.TryParse(line.Substring(0, num), out var tier)) { file.Warnings.Add(where + ": expected 'Biome: Item, Item, ...'."); return; } foreach (string item in SplitNames(line.Substring(num + 1))) { if (file.Materials.TryGetValue(item, out var value) && value != tier) { file.Warnings.Add($"{where}: {item} is listed under both {value} and {tier}; using {tier}."); } file.Materials[item] = tier; } } private static void ParseOverride(LootFile file, string line, string where) { int num = line.IndexOf(':'); if (num < 0 || !Tiers.TryParse(line.Substring(num + 1), out var tier)) { file.Warnings.Add(where + ": expected 'Item: Biome'."); } else { file.Overrides[line.Substring(0, num).Trim()] = tier; } } private static void ParseResource(LootFile file, Tier tier, string line, string where) { string[] array = line.Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 2 || array.Length > 3) { file.Warnings.Add(where + ": expected 'Item min-max [weight]'."); return; } int num = array[1].IndexOf('-'); int result; int result2; if (num < 0) { if (!int.TryParse(array[1], out result)) { file.Warnings.Add(where + ": '" + array[1] + "' is not an amount."); return; } result2 = result; } else if (!int.TryParse(array[1].Substring(0, num), out result) || !int.TryParse(array[1].Substring(num + 1), out result2)) { file.Warnings.Add(where + ": '" + array[1] + "' is not a range like 2-4."); return; } float result3 = 1f; if (array.Length == 3 && !float.TryParse(array[2], NumberStyles.Float, CultureInfo.InvariantCulture, out result3)) { file.Warnings.Add(where + ": '" + array[2] + "' is not a weight."); return; } if (result < 1 || result2 < result || result3 <= 0f) { file.Warnings.Add(where + ": amounts must be at least 1, max >= min, and weight above 0."); return; } file.Resources[(int)tier].Add(new ResourceEntry { Prefab = array[0], Min = result, Max = result2, Weight = result3 }); } } internal enum PoolKind { Gear, Ammo } internal sealed class PoolItem { internal string Prefab; internal GameObject Go; internal Tier Tier; internal PoolKind Kind; internal string Because; } internal sealed class Exclusion { internal string Prefab; internal PoolKind Kind; internal string Reason; } internal sealed class LootPools { private sealed class Classifier { private sealed class Resolution { internal bool Known; internal Tier Tier; internal string Because; } private readonly LootFile _file; private readonly Dictionary<string, List<Recipe>> _recipes = new Dictionary<string, List<Recipe>>(StringComparer.Ordinal); private readonly Dictionary<string, Resolution> _memo = new Dictionary<string, Resolution>(StringComparer.Ordinal); private readonly Dictionary<string, string> _gateMemo = new Dictionary<string, string>(StringComparer.Ordinal); private readonly HashSet<string> _visiting = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<string> _gateVisiting = new HashSet<string>(StringComparer.Ordinal); internal readonly HashSet<string> Unknown = new HashSet<string>(StringComparer.Ordinal); internal Classifier(LootFile file, ObjectDB db) { _file = file; foreach (Recipe recipe in db.m_recipes) { if (!((Object)(object)recipe == (Object)null) && recipe.m_enabled && !((Object)(object)recipe.m_item == (Object)null)) { string name = ((Object)((Component)recipe.m_item).gameObject).name; if (!_recipes.TryGetValue(name, out var value)) { value = (_recipes[name] = new List<Recipe>()); } value.Add(recipe); } } } internal bool HasRecipe(string name) { return _recipes.ContainsKey(name); } internal string BossGate(string name) { if (_file.BossMaterials.Contains(name)) { return name; } if (_gateMemo.TryGetValue(name, out var value)) { return value; } if (!_recipes.TryGetValue(name, out var value2) || !_gateVisiting.Add(name)) { return null; } string text = null; foreach (Recipe item in value2) { string text2 = RecipeGate(item); if (text2 == null) { text = null; break; } text = text ?? text2; } _gateVisiting.Remove(name); _gateMemo[name] = text; return text; } private static bool NeededToCraft(Requirement req) { if ((Object)(object)req?.m_resItem != (Object)null && !req.m_upgraderResource) { return req.m_amount > 0; } return false; } private string RecipeGate(Recipe recipe) { Requirement[] resources = recipe.m_resources; foreach (Requirement val in resources) { if (NeededToCraft(val)) { string text = BossGate(((Object)((Component)val.m_resItem).gameObject).name); if (text != null) { return text; } } } return null; } internal bool TryResolve(string name, out Tier tier, out string because) { Resolution resolution = Resolve(name); tier = resolution.Tier; because = resolution.Because; return resolution.Known; } private Resolution Resolve(string name) { if (_file.Materials.TryGetValue(name, out var value)) { return new Resolution { Known = true, Tier = value, Because = $"{name} ({value})" }; } if (_memo.TryGetValue(name, out var value2)) { return value2; } Resolution resolution = new Resolution { Because = name }; if (!_recipes.TryGetValue(name, out var value3)) { Unknown.Add(name); return resolution; } if (!_visiting.Add(name)) { return resolution; } string text = null; foreach (Recipe item in value3) { if (RecipeGate(item) == null) { Resolution resolution2 = ResolveRecipe(item); if (!resolution2.Known) { text = text ?? resolution2.Because; } else if (!resolution.Known || resolution2.Tier < resolution.Tier) { resolution = resolution2; } } } if (!resolution.Known && text != null) { resolution.Because = text; } _visiting.Remove(name); _memo[name] = resolution; return resolution; } private Resolution ResolveRecipe(Recipe recipe) { Resolution resolution = null; Requirement[] resources = recipe.m_resources; foreach (Requirement val in resources) { if (NeededToCraft(val)) { Resolution resolution2 = Resolve(((Object)((Component)val.m_resItem).gameObject).name); if (!resolution2.Known) { return new Resolution { Because = resolution2.Because }; } if (resolution == null || resolution2.Tier > resolution.Tier) { resolution = resolution2; } } } return resolution ?? new Resolution { Because = "(recipe has no ingredients)" }; } } internal readonly List<PoolItem>[] Gear = NewLists(); internal readonly List<PoolItem>[] Ammo = NewLists(); internal readonly List<Exclusion> Excluded = new List<Exclusion>(); internal readonly SortedSet<string> UnknownMaterials = new SortedSet<string>(StringComparer.Ordinal); internal readonly SortedSet<string> MissingNames = new SortedSet<string>(StringComparer.Ordinal); internal int ItemCount; private static LootPools _cached; private static int _cachedRevision = -1; private static ObjectDB _cachedDb; private static int _cachedRecipeCount; private static readonly HashSet<ItemType> GearTypes = new HashSet<ItemType> { (ItemType)3, (ItemType)14, (ItemType)22, (ItemType)4, (ItemType)5, (ItemType)6, (ItemType)7, (ItemType)11, (ItemType)12, (ItemType)17 }; private static List<PoolItem>[] NewLists() { List<PoolItem>[] array = new List<PoolItem>[8]; for (int i = 0; i < array.Length; i++) { array[i] = new List<PoolItem>(); } return array; } internal List<PoolItem> AtOrBelow(PoolKind kind, Tier tier, out Tier actual) { List<PoolItem>[] array = ((kind == PoolKind.Gear) ? Gear : Ammo); for (int num = (int)tier; num >= 0; num--) { if (array[num].Count > 0) { actual = (Tier)num; return array[num]; } } actual = tier; return null; } internal static LootPools Get() { ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null || instance.m_items == null || instance.m_items.Count == 0) { return null; } if (_cached == null || _cachedRevision != LootTables.Revision || _cachedDb != instance || _cachedRecipeCount != instance.m_recipes.Count) { _cached = Build(LootTables.Current, instance); _cachedRevision = LootTables.Revision; _cachedDb = instance; _cachedRecipeCount = instance.m_recipes.Count; GravegoodsPlugin.LogInfo("Built loot pools. Gear per rung: " + Summary(_cached.Gear) + ". Ammo per rung: " + Summary(_cached.Ammo) + "."); if (_cached.MissingNames.Count > 0) { GravegoodsPlugin.LogWarning($"{_cached.MissingNames.Count} name(s) in loot.txt are not items in this install: " + string.Join(", ", _cached.MissingNames.ToArray())); } LootReport.Write(_cached); } return _cached; } private static string Summary(List<PoolItem>[] lists) { List<string> list = new List<string>(); for (int i = 0; i < lists.Length; i++) { list.Add($"{(Tier)i} {lists[i].Count}"); } return string.Join(", ", list.ToArray()); } internal static bool TryGetKind(SharedData shared, out PoolKind kind) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Invalid comparison between Unknown and I4 kind = PoolKind.Gear; if (GearTypes.Contains(shared.m_itemType)) { return shared.m_maxStackSize <= 1; } if ((int)shared.m_itemType == 9) { string text = (shared.m_ammoType ?? "").ToLowerInvariant(); kind = PoolKind.Ammo; if (text.Contains("arrow") || text.Contains("bolt")) { return !text.Contains("turret"); } return false; } return false; } private static LootPools Build(LootFile file, ObjectDB db) { LootPools lootPools = new LootPools(); Classifier classifier = new Classifier(file, db); foreach (GameObject item2 in db.m_items) { if ((Object)(object)item2 == (Object)null) { continue; } ItemDrop component = item2.GetComponent<ItemDrop>(); if ((Object)(object)component == (Object)null || component.m_itemData?.m_shared == null) { continue; } lootPools.ItemCount++; SharedData shared = component.m_itemData.m_shared; if (!TryGetKind(shared, out var kind)) { continue; } string name = ((Object)item2).name; string text = null; Tier value = Tier.Meadows; string because = null; if (file.Never.Contains(name)) { text = "listed in [never]"; } else if (file.Overrides.TryGetValue(name, out value)) { because = "[overrides]"; } else if (!string.IsNullOrEmpty(shared.m_dlc)) { text = "DLC item"; } else if (!classifier.HasRecipe(name)) { text = "no enabled crafting recipe"; } else { string text2 = classifier.BossGate(name); if (text2 != null) { text = "needs boss material " + text2; } else if (!classifier.TryResolve(name, out value, out because)) { text = "unknown ingredient " + because; } } if (text != null) { lootPools.Excluded.Add(new Exclusion { Prefab = name, Kind = kind, Reason = text }); } else { PoolItem item = new PoolItem { Prefab = name, Go = item2, Tier = value, Kind = kind, Because = because }; ((kind == PoolKind.Gear) ? lootPools.Gear : lootPools.Ammo)[(int)value].Add(item); } } foreach (string item3 in classifier.Unknown) { lootPools.UnknownMaterials.Add(item3); } foreach (string item4 in file.Materials.Keys.Concat(file.BossMaterials).Concat(file.Never).Concat(file.Overrides.Keys) .Concat(from r in file.Resources.SelectMany((List<ResourceEntry> r) => r) select r.Prefab)) { if ((Object)(object)db.GetItemPrefab(item4) == (Object)null) { lootPools.MissingNames.Add(item4); } } foreach (List<PoolItem> item5 in lootPools.Gear.Concat(lootPools.Ammo)) { item5.Sort((PoolItem a, PoolItem b) => string.CompareOrdinal(a.Prefab, b.Prefab)); } return lootPools; } } internal sealed class RollResult { internal readonly List<ItemData> Items = new List<ItemData>(); internal readonly List<string> Notes = new List<string>(); internal readonly List<string> Misses = new List<string>(); } internal static class LootRoller { internal static RollResult Roll(Tier chestTier) { RollResult rollResult = new RollResult(); LootPools lootPools = LootPools.Get(); if (lootPools == null) { rollResult.Misses.Add("ObjectDB not ready"); return rollResult; } if (Random.value < GravegoodsConfig.ResourceChance.Value) { RollResource(chestTier, rollResult); } if (Random.value < GravegoodsConfig.AmmoChance.Value) { RollAmmo(lootPools, chestTier, rollResult); } if (Random.value < GravegoodsConfig.GearChance.Value) { RollGear(lootPools, chestTier, rollResult); } return rollResult; } private static void RollResource(Tier chestTier, RollResult result) { List<ResourceEntry> list = LootTables.Current.Resources[(int)chestTier]; if (list.Count == 0) { result.Misses.Add($"resource: no [resources {chestTier}] entries"); return; } float num = 0f; foreach (ResourceEntry item in list) { num += item.Weight; } float num2 = Random.Range(0f, num); ResourceEntry resourceEntry = list[list.Count - 1]; foreach (ResourceEntry item2 in list) { num2 -= item2.Weight; if (num2 <= 0f) { resourceEntry = item2; break; } } GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(resourceEntry.Prefab); if ((Object)(object)itemPrefab == (Object)null || (Object)(object)itemPrefab.GetComponent<ItemDrop>() == (Object)null) { result.Misses.Add("resource: '" + resourceEntry.Prefab + "' is not an item"); return; } ItemData val = Make(itemPrefab, Random.Range(resourceEntry.Min, resourceEntry.Max + 1), 1, 1f); result.Items.Add(val); result.Notes.Add($"resource {resourceEntry.Prefab} x{val.m_stack}"); } private static void RollAmmo(LootPools pools, Tier chestTier, RollResult result) { Tier tier = ((Random.value < GravegoodsConfig.AmmoSameTierChance.Value) ? chestTier : Tiers.Previous(chestTier)); Tier actual; List<PoolItem> list = pools.AtOrBelow(PoolKind.Ammo, tier, out actual); if (list == null) { result.Misses.Add($"ammo: no pool at or below {tier}"); return; } PoolItem poolItem = list[Random.Range(0, list.Count)]; int value = GravegoodsConfig.AmmoStackMin.Value; int num = Mathf.Max(value, GravegoodsConfig.AmmoStackMax.Value); ItemData val = Make(poolItem.Go, Random.Range(value, num + 1), 1, 1f); result.Items.Add(val); result.Notes.Add($"ammo {poolItem.Prefab} x{val.m_stack} (rung {actual})"); } private static void RollGear(LootPools pools, Tier chestTier, RollResult result) { bool flag = chestTier == Tier.Meadows || Random.value < GravegoodsConfig.GearSameTierChance.Value; Tier tier = (flag ? chestTier : Tiers.Previous(chestTier)); Tier actual; List<PoolItem> list = pools.AtOrBelow(PoolKind.Gear, tier, out actual); if (list == null) { result.Misses.Add($"gear: no pool at or below {tier}"); return; } int quality = 1; if (!flag) { float value = Random.value; if (value < GravegoodsConfig.GearQuality3Chance.Value) { quality = 3; } else if (value < GravegoodsConfig.GearQuality3Chance.Value + GravegoodsConfig.GearQuality2Chance.Value) { quality = 2; } } float value2 = GravegoodsConfig.GearDurabilityMin.Value; float num = Mathf.Max(value2, GravegoodsConfig.GearDurabilityMax.Value); PoolItem poolItem = list[Random.Range(0, list.Count)]; ItemData val = Make(poolItem.Go, 1, quality, Random.Range(value2, num)); result.Items.Add(val); result.Notes.Add($"gear {poolItem.Prefab} q{val.m_quality} " + $"{Mathf.RoundToInt(val.m_durability / Mathf.Max(1f, val.GetMaxDurability()) * 100f)}% " + string.Format("(rung {0}{1})", actual, flag ? ", same as chest" : "")); } private static ItemData Make(GameObject prefab, int stack, int quality, float durabilityFraction) { ItemData itemData = prefab.GetComponent<ItemDrop>().m_itemData; ItemData val = itemData.Clone(); val.m_dropPrefab = prefab; val.m_stack = Mathf.Clamp(stack, 1, Mathf.Max(1, itemData.m_shared.m_maxStackSize)); val.m_quality = Mathf.Clamp(quality, 1, Mathf.Max(1, itemData.m_shared.m_maxQuality)); val.m_worldLevel = Game.m_worldLevel; if (val.m_shared.m_useDurability) { val.m_durability = val.GetMaxDurability(val.m_quality) * Mathf.Clamp01(durabilityFraction); } return val; } } internal static class LootTables { internal const string FileName = "loot.txt"; private static string _localText = ""; private static string _serverText; private static string _parsedFrom; private static LootFile _parsed; internal static string Directory => Path.Combine(Paths.ConfigPath, "Gravegoods"); internal static string FilePath => Path.Combine(Directory, "loot.txt"); internal static int Revision { get; private set; } internal static string LocalText => _localText; private static bool UsesServerCopy { get { if ((Object)(object)ZNet.instance != (Object)null) { return !ZNet.instance.IsServer(); } return false; } } internal static bool IsReady { get { if (UsesServerCopy) { return _serverText != null; } return true; } } internal static string SourceDescription { get { if (UsesServerCopy) { if (_serverText == null) { return "waiting for server copy"; } return "server copy"; } return "local file (" + FilePath + ")"; } } internal static LootFile Current { get { string text = (UsesServerCopy ? (_serverText ?? "") : _localText); if (_parsed == null || (object)text != _parsedFrom) { _parsed = LootFile.Parse(text); _parsedFrom = text; foreach (string warning in _parsed.Warnings) { GravegoodsPlugin.LogWarning(warning); } } return _parsed; } } internal static void LoadLocal() { try { System.IO.Directory.CreateDirectory(Directory); if (!File.Exists(FilePath)) { File.WriteAllText(FilePath, "# Gravegoods loot tables\n#\n# World chests roll three independent bonus chances (resources, ammo, gear). The chances,\n# quality odds and durability are in com.ragemedia.gravegoods.cfg. This file decides WHAT\n# can drop. On a server, the server's copy is sent to every client; restart the server\n# after editing it.\n#\n# After a world loads, BepInEx/config/Gravegoods/report.md lists every pool this file\n# produced, every item left out and why, and every name below that did not resolve.\n#\n# Biome names: Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, Ashlands, DeepNorth\n# Everything after a # is a comment.\n\n\n# ---------------------------------------------------------------------------------------\n# [materials]\n# The rung each crafting material belongs to. Weapons, armor and ammo are not listed by\n# hand: each one is placed at the rung of its highest-rung ingredient, so ValheimArmory\n# and other content mods are picked up automatically. Intermediate items (Bronze,\n# BronzeNails, ...) resolve through their own recipes if they are not listed.\n# An item with any ingredient that is neither listed here nor craftable stays OUT of the\n# pools, and the report names the ingredient. Add it here to let those items drop.\n# ---------------------------------------------------------------------------------------\n[materials]\nMeadows: Wood, Stone, Flint, Resin, LeatherScraps, DeerHide, Feathers, Raspberry, Honey\nMeadows: Mushroom, Dandelion, NeckTail, TrophyDeer, TrophyBoar, TrophyNeck\n\nBlackForest: FineWood, RoundLog, CopperOre, Copper, CopperScrap, TinOre, Tin, Bronze\nBlackForest: BronzeScrap, BronzeNails, GreydwarfEye, TrollHide, SurtlingCore, BoneFragments\nBlackForest: Thistle, Blueberries, AncientSeed, Carrot, CarrotSeeds, MushroomYellow, Amber\nBlackForest: Ruby, TrophyGreydwarf, TrophyGreydwarfBrute, TrophyGreydwarfShaman\nBlackForest: TrophySkeleton, TrophySkeletonPoison, TrophyFrostTroll, Coal\nBlackForest: BjornHide, BjornPaw, TrophyBjorn\n\nSwamp: Iron, IronOre, IronScrap, IronNails, ElderBark, Guck, Ooze, Entrails, WitheredBone\nSwamp: Chain, Bloodbag, Turnip, TurnipSeeds, AmberPearl, Chitin, SerpentScale\nSwamp: TrophyBlob, TrophyDraugr, TrophyDraugrElite, TrophyLeech, TrophySurtling\nSwamp: TrophyAbomination, TrophyWraith, TrophySerpent, Root\n\nMountain: Silver, SilverOre, Obsidian, FreezeGland, WolfPelt, WolfFang, WolfClaw, Crystal\nMountain: JuteRed, YmirRemains, Onion, OnionSeeds, TrophyWolf, TrophyFenring, TrophyUlv\nMountain: TrophyCultist, TrophyHatchling, TrophySGolem, WolfHairBundle\n\nPlains: BlackMetal, BlackMetalScrap, LinenThread, Flax, Barley, BarleyFlour, Needle\nPlains: LoxPelt, Tar, GoblinTotem, Thunderstone, Cloudberry, TrophyDeathsquito\nPlains: TrophyGoblin, TrophyGoblinBrute, TrophyGoblinShaman, TrophyLox, TrophyGrowth\n\nMistlands: YggdrasilWood, BlackMarble, Eitr, Sap, Softtissue, Carapace, ScaleHide, JuteBlue\nMistlands: Mandible, Bilebag, BlackCore, DvergrNeedle, Wisp, RoyalJelly, MushroomJotunPuffs\nMistlands: MushroomMagecap, TrophyDvergr, TrophySeeker, TrophySeekerBrute, TrophyTick\nMistlands: TrophyGjall, TrophyHare\n\nAshlands: FlametalNew, FlametalOreNew, Flametal, FlametalOre, Blackwood, Grausten\nAshlands: CharredBone, Charredskull, AskHide, AskBladder, MorgenSinew, MorgenHeart\nAshlands: CelestialFeather, GemstoneRed, GemstoneBlue, GemstoneGreen, MoltenCore\nAshlands: ProustitePowder, CharredCogwheel, MechanicalSpring, Ironpit, Pukeberries\nAshlands: Vineberry, VineberrySeeds, Fiddleheadfern, TrophyAsksvin, TrophyMorgen\nAshlands: TrophyCharredMelee, TrophyCharredArcher, TrophyCharredMage, TrophyFallenValkyrie\nAshlands: TrophyVolture, BellFragment, CharcoalResin, BonemawSerpentTooth, SulfurStone\n\nDeepNorth: Gold, GoldOre, MooseHide, MooseSinew, SealHide, Frostwood, NornThread\n# Bloodgold gear is cast from a mold per weapon or armor type.\nDeepNorth: MoldSword, MoldSword2H, MoldAxe, MoldAxe2H, MoldMace, MoldMace2H, MoldKnife\nDeepNorth: MoldSpear, MoldAtgeir, MoldBow, MoldCrossbow, MoldFistweapon\nDeepNorth: MoldShieldRound, MoldShieldBuckler, MoldShieldTower\nDeepNorth: MoldArmorGoldHelmet, MoldArmorGoldChest, MoldArmorGoldLegs\nDeepNorth: MoldArmorMediumHelmet, MoldArmormediumChest, MoldArmorMediumLegs\nDeepNorth: MoldArmorMageHelmet, MoldArmorMageChest, MoldArmorMageLegs\n\n# Valheim 2.0 materials whose biome is not confirmed are placed HIGH on purpose: a rung too\n# high only makes an item rarer, a rung too low hands it out early. Move them down once known.\nPlains: TrophyBjornUndead, UndeadBjornRibcage, WrithanRoots\nMistlands: GiantBloodSack\nDeepNorth: Ice, BarkaBranch\n\n\n# ---------------------------------------------------------------------------------------\n# [boss materials]\n# Boss trophies and boss drops. Anything whose every recipe needs one of these stays out\n# of the pools: these are the progression gates, and a chest should never hand them over.\n# ---------------------------------------------------------------------------------------\n[boss materials]\nTrophyEikthyr, HardAntler\nTrophyTheElder, CryptKey\nTrophyBonemass, Wishbone\nTrophyDragonQueen, DragonTear\nTrophyGoblinKing, YagluthDrop\nTrophySeekerQueen, QueenDrop\nTrophyFader, FaderDrop, FaderEmber\n# Unique exploration rewards, not farmable materials.\nDyrnwynHiltFragment, CrownJewel\n\n\n# ---------------------------------------------------------------------------------------\n# [never]\n# Items that never appear, however they classify. One or more names per line.\n# ---------------------------------------------------------------------------------------\n[never]\n# Novelty and tool items that happen to use a weapon or armor slot.\nTankard, Tankard_dvergr, GrapplingHook, Scythe, Shovel, HelmetFishingHat, AxeEarly\n\n\n# ---------------------------------------------------------------------------------------\n# [overrides]\n# Force an item into a rung, skipping recipe classification. This also admits items that\n# have no recipe at all (trader stock, drops). Boss materials are NOT checked here.\n# SwordIron: Swamp\n# ---------------------------------------------------------------------------------------\n[overrides]\n\n\n# ---------------------------------------------------------------------------------------\n# [resources <Biome>]\n# What a resource roll can give, per biome. Item min-max [weight, default 1]\n# ---------------------------------------------------------------------------------------\n[resources Meadows]\nFlint 3-6\nResin 3-6\nLeatherScraps 2-5\nDeerHide 2-3\nHoney 2-4\nNeckTail 2-4\nCoins 5-15 0.5\n\n[resources BlackForest]\nGreydwarfEye 3-6\nResin 4-8\nThistle 2-4\nTrollHide 1-2 0.6\nSurtlingCore 1-2 0.6\nCopperOre 2-4 0.6\nTinOre 2-4 0.6\nAncientSeed 1 0.5\nAmber 1 0.4\nRuby 1 0.3\nCoins 10-30 0.7\n\n[resources Swamp]\nGuck 2-4\nEntrails 3-6\nOoze 2-4\nElderBark 3-6\nWitheredBone 1-2 0.6\nIronScrap 2-4 0.6\nBloodbag 2-3 0.6\nAmberPearl 1 0.4\nCoins 20-40 0.7\n\n[resources Mountain]\nFreezeGland 2-4\nWolfPelt 2-3\nWolfFang 2-4\nObsidian 3-6\nOnion 2-4\nCrystal 1-2 0.5\nSilverOre 2-3 0.5\nRuby 1 0.4\nCoins 20-50 0.7\n\n[resources Plains]\nBarley 3-6\nFlax 3-6\nNeedle 3-6\nCloudberry 3-6\nTar 2-4 0.7\nLoxPelt 1-2 0.6\nBlackMetalScrap 2-4 0.5\nGoblinTotem 1 0.3\nCoins 30-60 0.7\n\n[resources Mistlands]\nSofttissue 2-4\nBlackMarble 3-6\nMushroomJotunPuffs 3-6\nMushroomMagecap 3-6\nCarapace 2-4 0.7\nScaleHide 1-2 0.6\nMandible 1-2 0.5\nBilebag 1 0.4\nCoins 40-80 0.7\n\n[resources Ashlands]\nGrausten 3-6\nBlackwood 3-6\nCharredBone 3-6\nVineberry 3-5\nAskHide 1-2 0.6\nCelestialFeather 1-2 0.5\nFlametalOreNew 2-3 0.5\nGemstoneRed 1 0.2\nGemstoneBlue 1 0.2\nGemstoneGreen 1 0.2\nCoins 50-100 0.7\n\n[resources DeepNorth]\nFrostwood 3-6\nMooseHide 1-2 0.6\nSealHide 1-2 0.6\nCoins 60-120 0.7\n"); GravegoodsPlugin.LogInfo("Wrote default loot tables to " + FilePath + "."); } _localText = File.ReadAllText(FilePath); } catch (Exception ex) { GravegoodsPlugin.LogError("Could not read " + FilePath + ", using built-in defaults: " + ex.Message); _localText = "# Gravegoods loot tables\n#\n# World chests roll three independent bonus chances (resources, ammo, gear). The chances,\n# quality odds and durability are in com.ragemedia.gravegoods.cfg. This file decides WHAT\n# can drop. On a server, the server's copy is sent to every client; restart the server\n# after editing it.\n#\n# After a world loads, BepInEx/config/Gravegoods/report.md lists every pool this file\n# produced, every item left out and why, and every name below that did not resolve.\n#\n# Biome names: Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, Ashlands, DeepNorth\n# Everything after a # is a comment.\n\n\n# ---------------------------------------------------------------------------------------\n# [materials]\n# The rung each crafting material belongs to. Weapons, armor and ammo are not listed by\n# hand: each one is placed at the rung of its highest-rung ingredient, so ValheimArmory\n# and other content mods are picked up automatically. Intermediate items (Bronze,\n# BronzeNails, ...) resolve through their own recipes if they are not listed.\n# An item with any ingredient that is neither listed here nor craftable stays OUT of the\n# pools, and the report names the ingredient. Add it here to let those items drop.\n# ---------------------------------------------------------------------------------------\n[materials]\nMeadows: Wood, Stone, Flint, Resin, LeatherScraps, DeerHide, Feathers, Raspberry, Honey\nMeadows: Mushroom, Dandelion, NeckTail, TrophyDeer, TrophyBoar, TrophyNeck\n\nBlackForest: FineWood, RoundLog, CopperOre, Copper, CopperScrap, TinOre, Tin, Bronze\nBlackForest: BronzeScrap, BronzeNails, GreydwarfEye, TrollHide, SurtlingCore, BoneFragments\nBlackForest: Thistle, Blueberries, AncientSeed, Carrot, CarrotSeeds, MushroomYellow, Amber\nBlackForest: Ruby, TrophyGreydwarf, TrophyGreydwarfBrute, TrophyGreydwarfShaman\nBlackForest: TrophySkeleton, TrophySkeletonPoison, TrophyFrostTroll, Coal\nBlackForest: BjornHide, BjornPaw, TrophyBjorn\n\nSwamp: Iron, IronOre, IronScrap, IronNails, ElderBark, Guck, Ooze, Entrails, WitheredBone\nSwamp: Chain, Bloodbag, Turnip, TurnipSeeds, AmberPearl, Chitin, SerpentScale\nSwamp: TrophyBlob, TrophyDraugr, TrophyDraugrElite, TrophyLeech, TrophySurtling\nSwamp: TrophyAbomination, TrophyWraith, TrophySerpent, Root\n\nMountain: Silver, SilverOre, Obsidian, FreezeGland, WolfPelt, WolfFang, WolfClaw, Crystal\nMountain: JuteRed, YmirRemains, Onion, OnionSeeds, TrophyWolf, TrophyFenring, TrophyUlv\nMountain: TrophyCultist, TrophyHatchling, TrophySGolem, WolfHairBundle\n\nPlains: BlackMetal, BlackMetalScrap, LinenThread, Flax, Barley, BarleyFlour, Needle\nPlains: LoxPelt, Tar, GoblinTotem, Thunderstone, Cloudberry, TrophyDeathsquito\nPlains: TrophyGoblin, TrophyGoblinBrute, TrophyGoblinShaman, TrophyLox, TrophyGrowth\n\nMistlands: YggdrasilWood, BlackMarble, Eitr, Sap, Softtissue, Carapace, ScaleHide, JuteBlue\nMistlands: Mandible, Bilebag, BlackCore, DvergrNeedle, Wisp, RoyalJelly, MushroomJotunPuffs\nMistlands: MushroomMagecap, TrophyDvergr, TrophySeeker, TrophySeekerBrute, TrophyTick\nMistlands: TrophyGjall, TrophyHare\n\nAshlands: FlametalNew, FlametalOreNew, Flametal, FlametalOre, Blackwood, Grausten\nAshlands: CharredBone, Charredskull, AskHide, AskBladder, MorgenSinew, MorgenHeart\nAshlands: CelestialFeather, GemstoneRed, GemstoneBlue, GemstoneGreen, MoltenCore\nAshlands: ProustitePowder, CharredCogwheel, MechanicalSpring, Ironpit, Pukeberries\nAshlands: Vineberry, VineberrySeeds, Fiddleheadfern, TrophyAsksvin, TrophyMorgen\nAshlands: TrophyCharredMelee, TrophyCharredArcher, TrophyCharredMage, TrophyFallenValkyrie\nAshlands: TrophyVolture, BellFragment, CharcoalResin, BonemawSerpentTooth, SulfurStone\n\nDeepNorth: Gold, GoldOre, MooseHide, MooseSinew, SealHide, Frostwood, NornThread\n# Bloodgold gear is cast from a mold per weapon or armor type.\nDeepNorth: MoldSword, MoldSword2H, MoldAxe, MoldAxe2H, MoldMace, MoldMace2H, MoldKnife\nDeepNorth: MoldSpear, MoldAtgeir, MoldBow, MoldCrossbow, MoldFistweapon\nDeepNorth: MoldShieldRound, MoldShieldBuckler, MoldShieldTower\nDeepNorth: MoldArmorGoldHelmet, MoldArmorGoldChest, MoldArmorGoldLegs\nDeepNorth: MoldArmorMediumHelmet, MoldArmormediumChest, MoldArmorMediumLegs\nDeepNorth: MoldArmorMageHelmet, MoldArmorMageChest, MoldArmorMageLegs\n\n# Valheim 2.0 materials whose biome is not confirmed are placed HIGH on purpose: a rung too\n# high only makes an item rarer, a rung too low hands it out early. Move them down once known.\nPlains: TrophyBjornUndead, UndeadBjornRibcage, WrithanRoots\nMistlands: GiantBloodSack\nDeepNorth: Ice, BarkaBranch\n\n\n# ---------------------------------------------------------------------------------------\n# [boss materials]\n# Boss trophies and boss drops. Anything whose every recipe needs one of these stays out\n# of the pools: these are the progression gates, and a chest should never hand them over.\n# ---------------------------------------------------------------------------------------\n[boss materials]\nTrophyEikthyr, HardAntler\nTrophyTheElder, CryptKey\nTrophyBonemass, Wishbone\nTrophyDragonQueen, DragonTear\nTrophyGoblinKing, YagluthDrop\nTrophySeekerQueen, QueenDrop\nTrophyFader, FaderDrop, FaderEmber\n# Unique exploration rewards, not farmable materials.\nDyrnwynHiltFragment, CrownJewel\n\n\n# ---------------------------------------------------------------------------------------\n# [never]\n# Items that never appear, however they classify. One or more names per line.\n# ---------------------------------------------------------------------------------------\n[never]\n# Novelty and tool items that happen to use a weapon or armor slot.\nTankard, Tankard_dvergr, GrapplingHook, Scythe, Shovel, HelmetFishingHat, AxeEarly\n\n\n# ---------------------------------------------------------------------------------------\n# [overrides]\n# Force an item into a rung, skipping recipe classification. This also admits items that\n# have no recipe at all (trader stock, drops). Boss materials are NOT checked here.\n# SwordIron: Swamp\n# ---------------------------------------------------------------------------------------\n[overrides]\n\n\n# ---------------------------------------------------------------------------------------\n# [resources <Biome>]\n# What a resource roll can give, per biome. Item min-max [weight, default 1]\n# ---------------------------------------------------------------------------------------\n[resources Meadows]\nFlint 3-6\nResin 3-6\nLeatherScraps 2-5\nDeerHide 2-3\nHoney 2-4\nNeckTail 2-4\nCoins 5-15 0.5\n\n[resources BlackForest]\nGreydwarfEye 3-6\nResin 4-8\nThistle 2-4\nTrollHide 1-2 0.6\nSurtlingCore 1-2 0.6\nCopperOre 2-4 0.6\nTinOre 2-4 0.6\nAncientSeed 1 0.5\nAmber 1 0.4\nRuby 1 0.3\nCoins 10-30 0.7\n\n[resources Swamp]\nGuck 2-4\nEntrails 3-6\nOoze 2-4\nElderBark 3-6\nWitheredBone 1-2 0.6\nIronScrap 2-4 0.6\nBloodbag 2-3 0.6\nAmberPearl 1 0.4\nCoins 20-40 0.7\n\n[resources Mountain]\nFreezeGland 2-4\nWolfPelt 2-3\nWolfFang 2-4\nObsidian 3-6\nOnion 2-4\nCrystal 1-2 0.5\nSilverOre 2-3 0.5\nRuby 1 0.4\nCoins 20-50 0.7\n\n[resources Plains]\nBarley 3-6\nFlax 3-6\nNeedle 3-6\nCloudberry 3-6\nTar 2-4 0.7\nLoxPelt 1-2 0.6\nBlackMetalScrap 2-4 0.5\nGoblinTotem 1 0.3\nCoins 30-60 0.7\n\n[resources Mistlands]\nSofttissue 2-4\nBlackMarble 3-6\nMushroomJotunPuffs 3-6\nMushroomMagecap 3-6\nCarapace 2-4 0.7\nScaleHide 1-2 0.6\nMandible 1-2 0.5\nBilebag 1 0.4\nCoins 40-80 0.7\n\n[resources Ashlands]\nGrausten 3-6\nBlackwood 3-6\nCharredBone 3-6\nVineberry 3-5\nAskHide 1-2 0.6\nCelestialFeather 1-2 0.5\nFlametalOreNew 2-3 0.5\nGemstoneRed 1 0.2\nGemstoneBlue 1 0.2\nGemstoneGreen 1 0.2\nCoins 50-100 0.7\n\n[resources DeepNorth]\nFrostwood 3-6\nMooseHide 1-2 0.6\nSealHide 1-2 0.6\nCoins 60-120 0.7\n"; } Revision++; } internal static void BeginWaitingForServer() { _serverText = null; Revision++; } internal static void ReceiveServerCopy(string text) { _serverText = text ?? ""; Revision++; GravegoodsPlugin.LogInfo($"Received loot tables from the server ({_serverText.Length} chars)."); } } internal enum Tier { Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, Ashlands, DeepNorth } internal static class Tiers { internal const int Count = 8; internal static bool TryFromBiome(Biome biome, out Tier tier) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected I4, but got Unknown //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Invalid comparison between Unknown and I4 //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 if ((int)biome <= 16) { switch (biome - 1) { default: if ((int)biome != 8) { if ((int)biome != 16) { break; } tier = Tier.Plains; return true; } tier = Tier.BlackForest; return true; case 0: tier = Tier.Meadows; return true; case 1: tier = Tier.Swamp; return true; case 3: tier = Tier.Mountain; return true; case 2: break; } } else { if ((int)biome == 32) { tier = Tier.Ashlands; return true; } if ((int)biome == 64) { tier = Tier.DeepNorth; return true; } if ((int)biome == 512) { tier = Tier.Mistlands; return true; } } tier = Tier.Meadows; return false; } internal static bool TryParse(string text, out Tier tier) { tier = Tier.Meadows; if (string.IsNullOrEmpty(text)) { return false; } string b = text.Replace(" ", "").Replace("_", "").Trim(); foreach (Tier value in Enum.GetValues(typeof(Tier))) { if (string.Equals(value.ToString(), b, StringComparison.OrdinalIgnoreCase)) { tier = value; return true; } } return false; } internal static Tier Previous(Tier tier) { if (tier != Tier.Meadows) { return tier - 1; } return Tier.Meadows; } } } namespace Gravegoods.Diagnostics { internal static class LootReport { internal static string FilePath => Path.Combine(LootTables.Directory, "report.md"); internal static void Write(LootPools pools) { try { Directory.CreateDirectory(LootTables.Directory); File.WriteAllText(FilePath, Build(pools)); GravegoodsPlugin.LogInfo("Wrote " + FilePath + "."); } catch (Exception ex) { GravegoodsPlugin.LogWarning("Could not write report: " + ex.Message); } } private static string Build(LootPools pools) { LootFile current = LootTables.Current; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Gravegoods loot report"); stringBuilder.AppendLine(); stringBuilder.AppendLine($"Generated {DateTime.Now:yyyy-MM-dd HH:mm} "); stringBuilder.AppendLine("Gravegoods v0.1.1 "); stringBuilder.AppendLine("Loot tables: " + LootTables.SourceDescription + " "); stringBuilder.AppendLine($"{pools.ItemCount} item prefabs in ObjectDB."); stringBuilder.AppendLine(); if (current.Warnings.Count > 0 || pools.MissingNames.Count > 0) { stringBuilder.AppendLine("## Problems in loot.txt"); stringBuilder.AppendLine(); foreach (string warning in current.Warnings) { stringBuilder.AppendLine("- " + warning); } if (pools.MissingNames.Count > 0) { stringBuilder.AppendLine("- Not items in this install (typo, or the mod that adds them is not loaded): " + string.Join(", ", pools.MissingNames.Select((string n) => "`" + n + "`").ToArray())); } stringBuilder.AppendLine(); } for (int num = 0; num < 8; num++) { Tier tier = (Tier)num; stringBuilder.AppendLine($"## {tier}"); stringBuilder.AppendLine(); List<ResourceEntry> list = current.Resources[num]; stringBuilder.AppendLine($"**Resources** ({list.Count})"); stringBuilder.AppendLine(); if (list.Count == 0) { stringBuilder.AppendLine("_none_"); } else { float num2 = list.Sum((ResourceEntry r) => r.Weight); foreach (ResourceEntry item in list) { stringBuilder.AppendLine($"- `{item.Prefab}` {item.Min}-{item.Max}, {item.Weight / num2 * 100f:0}% of resource rolls"); } } stringBuilder.AppendLine(); AppendPool(stringBuilder, "Ammo", pools.Ammo[num]); AppendPool(stringBuilder, "Gear", pools.Gear[num]); } stringBuilder.AppendLine("## Left out of the pools"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Gear and ammo that exist in this install but will never drop. \"No enabled crafting recipe\" covers trader stock, boss drops and disabled recipes; add them under [overrides] to include them anyway."); stringBuilder.AppendLine(); foreach (IGrouping<string, Exclusion> item2 in from g in pools.Excluded.GroupBy(GroupName) orderby g.Key select g) { stringBuilder.AppendLine($"**{item2.Key}** ({item2.Count()})"); stringBuilder.AppendLine(); foreach (Exclusion item3 in item2.OrderBy<Exclusion, string>((Exclusion e) => e.Prefab, StringComparer.Ordinal)) { string text = ((GroupName(item3) != item3.Reason) ? (" — " + item3.Reason) : ""); stringBuilder.AppendLine("- `" + item3.Prefab + "` (" + item3.Kind.ToString().ToLowerInvariant() + ")" + text); } stringBuilder.AppendLine(); } stringBuilder.AppendLine("## Ingredients with no known rung"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Seen in recipes but not listed under [materials] and not craftable. Every recipe that uses one is unusable, so the items above marked \"unknown ingredient\" stay out of the pools until these are given a rung under [materials]."); stringBuilder.AppendLine(); stringBuilder.AppendLine((pools.UnknownMaterials.Count == 0) ? "_none_" : string.Join(", ", pools.UnknownMaterials.Select((string n) => "`" + n + "`").ToArray())); stringBuilder.AppendLine(); return stringBuilder.ToString(); } private static string GroupName(Exclusion e) { if (e.Reason.StartsWith("needs boss")) { return "needs a boss material"; } if (e.Reason.StartsWith("unknown ingredient")) { return "has an ingredient with no known rung"; } return e.Reason; } private static void AppendPool(StringBuilder sb, string title, List<PoolItem> items) { sb.AppendLine($"**{title}** ({items.Count})"); sb.AppendLine(); if (items.Count == 0) { sb.AppendLine("_none_"); } else { sb.AppendLine("| Prefab | Name | Placed by |"); sb.AppendLine("|---|---|---|"); foreach (PoolItem item in items) { sb.AppendLine("| `" + item.Prefab + "` | " + DisplayName(item) + " | " + item.Because + " |"); } } sb.AppendLine(); } private static string DisplayName(PoolItem item) { string text = item.Go.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_name ?? ""; if (Localization.instance == null) { return text; } return Localization.instance.Localize(text); } } } namespace Gravegoods.Config { internal static class GravegoodsConfig { internal static ConfigEntry<bool> VerboseLogging; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<string> ExcludedContainers; internal static ConfigEntry<float> ResourceChance; internal static ConfigEntry<float> AmmoChance; internal static ConfigEntry<float> GearChance; internal static ConfigEntry<float> GearSameTierChance; internal static ConfigEntry<float> GearQuality2Chance; internal static ConfigEntry<float> GearQuality3Chance; internal static ConfigEntry<float> GearDurabilityMin; internal static ConfigEntry<float> GearDurabilityMax; internal static ConfigEntry<float> AmmoSameTierChance; internal static ConfigEntry<int> AmmoStackMin; internal static ConfigEntry<int> AmmoStackMax; private static string[] _excludedCache = new string[0]; private static string _excludedRaw; internal static void Bind(ConfigFile config) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Expected O, but got Unknown //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Expected O, but got Unknown //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Expected O, but got Unknown //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Expected O, but got Unknown //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Expected O, but got Unknown //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_01eb: Expected O, but got Unknown //IL_021e: Unknown result type (might be due to invalid IL or missing references) //IL_0228: Expected O, but got Unknown //IL_025b: Unknown result type (might be due to invalid IL or missing references) //IL_0265: Expected O, but got Unknown //IL_0298: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Expected O, but got Unknown //IL_02c9: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Expected O, but got Unknown //IL_02fb: Unknown result type (might be due to invalid IL or missing references) //IL_0305: Expected O, but got Unknown ConfigurationManagerAttributes val = new ConfigurationManagerAttributes { IsAdminOnly = true }; VerboseLogging = config.Bind<bool>("General", "VerboseLogging", false, "Log every chest roll: biome, rung, and what was added. Local to this machine."); Enabled = config.Bind<bool>("General", "Enabled", true, new ConfigDescription("Master switch for bonus chest loot.", (AcceptableValueBase)null, new object[1] { val })); ExcludedContainers = config.Bind<string>("General", "ExcludedContainers", "MWL_portContainer", new ConfigDescription("Comma-separated prefab name fragments. A chest whose prefab name contains any of them never gets a bonus roll. The default skips More World Locations' shipment port containers.", (AcceptableValueBase)null, new object[1] { val })); ResourceChance = config.Bind<float>("Chances", "ResourceChance", 0.08f, new ConfigDescription("Chance per chest to add one stack from the biome's resource list. Capped at 0.75.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.75f), new object[1] { val })); AmmoChance = config.Bind<float>("Chances", "AmmoChance", 0.05f, new ConfigDescription("Chance per chest to add a stack of arrows or bolts. Capped at 0.45.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.45f), new object[1] { val })); GearChance = config.Bind<float>("Chances", "GearChance", 0.02f, new ConfigDescription("Chance per chest to add one weapon or armor piece. Capped at 0.35.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.35f), new object[1] { val })); GearSameTierChance = config.Bind<float>("Gear", "SameTierChance", 0.15f, new ConfigDescription("When gear drops, the chance it comes from the chest's own biome rung. Otherwise it comes from one rung behind. Meadows chests always use their own rung. Capped at 0.20.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.2f), new object[1] { val })); GearQuality2Chance = config.Bind<float>("Gear", "Quality2Chance", 0.17f, new ConfigDescription("Chance that gear from one rung behind arrives upgraded once (quality 2). Same-rung gear is always quality 1. Capped at 0.25.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.25f), new object[1] { val })); GearQuality3Chance = config.Bind<float>("Gear", "Quality3Chance", 0.03f, new ConfigDescription("Chance that gear from one rung behind arrives upgraded twice (quality 3). Checked before Quality2Chance; capped at the item's max quality. Capped at 0.15.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.15f), new object[1] { val })); GearDurabilityMin = config.Bind<float>("Gear", "DurabilityMin", 0.05f, new ConfigDescription("Lowest durability found gear can have, as a fraction of max. Each item rolls uniformly at random between DurabilityMin and DurabilityMax. 0 means broken until repaired. Capped at 0.30.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.3f), new object[1] { val })); GearDurabilityMax = config.Bind<float>("Gear", "DurabilityMax", 0.3f, new ConfigDescription("Highest durability found gear can have, as a fraction of max. Set equal to DurabilityMin for a fixed value. Capped at 0.30.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.3f), new object[1] { val })); AmmoSameTierChance = config.Bind<float>("Ammo", "SameTierChance", 0.7f, new ConfigDescription("When ammo drops, the chance it comes from the chest's own biome rung. Otherwise it comes from one rung behind. Capped at 0.75.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.75f), new object[1] { val })); AmmoStackMin = config.Bind<int>("Ammo", "StackMin", 3, new ConfigDescription("Smallest ammo stack. Each drop rolls uniformly between StackMin and StackMax. Range 1-3.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 3), new object[1] { val })); AmmoStackMax = config.Bind<int>("Ammo", "StackMax", 8, new ConfigDescription("Largest ammo stack. Range 5-15.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(5, 15), new object[1] { val })); } internal static bool IsExcludedContainer(string prefabName) { string text = ExcludedContainers.Value ?? ""; if ((object)text != _excludedRaw) { _excludedCache = (from s in text.Split(new char[1] { ',' }) select s.Trim() into s where s.Length > 0 select s).ToArray(); _excludedRaw = text; } string[] excludedCache = _excludedCache; foreach (string value in excludedCache) { if (prefabName.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } } } namespace Gravegoods.Compat { internal static class RegeneratingWorldCompat { internal const string Guid = "jg224.RegeneratingWorld"; private const string ProtectionType = "JG224.RegeneratingWorld.Runtime.LocationChestProtection"; private const string DepositKey = "jg224.rw.chest.deposit.v1"; private static bool _active; internal static void TryPatch(Harmony harmony) { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown if (Chainloader.PluginInfos.ContainsKey("jg224.RegeneratingWorld")) { Type type = AccessTools.TypeByName("JG224.RegeneratingWorld.Runtime.LocationChestProtection"); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "IsSensitive", new Type[2] { typeof(ZDO), typeof(GameObject) }, (Type[])null)); if (methodInfo == null) { GravegoodsPlugin.LogWarning("RegeneratingWorld is loaded but LocationChestProtection.IsSensitive was not found. Unlooted bonus items may stop locations from regenerating."); return; } harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(RegeneratingWorldCompat), "IsSensitivePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _active = true; GravegoodsPlugin.LogInfo(" bound RegeneratingWorld LocationChestProtection.IsSensitive (compat)"); } } internal static bool ReadDeposit(ZDO zdo) { if (_active) { return zdo.GetBool("jg224.rw.chest.deposit.v1", false); } return false; } internal static void RestoreDeposit(ZDO zdo, bool before) { if (_active && !before && zdo.GetBool("jg224.rw.chest.deposit.v1", false)) { zdo.Set("jg224.rw.chest.deposit.v1", false); } } private static void IsSensitivePostfix(ZDO zdo, GameObject prefab, ref bool __result) { if (!__result || zdo == null || (Object)(object)prefab == (Object)null) { return; } try { string text = zdo.GetString(ChestLoot.BonusKey, ""); if (text.Length != 0 && !zdo.GetBool("jg224.rw.chest.deposit.v1", false) && OnlyNativeOrBonus(zdo, prefab, new HashSet<string>(text.Split(new char[1] { ',' }), StringComparer.Ordinal))) { __result = false; GravegoodsPlugin.LogVerbose("RegeneratingWorld: chest " + ((Object)prefab).name + " holds only native and bonus loot."); } } catch (Exception ex) { GravegoodsPlugin.LogVerbose("RegeneratingWorld compat check failed: " + ex.Message); } } private static bool OnlyNativeOrBonus(ZDO zdo, GameObject prefab, HashSet<string> bonus) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_00af: 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) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) Container component = prefab.GetComponent<Container>(); byte[] byteArray = zdo.GetByteArray(ZDOVars.s_items, (byte[])null); if ((Object)(object)component == (Object)null || byteArray == null) { return false; } Dictionary<string, ItemData> dictionary = new Dictionary<string, ItemData>(StringComparer.Ordinal); foreach (DropData drop in component.m_defaultItems.m_drops) { ItemDrop val = (((Object)(object)drop.m_item != (Object)null) ? drop.m_item.GetComponent<ItemDrop>() : null); if ((Object)(object)val != (Object)null) { dictionary[((Object)drop.m_item).name] = val.m_itemData; } } Inventory val2 = new Inventory("gravegoods-probe", (Sprite)null, 32, 32); val2.Load(new ZPackage(byteArray)); foreach (ItemData allItem in val2.GetAllItems()) { if (allItem.m_crafterID != 0L || !string.IsNullOrEmpty(allItem.m_crafterName) || allItem.m_customData.Count > 0 || allItem.m_equipped || allItem.m_pickedUp) { return false; } string text = (((Object)(object)allItem.m_dropPrefab != (Object)null) ? ((Object)allItem.m_dropPrefab).name : ""); if ((!dictionary.TryGetValue(text, out var value) || allItem.m_quality != value.m_quality || allItem.m_variant != value.m_variant) && !bonus.Contains(text)) { return false; } } return true; } } } namespace Gravegoods.Commands { internal sealed class GravegoodsCommand : ConsoleCommand { public override string Name => "gravegoods"; public override string Help => "Bonus chest loot: status | sim <biome> [n] | report | legacy on|off"; public override bool IsCheat => false; public override List<string> CommandOptionList() { return new List<string> { "sim", "report", "legacy" }; } public override void Run(string[] args, Terminal context) { if ((Object)(object)context == (Object)null) { return; } try { string text = ((args.Length != 0) ? args[0].ToLowerInvariant() : ""); if (text == null || text.Length != 0) { switch (text) { case "status": break; case "sim": Sim(context, args); return; case "report": Report(context); return; case "legacy": Legacy(context, args); return; default: context.AddString(((ConsoleCommand)this).Help); return; } } Status(context); } catch (Exception arg) { context.AddString("Gravegoods command failed; see the log."); GravegoodsPlugin.LogError(string.Format("gravegoods {0} failed: {1}", string.Join(" ", args), arg)); } } private static void Status(Terminal context) { context.AddString("Gravegoods v0.1.1 — " + (GravegoodsConfig.Enabled.Value ? "enabled" : "DISABLED")); context.AddString("Loot tables: " + LootTables.SourceDescription); context.AddString($"Chances per chest: resource {GravegoodsConfig.ResourceChance.Value:P0}, " + $"ammo {GravegoodsConfig.AmmoChance.Value:P0}, gear {GravegoodsConfig.GearChance.Value:P0}"); context.AddString("Legacy rolls for old chests: " + (ChestLoot.LegacyEnabled ? "ON" : "off")); LootPools lootPools = (LootTables.IsReady ? LootPools.Get() : null); if (lootPools == null) { context.AddString("Pools not built yet (load into a world first)."); return; } for (int i = 0; i < 8; i++) { context.AddString($" {(Tier)i,-12} resources {LootTables.Current.Resources[i].Count,3}, " + $"ammo {lootPools.Ammo[i].Count,3}, gear {lootPools.Gear[i].Count,3}"); } if (lootPools.MissingNames.Count > 0) { context.AddString($"{lootPools.MissingNames.Count} unknown name(s) in loot.txt; see report.md."); } } private static void Sim(Terminal context, string[] args) { if (args.Length < 2 || !Tiers.TryParse(args[1], out var tier)) { context.AddString("usage: gravegoods sim <Meadows|BlackForest|Swamp|Mountain|Plains|Mistlands|Ashlands> [chests]"); return; } if (!LootTables.IsReady || LootPools.Get() == null) { context.AddString("Load into a world first."); return; } int result = 1000; if (args.Length > 2 && (!int.TryParse(args[2], out result) || result < 1 || result > 100000)) { context.AddString("chests must be between 1 and 100000"); return; } int num = 0; Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.Ordinal); HashSet<string> hashSet = new HashSet<string>(); for (int i = 0; i < result; i++) { RollResult rollResult = LootRoller.Roll(tier); if (rollResult.Items.Count > 0) { num++; } foreach (string note in rollResult.Notes) { string text = string.Join(" ", note.Split(new char[1] { ' ' }).Take(3).ToArray()); text = (text.StartsWith("gear") ? text : string.Join(" ", note.Split(new char[1] { ' ' }).Take(2).ToArray())); dictionary[text] = ((!dictionary.TryGetValue(text, out var value)) ? 1 : (value + 1)); } foreach (string miss in rollResult.Misses) { hashSet.Add(miss); } } context.AddString($"{result} {tier} chests: {num} got something ({(float)num / (float)result:P1})."); string[] array = new string[3] { "resource", "ammo", "gear" }; foreach (string kind in array) { List<KeyValuePair<string, int>> list = (from kv in dictionary where kv.Key.StartsWith(kind) orderby kv.Value descending select kv).ToList(); context.AddString($" {kind}: {list.Sum((KeyValuePair<string, int> kv) => kv.Value)} total"); foreach (KeyValuePair<string, int> item in list.Take(12)) { context.AddString($" {item.Value,5} {item.Key.Substring(kind.Length + 1)}"); } if (list.Count > 12) { context.AddString($" ... and {list.Count - 12} more"); } } foreach (string item2 in hashSet) { context.AddString(" miss: " + item2); } } private static void Report(Terminal context) { LootPools lootPools = (LootTables.IsReady ? LootPools.Get() : null); if (lootPools == null) { context.AddString("Load into a world first."); return; } LootReport.Write(lootPools); context.AddString("Wrote " + LootReport.FilePath); } private static void Legacy(Terminal context, string[] args) { if ((Object)(object)ZoneSystem.instance == (Object)null) { context.AddString("Load into a world first."); return; } if (args.Length < 2) { context.AddString("Legacy rolls for old chests: " + (ChestLoot.LegacyEnabled ? "ON" : "off") + ". Use 'gravegoods legacy on' or 'off'."); return; } if (!SynchronizationManager.Instance.PlayerIsAdmin) { context.AddString("Only server admins can change legacy rolls."); return; } string text = args[1].ToLowerInvariant(); if (!(text == "on")) { if (text == "off") { ZoneSystem.instance.RemoveGlobalKey("gravegoods_legacy"); context.AddString("Legacy rolls off. Chests that already rolled keep their loot."); GravegoodsPlugin.LogInfo("Legacy rolls turned off."); } else { context.AddString("usage: gravegoods legacy on|off"); } } else { ZoneSystem.instance.SetGlobalKey("gravegoods_legacy"); context.AddString("Legacy rolls ON for this world: chests from before Gravegoods roll once when first opened."); GravegoodsPlugin.LogInfo("Legacy rolls turned on."); } } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }