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 Odins Nudge v0.1.1
plugins/OdinsNudge.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("OdinsNudge")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyInformationalVersion("0.1.1+21f5148171e5c66ec0f3299e1ba04eae9b5c8cef")] [assembly: AssemblyProduct("OdinsNudge")] [assembly: AssemblyTitle("OdinsNudge")] [assembly: AssemblyVersion("0.1.1.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 OdinsNudge { internal static class HudText { private sealed class Scale : MonoBehaviour { private const float ReferenceWidth = 1920f; private const float ReferenceHeight = 1080f; private static readonly FieldRef<float> GameGuiScale = TryFindGuiScale(); internal CanvasScaler Scaler; private float _logged = -1f; private void Update() { if (!((Object)(object)Scaler == (Object)null)) { float num = ((GameGuiScale != null) ? GameGuiScale.Invoke() : 1f); if (num <= 0f) { num = 1f; } float num2 = Mathf.Min((float)Screen.width / 1920f, (float)Screen.height / 1080f) * num * Plugin.TextScale.Value; if (Mathf.Abs(Scaler.scaleFactor - num2) > 0.0001f) { Scaler.scaleFactor = num2; } if (Plugin.DebugLog.Value && Mathf.Abs(_logged - num2) > 0.0001f) { _logged = num2; Plugin.Log.LogInfo((object)$"Text scale {num2:0.###}: screen {Screen.width}x{Screen.height}, game GUI scale {num:0.##}, TextScale {Plugin.TextScale.Value:0.##}."); } } } private static FieldRef<float> TryFindGuiScale() { FieldInfo fieldInfo = AccessTools.Field(typeof(GuiScaler), "m_largeGuiScale"); if (fieldInfo == null || fieldInfo.FieldType != typeof(float) || !fieldInfo.IsStatic) { Plugin.Log.LogWarning((object)"Couldn't find the game's GUI scale; text follows the screen size only."); return null; } return AccessTools.StaticFieldRefAccess<float>(fieldInfo); } } private static Canvas _canvas; public static TMP_Text Create(string name, Transform owner) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) TMP_Text val = (((Object)(object)MessageHud.instance != (Object)null) ? MessageHud.instance.m_messageCenterText : null); if ((Object)(object)val == (Object)null || (Object)(object)val.font == (Object)null || !EnsureCanvas(owner)) { return null; } GameObject val2 = new GameObject(name, new Type[1] { typeof(RectTransform) }); val2.transform.SetParent(((Component)_canvas).transform, false); TextMeshProUGUI obj = val2.AddComponent<TextMeshProUGUI>(); ((TMP_Text)obj).font = val.font; ((TMP_Text)obj).fontSharedMaterial = val.fontSharedMaterial; ((TMP_Text)obj).alignment = (TextAlignmentOptions)514; ((TMP_Text)obj).textWrappingMode = (TextWrappingModes)1; ((Graphic)obj).raycastTarget = false; ((TMP_Text)obj).outlineWidth = 0.15f; ((TMP_Text)obj).outlineColor = new Color32((byte)0, (byte)0, (byte)0, (byte)200); RectTransform val3 = (RectTransform)val2.transform; val3.pivot = new Vector2(0.5f, 0.5f); val3.anchoredPosition = Vector2.zero; return (TMP_Text)(object)obj; } private static bool EnsureCanvas(Transform owner) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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) if ((Object)(object)_canvas != (Object)null) { return true; } GameObject val = new GameObject("OdinsNudge_Canvas"); val.transform.SetParent(owner, false); _canvas = val.AddComponent<Canvas>(); _canvas.renderMode = (RenderMode)0; _canvas.sortingOrder = 5000; CanvasScaler val2 = val.AddComponent<CanvasScaler>(); val2.uiScaleMode = (ScaleMode)0; val.AddComponent<Scale>().Scaler = val2; Plugin.Log.LogInfo((object)"On-screen text layer created."); return true; } } internal sealed class Nudge : MonoBehaviour { internal enum Kind { Death, Raid, Damage, Enemy, Name } private const float ScanInterval = 0.5f; private const float WelcomeDelay = 0.6f; private static readonly Dictionary<Kind, string[]> SignOffs = new Dictionary<Kind, string[]> { [Kind.Death] = new string[3] { "Odin did knock.", "Odin knocked. You were busy. Being dead, mostly.", "Odin would rather you heard it from him." }, [Kind.Raid] = new string[2] { "Odin knocked. The army didn't wait.", "Odin did knock. Loudly." }, [Kind.Damage] = new string[3] { "Odin counted.", "Odin knocked. Something else was knocking harder.", "Odin kept score. You lost." }, [Kind.Enemy] = new string[3] { "Odin knocked first.", "Odin thought you'd want to know.", "They didn't knock. Odin did." }, [Kind.Name] = new string[2] { "Odin isn't your secretary, but there it is.", "Odin passed the message on." } }; private readonly float[] _lastKnock = new float[5]; private readonly HashSet<int> _threats = new HashSet<int>(); private readonly HashSet<int> _threatsNow = new HashSet<int>(); private OdinLine _line; private AudioSource _audio; private bool _focused = true; private float _nextScan; private float _welcomeAt = -1f; private string _raid; private string _lastSignOff; private int _hits; private float _damage; private int _deaths; private int _enemies; private int _raids; private string _raidText; private int _names; private string _namedBy; private bool _knocked; internal static Nudge Instance { get; private set; } private void Awake() { Instance = this; _line = ((Component)this).gameObject.AddComponent<OdinLine>(); _audio = ((Component)this).gameObject.AddComponent<AudioSource>(); _audio.playOnAwake = false; _audio.spatialBlend = 0f; _audio.ignoreListenerPause = true; _audio.bypassReverbZones = true; for (int i = 0; i < _lastKnock.Length; i++) { _lastKnock[i] = float.MinValue; } } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void Update() { bool isFocused = Application.isFocused; if (isFocused != _focused) { _focused = isFocused; if (isFocused) { Back(); } else { Away(); } } if (_welcomeAt >= 0f && Time.unscaledTime >= _welcomeAt) { _welcomeAt = -1f; Welcome(); } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { _raid = null; return; } WatchRaid(isFocused); if (!isFocused && Plugin.Enabled.Value && !(Time.unscaledTime < _nextScan)) { _nextScan = Time.unscaledTime + 0.5f; if (Plugin.NudgeOnEnemy.Value && !((Character)localPlayer).IsDead()) { WatchEnemies(localPlayer); } if (Plugin.NudgeOnRaid.Value && _raid != null && !((Character)localPlayer).IsDead()) { Raise(Kind.Raid, "raid " + _raid + " still on around you", quietly: true); } } } private void Away() { Clear(); _threats.Clear(); for (int i = 0; i < _lastKnock.Length; i++) { _lastKnock[i] = float.MinValue; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && Plugin.NudgeOnEnemy.Value) { Threats(localPlayer, _threats, null, out var _); } if (_threats.Count > 0) { _lastKnock[3] = Time.unscaledTime; } if (_raid != null) { _lastKnock[1] = Time.unscaledTime; } Note($"Valheim is in the background. Odin is watching ({_threats.Count} awake enemies about already)."); } private void Back() { Taskbar.Stop(); if (!_knocked && Total() == 0) { Note("Back in Valheim. Nothing happened."); return; } Note($"Back in Valheim: {_hits} hits ({_damage:0} damage), {_deaths} deaths, {_enemies} enemies, {_raids} raids, {_names} mentions."); if (Plugin.WelcomeBack.Value && Total() > 0) { _welcomeAt = Time.unscaledTime + 0.6f; } else { Clear(); } } private int Total() { return _hits + _deaths + _enemies + _raids + _names; } private void Clear() { _hits = (_deaths = (_enemies = (_raids = (_names = 0)))); _damage = 0f; _raidText = (_namedBy = null); _knocked = false; } internal void OnHurt(float damage, Character attacker) { if (!_focused && Plugin.Enabled.Value && Plugin.NudgeOnDamage.Value && !(damage < Plugin.MinDamage.Value)) { _hits++; _damage += damage; Raise(Kind.Damage, string.Format("hit for {0:0.#} by {1}", damage, ((Object)(object)attacker != (Object)null) ? attacker.m_name : "the world")); } } internal void OnDied() { if (!_focused && Plugin.Enabled.Value && Plugin.NudgeOnDeath.Value) { _deaths++; Raise(Kind.Death, "you died"); } } internal void OnChat(Type type, UserInfo sender, string text) { //IL_0065: Unknown result type (might be due to invalid IL or missing references) if (!_focused && Plugin.Enabled.Value && Plugin.NudgeOnName.Value && !string.IsNullOrEmpty(text)) { string text2 = NameIn(text); if (text2 != null) { _names++; _namedBy = Who(sender); Raise(Kind.Name, string.Format("{0} said \"{1}\" ({2})", _namedBy ?? "someone", text2, type)); } } } private void WatchRaid(bool focused) { RandomEvent val = (((Object)(object)RandEventSystem.instance != (Object)null) ? RandEventSystem.instance.GetActiveEvent() : null); string text = val?.m_name; if (!(text == _raid)) { _raid = text; if (!(text == null || focused) && Plugin.Enabled.Value && Plugin.NudgeOnRaid.Value) { _raids++; _raidText = Localize(val.m_startMessage); Raise(Kind.Raid, "raid " + text + " started around you"); } } } private void WatchEnemies(Player player) { //IL_0077: 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) _threatsNow.Clear(); Character nearest; Character val = Threats(player, _threatsNow, _threats, out nearest); _threats.Clear(); _threats.UnionWith(_threatsNow); if (!((Object)(object)nearest == (Object)null)) { if ((Object)(object)val != (Object)null) { _enemies++; } Character val2 = val ?? nearest; Raise(Kind.Enemy, string.Format("{0} {1:0} m away{2}", val2.m_name, Vector3.Distance(((Component)val2).transform.position, ((Component)player).transform.position), ((Object)(object)val != (Object)null) ? "" : ", still"), (Object)(object)val == (Object)null); } } private static Character Threats(Player player, HashSet<int> into, HashSet<int> before, out Character nearest) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) Character val = null; nearest = null; float num = float.MaxValue; float value = Plugin.EnemyRadius.Value; Vector3 position = ((Component)player).transform.position; foreach (Character allCharacter in Character.GetAllCharacters()) { if ((Object)(object)allCharacter == (Object)null || (Object)(object)allCharacter == (Object)(object)player || allCharacter.IsPlayer() || allCharacter.IsDead() || allCharacter.IsTamed()) { continue; } float num2 = Vector3.Distance(((Component)allCharacter).transform.position, position) - allCharacter.GetRadius(); if (!(num2 > value) && IsThreat(player, allCharacter)) { if (num2 < num) { num = num2; nearest = allCharacter; } int instanceID = ((Object)allCharacter).GetInstanceID(); into.Add(instanceID); if (before != null && (Object)(object)val == (Object)null && !before.Contains(instanceID)) { val = allCharacter; } } } return val; } private static bool IsThreat(Player player, Character other) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Invalid comparison between Unknown and I4 if (!BaseAI.IsEnemy((Character)(object)player, other)) { return false; } BaseAI baseAI = other.GetBaseAI(); if ((Object)(object)baseAI == (Object)null) { return false; } Faction faction = other.GetFaction(); if ((int)faction != 1) { if ((int)faction == 12) { return false; } if (!baseAI.IsAlerted() && !baseAI.HaveTarget()) { return baseAI.HuntPlayer(); } return true; } if (baseAI is MonsterAI) { return baseAI.HaveTarget(); } return false; } private void Raise(Kind kind, string what, bool quietly = false) { float unscaledTime = Time.unscaledTime; float num = Plugin.Cooldown.Value - (unscaledTime - _lastKnock[(int)kind]); if (num > 0f) { if (!quietly) { Note($"{kind}: {what}. Knocked for that {unscaledTime - _lastKnock[(int)kind]:0} s ago; next knock in {num:0} s if it keeps up."); } return; } _lastKnock[(int)kind] = unscaledTime; bool flag = Plugin.FlashTaskbar.Value && Taskbar.Flash(); AudioClip val = Sounds.Current(); if ((Object)(object)val != (Object)null && Plugin.Volume.Value > 0f) { _audio.PlayOneShot(val, Plugin.Volume.Value); } _knocked = true; Note(string.Format("Nudge! {0}: {1}. Flash {2}, sound {3}.", kind, what, flag ? "on" : "off", ((Object)(object)val != (Object)null) ? ((Object)val).name : "none")); } private void Welcome() { string text = Summary(); Clear(); if (text != null) { _line.Show(text); } } private string Summary() { List<string> parts = new List<string>(); Kind top = Kind.Name; bool any = false; if (_deaths > 0) { Add(Kind.Death, (_deaths == 1) ? "you died" : $"you died {_deaths} times"); } if (_raids > 0) { Add(Kind.Raid, (!string.IsNullOrEmpty(_raidText)) ? ("a raid started (\"" + _raidText.TrimEnd('.', '!') + "\")") : "a raid started"); } if (_hits > 0) { Add(Kind.Damage, (_hits == 1) ? $"you were hit once ({_damage:0} damage)" : $"you were hit {_hits} times ({_damage:0} damage)"); } if (_enemies > 0) { Add(Kind.Enemy, (_enemies == 1) ? "something came looking for you" : $"{_enemies} things came looking for you"); } if (_names > 0) { Add(Kind.Name, (_names == 1) ? ((_namedBy ?? "someone") + " said your name in chat") : $"chat said your name {_names} times"); } if (!any) { return null; } string text = ((parts.Count == 1) ? parts[0] : (string.Join(", ", parts.GetRange(0, parts.Count - 1).ToArray()) + " and " + parts[parts.Count - 1])); return string.Concat(char.ToUpperInvariant(text[0]) + text.Substring(1) + " while you were away.", " ", SignOff(top)); void Add(Kind kind, string part) { parts.Add(part); if (!any || kind < top) { top = kind; } any = true; } } private string SignOff(Kind kind) { string[] array = SignOffs[kind]; string text = array[Random.Range(0, array.Length)]; if (text == _lastSignOff && array.Length > 1) { text = array[(Array.IndexOf(array, text) + 1) % array.Length]; } _lastSignOff = text; return text; } private static string NameIn(string text) { foreach (string item in Names()) { if (item.Length >= 2) { string pattern = "(?<![\\p{L}\\p{N}_])" + Regex.Escape(item) + "(?![\\p{L}\\p{N}_])"; if (Regex.IsMatch(text, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)) { return item; } } } return null; } private static IEnumerable<string> Names() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { yield return localPlayer.GetPlayerName().Trim(); } string[] array = (Plugin.ExtraNames.Value ?? "").Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { yield return text; } } } private static string Who(UserInfo sender) { if (sender == null) { return null; } try { string displayName = sender.GetDisplayName(); return string.IsNullOrEmpty(displayName) ? sender.Name : displayName; } catch (Exception) { return sender.Name; } } private static string Localize(string token) { if (!string.IsNullOrEmpty(token)) { if (Localization.instance == null) { return token.TrimStart(new char[1] { '$' }); } return Localization.instance.Localize(token); } return null; } private static void Note(string message) { if (Plugin.DebugLog.Value) { Plugin.Log.LogInfo((object)message); } } } internal class OdinLine : MonoBehaviour { private const float ScreenY = 0.72f; private const float FadeIn = 0.2f; private const float Hold = 4.5f; private const float FadeOut = 0.8f; private const float TextSize = 21f; private static readonly Color Tint = new Color(1f, 0.86f, 0.62f); private TMP_Text _text; private float _elapsed = -1f; public void Show(string line) { if (EnsureText()) { _text.text = line; _elapsed = 0f; SetAlpha(0f); ((Component)_text).gameObject.SetActive(true); } } private void Update() { if (_elapsed < 0f || (Object)(object)_text == (Object)null) { return; } _elapsed += Time.deltaTime; float num = 5.5f; if (_elapsed >= num) { _elapsed = -1f; ((Component)_text).gameObject.SetActive(false); return; } float num2 = Mathf.Min(Mathf.Clamp01(_elapsed / 0.2f), Mathf.Clamp01((num - _elapsed) / 0.8f)); if (!HudVisible()) { num2 = 0f; } SetAlpha(Mathf.SmoothStep(0f, 1f, num2)); } private void SetAlpha(float alpha) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) Color tint = Tint; tint.a = alpha; ((Graphic)_text).color = tint; } private bool EnsureText() { //IL_0041: 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_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_text != (Object)null) { return true; } _text = HudText.Create("OdinsNudge_Line", ((Component)this).transform); if ((Object)(object)_text == (Object)null) { return false; } RectTransform val = (RectTransform)_text.transform; val.anchorMin = new Vector2(0.5f, 0.72f); val.anchorMax = new Vector2(0.5f, 0.72f); val.sizeDelta = new Vector2(1400f, 160f); _text.fontSize = 21f; return true; } private static bool HudVisible() { if ((Object)(object)Hud.instance != (Object)null && !Hud.IsUserHidden()) { return Hud.instance.IsVisible(); } return false; } private void OnDestroy() { if ((Object)(object)_text != (Object)null) { Object.Destroy((Object)(object)((Component)_text).gameObject); } } } [HarmonyPatch(typeof(Player), "OnDamaged")] internal static class PlayerDamagedPatch { private static void Postfix(Player __instance, HitData hit) { if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && hit != null && !((Object)(object)Nudge.Instance == (Object)null)) { Nudge.Instance.OnHurt(hit.GetTotalDamage(), hit.GetAttacker()); } } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerDeathPatch { private static void Postfix(Player __instance) { if (!((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)Nudge.Instance == (Object)null)) { Nudge.Instance.OnDied(); } } } [HarmonyPatch(typeof(Chat), "OnNewChatMessage")] internal static class ChatMessagePatch { private static void Prefix(long senderID, Type type, UserInfo sender, string text) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)Nudge.Instance == (Object)null) && !((Object)(object)ZNet.instance == (Object)null)) { ZDOID localPlayerCharacterID = ZNet.instance.LocalPlayerCharacterID; if (senderID != ((ZDOID)(ref localPlayerCharacterID)).UserID) { Nudge.Instance.OnChat(type, sender, text); } } } } [BepInPlugin("hevyh2o.valheim.odinsnudge", "Odin's Nudge", "0.1.1")] public class Plugin : BaseUnityPlugin { public const string Guid = "hevyh2o.valheim.odinsnudge"; public const string Name = "Odin's Nudge"; public const string Version = "0.1.1"; internal static ManualLogSource Log; internal static Harmony Patcher; internal static string ConfigFolder; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<float> Cooldown; internal static ConfigEntry<bool> NudgeOnDamage; internal static ConfigEntry<float> MinDamage; internal static ConfigEntry<bool> NudgeOnEnemy; internal static ConfigEntry<float> EnemyRadius; internal static ConfigEntry<bool> NudgeOnRaid; internal static ConfigEntry<bool> NudgeOnDeath; internal static ConfigEntry<bool> NudgeOnName; internal static ConfigEntry<string> ExtraNames; internal static ConfigEntry<bool> FlashTaskbar; internal static ConfigEntry<string> Sound; internal static ConfigEntry<float> Volume; internal static ConfigEntry<bool> WelcomeBack; internal static ConfigEntry<float> TextScale; internal static ConfigEntry<bool> DebugLog; private Nudge _nudge; private void Awake() { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Expected O, but got Unknown //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Expected O, but got Unknown //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_0222: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Expected O, but got Unknown //IL_027f: Unknown result type (might be due to invalid IL or missing references) //IL_0289: Expected O, but got Unknown //IL_02b3: Unknown result type (might be due to invalid IL or missing references) //IL_02bd: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ConfigFolder = Paths.ConfigPath; Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Master switch."); Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("General", "Cooldown", 20f, new ConfigDescription("Seconds between knocks of the same kind, 5 to 300. While the trouble lasts (hits still landing, an awake enemy still close, a raid still on), Odin knocks again every Cooldown seconds.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 300f), Array.Empty<object>())); NudgeOnDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Nudges", "NudgeOnDamage", true, "Nudge when you take damage while Valheim is in the background."); MinDamage = ((BaseUnityPlugin)this).Config.Bind<float>("Nudges", "MinDamage", 1f, new ConfigDescription("Smallest hit, after armour, that counts, 0 to 100.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); NudgeOnEnemy = ((BaseUnityPlugin)this).Config.Bind<bool>("Nudges", "NudgeOnEnemy", true, "Nudge when an enemy within EnemyRadius is awake: alerted, hunting or after something. Animals only count while they're attacking. Tamed creatures never do."); EnemyRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Nudges", "EnemyRadius", 20f, new ConfigDescription("How close to you, in metres, an enemy has to be, 5 to 60.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 60f), Array.Empty<object>())); NudgeOnRaid = ((BaseUnityPlugin)this).Config.Bind<bool>("Nudges", "NudgeOnRaid", true, "Nudge when a raid or other event starts around you."); NudgeOnDeath = ((BaseUnityPlugin)this).Config.Bind<bool>("Nudges", "NudgeOnDeath", true, "Nudge when you die."); NudgeOnName = ((BaseUnityPlugin)this).Config.Bind<bool>("Nudges", "NudgeOnName", true, "Nudge when someone else's chat line has your character's name in it, as a whole word. Your own lines never count."); ExtraNames = ((BaseUnityPlugin)this).Config.Bind<string>("Nudges", "ExtraNames", "", "Other names to listen for in chat, separated by commas (a nickname, your Steam name). Empty = just your character's name."); FlashTaskbar = ((BaseUnityPlugin)this).Config.Bind<bool>("Sound", "FlashTaskbar", true, "Flash Valheim's taskbar button until you click back in. Windows only."); Sound = ((BaseUnityPlugin)this).Config.Bind<string>("Sound", "Sound", "knock", "What plays with a nudge: knock, horn, none, or the name of a .wav file in the BepInEx config folder (16-bit or float PCM)."); Volume = ((BaseUnityPlugin)this).Config.Bind<float>("Sound", "Volume", 0.8f, new ConfigDescription("How loud the sound is, 0 to 1. The game's master volume still applies.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); WelcomeBack = ((BaseUnityPlugin)this).Config.Bind<bool>("Messages", "WelcomeBack", true, "When you click back into Valheim, Odin says in one line what happened while you were away. Nothing, if nothing did."); TextScale = ((BaseUnityPlugin)this).Config.Bind<float>("Messages", "TextScale", 1f, new ConfigDescription("Size of Odin's lines, 0.5 to 2. 1 = sized to match the game's own HUD, which already follows your screen height and the game's GUI scale setting.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 2f), Array.Empty<object>())); DebugLog = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugLog", false, "Write each thing Odin notices while you're away, and each nudge or why there wasn't one, to the BepInEx log."); Patcher = new Harmony("hevyh2o.valheim.odinsnudge"); Patcher.PatchAll(typeof(Plugin).Assembly); _nudge = ((Component)this).gameObject.AddComponent<Nudge>(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Odin's Nudge 0.1.1 loaded. Odin will tap your shoulder."); } private void OnDestroy() { Harmony patcher = Patcher; if (patcher != null) { patcher.UnpatchSelf(); } if ((Object)(object)_nudge != (Object)null) { Object.Destroy((Object)(object)_nudge); } } } internal static class Sounds { private const int Rate = 44100; private static string _loadedFor; private static AudioClip _clip; public static AudioClip Current() { string text = (Plugin.Sound.Value ?? "").Trim(); if (text == _loadedFor) { return _clip; } _loadedFor = text; _clip = null; if (text.Length == 0 || text.Equals("none", StringComparison.OrdinalIgnoreCase)) { return null; } if (text.Equals("knock", StringComparison.OrdinalIgnoreCase)) { _clip = Make("OdinsNudge_Knock", Knock()); } else if (text.Equals("horn", StringComparison.OrdinalIgnoreCase)) { _clip = Make("OdinsNudge_Horn", Horn()); } else { _clip = FromFile(text); } return _clip; } private static float[] Knock() { float[] array = new float[27342]; Random random = new Random(9); float[] array2 = new float[3] { 0f, 0.16f, 0.32f }; float[] array3 = new float[3] { 1f, 0.85f, 0.95f }; int num = 4851; for (int i = 0; i < array2.Length; i++) { int num2 = (int)(array2[i] * 44100f); for (int j = 0; j < num && num2 + j < array.Length; j++) { float num3 = (float)j / 44100f; float num4 = Mathf.Sin((float)Math.PI * 280f * num3) * 0.75f + Mathf.Sin((float)Math.PI * 580f * num3) * 0.3f; float num5 = (float)(random.NextDouble() * 2.0 - 1.0) * Mathf.Exp((0f - num3) * 450f) * 0.5f; array[num2 + j] += (num4 * Mathf.Exp((0f - num3) * 45f) + num5) * array3[i]; } } return Normalise(array); } private static float[] Horn() { float num = 1.4f; float[] array = new float[(int)(44100f * num)]; float[] array2 = new float[5] { 1f, 0.5f, 0.32f, 0.18f, 0.08f }; for (int i = 0; i < array.Length; i++) { float num2 = (float)i / 44100f; float num3 = 98f * (1f + 0.004f * Mathf.Sin((float)Math.PI * 10f * num2)); float num4 = 0f; for (int j = 0; j < array2.Length; j++) { num4 += Mathf.Sin((float)Math.PI * 2f * num3 * (float)(j + 1) * num2) * array2[j]; } float num5 = Mathf.Clamp01(num2 / 0.18f) * Mathf.Clamp01((num - num2) / 0.55f); array[i] = num4 * num5; } return Normalise(array); } private static float[] Normalise(float[] data) { float num = 0f; foreach (float num2 in data) { num = Mathf.Max(num, Mathf.Abs(num2)); } if (num > 0f) { for (int j = 0; j < data.Length; j++) { data[j] = data[j] / num * 0.9f; } } return data; } private static AudioClip Make(string name, float[] data, int channels = 1, int rate = 44100) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown int at = 0; return AudioClip.Create(name, data.Length / channels, channels, rate, false, (PCMReaderCallback)delegate(float[] buffer) { int num = Math.Max(0, Math.Min(buffer.Length, data.Length - at)); Array.Copy(data, at, buffer, 0, num); Array.Clear(buffer, num, buffer.Length - num); at += num; }); } private static AudioClip FromFile(string name) { string text = Path.Combine(Plugin.ConfigFolder ?? "", name); if (!text.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) { text += ".wav"; } try { if (!File.Exists(text)) { Plugin.Log.LogWarning((object)("Sound \"" + name + "\" isn't knock, horn or none, and there's no " + text + ". Odin will flash without a sound.")); return null; } byte[] array = File.ReadAllBytes(text); if (array.Length < 12 || Tag(array, 0) != "RIFF" || Tag(array, 8) != "WAVE") { throw new InvalidDataException("not a WAV file"); } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = -1; int num6 = 0; int num7 = 12; while (num7 + 8 <= array.Length) { string text2 = Tag(array, num7); int num8 = BitConverter.ToInt32(array, num7 + 4); int num9 = num7 + 8; if (text2 == "fmt " && num8 >= 16) { num = BitConverter.ToUInt16(array, num9); num2 = BitConverter.ToUInt16(array, num9 + 2); num3 = BitConverter.ToInt32(array, num9 + 4); num4 = BitConverter.ToUInt16(array, num9 + 14); if (num == 65534 && num8 >= 26) { num = BitConverter.ToUInt16(array, num9 + 24); } } else if (text2 == "data") { num5 = num9; num6 = Math.Min(num8, array.Length - num9); break; } num7 = num9 + num8 + (num8 & 1); } if (num5 < 0 || num2 < 1 || num2 > 2 || num3 <= 0) { throw new InvalidDataException("no audio in it"); } int num10 = num4 / 8; if (num10 < 1 || ((num != 1 || (num4 != 8 && num4 != 16 && num4 != 24)) && (num != 3 || num4 != 32))) { throw new InvalidDataException($"format {num}, {num4}-bit isn't supported"); } float[] array2 = new float[num6 / num10 / num2 * num2]; for (int i = 0; i < array2.Length; i++) { int num11 = num5 + i * num10; switch (num4) { case 8: array2[i] = (float)(array[num11] - 128) / 128f; break; case 16: array2[i] = (float)BitConverter.ToInt16(array, num11) / 32768f; break; case 24: array2[i] = (float)(array[num11] | (array[num11 + 1] << 8) | ((sbyte)array[num11 + 2] << 16)) / 8388608f; break; default: array2[i] = BitConverter.ToSingle(array, num11); break; } } Plugin.Log.LogInfo((object)("Nudge sound: " + Path.GetFileName(text) + ".")); return Make("OdinsNudge_" + Path.GetFileNameWithoutExtension(text), array2, num2, num3); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Couldn't read " + text + " (" + ex.Message + "). Odin will flash without a sound.")); return null; } } private static string Tag(byte[] b, int at) { if (at + 4 > b.Length) { return ""; } return Encoding.ASCII.GetString(b, at, 4); } } internal static class Taskbar { private struct FlashInfo { public uint cbSize; public IntPtr hwnd; public uint dwFlags; public uint uCount; public uint dwTimeout; } private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); private const uint FlashStop = 0u; private const uint FlashAll = 3u; private const uint FlashUntilFront = 12u; private const string UnityClass = "UnityWndClass"; private static IntPtr _window = IntPtr.Zero; private static bool _broken; private static uint _pid; private static IntPtr _anyVisible; private static readonly EnumWindowsProc Visit = OnWindow; private static bool Supported { get { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Invalid comparison between Unknown and I4 if (!_broken) { if ((int)Application.platform != 2) { return (int)Application.platform == 7; } return true; } return false; } } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool FlashWindowEx(ref FlashInfo info); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr hWnd, StringBuilder className, int maxCount); [DllImport("kernel32.dll")] private static extern uint GetCurrentProcessId(); public static bool Flash() { if (!Supported || !FindWindow()) { return false; } try { FlashInfo info = Info(15u); FlashWindowEx(ref info); return true; } catch (Exception e) { Give(e); return false; } } public static void Stop() { if (!Supported || _window == IntPtr.Zero) { return; } try { FlashInfo info = Info(0u); FlashWindowEx(ref info); } catch (Exception e) { Give(e); } } private static FlashInfo Info(uint flags) { return new FlashInfo { cbSize = (uint)Marshal.SizeOf(typeof(FlashInfo)), hwnd = _window, dwFlags = flags, uCount = uint.MaxValue, dwTimeout = 0u }; } private static bool FindWindow() { if (_window != IntPtr.Zero) { return true; } try { _pid = GetCurrentProcessId(); _anyVisible = IntPtr.Zero; EnumWindows(Visit, IntPtr.Zero); if (_window == IntPtr.Zero) { _window = _anyVisible; } if (_window == IntPtr.Zero) { Plugin.Log.LogInfo((object)"Odin couldn't find Valheim's window. No taskbar flash; the sound still plays."); _broken = true; return false; } return true; } catch (Exception e) { Give(e); return false; } } private static bool OnWindow(IntPtr hWnd, IntPtr lParam) { GetWindowThreadProcessId(hWnd, out var processId); if (processId != _pid || !IsWindowVisible(hWnd)) { return true; } StringBuilder stringBuilder = new StringBuilder(64); GetClassName(hWnd, stringBuilder, stringBuilder.Capacity); if (stringBuilder.ToString() == "UnityWndClass") { _window = hWnd; return false; } if (_anyVisible == IntPtr.Zero) { _anyVisible = hWnd; } return true; } private static void Give(Exception e) { _broken = true; Plugin.Log.LogInfo((object)("Taskbar flash unavailable (" + e.GetType().Name + "). Odin will knock instead.")); } } internal static class BuildInfo { public const string Version = "0.1.1"; } }