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 Vaka v1.1.0
plugins/Vaka/Vaka.dll
Decompiled 3 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using Ezomic.Core; using HarmonyLib; 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: AssemblyCompany("Thijssen Software")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 Robbin Thijssen")] [assembly: AssemblyDescription("Fires keep while you are away. A single absence costs one fuel, however long it was.")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0+9dd611d374979e31bf520165b48e6e91a2fab10c")] [assembly: AssemblyProduct("Vaka")] [assembly: AssemblyTitle("Vaka")] [assembly: AssemblyVersion("1.1.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 Vaka { internal static class FireSurvey { private sealed class Row { public string Name; public string Fuel; public float Rate; public bool Infinite; public double Factor; public bool Cooks; public bool Stretched { get { if (!Infinite && Rate > 0f) { return Factor > 1.0; } return false; } } } private static ZNetScene _scene; private static bool _enabled; private static float _multiplier; private static string _fuels; private static bool _tabled; private static List<Row> _rows; private static FieldRef<ZNetScene, Dictionary<int, GameObject>> _named; private static bool _triedNamed; internal static void Tick() { if (!VakaPatches.Applied) { return; } ZNetScene scene = ZNetScene.instance; if ((Object)(object)scene == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null) { return; } bool value = VakaConfig.Enabled.Value; float value2 = VakaConfig.LightFuelMultiplier.Value; string text = VakaConfig.LightFuels.Value ?? ""; bool verbose = VakaConfig.Verbose.Value; if ((Object)(object)scene == (Object)(object)_scene && value == _enabled && value2.Equals(_multiplier) && text == _fuels) { if (verbose && !_tabled) { _tabled = true; Guarded(delegate { VakaPlugin.Log.LogInfo((object)Table(_rows)); }); } return; } _scene = scene; _enabled = value; _multiplier = value2; _fuels = text; _tabled = verbose; _rows = new List<Row>(); Guarded(delegate { Write(scene, verbose); }); } private static void Guarded(Action write) { try { write(); } catch (Exception ex) { VakaPlugin.Log.LogWarning((object)("Could not list this world's fireplaces: " + ex.Message)); } } private static void Write(ZNetScene scene, bool verbose) { List<Row> rows = _rows; Fireplace val = default(Fireplace); foreach (GameObject item in Prefabs(scene)) { if (!((Object)(object)item == (Object)null) && item.TryGetComponent<Fireplace>(ref val)) { string text = VakaPatches.FuelOf(val); rows.Add(new Row { Name = ((Object)item).name, Fuel = text, Rate = val.m_secPerFuel, Infinite = val.m_infiniteFuel, Factor = VakaConfig.LightFactor(text), Cooks = HasBurningArea(item) }); } } rows.Sort((Row a, Row b) => string.CompareOrdinal(a.Name, b.Name)); VakaPlugin.Log.LogInfo((object)Summary(rows)); if (verbose) { VakaPlugin.Log.LogInfo((object)Table(rows)); } } private static bool HasBurningArea(GameObject prefab) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) EffectArea[] componentsInChildren = prefab.GetComponentsInChildren<EffectArea>(true); foreach (EffectArea val in componentsInChildren) { if (!((Object)(object)val == (Object)null) && (val.m_type & 8) != 0) { return true; } } return false; } private static string Summary(List<Row> rows) { string text = VakaConfig.LightRuleOffBecause(); if (text != null) { return "Light fuel rule is off (" + text + "). " + rows.Count + " fireplaces in this world, every one at vanilla's rate."; } IList<string> list = VakaConfig.LightFuelNames(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Light fuels last ").Append(VakaConfig.LightFuelMultiplier.Value.ToString("0.##")).Append("x as long."); foreach (string item in list) { List<string> list2 = new List<string>(); foreach (Row row in rows) { if (row.Stretched && string.Equals(row.Fuel, item, StringComparison.OrdinalIgnoreCase)) { list2.Add(row.Name); } } stringBuilder.Append(' ').Append(item).Append(": "); stringBuilder.Append((list2.Count == 0) ? "no fireplace in this world burns it." : (string.Join(", ", list2.ToArray()) + ".")); } List<string> list3 = new List<string>(); foreach (Row row2 in rows) { if (row2.Stretched && row2.Cooks) { list3.Add(row2.Name); } } if (list3.Count > 0) { stringBuilder.Append(" A cooking station or cauldron placed over these counts them as a fire: ").Append(string.Join(", ", list3.ToArray())).Append('.'); } return stringBuilder.ToString(); } private static string Table(List<Row> rows) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("Every fireplace in this world, ").Append(rows.Count).Append(" of them:"); foreach (Row row in rows) { stringBuilder.Append('\n').Append(" ").Append(row.Name.PadRight(28)) .Append(' ') .Append(((row.Fuel.Length == 0) ? "(no fuel)" : row.Fuel).PadRight(16)) .Append(' '); if (row.Infinite) { stringBuilder.Append("infinite fuel, nothing to stretch"); } else if (row.Rate <= 0f) { stringBuilder.Append("burns no fuel"); } else if (row.Stretched) { stringBuilder.Append(row.Rate.ToString("0")).Append("s a fuel, burns as ").Append(((double)row.Rate * row.Factor).ToString("0")) .Append("s - light fuel"); } else { stringBuilder.Append(row.Rate.ToString("0")).Append("s a fuel, untouched"); } if (row.Cooks) { stringBuilder.Append(", can heat a cooking station"); } } return stringBuilder.ToString(); } private static ICollection<GameObject> Prefabs(ZNetScene scene) { if (!_triedNamed) { _triedNamed = true; try { _named = AccessTools.FieldRefAccess<ZNetScene, Dictionary<int, GameObject>>("m_namedPrefabs"); } catch (Exception) { _named = null; } } if (_named != null) { try { Dictionary<int, GameObject> dictionary = _named.Invoke(scene); if (dictionary != null) { return dictionary.Values; } } catch (Exception) { } } return scene.m_prefabs; } } internal static class VakaConfig { public static ConfigEntry<bool> Enabled; public static ConfigEntry<float> MaxFuelPerAbsence; public static ConfigEntry<string> LightFuels; public static ConfigEntry<float> LightFuelMultiplier; public static ConfigEntry<bool> Verbose; private static string _parsedFrom; private static readonly HashSet<string> _parsed = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static readonly List<string> _parsedOrder = new List<string>(); public static void Bind(ConfigFile config) { Enabled = config.Bind<bool>("Vaka", "Enabled", true, "Whether Vaka does anything at all. Off leaves the plugin loaded with both of its rules switched off: every absence is charged vanilla's full bill and every fuel burns at vanilla's rate, which is the behaviour this mod exists to change - so off is only useful for telling the two apart without uninstalling."); MaxFuelPerAbsence = config.Bind<float>("Vaka", "MaxFuelPerAbsence", 1f, "How much fuel a fire may lose to a single absence, in the units the hover text counts - logs, resin, whatever that fire burns. One, because a fire you banked full should still be lit when you get back, and one unit off the top is a bill you can pay by walking past it. It is not a discount on burning: a fire you are standing next to consumes exactly what vanilla says, and this number never touches it. 0 makes an absence free. Large numbers restore vanilla, where a fire is charged for every hour that passed whether anyone was there or not. A fire that was nearly empty when you left still goes out - the cap limits what an absence costs, it does not conjure fuel."); LightFuels = config.Bind<string>("Vaka", "LightFuels", "Resin, Coal", "Fuel items whose fires burn longer, by prefab name, separated by commas. A fire counts when the item it burns is on this list, so the rule follows the fuel rather than the piece and covers torches added by other mods too. The defaults are resin and coal. Cooking fires burn wood and are left alone, because a longer fuse on a campfire makes cooking cheaper and nobody asked for that. Each time you enter a world, one line in the log names the fires this reaches, and Verbose lists every fireplace with the name of its fuel, ready to copy here. Empty switches the rule off."); LightFuelMultiplier = config.Bind<float>("Vaka", "LightFuelMultiplier", 2f, "How many times longer each unit of a light fuel burns. 2 makes a torch that held a resin for five and a half hours of world time hold it for eleven. It applies whether you are at the fire or away from it, and it stacks with MaxFuelPerAbsence in the obvious way: an absence still costs at most that much fuel, it just takes longer to get there. 1 switches the rule off. Anything below 1 is treated as 1, because Vaka never makes a fire burn faster than vanilla."); Verbose = config.Bind<bool>("Diagnostics", "Verbose", false, "One line per fire every time one comes back from being unloaded, naming the fire, how long it was away, what that was worth in fuel and what it was actually charged - including the times the cap did not apply. This is the only precise readout there is. Hover text draws fuel rounded up to a whole unit, so a fire that lost a third of a log looks identical to one that lost nothing, and both look identical to a mod that never loaded. Quiet while you are near a fire, because a live update is two seconds and is never reported, but a base with thirty torches writes thirty lines the moment you walk into it. Also lists every fireplace in the game with the fuel it burns, its seconds per fuel and whether the light rule applies to it, once each time you enter a world, and straight away if this is switched on in game through a config manager."); } internal static double CapSeconds(float secondsPerFuel) { if (!Enabled.Value) { return -1.0; } if (secondsPerFuel <= 0f || float.IsNaN(secondsPerFuel) || float.IsInfinity(secondsPerFuel)) { return -1.0; } float num = MaxFuelPerAbsence.Value; if (float.IsNaN(num)) { return -1.0; } if (num < 0f) { num = 0f; } return (double)secondsPerFuel * (double)num; } internal static double LightFactor(string fuelPrefab) { if (!Enabled.Value) { return 1.0; } if (string.IsNullOrEmpty(fuelPrefab)) { return 1.0; } float value = LightFuelMultiplier.Value; if (float.IsNaN(value) || float.IsInfinity(value) || value <= 1f) { return 1.0; } if (!Parsed().Contains(fuelPrefab)) { return 1.0; } return value; } internal static string LightRuleOffBecause() { if (!Enabled.Value) { return "Enabled is false, so Vaka is off entirely"; } float value = LightFuelMultiplier.Value; if (float.IsNaN(value) || float.IsInfinity(value) || value <= 1f) { return "LightFuelMultiplier is " + value + ", and only a number above 1 stretches anything"; } if (LightFuelNames().Count == 0) { return "LightFuels is empty"; } return null; } internal static IList<string> LightFuelNames() { Parsed(); return _parsedOrder; } private static HashSet<string> Parsed() { string text = LightFuels.Value ?? ""; if (_parsedFrom != null && text == _parsedFrom) { return _parsed; } _parsed.Clear(); _parsedOrder.Clear(); string[] array = text.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0 && _parsed.Add(text2)) { _parsedOrder.Add(text2); } } _parsedFrom = text; return _parsed; } } internal static class VakaPatches { private const string Target = "GetTimeSinceLastUpdate"; internal static bool Applied; private static readonly ConditionalWeakTable<Fireplace, object> _billed = new ConditionalWeakTable<Fireplace, object>(); private static readonly ConditionalWeakTable<Fireplace, string> _fuel = new ConditionalWeakTable<Fireplace, string>(); private static readonly ConditionalWeakTable<Fireplace, string>.CreateValueCallback ReadFuel = FuelOf; private const double LiveInterval = 2.0; private const double WorthReporting = 6.0; internal static void Apply(Harmony harmony) { try { if (AccessTools.Method(typeof(Fireplace), "GetTimeSinceLastUpdate", (Type[])null, (Type[])null) == null) { VakaPlugin.Log.LogError((object)"Vaka is doing nothing: Fireplace.GetTimeSinceLastUpdate is not in this build of the game, so neither the absence cap nor the light fuel rule can run and fires burn the vanilla way. That method is private and matched by name, so a game update is the likely reason."); return; } harmony.PatchAll(typeof(VakaPatches)); Applied = true; } catch (Exception ex) { VakaPlugin.Log.LogError((object)("Vaka is doing nothing: could not patch Fireplace.GetTimeSinceLastUpdate, so fires burn the vanilla way. " + ex.Message)); } } internal static string FuelOf(Fireplace fire) { if ((Object)(object)fire == (Object)null || (Object)(object)fire.m_fuelItem == (Object)null) { return ""; } return Utils.GetPrefabName(((Object)((Component)fire.m_fuelItem).gameObject).name); } [HarmonyPostfix] [HarmonyPatch(typeof(Fireplace), "GetTimeSinceLastUpdate")] private static void GetTimeSinceLastUpdate(Fireplace __instance, ref double __result) { if ((Object)(object)__instance == (Object)null) { return; } double num = __result; double num2 = VakaConfig.LightFactor(_fuel.GetValue(__instance, ReadFuel)); double num3 = ((num2 > 1.0) ? (num / num2) : num); if (_billed.TryGetValue(__instance, out var _)) { __result = num3; return; } _billed.Add(__instance, null); double num4 = VakaConfig.CapSeconds(__instance.m_secPerFuel); double num5 = num3; if (num4 >= 0.0 && num5 > num4) { num5 = num4; } __result = num5; if (VakaConfig.Verbose.Value) { Report(__instance, num, num2, num3, num5); } } private static void Report(Fireplace fire, double charged, double factor, double burned, double billed) { if (!(charged < 6.0) && fire.IsBurning() && !fire.m_infiniteFuel) { string prefabName = Utils.GetPrefabName(((Object)((Component)fire).gameObject).name); float secPerFuel = fire.m_secPerFuel; string text = ((factor > 1.0) ? (" (" + FuelOf(fire) + " lasts " + factor.ToString("0.##") + "x as long)") : ""); if (billed >= burned) { VakaPlugin.Log.LogInfo((object)(prefabName + " was away " + Span(charged) + " of world time, worth " + Fuel(burned, secPerFuel) + " fuel" + text + ". Under the cap, so it was charged in full.")); } else { VakaPlugin.Log.LogInfo((object)(prefabName + " was away " + Span(charged) + " of world time, worth " + Fuel(burned, secPerFuel) + " fuel" + text + ". Charged " + Span(billed * factor) + ", " + Fuel(billed, secPerFuel) + " fuel.")); } } } private static string Span(double seconds) { if (seconds < 90.0) { return seconds.ToString("0") + "s"; } if (seconds < 5400.0) { return (seconds / 60.0).ToString("0.0") + "m"; } return (seconds / 3600.0).ToString("0.0") + "h"; } private static string Fuel(double seconds, float secondsPerFuel) { if (secondsPerFuel <= 0f) { return "0"; } return (seconds / (double)secondsPerFuel).ToString("0.00"); } } [BepInPlugin("ezomic.valheim.vaka", "Vaka", "1.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class VakaPlugin : BaseUnityPlugin { public const string PluginGuid = "ezomic.valheim.vaka"; public const string PluginName = "Vaka"; public const string PluginVersion = "1.1.0"; public const string PluginAuthor = "Robbin Thijssen"; private const string CoreGuid = "ezomic.valheim.core"; internal static ManualLogSource Log; internal static bool CorePresent; private Harmony _harmony; private void Awake() { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; VakaConfig.Bind(((BaseUnityPlugin)this).Config); TryRegisterWithCore(); _harmony = new Harmony("ezomic.valheim.vaka"); VakaPatches.Apply(_harmony); Log.LogInfo((object)"Vaka 1.1.0 by Robbin Thijssen - ready."); } private void Update() { FireSurvey.Tick(); } private void TryRegisterWithCore() { CorePresent = Chainloader.PluginInfos.ContainsKey("ezomic.valheim.core"); if (!CorePresent) { Log.LogInfo((object)"Core not installed - running standalone, without the version gate."); } else { RegisterWithCore(); } } [MethodImpl(MethodImplOptions.NoInlining)] private void RegisterWithCore() { Suite.Register("ezomic.valheim.vaka", "Vaka", "1.1.0", ((BaseUnityPlugin)this).Config, (Requirement)0, (Assembly)null); Suite.Sync((ConfigEntryBase[])(object)new ConfigEntryBase[4] { (ConfigEntryBase)VakaConfig.Enabled, (ConfigEntryBase)VakaConfig.MaxFuelPerAbsence, (ConfigEntryBase)VakaConfig.LightFuels, (ConfigEntryBase)VakaConfig.LightFuelMultiplier }); Suite.Local((ConfigEntryBase)(object)VakaConfig.Verbose); } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); } } } }