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 Utangard v1.0.0
plugins/Utangard/Utangard.dll
Decompiled 8 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; 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("Outside the fence: a biome you have not earned will not sustain you.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+c20f9fccc5309ee25f9edaf9f440936ac27c582b")] [assembly: AssemblyProduct("Utangard")] [assembly: AssemblyTitle("Utangard")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Utangard { internal static class BiomeGate { public static bool IsWithered(Player player) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || !UtangardConfig.Enabled.Value) { return false; } if ((Object)(object)player != (Object)(object)Player.m_localPlayer) { return false; } string text = UtangardConfig.RequiredKeyFor(player.GetCurrentBiome()); if (text == null) { return false; } ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return false; } if (!UtangardConfig.GateOnGroup.Value) { return !instance.GetGlobalKey(text); } return !Progression.GroupHasKey(text); } public static string BlockersHere(Player player) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || !UtangardConfig.GateOnGroup.Value) { return null; } string text = UtangardConfig.RequiredKeyFor(player.GetCurrentBiome()); if (text != null) { return Progression.BlockersFor(text, excludeSelf: true); } return null; } } internal static class BlockedEffects { private const string GuardianPrefix = "GP_"; private static readonly HashSet<int> Blocked = new HashSet<int>(); private static bool _built; public static void Rebuild() { ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null) { return; } Blocked.Clear(); foreach (GameObject item in instance.m_items) { if ((Object)(object)item == (Object)null) { continue; } ItemDrop component = item.GetComponent<ItemDrop>(); if (!((Object)(object)component == (Object)null) && component.m_itemData != null && component.m_itemData.m_shared != null) { StatusEffect consumeStatusEffect = component.m_itemData.m_shared.m_consumeStatusEffect; if ((Object)(object)consumeStatusEffect != (Object)null) { Add(consumeStatusEffect); } } } foreach (StatusEffect statusEffect in instance.m_StatusEffects) { if (!((Object)(object)statusEffect == (Object)null) && ((Object)statusEffect).name != null && ((Object)statusEffect).name.StartsWith("GP_")) { Add(statusEffect); } } if (UtangardConfig.BlockRested.Value) { Blocked.Add(SEMan.s_statusEffectRested); Blocked.Add(SEMan.s_statusEffectResting); } foreach (string alsoBlockName in UtangardConfig.AlsoBlockNames) { Blocked.Add(StringExtensionMethods.GetStableHashCode(alsoBlockName)); } foreach (string neverBlockName in UtangardConfig.NeverBlockNames) { Blocked.Remove(StringExtensionMethods.GetStableHashCode(neverBlockName)); } _built = true; UtangardPlugin.Log.LogInfo((object)("Buff set rebuilt: " + Blocked.Count + " status effects.")); if (UtangardConfig.LogBlockedEffects.Value) { LogNames(instance); } } private static void Add(StatusEffect effect) { if (!UtangardConfig.NeverBlockNames.Contains(((Object)effect).name)) { Blocked.Add(effect.NameHash()); } } public static bool IsBlocked(StatusEffect effect) { if (!_built || (Object)(object)effect == (Object)null) { return false; } if (effect is UtangardMarker || effect is SappedEffect) { return false; } return Blocked.Contains(effect.NameHash()); } public static bool IsBlockedHash(int nameHash) { if (!_built) { return false; } if (nameHash == UtangardEffectsRegistry.MarkerHash) { return false; } if (nameHash == UtangardEffectsRegistry.SappedHash) { return false; } return Blocked.Contains(nameHash); } private static void LogNames(ObjectDB db) { List<string> list = new List<string>(); List<string> list2 = new List<string>(); foreach (StatusEffect statusEffect in db.m_StatusEffects) { if (!((Object)(object)statusEffect == (Object)null)) { (Blocked.Contains(statusEffect.NameHash()) ? list : list2).Add(((Object)statusEffect).name); } } list.Sort(); list2.Sort(); UtangardPlugin.Log.LogInfo((object)"Treated as buffs (blocked and drained):"); UtangardPlugin.Log.LogInfo((object)(" " + Join(list))); UtangardPlugin.Log.LogInfo((object)"Left alone:"); UtangardPlugin.Log.LogInfo((object)(" " + Join(list2))); } private static string Join(List<string> names) { if (names.Count == 0) { return "(none)"; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < names.Count; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(names[i]); } return stringBuilder.ToString(); } } internal static class GlobalKeyDump { private static readonly FieldRef<ZoneSystem, HashSet<string>> KeysOf = AccessTools.FieldRefAccess<ZoneSystem, HashSet<string>>("m_globalKeys"); private static readonly Biome[] Gateable = UtangardConfig.GateableBiomes; public unsafe static void Log() { //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { UtangardPlugin.Log.LogWarning((object)"No ZoneSystem yet - nothing to report."); return; } List<string> list = new List<string>(KeysOf.Invoke(instance) ?? new HashSet<string>()); list.Sort(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < list.Count; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(list[i]); } UtangardPlugin.Log.LogInfo((object)("World global keys (" + list.Count + "): " + ((list.Count == 0) ? "(none)" : stringBuilder.ToString()))); LogRoster(); UtangardPlugin.Log.LogInfo((object)"Gate table:"); Biome[] gateable = Gateable; for (int j = 0; j < gateable.Length; j++) { Biome biome = gateable[j]; string text = UtangardConfig.RequiredKeyFor(biome); if (text == null) { UtangardPlugin.Log.LogInfo((object)(" " + ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString() + ": ungated")); } else if (!UtangardConfig.GateOnGroup.Value) { UtangardPlugin.Log.LogInfo((object)(" " + ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString() + ": needs '" + text + "' - " + (instance.GetGlobalKey(text) ? "set, biome is open" : "NOT set, biome withers"))); } else { bool num = Progression.GroupHasKey(text); string text2 = (num ? null : Progression.BlockersFor(text)); long num2 = Progression.SecondsLeft(text); string text3 = ((num2 < 0) ? "" : (" - " + Describe(num2) + " left to catch up")); string text4 = ((!num) ? ("biome withers" + ((text2 == null) ? " (roster empty; falling back to the world key)" : (", still owed by " + text2 + text3))) : (Progression.IsLatchedOpen(text) ? "cleared by the group, open for good" : "whole group has it, biome is open")); UtangardPlugin.Log.LogInfo((object)(" " + ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString() + ": needs '" + text + "' - " + text4)); } } } internal static string Describe(long seconds) { if (seconds >= 172800) { return seconds / 86400 + " days"; } if (seconds >= 7200) { return seconds / 3600 + " hours"; } if (seconds >= 120) { return seconds / 60 + " minutes"; } return seconds + " seconds"; } private static void LogRoster() { if (!UtangardConfig.GateOnGroup.Value) { UtangardPlugin.Log.LogInfo((object)"Gating on world keys; the group roster is not used."); return; } List<Progression.RosterEntry> list = Progression.Roster(); if (list.Count == 0) { UtangardPlugin.Log.LogInfo((object)"Roster is empty - falling back to world keys until somebody publishes. Expect this only on the first spawn after installing."); return; } UtangardPlugin.Log.LogInfo((object)("Roster (" + list.Count + " within " + UtangardConfig.RosterDays.Value + " days):")); foreach (Progression.RosterEntry item in list) { ManualLogSource log = UtangardPlugin.Log; string[] obj = new string[5] { " ", item.Name, " (id ", null, null }; long id = item.Id; obj[3] = id.ToString(); obj[4] = ")"; log.LogInfo((object)string.Concat(obj)); } } } internal static class Progression { internal struct RosterEntry { public long Id; public string Name; public long LastSeenDay; } private const string SeenPrefix = "utangard_seen_"; private const string DonePrefix = "utangard_p_"; private const string OpenPrefix = "utangard_open_"; private const string ClockPrefix = "utangard_first_"; private const char ValueSeparator = '|'; private const string NewPrefix = "utangard_"; private const string LegacyPrefix = "wither_"; private static readonly string LegacySeenPrefix = "wither_" + "utangard_seen_".Substring("utangard_".Length); private static readonly FieldRef<Player, HashSet<string>> UniquesOf = AccessTools.FieldRefAccess<Player, HashSet<string>>("m_uniques"); private static readonly List<Player> Attendees = new List<Player>(); private static List<RosterEntry> _roster; private static float _rosterBuiltAt = float.NegativeInfinity; private const float RosterCacheSeconds = 2f; private static string Legacy(string key) { if (key == null || !key.StartsWith("utangard_", StringComparison.Ordinal)) { return null; } return "wither_" + key.Substring("utangard_".Length); } private static bool Has(ZoneSystem zone, string key) { if (zone.GetGlobalKey(key)) { return true; } string text = Legacy(key); if (text != null) { return zone.GetGlobalKey(text); } return false; } private static bool Has(ZoneSystem zone, string key, out string value) { if (zone.GetGlobalKey(key, ref value)) { return true; } string text = Legacy(key); if (text != null) { return zone.GetGlobalKey(text, ref value); } return false; } private static long Today() { return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalDays; } public static void PublishLocal(Player player) { ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null || (Object)(object)player == (Object)null) { return; } long playerID = player.GetPlayerID(); if (playerID == 0L) { return; } string text = "utangard_seen_" + playerID; string text2 = Today() + "|" + Sanitise(player.GetPlayerName()); if (!Has(instance, text, out var value) || value != text2.ToLower()) { instance.SetGlobalKey(text + " " + text2); } if (!UtangardConfig.BackfillFromCharacter.Value) { return; } HashSet<string> hashSet = UniquesOf.Invoke(player); if (hashSet == null) { return; } foreach (string item in UtangardConfig.AllGateKeys()) { LatchIfGroupCleared(instance, item); if (HasLocally(hashSet, item) && instance.GetGlobalKey(item)) { string text3 = DoneKey(playerID, item); if (!Has(instance, text3)) { instance.SetGlobalKey(text3); StartClock(instance, item); InvalidateRoster(); UtangardPlugin.Log.LogInfo((object)("Published " + player.GetPlayerName() + "'s credit for " + item + ".")); } } } } public static void CreditAttendees(Vector3 where, string bossKey) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(bossKey) || !UtangardConfig.IsGateKey(bossKey)) { return; } ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return; } Attendees.Clear(); Player.GetPlayersInRange(where, UtangardConfig.CreditRadius.Value, Attendees); foreach (Player attendee in Attendees) { if ((Object)(object)attendee == (Object)null) { continue; } long playerID = attendee.GetPlayerID(); if (playerID != 0L) { string text = DoneKey(playerID, bossKey); if (!Has(instance, text)) { instance.SetGlobalKey(text); StartClock(instance, bossKey); InvalidateRoster(); UtangardPlugin.Log.LogInfo((object)("Credited " + attendee.GetPlayerName() + " for " + bossKey + " at the kill.")); } } } } public static bool GroupHasKey(string bossKey) { ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return false; } if (UtangardConfig.GateNeverRegresses.Value && Has(instance, OpenKey(bossKey))) { return true; } if (DeadlinePassed(instance, bossKey)) { return true; } List<RosterEntry> list = Roster(); bool flag = false; for (int i = 0; i < list.Count; i++) { if (Counts(list[i], bossKey)) { flag = true; if (!Has(instance, DoneKey(list[i].Id, bossKey))) { return false; } } } if (!flag) { return instance.GetGlobalKey(bossKey); } return true; } private static bool Counts(RosterEntry member, string bossKey) { long num = (long)Math.Max(1f, UtangardConfig.RosterDaysFor(bossKey)); return Today() - member.LastSeenDay <= num; } private static void StartClock(ZoneSystem zone, string bossKey) { string text = "utangard_first_" + bossKey.ToLowerInvariant(); if (!Has(zone, text)) { zone.SetGlobalKey(text + " " + Now()); UtangardPlugin.Log.LogInfo((object)("The clock on " + bossKey + " has started.")); } } internal static long SecondsLeft(string bossKey) { ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return -1L; } float num = UtangardConfig.CatchUpDaysFor(bossKey); if (num <= 0f) { return -1L; } if (!Has(instance, "utangard_first_" + bossKey.ToLowerInvariant(), out var value)) { return -1L; } if (!long.TryParse(value, out var result)) { return -1L; } return Math.Max(0L, result + (long)(num * 86400f) - Now()); } private static bool DeadlinePassed(ZoneSystem zone, string bossKey) { float num = UtangardConfig.CatchUpDaysFor(bossKey); if (num <= 0f) { return false; } if (!Has(zone, "utangard_first_" + bossKey.ToLowerInvariant(), out var value)) { return false; } if (!long.TryParse(value, out var result)) { return false; } return Now() >= result + (long)(num * 86400f); } private static long Now() { return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; } private static void LatchIfGroupCleared(ZoneSystem zone, string bossKey) { if (!UtangardConfig.GateNeverRegresses.Value) { return; } string text = OpenKey(bossKey); if (Has(zone, text)) { return; } bool flag = DeadlinePassed(zone, bossKey); if (!flag) { List<RosterEntry> list = Roster(); bool flag2 = false; for (int i = 0; i < list.Count; i++) { if (Counts(list[i], bossKey)) { flag2 = true; if (!Has(zone, DoneKey(list[i].Id, bossKey))) { return; } } } if (!flag2) { return; } } zone.SetGlobalKey(text); InvalidateRoster(); UtangardPlugin.Log.LogInfo((object)(flag ? ("The deadline on " + bossKey + " has run out; that biome is open for good.") : ("The group has cleared " + bossKey + "; that biome is open for good."))); } private static string OpenKey(string bossKey) { return "utangard_open_" + bossKey.ToLowerInvariant(); } public static bool IsLatchedOpen(string bossKey) { ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance != (Object)null) { return instance.GetGlobalKey(OpenKey(bossKey)); } return false; } public static string BlockersFor(string bossKey, bool excludeSelf = false) { ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return null; } long num = 0L; if (excludeSelf && (Object)(object)Player.m_localPlayer != (Object)null) { num = Player.m_localPlayer.GetPlayerID(); } StringBuilder stringBuilder = null; foreach (RosterEntry item in Roster()) { if ((!excludeSelf || item.Id != num || num == 0L) && Counts(item, bossKey) && !instance.GetGlobalKey(DoneKey(item.Id, bossKey))) { if (stringBuilder == null) { stringBuilder = new StringBuilder(); } else { stringBuilder.Append(", "); } stringBuilder.Append(item.Name); } } return stringBuilder?.ToString(); } public static List<RosterEntry> Roster() { if (_roster != null && Time.realtimeSinceStartup - _rosterBuiltAt < 2f) { return _roster; } List<RosterEntry> result = (_roster = BuildRoster()); _rosterBuiltAt = Time.realtimeSinceStartup; return result; } public static void InvalidateRoster() { _rosterBuiltAt = float.NegativeInfinity; } private static List<RosterEntry> BuildRoster() { List<RosterEntry> list = new List<RosterEntry>(); ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null) { return list; } HashSet<string> excludedPlayerIds = UtangardConfig.ExcludedPlayerIds; foreach (KeyValuePair<string, string> globalKeysValue in instance.m_globalKeysValues) { string text = (globalKeysValue.Key.StartsWith("utangard_seen_", StringComparison.Ordinal) ? "utangard_seen_" : (globalKeysValue.Key.StartsWith(LegacySeenPrefix, StringComparison.Ordinal) ? LegacySeenPrefix : null)); if (text == null) { continue; } string text2 = globalKeysValue.Key.Substring(text.Length); if (excludedPlayerIds.Contains(text2) || !long.TryParse(text2, out var id) || !ParseSeenValue(globalKeysValue.Value, out var day, out var name)) { continue; } int num = list.FindIndex((RosterEntry e) => e.Id == id); if (num >= 0) { if (day > list[num].LastSeenDay) { list[num] = new RosterEntry { Id = id, Name = (string.IsNullOrEmpty(name) ? text2 : name), LastSeenDay = day }; } } else { list.Add(new RosterEntry { Id = id, Name = (string.IsNullOrEmpty(name) ? text2 : name), LastSeenDay = day }); } } return list; } private static bool ParseSeenValue(string value, out long day, out string name) { day = 0L; name = null; if (string.IsNullOrEmpty(value)) { return false; } int num = value.IndexOf('|'); string s = ((num < 0) ? value : value.Substring(0, num)); if (num >= 0) { name = value.Substring(num + 1); } return long.TryParse(s, out day); } private static bool HasLocally(HashSet<string> uniques, string bossKey) { foreach (string unique in uniques) { if (string.Equals(unique, bossKey, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static string Sanitise(string name) { if (string.IsNullOrEmpty(name)) { return "unnamed"; } return name.Replace(' ', '_').Replace('|', '_'); } private static string DoneKey(long id, string bossKey) { return "utangard_p_" + id + "_" + bossKey.ToLowerInvariant(); } } internal static class UtangardConfig { private const string SecGate = "Gate"; private const string SecFood = "Food"; private const string SecBuffs = "Buffs"; private const string SecSapped = "Sapped"; private const string SecShow = "Presentation"; private const string SecDiag = "Diagnostics"; public static ConfigEntry<bool> Enabled; public static ConfigEntry<bool> GateOnGroup; public static ConfigEntry<bool> GateNeverRegresses; public static ConfigEntry<float> RosterDays; public static ConfigEntry<bool> BackfillFromCharacter; public static ConfigEntry<string> RosterDaysPerBoss; public static ConfigEntry<float> CatchUpDays; public static ConfigEntry<string> CatchUpDaysPerBoss; public static ConfigEntry<float> CreditRadius; public static ConfigEntry<string> ExcludePlayerIds; public static ConfigEntry<string> KeyMeadows; public static ConfigEntry<string> KeyBlackForest; public static ConfigEntry<string> KeySwamp; public static ConfigEntry<string> KeyMountain; public static ConfigEntry<string> KeyPlains; public static ConfigEntry<string> KeyMistlands; public static ConfigEntry<string> KeyAshLands; public static ConfigEntry<string> KeyDeepNorth; public static ConfigEntry<string> KeyOcean; public static ConfigEntry<float> FoodDrainMultiplier; public static ConfigEntry<bool> BlockEating; public static ConfigEntry<string> EatBlockedMessage; public static ConfigEntry<float> BuffDrainMultiplier; public static ConfigEntry<bool> BlockNewBuffs; public static ConfigEntry<bool> BlockRested; public static ConfigEntry<string> AlsoBlock; public static ConfigEntry<string> NeverBlock; public static ConfigEntry<string> BuffBlockedMessage; public static ConfigEntry<float> SappedStaminaRegen; public static ConfigEntry<float> SappedMaxSeconds; public static ConfigEntry<bool> ShowStatusEffects; public static ConfigEntry<string> MarkIconFrom; public static ConfigEntry<string> SappedIconFrom; public static ConfigEntry<string> EnterMessage; public static ConfigEntry<string> LeaveMessage; public static ConfigEntry<bool> NameTheBlockers; public static ConfigEntry<string> BlockedByPrefix; public static ConfigEntry<bool> Verbose; public static ConfigEntry<bool> LogGlobalKeys; public static ConfigEntry<bool> LogBlockedEffects; public static readonly Biome[] GateableBiomes; private static HashSet<string> _excludedIds; private static HashSet<string> _alsoBlock; private static HashSet<string> _neverBlock; public static HashSet<string> ExcludedPlayerIds { get { if (_excludedIds == null) { _excludedIds = Split(ExcludePlayerIds.Value); } return _excludedIds; } } public static HashSet<string> AlsoBlockNames { get { EnsureNameSets(); return _alsoBlock; } } public static HashSet<string> NeverBlockNames { get { EnsureNameSets(); return _neverBlock; } } public static void Bind(ConfigFile config) { Enabled = config.Bind<bool>("Gate", "Enabled", true, "Master switch. Off leaves the game completely untouched - the patches stay installed but every one of them returns immediately."); GateOnGroup = config.Bind<bool>("Gate", "GateOnGroup", true, "Gate on whether everyone in the group has done the boss, rather than on whether the boss is dead in this world.\nThe difference is the whole point. A world key is set once, by whoever landed the kill, and credits everybody - including someone who joined afterwards and has never seen the fight. On, the biome stays shut until every character on the roster was personally present at that boss's death, so carrying a friend through means actually bringing them.\nThe point is to keep a group together. Valheim groups come apart along progression: one person plays more, gets ahead, and the rest arrive to find the interesting content already cleared - so nobody wants to redo it and the person ahead has no reason to go back. This makes helping the people behind you the way you move forward yourself, rather than a favour. If your friend has not killed The Elder, the Swamp is shut for you too, so going back is the next thing on your own list.\nOff restores the original behaviour: one kill opens the biome for all."); GateNeverRegresses = config.Bind<bool>("Gate", "GateNeverRegresses", true, "Once the whole group has cleared a boss, that biome stays open forever, even if someone new joins later who has not done it.\nWithout this the gate runs backwards: a friend arriving with a fresh character decides nobody has done Eikthyr and shuts the Black Forest for the people who killed him - retroactively, and for as long as that friend keeps logging in. Progress the group paid for should not be revocable by somebody else's arrival.\nWhat a newcomer still gates is everything the group has NOT yet cleared, which is where the 'bring your friend' pressure belongs anyway. Off makes the gate strictly weakest-link at all times."); RosterDays = config.Bind<float>("Gate", "RosterDays", 14f, "How many days a character counts for after it was last seen. Real days, not in-game ones.\nThis is the answer to the obvious problem with a group gate: an alt made once, or a friend who visited for one evening, would otherwise hold every biome shut forever. Leaving is automatic and needs no admin command - stop logging in and you stop counting. The cost is that someone on a long holiday silently drops out and the gate may open without them, so set this comfortably longer than your group's normal gap between sessions."); BackfillFromCharacter = config.Bind<bool>("Gate", "BackfillFromCharacter", true, "Credit a character for a boss its own file says it has attended, as long as that boss has already died in this world.\nThis is the migration path and nothing else: nobody's past kills were recorded before this mod existed, so without it, installing on a long-running world gates every biome until every boss is killed again.\nIt is also the one hole in the 'you had to be there' rule. The record it reads lives on the client and is not world-specific, so a player who was never at the fight is credited the moment somebody else kills that boss here. Requiring the world to have seen the boss die bounds that - imported credit cannot open a biome on its own - but it does not close it.\nOff makes the rule exact: credit comes only from kills this mod watched happen. Correct for a world that started with the mod installed, and punishing for one that did not."); RosterDaysPerBoss = config.Bind<string>("Gate", "RosterDaysPerBoss", "", "Per-boss overrides for RosterDays, as 'key:days' pairs separated by commas. For example: defeated_eikthyr:7, defeated_fader:60\nA short window for an early boss says 'keep up or stop counting'; a long one for a late boss keeps somebody on holiday from being written out of a fight the group has been building towards for weeks. Anything not listed uses RosterDays.\nWritten as one line rather than nine config entries because the interesting case is overriding one or two of them, not filling in a table."); CatchUpDays = config.Bind<float>("Gate", "CatchUpDays", 0f, "Fallback deadline, in days, for any boss not named in CatchUpDaysPerBoss. 0 means no deadline for those.\nWithout a deadline of some kind, one person who stops logging in holds a biome shut for everybody until RosterDays finally drops them, which can be weeks."); CatchUpDaysPerBoss = config.Bind<string>("Gate", "CatchUpDaysPerBoss", "defeated_eikthyr:1, defeated_gdking:2, defeated_bonemass:3, defeated_dragon:4, defeated_goblinking:5, defeated_queen:6, defeated_fader:7", "Days the rest of the group has to catch up once the first player has done a boss, after which the biome opens whether they did or not.\nThe clock starts the first time this mod records anyone as having that boss in this world, and is written once and never moved.\nThe default is a ladder: one day for Eikthyr and one more for each boss after. Early bosses are short trips somebody can be brought along on the same evening, so a day is enough pressure; a late boss is a whole evening's expedition that has to be organised, and holding the group to one day there would just mean the deadline always wins and the gate never does."); CreditRadius = config.Bind<float>("Gate", "CreditRadius", 100f, "How close to a dying boss a character has to be to be credited for it, in metres.\nDeliberately generous. Only one machine sees a boss die - the client that owns it - so it credits everyone standing nearby on the group's behalf, and the two ways of getting that wrong are not symmetric. Too tight and somebody who fought the whole battle is not credited, which holds the gate shut for the entire group with no way to fix it short of killing the boss again. Too loose and a bystander is credited, which costs one person's sense of having earned it. Prefer too loose."); ExcludePlayerIds = config.Bind<string>("Gate", "ExcludePlayerIds", "", "Character IDs that never count towards the group gate, comma-separated. The manual override for a character that has to keep playing but should not hold the gate. IDs rather than names, because global keys are lowercased and two characters can share a name. The roster dump on spawn prints both."); KeyMeadows = BindKey(config, "Meadows", "", "Ungated by default. This is where you start."); KeyBlackForest = BindKey(config, "BlackForest", "defeated_eikthyr", "Eikthyr."); KeySwamp = BindKey(config, "Swamp", "defeated_gdking", "The Elder. 'gdking' really is the game's name for him."); KeyMountain = BindKey(config, "Mountain", "defeated_bonemass", "Bonemass."); KeyPlains = BindKey(config, "Plains", "defeated_dragon", "Moder."); KeyMistlands = BindKey(config, "Mistlands", "defeated_goblinking", "Yagluth."); KeyAshLands = BindKey(config, "AshLands", "defeated_queen", "The Queen. Not in the GlobalKeys enum - verify against LogGlobalKeys."); KeyDeepNorth = BindKey(config, "DeepNorth", "defeated_fader", "Fader. Not in the GlobalKeys enum - verify against LogGlobalKeys."); KeyOcean = BindKey(config, "Ocean", "", "Ungated by default. Gating the ocean gates every crossing to every biome, including the ones you are allowed into."); FoodDrainMultiplier = config.Bind<float>("Food", "FoodDrainMultiplier", 5f, "How much faster food burns down in a gated biome. 5 turns a 1600-second meal into a 320-second one. 1 disables the drain and leaves only the refusal to eat, which is a much gentler mod - you keep what you brought."); BlockEating = config.Bind<bool>("Food", "BlockEating", true, "Refuse to eat anything at all in a gated biome. This is the 'force' half: the drain alone can be beaten by carrying more food, and the block is what makes the timer real."); EatBlockedMessage = config.Bind<string>("Food", "EatBlockedMessage", "The land will not feed you here", "Shown centre-screen when a bite is refused."); BuffDrainMultiplier = config.Bind<float>("Buffs", "BuffDrainMultiplier", 5f, "How much faster an already-running buff burns down in a gated biome. Only applies to the effects this mod considers buffs - see the Buffs section below. Harmful effects are never touched, because speeding up Poison or Freezing would be a mercy, not a penalty."); BlockNewBuffs = config.Bind<bool>("Buffs", "BlockNewBuffs", true, "Refuse to apply any new buff in a gated biome."); BlockRested = config.Bind<bool>("Buffs", "BlockRested", true, "Treat Rested and Resting as buffs, so a fire and a roof buy you nothing inside a gated biome. This is the single harshest switch in the mod: it means a forward base in the Plains gives shelter and warmth but no regeneration. Turn it off and a well-built camp becomes a real answer."); AlsoBlock = config.Bind<string>("Buffs", "AlsoBlock", "", "Extra status effect names to treat as buffs, comma-separated. The mod finds potions and meads by walking ObjectDB for anything an item applies when consumed, and guardian powers by their GP_ prefix, so this list is only for the odd one out. Turn on Diagnostics.LogBlockedEffects to see names."); NeverBlock = config.Bind<string>("Buffs", "NeverBlock", "Puke", "Status effect names to leave alone even if the rules above caught them, comma-separated. Wins over AlsoBlock.\nPuke is here because the first run in a real world found it: something applies it on consume, so the potion rule catches it, and it is a debuff. Blocking it would have handed the player an immunity to bad food in the one biome meant to be punishing them, and the drain would have made it wear off faster there than anywhere else. This is the exact failure the harmful-allowlist design was meant to avoid, arriving from the other direction - worth remembering if a future update adds another debuff that an item hands you."); BuffBlockedMessage = config.Bind<string>("Buffs", "BuffBlockedMessage", "The land turns your power aside", "Shown centre-screen when a potion or a guardian power is refused. Effects that arrive without the player asking - equipment, weather, another player's buff - are refused silently, because a message for each of those would be a wall of text nobody reads."); SappedStaminaRegen = config.Bind<float>("Sapped", "StaminaRegenMultiplier", 0.25f, "Stamina regeneration while Sapped, as a fraction of normal. 0.25 is the 75% penalty. This is a multiplier applied alongside every other one the game has, so it stacks with food and with Rested rather than replacing them."); SappedMaxSeconds = config.Bind<float>("Sapped", "MaxSeconds", 30f, "Ceiling on how much Sapped you can accumulate. One second in a gated biome buys one second of it, so this is also 'how long you must stand in the biome before the penalty is at full length'. It keeps ticking down after you leave, which is the point - a dash in and out still costs you."); ShowStatusEffects = config.Bind<bool>("Presentation", "ShowStatusEffects", true, "Show the two effects on the HUD. Off makes the mod invisible, which is atmospheric and also completely baffling to a new player."); MarkIconFrom = config.Bind<string>("Presentation", "MarkIconFrom", "Poison", "Name of the vanilla status effect whose icon the in-biome marker borrows."); SappedIconFrom = config.Bind<string>("Presentation", "SappedIconFrom", "Encumbered", "Name of the vanilla status effect whose icon Sapped borrows. Encumbered's weight reads as 'heavy and slow', which is what the effect does."); EnterMessage = config.Bind<string>("Presentation", "EnterMessage", "Something here refuses you", "Shown once on entering a gated biome. Blank to say nothing."); LeaveMessage = config.Bind<string>("Presentation", "LeaveMessage", "The land loosens its grip", "Shown once on leaving a gated biome. Blank to say nothing."); NameTheBlockers = config.Bind<bool>("Presentation", "NameTheBlockers", true, "Under a group gate, name the characters the biome is still waiting on. Worth leaving on: the honest question a player asks is 'why is this still shut when I killed it myself', and without an answer the only way to find one is to read a log file. Off if you would rather it stayed mysterious."); BlockedByPrefix = config.Bind<string>("Presentation", "BlockedByPrefix", "Still owed by:", "Prefix for that list of names."); Verbose = config.Bind<bool>("Diagnostics", "Verbose", false, "Log every gate transition and every blocked effect as it happens."); LogGlobalKeys = config.Bind<bool>("Diagnostics", "LogGlobalKeys", true, "Log the world's global keys once when a world loads. This is how you check that the key names in the Gate table match what your world actually records - a typo there fails open and gates nothing, silently."); LogBlockedEffects = config.Bind<bool>("Diagnostics", "LogBlockedEffects", false, "Log the full list of status effects the mod decided are buffs, once, when ObjectDB is built. The list to consult before editing AlsoBlock."); AlsoBlock.SettingChanged += delegate { InvalidateNameSets(); }; NeverBlock.SettingChanged += delegate { InvalidateNameSets(); }; BlockRested.SettingChanged += delegate { BlockedEffects.Rebuild(); }; ExcludePlayerIds.SettingChanged += delegate { _excludedIds = null; }; } private static ConfigEntry<string> BindKey(ConfigFile config, string biome, string defaultKey, string note) { return config.Bind<string>("Gate", "Key_" + biome, defaultKey, note + " Global key that must be set before this biome stops withering you. Blank means the biome is never gated."); } public static string RequiredKeyFor(Biome biome) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002e: 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_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Invalid comparison between Unknown and I4 //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0033: 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_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Invalid comparison between Unknown and I4 //IL_0035: 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 string value; if ((int)biome <= 16) { switch (biome - 1) { case 0: goto IL_0051; case 1: goto IL_006b; case 3: goto IL_0078; case 2: goto IL_00c6; } if ((int)biome != 8) { if ((int)biome != 16) { goto IL_00c6; } value = KeyPlains.Value; } else { value = KeyBlackForest.Value; } } else if ((int)biome <= 64) { if ((int)biome != 32) { if ((int)biome != 64) { goto IL_00c6; } value = KeyDeepNorth.Value; } else { value = KeyAshLands.Value; } } else if ((int)biome != 256) { if ((int)biome != 512) { goto IL_00c6; } value = KeyMistlands.Value; } else { value = KeyOcean.Value; } goto IL_00c8; IL_006b: value = KeySwamp.Value; goto IL_00c8; IL_0078: value = KeyMountain.Value; goto IL_00c8; IL_00c8: if (!string.IsNullOrEmpty(value)) { return value.Trim(); } return null; IL_0051: value = KeyMeadows.Value; goto IL_00c8; IL_00c6: return null; } public static IEnumerable<string> AllGateKeys() { HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); Biome[] gateableBiomes = GateableBiomes; for (int i = 0; i < gateableBiomes.Length; i++) { string text = RequiredKeyFor(gateableBiomes[i]); if (text != null && seen.Add(text)) { yield return text; } } } public static ConfigEntryBase[] GateKeyEntries() { return (ConfigEntryBase[])(object)new ConfigEntryBase[9] { (ConfigEntryBase)KeyMeadows, (ConfigEntryBase)KeyBlackForest, (ConfigEntryBase)KeySwamp, (ConfigEntryBase)KeyMountain, (ConfigEntryBase)KeyPlains, (ConfigEntryBase)KeyMistlands, (ConfigEntryBase)KeyAshLands, (ConfigEntryBase)KeyDeepNorth, (ConfigEntryBase)KeyOcean }; } public static float RosterDaysFor(string bossKey) { if (!TryPerBoss(RosterDaysPerBoss.Value, bossKey, out var value)) { return RosterDays.Value; } return value; } public static float CatchUpDaysFor(string bossKey) { if (!TryPerBoss(CatchUpDaysPerBoss.Value, bossKey, out var value)) { return CatchUpDays.Value; } return value; } private static bool TryPerBoss(string line, string bossKey, out float value) { value = 0f; if (string.IsNullOrEmpty(line) || string.IsNullOrEmpty(bossKey)) { return false; } string[] array = line.Split(new char[1] { ',' }); foreach (string text in array) { int num = text.IndexOf(':'); if (num > 0 && string.Equals(text.Substring(0, num).Trim(), bossKey, StringComparison.OrdinalIgnoreCase)) { return float.TryParse(text.Substring(num + 1).Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value); } } return false; } public static bool IsGateKey(string key) { if (string.IsNullOrEmpty(key)) { return false; } foreach (string item in AllGateKeys()) { if (string.Equals(item, key, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static void InvalidateNameSets() { _alsoBlock = null; _neverBlock = null; BlockedEffects.Rebuild(); } private static void EnsureNameSets() { if (_alsoBlock == null) { _alsoBlock = Split(AlsoBlock.Value); _neverBlock = Split(NeverBlock.Value); } } private static HashSet<string> Split(string csv) { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array = (csv ?? "").Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { hashSet.Add(text); } } return hashSet; } static UtangardConfig() { Biome[] array = new Biome[9]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); GateableBiomes = (Biome[])(object)array; } } internal sealed class UtangardMarker : StatusEffect { public override bool IsDone() { Character character = base.m_character; return !BiomeGate.IsWithered((Player)(object)((character is Player) ? character : null)); } public override string GetIconText() { return ""; } } internal sealed class SappedEffect : StatusEffect { public override void Setup(Character character) { ((StatusEffect)this).Setup(character); base.m_ttl = Ceiling(); base.m_time = base.m_ttl; } public override void UpdateStatusEffect(float dt) { base.m_ttl = Ceiling(); Character character = base.m_character; if (BiomeGate.IsWithered((Player)(object)((character is Player) ? character : null))) { base.m_time = Mathf.Max(0f, base.m_time - dt * 2f); } ((StatusEffect)this).UpdateStatusEffect(dt); } public override void ModifyStaminaRegen(ref float staminaRegen) { staminaRegen *= Mathf.Clamp01(UtangardConfig.SappedStaminaRegen.Value); } private static float Ceiling() { return Mathf.Max(0.1f, UtangardConfig.SappedMaxSeconds.Value); } } internal static class UtangardEffectsRegistry { private const string MarkerName = "Wither_Denied"; private const string SappedName = "Wither_Sapped"; public static readonly int MarkerHash = StringExtensionMethods.GetStableHashCode("Wither_Denied"); public static readonly int SappedHash = StringExtensionMethods.GetStableHashCode("Wither_Sapped"); private static UtangardMarker _marker; private static SappedEffect _sapped; private static Sprite _markerIcon; private static Sprite _sappedIcon; public static void Build() { if ((Object)(object)_marker == (Object)null) { _marker = ScriptableObject.CreateInstance<UtangardMarker>(); ((Object)_marker).name = "Wither_Denied"; ((StatusEffect)_marker).m_name = "Denied"; ((StatusEffect)_marker).m_tooltip = "This land does not recognise you. Nothing you eat or drink will take hold here, and what you carry is burning away."; ((Object)_marker).hideFlags = (HideFlags)61; } if ((Object)(object)_sapped == (Object)null) { _sapped = ScriptableObject.CreateInstance<SappedEffect>(); ((Object)_sapped).name = "Wither_Sapped"; ((StatusEffect)_sapped).m_name = "Sapped"; ((StatusEffect)_sapped).m_tooltip = "The land took something with it. Stamina returns slowly until this wears off."; ((Object)_sapped).hideFlags = (HideFlags)61; } _markerIcon = BorrowIcon(UtangardConfig.MarkIconFrom.Value); _sappedIcon = BorrowIcon(UtangardConfig.SappedIconFrom.Value); } public static void Ensure(Player player) { SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan != null) { bool value = UtangardConfig.ShowStatusEffects.Value; if (value) { Apply(sEMan, MarkerHash, (StatusEffect)(object)_marker, _markerIcon); } Apply(sEMan, SappedHash, (StatusEffect)(object)_sapped, value ? _sappedIcon : null); } } private static void Apply(SEMan seman, int hash, StatusEffect prototype, Sprite icon) { if (!((Object)(object)prototype == (Object)null)) { StatusEffect val = seman.GetStatusEffect(hash); if ((Object)(object)val == (Object)null) { prototype.m_icon = icon; val = seman.AddStatusEffect(prototype, false, 0, 0f); } if ((Object)(object)val != (Object)null) { val.m_icon = icon; } } } private static Sprite BorrowIcon(string donorName) { if (string.IsNullOrEmpty(donorName)) { return null; } ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null) { return null; } foreach (StatusEffect statusEffect in instance.m_StatusEffects) { if ((Object)(object)statusEffect != (Object)null && ((Object)statusEffect).name == donorName) { if ((Object)(object)statusEffect.m_icon == (Object)null) { UtangardPlugin.Log.LogWarning((object)("Icon donor '" + donorName + "' exists but has no sprite.")); } return statusEffect.m_icon; } } if (instance.m_items.Count > 0) { UtangardPlugin.Log.LogWarning((object)("Icon donor '" + donorName + "' not found in ObjectDB - that effect will be invisible on the HUD. Turn on Diagnostics.LogBlockedEffects for names.")); } return null; } } internal static class UtangardPatches { private static readonly FieldRef<SEMan, Character> CharacterOf = AccessTools.FieldRefAccess<SEMan, Character>("m_character"); private static readonly FieldRef<Player, StatusEffect> GuardianSeOf = AccessTools.FieldRefAccess<Player, StatusEffect>("m_guardianSE"); [HarmonyPostfix] [HarmonyPatch(typeof(Player), "UpdateFood")] private static void OnUpdateFood(Player __instance, float dt, bool forceUpdate) { if (!forceUpdate) { UtangardTick.Run(__instance, dt); } } [HarmonyPrefix] [HarmonyPatch(typeof(Player), "CanConsumeItem")] private static bool OnCanConsumeItem(Player __instance, ItemData item, ref bool __result) { if (item == null || item.m_shared == null) { return true; } if (!BiomeGate.IsWithered(__instance)) { return true; } if ((item.m_shared.m_food > 0f || item.m_shared.m_foodStamina > 0f || item.m_shared.m_foodEitr > 0f) && UtangardConfig.BlockEating.Value) { return Refuse(__instance, ref __result, UtangardConfig.EatBlockedMessage.Value); } if (UtangardConfig.BlockNewBuffs.Value && BlockedEffects.IsBlocked(item.m_shared.m_consumeStatusEffect)) { return Refuse(__instance, ref __result, UtangardConfig.BuffBlockedMessage.Value); } return true; } private static bool Refuse(Player player, ref bool __result, string message) { if (!string.IsNullOrEmpty(message)) { ((Character)player).Message((MessageType)2, message, 0, (Sprite)null); } __result = false; return false; } [HarmonyPrefix] [HarmonyPatch(typeof(SEMan), "AddStatusEffect", new Type[] { typeof(StatusEffect), typeof(bool), typeof(int), typeof(float) })] private static bool OnAddStatusEffect(SEMan __instance, StatusEffect statusEffect, ref StatusEffect __result) { if (!UtangardConfig.BlockNewBuffs.Value) { return true; } if ((Object)(object)statusEffect == (Object)null) { return true; } if (!BlockedEffects.IsBlocked(statusEffect)) { return true; } Character obj = CharacterOf.Invoke(__instance); if (!BiomeGate.IsWithered((Player)(object)((obj is Player) ? obj : null))) { return true; } if (UtangardConfig.Verbose.Value) { UtangardPlugin.Log.LogInfo((object)("Refused status effect " + ((Object)statusEffect).name)); } __result = null; return false; } [HarmonyPrefix] [HarmonyPatch(typeof(SEMan), "Internal_AddStatusEffect")] private static bool OnInternalAddStatusEffect(SEMan __instance, int nameHash, ref StatusEffect __result) { if (!UtangardConfig.BlockNewBuffs.Value) { return true; } if (!BlockedEffects.IsBlockedHash(nameHash)) { return true; } Character obj = CharacterOf.Invoke(__instance); if (!BiomeGate.IsWithered((Player)(object)((obj is Player) ? obj : null))) { return true; } __result = null; return false; } [HarmonyPrefix] [HarmonyPatch(typeof(Player), "StartGuardianPower")] private static bool OnStartGuardianPower(Player __instance, ref bool __result) { if (!UtangardConfig.BlockNewBuffs.Value) { return true; } if (!BiomeGate.IsWithered(__instance)) { return true; } if (!BlockedEffects.IsBlocked(GuardianSeOf.Invoke(__instance))) { return true; } return Refuse(__instance, ref __result, UtangardConfig.BuffBlockedMessage.Value); } [HarmonyPostfix] [HarmonyPatch(typeof(Character), "OnDeath")] private static void OnCharacterDeath(Character __instance) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (UtangardConfig.Enabled.Value && UtangardConfig.GateOnGroup.Value && !((Object)(object)__instance == (Object)null)) { Progression.CreditAttendees(((Component)__instance).transform.position, __instance.m_defeatSetGlobalKey); } } [HarmonyPostfix] [HarmonyPatch(typeof(ObjectDB), "Awake")] private static void OnObjectDbAwake() { RebuildFromObjectDb(); } [HarmonyPostfix] [HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")] private static void OnObjectDbCopy() { RebuildFromObjectDb(); } private static void RebuildFromObjectDb() { BlockedEffects.Rebuild(); UtangardEffectsRegistry.Build(); } [HarmonyPostfix] [HarmonyPatch(typeof(Player), "OnSpawned")] private static void OnPlayerSpawned(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { if (UtangardConfig.LogGlobalKeys.Value) { GlobalKeyDump.Log(); } WarnIfGateIsUnenforceable(); } } private static void WarnIfGateIsUnenforceable() { if (!UtangardPlugin.CorePresent && UtangardConfig.GateOnGroup.Value) { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && instance.GetPeerConnections() > 0) { UtangardPlugin.Log.LogWarning((object)"The group gate is on in a multiplayer session, but Core is not installed. Nothing can refuse a player who does not have Utangard, so anyone without it is not gated at all. Install Core on the server and every client to enforce it, or set GateOnGroup = false to gate on the world instead."); } } } } [BepInPlugin("ezomic.valheim.utangard", "Utangard", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class UtangardPlugin : BaseUnityPlugin { public const string PluginGuid = "ezomic.valheim.utangard"; public const string PluginName = "Utangard"; public const string PluginVersion = "1.0.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; UtangardConfig.Bind(((BaseUnityPlugin)this).Config); TryRegisterWithCore(); _harmony = new Harmony("ezomic.valheim.utangard"); _harmony.PatchAll(typeof(UtangardPatches)); Log.LogInfo((object)"Utangard 1.0.0 by Robbin Thijssen - ready."); } 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.utangard", "Utangard", "1.0.0", ((BaseUnityPlugin)this).Config, (Requirement)0, (Assembly)null); Suite.Sync((ConfigEntryBase[])(object)new ConfigEntryBase[19] { (ConfigEntryBase)UtangardConfig.Enabled, (ConfigEntryBase)UtangardConfig.GateOnGroup, (ConfigEntryBase)UtangardConfig.GateNeverRegresses, (ConfigEntryBase)UtangardConfig.BackfillFromCharacter, (ConfigEntryBase)UtangardConfig.RosterDays, (ConfigEntryBase)UtangardConfig.RosterDaysPerBoss, (ConfigEntryBase)UtangardConfig.CatchUpDays, (ConfigEntryBase)UtangardConfig.CatchUpDaysPerBoss, (ConfigEntryBase)UtangardConfig.CreditRadius, (ConfigEntryBase)UtangardConfig.ExcludePlayerIds, (ConfigEntryBase)UtangardConfig.FoodDrainMultiplier, (ConfigEntryBase)UtangardConfig.BlockEating, (ConfigEntryBase)UtangardConfig.BuffDrainMultiplier, (ConfigEntryBase)UtangardConfig.BlockNewBuffs, (ConfigEntryBase)UtangardConfig.BlockRested, (ConfigEntryBase)UtangardConfig.AlsoBlock, (ConfigEntryBase)UtangardConfig.NeverBlock, (ConfigEntryBase)UtangardConfig.SappedStaminaRegen, (ConfigEntryBase)UtangardConfig.SappedMaxSeconds }); Suite.Sync(UtangardConfig.GateKeyEntries()); } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); } } } internal static class UtangardTick { private static readonly FieldRef<Player, List<Food>> FoodsOf = AccessTools.FieldRefAccess<Player, List<Food>>("m_foods"); private static readonly FieldRef<StatusEffect, float> TimeOf = AccessTools.FieldRefAccess<StatusEffect, float>("m_time"); private static bool _wasWithered; private static Player _watching; private const float PublishInterval = 5f; private static float _publishTimer; public static void Run(Player player, float dt) { if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } if (_watching != player) { _watching = player; _wasWithered = false; _publishTimer = 5f; } if (UtangardConfig.GateOnGroup.Value) { _publishTimer += dt; if (_publishTimer >= 5f) { _publishTimer = 0f; Progression.PublishLocal(player); } } bool flag = BiomeGate.IsWithered(player); if (flag != _wasWithered) { _wasWithered = flag; Announce(player, flag); } if (flag) { UtangardEffectsRegistry.Ensure(player); DrainFood(player, dt); DrainBuffs(player, dt); } } private static void DrainFood(Player player, float dt) { float num = (UtangardConfig.FoodDrainMultiplier.Value - 1f) * dt; if (num <= 0f) { return; } List<Food> list = FoodsOf.Invoke(player); if (list == null) { return; } for (int i = 0; i < list.Count; i++) { Food val = list[i]; if (val != null) { val.m_time -= num; } } } private static void DrainBuffs(Player player, float dt) { float num = (UtangardConfig.BuffDrainMultiplier.Value - 1f) * dt; if (num <= 0f) { return; } SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan == null) { return; } List<StatusEffect> statusEffects = sEMan.GetStatusEffects(); for (int i = 0; i < statusEffects.Count; i++) { StatusEffect val = statusEffects[i]; if (!((Object)(object)val == (Object)null) && !(val.m_ttl <= 0f) && BlockedEffects.IsBlocked(val)) { TimeOf.Invoke(val) += num; } } } private static void Announce(Player player, bool withered) { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) string text = (withered ? UtangardConfig.EnterMessage.Value : UtangardConfig.LeaveMessage.Value); string text2 = (withered ? BiomeGate.BlockersHere(player) : null); if (withered && !string.IsNullOrEmpty(text2) && UtangardConfig.NameTheBlockers.Value) { text = text + "\n" + UtangardConfig.BlockedByPrefix.Value + " " + text2; string text3 = UtangardConfig.RequiredKeyFor(player.GetCurrentBiome()); long num = ((text3 == null) ? (-1) : Progression.SecondsLeft(text3)); if (num >= 0) { text = text + " (" + GlobalKeyDump.Describe(num) + " left)"; } } if (!string.IsNullOrEmpty(text)) { ((Character)player).Message((MessageType)2, text, 0, (Sprite)null); } if (UtangardConfig.Verbose.Value) { UtangardPlugin.Log.LogInfo((object)((withered ? "Gate closed in " : "Gate opened in ") + ((object)player.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString() + (string.IsNullOrEmpty(text2) ? "" : (" - waiting on " + text2)))); } } } }