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 balrond Achievement Rewards v0.1.0
plugins/BalrondAchievementRewards.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using BalrondAchievementRewards.AchievementIntegration; using BalrondAchievementRewards.Configuration; using BalrondAchievementRewards.Core; using BalrondAchievementRewards.Data; using BalrondAchievementRewards.Networking; using BalrondAchievementRewards.Patches; using BalrondAchievementRewards.Runtime; using BalrondAchievementRewards.UI; using BalrondAchievementRewards.Utils; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using ServerSync; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("BalrondAchievementRewards")] [assembly: AssemblyDescription("Cosmetic achievement reward titles for Valheim 1.0")] [assembly: AssemblyCompany("Balrond")] [assembly: AssemblyProduct("BalrondAchievementRewards")] [assembly: ComVisible(false)] [assembly: Guid("0b62d0e0-7d43-47cb-9acd-cac9d7212aae")] [assembly: AssemblyFileVersion("0.3.3.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.3.0")] [module: UnverifiableCode] namespace BalrondAchievementRewards.Utils { internal static class ModLog { private static ManualLogSource _log; private static readonly HashSet<string> OnceKeys = new HashSet<string>(); internal static void Initialize(ManualLogSource log) { _log = log; OnceKeys.Clear(); } internal static void Info(string message) { if (_log != null) { _log.LogInfo((object)message); } } internal static void Warning(string message) { if (_log != null) { _log.LogWarning((object)message); } } internal static void Error(string message) { if (_log != null) { _log.LogError((object)message); } } internal static void Debug(string message) { if (_log != null && BARConfig.VerboseLogging != null && BARConfig.VerboseLogging.Value) { _log.LogInfo((object)("[DEBUG] " + message)); } } internal static void WarningOnce(string key, string message) { if (OnceKeys.Add("W:" + key)) { Warning(message); } } internal static void ErrorOnce(string key, string message) { if (OnceKeys.Add("E:" + key)) { Error(message); } } internal static void Reset() { OnceKeys.Clear(); _log = null; } } internal static class UnsafeTextHash { internal unsafe static uint Compute(string value) { if (string.IsNullOrEmpty(value)) { return 2166136261u; } uint num = 2166136261u; fixed (char* ptr = value) { for (int i = 0; i < value.Length; i++) { char c = ptr[i]; num ^= (byte)(c & 0xFF); num *= 16777619; num ^= (byte)(((int)c >> 8) & 0xFF); num *= 16777619; } } return num; } } } namespace BalrondAchievementRewards.UI { internal static class TitleSelectionGui { private sealed class AchievementButtonHook { internal Button Button; internal UnityAction Listener; } private const string NoTitleButtonName = "BAR_NoTitleButton"; private const string TitleClickOverlayName = "BAR_TitleClickOverlay"; private static readonly List<AchievementButtonHook> AchievementButtonHooks = new List<AchievementButtonHook>(); private static InventoryGui _inventoryOwner; private static Button _noTitleButton; private static TMP_Text _noTitleButtonText; private static bool _refreshRequested; private static bool _overlayReadyLogged; internal static void Tick() { if (_refreshRequested) { _refreshRequested = false; InventoryGui instance = InventoryGui.m_instance; if (IsAchievementsPanelVisible(instance)) { EnsureNoTitleButton(instance); UpdateNoTitleButton(); BindNativeAchievementButtons(instance); } } } internal static void Shutdown() { TitleConfirmationPopup.Shutdown(); ClearNativeAchievementButtonHooks(); DestroyNoTitleButton(); _inventoryOwner = null; _refreshRequested = false; _overlayReadyLogged = false; } internal static void RequestRefresh() { _refreshRequested = true; } internal static void ShowToast(string message) { if (string.IsNullOrWhiteSpace(message) || (Object)(object)Player.m_localPlayer == (Object)null) { return; } try { ((Character)Player.m_localPlayer).Message((MessageType)1, message.Trim(), 0, (Sprite)null, false); } catch (Exception ex) { ModLog.Debug("Could not show title notification through MessageHud: " + ex.Message); } } internal static void OnInventoryGuiAwake(InventoryGui gui) { if (!((Object)(object)gui == (Object)null)) { if ((Object)(object)_inventoryOwner != (Object)null && (Object)(object)_inventoryOwner != (Object)(object)gui) { TitleConfirmationPopup.Shutdown(); ClearNativeAchievementButtonHooks(); DestroyNoTitleButton(); } _inventoryOwner = gui; } } internal static void OnAchievementsOpened(InventoryGui gui) { if (!((Object)(object)gui == (Object)null)) { _inventoryOwner = gui; AchievementWatcher.ScanImmediately("native Achievements menu opened"); EnsureNoTitleButton(gui); UpdateNoTitleButton(); BindNativeAchievementButtons(gui); } } internal static void OnAchievementsClosed(InventoryGui gui) { TitleConfirmationPopup.Hide(); ClearNativeAchievementButtonHooks(); } internal static void OnVanillaAchievementsListUpdated(InventoryGui gui) { if (!((Object)(object)gui == (Object)null) && IsAchievementsPanelVisible(gui)) { EnsureNoTitleButton(gui); UpdateNoTitleButton(); BindNativeAchievementButtons(gui); } } internal static void OnInventoryHidden() { TitleConfirmationPopup.Hide(); ClearNativeAchievementButtonHooks(); } internal static void OnLocalPlayerCleared() { TitleConfirmationPopup.Hide(); ClearNativeAchievementButtonHooks(); RequestRefresh(); } internal static void OnNativeAchievementClicked(Achievement achievement) { if (!((Object)(object)achievement == (Object)null) && !((Object)(object)Player.m_localPlayer == (Object)null) && (BARConfig.Enabled == null || BARConfig.Enabled.Value)) { AchievementWatcher.ScanImmediately("vanilla achievement button clicked: " + (achievement.m_id ?? "<no id>")); TitleDefinition titleDefinition = TitleRewardCatalog.FindForAchievement(achievement); if (titleDefinition == null) { ModLog.Debug("Clicked vanilla achievement has no synchronized title reward: " + (achievement.m_id ?? "<no id>")); } else if (!achievement.m_unlocked) { ModLog.Debug("Clicked achievement is not unlocked for title selection: " + (achievement.m_id ?? "<no id>")); } else if (string.Equals(TitleRuntime.ActiveTitleId, titleDefinition.TitleId, StringComparison.OrdinalIgnoreCase)) { ShowToast("Title already active: " + titleDefinition.DisplayName); } else { TitleConfirmationPopup.Show(titleDefinition, achievement); } } } private static void BindNativeAchievementButtons(InventoryGui gui) { //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Expected O, but got Unknown //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01ba: Expected O, but got Unknown ClearNativeAchievementButtonHooks(); if ((Object)(object)gui == (Object)null || (Object)(object)gui.m_achievementsPanel == (Object)null) { return; } AchievementsGui achievementsPanel = gui.m_achievementsPanel; if (achievementsPanel.m_achievementsList == null || achievementsPanel.m_achievementsList.Count == 0) { return; } List<Achievement> orderedVanillaAchievements = GetOrderedVanillaAchievements(); if (orderedVanillaAchievements.Count == 0) { return; } if (achievementsPanel.m_achievementsList.Count != orderedVanillaAchievements.Count) { ModLog.WarningOnce("achievement_ui_row_count_mismatch", "Valheim generated " + achievementsPanel.m_achievementsList.Count + " achievement UI row(s), while the current achievement registry exposes " + orderedVanillaAchievements.Count + ". Title overlays will use visible row-name matching first and registry order only as a fallback."); } int num = 0; int num2 = 0; for (int i = 0; i < achievementsPanel.m_achievementsList.Count; i++) { GameObject val = achievementsPanel.m_achievementsList[i]; if ((Object)(object)val == (Object)null) { continue; } Achievement val2 = ResolveAchievementForRow(val, i, orderedVanillaAchievements); TitleDefinition titleDefinition = (((Object)(object)val2 != (Object)null) ? TitleRewardCatalog.FindForAchievement(val2) : null); if ((Object)(object)val2 == (Object)null || !val2.m_unlocked || titleDefinition == null) { SetExistingOverlayActive(val, active: false); continue; } bool created; Button val3 = EnsureUnlockedAchievementOverlay(val, out created); if (!((Object)(object)val3 == (Object)null)) { if (created) { num2++; } Achievement captured = val2; UnityAction val4 = (UnityAction)delegate { OnNativeAchievementClicked(captured); }; val3.onClick = new ButtonClickedEvent(); ((UnityEvent)val3.onClick).AddListener(val4); ButtonSfx component = val.GetComponent<ButtonSfx>(); if ((Object)(object)component != (Object)null) { component.m_selectable = (Selectable)(object)val3; } UIGamePad component2 = val.GetComponent<UIGamePad>(); if ((Object)(object)component2 != (Object)null) { component2.m_button = val3; component2.m_submit = (ISubmitHandler)(object)val3; } AchievementButtonHooks.Add(new AchievementButtonHook { Button = val3, Listener = val4 }); num++; } } if (num > 0 && !_overlayReadyLogged) { _overlayReadyLogged = true; ModLog.Info("Achievement title overlay input ready: " + num + " unlocked reward row(s) are clickable through BAR child overlays."); } ModLog.Debug("Achievement title overlay input prepared: " + num + " unlocked reward row(s) bound, " + num2 + " overlay child object(s) created."); } private static Button EnsureUnlockedAchievementOverlay(GameObject row, out bool created) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Expected O, but got Unknown //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: 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_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: Unknown result type (might be due to invalid IL or missing references) created = false; if ((Object)(object)row == (Object)null) { return null; } Transform val = row.transform.Find("BAR_TitleClickOverlay"); GameObject val2; if ((Object)(object)val == (Object)null) { val2 = new GameObject("BAR_TitleClickOverlay", new Type[4] { typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button) }); RectTransform component = val2.GetComponent<RectTransform>(); ((Transform)component).SetParent(row.transform, false); component.anchorMin = Vector2.zero; component.anchorMax = Vector2.one; component.pivot = new Vector2(0.5f, 0.5f); component.offsetMin = Vector2.zero; component.offsetMax = Vector2.zero; ((Transform)component).localScale = Vector3.one; ((Transform)component).SetAsLastSibling(); created = true; } else { val2 = ((Component)val).gameObject; RectTransform val3 = (RectTransform)(object)((val is RectTransform) ? val : null); if ((Object)(object)val3 != (Object)null) { val3.anchorMin = Vector2.zero; val3.anchorMax = Vector2.one; val3.offsetMin = Vector2.zero; val3.offsetMax = Vector2.zero; ((Transform)val3).SetAsLastSibling(); } } val2.layer = row.layer; val2.SetActive(true); Image val4 = val2.GetComponent<Image>(); if ((Object)(object)val4 == (Object)null) { val4 = val2.AddComponent<Image>(); } val4.sprite = null; ((Graphic)val4).color = Color.clear; ((Graphic)val4).raycastTarget = true; ((MaskableGraphic)val4).maskable = true; Button val5 = val2.GetComponent<Button>(); if ((Object)(object)val5 == (Object)null) { val5 = val2.AddComponent<Button>(); } ((Selectable)val5).interactable = true; ((Selectable)val5).transition = (Transition)0; ((Selectable)val5).targetGraphic = (Graphic)(object)val4; Button obj = val5; Navigation navigation = default(Navigation); ((Navigation)(ref navigation)).mode = (Mode)3; ((Selectable)obj).navigation = navigation; return val5; } private static void SetExistingOverlayActive(GameObject row, bool active) { if (!((Object)(object)row == (Object)null)) { Transform val = row.transform.Find("BAR_TitleClickOverlay"); if ((Object)(object)val != (Object)null && ((Component)val).gameObject.activeSelf != active) { ((Component)val).gameObject.SetActive(active); } } } private static List<Achievement> GetOrderedVanillaAchievements() { List<Achievement> list = new List<Achievement>(); Achievements instance = Achievements.m_instance; if ((Object)(object)instance == (Object)null || instance.m_achievementLists == null) { return list; } for (int i = 0; i < instance.m_achievementLists.Count; i++) { AchievementList val = instance.m_achievementLists[i]; if ((Object)(object)val == (Object)null || val.m_achievements == null) { continue; } for (int j = 0; j < val.m_achievements.Count; j++) { Achievement val2 = val.m_achievements[j]; if ((Object)(object)val2 != (Object)null) { list.Add(val2); } } } return list; } private static Achievement ResolveAchievementForRow(GameObject row, int index, List<Achievement> orderedAchievements) { string rowAchievementName = GetRowAchievementName(row); if (!string.IsNullOrWhiteSpace(rowAchievementName) && !rowAchievementName.Equals("???", StringComparison.OrdinalIgnoreCase) && rowAchievementName.IndexOf("conceal", StringComparison.OrdinalIgnoreCase) < 0) { Achievement val = null; for (int i = 0; i < orderedAchievements.Count; i++) { Achievement val2 = orderedAchievements[i]; if ((Object)(object)val2 == (Object)null) { continue; } string text = AchievementApi.Localize(val2.m_name); if (string.Equals((text ?? string.Empty).Trim(), rowAchievementName.Trim(), StringComparison.OrdinalIgnoreCase)) { if ((Object)(object)val != (Object)null) { val = null; break; } val = val2; } } if ((Object)(object)val != (Object)null) { return val; } } if (index >= 0 && index < orderedAchievements.Count) { return orderedAchievements[index]; } return null; } private static string GetRowAchievementName(GameObject row) { if ((Object)(object)row == (Object)null) { return string.Empty; } Transform val = row.transform.Find("name"); if ((Object)(object)val == (Object)null) { val = row.transform.Find("Name"); } TMP_Text val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent<TMP_Text>() : null); return ((Object)(object)val2 != (Object)null) ? (val2.text ?? string.Empty) : string.Empty; } private static void ClearNativeAchievementButtonHooks() { for (int num = AchievementButtonHooks.Count - 1; num >= 0; num--) { AchievementButtonHook achievementButtonHook = AchievementButtonHooks[num]; if (achievementButtonHook != null) { try { if ((Object)(object)achievementButtonHook.Button != (Object)null && achievementButtonHook.Listener != null) { ((UnityEvent)achievementButtonHook.Button.onClick).RemoveListener(achievementButtonHook.Listener); } } catch { } } } AchievementButtonHooks.Clear(); } private static void EnsureNoTitleButton(InventoryGui gui) { //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Expected O, but got Unknown //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Expected O, but got Unknown if ((Object)(object)gui == (Object)null || (Object)(object)gui.m_achievementsPanel == (Object)null || ((Object)(object)_noTitleButton != (Object)null && (Object)(object)((Component)_noTitleButton).gameObject != (Object)null && (Object)(object)_inventoryOwner == (Object)(object)gui)) { return; } DestroyNoTitleButton(); _inventoryOwner = gui; Button val = FindNativeCloseButton(gui, gui.m_achievementsPanel); if ((Object)(object)val == (Object)null) { ModLog.WarningOnce("no_title_button_source", "Could not find Valheim's native Achievements Close button. Achievement title activation still works, but the No Title control could not be created."); return; } GameObject val2 = Object.Instantiate<GameObject>(((Component)val).gameObject, ((Component)val).transform.parent, false); if ((Object)(object)val2 == (Object)null) { return; } ((Object)val2).name = "BAR_NoTitleButton"; val2.SetActive(true); val2.transform.SetAsLastSibling(); _noTitleButton = val2.GetComponent<Button>(); if ((Object)(object)_noTitleButton == (Object)null) { _noTitleButton = val2.GetComponentInChildren<Button>(true); } if ((Object)(object)_noTitleButton == (Object)null) { Object.Destroy((Object)(object)val2); return; } _noTitleButton.onClick = new ButtonClickedEvent(); ((UnityEvent)_noTitleButton.onClick).AddListener(new UnityAction(OnNoTitleButtonClicked)); UIInputHandler[] componentsInChildren = val2.GetComponentsInChildren<UIInputHandler>(true); for (int i = 0; i < componentsInChildren.Length; i++) { componentsInChildren[i].m_onLeftClick = null; componentsInChildren[i].m_onRightClick = null; componentsInChildren[i].m_onLeftDown = null; componentsInChildren[i].m_onRightDown = null; } _noTitleButtonText = FindPrimaryButtonText(val2); PositionNoTitleButton(val, _noTitleButton); UpdateNoTitleButton(); ModLog.Debug("Created native-styled No Title button beside Valheim's Achievements Close button."); } private static Button FindNativeCloseButton(InventoryGui gui, AchievementsGui achievementsGui) { if ((Object)(object)gui == (Object)null || (Object)(object)((Component)gui).gameObject == (Object)null || (Object)(object)achievementsGui == (Object)null) { return null; } string b = "Close"; try { if (Localization.instance != null) { b = Localization.instance.Localize("$menu_close"); } } catch { } Button[] componentsInChildren = ((Component)gui).gameObject.GetComponentsInChildren<Button>(true); Button val = null; int num = int.MinValue; foreach (Button val2 in componentsInChildren) { if ((Object)(object)val2 == (Object)null || (Object)(object)((Component)val2).gameObject == (Object)null || ((Object)((Component)val2).gameObject).name == "BAR_NoTitleButton" || !IsUnderNamedAncestor(((Component)val2).transform, "Achievements", ((Component)gui).transform) || ((Object)(object)achievementsGui.m_achievementDetails != (Object)null && ((Component)val2).transform.IsChildOf(achievementsGui.m_achievementDetails.transform))) { continue; } int num2 = 0; string text = ((Object)((Component)val2).gameObject).name ?? string.Empty; if (text.Equals("Closebutton", StringComparison.OrdinalIgnoreCase)) { num2 += 120; } else if (text.IndexOf("close", StringComparison.OrdinalIgnoreCase) >= 0) { num2 += 60; } TMP_Text val3 = FindPrimaryButtonText(((Component)val2).gameObject); if ((Object)(object)val3 != (Object)null) { string a = (val3.text ?? string.Empty).Trim(); if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase) || string.Equals(a, "Close", StringComparison.OrdinalIgnoreCase)) { num2 += 100; } } if (((Component)val2).gameObject.activeInHierarchy) { num2 += 30; } if (num2 > num) { val = val2; num = num2; } } return (num > 0) ? val : null; } private static bool IsUnderNamedAncestor(Transform transform, string ancestorName, Transform stopAt) { Transform val = transform; while ((Object)(object)val != (Object)null) { if (((Object)val).name.Equals(ancestorName, StringComparison.OrdinalIgnoreCase)) { return true; } if ((Object)(object)val == (Object)(object)stopAt) { break; } val = val.parent; } return false; } private static TMP_Text FindPrimaryButtonText(GameObject root) { if ((Object)(object)root == (Object)null) { return null; } TMP_Text[] componentsInChildren = root.GetComponentsInChildren<TMP_Text>(true); foreach (TMP_Text val in componentsInChildren) { if ((Object)(object)val != (Object)null && ((Component)val).gameObject.activeInHierarchy) { return val; } } return (componentsInChildren.Length != 0) ? componentsInChildren[0] : null; } private static void PositionNoTitleButton(Button source, Button clone) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: 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_008d: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) RectTransform val = (RectTransform)(((Object)(object)source != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); RectTransform val2 = (RectTransform)(((Object)(object)clone != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val2 == (Object)null)) { float[] obj = new float[3] { 180f, 0f, 0f }; Rect rect = val.rect; obj[1] = Mathf.Abs(((Rect)(ref rect)).width); obj[2] = Mathf.Abs(val.sizeDelta.x); float num = Mathf.Max(obj); val2.anchorMin = val.anchorMin; val2.anchorMax = val.anchorMax; val2.pivot = val.pivot; val2.sizeDelta = val.sizeDelta; ((Transform)val2).localScale = ((Transform)val).localScale; val2.anchoredPosition = val.anchoredPosition + new Vector2(0f - (num + 24f), 0f); } } private static void UpdateNoTitleButton() { if (!((Object)(object)_noTitleButton == (Object)null)) { bool flag = string.IsNullOrEmpty(TitleRuntime.ActiveTitleId); ((Selectable)_noTitleButton).interactable = !flag; if ((Object)(object)_noTitleButtonText != (Object)null) { _noTitleButtonText.richText = false; _noTitleButtonText.text = (flag ? "✓ No Title" : "No Title"); } } } private static void OnNoTitleButtonClicked() { if (!string.IsNullOrEmpty(TitleRuntime.ActiveTitleId) && TitleRuntime.SelectTitle(string.Empty)) { ShowToast("Title disabled."); UpdateNoTitleButton(); } } private static void DestroyNoTitleButton() { if ((Object)(object)_noTitleButton != (Object)null && (Object)(object)((Component)_noTitleButton).gameObject != (Object)null) { Object.Destroy((Object)(object)((Component)_noTitleButton).gameObject); } _noTitleButton = null; _noTitleButtonText = null; } private static bool IsAchievementsPanelVisible(InventoryGui gui) { return (Object)(object)gui != (Object)null && (Object)(object)gui.m_achievementsPanel != (Object)null && (Object)(object)((Component)gui.m_achievementsPanel).gameObject != (Object)null && ((Component)gui.m_achievementsPanel).gameObject.activeInHierarchy; } } internal static class TitleConfirmationPopup { private static YesNoPopup _popup; private static string _pendingTitleId = string.Empty; internal static bool IsVisible => IsOurPopupInStack(); internal static void Show(TitleDefinition definition, Achievement achievement) { //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Expected O, but got Unknown //IL_00eb: Expected O, but got Unknown //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Expected O, but got Unknown if (definition != null && !string.IsNullOrEmpty(definition.TitleId)) { if (!UnifiedPopup.IsAvailable()) { TitleSelectionGui.ShowToast("Valheim confirmation popup is not available yet."); return; } Hide(); _pendingTitleId = definition.TitleId; string playerName = (((Object)(object)Player.m_localPlayer != (Object)null) ? Player.m_localPlayer.GetPlayerName() : "Player"); string text = TitleTextFormatter.FormatHoverName(playerName, definition); string text2 = (((Object)(object)achievement != (Object)null) ? AchievementApi.Localize(achievement.m_name) : TitleRewardCatalog.GetRequirementText(definition)); string text3 = "Achievement: " + text2 + "\nTitle reward: " + definition.DisplayName + "\n\nUse this title?\n" + text; _popup = new YesNoPopup("Activate title?", text3, new PopupButtonCallback(OnYes), new PopupButtonCallback(OnNo), false, true); UnifiedPopup.Push((PopupBase)(object)_popup); } } internal static void Hide() { if (IsOurPopupTopmost()) { UnifiedPopup.Pop(); } _popup = null; _pendingTitleId = string.Empty; } internal static void Shutdown() { Hide(); } private static void OnNo() { if (IsOurPopupTopmost()) { UnifiedPopup.Pop(); } _popup = null; _pendingTitleId = string.Empty; } private static void OnYes() { string pendingTitleId = _pendingTitleId; if (IsOurPopupTopmost()) { UnifiedPopup.Pop(); } _popup = null; _pendingTitleId = string.Empty; if (!string.IsNullOrEmpty(pendingTitleId) && TitleRewardCatalog.TryGet(pendingTitleId, out var definition) && definition != null && TitleRuntime.SelectTitle(pendingTitleId)) { TitleSelectionGui.ShowToast("Title active: " + definition.DisplayName); TitleSelectionGui.RequestRefresh(); LocalPlayerTitleHud.RequestRefresh(); } } private static bool IsOurPopupInStack() { if (_popup == null || (Object)(object)UnifiedPopup.instance == (Object)null || UnifiedPopup.instance.popupStack == null) { return false; } foreach (PopupBase item in UnifiedPopup.instance.popupStack) { if ((object)item == _popup) { return true; } } return false; } private static bool IsOurPopupTopmost() { if (_popup == null || (Object)(object)UnifiedPopup.instance == (Object)null || UnifiedPopup.instance.popupStack == null || UnifiedPopup.instance.popupStack.Count == 0) { return false; } return (object)UnifiedPopup.instance.popupStack.Peek() == _popup; } } internal static class LocalPlayerTitleHud { private const string LocalHudName = "BAR_LocalPlayerTitleHud"; private static EnemyHud _enemyHudOwner; private static Player _playerOwner; private static GameObject _hudObject; private static TMP_Text _nameText; private static string _lastText; private static bool _refreshRequested = true; internal static void RequestRefresh() { _refreshRequested = true; } internal static void Tick() { if (BARConfig.Enabled != null && !BARConfig.Enabled.Value) { SetVisible(visible: false); return; } if (BARConfig.ShowOwnNameplate == null || !BARConfig.ShowOwnNameplate.Value) { SetVisible(visible: false); return; } Player localPlayer = Player.m_localPlayer; EnemyHud instance = EnemyHud.instance; if ((Object)(object)localPlayer == (Object)null || (Object)(object)instance == (Object)null || (Object)(object)instance.m_hudRoot == (Object)null || (Object)(object)instance.m_baseHudPlayer == (Object)null) { SetVisible(visible: false); } else if (((!((Object)(object)_hudObject == (Object)null) && !((Object)(object)_playerOwner != (Object)(object)localPlayer) && !((Object)(object)_enemyHudOwner != (Object)(object)instance)) || Create(localPlayer, instance)) && !((Object)(object)_nameText == (Object)null) && !((Object)(object)_hudObject == (Object)null)) { if (_refreshRequested) { _refreshRequested = false; _lastText = null; } UpdateText(localPlayer); UpdatePosition(localPlayer); } } internal static void Shutdown() { if ((Object)(object)_hudObject != (Object)null) { Object.Destroy((Object)(object)_hudObject); } _enemyHudOwner = null; _playerOwner = null; _hudObject = null; _nameText = null; _lastText = null; _refreshRequested = true; } private static bool Create(Player player, EnemyHud enemyHud) { Shutdown(); try { _enemyHudOwner = enemyHud; _playerOwner = player; _hudObject = Object.Instantiate<GameObject>(enemyHud.m_baseHudPlayer, enemyHud.m_hudRoot.transform, false); if ((Object)(object)_hudObject == (Object)null) { return false; } ((Object)_hudObject).name = "BAR_LocalPlayerTitleHud"; _hudObject.SetActive(true); DisableChild(_hudObject.transform, "Health"); DisableChild(_hudObject.transform, "Stamina"); DisableChild(_hudObject.transform, "level_2"); DisableChild(_hudObject.transform, "level_3"); DisableChild(_hudObject.transform, "Alerted"); DisableChild(_hudObject.transform, "Aware"); Transform val = _hudObject.transform.Find("Name"); if ((Object)(object)val != (Object)null) { _nameText = (TMP_Text)(object)((Component)val).GetComponent<TextMeshProUGUI>(); } if ((Object)(object)_nameText == (Object)null) { TMP_Text[] componentsInChildren = _hudObject.GetComponentsInChildren<TMP_Text>(true); for (int i = 0; i < componentsInChildren.Length; i++) { if ((Object)(object)componentsInChildren[i] != (Object)null && ((Object)componentsInChildren[i]).name.Equals("Name", StringComparison.OrdinalIgnoreCase)) { _nameText = componentsInChildren[i]; break; } } } if ((Object)(object)_nameText == (Object)null) { ModLog.WarningOnce("local_nameplate_name_missing", "Valheim's m_baseHudPlayer prefab did not expose its expected Name TMP element. Local self title HUD was disabled; remote title display is unaffected."); Shutdown(); return false; } _nameText.richText = true; ((Behaviour)_nameText).enabled = true; _refreshRequested = true; return true; } catch (Exception ex) { ModLog.ErrorOnce("local_nameplate_create", "Could not create local title nameplate from Valheim's native player HUD prefab: " + ex); Shutdown(); return false; } } private static void UpdateText(Player player) { string text = player.GetPlayerName() ?? string.Empty; TitleDefinition activeDefinition = TitleRuntime.GetActiveDefinition(); if (activeDefinition != null) { text = TitleTextFormatter.FormatHoverName(text, activeDefinition); } if (!string.Equals(_lastText, text, StringComparison.Ordinal)) { _lastText = text; _nameText.text = text; } } private static void UpdatePosition(Player player) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) Camera mainCamera = Utils.GetMainCamera(); if ((Object)(object)mainCamera == (Object)null || (Object)(object)player == (Object)null || ((Character)player).IsDead()) { SetVisible(visible: false); return; } if ((Object)(object)Hud.instance != (Object)null && !Hud.instance.IsVisible()) { SetVisible(visible: false); return; } float num = ((BARConfig.OwnNameplateVerticalOffset != null) ? BARConfig.OwnNameplateVerticalOffset.Value : 0.3f); Vector3 val = ((Character)player).GetHeadPoint() + Vector3.up * num; Vector3 val2 = Utils.WorldToScreenPointScaled(mainCamera, val); if (!(val2.z > 0f) || !(val2.x >= 0f) || !(val2.x <= (float)Screen.width) || !(val2.y >= 0f) || !(val2.y <= (float)Screen.height)) { SetVisible(visible: false); return; } _hudObject.transform.position = val2; SetVisible(visible: true); } private static void DisableChild(Transform root, string path) { if (!((Object)(object)root == (Object)null)) { Transform val = root.Find(path); if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.SetActive(false); } } } private static void SetVisible(bool visible) { if ((Object)(object)_hudObject != (Object)null && _hudObject.activeSelf != visible) { _hudObject.SetActive(visible); } } } } namespace BalrondAchievementRewards.Runtime { internal static class CharacterIdentity { internal static string GetStableId(Player player) { PlayerProfile currentProfile = GetCurrentProfile(); if (currentProfile != null) { long playerID = currentProfile.GetPlayerID(); if (playerID != 0) { return "player-id:" + playerID.ToString(CultureInfo.InvariantCulture); } string filename = currentProfile.GetFilename(); if (!string.IsNullOrWhiteSpace(filename)) { return "profile-file:" + filename.Trim(); } if (!string.IsNullOrWhiteSpace(currentProfile.m_filename)) { return "profile-file:" + currentProfile.m_filename.Trim(); } string name = currentProfile.GetName(); if (!string.IsNullOrWhiteSpace(name)) { return "profile-name-fallback:" + name.Trim(); } } if ((Object)(object)player != (Object)null) { string playerName = player.GetPlayerName(); if (!string.IsNullOrWhiteSpace(playerName)) { return "player-name-fallback:" + playerName.Trim(); } } return null; } internal static PlayerProfile GetCurrentProfile() { if ((Object)(object)Game.instance == (Object)null) { return null; } try { return Game.instance.GetPlayerProfile(); } catch { return null; } } } internal static class TitleRewardCatalog { private static readonly List<TitleDefinition> DefinitionsInternal = new List<TitleDefinition>(); private static readonly Dictionary<string, TitleDefinition> ById = new Dictionary<string, TitleDefinition>(StringComparer.OrdinalIgnoreCase); private static uint _fingerprint; private static string _lastCoverageSignature; internal static IReadOnlyList<TitleDefinition> Definitions => DefinitionsInternal; internal static uint Fingerprint => _fingerprint; internal static void Initialize() { RebuildFromConfig(); } internal static void Shutdown() { DefinitionsInternal.Clear(); ById.Clear(); _fingerprint = 0u; _lastCoverageSignature = null; } internal static void RebuildFromConfig() { string text = ((BARConfig.TitleRewards != null) ? BARConfig.TitleRewards.Value : "id:AllBosses|forsakensbane|Forsaken's Bane;id:AllBossesHard|doomwalker|Doomwalker;id:AllBossesNormal|realmwarden|Realmwarden;id:AllBuildPieces|masterbuilder|Master Builder;id:AllFoodCooked|masterchef|Master Chef;id:AllFoodEaten|ironstomach|Iron Stomach;id:AllItemCraft|masterartisan|Master Artisan;id:AllMiniBosses|legendhunter|Legend Hunter;id:AllWeaponCraft|arsenalkeeper|Arsenal Keeper;id:Arrived|newblood|New Blood;id:BigFish|masterangler|Master Angler;id:Boss1Eikthyr|stormtouched|Stormtouched;id:Boss2Elder|rootbreaker|Rootbreaker;id:Boss3Bonemass|lightbringer|Lightbringer;id:Boss4Moder|dragonsbane|Dragonsbane;id:Boss5Yagluth|kingsbane|Kingsbane;id:Boss6Queen|hivebreaker|Hivebreaker;id:Boss7Fader|flamebreaker|Flamebreaker;id:Boss8FrozenKing|chainbreaker|Chainbreaker;id:BuildDyrnwyn|flameforged|Flameforged;id:BuildHigh|skybuilder|Skybuilder;id:BuildHighWorld|cloudpiercer|Cloudpiercer;id:BuildHouse|homesteader|Homesteader;id:BuildVillage|hearthjarl|Hearth Jarl;id:ChildOfOdin|odinschosen|Odin's Chosen;id:DeathByAllTypes|deathscompanion|Death's Companion;id:DeathByObliterator|atomized|Atomized;id:DeathByTree|timbered|Timbered;id:DeathByTreeAll|treemagnet|Tree Magnet;id:ExploreNSEW|wayfinder|Wayfinder;id:ExploreNSEWNoMap|pathless|Pathless;id:FindAllTrophies|trophykeeper|Trophy Keeper;id:FirstDeath|returned|Returned;id:GrindBuild|architect|Architect;id:GrindComfort|hearthkeeper|Hearthkeeper;id:GrindCook|cookfireveteran|Cookfire Veteran;id:GrindDays|survivor|Survivor;id:GrindEat|bottomless|Bottomless;id:GrindFish|fishmaster|Fishmaster;id:GrindForager|wildharvester|Wild Harvester;id:GrindHarvest|harvestlord|Harvest Lord;id:GrindHoney|beekeeper|Beekeeper;id:GrindLeviathan|leviathansbane|Leviathan's Bane;id:GrindSail|searover|Sea Rover;id:GrindSailHelm|helmsman|Helmsman;id:GrindTreasure|treasurehunter|Treasure Hunter;id:GrindTrees|woodsman|Woodsman;id:KillAllCreatures|beastbane|Beastbane;id:KillAllCreaturesHard|apexhunter|Apex Hunter;id:KingsQueens|crownbreaker|Crownbreaker;id:MultiplayerBoss|oathbound|Oathbound;id:SailEdge|edgewalker|Edgewalker;id:SoloBoss|lonewolf|Lone Wolf"); _fingerprint = UnsafeTextHash.Compute(text ?? string.Empty); DefinitionsInternal.Clear(); ById.Clear(); _lastCoverageSignature = null; if (string.IsNullOrWhiteSpace(text)) { ModLog.Warning("Title reward catalog is empty."); return; } string[] array = text.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { if (!TryParse(array[i], out var definition, out var error)) { ModLog.Warning("Skipping title reward entry #" + (i + 1).ToString(CultureInfo.InvariantCulture) + ": " + error); continue; } if (ById.ContainsKey(definition.TitleId)) { ModLog.Warning("Skipping duplicate TitleId '" + definition.TitleId + "'. Title IDs must be unique."); continue; } DefinitionsInternal.Add(definition); ById.Add(definition.TitleId, definition); } ResolveRequirements(AchievementWatcher.LastScan); ModLog.Info("Loaded " + DefinitionsInternal.Count.ToString(CultureInfo.InvariantCulture) + " vanilla-achievement title reward(s). Catalog fingerprint 0x" + _fingerprint.ToString("X8", CultureInfo.InvariantCulture) + "."); } internal static bool TryGet(string titleId, out TitleDefinition definition) { definition = null; if (string.IsNullOrWhiteSpace(titleId)) { return false; } return ById.TryGetValue(titleId.Trim(), out definition); } internal static TitleDefinition FindForAchievement(Achievement achievement) { if ((Object)(object)achievement == (Object)null) { return null; } string text = ((achievement.m_id == null) ? string.Empty : achievement.m_id.Trim()); string text2 = (string.IsNullOrEmpty(text) ? string.Empty : ("id:" + text)); string text3 = ((achievement.m_name == null) ? string.Empty : achievement.m_name.Trim()); TitleDefinition result = null; for (int i = 0; i < DefinitionsInternal.Count; i++) { TitleDefinition titleDefinition = DefinitionsInternal[i]; if (!string.IsNullOrEmpty(text2) && string.Equals(titleDefinition.ResolvedAchievementKey, text2, StringComparison.Ordinal)) { return titleDefinition; } if (titleDefinition.MatchMode == AchievementMatchMode.Id && !string.IsNullOrEmpty(text) && string.Equals(titleDefinition.AchievementMatcher, text, StringComparison.OrdinalIgnoreCase)) { return titleDefinition; } if (titleDefinition.MatchMode == AchievementMatchMode.Name && !string.IsNullOrEmpty(text3) && string.Equals(titleDefinition.AchievementMatcher, text3, StringComparison.OrdinalIgnoreCase)) { result = titleDefinition; } } return result; } internal static void ResolveRequirements(AchievementScanResult scan) { for (int i = 0; i < DefinitionsInternal.Count; i++) { TitleDefinition titleDefinition = DefinitionsInternal[i]; titleDefinition.ResolvedAchievementKey = null; titleDefinition.ResolvedAchievementDisplayName = null; titleDefinition.AmbiguousRequirement = false; if (scan == null || scan.Achievements == null || scan.Achievements.Count == 0) { continue; } AchievementSnapshot achievementSnapshot = null; int num = 0; bool flag = false; for (int j = 0; j < scan.Achievements.Count; j++) { AchievementSnapshot achievementSnapshot2 = scan.Achievements[j]; int matchScore = GetMatchScore(titleDefinition, achievementSnapshot2); if (matchScore > 0) { if (matchScore > num) { achievementSnapshot = achievementSnapshot2; num = matchScore; flag = false; } else if (matchScore == num && achievementSnapshot != null && !string.Equals(achievementSnapshot.StableKey, achievementSnapshot2.StableKey, StringComparison.Ordinal)) { flag = true; } } } if (achievementSnapshot != null && !flag) { titleDefinition.ResolvedAchievementKey = achievementSnapshot.StableKey; titleDefinition.ResolvedAchievementDisplayName = achievementSnapshot.DisplayName; ModLog.Debug("Resolved title '" + titleDefinition.TitleId + "' -> vanilla achievement '" + achievementSnapshot.DisplayName + "' [" + achievementSnapshot.StableKey + "] score=" + num.ToString(CultureInfo.InvariantCulture)); } else if (flag) { titleDefinition.AmbiguousRequirement = true; ModLog.WarningOnce("ambiguous_" + titleDefinition.TitleId, "Title '" + titleDefinition.TitleId + "' matcher '" + titleDefinition.AchievementMatcher + "' matches multiple vanilla achievements equally. Prefer an id: matcher."); } else { ModLog.WarningOnce("unresolved_" + titleDefinition.TitleId, "Title '" + titleDefinition.TitleId + "' could not resolve vanilla achievement matcher '" + titleDefinition.AchievementMatcher + "'."); } } ValidateCoverage(scan); } internal static bool IsUnlocked(TitleDefinition definition, AchievementScanResult scan) { if (definition == null || scan == null || scan.Achievements == null || string.IsNullOrEmpty(definition.ResolvedAchievementKey)) { return false; } for (int i = 0; i < scan.Achievements.Count; i++) { AchievementSnapshot achievementSnapshot = scan.Achievements[i]; if (string.Equals(achievementSnapshot.StableKey, definition.ResolvedAchievementKey, StringComparison.Ordinal)) { return achievementSnapshot.Unlocked; } } return false; } internal static string GetRequirementText(TitleDefinition definition) { if (definition == null) { return string.Empty; } return (!string.IsNullOrWhiteSpace(definition.ResolvedAchievementDisplayName)) ? definition.ResolvedAchievementDisplayName : (definition.AchievementMatcher ?? string.Empty); } private static void ValidateCoverage(AchievementScanResult scan) { if (scan == null || scan.Achievements == null || scan.Achievements.Count == 0) { return; } int num = 0; List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int i = 0; i < scan.Achievements.Count; i++) { AchievementSnapshot achievementSnapshot = scan.Achievements[i]; bool flag = false; for (int j = 0; j < DefinitionsInternal.Count; j++) { TitleDefinition titleDefinition = DefinitionsInternal[j]; if (!string.IsNullOrEmpty(titleDefinition.ResolvedAchievementKey) && string.Equals(titleDefinition.ResolvedAchievementKey, achievementSnapshot.StableKey, StringComparison.Ordinal)) { flag = true; hashSet.Add(titleDefinition.ResolvedAchievementKey); break; } } if (flag) { num++; } else { list.Add((!string.IsNullOrWhiteSpace(achievementSnapshot.InternalId)) ? achievementSnapshot.InternalId : achievementSnapshot.StableKey); } } string text = num.ToString(CultureInfo.InvariantCulture) + "/" + scan.Achievements.Count.ToString(CultureInfo.InvariantCulture) + ":" + string.Join(",", list.ToArray()); if (!string.Equals(_lastCoverageSignature, text, StringComparison.Ordinal)) { _lastCoverageSignature = text; ModLog.Info("Vanilla achievement title coverage: " + num.ToString(CultureInfo.InvariantCulture) + "/" + scan.Achievements.Count.ToString(CultureInfo.InvariantCulture) + "."); if (list.Count > 0) { ModLog.Warning("Vanilla achievements without a configured title: " + string.Join(", ", list.ToArray())); } } } private static bool TryParse(string rawEntry, out TitleDefinition definition, out string error) { definition = null; error = null; string[] array = rawEntry.Split(new char[1] { '|' }, StringSplitOptions.None); if (array.Length < 3 || array.Length > 5) { error = "expected 3-5 '|' separated fields, got " + array.Length.ToString(CultureInfo.InvariantCulture) + "."; return false; } string text = (array[0] ?? string.Empty).Trim(); string text2 = (array[1] ?? string.Empty).Trim(); string text3 = (array[2] ?? string.Empty).Trim(); string text4 = ((array.Length >= 4) ? (array[3] ?? string.Empty).Trim() : "{player} the {title}"); string colorHex = ((array.Length >= 5) ? (array[4] ?? string.Empty).Trim() : string.Empty); if (string.IsNullOrWhiteSpace(text)) { error = "vanilla achievement matcher is empty."; return false; } if (!IsValidTitleId(text2)) { error = "TitleId '" + text2 + "' is invalid. Use only letters, numbers, '.', '_' or '-'."; return false; } if (string.IsNullOrWhiteSpace(text3)) { error = "display name is empty."; return false; } if (text3.IndexOf('|') >= 0 || text3.IndexOf(';') >= 0) { error = "display name contains a reserved delimiter ('|' or ';')."; return false; } if (string.IsNullOrWhiteSpace(text4)) { text4 = "{player} the {title}"; } if (text4.IndexOf("{player}", StringComparison.OrdinalIgnoreCase) < 0 || text4.IndexOf("{title}", StringComparison.OrdinalIgnoreCase) < 0) { error = "NameFormat must contain both {player} and {title}."; return false; } int num = text.IndexOf(':'); string text5 = ((num > 0) ? text.Substring(0, num).Trim() : "auto"); string text6 = ((num > 0) ? text.Substring(num + 1).Trim() : text); AchievementMatchMode matchMode; if (text5.Equals("id", StringComparison.OrdinalIgnoreCase)) { matchMode = AchievementMatchMode.Id; } else if (text5.Equals("name", StringComparison.OrdinalIgnoreCase)) { matchMode = AchievementMatchMode.Name; } else if (text5.Equals("auto", StringComparison.OrdinalIgnoreCase)) { matchMode = AchievementMatchMode.Auto; } else { if (num > 0) { error = "unsupported matcher prefix '" + text5 + "'. BalrondAchievementRewards 0.3.2 supports VANILLA achievements only: id:, name:, auto:."; return false; } matchMode = AchievementMatchMode.Auto; text6 = text; } if (string.IsNullOrWhiteSpace(text6)) { error = "achievement matcher is empty after its prefix."; return false; } definition = new TitleDefinition { MatchMode = matchMode, AchievementMatcher = text6, TitleId = text2, DisplayName = text3, NameFormat = text4, ColorHex = colorHex }; return true; } private static bool IsValidTitleId(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '.' && c != '_' && c != '-') { return false; } } return true; } private static int GetMatchScore(TitleDefinition definition, AchievementSnapshot snapshot) { if (definition == null || snapshot == null) { return 0; } string matcher = definition.AchievementMatcher ?? string.Empty; if (definition.MatchMode == AchievementMatchMode.Id) { return ScoreId(matcher, snapshot); } if (definition.MatchMode == AchievementMatchMode.Name) { return ScoreName(matcher, snapshot); } int num = ScoreId(matcher, snapshot); if (num > 0) { return num; } return ScoreName(matcher, snapshot); } private static int ScoreId(string matcher, AchievementSnapshot snapshot) { if (EqualsLoose(matcher, snapshot.InternalId)) { return 100; } if (EqualsLoose(matcher, snapshot.PlatformId)) { return 98; } if (EqualsLoose(matcher, snapshot.StableKey)) { return 96; } string text = Normalize(matcher); if (text.Length == 0) { return 0; } if (Normalize(snapshot.InternalId) == text) { return 92; } if (Normalize(snapshot.PlatformId) == text) { return 90; } if (Normalize(snapshot.StableKey) == text) { return 88; } return 0; } private static int ScoreName(string matcher, AchievementSnapshot snapshot) { if (EqualsLoose(matcher, snapshot.DisplayName)) { return 86; } if (EqualsLoose(matcher, snapshot.NameTextId)) { return 84; } string text = ReduceName(Normalize(matcher)); if (text.Length == 0) { return 0; } if (ReduceName(Normalize(snapshot.DisplayName)) == text) { return 82; } if (ReduceName(Normalize(snapshot.NameTextId)) == text) { return 80; } return 0; } private static bool EqualsLoose(string a, string b) { return !string.IsNullOrWhiteSpace(a) && !string.IsNullOrWhiteSpace(b) && string.Equals(a.Trim(), b.Trim(), StringComparison.OrdinalIgnoreCase); } private static string Normalize(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(value.Length); foreach (char c in value) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } private static string ReduceName(string normalized) { if (string.IsNullOrEmpty(normalized)) { return string.Empty; } string text = normalized; text = text.Replace("achievements", string.Empty).Replace("achievement", string.Empty).Replace("name", string.Empty) .Replace("title", string.Empty); if (text.StartsWith("the", StringComparison.Ordinal) && text.Length > 3) { text = text.Substring(3); } return text; } } internal static class TitleRuntime { private static readonly HashSet<string> UnlockedTitleIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static Player _localPlayer; private static string _characterIdentity; private static string _preferredTitleId = string.Empty; private static string _activeTitleId = string.Empty; private static float _nextPublishTime; private static bool _initialized; internal static string ActiveTitleId => _activeTitleId; internal static string PreferredTitleId => _preferredTitleId; internal static string CharacterIdentityKey => _characterIdentity; internal static void Initialize() { UnlockedTitleIds.Clear(); _localPlayer = null; _characterIdentity = null; _preferredTitleId = string.Empty; _activeTitleId = string.Empty; _nextPublishTime = 0f; _initialized = true; } internal static void Shutdown() { if ((Object)(object)_localPlayer != (Object)null) { try { TitleNetwork.Publish(_localPlayer, string.Empty); } catch { } } UnlockedTitleIds.Clear(); _localPlayer = null; _characterIdentity = null; _preferredTitleId = string.Empty; _activeTitleId = string.Empty; _nextPublishTime = 0f; _initialized = false; } internal static void Tick(float now) { if (!_initialized) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)(object)_localPlayer) { if ((Object)(object)localPlayer != (Object)null) { OnLocalPlayerSpawned(localPlayer); } else { ClearLocalPlayer(); } } if ((Object)(object)_localPlayer != (Object)null && now >= _nextPublishTime) { _nextPublishTime = now + 5f; TitleNetwork.Publish(_localPlayer, _activeTitleId); } } internal static void OnLocalPlayerSpawned(Player player) { if (_initialized && !((Object)(object)player == (Object)null)) { _localPlayer = player; _characterIdentity = CharacterIdentity.GetStableId(player); _preferredTitleId = TitleSelectionStore.GetPreferredTitle(_characterIdentity); _activeTitleId = string.Empty; _nextPublishTime = 0f; AchievementWatcher.MarkDirty("local player spawned"); RefreshUnlocks(AchievementWatcher.LastScan, initial: true); EvaluateActiveTitle(immediatePublish: false); TitleSelectionGui.RequestRefresh(); LocalPlayerTitleHud.RequestRefresh(); ModLog.Debug("Local character identity resolved: " + (_characterIdentity ?? "<none>") + "; preferred title='" + _preferredTitleId + "'."); } } internal static void OnAchievementSnapshot(AchievementScanResult scan, IList<string> newlyUnlockedAchievementKeys, bool initial) { if (_initialized) { RefreshUnlocks(scan, initial); } } internal static void OnRewardConfigurationChanged() { RefreshUnlocks(AchievementWatcher.LastScan, initial: true); } internal static bool IsTitleUnlocked(string titleId) { return !string.IsNullOrWhiteSpace(titleId) && UnlockedTitleIds.Contains(titleId.Trim()); } internal static bool SelectTitle(string titleId) { if ((Object)(object)_localPlayer == (Object)null || string.IsNullOrWhiteSpace(_characterIdentity)) { return false; } string text = ((titleId == null) ? string.Empty : titleId.Trim()); if (!string.IsNullOrEmpty(text) && (!TitleRewardCatalog.TryGet(text, out var _) || !IsTitleUnlocked(text))) { ModLog.Warning("Rejected title selection '" + text + "' because its vanilla achievement is not unlocked in the synchronized catalog."); return false; } _preferredTitleId = text; TitleSelectionStore.SetPreferredTitle(_characterIdentity, _preferredTitleId); EvaluateActiveTitle(immediatePublish: true); TitleSelectionGui.RequestRefresh(); LocalPlayerTitleHud.RequestRefresh(); return true; } internal static TitleDefinition GetActiveDefinition() { TitleDefinition definition; return TitleRewardCatalog.TryGet(_activeTitleId, out definition) ? definition : null; } internal static string GetStatusText() { if (BARConfig.Enabled != null && !BARConfig.Enabled.Value) { return "Achievement title rewards are disabled by the server."; } AchievementScanResult lastScan = AchievementWatcher.LastScan; if (lastScan == null || !lastScan.ManagerAvailable) { return "Vanilla achievements are still initializing."; } if (!lastScan.PlatformInitialized) { return "Platform achievements are still initializing."; } if (lastScan.CanGetAchievements.HasValue && !lastScan.CanGetAchievements.Value) { return "Valheim currently blocks achievement progress for this character/session."; } if (!string.IsNullOrEmpty(lastScan.Error) && lastScan.Achievements.Count == 0) { return lastScan.Error; } if (!string.IsNullOrEmpty(_preferredTitleId) && string.IsNullOrEmpty(_activeTitleId)) { return "Preferred title is not currently available or its vanilla achievement is no longer unlocked."; } return "Unlocked titles: " + UnlockedTitleIds.Count.ToString(CultureInfo.InvariantCulture) + "/" + TitleRewardCatalog.Definitions.Count.ToString(CultureInfo.InvariantCulture); } private static void RefreshUnlocks(AchievementScanResult scan, bool initial) { if (!_initialized) { return; } HashSet<string> hashSet = new HashSet<string>(UnlockedTitleIds, StringComparer.OrdinalIgnoreCase); string activeTitleId = _activeTitleId; UnlockedTitleIds.Clear(); if (BARConfig.Enabled != null && BARConfig.Enabled.Value) { IReadOnlyList<TitleDefinition> definitions = TitleRewardCatalog.Definitions; for (int i = 0; i < definitions.Count; i++) { TitleDefinition titleDefinition = definitions[i]; if (TitleRewardCatalog.IsUnlocked(titleDefinition, scan)) { UnlockedTitleIds.Add(titleDefinition.TitleId); } } } EvaluateActiveTitle(immediatePublish: false); if (!hashSet.SetEquals(UnlockedTitleIds) || !string.Equals(activeTitleId, _activeTitleId, StringComparison.OrdinalIgnoreCase)) { TitleSelectionGui.RequestRefresh(); LocalPlayerTitleHud.RequestRefresh(); } if (!initial) { foreach (string unlockedTitleId in UnlockedTitleIds) { if (!hashSet.Contains(unlockedTitleId) && TitleRewardCatalog.TryGet(unlockedTitleId, out var definition)) { TitleSelectionGui.ShowToast("Title unlocked: " + definition.DisplayName); } } } ModLog.Debug("Vanilla achievement reward snapshot applied. Unlocked titles=" + UnlockedTitleIds.Count.ToString(CultureInfo.InvariantCulture) + ", active='" + _activeTitleId + "'."); } private static void EvaluateActiveTitle(bool immediatePublish) { string text = string.Empty; if (BARConfig.Enabled != null && BARConfig.Enabled.Value && !string.IsNullOrWhiteSpace(_preferredTitleId) && IsTitleUnlocked(_preferredTitleId) && TitleRewardCatalog.TryGet(_preferredTitleId, out var definition)) { text = definition.TitleId; } if (!string.Equals(_activeTitleId, text, StringComparison.OrdinalIgnoreCase)) { _activeTitleId = text; immediatePublish = true; LocalPlayerTitleHud.RequestRefresh(); } if (immediatePublish && (Object)(object)_localPlayer != (Object)null) { TitleNetwork.Publish(_localPlayer, _activeTitleId); _nextPublishTime = Time.unscaledTime + 5f; } } private static void ClearLocalPlayer() { _localPlayer = null; _characterIdentity = null; _preferredTitleId = string.Empty; _activeTitleId = string.Empty; _nextPublishTime = 0f; TitleSelectionGui.OnLocalPlayerCleared(); LocalPlayerTitleHud.Shutdown(); } } internal static class TitleSelectionStore { private const string SelectionMagic = "BAR_TITLE_SELECTIONS_V1"; private static readonly Dictionary<string, string> SelectionByIdentityHash = new Dictionary<string, string>(StringComparer.Ordinal); private static string _selectionPath; private static bool _selectionDirty; private static bool _initialized; internal static void Initialize() { string text = Path.Combine(Paths.ConfigPath, "BalrondAchievementRewards"); Directory.CreateDirectory(text); _selectionPath = Path.Combine(text, "character_titles.v1.tsv"); SelectionByIdentityHash.Clear(); _selectionDirty = false; LoadSelections(); _initialized = true; } internal static string GetPreferredTitle(string stableIdentity) { if (!_initialized || string.IsNullOrWhiteSpace(stableIdentity)) { return string.Empty; } string value; return SelectionByIdentityHash.TryGetValue(HashIdentity(stableIdentity), out value) ? value : string.Empty; } internal static void SetPreferredTitle(string stableIdentity, string titleId) { if (!_initialized || string.IsNullOrWhiteSpace(stableIdentity)) { return; } string key = HashIdentity(stableIdentity); string text = ((titleId == null) ? string.Empty : titleId.Trim()); if (!SelectionByIdentityHash.TryGetValue(key, out var value) || !string.Equals(value, text, StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrEmpty(text)) { SelectionByIdentityHash.Remove(key); } else { SelectionByIdentityHash[key] = text; } _selectionDirty = true; Flush(); } } internal static void Flush() { if (!_initialized || !_selectionDirty || string.IsNullOrEmpty(_selectionPath)) { return; } try { StringBuilder stringBuilder = new StringBuilder(2048); stringBuilder.AppendLine("BAR_TITLE_SELECTIONS_V1"); foreach (KeyValuePair<string, string> item in SelectionByIdentityHash) { stringBuilder.Append(item.Key).Append('\t').Append(ToBase64(item.Value ?? string.Empty)) .AppendLine(); } AtomicWrite(_selectionPath, stringBuilder.ToString()); _selectionDirty = false; } catch (Exception ex) { ModLog.Warning("Could not persist selected title state: " + ex.Message); } } private static void LoadSelections() { if (string.IsNullOrEmpty(_selectionPath) || !File.Exists(_selectionPath)) { return; } try { string[] array = File.ReadAllLines(_selectionPath); if (array.Length == 0 || !string.Equals(array[0].Trim(), "BAR_TITLE_SELECTIONS_V1", StringComparison.Ordinal)) { ModLog.Warning("Ignoring unknown title selection file format at " + _selectionPath); return; } for (int i = 1; i < array.Length; i++) { if (!TrySplitLine(array[i], out var key, out var payload)) { continue; } try { string text = FromBase64(payload); if (!string.IsNullOrWhiteSpace(key) && !string.IsNullOrWhiteSpace(text)) { SelectionByIdentityHash[key] = text.Trim(); } } catch { } } } catch (Exception ex) { ModLog.Warning("Could not load title selection state: " + ex.Message); } } private static bool TrySplitLine(string line, out string key, out string payload) { key = null; payload = null; if (string.IsNullOrWhiteSpace(line)) { return false; } int num = line.IndexOf('\t'); if (num <= 0 || num >= line.Length - 1) { return false; } key = line.Substring(0, num).Trim(); payload = line.Substring(num + 1).Trim(); return key.Length > 0 && payload.Length > 0; } private static void AtomicWrite(string path, string content) { string text = path + ".tmp"; File.WriteAllText(text, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); File.Copy(text, path, overwrite: true); File.Delete(text); } private static string ToBase64(string value) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(value ?? string.Empty)); } private static string FromBase64(string value) { return Encoding.UTF8.GetString(Convert.FromBase64String(value ?? string.Empty)); } private static string HashIdentity(string stableIdentity) { byte[] bytes = Encoding.UTF8.GetBytes(stableIdentity ?? string.Empty); using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(bytes); StringBuilder stringBuilder = new StringBuilder(32); for (int i = 0; i < 16; i++) { stringBuilder.Append(array[i].ToString("x2")); } return stringBuilder.ToString(); } } internal static class TitleTextFormatter { internal static string FormatHoverName(string playerName, TitleDefinition definition) { if (definition == null) { return playerName ?? string.Empty; } string text = playerName ?? string.Empty; string text2 = SanitizeLiteral(definition.DisplayName); string text3 = (string.IsNullOrWhiteSpace(definition.NameFormat) ? "{player} the {title}" : SanitizeFormat(definition.NameFormat)); if (BARConfig.UseTitleColors != null && BARConfig.UseTitleColors.Value && definition.TryGetColor(out var _)) { string text4 = NormalizeColorHex(definition.ColorHex); text2 = "<color=" + text4 + ">" + text2 + "</color>"; } string text5 = text3.Replace("{player}", text).Replace("{title}", text2); if (text5.IndexOf("{player}", StringComparison.OrdinalIgnoreCase) >= 0 || text5.IndexOf("{title}", StringComparison.OrdinalIgnoreCase) >= 0) { return text + " " + text2; } return text5.Trim(); } internal static string SanitizeLiteral(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } return value.Replace('<', '‹').Replace('>', '›'); } private static string SanitizeFormat(string value) { if (string.IsNullOrEmpty(value)) { return "{player} the {title}"; } StringBuilder stringBuilder = new StringBuilder(value.Length); foreach (char c in value) { switch (c) { case '<': stringBuilder.Append('‹'); break; case '>': stringBuilder.Append('›'); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString(); } private static string NormalizeColorHex(string value) { if (string.IsNullOrWhiteSpace(value)) { return "#FFFFFF"; } string text = value.Trim(); if (!text.StartsWith("#", StringComparison.Ordinal)) { text = "#" + text; } return text; } } } namespace BalrondAchievementRewards.Patches { internal static class PatchInstaller { internal static void Install(Harmony harmony) { if (harmony == null) { throw new ArgumentNullException("harmony"); } harmony.PatchAll(typeof(PatchInstaller).Assembly); ModLog.Info("Installed direct publicized Valheim API patches for vanilla achievement rewards, child-overlay click-to-title selection on unlocked AchElements, and player title presentation."); } } [HarmonyPatch(typeof(Player), "OnSpawned", new Type[] { typeof(bool) })] internal static class PlayerOnSpawnedPatch { private static void Postfix(Player __instance) { if ((Object)(object)__instance != (Object)null && (Object)(object)__instance == (Object)(object)Player.m_localPlayer) { TitleRuntime.OnLocalPlayerSpawned(__instance); } } } [HarmonyPatch(typeof(Player), "GetHoverName")] internal static class PlayerGetHoverNamePatch { [HarmonyPriority(0)] private static void Postfix(Player __instance, ref string __result) { if (!((Object)(object)__instance == (Object)null) && BARConfig.ShowTitlesAbovePlayers != null && BARConfig.ShowTitlesAbovePlayers.Value && (BARConfig.Enabled == null || BARConfig.Enabled.Value)) { string text = TitleNetwork.ReadPublishedTitle(__instance); if (!string.IsNullOrEmpty(text) && TitleRewardCatalog.TryGet(text, out var definition)) { __result = TitleTextFormatter.FormatHoverName(__result, definition); } } } } [HarmonyPatch(typeof(Achievements), "SetPlatformInitialized")] internal static class AchievementsSetPlatformInitializedPatch { private static void Postfix() { AchievementWatcher.MarkDirty("Achievements.SetPlatformInitialized"); } } [HarmonyPatch(typeof(Achievements), "OnAchievementUnlocked", new Type[] { typeof(bool) })] internal static class AchievementsOnAchievementUnlockedPatch { private static void Postfix() { AchievementWatcher.MarkDirty("Achievements.OnAchievementUnlocked"); } } [HarmonyPatch(typeof(Achievements), "ResetAchievement", new Type[] { typeof(string) })] internal static class AchievementsResetAchievementPatch { private static void Postfix() { AchievementWatcher.MarkDirty("Achievements.ResetAchievement"); } } [HarmonyPatch(typeof(Achievements), "ResetAllAchievements")] internal static class AchievementsResetAllAchievementsPatch { private static void Postfix() { AchievementWatcher.MarkDirty("Achievements.ResetAllAchievements"); } } [HarmonyPatch(typeof(InventoryGui), "Awake")] internal static class InventoryGuiAwakeAchievementRewardsPatch { private static void Postfix(InventoryGui __instance) { TitleSelectionGui.OnInventoryGuiAwake(__instance); } } [HarmonyPatch(typeof(InventoryGui), "OnOpenAchievements")] internal static class InventoryGuiOpenAchievementsRewardPatch { private static void Postfix(InventoryGui __instance) { TitleSelectionGui.OnAchievementsOpened(__instance); } } [HarmonyPatch(typeof(InventoryGui), "OnCloseAchievements")] internal static class InventoryGuiCloseAchievementsRewardPatch { private static void Postfix(InventoryGui __instance) { TitleSelectionGui.OnAchievementsClosed(__instance); } } [HarmonyPatch(typeof(InventoryGui), "UpdateAchievementsList")] internal static class InventoryGuiUpdateAchievementsListRewardPatch { private static void Postfix(InventoryGui __instance) { TitleSelectionGui.OnVanillaAchievementsListUpdated(__instance); } } [HarmonyPatch(typeof(InventoryGui), "Hide")] internal static class InventoryGuiHideAchievementRewardsPatch { private static void Postfix() { TitleSelectionGui.OnInventoryHidden(); } } } namespace BalrondAchievementRewards.Networking { internal static class TitleNetwork { internal const string ZdoTitleKey = "balrond_achievement_rewards_title_v1"; internal static void Publish(Player player, string titleId) { if ((Object)(object)player == (Object)null) { return; } try { ZNetView nview = ((Character)player).m_nview; if ((Object)(object)nview == (Object)null || !nview.IsValid() || !nview.IsOwner()) { return; } ZDO zDO = nview.GetZDO(); if (zDO != null) { string text = string.Empty; if (!string.IsNullOrWhiteSpace(titleId) && TitleRewardCatalog.TryGet(titleId.Trim(), out var definition)) { text = definition.TitleId; } string a = zDO.GetString("balrond_achievement_rewards_title_v1", string.Empty); if (!string.Equals(a, text, StringComparison.Ordinal)) { zDO.Set("balrond_achievement_rewards_title_v1", text); } } } catch (Exception ex) { ModLog.ErrorOnce("title_publish", "Could not publish active title to Player ZDO: " + ex.Message); } } internal static string ReadPublishedTitle(Player player) { if ((Object)(object)player == (Object)null) { return string.Empty; } try { ZNetView nview = ((Character)player).m_nview; if ((Object)(object)nview == (Object)null || !nview.IsValid()) { return string.Empty; } ZDO zDO = nview.GetZDO(); if (zDO == null) { return string.Empty; } string text = zDO.GetString("balrond_achievement_rewards_title_v1", string.Empty); if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } TitleDefinition definition; return TitleRewardCatalog.TryGet(text.Trim(), out definition) ? definition.TitleId : string.Empty; } catch { return string.Empty; } } } } namespace BalrondAchievementRewards.Data { internal sealed class AchievementSnapshot { internal string StableKey; internal string InternalId; internal string PlatformId; internal string NameTextId; internal string DisplayName; internal string DescriptionTextId; internal string DisplayDescription; internal bool Unlocked; } internal sealed class AchievementScanResult { internal bool ManagerAvailable; internal bool PlatformInitialized; internal bool? CanGetAchievements; internal string Error; internal List<AchievementSnapshot> Achievements = new List<AchievementSnapshot>(); } internal enum AchievementMatchMode { Auto, Id, Name } internal sealed class TitleDefinition { internal AchievementMatchMode MatchMode; internal string AchievementMatcher; internal string TitleId; internal string DisplayName; internal string NameFormat; internal string ColorHex; internal string ResolvedAchievementKey; internal string ResolvedAchievementDisplayName; internal bool AmbiguousRequirement; internal bool TryGetColor(out Color color) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) color = Color.white; if (string.IsNullOrWhiteSpace(ColorHex)) { return false; } string text = ColorHex.Trim(); if (!text.StartsWith("#", StringComparison.Ordinal)) { text = "#" + text; } return ColorUtility.TryParseHtmlString(text, ref color); } } } namespace BalrondAchievementRewards.Core { [BepInPlugin("balrond.astafaraios.BalrondAchievementRewards", "BalrondAchievementRewards", "0.1.0")] public sealed class Launch : BaseUnityPlugin { public const string PluginGUID = "balrond.astafaraios.BalrondAchievementRewards"; public const string PluginName = "BalrondAchievementRewards"; public const string PluginVersion = "0.1.0"; private Harmony _harmony; private bool _shutdown; internal static Launch Instance { get; private set; } private void Awake() { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown Instance = this; ModLog.Initialize(((BaseUnityPlugin)this).Logger); try { ServerSyncBridge.Initialize("balrond.astafaraios.BalrondAchievementRewards", "BalrondAchievementRewards", "0.1.0"); BARConfig.Initialize(this); TitleSelectionStore.Initialize(); TitleRewardCatalog.Initialize(); AchievementWatcher.Initialize(); TitleRuntime.Initialize(); _harmony = new Harmony("balrond.astafaraios.BalrondAchievementRewards"); PatchInstaller.Install(_harmony); ModLog.Info("BalrondAchievementRewards 0.1.0 initialized."); } catch (Exception ex) { ModLog.Error("Initialization failed; BalrondAchievementRewards is disabling itself for this session: " + ex); SafeShutdown(); ((Behaviour)this).enabled = false; } } private void Update() { if (_shutdown) { return; } try { AchievementWatcher.Tick(Time.unscaledTime); TitleRuntime.Tick(Time.unscaledTime); TitleSelectionGui.Tick(); LocalPlayerTitleHud.Tick(); } catch (Exception ex) { ModLog.ErrorOnce("update_exception", "Runtime update failed: " + ex); } } private void OnDestroy() { SafeShutdown(); } private void OnApplicationQuit() { try { TitleSelectionStore.Flush(); } catch { } } private void SafeShutdown() { if (_shutdown) { return; } _shutdown = true; try { TitleSelectionGui.Shutdown(); } catch { } try { LocalPlayerTitleHud.Shutdown(); } catch { } try { TitleRuntime.Shutdown(); } catch { } try { AchievementWatcher.Shutdown(); } catch { } try { TitleRewardCatalog.Shutdown(); } catch { } try { TitleSelectionStore.Flush(); } catch { } try { if (_harmony != null) { _harmony.UnpatchSelf(); } } catch { } try { ServerSyncBridge.Reset(); } catch { } _harmony = null; Instance = null; ModLog.Reset(); } } } namespace BalrondAchievementRewards.Configuration { internal static class BARConfig { internal const string DefaultTitleRewards = "id:AllBosses|forsakensbane|Forsaken's Bane;id:AllBossesHard|doomwalker|Doomwalker;id:AllBossesNormal|realmwarden|Realmwarden;id:AllBuildPieces|masterbuilder|Master Builder;id:AllFoodCooked|masterchef|Master Chef;id:AllFoodEaten|ironstomach|Iron Stomach;id:AllItemCraft|masterartisan|Master Artisan;id:AllMiniBosses|legendhunter|Legend Hunter;id:AllWeaponCraft|arsenalkeeper|Arsenal Keeper;id:Arrived|newblood|New Blood;id:BigFish|masterangler|Master Angler;id:Boss1Eikthyr|stormtouched|Stormtouched;id:Boss2Elder|rootbreaker|Rootbreaker;id:Boss3Bonemass|lightbringer|Lightbringer;id:Boss4Moder|dragonsbane|Dragonsbane;id:Boss5Yagluth|kingsbane|Kingsbane;id:Boss6Queen|hivebreaker|Hivebreaker;id:Boss7Fader|flamebreaker|Flamebreaker;id:Boss8FrozenKing|chainbreaker|Chainbreaker;id:BuildDyrnwyn|flameforged|Flameforged;id:BuildHigh|skybuilder|Skybuilder;id:BuildHighWorld|cloudpiercer|Cloudpiercer;id:BuildHouse|homesteader|Homesteader;id:BuildVillage|hearthjarl|Hearth Jarl;id:ChildOfOdin|odinschosen|Odin's Chosen;id:DeathByAllTypes|deathscompanion|Death's Companion;id:DeathByObliterator|atomized|Atomized;id:DeathByTree|timbered|Timbered;id:DeathByTreeAll|treemagnet|Tree Magnet;id:ExploreNSEW|wayfinder|Wayfinder;id:ExploreNSEWNoMap|pathless|Pathless;id:FindAllTrophies|trophykeeper|Trophy Keeper;id:FirstDeath|returned|Returned;id:GrindBuild|architect|Architect;id:GrindComfort|hearthkeeper|Hearthkeeper;id:GrindCook|cookfireveteran|Cookfire Veteran;id:GrindDays|survivor|Survivor;id:GrindEat|bottomless|Bottomless;id:GrindFish|fishmaster|Fishmaster;id:GrindForager|wildharvester|Wild Harvester;id:GrindHarvest|harvestlord|Harvest Lord;id:GrindHoney|beekeeper|Beekeeper;id:GrindLeviathan|leviathansbane|Leviathan's Bane;id:GrindSail|searover|Sea Rover;id:GrindSailHelm|helmsman|Helmsman;id:GrindTreasure|treasurehunter|Treasure Hunter;id:GrindTrees|woodsman|Woodsman;id:KillAllCreatures|beastbane|Beastbane;id:KillAllCreaturesHard|apexhunter|Apex Hunter;id:KingsQueens|crownbreaker|Crownbreaker;id:MultiplayerBoss|oathbound|Oathbound;id:SailEdge|edgewalker|Edgewalker;id:SoloBoss|lonewolf|Lone Wolf"; private const string Legacy010TitleRewards = "name:$ach_boss1eikthyr|stormtouched|Stormtouched|{player} the {title}|#8FD3FF;name:$ach_boss2elder|rootbreaker|Rootbreaker|{player} the {title}|#A7D46F;name:$ach_boss3bonemass|lightbringer|Lightbringer|{player} the {title}|#E7F5B6;name:$ach_boss4moder|dragonsbane|Dragonsbane|{player} the {title}|#C3E5FF;name:$ach_boss5yagluth|kingsbane|Kingsbane|{player} the {title}|#FFB072;name:$ach_boss6queen|hivebreaker|Hivebreaker|{player} the {title}|#D8B5FF;name:$ach_boss7fader|flamebreaker|Flamebreaker|{player} the {title}|#7FFFC7;name:$ach_boss8frozenking|chainbreaker|Chainbreaker|{player} the {title}|#C6EEFF;name:$ach_multiplayerboss|oathbound|Oathbound|{player} the {title}|#FFD27A;name:$ach_soloboss|lonewolf|Lone Wolf|{player} the {title}|#D0D0D0"; internal static ConfigEntry<bool> LockConfiguration; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<string> TitleRewards; internal static ConfigEntry<bool> ShowTitlesAbovePlayers; internal static ConfigEntry<bool> ShowOwnNameplate; internal static ConfigEntry<float> OwnNameplateVerticalOffset; internal static ConfigEntry<bool> UseTitleColors; internal static ConfigEntry<bool> VerboseLogging; internal static ConfigEntry<bool> WriteAchievementDumpOnInitialize; internal static void Initialize(Launch plugin) { LockConfiguration = ((BaseUnityPlugin)plugin).Config.Bind<bool>("00 - Server", "Lock Configuration", true, "Lock server-synchronized BalrondAchievementRewards settings to the server configuration."); ServerSyncBridge.AddLockingConfigEntry(LockConfiguration); Enabled = ((BaseUnityPlugin)plugin).Config.Bind<bool>("01 - Rewards", "Enabled", true, "Enable achievement reward titles. Server-synchronized."); ServerSyncBridge.AddConfigEntry<bool>(Enabled); TitleRewards = ((BaseUnityPlugin)plugin).Config.Bind<string>("01 - Rewards", "Title Rewards", "id:AllBosses|forsakensbane|Forsaken's Bane;id:AllBossesHard|doomwalker|Doomwalker;id:AllBossesNormal|realmwarden|Realmwarden;id:AllBuildPieces|masterbuilder|Master Builder;id:AllFoodCooked|masterchef|Master Chef;id:AllFoodEaten|ironstomach|Iron Stomach;id:AllItemCraft|masterartisan|Master Artisan;id:AllMiniBosses|legendhunter|Legend Hunter;id:AllWeaponCraft|arsenalkeeper|Arsenal Keeper;id:Arrived|newblood|New Blood;id:BigFish|masterangler|Master Angler;id:Boss1Eikthyr|stormtouched|Stormtouched;id:Boss2Elder|rootbreaker|Rootbreaker;id:Boss3Bonemass|lightbringer|Lightbringer;id:Boss4Moder|dragonsbane|Dragonsbane;id:Boss5Yagluth|kingsbane|Kingsbane;id:Boss6Queen|hivebreaker|Hivebreaker;id:Boss7Fader|flamebreaker|Flamebreaker;id:Boss8FrozenKing|chainbreaker|Chainbreaker;id:BuildDyrnwyn|flameforged|Flameforged;id:BuildHigh|skybuilder|Skybuilder;id:BuildHighWorld|cloudpiercer|Cloudpiercer;id:BuildHouse|homesteader|Homesteader;id:BuildVillage|hearthjarl|Hearth Jarl;id:ChildOfOdin|odinschosen|Odin's Chosen;id:DeathByAllTypes|deathscompanion|Death's Companion;id:DeathByObliterator|atomized|Atomized;id:DeathByTree|timbered|Timbered;id:DeathByTreeAll|treemagnet|Tree Magnet;id:ExploreNSEW|wayfinder|Wayfinder;id:ExploreNSEWNoMap|pathless|Pathless;id:FindAllTrophies|trophykeeper|Trophy Keeper;id:FirstDeath|returned|Returned;id:GrindBuild|architect|Architect;id:GrindComfort|hearthkeeper|Hearthkeeper;id:GrindCook|cookfireveteran|Cookfire Veteran;id:GrindDays|survivor|Survivor;id:GrindEat|bottomless|Bottomless;id:GrindFish|fishmaster|Fishmaster;id:GrindForager|wildharvester|Wild Harvester;id:GrindHarvest|harvestlord|Harvest Lord;id:GrindHoney|beekeeper|Beekeeper;id:GrindLeviathan|leviathansbane|Leviathan's Bane;id:GrindSail|searover|Sea Rover;id:GrindSailHelm|helmsman|Helmsman;id:GrindTreasure|treasurehunter|Treasure Hunter;id:GrindTrees|woodsman|Woodsman;id:KillAllCreatures|beastbane|Beastbane;id:KillAllCreaturesHard|apexhunter|Apex Hunter;id:KingsQueens|crownbreaker|Crownbreaker;id:MultiplayerBoss|oathbound|Oathbound;id:SailEdge|edgewalker|Edgewalker;id:SoloBoss|lonewolf|Lone Wolf", "Server-synchronized one-to-one mapping from VANILLA Valheim achievements to cosmetic titles. Entries are separated by ';'. Fields: AchievementMatcher|TitleId|DisplayName|NameFormat|OptionalColor. Supported matcher prefixes: id:, name:, auto:. Tokens in NameFormat: {player}, {title}. This mod does not create custom achievements or custom progression conditions."); ServerSyncBridge.AddConfigEntry<string>(TitleRewards); MigrateLegacyDefaultCatalog(); ShowTitlesAbovePlayers = ((BaseUnityPlugin)plugin).Config.Bind<bool>("10 - Client Appearance", "Show Titles Above Players", true, "Show active cosmetic titles in other players' native hover/name HUD. Client-local."); ShowOwnNameplate = ((BaseUnityPlugin)plugin).Config.Bind<bool>("10 - Client Appearance", "Show Own Nameplate", true, "Show your own character name and active title above your character using Valheim's native player HUD prefab. Client-local."); OwnNameplateVerticalOffset = ((BaseUnityPlugin)plugin).Config.Bind<float>("10 - Client Appearance", "Own Nameplate Vertical Offset", 0.3f, "Additional world-space height above the local player's head for the self nameplate. Valheim's native player HUD uses roughly 0.30. Client-local."); UseTitleColors = ((BaseUnityPlugin)plugin).Config.Bind<bool>("10 - Client Appearance", "Use Title Colors", true, "Render optional server-defined title colors using TextMeshPro rich text. Client-local."); VerboseLogging = ((BaseUnityPlugin)plugin).Config.Bind<bool>("90 - Diagnostics", "Verbose Logging", false, "Write detailed achievement matching and title state diagnostics. Client-local."); WriteAchievementDumpOnInitialize = ((BaseUnityPlugin)plugin).Config.Bind<bool>("90 - Diagnostics", "Write Achievement Dump On Initialize", false, "Write discovered vanilla achievement identifiers and state to BepInEx/config/BalrondAchievementRewards/achievement_dump.txt after the achievement system initializes. Client-local."); Enabled.SettingChanged += delegate { OnSynchronizedRewardConfigurationChanged(); }; TitleRewards.SettingChanged += delegate { OnSynchronizedRewardConfigurationChanged(); }; UseTitleColors.SettingChanged += delegate { TitleSelectionGui.RequestRefresh(); }; ShowOwnNameplate.SettingChanged += delegate { LocalPlayerTitleHud.RequestRefresh(); }; OwnNameplateVerticalOffset.SettingChanged += delegate { LocalPlayerTitleHud.RequestRefresh(); }; } private static void MigrateLegacyDefaultCatalog() { string text = TitleRewards.Value ?? string.Empty; if (string.Equals(text, "name:$ach_boss1eikthyr|stormtouched|Stormtouched|{player} the {title}|#8FD3FF;name:$ach_boss2elder|rootbreaker|Rootbreaker|{player} the {title}|#A7D46F;name:$ach_boss3bonemass|lightbringer|Lightbringer|{player} the {title}|#E7F5B6;name:$ach_boss4moder|dragonsbane|Dragonsbane|{player} the {title}|#C3E5FF;name:$ach_boss5yagluth|kingsbane|Kingsbane|{player} the {title}|#FFB072;name:$ach_boss6queen|hivebreaker|Hivebreaker|{player} the {title}|#D8B5FF;name:$ach_boss7fader|flamebreaker|Flamebreaker|{player} the {title}|#7FFFC7;name:$ach_boss8frozenking|chainbreaker|Chainbreaker|{player} the {title}|#C6EEFF;name:$ach_multiplayerboss|oathbound|Oathbound|{player} the {title}|#FFD27A;name:$ach_soloboss|lonewolf|Lone Wolf|{player} the {title}|#D0D0D0", StringComparison.Ordinal)) { TitleRewards.Value = "id:AllBosses|forsakensbane|Forsaken's Bane;id:AllBossesHard|doomwalker|Doomwalker;id:AllBossesNormal|realmwarden|Realmwarden;id:AllBuildPieces|masterbuilder|Master Builder;id:AllFoodCooked|masterchef|Master Chef;id:AllFoodEaten|ironstomach|Iron Stomach;id:AllItemCraft|masterartisan|Master Artisan;id:AllMiniBosses|legendhunter|Legend Hunter;id:AllWeaponCraft|arsenalkeeper|Arsenal Keeper;id:Arrived|newblood|New Blood;id:BigFish|masterangler|Master Angler;id:Boss1Eikthyr|stormtouched|Stormtouched;id:Boss2Elder|rootbreaker|Rootbreaker;id:Boss3Bonemass|lightbringer|Lightbringer;id:Boss4Moder|dragonsbane|Dragonsbane;id:Boss5Yagluth|kingsbane|Kingsbane;id:Boss6Queen|hivebreaker|Hivebreaker;id:Boss7Fader|flamebreaker|Flamebreaker;id:Boss8FrozenKing|chainbreaker|Chainbreaker;id:BuildDyrnwyn|flameforged|Flameforged;id:BuildHigh|skybuilder|Skybuilder;id:BuildHighWorld|cloudpiercer|Cloudpiercer;id:BuildHouse|homesteader|Homesteader;id:BuildVillage|hearthjarl|Hearth Jarl;id:ChildOfOdin|odinschosen|Odin's Chosen;id:DeathByAllTypes|deathscompanion|Death's Companion;id:DeathByObliterator|atomized|Atomized;id:DeathByTree|timbered|Timbered;id:DeathByTreeAll|treemagnet|Tree Magnet;id:ExploreNSEW|wayfinder|Wayfinder;id:ExploreNSEWNoMap|pathless|Pathless;id:FindAllTrophies|trophykeeper|Trophy Keeper;id:FirstDeath|returned|Returned;id:GrindBuild|architect|Architect;id:GrindComfort|hearthkeeper|Hearthkeeper;id:GrindCook|cookfireveteran|Cookfire Veteran;id:GrindDays|survivor|Survivor;id:GrindEat|bottomless|Bottomless;id:GrindFish|fishmaster|Fishmaster;id:GrindForager|wildharvester|Wild Harvester;id:GrindHarvest|harvestlord|Harvest Lord;id:GrindHoney|beekeeper|Beekeeper;id:GrindLeviathan|leviathansbane|Leviathan's Bane;id:GrindSail|searover|Sea Rover;id:GrindSailHelm|helmsman|Helmsman;id:GrindTreasure|treasurehunter|Treasure Hunter;id:GrindTrees|woodsman|Woodsman;id:KillAllCreatures|beastbane|Beastbane;id:KillAllCreaturesHard|apexhunter|Apex Hunter;id:KingsQueens|crownbreaker|Crownbreaker;id:MultiplayerBoss|oathbound|Oathbound;id:SailEdge|edgewalker|Edgewalker;id:SoloBoss|lonewolf|Lone Wolf"; ModLog.Info("Migrated the untouched 0.1.0 title catalog to the complete vanilla-achievement catalog."); } else if (text.StartsWith("id:AllBosses|forsakensbane|Forsaken's Bane;id:AllBossesHard|doomwalker|Doomwalker;id:AllBossesNormal|realmwarden|Realmwarden;id:AllBuildPieces|masterbuilder|Master Builder;id:AllFoodCooked|masterchef|Master Chef;id:AllFoodEaten|ironstomach|Iron Stomach;id:AllItemCraft|masterartisan|Master Artisan;id:AllMiniBosses|legendhunter|Legend Hunter;id:AllWeaponCraft|arsenalkeeper|Arsenal Keeper;id:Arrived|newblood|New Blood;id:BigFish|masterangler|Master Angler;id:Boss1Eikthyr|stormtouched|Stormtouched;id:Boss2Elder|rootbreaker|Rootbreaker;id:Boss3Bonemass|lightbringer|Lightbringer;id:Boss4Moder|dragonsbane|Dragonsbane;id:Boss5Yagluth|kingsbane|Kingsbane;id:Boss6Queen|hivebreaker|Hivebreaker;id:Boss7Fader|flamebreaker|Flamebreaker;id:Boss8FrozenKing|chainbreaker|Chainbreaker;id:BuildDyrnwyn|flameforged|Flameforged;id:BuildHigh|skybuilder|Skybuilder;id:BuildHighWorld|cloudpiercer|Cloudpiercer;id:BuildHouse|homesteader|Homesteader;id:BuildVillage|hearthjarl|Hearth Jarl;id:ChildOfOdin|odinschosen|Odin's Chosen;id:DeathByAllTypes|deathscompanion|Death's Companion;id:DeathByObliterator|atomized|Atomized;id:DeathByTree|timbered|Timbered;id:DeathByTreeAll|treemagnet|Tree Magnet;id:ExploreNSEW|wayfinder|Wayfinder;id:ExploreNSEWNoMap|pathless|Pathless;id:FindAllTrophies|trophykeeper|Trophy Keeper;id:FirstDeath|returned|Returned;id:GrindBuild|architect|Architect;id:GrindComfort|hearthkeeper|Hearthkeeper;id:GrindCook|cookfireveteran|Cookfire Veteran;id:GrindDays|survivor|Survivor;id:GrindEat|bottomless|Bottomless;id:GrindFish|fishmaster|Fishmaster;id:GrindForager|wildharvester|Wild Harvester;id:GrindHarvest|harvestlord|Harvest Lord;id:GrindHoney|beekeeper|Beekeeper;id:GrindLeviathan|leviathansbane|Leviathan's Bane;id:GrindSail|searover|Sea Rover;id:GrindSailHelm|helmsman|Helmsman;id:GrindTreasure|treasurehunter|Treasure Hunter;id:GrindTrees|woodsman|Woodsman;id:KillAllCreatures|beastbane|Beastbane;id:KillAllCreaturesHard|apexhunter|Apex Hunter;id:KingsQueens|crownbreaker|Crownbreaker;id:MultiplayerBoss|oathbound|Oathbound;id:SailEdge|edgewalker|Edgewalker;id:SoloBoss|lonewolf|Lone Wolf;pickupsum:", StringComparison.Ordinal) && text.IndexOf("allskills:100|allstar|Allstar", StringComparison.Ordinal) >= 0) { TitleRewards.Value = "id:AllBosses|forsakensbane|Forsaken's Bane;id:AllBossesHard|doomwalker|Doomwalker;id:AllBossesNormal|realmwarden|Realmwarden;id:AllBuildPieces|masterbuilder|Master Builder;id:AllFoodCooked|masterchef|Master Chef;id:AllFoodEaten|ironstomach|Iron Stomach;id:AllItemCraft|masterartisan|Master Artisan;id:AllMiniBosses|legendhunter|Legend Hunter;id:AllWeaponCraft|arsenalkeeper|Arsenal Keeper;id:Arrived|newblood|New Blood;id:BigFish|masterangler|Master Angler;id:Boss1Eikthyr|stormtouched|Stormtouched;id:Boss2Elder|rootbreaker|Rootbreaker;id:Boss3Bonemass|lightbringer|Lightbringer;id:Boss4Moder|dragonsbane|Dragonsbane;id:Boss5Yagluth|kingsbane|Kingsbane;id:Boss6Queen|hivebreaker|Hivebreaker;id:Boss7Fader|flamebreaker|Flamebreaker;id:Boss8FrozenKing|chainbreaker|Chainbreaker;id:BuildDyrnwyn|flameforged|Flameforged;id:BuildHigh|skybuilder|Skybuilder;id:BuildHighWorld|cloudpiercer|Cloudpiercer;id:BuildHouse|homesteader|Homesteader;id:BuildVillage|hearthjarl|Hearth Jarl;id:ChildOfOdin|odinschosen|Odin's Chosen;id:DeathByAllTypes|deathscompanion|Death's Companion;id:DeathByObliterator|atomized|Atomized;id:DeathByTree|timbered|Timbered;id:DeathByTreeAll|treemagnet|Tree Magnet;id:ExploreNSEW|wayfinder|Wayfinder;id:ExploreNSEWNoMap|pathless|Pathless;id:FindAllTrophies|trophykeeper|Trophy Keeper;id:FirstDeath|returned|Returned;id:GrindBuild|architect|Architect;id:GrindComfort|hearthkeeper|Hearthkeeper;id:GrindCook|cookfireveteran|Cookfire Veteran;id:GrindDays|survivor|Survivor;id:GrindEat|bottomless|Bottomless;id:GrindFish|fishmaster|Fishmaster;id:GrindForager|wildharvester|Wild Harvester;id:GrindHarvest|harvestlord|Harvest Lord;id:GrindHoney|beekeeper|Beekeeper;id:GrindLeviathan|leviathansbane|Leviathan's Bane;id:GrindSail|searover|Sea Rover;id:GrindSailHelm|helmsman|Helmsman;id:GrindTreasure|treasurehunter|Treasure Hunter;id:GrindTrees|woodsman|Woodsman;id:KillAllCreatures|beastbane|Beastbane;id:KillAllCreaturesHard|apexhunter|Apex Hunter;id:KingsQueens|crownbreaker|Crownbreaker;id:MultiplayerBoss|oathbound|Oathbound;id:SailEdge|edgewalker|Edgewalker;id:SoloBoss|lonewolf|Lone Wolf"; ModLog.Info("Migrated the untouched 0.2.x catalog: removed mod-side pseudo-achievements and retained all 53 vanilla achievement title mappings."); } } private static void OnSynchronizedRewardConfigurationChanged() { TitleRewardCatalog.RebuildFromConfig(); TitleRuntime.OnRewardConfigurationChanged(); TitleSelectionGui.RequestRefresh(); LocalPlayerTitleHud.RequestRefresh(); } } internal static class ServerSyncBridge { private static object _syncObject; internal static void Initialize(string pluginGuid, string displayName, string version) { ConfigSync syncObject = new ConfigSync(pluginGuid) { DisplayName = displayName, CurrentVersion = version, MinimumRequiredVersion = version }; _syncObject = syncObject; } internal static void AddLockingConfigEntry(ConfigEntry<bool> entry) { GetSync().AddLockingConfigEntry<bool>(entry); } internal static void AddConfigEntry<T>(ConfigEntry<T> entry) { SyncedConfigEntry<T> syncedConfigEntry = GetSync().AddConfigEntry<T>(entry); syncedConfigEntry.SynchronizedConfig = true; } internal static void Reset() { _syncObject = null; } private static ConfigSync GetSync() { if (!(_syncObject is ConfigSync result)) { throw new InvalidOperationException("ServerSyncBridge used before initialization."); } return result; } } } namespace BalrondAchievementRewards.AchievementIntegration { internal static class AchievementApi { internal static AchievementScanResult Scan() { AchievementScanResult achievementScanResult = new AchievementScanResult(); try { Achievements instance = Achievements.m_instance; if ((Object)(object)instance == (Object)null) { achievementScanResult.ManagerAvailable = false; achievementScanResult.PlatformInitialized = Achievements.m_platformInitialized; achievementScanResult.Error = "Achievements.m_instance is not active yet."; return achievementScanResult; } achievementScanResult.ManagerAvailable = true; achievementScanResult.PlatformInitialized = Achievements.m_platformInitialized; if ((Object)(object)Player.m_localPlayer != (Object)null && (Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile() != null) { try { achievementScanResult.CanGetAchievements = Achievements.CanGetAchievements(false); } catch (Exception ex) { ModLog.Debug("ValheimAchievements.CanGetAchievements(false) was not ready: " + ex.Message); } } if (instance.m_achievementLists == null) { achievementScanResult.Error = "Achievements.m_achievementLists is null."; return achievementScanResult; } for (int i = 0; i < instance.m_achievementLists.Count; i++) { AchievementList val = instance.m_achievementLists[i]; if ((Object)(object)val == (Object)null || val.m_achievements == null) { continue; } for (int j = 0; j < val.m_achievements.Count; j++) { Achievement val2 = val.m_achievements[j]; if (!((Object)(object)val2 == (Object)null)) { AchievementSnapshot achievementSnapshot = ReadAchievement(val2); if (achievementSnapshot != null) { achievementScanResult.Achievements.Add(achievementSnapshot); } } } } if (achievementScanResult.Achievements.Count == 0) { achievementScanResult.Error = "Achievements manager is active, but no achievement definitions are populated yet."; } } catch (Exception ex2) { achievementScanResult.Error = ex2.GetType().Name + ": " + ex2.Message; ModLog.ErrorOnce("achievement_scan", "Direct achievement scan failed: " + ex2); } return achievementScanResult; } internal static string Localize(string textId) { if (string.IsNullOrEmpty(textId)) { return string.Empty; } try { if (Localization.instance != null) { string text = Localization.instance.Localize(textId); if (!string.IsNullOrEmpty(text)) { return text; } } } catch { } return textId; } private static AchievementSnapshot ReadAchievement(Achievement achievement) { string text = achievement.m_id ?? string.Empty; string text2 = achievement.m_name ?? string.Empty; string text3 = achievement.m_description ?? string.Empty; string text4 = string.Empty; try { text4 = Achievements.GetAchievementInternalPlatformId(achievement) ?? string.Empty; } catch { } string text5 = Localize(text2); if (string.IsNullOrWhiteSpace(text5)) { text5 = ((!string.IsNullOrWhiteSpace(text)) ? text : text4); } string stableKey = ((!string.IsNullOrWhiteSpace(text)) ? ("id:" + text.Trim()) : ((!string.IsNullOrWhiteSpace(text4)) ? ("platform:" + text4.Trim()) : ("name:" + text2.Trim()))); return new AchievementSnapshot { StableKey = stableKey, InternalId = text, PlatformId = text4, NameTextId = text2, DisplayName = text5, DescriptionTextId = text3, DisplayDescription = Localize(text3), Unlocked = achievement.m_unlocked }; } } internal static class AchievementWatcher { private static readonly Dictionary<string, bool> PreviousUnlocked = new Dictionary<string, bool>(StringComparer.Ordinal); private static bool _initialized; private static bool _hasSuccessfulScan; private static bool _dirty; private static float _nextScanTime; private static bool _initialDumpWritten; internal static AchievementScanResult LastScan { get; private set; } internal static void Initialize() { PreviousUnlocked.Clear(); LastScan = null; _initialized = true; _hasSuccessfulScan = false; _dirty = true; _nextScanTime = 0f; _initialDumpWritten = false; } internal static void Shutdown() { PreviousUnlocked.Clear(); LastScan = null; _initialized = false; _hasSuccessfulScan = false; _dirty = false; } internal static void MarkDirty(string reason) { if (_initialized) { _dirty = true; _nextScanTime = 0f; ModLog.Debug("Achievement rescan requested: " + reason); } } internal static void Tick(float now) { if (_initialized) { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsDedicated()) { _dirty = false; _nextScanTime = now + 60f; } else if ((Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)Game.instance == (Object)null || Game.instance.GetPlayerProfile() == null) { _dirty = true; _nextScanTime = now + 1f; } else if (_dirty || !(now < _nextScanTime)) { _dirty = false; ScanNow(now); } } } internal static void ScanImmediately(string reason) { MarkDirty(reason); if (!((Object)(object)Player.m_localPlayer == (Object)null) && !((Object)(object)Game.instance == (Object)null) && Game.instance.GetPlayerProfile() != null) { ScanNow(Time.unscaledTime); } } internal static string WriteDump() { AchievementScanResult achievementScanResult = LastScan; if (achievementScanResult == null || achievementScanResult.Achievements == null || achievementScanResult.Achievements.Count == 0) { achievementScanResult = AchievementApi.Scan(); } string text = Path.Combine(Paths.ConfigPath, "BalrondAchievementRewards"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, "achievement_dump.txt"); StringBuilder stringBuilder = new StringBuilder(8192); stringBuilder.AppendLine("BalrondAchievementRewards vanilla achievement dump"); stringBuilder.AppendLine("Generated: " + DateTime.UtcNow.ToString("u", CultureInfo.InvariantCulture)); stringBuilder.AppendLine("ManagerAvailable=" + (achievementScanResul