Decompiled source of PickupTabOverlay v0.4.0
PickupTabOverlay.dll
Decompiled a month 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.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using HarmonyLib; using Il2CppTMPro; using Kp0hyc.Labyrinthine.Shared; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(PickupTabOverlay), "Pickup Tab Overlay", "0.4.0", "kp0hyc", null)] [assembly: MelonGame("Valko Game Studios", "Labyrinthine")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] [assembly: AssemblyCompany("PickupTabOverlay")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("PickupTabOverlay")] [assembly: AssemblyTitle("PickupTabOverlay")] [assembly: AssemblyVersion("1.0.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } public class PickupTabOverlay : MelonMod { private readonly struct TmpOverlayModel { public readonly string Text; public readonly float Width; public readonly float Height; public readonly int StyleKey; public TmpOverlayModel(string text, float width, float height, int styleKey) { Text = text; Width = width; Height = height; StyleKey = styleKey; } } private readonly struct ObjectiveTextStyle { public static readonly ObjectiveTextStyle Fallback = new ObjectiveTextStyle("fallback", 30, 28, 24, null, null); public readonly string Source; public readonly int HeaderFontSize; public readonly int RowFontSize; public readonly int FooterFontSize; public readonly object Color; public readonly object MutedColor; public int StyleKey => (((HeaderFontSize * 397) ^ RowFontSize) * 397) ^ FooterFontSize; public ObjectiveTextStyle(string source, int headerFontSize, int rowFontSize, int footerFontSize, object color, object mutedColor) { Source = source; HeaderFontSize = headerFontSize; RowFontSize = rowFontSize; FooterFontSize = footerFontSize; Color = color; MutedColor = mutedColor; } public static ObjectiveTextStyle FromConfig(int headerFontSize, int rowFontSize, int footerFontSize, object color) { return new ObjectiveTextStyle("internal", headerFontSize, rowFontSize, footerFontSize, color, CopyColorWithAlpha(color, 0.82f)); } } private readonly struct CollectibleEntry { public readonly string Label; public readonly int Amount; public CollectibleEntry(string label, int amount) { Label = label; Amount = amount; } } private readonly struct UnlockEntry { public readonly string Category; public readonly string Name; public readonly int Id; public readonly string ItemClass; public readonly CosmeticUnlockState CosmeticState; public readonly int Count; public UnlockEntry(string category, string name, int id, string itemClass, CosmeticUnlockState cosmeticState, int count) { Category = category; Name = name; Id = id; ItemClass = itemClass; CosmeticState = cosmeticState; Count = count; } public UnlockEntry WithCount(int count) { return new UnlockEntry(Category, Name, Id, ItemClass, CosmeticState, count); } } private enum CosmeticUnlockState { None, New, Old } private readonly struct ShandadapandaMissingTarget { public readonly int GameItemId; public readonly int WebsiteItemId; public readonly string ItemName; public ShandadapandaMissingTarget(int gameItemId, int websiteItemId, string itemName) { GameItemId = gameItemId; WebsiteItemId = websiteItemId; ItemName = itemName; } } private readonly struct ShandadapandaMissingRow { public readonly string ItemName; public readonly List<string> Names; public ShandadapandaMissingRow(string itemName, List<string> names) { ItemName = itemName; Names = names ?? new List<string>(); } } private sealed class ShandadapandaMissingLookupState { public int GameItemId; public readonly int WebsiteItemId; public string ItemName; public List<string> MissingNames = new List<string>(); public bool Running; public bool Completed; public DateTime NextRetryUtc = DateTime.MinValue; public string Error; public ShandadapandaMissingLookupState(int gameItemId, int websiteItemId, string itemName) { GameItemId = gameItemId; WebsiteItemId = websiteItemId; ItemName = itemName; } } private readonly struct ShandadapandaOwnershipSnapshot { public readonly Dictionary<int, ShandadapandaDesiredItemState> States; public readonly int OwnedCount; public readonly string Source; public ShandadapandaOwnershipSnapshot(Dictionary<int, ShandadapandaDesiredItemState> states, int ownedCount, string source) { States = states; OwnedCount = ownedCount; Source = source; } } private readonly struct ShandadapandaDesiredItemState { public readonly bool Checked; public readonly int WebsiteId; public readonly string Name; public readonly string Category; public readonly string LocalKey; public ShandadapandaDesiredItemState(bool isChecked, int websiteId, string name, string category, string localKey) { Checked = isChecked; WebsiteId = websiteId; Name = name ?? string.Empty; Category = category ?? string.Empty; LocalKey = localKey ?? string.Empty; } } private readonly struct ShandadapandaSaveBackedItem { public readonly int WebsiteId; public readonly string Name; public readonly string Category; public readonly string SaveKey; public readonly int MazeFlag; public bool IsBlueprint => MazeFlag != 0; public ShandadapandaSaveBackedItem(int websiteId, string name, string category, string saveKey, int mazeFlag) { WebsiteId = websiteId; Name = name; Category = category; SaveKey = saveKey; MazeFlag = mazeFlag; } } private readonly struct ShandadapandaAuth { public readonly int AccountId; public readonly string ApiKey; public ShandadapandaAuth(int accountId, string apiKey) { AccountId = accountId; ApiKey = apiKey; } } private readonly struct ShandadapandaUserItem { public readonly int UserItemId; public readonly int WebsiteItemId; public readonly int GamesId; public readonly bool Checked; public readonly string Name; public ShandadapandaUserItem(int userItemId, int websiteItemId, int gamesId, bool isChecked, string name) { UserItemId = userItemId; WebsiteItemId = websiteItemId; GamesId = gamesId; Checked = isChecked; Name = name; } } private readonly struct ShandadaItemInfo { public readonly int WebsiteId; public readonly string Name; public readonly string ItemClass; public ShandadaItemInfo(int websiteId, string name, string itemClass) { WebsiteId = websiteId; Name = name; ItemClass = itemClass; } } private readonly struct ShandadaWebsiteItemInfo { public readonly int WebsiteId; public readonly int GameItemId; public readonly bool HasGameItemId; public readonly int StoreCustomizationItemId; public readonly bool HasStoreCustomizationItemId; public readonly string Name; public readonly string ItemType; public readonly string ItemClass; public bool IsBlueprint => string.Equals(ItemType, "Blueprints", StringComparison.OrdinalIgnoreCase); public ShandadaWebsiteItemInfo(int websiteId, int gameItemId, bool hasGameItemId, int storeCustomizationItemId, bool hasStoreCustomizationItemId, string name, string itemType, string itemClass) { WebsiteId = websiteId; GameItemId = gameItemId; HasGameItemId = hasGameItemId; StoreCustomizationItemId = storeCustomizationItemId; HasStoreCustomizationItemId = hasStoreCustomizationItemId; Name = name ?? string.Empty; ItemType = itemType ?? string.Empty; ItemClass = itemClass ?? string.Empty; } } private const string HarmonyId = "kp0hyc.pickuptaboverlay"; private const string PrefCategoryName = "PickupTabOverlay"; private const int MinimumReadableRowFontSize = 18; private const int MinimumReadableFooterFontSize = 16; private const int PanelWidth = 320; private const int HeaderFontSize = 30; private const int RowFontSize = 28; private const int FooterFontSize = 24; private const string NewCosmeticMarkerColor = "#42ff80"; private const string OldCosmeticMarkerColor = "#8a8a8a"; private const string DefaultShandadapandaApiBaseUrl = "https://api.shandadapanda.com"; private const string ShandadapandaSyncReadOnly = "ReadOnly"; private const string ShandadapandaSyncWritePickedCosmetics = "WritePickedCosmetics"; private const string ShandadapandaMissingNamesFileName = "PickupTabOverlayShandadapandaNames.txt"; private const int ShandadapandaMissingNamesReloadIntervalSeconds = 5; private const int ShandadapandaMissingLookupRetrySeconds = 120; private static readonly Dictionary<string, int> s_collectiblesFound = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<int> s_countedCollectibles = new HashSet<int>(); private static readonly List<UnlockEntry> s_unlockEntries = new List<UnlockEntry>(); private static readonly Dictionary<string, int> s_unlockEntryIndexes = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<int> s_countedUnlockPickups = new HashSet<int>(); private static readonly Dictionary<int, string> s_customizationNameCache = new Dictionary<int, string>(); private static readonly Dictionary<int, string> s_customizationClassCache = new Dictionary<int, string>(); private static readonly Dictionary<int, ShandadaItemInfo> s_shandadaItems = new Dictionary<int, ShandadaItemInfo>(); private static readonly Dictionary<int, ShandadaWebsiteItemInfo> s_shandadaWebsiteItems = new Dictionary<int, ShandadaWebsiteItemInfo>(); private static readonly Dictionary<int, int> s_shandadapandaWardrobeOnlyGameIdsByWebsiteId = new Dictionary<int, int> { { 275, 178 }, { 277, 177 }, { 279, 299 }, { 396, 362 } }; private static readonly ShandadapandaSaveBackedItem[] s_shandadapandaSaveBackedItems = new ShandadapandaSaveBackedItem[19] { new ShandadapandaSaveBackedItem(316, "Unkept Hedges", "Blueprint", "Maze_A", 1), new ShandadapandaSaveBackedItem(319, "Kept Hedges", "Blueprint", "Maze_B", 2), new ShandadapandaSaveBackedItem(322, "Forest", "Blueprint", "Maze_C", 4), new ShandadapandaSaveBackedItem(327, "Brick Maze", "Blueprint", "Maze_D", 8), new ShandadapandaSaveBackedItem(324, "Trenches", "Blueprint", "Maze_E", 16), new ShandadapandaSaveBackedItem(326, "Cornfield", "Blueprint", "Maze_F", 32), new ShandadapandaSaveBackedItem(321, "Manor", "Blueprint", "Maze_G", 64), new ShandadapandaSaveBackedItem(318, "Crypts", "Blueprint", "Maze_H", 128), new ShandadapandaSaveBackedItem(320, "Sewers", "Blueprint", "Maze_I", 256), new ShandadapandaSaveBackedItem(328, "Carnival", "Blueprint", "Maze_J", 512), new ShandadapandaSaveBackedItem(329, "Mines", "Blueprint", "Maze_K", 1024), new ShandadapandaSaveBackedItem(317, "Snowy Hedges", "Blueprint", "Maze_L", 2048), new ShandadapandaSaveBackedItem(330, "Jungle", "Blueprint", "Maze_M", 4096), new ShandadapandaSaveBackedItem(331, "Dead Forest", "Blueprint", "Maze_N", 8192), new ShandadapandaSaveBackedItem(332, "Fog City", "Blueprint", "Maze_O", 16384), new ShandadapandaSaveBackedItem(323, "Bamboo Forest", "Blueprint", "Maze_P", 32768), new ShandadapandaSaveBackedItem(325, "Backrooms", "Blueprint", "Maze_Backrooms", 65536), new ShandadapandaSaveBackedItem(333, "Swamp", "Blueprint", "Maze_Q", 131072), new ShandadapandaSaveBackedItem(334, "Castle", "Blueprint", "Maze_R", 262144) }; private static readonly object s_shandadapandaSyncLock = new object(); private static readonly object s_shandadapandaMissingLock = new object(); private static readonly List<int> s_currentMapCosmeticItemIds = new List<int>(); private static readonly Dictionary<int, ShandadapandaMissingLookupState> s_shandadapandaMissingByWebsiteId = new Dictionary<int, ShandadapandaMissingLookupState>(); private static List<string> s_shandadapandaWatchedNames = new List<string>(); private static int s_shandadapandaMissingGeneration; private static MelonPreferences_Entry<bool> s_debugLogging; private static MelonPreferences_Entry<bool> s_enableShandadapandaSync; private static MelonPreferences_Entry<bool> s_showShandadapandaMissingOwners; private static MelonPreferences_Entry<bool> s_showMissingOwnersBeforePickup; private static MelonPreferences_Entry<string> s_shandadapandaApiBaseUrl; private static MelonPreferences_Entry<string> s_shandadapandaApiKey; private static MelonPreferences_Entry<string> s_shandadapandaUsername; private static MelonPreferences_Entry<string> s_shandadapandaSyncMode; private static DateTime s_nextWarningUtc = DateTime.MinValue; private static DateTime s_nextTmpOverlayRetryUtc = DateTime.MinValue; private static DateTime s_nextTmpDiagnosticsUtc = DateTime.MinValue; private static string s_lastLoggedRendererSignature; private static string s_lastLoggedTmpDiagnosticsSignature; private static string s_lastLoggedTmpLayoutSignature; private static string s_lastLoggedDirectTmpFailure; private static bool s_tmpOverlayReady; private static bool s_tmpOverlayFailed; private static bool s_tmpOverlayVisible; private static bool s_loggedTmpOverlayReady; private static bool s_tmpOverlayUsesDirectUi; private static bool s_shandadaItemsLoaded; private static bool s_shandadapandaSyncRunning; private static int s_shandadapandaLobbyGeneration; private static int s_shandadapandaSyncedLobbyGeneration; private static bool s_shandadapandaInLobbyVisit; private static DateTime s_nextShandadapandaNamesReloadUtc = DateTime.MinValue; private static DateTime s_shandadapandaNamesFileLastWriteUtc = DateTime.MinValue; private static DateTime s_currentMapCosmeticObservedUtc = DateTime.MinValue; private static string s_lastLoggedMissingOwnerSignature; private static readonly Dictionary<string, string> s_lastLoggedBlueprintSourceSignaturesBySource = new Dictionary<string, string>(StringComparer.Ordinal); private static string s_tmpLastText; private static int s_tmpLastStyleKey; private static float s_tmpLastWidth; private static float s_tmpLastHeight; private static Type s_rectType; private static Type s_colorType; private static Type s_vector2Type; private static Type s_guiType; private static Type s_screenType; private static Type s_inputType; private static Type s_keyCodeType; private static Type s_labyrinthineInputManagerType; private static Type s_inputButtonType; private static Type s_objectType; private static Type s_rndCollectibleType; private static Type s_customizationPickupType; private static Type s_customCaseItemUnlockType; private static Type s_customizationManagerType; private static Type s_customizationUiControllerType; private static Type s_equipmentSaveType; private static Type s_localisationManagerType; private static Type s_tmpTextType; private static Type s_tmpFontAssetType; private static Type s_canvasType; private static Type s_componentType; private static Type s_gameObjectType; private static Type s_transformType; private static Type s_rectTransformType; private static Type s_resourcesType; private static Type s_textMeshProUguiType; private static Type s_renderModeType; private static Type s_textAlignmentOptionsType; private static Type s_textOverflowModesType; private static ConstructorInfo s_rectCtor; private static ConstructorInfo s_colorCtor; private static ConstructorInfo s_vector2Ctor; private static MethodInfo s_guiLabel; private static MethodInfo s_inputGetKey; private static MethodInfo s_gameGetButton; private static MethodInfo s_findObjectsOfTypeMethod; private static MethodInfo s_componentGetComponentInParentMethod; private static MethodInfo s_gameObjectGetComponentMethod; private static MethodInfo s_gameObjectSetActiveMethod; private static MethodInfo s_objectDestroyMethod; private static MethodInfo s_objectDontDestroyOnLoadMethod; private static MethodInfo s_resourcesFindObjectsOfTypeAllMethod; private static PropertyInfo s_guiColorProperty; private static PropertyInfo s_screenWidthProperty; private static PropertyInfo s_screenHeightProperty; private static PropertyInfo s_guiSkinProperty; private static PropertyInfo s_guiSkinLabelProperty; private static PropertyInfo s_styleFontSizeProperty; private static PropertyInfo s_styleNormalProperty; private static PropertyInfo s_styleStateTextColorProperty; private static object s_keyCodeTab; private static object s_showTabMenuButton; private static object s_tmpOverlayRoot; private static object s_tmpOverlayTextObject; private static object s_tmpOverlayText; private static object s_tmpOverlayRectTransform; private static object s_larkeSansFont; private static bool s_reflectionReady; private static IEnumerable<object> EnumerateItems(object list) { if (list == null) { yield break; } if (list is Array array) { foreach (object item in array) { yield return item; } yield break; } if (list is IEnumerable enumerable) { foreach (object item2 in enumerable) { yield return item2; } yield break; } if (!ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(list, "Count"), out var count)) { ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(list, "Length"), out count); } if (count <= 0) { yield break; } MethodInfo getItem = list.GetType().GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (getItem == null) { yield break; } for (int i = 0; i < count; i++) { object obj = null; try { obj = getItem.Invoke(list, new object[1] { i }); } catch { } if (obj != null) { yield return obj; } } } private static bool TryClassifyCollectible(object collectible, out string label, out int amount) { label = null; if (!ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(collectible, "amount"), out amount) || amount <= 0) { amount = 1; } object fieldOrProp = Il2CppReflection.GetFieldOrProp(collectible, "collectibleType"); string text = ((fieldOrProp == null) ? string.Empty : fieldOrProp.ToString()); string collectibleObjectName = GetCollectibleObjectName(collectible); string text2 = (text + " " + collectibleObjectName + " " + (Il2CppReflection.GetFieldOrProp(collectible, "pickupMessage") ?? string.Empty)).ToLowerInvariant(); if (text2.Contains("ticket")) { label = "Tickets"; return true; } if (text.IndexOf("Reroll", StringComparison.OrdinalIgnoreCase) >= 0 || text2.Contains("reroll")) { label = "Reroll Coins"; return true; } return false; } private static string GetCollectibleObjectName(object collectible) { string text = Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(collectible, "gameObject") ?? collectible, "name") as string; if (!string.IsNullOrWhiteSpace(text)) { return text; } return string.Empty; } private static void AddUnlockEntry(string category, string name, int id, string itemClass = null, CosmeticUnlockState cosmeticState = CosmeticUnlockState.None) { if (string.IsNullOrWhiteSpace(category)) { category = "Unlock"; } if (string.IsNullOrWhiteSpace(name)) { name = "Unknown"; } name = CleanUnityName(name); itemClass = FormatItemClass(itemClass); if (!string.Equals(category, "Cosmetic", StringComparison.OrdinalIgnoreCase)) { cosmeticState = CosmeticUnlockState.None; } string key = category + "|" + id + "|" + itemClass + "|" + name + "|" + cosmeticState; if (s_unlockEntryIndexes.TryGetValue(key, out var value) && value >= 0 && value < s_unlockEntries.Count) { s_unlockEntries[value] = s_unlockEntries[value].WithCount(s_unlockEntries[value].Count + 1); return; } s_unlockEntryIndexes[key] = s_unlockEntries.Count; s_unlockEntries.Add(new UnlockEntry(category, name, id, itemClass, cosmeticState, 1)); } private static string ResolveCustomizationName(int itemId) { if (s_customizationNameCache.TryGetValue(itemId, out var value)) { return value; } string text = null; if (TryGetShandadaItemInfo(itemId, out var info) && !string.IsNullOrWhiteSpace(info.Name)) { text = info.Name; } object obj = TryGetCustomizationItem(itemId); if (obj != null && string.IsNullOrWhiteSpace(text)) { text = Il2CppReflection.GetFieldOrProp(obj, "Name") as string; if (string.IsNullOrWhiteSpace(text)) { text = GetLocalisation(Il2CppReflection.GetFieldOrProp(obj, "LocalisationKey") as string, null); } } if (!string.IsNullOrWhiteSpace(text)) { s_customizationNameCache[itemId] = text; return text; } return null; } private static object TryGetCustomizationItem(int itemId) { try { object customizationCollection = GetCustomizationCollection(); if (customizationCollection == null || itemId < 0 || itemId > 65535) { return null; } MethodInfo methodInfo = customizationCollection.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "TryGetItem" && m.GetParameters().Length == 2); if (methodInfo == null) { return null; } object[] array = new object[2] { (ushort)itemId, null }; if (ValueConversion.TryConvertBool(methodInfo.Invoke(customizationCollection, array), out var result) && result) { return array[1]; } } catch { } return null; } private static string ResolveCustomizationClass(int itemId) { if (s_customizationClassCache.TryGetValue(itemId, out var value)) { return value; } string text = null; if (TryGetShandadaItemInfo(itemId, out var info)) { text = info.ItemClass; } if (string.IsNullOrWhiteSpace(text)) { object obj = TryGetCustomizationItem(itemId); text = (Il2CppReflection.GetFieldOrProp(obj, "ItemRarity") ?? Il2CppReflection.GetFieldOrProp(obj, "itemRarity"))?.ToString(); } text = FormatItemClass(text); if (!string.IsNullOrWhiteSpace(text)) { s_customizationClassCache[itemId] = text; } return text; } private static bool TryGetShandadaItemInfo(int itemId, out ShandadaItemInfo info) { EnsureShandadaItemsLoaded(); return s_shandadaItems.TryGetValue(itemId, out info); } private static void EnsureShandadaItemsLoaded() { if (s_shandadaItemsLoaded) { return; } s_shandadaItemsLoaded = true; try { Assembly assembly = typeof(PickupTabOverlay).Assembly; string text = assembly.GetManifestResourceNames().FirstOrDefault((string name) => name.EndsWith("shandada_game_item_id_map.json", StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrWhiteSpace(text)) { LogDebug("Embedded Shandadapanda item map was not found."); return; } using Stream stream = assembly.GetManifestResourceStream(text); if (stream == null) { return; } using JsonDocument jsonDocument = JsonDocument.Parse(stream); if (!jsonDocument.RootElement.TryGetProperty("gameToWebsite", out var value) || value.ValueKind != JsonValueKind.Array) { return; } foreach (JsonElement item in value.EnumerateArray()) { if (TryGetJsonInt(item, "gameItemId", out var value2) && value2 >= 0) { TryGetJsonInt(item, "websiteId", out var value3); string jsonString = GetJsonString(item, "websiteName"); string text2 = GetJsonString(item, "class") ?? GetJsonString(item, "websiteMapClass") ?? GetJsonString(item, "websiteEvent"); if (value3 > 0 || !string.IsNullOrWhiteSpace(jsonString) || !string.IsNullOrWhiteSpace(text2)) { s_shandadaItems[value2] = new ShandadaItemInfo(value3, jsonString, text2); } } } if (jsonDocument.RootElement.TryGetProperty("websiteToGame", out var value4) && value4.ValueKind == JsonValueKind.Array) { LoadShandadaWebsiteItems(value4); } LogDebug("Loaded " + s_shandadaItems.Count + " Shandadapanda game item names and " + s_shandadaWebsiteItems.Count + " website rows."); } catch (Exception ex) { LogWarning("Shandadapanda item map load failed: " + ex.GetType().Name + " " + ex.Message); } } private static void LoadShandadaWebsiteItems(JsonElement rows) { foreach (JsonElement item in rows.EnumerateArray()) { if (TryGetJsonInt(item, "websiteId", out var value) && value > 0) { int value2; bool flag = TryGetJsonInt(item, "gameItemId", out value2) && value2 >= 0; int value3; bool flag2 = (TryGetJsonInt(item, "storeCustomizationItemId", out value3) || TryGetJsonInt(item, "storeCustomizationID", out value3) || TryGetJsonInt(item, "customizationID", out value3)) && value3 >= 0; string text = GetJsonString(item, "name") ?? GetJsonString(item, "websiteName"); string text2 = GetJsonString(item, "type") ?? GetJsonString(item, "websiteType"); string text3 = GetJsonString(item, "class") ?? GetJsonString(item, "mapClass") ?? GetJsonString(item, "websiteMapClass") ?? GetJsonString(item, "event") ?? GetJsonString(item, "websiteEvent"); if (!string.IsNullOrWhiteSpace(text) || !string.IsNullOrWhiteSpace(text2) || !string.IsNullOrWhiteSpace(text3)) { s_shandadaWebsiteItems[value] = new ShandadaWebsiteItemInfo(value, flag ? value2 : (-1), flag, flag2 ? value3 : (-1), flag2, text, text2, text3); } } } } private static bool TryGetJsonInt(JsonElement element, string name, out int value) { value = 0; if (!element.TryGetProperty(name, out var value2)) { return false; } if (value2.ValueKind == JsonValueKind.Number) { return value2.TryGetInt32(out value); } if (value2.ValueKind == JsonValueKind.String) { return int.TryParse(value2.GetString(), out value); } return false; } private static string GetJsonString(JsonElement element, string name) { if (!element.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String) { return null; } return value.GetString(); } private static bool TryGetJsonBool(JsonElement element, string name, out bool value) { value = false; if (!element.TryGetProperty(name, out var value2)) { return false; } if (value2.ValueKind == JsonValueKind.True) { value = true; return true; } if (value2.ValueKind == JsonValueKind.False) { value = false; return true; } if (value2.ValueKind == JsonValueKind.Number && value2.TryGetInt32(out var value3)) { value = value3 != 0; return true; } if (value2.ValueKind == JsonValueKind.String) { return bool.TryParse(value2.GetString(), out value); } return false; } private static string FormatItemClass(string itemClass) { if (string.IsNullOrWhiteSpace(itemClass)) { return null; } itemClass = itemClass.Trim(); int num = itemClass.LastIndexOf('.'); if (num >= 0 && num + 1 < itemClass.Length) { itemClass = itemClass.Substring(num + 1); } itemClass = CleanUnityName(itemClass).Trim(); if (!string.IsNullOrWhiteSpace(itemClass)) { return itemClass.ToLowerInvariant(); } return null; } private static bool IsStoreBoughtItemClass(string itemClass) { if (!string.IsNullOrWhiteSpace(itemClass)) { return itemClass.IndexOf("Store Bought", StringComparison.OrdinalIgnoreCase) >= 0; } return false; } private static object GetCustomizationCollection() { object customizationManager = GetCustomizationManager(); return Il2CppReflection.GetFieldOrProp(customizationManager, "Collection") ?? Il2CppReflection.GetFieldOrProp(customizationManager, "collection"); } private static object GetCustomizationManager() { Type customizationManagerType = GetCustomizationManagerType(); return Il2CppReflection.GetStaticFieldOrProp(customizationManagerType, "Instance") ?? Il2CppReflection.FindFirstObjectOfType(customizationManagerType); } private static bool IsCustomizationItemAlreadyUnlocked(int itemId) { if (itemId < 0 || itemId > 65535) { return false; } object customizationManager = GetCustomizationManager(); HashSet<int> hashSet = new HashSet<int>(); if (TryCollectUnlockedItemIds(Il2CppReflection.GetFieldOrProp(customizationManager, "UnlockedItems"), hashSet, out var _)) { return hashSet.Contains(itemId); } LogDebug("Could not determine wardrobe unlock state for itemID=" + itemId + "; treating pickup as new."); return false; } private static bool IsLobbyScene(string sceneName) { if (!string.Equals(sceneName, "Lobby", StringComparison.OrdinalIgnoreCase) && !string.Equals(sceneName, "Lobby_PC", StringComparison.OrdinalIgnoreCase)) { return string.Equals(sceneName, "MainMenu", StringComparison.OrdinalIgnoreCase); } return true; } private static bool IsLobbyVisitScene(string sceneName) { if (!string.Equals(sceneName, "Lobby", StringComparison.OrdinalIgnoreCase)) { return string.Equals(sceneName, "Lobby_PC", StringComparison.OrdinalIgnoreCase); } return true; } private static void LogOverlayRenderer(string renderer, string details) { string b = renderer + "|" + details; if (!string.Equals(s_lastLoggedRendererSignature, b, StringComparison.Ordinal)) { s_lastLoggedRendererSignature = b; LogDebug("Pickup overlay renderer=" + renderer + " " + details); } } private static string GetFallbackRendererReason() { if (s_tmpOverlayFailed) { return "reason=TextMeshPro unavailable/retrying"; } if (!s_tmpOverlayReady) { return "reason=TextMeshPro not ready"; } return "reason=TextMeshPro ready"; } private static string DescribeTextStyleForLog(ObjectiveTextStyle style) { string text = (string.IsNullOrWhiteSpace(style.Source) ? "unknown" : style.Source); return "styleSource=" + text + " headerFont=" + style.HeaderFontSize + " rowFont=" + style.RowFontSize + " footerFont=" + style.FooterFontSize; } private static string DescribeRectTransform(object rectTransform) { if (rectTransform == null) { return "none"; } try { object fieldOrProp = Il2CppReflection.GetFieldOrProp(rectTransform, "rect"); return "name='" + TruncateForLog(GetUnityObjectName(rectTransform), 50) + "' anchorMin=" + DescribeVector(Il2CppReflection.GetFieldOrProp(rectTransform, "anchorMin")) + " anchorMax=" + DescribeVector(Il2CppReflection.GetFieldOrProp(rectTransform, "anchorMax")) + " pivot=" + DescribeVector(Il2CppReflection.GetFieldOrProp(rectTransform, "pivot")) + " anchoredPosition=" + DescribeVector(Il2CppReflection.GetFieldOrProp(rectTransform, "anchoredPosition")) + " sizeDelta=" + DescribeVector(Il2CppReflection.GetFieldOrProp(rectTransform, "sizeDelta")) + " rect=" + DescribeRect(fieldOrProp); } catch { return rectTransform.GetType().Name; } } private static string DescribeRect(object rect) { if (rect == null) { return "none"; } float result; string obj = (ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(rect, "width"), out result) ? result.ToString("0.#") : "?"); float result2; string text = (ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(rect, "height"), out result2) ? result2.ToString("0.#") : "?"); return obj + "x" + text; } private static string DescribeVector(object vector) { if (vector == null) { return "none"; } float result; bool num = ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(vector, "x"), out result); float result2; bool flag = ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(vector, "y"), out result2); float result3; bool flag2 = ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(vector, "z"), out result3); if (!num && !flag && !flag2) { return vector.ToString(); } if (!flag2) { return "(" + result.ToString("0.###") + "," + result2.ToString("0.###") + ")"; } return "(" + result.ToString("0.###") + "," + result2.ToString("0.###") + "," + result3.ToString("0.###") + ")"; } private static string GetUnityObjectName(object obj) { if (obj == null) { return null; } try { string text = Il2CppReflection.GetFieldOrProp(obj, "name") as string; if (!string.IsNullOrWhiteSpace(text)) { return text.Trim(); } object fieldOrProp = Il2CppReflection.GetFieldOrProp(obj, "gameObject"); if (fieldOrProp != null && fieldOrProp != obj) { text = Il2CppReflection.GetFieldOrProp(fieldOrProp, "name") as string; if (!string.IsNullOrWhiteSpace(text)) { return text.Trim(); } } } catch { } return null; } private static string TruncateForLog(string text, int maxLength) { if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } string text2 = text.Replace("\r", " ").Replace("\n", " ").Trim(); if (text2.Length <= maxLength) { return text2; } return text2.Substring(0, Math.Max(0, maxLength - 3)) + "..."; } private static string TypeNameForLog(Type type) { object obj; if (!(type == null)) { obj = type.FullName; if (obj == null) { return type.Name; } } else { obj = "null"; } return (string)obj; } private static void LogWarning(string message) { if (!(DateTime.UtcNow < s_nextWarningUtc)) { s_nextWarningUtc = DateTime.UtcNow.AddSeconds(10.0); MelonLogger.Warning(message); } } private static void LogDebug(string message) { if (IsDebugLoggingEnabled()) { MelonLogger.Msg("[Debug] " + message); } } private static bool IsDebugLoggingEnabled() { if (s_debugLogging != null) { return s_debugLogging.Value; } return false; } private static void TryPatchCollectiblePickup() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown try { MethodInfo methodInfo = GetRndCollectibleType()?.GetMethod("Pickup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { MelonLogger.Warning("Could not find RndCollectible.Pickup; random collectibles will not be tracked."); return; } new Harmony("kp0hyc.pickuptaboverlay").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(PickupTabOverlay).GetMethod("RndCollectiblePickupPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); LogDebug("Patched RndCollectible.Pickup for collectible tracking."); } catch (Exception ex) { MelonLogger.Warning("Could not patch RndCollectible.Pickup: " + ex.GetType().Name + " " + ex.Message); } } private static void TryPatchCustomizationPickup() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown try { MethodInfo methodInfo = GetCustomizationPickupType()?.GetMethod("Pickup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { MelonLogger.Warning("Could not find CustomizationPickup.Pickup; cosmetic pickups will not be tracked."); return; } new Harmony("kp0hyc.pickuptaboverlay").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(PickupTabOverlay).GetMethod("CustomizationPickupPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); LogDebug("Patched CustomizationPickup.Pickup for cosmetic tracking."); } catch (Exception ex) { MelonLogger.Warning("Could not patch CustomizationPickup.Pickup: " + ex.GetType().Name + " " + ex.Message); } } private static void TryPatchCustomizationPickupStart() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown try { MethodInfo methodInfo = GetCustomizationPickupType()?.GetMethod("Start", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { MelonLogger.Warning("Could not find CustomizationPickup.Start; map cosmetic missing-owner lookup will start after pickup only."); return; } new Harmony("kp0hyc.pickuptaboverlay").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(PickupTabOverlay).GetMethod("CustomizationPickupStartPostfix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); LogDebug("Patched CustomizationPickup.Start for spawned cosmetic tracking."); } catch (Exception ex) { MelonLogger.Warning("Could not patch CustomizationPickup.Start: " + ex.GetType().Name + " " + ex.Message); } } private static void TryPatchCustomCaseItemUnlock() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown try { MethodInfo methodInfo = GetCustomCaseItemUnlockType()?.GetMethod("Pickup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { MelonLogger.Warning("Could not find CustomCaseItemUnlock.Pickup; blueprint/stamp pickups will not be tracked."); return; } new Harmony("kp0hyc.pickuptaboverlay").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(PickupTabOverlay).GetMethod("CustomCaseItemUnlockPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); LogDebug("Patched CustomCaseItemUnlock.Pickup for blueprint/stamp tracking."); } catch (Exception ex) { MelonLogger.Warning("Could not patch CustomCaseItemUnlock.Pickup: " + ex.GetType().Name + " " + ex.Message); } } private static void TryPatchCustomizationUiUpdateItems() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown try { MethodInfo methodInfo = GetCustomizationUiControllerType()?.GetMethod("UpdateItemsUI", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { MelonLogger.Warning("Could not find CustomizationUIController.UpdateItemsUI; Shandadapanda sync will only refresh from init/scene triggers."); return; } new Harmony("kp0hyc.pickuptaboverlay").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(PickupTabOverlay).GetMethod("CustomizationUiUpdateItemsPostfix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); LogDebug("Patched CustomizationUIController.UpdateItemsUI for wardrobe ownership refresh."); } catch (Exception ex) { MelonLogger.Warning("Could not patch CustomizationUIController.UpdateItemsUI: " + ex.GetType().Name + " " + ex.Message); } } private static void RndCollectiblePickupPrefix(object __instance) { try { if (__instance == null) { return; } int hashCode = RuntimeHelpers.GetHashCode(__instance); if (!s_countedCollectibles.Contains(hashCode) && TryClassifyCollectible(__instance, out var label, out var amount)) { s_countedCollectibles.Add(hashCode); if (!s_collectiblesFound.TryGetValue(label, out var value)) { value = 0; } s_collectiblesFound[label] = value + amount; } } catch (Exception ex) { LogWarning("collectible pickup tracking failed: " + ex.GetType().Name + " " + ex.Message); } } private static void CustomizationUiUpdateItemsPostfix() { LogShandadapandaOwnershipReadinessProbe("CustomizationUIController.UpdateItemsUI"); TryStartShandadapandaSyncForCurrentLobby("CustomizationUIController.UpdateItemsUI"); } private static void CustomizationPickupPrefix(object __instance) { try { if (__instance == null) { return; } int hashCode = RuntimeHelpers.GetHashCode(__instance); if (!s_countedUnlockPickups.Contains(hashCode) && ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(__instance, "ItemID") ?? Il2CppReflection.GetFieldOrProp(__instance, "itemID"), out var result)) { if (IsMapCustomizationPickupCandidate(__instance)) { RememberCurrentMapCosmeticItemId(result, "pickup", queueMissingOwnerLookup: true); } s_countedUnlockPickups.Add(hashCode); bool flag = IsCustomizationItemAlreadyUnlocked(result); CosmeticUnlockState cosmeticState = ((!flag) ? CosmeticUnlockState.New : CosmeticUnlockState.Old); string text = ResolveCustomizationName(result); if (string.IsNullOrWhiteSpace(text)) { text = CleanPickupObjectName(__instance, "Cosmetic"); } string text2 = ResolveCustomizationClass(result); AddUnlockEntry("Cosmetic", text, result, text2, cosmeticState); LogDebug("Cosmetic pickup itemID=" + result + " alreadyUnlocked=" + flag + " status=" + cosmeticState.ToString() + " class=" + (text2 ?? "{unknown}") + " name=" + text); } } catch (Exception ex) { LogWarning("cosmetic pickup tracking failed: " + ex.GetType().Name + " " + ex.Message); } } private static void CustomizationPickupStartPostfix(object __instance) { try { if (__instance != null && IsMapCustomizationPickupCandidate(__instance) && TryGetCustomizationPickupItemId(__instance, out var itemId)) { RememberCurrentMapCosmeticItemId(itemId, "spawn-start", s_showMissingOwnersBeforePickup != null && s_showMissingOwnersBeforePickup.Value); } } catch (Exception ex) { LogWarning("spawned cosmetic tracking failed: " + ex.GetType().Name + " " + ex.Message); } } private static void CustomCaseItemUnlockPrefix(object __instance) { try { if (__instance == null) { return; } int hashCode = RuntimeHelpers.GetHashCode(__instance); if (!s_countedUnlockPickups.Contains(hashCode)) { bool flag = false; bool flag2 = false; if (ValueConversion.TryConvertBool(Il2CppReflection.GetFieldOrProp(__instance, "unlockMazeType"), out var result) && result) { object fieldOrProp = Il2CppReflection.GetFieldOrProp(__instance, "mazeType"); AddUnlockEntry("Map Blueprint", "Picked up", -1); LogDebug("Map blueprint pickup mazeType=" + FormatEnumValue(fieldOrProp, "Map")); flag = true; flag2 = true; } if (ValueConversion.TryConvertBool(Il2CppReflection.GetFieldOrProp(__instance, "unlockMonsterType"), out var result2) && result2) { object fieldOrProp2 = Il2CppReflection.GetFieldOrProp(__instance, "monsterType"); LogDebug("Unexpected monster blueprint pickup monsterType=" + FormatEnumValue(fieldOrProp2, "Monster")); flag = true; } if (ValueConversion.TryConvertBool(Il2CppReflection.GetFieldOrProp(__instance, "unlockContractType"), out var result3) && result3) { object fieldOrProp3 = Il2CppReflection.GetFieldOrProp(__instance, "contractType"); AddUnlockEntry("Stamps", "Collected", -1); LogDebug("Stamp pickup contractType=" + FormatEnumValue(fieldOrProp3, "Contract")); flag = true; flag2 = true; } if (!flag) { AddUnlockEntry("Blueprint", CleanPickupObjectName(__instance, "Custom Case Unlock"), -1); flag2 = true; LogDebug("Unknown custom-case unlock pickup name=" + CleanPickupObjectName(__instance, "Custom Case Unlock")); } else if (!flag2) { LogDebug("Custom-case unlock pickup had no visible overlay row."); } s_countedUnlockPickups.Add(hashCode); } } catch (Exception ex) { LogWarning("blueprint/stamp pickup tracking failed: " + ex.GetType().Name + " " + ex.Message); } } private static bool TryGetItemId(object item, out int id) { if (ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(item, "SerializedID"), out id)) { return true; } return ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(item, "ID"), out id); } private static string CleanUnityName(string name) { if (string.IsNullOrWhiteSpace(name)) { return name; } string text = name.Trim(); if (text.EndsWith("(Clone)", StringComparison.OrdinalIgnoreCase)) { text = text.Substring(0, text.Length - 7).Trim(); } if (text.StartsWith("SO_", StringComparison.OrdinalIgnoreCase)) { text = text.Substring(3); } return text.Replace('_', ' '); } private static bool IsTabMenuPressed() { if (IsTabMenuObjectActive()) { return true; } try { if (s_gameGetButton != null && s_showTabMenuButton != null && ValueConversion.TryConvertBool(s_gameGetButton.Invoke(null, new object[1] { s_showTabMenuButton }), out var result)) { return result; } } catch { } try { if (s_inputGetKey != null && s_keyCodeTab != null && ValueConversion.TryConvertBool(s_inputGetKey.Invoke(null, new object[1] { s_keyCodeTab }), out var result2)) { return result2; } } catch { } return false; } private static bool IsTabMenuObjectActive() { try { if (IsActiveInHierarchy(Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetStaticFieldOrProp(Il2CppReflection.FindType("UI_Controller"), "instance"), "tabMenu"))) { return true; } GameObject val = GameObject.Find("Global/Global Canvas/Player_UI/Tab Menu"); return (Object)(object)val != (Object)null && val.activeInHierarchy; } catch { return false; } } private static bool IsActiveInHierarchy(object obj) { if (obj == null) { return false; } bool result; return ValueConversion.TryConvertBool(Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(obj, "gameObject") ?? obj, "activeInHierarchy"), out result) && result; } private static string GetObjectSceneName(object obj) { if (obj == null) { return null; } try { return Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(obj, "gameObject") ?? obj, "scene") ?? Il2CppReflection.GetFieldOrProp(obj, "scene"), "name") as string; } catch { return null; } } private static string GetLocalisation(string key, string defaultValue) { if (string.IsNullOrWhiteSpace(key)) { return defaultValue; } try { string text = (GetLocalisationManagerType()?.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "GetLocalisation" && m.GetParameters().Length == 2))?.Invoke(null, new object[2] { key, defaultValue }) as string; return string.IsNullOrWhiteSpace(text) ? defaultValue : text; } catch { return defaultValue; } } private static string FormatEnumValue(object value, string fallback) { if (value == null) { return fallback; } string text = value.ToString(); if (string.IsNullOrWhiteSpace(text)) { return fallback; } return CleanUnityName(text); } private static string CleanPickupObjectName(object pickup, string fallback) { string text = Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(pickup, "gameObject") ?? pickup, "name") as string; if (string.IsNullOrWhiteSpace(text)) { return fallback; } string text2 = CleanUnityName(text); if (text2.StartsWith("CPickup - ", StringComparison.OrdinalIgnoreCase)) { text2 = text2.Substring("CPickup - ".Length).Trim(); } if (!string.IsNullOrWhiteSpace(text2)) { return text2; } return fallback; } public override void OnUpdate() { try { UpdateTmpOverlay(); } catch (Exception ex) { ResetTmpOverlayState(failed: false); LogWarning("TextMeshPro pickup overlay update failed: " + ex.GetType().Name + " " + ex.Message); } } public override void OnGUI() { if (!EnsureReflection() || !IsTabMenuPressed() || s_tmpOverlayReady) { return; } try { DrawPickupPanel(); } catch (Exception ex) { LogWarning("pickup overlay draw failed: " + ex.GetType().Name + " " + ex.Message); } } private static void DrawPickupPanel() { ObjectiveTextStyle textStyle = ResolveTextStyle(); LogOverlayRenderer("IMGUI fallback", GetFallbackRendererReason() + " " + DescribeTextStyleForLog(textStyle)); List<CollectibleEntry> collectibleRows = GetCollectibleRows(); List<UnlockEntry> unlockRows = GetUnlockRows(); List<ShandadapandaMissingRow> shandadapandaMissingRows = GetShandadapandaMissingRows(); float width = ResolveOverlayWidth(unlockRows, shandadapandaMissingRows); float singleRowHeight = GetSingleRowHeight(textStyle.RowFontSize); float num = collectibleRows.Sum((CollectibleEntry row) => EstimateDisplayRowHeight(row.Label + ": " + row.Amount, textStyle.RowFontSize, width)); float num2 = unlockRows.Sum((UnlockEntry row) => EstimateDisplayRowHeight(FormatUnlockEntry(row), textStyle.RowFontSize, width)); float num3 = shandadapandaMissingRows.Sum((ShandadapandaMissingRow row) => EstimateDisplayRowHeight(FormatShandadapandaMissingRow(row), textStyle.RowFontSize, width)); bool flag = HasAnyDisplayedContent(shandadapandaMissingRows); float num4 = ((!flag) ? singleRowHeight : 0f); float num5 = num + num2 + num3 + num4 + 8f; float num6 = 34f; float x = Math.Max(12f, (float)ScreenWidth() - width - num6); float num7 = Math.Max(12f, (float)ScreenHeight() - num5 - num6); foreach (CollectibleEntry item in collectibleRows) { string text = item.Label + ": " + item.Amount; float num8 = EstimateDisplayRowHeight(text, textStyle.RowFontSize, width); LabelShadowed(x, num7, width, num8, text, textStyle.RowFontSize, textStyle.Color); num7 += num8; } foreach (UnlockEntry item2 in unlockRows) { string text2 = FormatUnlockEntry(item2); float num9 = EstimateDisplayRowHeight(text2, textStyle.RowFontSize, width); LabelShadowed(x, num7, width, num9, text2, textStyle.RowFontSize, textStyle.Color); num7 += num9; } if (!flag) { LabelShadowed(x, num7, width, singleRowHeight, "No pickups", textStyle.RowFontSize, textStyle.MutedColor); return; } foreach (ShandadapandaMissingRow item3 in shandadapandaMissingRows) { string text3 = FormatShandadapandaMissingRow(item3); float num10 = EstimateDisplayRowHeight(text3, textStyle.RowFontSize, width); LabelShadowed(x, num7, width, num10, text3, textStyle.RowFontSize, textStyle.Color); num7 += num10; } } public override void OnInitializeMelon() { MelonPreferences_Category obj = MelonPreferences.CreateCategory("PickupTabOverlay"); s_debugLogging = obj.CreateEntry<bool>("VerboseLogs", false, "Log pickup overlay diagnostics for troubleshooting.", (string)null, false, false, (ValueValidator)null, (string)null); s_enableShandadapandaSync = obj.CreateEntry<bool>("EnableShandadapandaSync", false, "Sync local cosmetic ownership with shandadapanda.com.", (string)null, false, false, (ValueValidator)null, (string)null); s_showShandadapandaMissingOwners = obj.CreateEntry<bool>("ShowShandadapandaMissingOwners", true, "Show watched Shandadapanda nicknames that have not checked the current map cosmetic.", (string)null, false, false, (ValueValidator)null, (string)null); s_showMissingOwnersBeforePickup = obj.CreateEntry<bool>("ShowMissingOwnersBeforePickup", false, "Show missing-owner rows when the map cosmetic spawns instead of waiting until pickup.", (string)null, false, false, (ValueValidator)null, (string)null); s_shandadapandaApiBaseUrl = obj.CreateEntry<string>("ShandadapandaApiBaseUrl", "https://api.shandadapanda.com", "Shandadapanda API base URL.", (string)null, false, false, (ValueValidator)null, (string)null); s_shandadapandaApiKey = obj.CreateEntry<string>("ShandadapandaApiKey", string.Empty, "Shandadapanda API key.", (string)null, false, false, (ValueValidator)null, (string)null); s_shandadapandaUsername = obj.CreateEntry<string>("ShandadapandaUsername", string.Empty, "Shandadapanda username. Required for ownership sync account lookup.", (string)null, false, false, (ValueValidator)null, (string)null); s_shandadapandaSyncMode = obj.CreateEntry<string>("SyncMode", "ReadOnly", "ReadOnly or WritePickedCosmetics.", (string)null, false, false, (ValueValidator)null, (string)null); ReloadShandadapandaWatchedNames(force: true); TryPatchCollectiblePickup(); TryPatchCustomizationPickup(); TryPatchCustomizationPickupStart(); TryPatchCustomCaseItemUnlock(); TryPatchCustomizationUiUpdateItems(); LogDebug("PickupTabOverlay initialized."); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { DestroyTmpOverlay(); s_collectiblesFound.Clear(); s_countedCollectibles.Clear(); s_unlockEntries.Clear(); s_unlockEntryIndexes.Clear(); s_countedUnlockPickups.Clear(); ClearCurrentMapCosmeticState("scene-loaded"); s_lastLoggedRendererSignature = null; s_lastLoggedTmpDiagnosticsSignature = null; s_lastLoggedTmpLayoutSignature = null; s_lastLoggedDirectTmpFailure = null; s_nextTmpDiagnosticsUtc = DateTime.MinValue; if (IsLobbyScene(sceneName)) { LogShandadapandaOwnershipReadinessProbe("scene-loaded:" + sceneName); } if (IsLobbyVisitScene(sceneName)) { int num; lock (s_shandadapandaSyncLock) { if (!s_shandadapandaInLobbyVisit) { s_shandadapandaLobbyGeneration++; } s_shandadapandaInLobbyVisit = true; num = s_shandadapandaLobbyGeneration; } LogDebug("Shandadapanda lobby sync generation=" + num.ToString(CultureInfo.InvariantCulture) + " scene=" + sceneName); return; } lock (s_shandadapandaSyncLock) { s_shandadapandaInLobbyVisit = false; } } private static bool EnsureReflection() { if (s_reflectionReady) { return true; } try { s_rectType = Il2CppReflection.FindType("UnityEngine.Rect"); s_colorType = Il2CppReflection.FindType("UnityEngine.Color"); s_vector2Type = Il2CppReflection.FindType("UnityEngine.Vector2"); s_guiType = Il2CppReflection.FindType("UnityEngine.GUI"); s_screenType = Il2CppReflection.FindType("UnityEngine.Screen"); s_inputType = Il2CppReflection.FindType("UnityEngine.Input"); s_keyCodeType = Il2CppReflection.FindType("UnityEngine.KeyCode"); s_objectType = Il2CppReflection.FindType("UnityEngine.Object"); s_componentType = Il2CppReflection.FindType("UnityEngine.Component"); s_gameObjectType = Il2CppReflection.FindType("UnityEngine.GameObject"); s_transformType = Il2CppReflection.FindType("UnityEngine.Transform"); s_rectTransformType = Il2CppReflection.FindType("UnityEngine.RectTransform"); s_tmpTextType = FindTmpType("TMP_Text"); s_tmpFontAssetType = FindTmpType("TMP_FontAsset"); s_textMeshProUguiType = FindTmpType("TextMeshProUGUI"); s_canvasType = Il2CppReflection.FindType("UnityEngine.Canvas"); s_renderModeType = Il2CppReflection.FindType("UnityEngine.RenderMode"); s_resourcesType = Il2CppReflection.FindType("UnityEngine.Resources"); s_textAlignmentOptionsType = FindTmpType("TextAlignmentOptions"); s_textOverflowModesType = FindTmpType("TextOverflowModes"); s_labyrinthineInputManagerType = Il2CppReflection.FindType("LabyrinthineInputManager"); s_inputButtonType = Il2CppReflection.FindType("InputButton"); if (s_rectType == null || s_colorType == null || s_guiType == null) { return LogReflectionNotReady(); } s_rectCtor = s_rectType.GetConstructor(new Type[4] { typeof(float), typeof(float), typeof(float), typeof(float) }); s_colorCtor = s_colorType.GetConstructor(new Type[4] { typeof(float), typeof(float), typeof(float), typeof(float) }); s_vector2Ctor = s_vector2Type?.GetConstructor(new Type[2] { typeof(float), typeof(float) }); s_guiColorProperty = s_guiType.GetProperty("color", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); s_screenWidthProperty = s_screenType?.GetProperty("width", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); s_screenHeightProperty = s_screenType?.GetProperty("height", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); s_guiSkinProperty = s_guiType.GetProperty("skin", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); s_guiLabel = s_guiType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "Label" && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == s_rectType && m.GetParameters()[1].ParameterType == typeof(string)); s_inputGetKey = s_inputType?.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "GetKey" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == s_keyCodeType); if (s_keyCodeType != null) { s_keyCodeTab = Enum.Parse(s_keyCodeType, "Tab"); } if (s_labyrinthineInputManagerType != null && s_inputButtonType != null) { s_gameGetButton = s_labyrinthineInputManagerType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "GetButton" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == s_inputButtonType); s_showTabMenuButton = Enum.Parse(s_inputButtonType, "ShowTabMenu"); } s_findObjectsOfTypeMethod = s_objectType?.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "FindObjectsOfType" && !m.ContainsGenericParameters && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(Type)); s_componentGetComponentInParentMethod = s_componentType?.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "GetComponentInParent" && !m.ContainsGenericParameters && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(Type)); s_gameObjectGetComponentMethod = s_gameObjectType?.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "GetComponent" && !m.ContainsGenericParameters && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(Type)); s_gameObjectSetActiveMethod = s_gameObjectType?.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "SetActive" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(bool)); s_objectDestroyMethod = s_objectType?.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "Destroy" && m.GetParameters().Length == 1); s_objectDontDestroyOnLoadMethod = s_objectType?.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "DontDestroyOnLoad" && m.GetParameters().Length == 1); s_resourcesFindObjectsOfTypeAllMethod = s_resourcesType?.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "FindObjectsOfTypeAll" && !m.ContainsGenericParameters && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(Type)); if (s_rectCtor == null || s_colorCtor == null || s_guiLabel == null) { return LogReflectionNotReady(); } InitLabelSkinAccessors(); s_reflectionReady = true; return true; } catch (Exception ex) { LogWarning("Unity reflection init failed: " + ex.GetType().Name + " " + ex.Message); return false; } } private static void InitLabelSkinAccessors() { try { object obj = s_guiSkinProperty?.GetValue(null); PropertyInfo propertyInfo = ((s_guiSkinLabelProperty == null && obj != null) ? obj.GetType().GetProperty("label", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : s_guiSkinLabelProperty); if (propertyInfo != null) { s_guiSkinLabelProperty = propertyInfo; } object obj2 = s_guiSkinLabelProperty?.GetValue(obj); if (obj2 != null) { Type type = obj2.GetType(); s_styleFontSizeProperty = type.GetProperty("fontSize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); s_styleNormalProperty = type.GetProperty("normal", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); s_styleStateTextColorProperty = (s_styleNormalProperty?.GetValue(obj2))?.GetType().GetProperty("textColor", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } } catch { } } private static bool LogReflectionNotReady() { LogWarning("Unity IMGUI reflection is not ready yet."); return false; } private static void LabelShadowed(float x, float y, float width, float height, string text, int fontSize, object color) { Label(x + 1f, y + 1f, width, height, text, Color(0f, 0f, 0f, 0.7f), fontSize); Label(x, y, width, height, text, color, fontSize); } private static object ObjectiveColor() { return Color(1f, 1f, 1f, 1f); } private static object MutedObjectiveColor() { return Color(1f, 1f, 1f, 0.82f); } private static object CopyColorWithAlpha(object color, float alpha) { if (TryReadColor(color, out var r, out var g, out var b, out var _)) { return Color(ValueConversion.Clamp01(r), ValueConversion.Clamp01(g), ValueConversion.Clamp01(b), ValueConversion.Clamp01(alpha)); } return Color(1f, 1f, 1f, ValueConversion.Clamp01(alpha)); } private static bool TryReadColor(object color, out float r, out float g, out float b, out float a) { r = 1f; g = 1f; b = 1f; a = 1f; if (color == null) { return false; } if (ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(color, "r"), out r) && ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(color, "g"), out g) && ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(color, "b"), out b)) { return ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(color, "a"), out a); } return false; } private static void Label(float x, float y, float width, float height, string text, object color, int fontSize) { SetGuiColor(color); object oldFontSize = null; object oldTextColor = null; object skinLabelStyle = GetSkinLabelStyle(); object obj = ((skinLabelStyle == null) ? null : s_styleNormalProperty?.GetValue(skinLabelStyle)); try { if (skinLabelStyle != null && s_styleFontSizeProperty != null) { oldFontSize = s_styleFontSizeProperty.GetValue(skinLabelStyle); } if (obj != null && s_styleStateTextColorProperty != null) { oldTextColor = s_styleStateTextColorProperty.GetValue(obj); } SetLabelFontSize(skinLabelStyle, fontSize); SetLabelTextColor(obj, color); s_guiLabel.Invoke(null, new object[2] { Rect(x, y, width, height), text }); } finally { RestoreLabelStyle(skinLabelStyle, obj, oldFontSize, oldTextColor); SetGuiColor(Color(1f, 1f, 1f, 1f)); } } private static object GetSkinLabelStyle() { try { object obj = s_guiSkinProperty?.GetValue(null); return (obj == null) ? null : s_guiSkinLabelProperty?.GetValue(obj); } catch { return null; } } private static void SetLabelFontSize(object style, int fontSize) { try { s_styleFontSizeProperty?.SetValue(style, fontSize); } catch { } } private static void SetLabelTextColor(object normal, object color) { try { s_styleStateTextColorProperty?.SetValue(normal, color); } catch { } } private static void RestoreLabelStyle(object style, object normal, object oldFontSize, object oldTextColor) { try { if (style != null && oldFontSize != null) { s_styleFontSizeProperty?.SetValue(style, oldFontSize); } if (normal != null && oldTextColor != null) { s_styleStateTextColorProperty?.SetValue(normal, oldTextColor); } } catch { } } private static object Rect(float x, float y, float width, float height) { return s_rectCtor.Invoke(new object[4] { x, y, width, height }); } private static object Color(float r, float g, float b, float a) { return s_colorCtor.Invoke(new object[4] { r, g, b, a }); } private static object Vector2(float x, float y) { return s_vector2Ctor.Invoke(new object[2] { x, y }); } private static void SetGuiColor(object color) { try { s_guiColorProperty?.SetValue(null, color); } catch { } } private static int ScreenWidth() { try { return (s_screenWidthProperty == null) ? 1920 : Convert.ToInt32(s_screenWidthProperty.GetValue(null)); } catch { return 1920; } } private static int ScreenHeight() { try { return (s_screenHeightProperty == null) ? 1080 : Convert.ToInt32(s_screenHeightProperty.GetValue(null)); } catch { return 1080; } } private static bool HasAnyDisplayedPickup() { if (GetCollectibleRows().Count <= 0) { return GetUnlockRows().Count > 0; } return true; } private static bool HasAnyDisplayedContent() { return HasAnyDisplayedContent(GetShandadapandaMissingRows()); } private static bool HasAnyDisplayedContent(List<ShandadapandaMissingRow> missingRows) { if (!HasAnyDisplayedPickup()) { if (missingRows != null) { return missingRows.Count > 0; } return false; } return true; } private static List<CollectibleEntry> GetCollectibleRows() { List<CollectibleEntry> list = new List<CollectibleEntry>(); AddCollectibleRow(list, "Tickets"); AddCollectibleRow(list, "Reroll Coins"); return list; } private static void AddCollectibleRow(List<CollectibleEntry> rows, string label) { if (s_collectiblesFound.TryGetValue(label, out var value) && value > 0) { rows.Add(new CollectibleEntry(label, value)); } } private static List<UnlockEntry> GetUnlockRows() { return s_unlockEntries.ToList(); } private static List<ShandadapandaMissingRow> GetShandadapandaMissingRows() { List<ShandadapandaMissingRow> list = new List<ShandadapandaMissingRow>(); if (s_showShandadapandaMissingOwners == null || !s_showShandadapandaMissingOwners.Value) { return list; } List<ShandadapandaMissingTarget> shandadapandaMissingTargets = GetShandadapandaMissingTargets(); lock (s_shandadapandaMissingLock) { foreach (ShandadapandaMissingTarget item in shandadapandaMissingTargets) { if (s_shandadapandaMissingByWebsiteId.TryGetValue(item.WebsiteItemId, out var value) && value.Completed && value.MissingNames.Count != 0) { list.Add(new ShandadapandaMissingRow(value.ItemName, value.MissingNames.ToList())); } } return list; } } private static List<ShandadapandaMissingTarget> GetShandadapandaMissingTargets() { List<int> list = new List<int>(); foreach (int s_currentMapCosmeticItemId in s_currentMapCosmeticItemIds) { if (s_currentMapCosmeticItemId >= 0 && !list.Contains(s_currentMapCosmeticItemId)) { list.Add(s_currentMapCosmeticItemId); } } List<ShandadapandaMissingTarget> list2 = new List<ShandadapandaMissingTarget>(); foreach (int item in list.OrderBy((int id) => id)) { if (TryGetShandadaItemInfo(item, out var info) && info.WebsiteId > 0) { string itemName = ((!string.IsNullOrWhiteSpace(info.Name)) ? info.Name : (ResolveCustomizationName(item) ?? ("#" + item))); list2.Add(new ShandadapandaMissingTarget(item, info.WebsiteId, itemName)); } } return list2; } private static string FormatUnlockEntry(UnlockEntry entry, bool richText = false) { if (string.Equals(entry.Category, "Stamps", StringComparison.OrdinalIgnoreCase)) { return "Stamps: " + entry.Count; } string obj = (richText ? EscapeTmpRichText(entry.Category) : entry.Category); string text = (richText ? EscapeTmpRichText(entry.Name) : entry.Name); string text2 = (richText ? EscapeTmpRichText(entry.ItemClass) : entry.ItemClass); string text3 = obj + ":"; if (string.Equals(entry.Category, "Cosmetic", StringComparison.OrdinalIgnoreCase) && entry.CosmeticState != CosmeticUnlockState.None) { text3 = text3 + " " + FormatCosmeticStateMarker(entry.CosmeticState, richText); } text3 = text3 + " " + text; if (entry.Id >= 0) { text3 = text3 + " #" + entry.Id; } if (string.Equals(entry.Category, "Cosmetic", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(text2)) { text3 = text3 + " (" + text2 + ")"; } if (entry.Count > 1) { text3 = text3 + " x" + entry.Count; } return text3; } private static string FormatShandadapandaMissingRow(ShandadapandaMissingRow row, bool richText = false) { string text = "Missing"; string text2 = string.Join(", ", row.Names); if (richText) { text = EscapeTmpRichText(text); text2 = EscapeTmpRichText(text2); } return text + ": " + text2; } private static string FormatCosmeticStateMarker(CosmeticUnlockState state, bool richText) { string text = ((state == CosmeticUnlockState.New) ? "[new]" : "[old]"); if (!richText) { return text; } string text2 = ((state == CosmeticUnlockState.New) ? "#42ff80" : "#8a8a8a"); return "<color=" + text2 + ">" + text + "</color>"; } private static void ReloadShandadapandaWatchedNames(bool force) { DateTime utcNow = DateTime.UtcNow; if (!force && utcNow < s_nextShandadapandaNamesReloadUtc) { return; } s_nextShandadapandaNamesReloadUtc = utcNow.AddSeconds(5.0); try { string shandadapandaMissingNamesFilePath = GetShandadapandaMissingNamesFilePath(); string directoryName = Path.GetDirectoryName(shandadapandaMissingNamesFilePath); if (!string.IsNullOrWhiteSpace(directoryName)) { Directory.CreateDirectory(directoryName); } if (!File.Exists(shandadapandaMissingNamesFilePath)) { File.WriteAllText(shandadapandaMissingNamesFilePath, "# One Shandadapanda nickname per line.\r\n# Lines starting with # are ignored.\r\n"); LogDebug("Created Shandadapanda missing-owner watch list at " + shandadapandaMissingNamesFilePath); } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(shandadapandaMissingNamesFilePath); if (!force && lastWriteTimeUtc == s_shandadapandaNamesFileLastWriteUtc) { return; } List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array = File.ReadAllLines(shandadapandaMissingNamesFilePath); for (int i = 0; i < array.Length; i++) { string text = (array[i] ?? string.Empty).Trim(); if (text.Length != 0 && !text.StartsWith("#", StringComparison.Ordinal)) { string text2 = NormalizeShandadapandaName(text); if (!string.IsNullOrWhiteSpace(text2) && hashSet.Add(text2)) { list.Add(text); } } } int num; int num2; lock (s_shandadapandaMissingLock) { s_shandadapandaWatchedNames = list; num = ClearShandadapandaMissingOwnerLookupStateLocked(); num2 = s_shandadapandaMissingGeneration; } s_shandadapandaNamesFileLastWriteUtc = lastWriteTimeUtc; LogDebug("Loaded Shandadapanda missing-owner watch names count=" + list.Count + " clearedLookups=" + num + " generation=" + num2 + " file=" + shandadapandaMissingNamesFilePath); } catch (Exception ex) { LogWarning("Shandadapanda watch-list load failed: " + ex.GetType().Name + " " + ex.Message); } } private static string GetShandadapandaMissingNamesFilePath() { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserData", "PickupTabOverlayShandadapandaNames.txt"); } private static bool IsMapCustomizationPickupCandidate(object pickup) { if (pickup == null || !IsActiveInHierarchy(pickup)) { return false; } string objectSceneName = GetObjectSceneName(pickup); if (!string.IsNullOrWhiteSpace(objectSceneName) && (IsLobbyScene(objectSceneName) || string.Equals(objectSceneName, "Init", StringComparison.OrdinalIgnoreCase))) { return false; } return HasNonZeroLocalPosition(pickup); } private static bool HasNonZeroLocalPosition(object obj) { try { object fieldOrProp = Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(Il2CppReflection.GetFieldOrProp(obj, "gameObject") ?? obj, "transform") ?? Il2CppReflection.GetFieldOrProp(obj, "transform"), "localPosition"); if (ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(fieldOrProp, "x"), out var result) && ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(fieldOrProp, "y"), out var result2) && ValueConversion.TryConvertFloat(Il2CppReflection.GetFieldOrProp(fieldOrProp, "z"), out var result3)) { return Math.Abs(result) > 0.0001f || Math.Abs(result2) > 0.0001f || Math.Abs(result3) > 0.0001f; } LogDebug("Current map cosmetic candidate rejected: localPosition unavailable object=" + TruncateForLog(GetUnityObjectName(obj) ?? obj.GetType().Name, 80)); } catch (Exception ex) { LogDebug("Current map cosmetic candidate rejected: localPosition read failed " + ex.GetType().Name + " object=" + TruncateForLog(GetUnityObjectName(obj) ?? obj.GetType().Name, 80)); } return false; } private static bool TryGetCustomizationPickupItemId(object pickup, out int itemId) { return ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(pickup, "ItemID") ?? Il2CppReflection.GetFieldOrProp(pickup, "itemID"), out itemId); } private static void ClearCurrentMapCosmeticState(string reason) { bool flag = s_currentMapCosmeticItemIds.Count > 0; if (flag) { s_currentMapCosmeticItemIds.Clear(); } s_currentMapCosmeticObservedUtc = DateTime.MinValue; int num = ClearShandadapandaMissingOwnerLookupState(reason); if (flag || num > 0) { LogDebug("Current map cosmetic state cleared reason=" + reason + " hadCurrent=" + flag + " clearedMissingLookups=" + num); } } private static int ClearShandadapandaMissingOwnerLookupState(string reason) { int num; int num2; lock (s_shandadapandaMissingLock) { num = ClearShandadapandaMissingOwnerLookupStateLocked(); num2 = s_shandadapandaMissingGeneration; } if (num > 0) { LogDebug("Shandadapanda missing-owner lookup state cleared reason=" + reason + " cleared=" + num + " generation=" + num2); } return num; } private static int ClearShandadapandaMissingOwnerLookupStateLocked() { int count = s_shandadapandaMissingByWebsiteId.Count; if (count <= 0) { return 0; } s_shandadapandaMissingByWebsiteId.Clear(); s_shandadapandaMissingGeneration++; s_lastLoggedMissingOwnerSignature = null; return count; } private static bool RememberCurrentMapCosmeticItemId(int itemId, string source, bool queueMissingOwnerLookup) { if (itemId < 0) { return false; } bool flag = s_currentMapCosmeticItemIds.Count != 1 || s_currentMapCosmeticItemIds[0] != itemId; if (flag) { ClearShandadapandaMissingOwnerLookupState("current-map-cosmetic-changed"); s_currentMapCosmeticItemIds.Clear(); s_currentMapCosmeticItemIds.Add(itemId); s_currentMapCosmeticObservedUtc = DateTime.UtcNow; } string text = ResolveCustomizationName(itemId) ?? ("#" + itemId); ShandadaItemInfo info; int num = (TryGetShandadaItemInfo(itemId, out info) ? info.WebsiteId : 0); if (flag) { LogDebug("Current map cosmetic observed source=" + source + " itemID=" + itemId + " websiteId=" + num + " name=" + text + " queueMissingOwners=" + queueMissingOwnerLookup); } if (queueMissingOwnerLookup) { ReloadShandadapandaWatchedNames(force: true); QueueShandadapandaMissingOwnerLookups(); } else if (flag) { LogMissingOwnerState("waiting-for-pickup"); } return flag; } private static void QueueShandadapandaMissingOwnerLookups() { if (s_showShandadapandaMissingOwners == null || !s_showShandadapandaMissingOwners.Value) { return; } string configuredShandadapandaApiKey = GetConfiguredShandadapandaApiKey(); if (string.IsNullOrWhiteSpace(configuredShandadapandaApiKey)) { ClearShandadapandaMissingOwnerLookupState("missing-auth"); LogMissingOwnerState("missing-auth"); return; } List<ShandadapandaMissingTarget> shandadapandaMissingTargets = GetShandadapandaMissingTargets(); if (shandadapandaMissingTargets.Count == 0) { ClearShandadapandaMissingOwnerLookupState("no-map-cosmetic"); LogMissingOwnerState("no-map-cosmetic"); return; } foreach (ShandadapandaMissingTarget item in shandadapandaMissingTargets) { TryQueueShandadapandaMissingOwnerLookup(item, configuredShandadapandaApiKey); } } private static void TryQueueShandadapandaMissingOwnerLookup(ShandadapandaMissingTarget target, string apiKey) { DateTime utcNow = DateTime.UtcNow; int generation; lock (s_shandadapandaMissingLock) { if (!s_shandadapandaMissingByWebsiteId.TryGetValue(target.WebsiteItemId, out var value)) { value = new ShandadapandaMissingLookupState(target.GameItemId, target.WebsiteItemId, target.ItemName); s_shandadapandaMissingByWebsiteId[target.WebsiteItemId] = value; } value.GameItemId = target.GameItemId; value.ItemName = target.ItemName; if (value.Running || value.Completed || utcNow < value.NextRetryUtc) { return; } value.Running = true; value.Error = null; generation = s_shandadapandaMissingGeneration; } Task.Run(async delegate { try { await RunShandadapandaMissingOwnerLookupAsync(target, apiKey, generation, CancellationToken.None); } catch (Exception ex) { bool flag = false; lock (s_shandadapandaMissingLock) { flag = generation != s_shandadapandaMissingGeneration; if (!flag && s_shandadapandaMissingByWebsiteId.TryGetValue(target.WebsiteItemId, out var value2)) { value2.Running = false; value2.Completed = false; value2.NextRetryUtc = DateTime.UtcNow.AddSeconds(120.0); value2.Error = ex.GetType().Name + " " + ex.Message; } } if (flag) { LogDebug("Discarded stale Shandadapanda missing-owner failure item=" + target.ItemName + " websiteId=" + target.WebsiteItemId + " generation=" + generation); } else { MelonLogger.Warning("Shandadapanda missing-owner lookup failed: item=" + target.ItemName + " websiteId=" + target.WebsiteItemId + " " + ex.GetType().Name + " " + ex.Message); } } }); } private static async Task RunShandadapandaMissingOwnerLookupAsync(ShandadapandaMissingTarget target, string apiKey, int generation, CancellationToken cancellationToken) { using HttpClient client = new HttpClient { BaseAddress = new Uri(GetConfiguredShandadapandaApiBaseUrl()), Timeout = TimeSpan.FromSeconds(30.0) }; ShandadapandaAuth auth = await GetShandadapandaAuthAsync(client, null, apiKey, requireAccountId: false, cancellationToken); HashSet<string> uncheckedNames = await GetShandadapandaUncheckedUserNamesAsync(client, auth, target.WebsiteItemId, cancellationToken); List<string> list = await GetShandadapandaFriendNamesAsync(client, auth, cancellationToken); List<string> list2 = FilterWatchedShandadapandaNames(uncheckedNames, list); lock (s_shandadapandaMissingLock) { if (generation != s_shandadapandaMissingGeneration) { LogDebug("Discarded stale Shandadapanda missing-owner result item=" + target.ItemName + " websiteId=" + target.WebsiteItemId + " generation=" + generation + " currentGeneration=" + s_shandadapandaMissingGeneration); return; } if (!s_shandadapandaMissingByWebsiteId.TryGetValue(target.WebsiteItemId, out var value)) { value = new ShandadapandaMissingLookupState(target.GameItemId, target.WebsiteItemId, target.ItemName); s_shandadapandaMissingByWebsiteId[target.WebsiteItemId] = value; } value.GameItemId = target.GameItemId; value.ItemName = target.ItemName; value.MissingNames = list2; value.Running = false; value.Completed = true; value.NextRetryUtc = DateTime.MinValue; value.Error = null; } LogDebug("Shandadapanda missing-owner lookup item=" + target.ItemName + " websiteId=" + target.WebsiteItemId + " uncheckedUsers=" + uncheckedNames.Count + " friendNames=" + list.Count + " watchedMissing=" + list2.Count); } private static async Task<HashSet<string>> GetShandadapandaUncheckedUserNamesAsync(HttpClient client, ShandadapandaAuth auth, int websiteItemId, CancellationToken cancellationToken) { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "items/" + websiteItemId + "/users/unchecked"); ApplyShandadapandaAuth(request, auth); using HttpResponseMessage response = await client.SendAsync(request, cancellationToken); string text = await ReadShandadapandaResponseAsync(response, "get unchecked item users"); using JsonDocument jsonDocument = JsonDocument.Parse(text); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); AddShandadapandaUserNames(jsonDocument.RootElement, hashSet, allowContainers: true); if (hashSet.Count == 0) { LogDebug("Shandadapanda unchecked-users response had no parseable names for websiteId=" + websiteItemId + " body=" + TruncateForLog(text, 180)); } return hashSet; } private static async Task<List<string>> GetShandadapandaFriendNamesAsync(HttpClient client, ShandadapandaAuth auth, CancellationToken cancellationToken) { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "api/friendsList"); ApplyShandadapandaAuth(request, auth); using HttpResponseMessage response = await client.SendAsync(request, cancellationToken); using JsonDocument jsonDocument = JsonDocument.Parse(await ReadShandadapandaResponseAsync(response, "get friends list")); if (jsonDocument.RootElement.ValueKind != JsonValueKind.Array) { throw new InvalidOperationException("friends list response was not an array"); } List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); int num = 0; foreach (JsonElement item in jsonDocument.RootElement.EnumerateArray()) { if (!TryParseShandadapandaFriendName(item, out var name)) { num++; continue; } string text = NormalizeShandadapandaName(name); if (!string.IsNullOrWhiteSpace(text) && hashSet.Add(text)) { list.Add(name.Trim()); } } LogDebug("Shandadapanda friends loaded names=" + list.Count + " skippedRows=" + num); return list; } private static bool TryParseShandadapandaFriendName(JsonElement row, out string name) { name = null; if (row.ValueKind != JsonValueKind.Object) { return false; } string jsonString = GetJsonString(row, "status"); if (!string.IsNullOrWhiteSpace(jsonString) && !string.Equals(jsonString, "accepted", StringComparison.OrdinalIgnoreCase)) { return false; } name = GetJsonString(row, "friend_username"); if (string.IsNullOrWhiteSpace(name)) { name = GetJsonString(row, "username"); } return !string.IsNullOrWhiteSpace(name); } private static void AddShandadapandaUserNames(JsonElement element, HashSet<string> normalizedNames, bool allowContainers) { if (element.ValueKind == JsonValueKind.Array) { foreach (JsonElement item in element.EnumerateArray()) { AddShandadapandaUserNames(item, normalizedNames, allowContainers: false); } return; } if (element.ValueKind == JsonValueKind.String) { AddNormalizedShandadapandaName(normalizedNames, element.GetString()); } else { if (element.ValueKind != JsonValueKind.Object) { return; } TryAddShandadapandaNameFromObject(element, normalizedNames); string[] array = new string[3] { "user", "account", "profile" }; foreach (string propertyName in array) { if (element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.Object) { TryAddShandadapandaNameFromObject(value, normalizedNames); } } if (!allowContainers) { return; } array = new string[7] { "users", "unchecked", "unchecked_users", "uncheckedUsers", "data", "results", "items" }; foreach (string propertyName2 in array) { if (element.TryGetProperty(propertyName2, out var value2)) { AddShandadapandaUserNames(value2, normalizedNames, allowContainers: false); } } } } private static void TryAddShandadapandaNameFromObject(JsonElement element, HashSet<string> normalizedNames) { string[] array = new string[7] { "username", "user_name", "userName", "display_name", "displayName", "nickname", "name" }; foreach (string name in array) { AddNormalizedShandadapandaName(normalizedNames, GetJsonString(element, name)); } } private static void AddNormalizedShandadapandaName(HashSet<string> normalizedNames, string name) { string text = NormalizeShandadapandaName(name); if (!string.IsNullOrWhiteSpace(text)) { normalizedNames.Add(text); } } private static List<string> FilterWatchedShandadapandaNames(HashSet<string> uncheckedNames, List<string> friendNames) { List<string> result = new List<string>(); if (uncheckedNames == null || uncheckedNames.Count == 0) { return result; } List<string> list; lock (s_shandadapandaMissingLock) { list = s_shandadapandaWatchedNames.ToList(); } foreach (string item in list) { AddMissingOwnerNameIfUnchecked(result, uncheckedNames, item); } if (friendNames != null) { foreach (string friendName in friendNames) { AddMissingOwnerNameIfUnchecked(result, uncheckedNames, friendName); } } return result; } private static void AddMissingOwnerNameIfUnchecked(List<string> result, HashSet<string> uncheckedNames, string name) { if (result == null || uncheckedNames == null || string.IsNullOrWhiteSpace(name)) { return; } string text = NormalizeShandadapandaName(name); if (string.IsNullOrWhiteSpace(text) || !uncheckedNames.Contains(text)) { return; } foreach (string item in result) { if (string.Equals(NormalizeShandadapandaName(item), text, StringComparison.OrdinalIgnoreCase)) { return; } } result.Add(name.Trim()); } private static string NormalizeShandadapandaName(string name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } string text = name.Trim(); if (text.StartsWith("@", StringComparison.Ordinal)) { text = text.Substring(1).Trim(); } return text.ToLowerInvariant(); } private static void LogMissingOwnerState(string state) { if (IsDebugLoggingEnabled() && !string.Equals(s_lastLoggedMissingOwnerSignature, state, StringComparison.Ordinal)) { s_lastLoggedMissingOwnerSignature = state; LogDebug("Shandadapanda missing-owner lookup state=" + state); } } private static void TryStartShandadapandaSyncForCurrentLobby(string reason) { int num; lock (s_shandadapandaSyncLock) { num = s_shandadapandaLobbyGeneration; if (num <= 0) { LogDebug("Shandadapanda sync skipped: no lobby generation trigger=" + reason); return; } if (s_shandadapandaSyncedLobbyGeneration == num) { LogDebug("Shandadapanda sync skipped: already synced lobbyGeneration=" + num.ToString(CultureInfo.InvariantCulture) + " trigger=" + reason); return; } if (s_shandadapandaSyncRunning) { LogDebug("Shandadapanda sync skipped: already running lobbyGeneration=" + num.ToString(CultureInfo.InvariantCulture) + " trigger=" + reason); return; } } if (s_enableShandadapandaSync == null || !s_enableShandadapandaSync.Value) { LogDebug("Shandadapanda sync skipped: disabled trigger=" + reason); return; } string username = (s_shandadapandaUsername?.Value ?? string.Empty).Trim(); string apiKey = GetConfiguredShandadapandaApiKey(); if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(apiKey)) { LogDebug("Shandadapanda sync skipped: missing username or API key trigger=" + reason); return; } string mode = NormalizeShandadapandaSyncMode(s_shandadapandaSyncMode?.Value); if (string.IsNullOrWhiteSpace(mode)) { MelonLogger.Warning("Shandadapanda sync skipped: invalid SyncMode '" + (s_shandadapandaSyncMode?.Value ?? "{null}") + "'. Use ReadOnly or WritePickedCosmetics."); return; } EnsureShandadaItemsLoaded(); ShandadapandaOwnershipSnapshot ownership = GetMappedShandadapandaOwnershipSnapshot(); if (ownership.States.Count == 0) { MelonLogger.Warning("Shandadapanda sync skipped: local ownership was not available at source-ready event. trigger=" + reason + " lobbyGeneration=" + num.ToString(CultureInfo.InvariantCulture)); return; } lock (s_shandadapandaSyncLock) { if (s_shandadapandaSyncRunning) { LogDebug("Shandadapanda sync skipped: already running trigger=" + reason); return; } if (s_shandadapandaSyncedLobbyGeneration == num) { LogDebug("Shandadapanda sync skipped: already synced lobbyGeneration=" + num.ToString(CultureInfo.InvariantCulture) + " trigger=" + reason); return; } s_shandadapandaSyncRunning = true; s_shandadapandaSyncedLobbyGeneration = num; } LogDebug("Shandadapanda sync starting trigger=" + reason + " lobbyGeneration=" + num.ToString(CultureInfo.InvariantCulture)); Task.Run(async delegate { try { await RunShandadapandaSyncAsync(reason, username, apiKey, mode, ownership, CancellationToken.None); } catch (Exception ex) { MelonLogger.Warning("Shandadapanda sync failed: " + ex.GetType().Name + " " + ex.Message); } finally { lock (s_shandadapandaSyncLock) { s_shandadapandaSyncRunning = false; } } }); } private static async Task RunShandadapandaSyncAsync(string reason, string username, string apiKey, string mode, ShandadapandaOwnershipSnapshot ownership, CancellationToken cancellationToken) { using HttpClient client = new HttpClient { BaseAddress = new Uri(GetConfiguredShandadapandaApiBaseUrl()), Timeout = TimeSpan.FromSeconds(30.0) }; ShandadapandaAuth auth = await GetShandadapandaAuthAsync(client, username, apiKey, requireAccountId: true, cancellationToken); Dictionary<int, ShandadapandaUserItem> rowsByWebsiteId = (from item in await GetShandadapandaUserItemsAsync(client, auth, cancellationToken) where item.WebsiteItemId > 0 && (item.GamesId <= 0 || item.GamesId == 1) group item by item.WebsiteItemId).ToDictionary((IGrouping<int, ShandadapandaUserItem> group) => group.Key, (IGrouping<int, ShandadapandaUserItem> group) => group.First()); bool writeMode = string.Equals(mode, "WritePickedCosmetics", StringComparison.Ordinal); int mapped = 0; int matched = 0; int alreadyCorrect = 0; int differences = 0; int changed = 0; int missingWebsiteRows = 0; int failedUpdates = 0; foreach (KeyValuePair<int, ShandadapandaDesiredItemState> item in ownership.States.OrderBy((KeyValuePair<int, ShandadapandaDesiredItemState> pair) => pair.Key)) { int websiteId = item.Key; ShandadapandaDesiredItemState desiredState = item.Value; if (websiteId <= 0) { continue; } mapped++; bool desiredChecked = desiredState.Checked; if (!rowsByWebsiteId.TryGetValue(websiteId, out var value)) { missingWebsiteRows++; LogDebug("Shandadapanda sync has no user row for websiteId=" + websiteId + " local=" + desiredState.LocalKey + " name=" + desiredState.Name); continue; } matched++; if (value.Checked == desiredChecked) { alreadyCorrect++; continue; } differences++; if (writeMode) { if (await UpdateShandadapandaItemAsync(client, auth, value.UserItemId, desiredChecked, cancellationToken)) { changed++; LogDebug("Shandadapanda sync updated websiteId=" + websiteId + " local=" + desiredState.LocalKey + " name=" + desiredState.Name + " checked=" + desiredChecked); } else { failedUpdates++; } } } MelonLogger.Msg("Shandadapanda sync " + mode + " trigger=" + reason + " user=" + username + " auth=api-key ownership=" + ownership.Source + " owned=" + ownership.OwnedCount + "/" + ownership.States.Count + " mapped=" + mapped + " matched=" + matched + " alreadyCorrect=" + alreadyCorrect + " differences=" + differences + " changed=" + changed + " missingRows=" + missingWebsiteRows + " failedUpdates=" + failedUpdates); } private static string NormalizeShandadapandaSyncMode(string mode) { if (string.Equals(mode, "ReadOnly", StringComparison.OrdinalIgnoreCase)) { return "ReadOnly"; } if (string.Equals(mode, "WritePickedCosmetics", StringComparison.OrdinalIgnoreCase)) { return "WritePickedCosmetics"; } return null; } private static ShandadapandaOwnershipSnapshot GetMappedShandadapandaOwnershipSnapshot() { Dictionary<int, ShandadapandaDesiredItemState> dictionary = new Dictionary<int, ShandadapandaDesiredItemState>(); List<string> list = new List<string>(); AddMappedCustomizationOwnershipStates(dictionary, list); AddSaveBackedShandadapandaOwnershipStates(dictionary, list); int ownedCount = dictionary.Values.Count((ShandadapandaDesiredItemState value) => value.Checked); return new ShandadapandaOwnershipSnapshot(dictionary, ownedCount, (list.Count > 0) ? string.Join("+", list) : "{none}"); } private static void AddMappedCustomizationOwnershipStates(Dictionary<int, ShandadapandaDesiredItemState> states, List<string> sources) { if (!TryCollectWardrobeUnlockedItems(out var unlockedItemIds, out var source)) { LogDebug("Shandadapanda sync customization ownership unavailable: " + source); return; } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; int num6 = 0; foreach (KeyValuePair<int, ShandadaItemInfo> item in s_shandadaItems.OrderBy((KeyValuePair<int, ShandadaItemInfo> pair) => pair.Key)) { int key = item.Key; ShandadaItemInfo value = item.Value; if (key >= 0 && key <= 65535 && value.WebsiteId > 0) { bool flag = unlockedItemIds.Contains(key); states[value.WebsiteId] = new ShandadapandaDesiredItemState(flag, value.WebsiteId, value.Name, "Cosmetic", "wardrobeGameItemId=" + key); num++; if (flag) { num2++; } } } Dictionary<int, int> explicitWardrobeOnlyWebsiteItemMatches = GetExplicitWardrobeOnlyWebsiteItemMatches(); int num7 = 0; int num8 = 0; foreach (KeyValuePair<int, int> item2 in explicitWardrobeOnlyWebsiteItemMatches.OrderBy((KeyValuePair<int, int> pair) => pair.Key)) { if (s_shandadaWebsiteItems.TryGetValue(item2.Key, out var value2) && !states.ContainsKey(value2.WebsiteId)) { int value3 = item2.Value; bool flag2 = unlockedItemIds.Contains(value3); states[value2.WebsiteId] = new ShandadapandaDesiredItemState(flag2, value2.WebsiteId, value2.Name, "Cosmetic", "wardrobeGameItemId=" + value3); num7++; if (flag2) { num8++; } } } foreach (ShandadaWebsiteItemInfo item3 in from item in s_shandadaWebsiteItems.Values where IsStoreBoughtItemClass(item.ItemClass) orderby item.WebsiteId select item) { if (!item3.HasStoreCustomizationItemId) { if (!states.ContainsKey(item3.WebsiteId)) { num6++; } continue; } int storeCustomizationItemId = item3.StoreCustomizationItemId; bool flag3 = unlockedItemIds.Contains(storeCustomizationItemId); if (states.TryGetValue(item3.WebsiteId, out var value4) && !string.Equals(value4.LocalKey, "storeCustomizationId=" + storeCustomizationItemId, StringComparison.Ordinal)) { num5++; } states[item3.WebsiteId] = new ShandadapandaDesiredItemState(flag3, item3.WebsiteId, item3.Name, "Cosmetic", "storeCustomizationId=" + storeCustomizationItemId); num3++; if (flag3) { num4++; } } int num9 = 0; int num10 = 0; foreach (KeyValuePair<int, ShandadapandaDesiredItemState> state in states) { if (s_shandadaWebsiteItems.TryGetValue(state.Key, out var value5) && IsStoreBoughtItemClass(value5.ItemClass)) { num9++; if (state.Value.Checked) { num10++; } } } sources.Add("Wardrobe:" + source + "(mapped=" + num2 + "/" + num + ",websiteOnly=" + num8 + "/" + num7 + ",store=" + num10 + "/" + num9 + ",storeExplicit=" + num4 + "/" + num3 + ",storeOverrides=" + num5 + ",storeUnmapped=" + num6 + ")"); LogDebug("Shandadapanda wardrobe ownership source=" + source + " mapped=" + num2 + "/" + num + " websiteOnly=" + num8 + "/" + num7 + " store=" + num10 + "/" + num9 + " storeExplicit=" + num4 + "/" + num3 + " storeOverrides=" + num5 + " storeUnmapped=" + num6); } private static bool TryCollectWardrobeUnlockedItems(out HashSet<int> unlockedItemIds, out string source) { unlockedItemIds = new HashSet<int>(); source = "{none}"; object customizationManager = GetCustomizationManager(); if (customizationManager == null) { source = "CustomizationManager unavailable"; return false; } if (!TryCollectUnlockedItemIds(Il2CppReflection.GetFieldOrProp(customizationManager, "UnlockedItems"), unlockedItemIds, out var listCount)) { source = "CustomizationManager.UnlockedItems unavailable"; return false; } source = "CustomizationManager.UnlockedItems(" + unlockedItemIds.Count + "/" + listCount + ")"; return true; } private static void LogShandadapandaOwnershipReadinessProbe(string source) { if (IsDebugLoggingEnabled()) { object customizationManager = GetCustomizationManager(); HashSet<int> unlockedItemIds; string source2; bool flag = TryCollectWardrobeUnlockedItems(out unlockedItemIds, out source2); int flags; string source3; bool flag2 = TryGetUnlockedMazeTypeFlags(out flags, out source3); bool flag3 = s_shandadaItemsLoaded && s_shandadaItems.Count > 0 && s_shandadaWebsiteItems.Count > 0; LogDebug("Shandadapanda ownership readiness source=" + source + " wardrobeReady=" + flag + " wardrobe=" + source2 + " unlockedItems=" + unlockedItemIds.Count.ToString(CultureInfo.InvariantCulture) + " manager=" + DescribeRuntimeObject(customizationManager) + " mappedRowsReady=" + flag3 + " gameRows=" + s_shandadaItems.Count.ToString(CultureInfo.InvariantCulture) + " websiteRows=" + s_shandadaWebsiteItems.Count.ToString(CultureInfo.InvariantCulture) + " blueprintReady=" + flag2 + " blueprint=" + source3 + " flags=" + flags.ToString(CultureInfo.InvariantCulture) + " names=" + FormatMazeFlags(flags)); } } private static bool TryCollectUnlockedItemIds(object list, HashSet<int> itemIds, out int listCount) { listCount = 0; if (list == null || itemIds == null) { return false; } bool flag = ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(list, "Count"), out listCount) || ValueConversion.TryConvertInt(Il2CppReflection.GetFieldOrProp(list, "Length"), out listCount); int num = 0; foreach (object item in EnumerateItems(list)) { num++; if (ValueConversion.TryConvertInt(item, out var result) && result >= 0 && result <= 65535) { itemIds.Add(result); } } if (!flag) { listCount = num; } if (num > 0 && itemIds.Count == 0) { return false; } if (!flag) { return num > 0; } return true; } private static Dictionary<int, int> GetExplicitWardrobeOnlyWebsiteItemMatches() { Dictionary<int, int> dictionary = new Dictionary<int, int>(); foreach (KeyValuePair<int, int> item in s_shandadapandaWardrobeOnlyGameIdsByWebsiteId) { dictionary[item.Key] = item.Value; } if (dictionary.Count > 0) { string text = string.Join(", ", from pair in dictionary.OrderBy((KeyValuePair<int, int> pair) => pair.Key).Take(12) select pair.Key + "->" + pair.Value); LogDebug("Shandadapanda explicit website-only item matches count=" + dictionary.Count + " sample=" + text); } return dictionary; } private static void AddSaveBackedShandadapandaOwnershipStates(Dictionary<int, ShandadapandaDesiredItemState> states, List<string> sources) { if (TryGetUnlockedMazeTypeFlags(out var flags, out var source)) { int num = 0; int num2 = 0; foreach (ShandadapandaSaveBackedItem item in s_shandadapandaSaveBackedItems.Where((ShandadapandaSaveBackedItem item) => item.IsBlueprint)) { bool flag = (flags & item.MazeFlag) == item.MazeFlag; states[item.WebsiteId] = new ShandadapandaDesiredItemState(flag, item.WebsiteId, item.Name, item.Category, item.SaveKey); num++; if (flag) { num2++; } } sources.Add("Blueprints:" + source + "(" + num2 + "/" + num + ")"); LogDebug("Shandadapanda blueprint ownership source=" + source + " flags=" + flags + " unlocked=" + num2 + "/" + num); } else { LogDebug("Shandadapanda blueprint ownership unavailable: " + source); } } private static bool TryGetUnlockedMazeTypeFlags(out int flags, out string source)