using 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 HarmonyLib; using Microsoft.CodeAnalysis; using RunicAwareness.Core; using RunicAwareness.Integration; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Awareness")] [assembly: AssemblyDescription("Bounded, display-only explanations of already-known Valheim state.")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Awareness")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: InternalsVisibleTo("RunicAwareness.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [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 RunicAwareness { public enum AwarenessTimerPrecision { WholeSecond = 1, FiveSeconds = 5, TenSeconds = 10, WholeMinute = 60 } public enum AwarenessOverlayAnchor { TopRight, TopLeft, BottomRight, BottomLeft, MiddleLeft } internal static class AwarenessConfig { internal const int HardMaximumFoodScan = 8; internal const int HardMaximumEffectScan = 32; internal const int HardMaximumComfortPieces = 64; internal const int HardMaximumPanelLines = 40; internal const int HardMaximumPanelCharacters = 4096; internal static ConfigEntry Enabled { get; private set; } internal static ConfigEntry ShowFoodTimers { get; private set; } internal static ConfigEntry ShowEffectTimers { get; private set; } internal static ConfigEntry ShowComfort { get; private set; } internal static ConfigEntry ShowItemComparison { get; private set; } internal static ConfigEntry ShowProduction { get; private set; } internal static ConfigEntry ShowAgriculture { get; private set; } internal static ConfigEntry ShowBuilding { get; private set; } internal static ConfigEntry ShowTamedAnimals { get; private set; } internal static ConfigEntry TimerPrecision { get; private set; } internal static ConfigEntry RefreshInterval { get; private set; } internal static ConfigEntry MaximumEffectRows { get; private set; } internal static ConfigEntry MaximumComfortRows { get; private set; } internal static ConfigEntry MaximumContextLines { get; private set; } internal static ConfigEntry UiScale { get; private set; } internal static ConfigEntry ControllerScaleMultiplier { get; private set; } internal static ConfigEntry Anchor { get; private set; } internal static ConfigEntry MiddleLeftAnchorMigrationApplied { get; private set; } internal static ConfigEntry VerboseLogging { get; private set; } internal static void Bind(ConfigFile config) { //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Expected O, but got Unknown //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0145: 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_0193: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Expected O, but got Unknown //IL_01cb: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Expected O, but got Unknown //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_020d: Expected O, but got Unknown Enabled = config.Bind("General", "Enabled", true, "Enable Runic Awareness. This module only observes and renders already-known local state."); ShowFoodTimers = Panel(config, "FoodTimers", defaultValue: true, "Show remaining time and the local food item that supplied each active food slot."); ShowEffectTimers = Panel(config, "EffectTimers", defaultValue: true, "Show locally active HUD status effects and their remaining time."); ShowComfort = Panel(config, "ComfortBreakdown", defaultValue: true, "Show current comfort, Rested time, and bounded category results from vanilla's own comfort pass."); ShowItemComparison = Panel(config, "ItemComparison", defaultValue: true, "Retain bounded item-comparison capture for compatibility; inventory UI suppression takes precedence, so this capture is not rendered as an Awareness panel while inventory is open."); ShowProduction = Panel(config, "ProductionContext", defaultValue: true, "Show the currently hovered station's visible state and optional Runic Production status when its runtime contract is available."); ShowAgriculture = Panel(config, "AgricultureContext", defaultValue: true, "Show bounded text already exposed by the currently hovered crop or beehive."); ShowBuilding = Panel(config, "BuildingContext", defaultValue: true, "Show the currently selected or hovered build piece's known transform, health, station level, and vanilla support legend."); ShowTamedAnimals = Panel(config, "TamedAnimalContext", defaultValue: true, "Show bounded text already exposed by the currently hovered tameable animal."); TimerPrecision = config.Bind("Display", "TimerPrecision", AwarenessTimerPrecision.WholeSecond, "Round remaining timers upward to whole seconds, five seconds, ten seconds, or whole minutes."); RefreshInterval = config.Bind("Display", "RefreshIntervalSeconds", 0.35f, new ConfigDescription("Low-frequency state sampling interval. Text is rebuilt only when a bounded state signature changes.", (AcceptableValueBase)(object)new AcceptableValueRange(0.2f, 2f), Array.Empty())); MaximumEffectRows = config.Bind("Display", "MaximumEffectRows", 5, new ConfigDescription("Maximum visible HUD-effect rows. The scan itself is hard-capped at 32 local effects.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 8), Array.Empty())); MaximumComfortRows = config.Bind("Display", "MaximumComfortWinnerRows", 6, new ConfigDescription("Maximum named comfort winners. Oversized vanilla comfort sets fail closed to a level-only explanation.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 8), Array.Empty())); MaximumContextLines = config.Bind("Display", "MaximumContextLines", 5, new ConfigDescription("Maximum lines copied from already-visible current-hover text.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 6), Array.Empty())); UiScale = config.Bind("Display", "UiScale", 1f, new ConfigDescription("Overlay text and panel scale.", (AcceptableValueBase)(object)new AcceptableValueRange(0.75f, 2f), Array.Empty())); ControllerScaleMultiplier = config.Bind("Display", "ControllerScaleMultiplier", 1.15f, new ConfigDescription("Additional readability scale while Valheim reports controller navigation as active.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 1.5f), Array.Empty())); Anchor = config.Bind("Display", "Anchor", AwarenessOverlayAnchor.MiddleLeft, "Place the non-interactive overlay at a safe-area anchor. The default is left-middle."); MiddleLeftAnchorMigrationApplied = config.Bind("Migrations", "DefaultAnchorMovedToMiddleLeft", false, "Internal one-time migration marker. Once true, later Anchor choices are never rewritten by this migration."); if (!MiddleLeftAnchorMigrationApplied.Value) { AwarenessOverlayAnchor awarenessOverlayAnchor = MigrateLegacyAnchor(Anchor.Value, migrationApplied: false); if (awarenessOverlayAnchor != Anchor.Value) { Anchor.Value = awarenessOverlayAnchor; } MiddleLeftAnchorMigrationApplied.Value = true; } VerboseLogging = config.Bind("Diagnostics", "VerboseLogging", false, "Log Awareness configuration refreshes. Never logs hidden world state."); } internal static AwarenessOverlayAnchor MigrateLegacyAnchor(AwarenessOverlayAnchor anchor, bool migrationApplied) { if (migrationApplied || anchor != AwarenessOverlayAnchor.TopRight) { return anchor; } return AwarenessOverlayAnchor.MiddleLeft; } private static ConfigEntry Panel(ConfigFile config, string key, bool defaultValue, string description) { return config.Bind("Panels", key, defaultValue, description); } } [BepInPlugin("chazman.RunicAwareness", "Runic Awareness", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicAwareness"; public const string Name = "Runic Awareness"; public const string Version = "1.0.0"; private Harmony _harmony; private AwarenessRuntime _runtime; private bool _languageSubscribed; internal static Plugin Instance { get; private set; } internal AwarenessRuntime Runtime => _runtime; private void Awake() { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Expected O, but got Unknown Instance = this; AwarenessConfig.Bind(((BaseUnityPlugin)this).Config); ((BaseUnityPlugin)this).Config.SettingChanged += OnSettingChanged; try { ValheimContracts.VerifyInstalledSignatures(); _runtime = new AwarenessRuntime(((BaseUnityPlugin)this).Logger, Application.isBatchMode); Localization.OnLanguageChange = (Action)Delegate.Combine(Localization.OnLanguageChange, new Action(OnLanguageChange)); _languageSubscribed = true; if (!Application.isBatchMode) { _harmony = new Harmony("chazman.RunicAwareness"); _harmony.PatchAll(typeof(Plugin).Assembly); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Awareness v1.0.0 ready: bounded local timers, comfort explanation, equipment comparison, and current-context display panels; gameplay state is never changed."); if (Application.isBatchMode) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Dedicated/batch process detected; the display runtime is intentionally inert."); } } catch (Exception ex) { Cleanup(); ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Awareness failed closed during startup; vanilla UI and gameplay remain unchanged. " + ex.GetType().Name + ": " + ex.Message)); } } private void Update() { try { _runtime?.Update(); } catch (Exception exception) { _runtime?.FailClosed("update", exception); } } private void OnGUI() { try { _runtime?.Draw(); } catch (Exception exception) { _runtime?.FailClosed("draw", exception); } } private void OnSettingChanged(object sender, SettingChangedEventArgs arguments) { try { AwarenessRuntime runtime = _runtime; if (runtime != null) { object definition; if (arguments == null) { definition = null; } else { ConfigEntryBase changedSetting = arguments.ChangedSetting; definition = ((changedSetting != null) ? changedSetting.Definition : null); } runtime.OnConfigurationChanged((ConfigDefinition)definition); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Awareness configuration refresh failed closed: " + ex.Message)); } } private void OnLanguageChange() { try { ComfortCapture.Reset(); ContextCapture.Reset(); HoverItemCapture.Reset(); _runtime?.OnLocalizationChanged(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Awareness language refresh failed closed: " + ex.Message)); } } private void OnDestroy() { ((BaseUnityPlugin)this).Config.SettingChanged -= OnSettingChanged; Cleanup(); Instance = null; } private void Cleanup() { if (_languageSubscribed) { Localization.OnLanguageChange = (Action)Delegate.Remove(Localization.OnLanguageChange, new Action(OnLanguageChange)); _languageSubscribed = false; } _runtime?.Dispose(); _runtime = null; Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; ContextCapture.Reset(); HoverItemCapture.Reset(); ComfortCapture.Reset(); } } } namespace RunicAwareness.Integration { internal sealed class AwarenessRuntime : IDisposable { private const int MaximumFoodRows = 3; private const int MaximumContextCharacters = 768; private readonly ManualLogSource _log; private readonly bool _inert; private readonly PanelCache _panels = new PanelCache(); private readonly StringBuilder _builder = new StringBuilder(1024); private readonly OptionalPortalPanelAdapter _portalPanels = new OptionalPortalPanelAdapter(); private readonly GUIContent _content = new GUIContent(); private GUIStyle _textStyle; private GUIStyle _boxStyle; private float _nextRefresh; private float _nextStationLevelRefresh; private bool _failed; private bool _layoutDirty = true; private int _styleFontSize; private float _cachedHeight; private float _lastInnerWidth; private float _lastMaximumHeight; private int _hookFailureCount; private ulong _foodSignature; private ulong _effectSignature; private ulong _comfortSignature; private ulong _itemSignature; private ulong _contextSignature; private Piece _lastBuildingPiece; private int _cachedStationLevel; private bool _hadFoodSignature; private bool _hadEffectSignature; private bool _hadComfortSignature; private bool _hadItemSignature; private bool _hadContextSignature; internal AwarenessRuntime(ManualLogSource log, bool inert) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown _log = log ?? throw new ArgumentNullException("log"); _inert = inert; } internal void Update() { if (_inert || _failed) { return; } float unscaledTime = Time.unscaledTime; if (unscaledTime < _nextRefresh && unscaledTime >= 0f) { return; } _nextRefresh = unscaledTime + FiniteClamp(AwarenessConfig.RefreshInterval.Value, 0.2f, 2f, 0.35f); if (!AwarenessConfig.Enabled.Value) { ClearAll(); return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { ClearAll(); return; } if (InventoryGui.IsVisible()) { UpdateItemComparison(localPlayer, unscaledTime); return; } _panels.Clear(AwarenessPanel.ItemComparison); UpdateFood(localPlayer); UpdateEffects(localPlayer); UpdateComfort(localPlayer); UpdateContext(localPlayer, unscaledTime); } internal void Draw() { //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0103: 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_002e: Invalid comparison between Unknown and I4 //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_0238: Unknown result type (might be due to invalid IL or missing references) //IL_023d: Unknown result type (might be due to invalid IL or missing references) //IL_027f: Unknown result type (might be due to invalid IL or missing references) //IL_0284: Unknown result type (might be due to invalid IL or missing references) if (_inert || _failed || !AwarenessConfig.Enabled.Value || Event.current == null || (int)Event.current.type != 7 || ShouldSuppressOverlay()) { return; } bool inventoryOnly = InventoryGui.IsVisible(); if (_panels.Compose(inventoryOnly)) { _content.text = _panels.ComposedText; _layoutDirty = true; } if (!string.IsNullOrEmpty(_content.text)) { bool flag = false; try { flag = ZInput.IsGamepadActive(); } catch (Exception) { } float num = Mathf.Clamp(FiniteClamp(AwarenessConfig.UiScale.Value, 0.75f, 2f, 1f) * (flag ? FiniteClamp(AwarenessConfig.ControllerScaleMultiplier.Value, 1f, 1.5f, 1.15f) : 1f), 0.75f, 3f); int num2 = Mathf.Clamp(Mathf.RoundToInt(14f * num), 11, 42); EnsureStyles(num2); Rect safeArea = Screen.safeArea; float num3 = Mathf.Max(10f, 14f * num); float num4 = Mathf.Max(120f, ((Rect)(ref safeArea)).width - num3 * 2f); float num5 = Mathf.Min(280f, num4); float num6 = Mathf.Clamp(430f * num, num5, num4); float num7 = Mathf.Max(120f, num6 - num3 * 1.6f); float num8 = Mathf.Max((float)num2 + num3, ((Rect)(ref safeArea)).height - num3 * 2f); if (Mathf.Abs(num7 - _lastInnerWidth) > 0.5f) { _lastInnerWidth = num7; _layoutDirty = true; } if (Mathf.Abs(num8 - _lastMaximumHeight) > 0.5f) { _lastMaximumHeight = num8; _layoutDirty = true; } if (_layoutDirty) { _cachedHeight = Mathf.Min(num8, _textStyle.CalcHeight(_content, num7) + num3 * 1.4f); _layoutDirty = false; } float height = Mathf.Min(num8, Mathf.Max((float)num2 + num3, _cachedHeight)); Rect val = AnchoredRect(safeArea, Screen.height, num6, height, num3, AwarenessConfig.Anchor.Value); Rect val2 = new Rect(((Rect)(ref val)).x + num3 * 0.8f, ((Rect)(ref val)).y + num3 * 0.65f, ((Rect)(ref val)).width - num3 * 1.6f, ((Rect)(ref val)).height - num3 * 1.3f); GUI.Box(val, GUIContent.none, _boxStyle); GUI.Label(val2, _content, _textStyle); } } internal void OnConfigurationChanged(ConfigDefinition definition) { _nextRefresh = 0f; ResetSignatures(); if (definition == (ConfigDefinition)null || (definition.Section == "Display" && definition.Key == "MaximumComfortWinnerRows")) { ComfortCapture.Reset(); } _styleFontSize = 0; _layoutDirty = true; if (!AwarenessConfig.ShowFoodTimers.Value) { _panels.Clear(AwarenessPanel.Food); } if (!AwarenessConfig.ShowEffectTimers.Value) { _panels.Clear(AwarenessPanel.Effects); } if (!AwarenessConfig.ShowComfort.Value) { _panels.Clear(AwarenessPanel.Comfort); } if (!AwarenessConfig.ShowComfort.Value) { ComfortCapture.Reset(); } if (!AwarenessConfig.ShowItemComparison.Value) { _panels.Clear(AwarenessPanel.ItemComparison); HoverItemCapture.Reset(); } if (!AwarenessConfig.ShowProduction.Value) { _panels.Clear(AwarenessPanel.Production); } if (!AwarenessConfig.ShowAgriculture.Value) { _panels.Clear(AwarenessPanel.Agriculture); } if (!AwarenessConfig.ShowBuilding.Value) { _panels.Clear(AwarenessPanel.Building); } if (!AwarenessConfig.ShowTamedAnimals.Value) { _panels.Clear(AwarenessPanel.TamedAnimal); } if (!AwarenessConfig.ShowProduction.Value && !AwarenessConfig.ShowAgriculture.Value && !AwarenessConfig.ShowBuilding.Value && !AwarenessConfig.ShowTamedAnimals.Value) { ContextCapture.Reset(); } if (!AwarenessConfig.Enabled.Value) { ComfortCapture.Reset(); HoverItemCapture.Reset(); ContextCapture.Reset(); ClearAll(); } if (AwarenessConfig.VerboseLogging.Value) { _log.LogInfo((object)("Awareness configuration refreshed: " + ((definition == (ConfigDefinition)null) ? "unknown" : (definition.Section + "/" + definition.Key)))); } } internal void OnLocalizationChanged() { _nextRefresh = 0f; ResetSignatures(); ClearAll(); if (AwarenessConfig.VerboseLogging.Value) { _log.LogInfo((object)"Awareness display cache cleared for a language change."); } } internal void FailClosed(string operation, Exception exception) { if (!_failed) { _failed = true; ClearAll(); _log.LogError((object)("Runic Awareness display runtime stopped after " + operation + "; gameplay and vanilla UI remain unchanged. " + exception.GetType().Name + ": " + exception.Message)); } } internal void FailHook(string hook, Exception exception) { if (_hookFailureCount < 3) { _hookFailureCount++; _log.LogWarning((object)("Awareness ignored a display capture failure in " + hook + ": " + exception.GetType().Name + ".")); } } public void Dispose() { ClearAll(); _textStyle = null; _boxStyle = null; } private void UpdateFood(Player player) { if (!AwarenessConfig.ShowFoodTimers.Value) { _panels.Clear(AwarenessPanel.Food); return; } List foods = player.GetFoods(); int num = foods?.Count ?? 0; ulong num2 = HashStart(num, (int)AwarenessConfig.TimerPrecision.Value); if (num > 8) { num2 = HashMix(num2, num); if (!SameSignature(ref _foodSignature, ref _hadFoodSignature, num2)) { _panels.Set(AwarenessPanel.Food, "Details withheld: local food list exceeds the hard limit of " + 8 + "."); } return; } for (int i = 0; i < num; i++) { Food val = foods[i]; num2 = HashMix(num2, TimerFormatter.Bucket(val?.m_time ?? 0f, AwarenessConfig.TimerPrecision.Value)); num2 = HashMix(num2, BoundedText.HashBounded(FoodName(val), 96)); } if (SameSignature(ref _foodSignature, ref _hadFoodSignature, num2)) { return; } if (num == 0) { _panels.Clear(AwarenessPanel.Food); return; } _builder.Clear(); int num3 = Math.Min(num, 3); for (int j = 0; j < num3; j++) { if (j > 0) { _builder.Append('\n'); } Food val2 = foods[j]; _builder.Append(BoundedLocalization.Label(FoodName(val2))).Append(": "); TimerFormatter.Append(_builder, TimerFormatter.Bucket(val2?.m_time ?? 0f, AwarenessConfig.TimerPrecision.Value)); } if (num > num3) { _builder.Append("\n+").Append(num - num3).Append(" more local food slots"); } _panels.Set(AwarenessPanel.Food, BoundedText.Sanitize(_builder.ToString(), 512, 4)); } private void UpdateEffects(Player player) { if (!AwarenessConfig.ShowEffectTimers.Value) { _panels.Clear(AwarenessPanel.Effects); return; } SEMan sEMan = ((Character)player).GetSEMan(); List list = ((sEMan != null) ? sEMan.GetStatusEffects() : null); int num = list?.Count ?? 0; ulong hash = HashStart(num, (int)AwarenessConfig.TimerPrecision.Value); if (num > 32) { hash = HashMix(hash, num); if (!SameSignature(ref _effectSignature, ref _hadEffectSignature, hash)) { _panels.Set(AwarenessPanel.Effects, "Details withheld: local effect list exceeds the hard limit of " + 32 + "."); } return; } int num2 = 0; for (int i = 0; i < num; i++) { StatusEffect val = list[i]; if (!((Object)(object)val == (Object)null) && !((Object)(object)val.m_icon == (Object)null)) { num2++; int value = ((val.m_ttl > 0f) ? TimerFormatter.Bucket(val.GetRemaningTime(), AwarenessConfig.TimerPrecision.Value) : (-1)); hash = HashMix(hash, value); hash = HashMix(hash, BoundedText.HashBounded(val.m_name, 96)); } } hash = HashMix(hash, num2); if (SameSignature(ref _effectSignature, ref _hadEffectSignature, hash)) { return; } if (num2 == 0) { _panels.Clear(AwarenessPanel.Effects); return; } _builder.Clear(); int num3 = Math.Max(1, Math.Min(8, AwarenessConfig.MaximumEffectRows.Value)); int num4 = 0; for (int j = 0; j < num; j++) { if (num4 >= num3) { break; } StatusEffect val2 = list[j]; if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2.m_icon == (Object)null)) { if (num4 > 0) { _builder.Append('\n'); } _builder.Append(BoundedLocalization.Label(val2.m_name)).Append(": "); if (val2.m_ttl > 0f) { TimerFormatter.Append(_builder, TimerFormatter.Bucket(val2.GetRemaningTime(), AwarenessConfig.TimerPrecision.Value)); } else { _builder.Append("active"); } num4++; } } if (num2 > num4) { _builder.Append("\n+").Append(num2 - num4).Append(" more HUD effects"); } _panels.Set(AwarenessPanel.Effects, BoundedText.Sanitize(_builder.ToString(), 768, num3 + 1)); } private void UpdateComfort(Player player) { if (!AwarenessConfig.ShowComfort.Value) { _panels.Clear(AwarenessPanel.Comfort); return; } int num = Math.Max(0, player.GetComfortLevel()); bool flag = player.InShelter(); ComfortCapture.TryGet(player, num, flag, Time.unscaledTime, out var captured); SEMan sEMan = ((Character)player).GetSEMan(); StatusEffect val = ((sEMan != null) ? sEMan.GetStatusEffect(SEMan.s_statusEffectRested) : null); int num2 = (((Object)(object)val != (Object)null && val.m_ttl > 0f) ? TimerFormatter.Bucket(val.GetRemaningTime(), AwarenessConfig.TimerPrecision.Value) : (-1)); ulong hash = HashStart(num, flag ? 1 : 0); hash = HashMix(hash, captured.Version); hash = HashMix(hash, num2); if (!SameSignature(ref _comfortSignature, ref _hadComfortSignature, hash)) { _builder.Clear(); _builder.Append("Level ").Append(num).Append(flag ? " (sheltered)" : " (not sheltered)"); _builder.Append("\nRested: "); if (num2 >= 0) { TimerFormatter.Append(_builder, num2); } else { _builder.Append("inactive"); } if (!string.IsNullOrEmpty(captured.Detail)) { _builder.Append('\n').Append(captured.Detail); } else { _builder.Append("\nCategory winners appear after vanilla's next comfort check."); } _panels.Set(AwarenessPanel.Comfort, BoundedText.Sanitize(_builder.ToString(), 1024, 12)); } } private void UpdateItemComparison(Player player, float now) { if (!AwarenessConfig.ShowItemComparison.Value || !HoverItemCapture.TryGet(player, now, out var item) || item?.m_shared == null) { _panels.Clear(AwarenessPanel.ItemComparison); _hadItemSignature = false; return; } string text = ((object)Unsafe.As(ref item.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString(); ItemComparisonDisposition itemComparisonDisposition = ItemTypePolicy.Classify(text); if (itemComparisonDisposition == ItemComparisonDisposition.HideKnownNonEquipment) { _panels.Clear(AwarenessPanel.ItemComparison); _hadItemSignature = false; return; } try { ItemData val = ((itemComparisonDisposition == ItemComparisonDisposition.CompareKnownEquipment) ? ValheimContracts.EquippedFor(player, item) : null); ItemMetrics itemMetrics = ReadMetrics(player, item, text); ItemMetrics? itemMetrics2 = ((val?.m_shared == null) ? ((ItemMetrics?)null) : new ItemMetrics?(ReadMetrics(player, val, ((object)Unsafe.As(ref val.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString()))); ulong current = ItemSignature(item, val, itemMetrics, itemMetrics2); if (!SameSignature(ref _itemSignature, ref _hadItemSignature, current)) { _panels.Set(AwarenessPanel.ItemComparison, ItemComparisonFormatter.Format(itemMetrics, itemMetrics2, item == val)); } } catch (Exception exception) { _panels.Clear(AwarenessPanel.ItemComparison); _hadItemSignature = false; FailHook("item comparison", exception); } } private void UpdateContext(Player player, float now) { Piece val = (AwarenessConfig.ShowBuilding.Value ? player.GetHoveringPiece() : null); if ((Object)(object)val != (Object)null) { ClearContextExcept(AwarenessPanel.Building); if (AllowsDetailedDisclosure(player, (Component)(object)val)) { UpdateBuilding(val, now); } else { UpdateBuildingUnavailable(); } return; } if (!ContextCapture.TryGet(((Humanoid)player).GetHoverObject(), now, out var context) || !ContextEnabled(context.Kind)) { ClearContextPanels(); if (_hadContextSignature) { _hadContextSignature = false; _contextSignature = 0uL; } return; } AwarenessPanel awarenessPanel = PanelFor(context.Kind); ClearContextExcept(awarenessPanel); ulong hash = HashStart((int)context.Kind, (!((Object)(object)context.Subject == (Object)null)) ? ((Object)context.Subject).GetInstanceID() : 0); hash = HashMix(hash, BoundedText.HashBounded(context.VisibleText, 768)); if (!SameSignature(ref _contextSignature, ref _hadContextSignature, hash)) { string text = BoundedText.Sanitize(context.VisibleText, 768, Math.Max(1, Math.Min(6, AwarenessConfig.MaximumContextLines.Value))); if (string.IsNullOrEmpty(text)) { _panels.Clear(awarenessPanel); } else { _panels.Set(awarenessPanel, text); } } } private void UpdateBuilding(Piece piece, float now) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Unknown result type (might be due to invalid IL or missing references) WearNTear componentInParent = ((Component)piece).GetComponentInParent(); float num = (((Object)(object)componentInParent == (Object)null) ? (-1f) : (componentInParent.GetHealthPercentage() * 100f)); CraftingStation craftingStation = piece.m_craftingStation; if (piece != _lastBuildingPiece || now >= _nextStationLevelRefresh) { _lastBuildingPiece = piece; _cachedStationLevel = ((!((Object)(object)craftingStation == (Object)null)) ? Math.Max(0, craftingStation.GetLevel(false)) : 0); _nextStationLevelRefresh = now + 2f; } Vector3 position = ((Component)piece).transform.position; Vector3 eulerAngles = ((Component)piece).transform.eulerAngles; ulong hash = HashStart(((Object)piece).GetInstanceID(), Quantize(num)); hash = HashMix(hash, Quantize(position.x)); hash = HashMix(hash, Quantize(position.y)); hash = HashMix(hash, Quantize(position.z)); hash = HashMix(hash, Quantize(eulerAngles.x)); hash = HashMix(hash, Quantize(eulerAngles.y)); hash = HashMix(hash, Quantize(eulerAngles.z)); hash = HashMix(hash, _cachedStationLevel); if (!SameSignature(ref _contextSignature, ref _hadContextSignature, hash)) { _builder.Clear(); _builder.Append(BoundedLocalization.Label(piece.m_name)); if (num >= 0f) { _builder.Append(" | health ").Append(num.ToString("0", CultureInfo.InvariantCulture)).Append('%'); } _builder.Append("\nPosition: ").Append(VectorText(position)); _builder.Append("\nRotation: ").Append(VectorText(eulerAngles)); if (_cachedStationLevel > 0) { _builder.Append("\nStation level: ").Append(_cachedStationLevel); } _builder.Append("\nSupport colors: blue grounded; green/yellow/orange supported; red weakest."); _panels.Set(AwarenessPanel.Building, BoundedText.Sanitize(_builder.ToString(), 768, 6)); } } private void UpdateBuildingUnavailable() { ulong current = HashStart(6, 1431191894); if (!SameSignature(ref _contextSignature, ref _hadContextSignature, current)) { _lastBuildingPiece = null; _cachedStationLevel = 0; _nextStationLevelRefresh = 0f; _panels.Set(AwarenessPanel.Building, "Building details unavailable outside avatar interaction reach or without current ward access."); } } private unsafe static ItemMetrics ReadMetrics(Player player, ItemData item, string itemType) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) SkillType skillType = item.m_shared.m_skillType; float skillFactor = ((Character)player).GetSkillFactor(skillType); float skillLevel = ((Character)player).GetSkillLevel(skillType); string name = BoundedLocalization.Label(item.m_shared.m_name); int quality = item.m_quality; DamageTypes damage = item.GetDamage(); return new ItemMetrics(name, itemType, quality, ((DamageTypes)(ref damage)).GetTotalDamage(), item.GetArmor(), item.GetBlockPower(skillFactor), item.m_shared.m_movementModifier * 100f, item.GetNonStackedWeight(), item.m_durability, item.GetMaxDurability(), ((object)(*(SkillType*)(&skillType))/*cast due to .constrained prefix*/).ToString(), skillLevel); } private static ulong ItemSignature(ItemData selected, ItemData equipped, ItemMetrics selectedMetrics, ItemMetrics? equippedMetrics) { ulong signature = HashStart(RuntimeHelpers.GetHashCode(selected), (equipped != null) ? RuntimeHelpers.GetHashCode(equipped) : 0); signature = HashMetrics(signature, selectedMetrics); if (equippedMetrics.HasValue) { signature = HashMix(signature, 1); signature = HashMetrics(signature, equippedMetrics.Value); } return signature; } private static ulong HashMetrics(ulong signature, ItemMetrics metrics) { signature = HashMix(signature, BoundedText.HashBounded(metrics.Name, 96)); signature = HashMix(signature, BoundedText.HashBounded(metrics.ItemType, 96)); signature = HashMix(signature, metrics.Quality); signature = HashMix(signature, Quantize(metrics.Damage)); signature = HashMix(signature, Quantize(metrics.Armor)); signature = HashMix(signature, Quantize(metrics.Block)); signature = HashMix(signature, Quantize(metrics.MovementPercent)); signature = HashMix(signature, Quantize(metrics.Weight)); signature = HashMix(signature, Quantize(metrics.Durability)); signature = HashMix(signature, Quantize(metrics.MaximumDurability)); signature = HashMix(signature, BoundedText.HashBounded(metrics.SkillName, 96)); return HashMix(signature, Quantize(metrics.SkillLevel)); } private static string FoodName(Food food) { return food?.m_item?.m_shared?.m_name ?? food?.m_name ?? string.Empty; } private static bool AllowsDetailedDisclosure(Player player, Component subject) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: 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_0049: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || (Object)(object)subject == (Object)null) { return false; } Transform transform = subject.transform; Transform transform2 = ((Component)player).transform; if ((Object)(object)transform == (Object)null || (Object)(object)transform2 == (Object)null) { return false; } Vector3 position = transform.position; Vector3 val = position - transform2.position; if (ContextDisclosurePolicy.AllowsDetailedDisclosure(((Vector3)(ref val)).sqrMagnitude, player.m_maxInteractDistance)) { return StrictWardDisclosure.Allows(position); } return false; } private bool ContextEnabled(AwarenessContextKind kind) { return kind switch { AwarenessContextKind.Production => AwarenessConfig.ShowProduction.Value, AwarenessContextKind.Agriculture => AwarenessConfig.ShowAgriculture.Value, AwarenessContextKind.Building => AwarenessConfig.ShowBuilding.Value, AwarenessContextKind.TamedAnimal => AwarenessConfig.ShowTamedAnimals.Value, _ => false, }; } private static AwarenessPanel PanelFor(AwarenessContextKind kind) { return kind switch { AwarenessContextKind.Production => AwarenessPanel.Production, AwarenessContextKind.Agriculture => AwarenessPanel.Agriculture, AwarenessContextKind.Building => AwarenessPanel.Building, AwarenessContextKind.TamedAnimal => AwarenessPanel.TamedAnimal, _ => throw new ArgumentOutOfRangeException("kind"), }; } private void ClearContextPanels() { _panels.Clear(AwarenessPanel.Production); _panels.Clear(AwarenessPanel.Agriculture); _panels.Clear(AwarenessPanel.Building); _panels.Clear(AwarenessPanel.TamedAnimal); } private void ClearContextExcept(AwarenessPanel retained) { if (retained != AwarenessPanel.Production) { _panels.Clear(AwarenessPanel.Production); } if (retained != AwarenessPanel.Agriculture) { _panels.Clear(AwarenessPanel.Agriculture); } if (retained != AwarenessPanel.Building) { _panels.Clear(AwarenessPanel.Building); } if (retained != AwarenessPanel.TamedAnimal) { _panels.Clear(AwarenessPanel.TamedAnimal); } } private void ClearAll() { for (int i = 0; i <= 7; i++) { _panels.Clear((AwarenessPanel)i); } _content.text = string.Empty; _layoutDirty = true; ResetSignatures(); _lastBuildingPiece = null; _cachedStationLevel = 0; _nextStationLevelRefresh = 0f; } private void ResetSignatures() { _foodSignature = (_effectSignature = (_comfortSignature = (_itemSignature = (_contextSignature = 0uL)))); _hadFoodSignature = (_hadEffectSignature = (_hadComfortSignature = (_hadItemSignature = (_hadContextSignature = false)))); } private bool ShouldSuppressOverlay() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || Menu.IsVisible() || Console.IsVisible() || TextInput.IsVisible() || StoreGui.IsVisible() || InventoryGui.IsVisible() || Minimap.IsOpen() || Hud.IsPieceSelectionVisible() || PlayerCustomizaton.IsBarberGuiVisible() || Feedback.IsVisible() || UnifiedPopup.IsVisible() || ConnectPanel.IsVisible() || ZInput.VirtualKeyboardOpen) { return true; } TextViewer instance = TextViewer.instance; if ((Object)(object)instance != (Object)null && instance.IsVisible()) { return true; } Chat instance2 = Chat.instance; if ((Object)(object)instance2 != (Object)null && (instance2.HasFocus() || instance2.IsChatDialogWindowVisible())) { return true; } if (Game.IsPaused() || (Object)(object)Hud.instance == (Object)null || Hud.instance.m_userHidden) { return true; } return _portalPanels.SetupPanelVisible(localPlayer); } private void EnsureStyles(int fontSize) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown //IL_005a: Expected O, but got Unknown //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_00a3: Unknown result type (might be due to invalid IL or missing references) if (_textStyle == null || _boxStyle == null || _styleFontSize != fontSize) { _textStyle = new GUIStyle(GUI.skin.label) { richText = true, wordWrap = true, fontSize = fontSize, alignment = (TextAnchor)0, padding = new RectOffset(0, 0, 0, 0) }; _textStyle.normal.textColor = new Color(0.92f, 0.95f, 1f, 1f); _boxStyle = new GUIStyle(GUI.skin.box); _boxStyle.normal.textColor = Color.clear; _styleFontSize = fontSize; _lastInnerWidth = 0f; _lastMaximumHeight = 0f; _layoutDirty = true; } } internal static Rect AnchoredRect(Rect safe, float screenHeight, float width, float height, float margin, AwarenessOverlayAnchor anchor) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) bool flag = anchor == AwarenessOverlayAnchor.MiddleLeft; bool num = anchor == AwarenessOverlayAnchor.TopRight || anchor == AwarenessOverlayAnchor.BottomRight; bool flag2 = anchor == AwarenessOverlayAnchor.BottomRight || anchor == AwarenessOverlayAnchor.BottomLeft; float num2 = (num ? (((Rect)(ref safe)).xMax - width - margin) : (((Rect)(ref safe)).xMin + margin)); float num3 = screenHeight - ((Rect)(ref safe)).yMax; float num4 = screenHeight - ((Rect)(ref safe)).yMin; float num5 = (flag ? (num3 + (((Rect)(ref safe)).height - height) * 0.5f) : (flag2 ? (num4 - height - margin) : (num3 + margin))); return new Rect(num2, num5, width, height); } private static bool SameSignature(ref ulong stored, ref bool hasStored, ulong current) { if (hasStored && stored == current) { return true; } stored = current; hasStored = true; return false; } private static ulong HashStart(int left, int right) { return HashMix(HashMix(1469598103934665603uL, left), right); } private static ulong HashMix(ulong hash, int value) { hash ^= (uint)value; return hash * 1099511628211L; } private static ulong HashMix(ulong hash, ulong value) { hash ^= value; return hash * 1099511628211L; } private static int Quantize(float value) { if (float.IsNaN(value) || float.IsInfinity(value)) { return 0; } double num = Math.Round((double)value * 10.0, MidpointRounding.AwayFromZero); if (!(num > 2147483647.0)) { if (!(num < -2147483648.0)) { return (int)num; } return int.MinValue; } return int.MaxValue; } private static float FiniteClamp(float value, float minimum, float maximum, float fallback) { if (float.IsNaN(value) || float.IsInfinity(value)) { value = fallback; } return Math.Max(minimum, Math.Min(maximum, value)); } private static string VectorText(Vector3 value) { return value.x.ToString("0.0", CultureInfo.InvariantCulture) + ", " + value.y.ToString("0.0", CultureInfo.InvariantCulture) + ", " + value.z.ToString("0.0", CultureInfo.InvariantCulture); } } internal static class BoundedLocalization { internal const int MaximumTokenCharacters = 128; private static readonly FieldInfo Translations = ResolveTranslations(); internal static bool IsSupported => Translations != null; internal static string Label(string raw) { if (string.IsNullOrEmpty(raw)) { return string.Empty; } if (!TryGetTranslationKey(raw, out var key)) { return BoundedText.Label(raw); } try { Localization instance = Localization.instance; string value = key; if (instance != null && Translations != null && Translations.GetValue(instance) is Dictionary dictionary && dictionary.TryGetValue(key, out var value2) && value2 != null) { value = value2; } return BoundedText.Label(value); } catch { return BoundedText.Label(key); } } internal static bool TryGetTranslationKey(string raw, out string key) { key = null; if (string.IsNullOrEmpty(raw) || raw.Length < 2 || raw.Length > 128 || raw[0] != '$') { return false; } int i; for (i = 1; i < raw.Length && raw[i] == '$'; i++) { } if (i == raw.Length) { return false; } for (int j = i; j < raw.Length; j++) { char c = raw[j]; if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_' && c != '-' && c != '.') { return false; } } key = raw.Substring(i); if (key.StartsWith("KEY_", StringComparison.Ordinal)) { key = null; return false; } return true; } private static FieldInfo ResolveTranslations() { try { FieldInfo field = typeof(Localization).GetField("m_translations", BindingFlags.Instance | BindingFlags.NonPublic); return (field != null && field.FieldType == typeof(Dictionary) && !field.IsStatic) ? field : null; } catch { return null; } } } internal readonly struct CapturedComfort { internal int Level { get; } internal bool Sheltered { get; } internal string Detail { get; } internal ulong Signature { get; } internal int Version { get; } internal int SourceInstanceId { get; } internal float CapturedAt { get; } internal CapturedComfort(int level, bool sheltered, string detail, ulong signature, int version) : this(level, sheltered, detail, signature, version, 0, 0f) { } internal CapturedComfort(int level, bool sheltered, string detail, ulong signature, int version, int sourceInstanceId, float capturedAt) { Level = level; Sheltered = sheltered; Detail = detail ?? string.Empty; Signature = signature; Version = version; SourceInstanceId = sourceInstanceId; CapturedAt = capturedAt; } } internal static class ComfortCapture { internal const float MaximumAgeSeconds = 5f; private static readonly StringBuilder Builder = new StringBuilder(512); private static CapturedComfort _current; internal static CapturedComfort Current => _current; internal static bool TryGet(Player player, int level, bool sheltered, float now, out CapturedComfort captured) { captured = _current; if ((Object)(object)player != (Object)null && captured.Version > 0 && captured.SourceInstanceId == ((Object)player).GetInstanceID() && captured.Level == level && captured.Sheltered == sheltered && now >= captured.CapturedAt && now - captured.CapturedAt <= 5f) { return true; } captured = default(CapturedComfort); return false; } internal static void Record(Player player, int level) { if (Plugin.Instance?.Runtime == null || !AwarenessConfig.Enabled.Value || !AwarenessConfig.ShowComfort.Value || (Object)(object)player == (Object)null || player != Player.m_localPlayer) { return; } bool flag = player.InShelter(); FieldInfo comfortPiecesField = ValheimContracts.ComfortPiecesField; List list = (flag ? (comfortPiecesField.GetValue(null) as List) : null); int num = list?.Count ?? 0; int instanceID = ((Object)player).GetInstanceID(); ulong hash = Hash(level, flag, list, num); hash = Mix(hash, instanceID); if (_current.Version > 0 && _current.Signature == hash) { _current = new CapturedComfort(_current.Level, _current.Sheltered, _current.Detail, _current.Signature, _current.Version, _current.SourceInstanceId, Time.unscaledTime); return; } Builder.Clear(); if (num > 64) { Builder.Append("Nearby category details withheld: vanilla returned ").Append(num).Append(" comfort pieces (hard limit ") .Append(64) .Append(")."); } else { AppendWinners(Builder, list, num, flag); if (!flag) { if (Builder.Length > 0) { Builder.Append('\n'); } Builder.Append("Known opportunity: shelter is not currently counted."); } } _current = new CapturedComfort(Math.Max(0, level), flag, BoundedText.Sanitize(Builder.ToString(), 768, 10), hash, _current.Version + 1, instanceID, Time.unscaledTime); } internal static void Reset() { _current = default(CapturedComfort); Builder.Clear(); } private static void AppendWinners(StringBuilder builder, List pieces, int count, bool sheltered) { //IL_0086: Unknown result type (might be due to invalid IL or missing references) int num = Math.Max(1, Math.Min(8, AwarenessConfig.MaximumComfortRows.Value)); int num2 = 0; int num3 = 0; Piece val = null; builder.Append("Counted: base +1"); if (sheltered) { builder.Append(", shelter +1"); } for (int i = 0; i < count; i++) { Piece val2 = pieces[i]; if ((Object)(object)val2 == (Object)null) { continue; } int num4; if ((Object)(object)val != (Object)null) { if (((object)Unsafe.As(ref val2.m_comfortGroup)/*cast due to .constrained prefix*/).ToString() != "None") { ref ComfortGroup comfortGroup = ref val2.m_comfortGroup; object obj = val.m_comfortGroup; if (((object)Unsafe.As(ref comfortGroup)/*cast due to .constrained prefix*/).Equals(obj)) { num4 = 1; goto IL_00b6; } } num4 = (string.Equals(val2.m_name, val.m_name, StringComparison.Ordinal) ? 1 : 0); } else { num4 = 0; } goto IL_00b6; IL_00b6: int comfort = val2.GetComfort(); if (num4 != 0 || comfort <= 0) { num3++; } else { num2++; if (num2 <= num) { builder.Append(", ").Append(BoundedLocalization.Label(val2.m_name)).Append(" +") .Append(comfort); } } val = val2; } if (num2 > num) { builder.Append(" (+").Append(num2 - num).Append(" more winners)"); } if (num3 > 0) { builder.Append("\nNearby alternatives not counted: ").Append(num3).Append(" (same category/name or zero comfort)."); } } private static ulong Hash(int level, bool sheltered, List pieces, int count) { ulong hash = 1469598103934665603uL; hash = Mix(hash, level); hash = Mix(hash, sheltered ? 1 : 0); hash = Mix(hash, count); if (pieces == null || count > 64) { return hash; } for (int i = 0; i < count; i++) { Piece val = pieces[i]; if ((Object)(object)val == (Object)null) { hash = Mix(hash, -1); continue; } hash = Mix(hash, ((object)Unsafe.As(ref val.m_comfortGroup)/*cast due to .constrained prefix*/).GetHashCode()); hash = Mix(hash, val.GetComfort()); hash ^= BoundedText.HashBounded(val.m_name, 96); hash *= 1099511628211L; } return hash; } private static ulong Mix(ulong hash, int value) { hash ^= (uint)value; return hash * 1099511628211L; } } internal enum AwarenessContextKind { None, Production, Agriculture, Building, TamedAnimal } internal readonly struct CapturedContext { internal AwarenessContextKind Kind { get; } internal Component Source { get; } internal Component Subject { get; } internal string VisibleText { get; } internal float CapturedAt { get; } internal CapturedContext(AwarenessContextKind kind, Component source, Component subject, string visibleText, float capturedAt) { Kind = kind; Source = source; Subject = subject; VisibleText = visibleText ?? string.Empty; CapturedAt = capturedAt; } } internal static class ContextCapture { internal const float MaximumAgeSeconds = 0.8f; internal const int MaximumVisibleTextCharacters = 8192; private static CapturedContext _current; internal static void Record(AwarenessContextKind kind, Component source, Component subject, string visibleText) { if (Plugin.Instance?.Runtime != null && AwarenessConfig.Enabled.Value && IsEnabled(kind) && !((Object)(object)source == (Object)null) && !((Object)(object)subject == (Object)null) && kind != AwarenessContextKind.None) { if (visibleText != null && visibleText.Length > 8192) { _current = default(CapturedContext); } else { _current = new CapturedContext(kind, source, subject, visibleText, Time.unscaledTime); } } } internal static bool TryGet(GameObject hoverObject, float now, out CapturedContext context) { context = _current; if ((Object)(object)hoverObject == (Object)null || (Object)(object)context.Source == (Object)null || (Object)(object)context.Subject == (Object)null || now < context.CapturedAt || now - context.CapturedAt > 0.8f) { _current = default(CapturedContext); return false; } Transform transform = hoverObject.transform; if (!SameHierarchy(transform, context.Source.transform)) { return SameHierarchy(transform, context.Subject.transform); } return true; } internal static void Reset() { _current = default(CapturedContext); } private static bool IsEnabled(AwarenessContextKind kind) { return kind switch { AwarenessContextKind.Production => AwarenessConfig.ShowProduction.Value, AwarenessContextKind.Agriculture => AwarenessConfig.ShowAgriculture.Value, AwarenessContextKind.Building => AwarenessConfig.ShowBuilding.Value, AwarenessContextKind.TamedAnimal => AwarenessConfig.ShowTamedAnimals.Value, _ => false, }; } private static bool SameHierarchy(Transform left, Transform right) { if ((Object)(object)left == (Object)null || (Object)(object)right == (Object)null) { return false; } Transform val = left; int num = 0; while ((Object)(object)val != (Object)null && num < 16) { if ((Object)(object)val == (Object)(object)right) { return true; } num++; val = val.parent; } val = right; int num2 = 0; while ((Object)(object)val != (Object)null && num2 < 16) { if ((Object)(object)val == (Object)(object)left) { return true; } num2++; val = val.parent; } return false; } } internal static class HoverItemCapture { internal const float MaximumAgeSeconds = 0.8f; private static ItemData _item; private static float _capturedAt; private static int _sourcePlayerInstanceId; internal static void Record(ItemData item) { Player localPlayer = Player.m_localPlayer; if (Plugin.Instance?.Runtime != null && AwarenessConfig.Enabled.Value && AwarenessConfig.ShowItemComparison.Value && item != null && !((Object)(object)localPlayer == (Object)null)) { _item = item; _capturedAt = Time.unscaledTime; _sourcePlayerInstanceId = ((Object)localPlayer).GetInstanceID(); } } internal static bool TryGet(Player player, float now, out ItemData item) { item = _item; if ((Object)(object)player != (Object)null && item != null && _sourcePlayerInstanceId == ((Object)player).GetInstanceID() && now >= _capturedAt && now - _capturedAt <= 0.8f) { return true; } Reset(); item = null; return false; } internal static void Reset() { _item = null; _capturedAt = 0f; _sourcePlayerInstanceId = 0; } } [HarmonyPatch(typeof(InventoryGrid), "CreateItemTooltip", new Type[] { typeof(ItemData), typeof(UITooltip) })] internal static class InventoryGridCreateItemTooltipPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(ItemData __0) { try { HoverItemCapture.Record(__0); } catch (Exception exception) { Plugin.Instance?.Runtime?.FailHook("item tooltip", exception); } } } [HarmonyPatch(typeof(SE_Rested), "CalculateComfortLevel", new Type[] { typeof(Player) })] internal static class RestedCalculateComfortPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Player __0, int __result) { try { ComfortCapture.Record(__0, __result); } catch (Exception exception) { Plugin.Instance?.Runtime?.FailHook("comfort capture", exception); } } } [HarmonyPatch(typeof(CraftingStation), "GetHoverText", new Type[] { })] internal static class CraftingStationHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(CraftingStation __instance, string __result) { SafeContext.Record(AwarenessContextKind.Building, (Component)(object)__instance, (Component)(object)__instance, __result, "crafting station hover"); } } [HarmonyPatch(typeof(CookingStation), "GetHoverText", new Type[] { })] internal static class CookingStationHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(CookingStation __instance, string __result) { SafeContext.Record(AwarenessContextKind.Production, (Component)(object)__instance, (Component)(object)__instance, __result, "cooking station hover"); } } [HarmonyPatch(typeof(Fermenter), "GetHoverText", new Type[] { })] internal static class FermenterHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Fermenter __instance, string __result) { SafeContext.Record(AwarenessContextKind.Production, (Component)(object)__instance, (Component)(object)__instance, __result, "fermenter hover"); } } [HarmonyPatch(typeof(Plant), "GetHoverText", new Type[] { })] internal static class PlantHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Plant __instance, string __result) { SafeContext.Record(AwarenessContextKind.Agriculture, (Component)(object)__instance, (Component)(object)__instance, __result, "plant hover"); } } [HarmonyPatch(typeof(Beehive), "GetHoverText", new Type[] { })] internal static class BeehiveHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Beehive __instance, string __result) { SafeContext.Record(AwarenessContextKind.Agriculture, (Component)(object)__instance, (Component)(object)__instance, __result, "beehive hover"); } } [HarmonyPatch(typeof(Tameable), "GetHoverText", new Type[] { })] internal static class TameableHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Tameable __instance, string __result) { SafeContext.Record(AwarenessContextKind.TamedAnimal, (Component)(object)__instance, (Component)(object)__instance, __result, "tameable hover"); } } [HarmonyPatch(typeof(Switch), "GetHoverText", new Type[] { })] internal static class ProductionSwitchHoverPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Switch __instance, string __result) { try { if ((Object)(object)__instance == (Object)null) { return; } Smelter componentInParent = ((Component)__instance).GetComponentInParent(); if ((Object)(object)componentInParent != (Object)null) { ContextCapture.Record(AwarenessContextKind.Production, (Component)(object)__instance, (Component)(object)componentInParent, __result); return; } CookingStation componentInParent2 = ((Component)__instance).GetComponentInParent(); if ((Object)(object)componentInParent2 != (Object)null) { ContextCapture.Record(AwarenessContextKind.Production, (Component)(object)__instance, (Component)(object)componentInParent2, __result); return; } Fermenter componentInParent3 = ((Component)__instance).GetComponentInParent(); if ((Object)(object)componentInParent3 != (Object)null) { ContextCapture.Record(AwarenessContextKind.Production, (Component)(object)__instance, (Component)(object)componentInParent3, __result); } } catch (Exception exception) { Plugin.Instance?.Runtime?.FailHook("production switch hover", exception); } } } internal static class SafeContext { internal static void Record(AwarenessContextKind kind, Component source, Component subject, string text, string hook) { try { ContextCapture.Record(kind, source, subject, text); } catch (Exception exception) { Plugin.Instance?.Runtime?.FailHook(hook, exception); } } } internal sealed class OptionalPortalPanelAdapter { internal const string PortalPluginGuid = "chazman.RunicPortals"; private BaseUnityPlugin _plugin; private ConfigEntry _enabled; private ConfigEntry _universalRouting; private ConfigEntry _showSetupPanel; private int _lastPortalHoverFrame = -10; internal bool SetupPanelVisible(Player player) { if ((Object)(object)player == (Object)null) { _lastPortalHoverFrame = -10; return false; } if (!TryBind()) { return false; } if (!_enabled.Value || !_universalRouting.Value || !_showSetupPanel.Value) { _lastPortalHoverFrame = -10; return false; } GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if ((Object)(object)hoverObject != (Object)null && (Object)(object)hoverObject.GetComponentInParent() != (Object)null) { _lastPortalHoverFrame = Time.frameCount; } int num = Time.frameCount - _lastPortalHoverFrame; if (num >= 0) { return num <= 1; } return false; } private bool TryBind() { try { if (!Chainloader.PluginInfos.TryGetValue("chazman.RunicPortals", out var value) || (Object)(object)((value != null) ? value.Instance : null) == (Object)null) { Clear(); return false; } BaseUnityPlugin instance = value.Instance; if (_plugin == instance && _enabled != null && _universalRouting != null && _showSetupPanel != null) { return true; } ConfigFile config = instance.Config; ConfigEntry enabled = default(ConfigEntry); ConfigEntry universalRouting = default(ConfigEntry); ConfigEntry showSetupPanel = default(ConfigEntry); if (config == null || !config.TryGetEntry("General", "Enabled", ref enabled) || !config.TryGetEntry("Features", "UniversalRouting", ref universalRouting) || !config.TryGetEntry("Display", "ShowSetupPanel", ref showSetupPanel)) { Clear(); return false; } _plugin = instance; _enabled = enabled; _universalRouting = universalRouting; _showSetupPanel = showSetupPanel; return true; } catch (Exception) { Clear(); return false; } } private void Clear() { _plugin = null; _enabled = null; _universalRouting = null; _showSetupPanel = null; _lastPortalHoverFrame = -10; } } internal static class StrictWardDisclosure { private delegate bool WardStateDelegate(PrivateArea area); private delegate bool WardContainsDelegate(PrivateArea area, Vector3 point, float radius); internal const int MaximumWardAreasExamined = 4096; private static readonly List WardAreas = ResolveWardAreas(); private static readonly WardStateDelegate WardEnabled = ResolveWardState("IsEnabled"); private static readonly WardStateDelegate WardLocalAccess = ResolveWardState("HaveLocalAccess"); private static readonly WardContainsDelegate WardContains = ResolveWardContains(); internal static bool IsSupported { get { if (WardAreas != null && WardEnabled != null && WardLocalAccess != null) { return WardContains != null; } return false; } } internal static bool Allows(Vector3 position) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (!IsSupported || !IsFinite(position)) { return false; } try { int count = WardAreas.Count; if (count > 4096) { return false; } for (int i = 0; i < count; i++) { PrivateArea val = WardAreas[i]; if (!((Object)(object)val == (Object)null)) { bool num = WardEnabled(val); bool flag = num && WardContains(val, position, 0f); bool localAccess = !flag || WardLocalAccess(val); if (IsHostileOverlap(num, flag, localAccess)) { return false; } } } return true; } catch { return false; } } internal static bool IsHostileOverlap(bool enabled, bool containsTarget, bool localAccess) { if (enabled && containsTarget) { return !localAccess; } return false; } private static bool IsFinite(Vector3 value) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001a: 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_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) if (!float.IsNaN(value.x) && !float.IsInfinity(value.x) && !float.IsNaN(value.y) && !float.IsInfinity(value.y) && !float.IsNaN(value.z)) { return !float.IsInfinity(value.z); } return false; } private static List ResolveWardAreas() { try { FieldInfo field = typeof(PrivateArea).GetField("m_allAreas", BindingFlags.Static | BindingFlags.NonPublic); return (field != null && field.FieldType == typeof(List)) ? (field.GetValue(null) as List) : null; } catch { return null; } } private static WardStateDelegate ResolveWardState(string name) { try { MethodInfo method = typeof(PrivateArea).GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); return (method != null && method.ReturnType == typeof(bool) && !method.IsStatic) ? ((WardStateDelegate)method.CreateDelegate(typeof(WardStateDelegate))) : null; } catch { return null; } } private static WardContainsDelegate ResolveWardContains() { try { MethodInfo method = typeof(PrivateArea).GetMethod("IsInside", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2] { typeof(Vector3), typeof(float) }, null); return (method != null && method.ReturnType == typeof(bool) && !method.IsStatic) ? ((WardContainsDelegate)method.CreateDelegate(typeof(WardContainsDelegate))) : null; } catch { return null; } } } internal static class ValheimContracts { private const BindingFlags InstanceFields = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static FieldInfo _rightItem; private static FieldInfo _leftItem; private static FieldInfo _helmetItem; private static FieldInfo _chestItem; private static FieldInfo _legItem; private static FieldInfo _shoulderItem; private static FieldInfo _utilityItem; private static FieldInfo _trinketItem; private static FieldInfo _ammoItem; private static FieldInfo _comfortPieces; private static bool _verified; internal static FieldInfo ComfortPiecesField => _comfortPieces ?? throw new InvalidOperationException("Valheim contracts are not initialized."); internal static void VerifyInstalledSignatures() { if (!_verified) { RequireMethod(typeof(Player), "GetFoods", typeof(List), false); RequireMethod(typeof(Player), "GetComfortLevel", typeof(int), false); RequireMethod(typeof(Player), "GetHoverObject", typeof(GameObject), false); RequireMethod(typeof(Player), "GetHoveringPiece", typeof(Piece), false); RequireMethod(typeof(Player), "GetSEMan", typeof(SEMan), false); RequireMethod(typeof(Player), "InShelter", typeof(bool), false); RequireMethod(typeof(Minimap), "IsOpen", typeof(bool), true); RequireMethod(typeof(Hud), "IsPieceSelectionVisible", typeof(bool), true); RequireMethod(typeof(Chat), "HasFocus", typeof(bool), false); RequireMethod(typeof(Chat), "IsChatDialogWindowVisible", typeof(bool), false); RequireMethod(typeof(PlayerCustomizaton), "IsBarberGuiVisible", typeof(bool), true); RequireMethod(typeof(Feedback), "IsVisible", typeof(bool), true); RequireMethod(typeof(UnifiedPopup), "IsVisible", typeof(bool), true); RequireMethod(typeof(ConnectPanel), "IsVisible", typeof(bool), true); RequireMethod(typeof(TextViewer), "IsVisible", typeof(bool), false); RequireMethod(typeof(SEMan), "GetStatusEffects", typeof(List), false); RequireMethod(typeof(SEMan), "GetStatusEffect", typeof(StatusEffect), false, typeof(int)); RequireMethod(typeof(StatusEffect), "GetRemaningTime", typeof(float), false); RequireField(typeof(Localization), "m_translations", typeof(Dictionary)); if (!BoundedLocalization.IsSupported) { throw new MissingFieldException(typeof(Localization).FullName, "m_translations"); } RequireMethod(typeof(InventoryGrid), "CreateItemTooltip", typeof(void), false, typeof(ItemData), typeof(UITooltip)); RequireMethod(typeof(SE_Rested), "CalculateComfortLevel", typeof(int), true, typeof(Player)); RequireMethod(typeof(CraftingStation), "GetHoverText", typeof(string), false); RequireMethod(typeof(CookingStation), "GetHoverText", typeof(string), false); RequireMethod(typeof(Fermenter), "GetHoverText", typeof(string), false); RequireMethod(typeof(Plant), "GetHoverText", typeof(string), false); RequireMethod(typeof(Beehive), "GetHoverText", typeof(string), false); RequireMethod(typeof(Tameable), "GetHoverText", typeof(string), false); RequireMethod(typeof(Switch), "GetHoverText", typeof(string), false); RequireMethod(typeof(WearNTear), "GetHealthPercentage", typeof(float), false); RequireMethod(typeof(CraftingStation), "GetLevel", typeof(int), false, typeof(bool)); RequireField(typeof(PrivateArea), "m_allAreas", typeof(List), BindingFlags.Static | BindingFlags.NonPublic); RequireMethod(typeof(PrivateArea), "IsEnabled", typeof(bool), false); RequireMethod(typeof(PrivateArea), "HaveLocalAccess", typeof(bool), false); RequireMethod(typeof(PrivateArea), "IsInside", typeof(bool), false, typeof(Vector3), typeof(float)); if (!StrictWardDisclosure.IsSupported) { throw new MissingMethodException(typeof(PrivateArea).FullName, "strict disclosure delegates"); } _rightItem = RequireField(typeof(Humanoid), "m_rightItem", typeof(ItemData)); _leftItem = RequireField(typeof(Humanoid), "m_leftItem", typeof(ItemData)); _helmetItem = RequireField(typeof(Humanoid), "m_helmetItem", typeof(ItemData)); _chestItem = RequireField(typeof(Humanoid), "m_chestItem", typeof(ItemData)); _legItem = RequireField(typeof(Humanoid), "m_legItem", typeof(ItemData)); _shoulderItem = RequireField(typeof(Humanoid), "m_shoulderItem", typeof(ItemData)); _utilityItem = RequireField(typeof(Humanoid), "m_utilityItem", typeof(ItemData)); _trinketItem = RequireField(typeof(Humanoid), "m_trinketItem", typeof(ItemData)); _ammoItem = RequireField(typeof(Humanoid), "m_ammoItem", typeof(ItemData)); _comfortPieces = RequireField(typeof(SE_Rested), "s_tempPieces", typeof(List), BindingFlags.Static | BindingFlags.NonPublic); _verified = true; } } internal static ItemData EquippedFor(Player player, ItemData selected) { if (!_verified || (Object)(object)player == (Object)null || selected?.m_shared == null) { return null; } string text = ((object)Unsafe.As(ref selected.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString(); switch (text) { case "Helmet": return ReadItem(_helmetItem, (Humanoid)(object)player); case "Chest": return ReadItem(_chestItem, (Humanoid)(object)player); case "Legs": return ReadItem(_legItem, (Humanoid)(object)player); case "Shoulder": return ReadItem(_shoulderItem, (Humanoid)(object)player); case "Utility": return ReadItem(_utilityItem, (Humanoid)(object)player); case "Trinket": return ReadItem(_trinketItem, (Humanoid)(object)player); case "Ammo": return ReadItem(_ammoItem, (Humanoid)(object)player); case "Shield": return ReadItem(_leftItem, (Humanoid)(object)player); case "TwoHandedWeaponLeft": return ReadItem(_leftItem, (Humanoid)(object)player); default: { ItemData val = ReadItem(_rightItem, (Humanoid)(object)player); if (val != null && val.m_shared != null && string.Equals(((object)Unsafe.As(ref val.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString(), text, StringComparison.Ordinal)) { return val; } ItemData val2 = ReadItem(_leftItem, (Humanoid)(object)player); if (val2 == null || val2.m_shared == null || !string.Equals(((object)Unsafe.As(ref val2.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString(), text, StringComparison.Ordinal)) { return null; } return val2; } } } private static ItemData ReadItem(FieldInfo field, Humanoid player) { object? obj = field?.GetValue(player); return (ItemData)((obj is ItemData) ? obj : null); } private static MethodInfo RequireMethod(Type type, string name, Type returnType, bool isStatic, params Type[] parameters) { MethodInfo method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, parameters ?? Type.EmptyTypes, null); if (method == null || method.ReturnType != returnType || method.IsStatic != isStatic) { throw new MissingMethodException(type.FullName, name); } return method; } private static FieldInfo RequireField(Type type, string name, Type fieldType) { return RequireField(type, name, fieldType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } private static FieldInfo RequireField(Type type, string name, Type fieldType, BindingFlags flags) { FieldInfo field = type.GetField(name, flags); if (field == null || field.FieldType != fieldType) { throw new MissingFieldException(type.FullName, name); } return field; } } } namespace RunicAwareness.Core { internal static class BoundedText { internal const int MaximumLabelCharacters = 96; internal static string Sanitize(string value, int maximumCharacters, int maximumLines) { maximumCharacters = Math.Max(1, maximumCharacters); maximumLines = Math.Max(1, maximumLines); if (string.IsNullOrEmpty(value)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(Math.Min(maximumCharacters, value.Length)); int num = 1; bool flag = false; int num2 = Math.Min(value.Length, Math.Max(256, maximumCharacters * 4)); for (int i = 0; i < num2 && stringBuilder.Length < maximumCharacters; i++) { char c = value[i]; switch (c) { case '<': { int num3 = FindTagEnd(value, i, 64); if (num3 >= 0) { i = num3; continue; } c = '['; goto default; } case '>': c = ']'; goto default; default: switch (c) { case '\n': break; default: { UnicodeCategory unicodeCategory = char.GetUnicodeCategory(c); if (unicodeCategory == UnicodeCategory.Control || unicodeCategory == UnicodeCategory.Format || IsBidiControl(c)) { continue; } if (c == '\t' || char.IsWhiteSpace(c)) { if (!flag && stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '\n') { stringBuilder.Append(' '); flag = true; } } else { stringBuilder.Append(c); flag = false; } continue; } case '\r': continue; } if (num < maximumLines) { TrimTrailingSpace(stringBuilder); if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '\n') { stringBuilder.Append('\n'); num++; } flag = false; continue; } break; } break; } TrimTrailingSpace(stringBuilder); while (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] == '\n') { stringBuilder.Length--; } if ((stringBuilder.Length == maximumCharacters && value.Length > maximumCharacters) || num2 < value.Length) { AddEllipsis(stringBuilder, maximumCharacters); } return stringBuilder.ToString(); } internal static string Label(string value) { return Sanitize(value, 96, 1); } internal static ulong HashBounded(string value, int maximumCharacters) { ulong num = 1469598103934665603uL; if (value == null) { return num; } int num2 = Math.Min(Math.Max(0, maximumCharacters), value.Length); for (int i = 0; i < num2; i++) { num ^= value[i]; num *= 1099511628211L; } num ^= (ulong)Math.Min(value.Length, maximumCharacters + 1); return num * 1099511628211L; } internal static int CountLines(string value) { if (string.IsNullOrEmpty(value)) { return 0; } int num = 1; for (int i = 0; i < value.Length; i++) { if (value[i] == '\n') { num++; } } return num; } private static int FindTagEnd(string value, int start, int maximumDistance) { int num = Math.Min(value.Length, start + maximumDistance + 1); for (int i = start + 1; i < num; i++) { if (value[i] == '>') { return i; } } return -1; } private static bool IsBidiControl(char value) { if ((value < '\u202a' || value > '\u202e') && (value < '\u2066' || value > '\u2069') && (value < '\u200b' || value > '\u200f') && value != '\u061c') { return value == '\ufeff'; } return true; } private static void TrimTrailingSpace(StringBuilder builder) { while (builder.Length > 0 && builder[builder.Length - 1] == ' ') { builder.Length--; } } private static void AddEllipsis(StringBuilder builder, int maximumCharacters) { if (maximumCharacters == 1) { builder[0] = '…'; return; } builder.Length = Math.Min(builder.Length, maximumCharacters - 1); builder.Append('…'); } } internal static class ContextDisclosurePolicy { internal const float HardMaximumDetailReachMeters = 10f; internal static bool AllowsDetailedDisclosure(float squaredAvatarDistance, float vanillaInteractDistance) { if (float.IsNaN(squaredAvatarDistance) || float.IsInfinity(squaredAvatarDistance) || float.IsNaN(vanillaInteractDistance) || float.IsInfinity(vanillaInteractDistance) || squaredAvatarDistance < 0f || vanillaInteractDistance <= 0f) { return false; } float num = Math.Min(vanillaInteractDistance, 10f); return squaredAvatarDistance <= num * num; } } internal readonly struct ItemMetrics { internal string Name { get; } internal string ItemType { get; } internal int Quality { get; } internal float Damage { get; } internal float Armor { get; } internal float Block { get; } internal float MovementPercent { get; } internal float Weight { get; } internal float Durability { get; } internal float MaximumDurability { get; } internal string SkillName { get; } internal float SkillLevel { get; } internal ItemMetrics(string name, string itemType, int quality, float damage, float armor, float block, float movementPercent, float weight, float durability, float maximumDurability, string skillName, float skillLevel) { Name = name ?? string.Empty; ItemType = itemType ?? string.Empty; Quality = Math.Max(0, quality); Damage = FiniteOrZero(damage); Armor = FiniteOrZero(armor); Block = FiniteOrZero(block); MovementPercent = FiniteOrZero(movementPercent); Weight = FiniteOrZero(weight); Durability = FiniteOrZero(durability); MaximumDurability = FiniteOrZero(maximumDurability); SkillName = skillName ?? string.Empty; SkillLevel = FiniteOrZero(skillLevel); } private static float FiniteOrZero(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return value; } return 0f; } } internal static class ItemComparisonFormatter { internal const int MaximumLines = 8; internal static string Format(ItemMetrics selected, ItemMetrics? equipped, bool isEquipped) { StringBuilder stringBuilder = new StringBuilder(384); stringBuilder.Append(BoundedText.Label(selected.Name)); if (isEquipped) { stringBuilder.Append(" (equipped)"); } else if (equipped.HasValue) { stringBuilder.Append(" vs ").Append(BoundedText.Label(equipped.Value.Name)); } else { stringBuilder.Append(" vs empty slot"); } int lines = 1; AppendText(stringBuilder, ref lines, "Type", BoundedText.Label(selected.ItemType)); AppendInteger(stringBuilder, ref lines, "Quality", selected.Quality, equipped?.Quality); AppendNumber(stringBuilder, ref lines, "Damage (item)", selected.Damage, equipped?.Damage, selected.Damage > 0f || (equipped?.Damage ?? 0f) > 0f); AppendNumber(stringBuilder, ref lines, "Armor", selected.Armor, equipped?.Armor, selected.Armor > 0f || (equipped?.Armor ?? 0f) > 0f); AppendNumber(stringBuilder, ref lines, "Block (skill)", selected.Block, equipped?.Block, selected.Block > 0f || (equipped?.Block ?? 0f) > 0f); AppendNumber(stringBuilder, ref lines, "Move", selected.MovementPercent, equipped?.MovementPercent, include: true, "%"); if (lines < 8 && !string.IsNullOrEmpty(selected.SkillName)) { stringBuilder.Append("\nSkill: ").Append(BoundedText.Label(selected.SkillName)).Append(' ') .Append(FormatNumber(selected.SkillLevel)); lines++; } AppendNumber(stringBuilder, ref lines, "Weight", selected.Weight, equipped?.Weight, include: true); if (lines < 8 && selected.MaximumDurability > 0f) { stringBuilder.Append("\nDurability: ").Append(FormatNumber(selected.Durability)).Append('/') .Append(FormatNumber(selected.MaximumDurability)); lines++; } return BoundedText.Sanitize(stringBuilder.ToString(), 640, 8); } private static void AppendText(StringBuilder builder, ref int lines, string label, string value) { if (lines < 8 && !string.IsNullOrEmpty(value)) { builder.Append('\n').Append(label).Append(": ") .Append(value); lines++; } } private static void AppendInteger(StringBuilder builder, ref int lines, string label, int current, int? comparison) { if (lines < 8) { builder.Append('\n').Append(label).Append(": ") .Append(current); if (comparison.HasValue) { AppendDelta(builder, current - comparison.Value, string.Empty); } lines++; } } private static void AppendNumber(StringBuilder builder, ref int lines, string label, float current, float? comparison, bool include, string suffix = "") { if (include && lines < 8) { builder.Append('\n').Append(label).Append(": ") .Append(FormatNumber(current)) .Append(suffix); if (comparison.HasValue) { AppendDelta(builder, current - comparison.Value, suffix); } lines++; } } private static void AppendDelta(StringBuilder builder, float delta, string suffix) { if (!(Math.Abs(delta) < 0.05f)) { builder.Append(" ("); if (delta > 0f) { builder.Append('+'); } builder.Append(FormatNumber(delta)).Append(suffix).Append(')'); } } private static string FormatNumber(float value) { return value.ToString("0.#", CultureInfo.InvariantCulture); } } internal enum ItemComparisonDisposition { HideKnownNonEquipment, CompareKnownEquipment, ShowRawUnsupported } internal static class ItemTypePolicy { internal static ItemComparisonDisposition Classify(string type) { switch (type) { case "Helmet": case "Shield": case "Chest": case "Hands": case "Torch": case "Legs": case "Ammo": case "Tool": case "Shoulder": case "Trinket": case "Utility": case "OneHandedWeapon": case "TwoHandedWeapon": case "Attach_Atgeir": case "TwoHandedWeaponLeft": case "Bow": return ItemComparisonDisposition.CompareKnownEquipment; case "Trophy": case "None": case "Misc": case "Fish": case "Material": case "Customization": case "Consumable": case "AmmoNonEquipable": return ItemComparisonDisposition.HideKnownNonEquipment; default: if (!string.IsNullOrEmpty(type)) { return ItemComparisonDisposition.ShowRawUnsupported; } return ItemComparisonDisposition.HideKnownNonEquipment; } } } internal enum AwarenessPanel { Food, Effects, Comfort, ItemComparison, Production, Agriculture, Building, TamedAnimal } internal sealed class PanelCache { private static readonly string[] Titles = new string[8] { "Food", "Effects", "Comfort", "Item comparison", "Production", "Agriculture", "Building", "Tamed animal" }; private readonly string[] _bodies = new string[Titles.Length]; private readonly int[] _bodyLines = new int[Titles.Length]; private readonly StringBuilder _builder = new StringBuilder(4096); private int _version; private int _composedVersion = -1; private bool _composedInventoryOnly; private string _composed = string.Empty; private int _composedLines; internal int Version => _version; internal string ComposedText => _composed; internal int ComposedLines => _composedLines; internal bool Set(AwarenessPanel panel, string body) { body = body ?? string.Empty; if (string.Equals(_bodies[(int)panel], body, StringComparison.Ordinal)) { return false; } _bodies[(int)panel] = body; _bodyLines[(int)panel] = BoundedText.CountLines(body); _version++; return true; } internal bool Clear(AwarenessPanel panel) { return Set(panel, string.Empty); } internal bool Compose(bool inventoryOnly) { if (_composedVersion == _version && _composedInventoryOnly == inventoryOnly) { return false; } _builder.Clear(); int num = 0; for (int i = 0; i < _bodies.Length; i++) { bool flag = i == 3; if (inventoryOnly == flag && !string.IsNullOrEmpty(_bodies[i])) { int num2 = _bodyLines[i] + 1 + ((_builder.Length != 0) ? 1 : 0); if (num + num2 > 40) { break; } int num3 = ((_builder.Length != 0) ? 2 : 0) + 3 + Titles[i].Length + 5 + _bodies[i].Length; if (_builder.Length + num3 > 4096) { break; } if (_builder.Length > 0) { _builder.Append("\n\n"); num++; } _builder.Append("").Append(Titles[i]).Append("\n"); _builder.Append(_bodies[i]); num += _bodyLines[i] + 1; } } _composed = _builder.ToString(); _composedLines = num; _composedVersion = _version; _composedInventoryOnly = inventoryOnly; return true; } } internal static class TimerFormatter { internal const int MaximumDisplaySeconds = 359999; internal static int Bucket(float remainingSeconds, AwarenessTimerPrecision precision) { if (float.IsNaN(remainingSeconds) || float.IsInfinity(remainingSeconds) || remainingSeconds <= 0f) { return 0; } int num = Math.Max(1, (int)precision); double num2 = Math.Min(359999f, remainingSeconds); return Math.Min(359999, checked((int)Math.Ceiling(num2 / (double)num) * num)); } internal static void Append(StringBuilder builder, int bucketSeconds) { int num = Math.Max(0, Math.Min(359999, bucketSeconds)); int num2 = num / 3600; int value = num / 60 % 60; int value2 = num % 60; if (num2 > 0) { builder.Append(num2).Append(':'); AppendTwoDigits(builder, value); builder.Append(':'); AppendTwoDigits(builder, value2); } else { builder.Append(value).Append(':'); AppendTwoDigits(builder, value2); } } internal static string Format(int bucketSeconds) { StringBuilder stringBuilder = new StringBuilder(10); Append(stringBuilder, bucketSeconds); return stringBuilder.ToString(); } private static void AppendTwoDigits(StringBuilder builder, int value) { if (value < 10) { builder.Append('0'); } builder.Append(value); } } }