using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ValheimTwitchArmoury")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+81a4d028dbd57ec84942ae591bdf7cc725b81313")] [assembly: AssemblyProduct("ValheimTwitchArmoury")] [assembly: AssemblyTitle("ValheimTwitchArmoury")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.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; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimTwitchArmoury { internal sealed class ArmouryExporter { private static readonly FieldInfo HelmetField = GetHumanoidField("m_helmetItem"); private static readonly FieldInfo ChestField = GetHumanoidField("m_chestItem"); private static readonly FieldInfo LegsField = GetHumanoidField("m_legItem"); private static readonly FieldInfo CapeField = GetHumanoidField("m_shoulderItem"); private static readonly FieldInfo MainHandField = GetHumanoidField("m_rightItem"); private static readonly FieldInfo OffHandField = GetHumanoidField("m_leftItem"); private readonly string _iconDirectory; public ArmouryExporter(string iconDirectory) { _iconDirectory = iconDirectory; } public ArmourySnapshot CreateSnapshot(Player player) { EquipmentSnapshot equipment = new EquipmentSnapshot { Helmet = ToItem("helmet", GetItem((Humanoid)(object)player, HelmetField)), Chest = ToItem("chest", GetItem((Humanoid)(object)player, ChestField)), Legs = ToItem("legs", GetItem((Humanoid)(object)player, LegsField)), Cape = ToItem("cape", GetItem((Humanoid)(object)player, CapeField)), MainHand = ToItem("mainHand", GetItem((Humanoid)(object)player, MainHandField)), OffHand = ToItem("offHand", GetItem((Humanoid)(object)player, OffHandField)) }; return new ArmourySnapshot { CharacterName = SafeText(player.GetPlayerName()), WorldName = GetWorldName(), UpdatedAt = DateTime.UtcNow.ToString("O"), TotalArmor = SumArmor(equipment), Equipment = equipment, InventoryEquipment = GetInventoryEquipment(player, equipment), Skills = GetSkills(player) }; } public EquipmentIconExportResult ExportAllEquipmentIcons() { HashSet seen = new HashSet(StringComparer.Ordinal); EquipmentIconExportResult result = new EquipmentIconExportResult(); ExportEquipmentIcons(ObjectDB.instance?.m_items, seen, result); ExportEquipmentIcons(ZNetScene.instance?.m_prefabs, seen, result); return result; } private void ExportEquipmentIcons(IEnumerable? prefabs, HashSet seen, EquipmentIconExportResult result) { if (prefabs == null) { return; } foreach (GameObject prefab in prefabs) { result.PrefabsScanned++; try { if ((Object)(object)prefab == (Object)null || !seen.Add(((Object)prefab).name)) { continue; } ItemData val = prefab.GetComponent()?.m_itemData; if (val?.m_shared == null) { continue; } result.ItemDropsScanned++; if (IsEquipmentLike(val)) { result.EquipmentItemsFound++; string internalName = SafeText(((Object)(object)val.m_dropPrefab != (Object)null) ? ((Object)val.m_dropPrefab).name : ((Object)prefab).name); if (!string.IsNullOrWhiteSpace(ExportIcon(val, internalName))) { result.IconsAvailable++; } } } catch { result.FailedItems++; } } } private static string GetWorldName() { ZNet instance = ZNet.instance; return SafeText(((instance != null) ? instance.GetWorldName() : null) ?? ""); } private List GetInventoryEquipment(Player player, EquipmentSnapshot equipment) { List list = new List(); Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory == null) { return list; } foreach (ItemData allItem in inventory.GetAllItems()) { if (!IsEquippedItem(allItem, equipment) && IsEquipmentLike(allItem)) { ArmouryItemSnapshot armouryItemSnapshot = ToItem("inventory", allItem); if (armouryItemSnapshot != null) { list.Add(armouryItemSnapshot); } } } return list; } private static List GetSkills(Player player) { List list = new List(); if (!(GetFieldValue(((Character)player).GetSkills(), "m_skillData") is IDictionary dictionary)) { return list; } foreach (object key in dictionary.Keys) { object obj = dictionary[key]; string text = key?.ToString() ?? ""; if (obj != null && !string.IsNullOrEmpty(text) && !(text == "None") && !(text == "All")) { int val = (int)GetFloatField(obj, "m_level"); int val2 = (int)Math.Round(InvokeFloat(obj, "GetLevelPercentage") * 100f); list.Add(new SkillSnapshot { Skill = text, Level = Math.Max(0, val), Progress = Math.Max(0, Math.Min(100, val2)) }); } } return list; } private static bool IsEquippedItem(ItemData item, EquipmentSnapshot equipment) { if (!IsSameItem(item, equipment.Helmet) && !IsSameItem(item, equipment.Chest) && !IsSameItem(item, equipment.Legs) && !IsSameItem(item, equipment.Cape) && !IsSameItem(item, equipment.MainHand)) { return IsSameItem(item, equipment.OffHand); } return true; } private static bool IsSameItem(ItemData item, ArmouryItemSnapshot? snapshot) { if (snapshot == null) { return false; } object obj = item.m_shared?.m_name; if (obj == null) { GameObject dropPrefab = item.m_dropPrefab; obj = ((dropPrefab != null) ? ((Object)dropPrefab).name : null) ?? ""; } string text = (string)obj; if (SafeText(((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : text) == snapshot.InternalName && item.m_quality == snapshot.Quality) { return Math.Abs(item.m_durability - snapshot.Durability) < 0.001f; } return false; } private static bool IsEquipmentLike(ItemData item) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Expected I4, but got Unknown if (item.m_shared == null) { return false; } ItemType itemType = item.m_shared.m_itemType; switch (itemType - 3) { case 0: case 1: case 2: case 3: case 4: case 8: case 11: case 12: case 14: case 15: case 16: case 19: return true; default: return false; } } private static float SumArmor(EquipmentSnapshot equipment) { return (equipment.Helmet?.Armor ?? 0f) + (equipment.Chest?.Armor ?? 0f) + (equipment.Legs?.Armor ?? 0f) + (equipment.Cape?.Armor ?? 0f); } private static FieldInfo GetHumanoidField(string name) { return typeof(Humanoid).GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeof(Humanoid).FullName, name); } private static ItemData? GetItem(Humanoid humanoid, FieldInfo field) { object? value = field.GetValue(humanoid); return (ItemData?)((value is ItemData) ? value : null); } private ArmouryItemSnapshot? ToItem(string slot, ItemData? item) { if (item == null) { return null; } float maxDurability = item.GetMaxDurability(); float durabilityPercent = ((maxDurability > 0f) ? (item.m_durability / maxDurability) : 0f); object obj = item.m_shared?.m_name; if (obj == null) { GameObject dropPrefab = item.m_dropPrefab; obj = ((dropPrefab != null) ? ((Object)dropPrefab).name : null) ?? "Unknown"; } string text = (string)obj; string internalName = SafeText(((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : text); object shared = item.m_shared; var (equipEffectName, equipEffectText) = GetEquipEffect(shared, item.m_quality); var (setName, setSize, setEffectText) = GetSetEffect(shared, item.m_quality); return new ArmouryItemSnapshot { Slot = slot, Name = GetDisplayName(text), InternalName = internalName, IconPath = ExportIcon(item, internalName), Description = GetDisplayName(GetStringField(shared, "m_description")), CraftedBy = SafeText(GetStringField(item, "m_crafterName")), Quality = item.m_quality, Durability = item.m_durability, MaxDurability = maxDurability, DurabilityPercent = durabilityPercent, Armor = GetArmorValue(item), ItemType = ((item.m_shared != null) ? ((object)Unsafe.As(ref item.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString() : ""), Weight = (item.m_shared?.m_weight ?? 0f), RepairStationLevel = GetRepairStationLevel(shared, item), UseStamina = GetFloatField(shared, "m_attackStamina"), BlockArmor = GetScaledFloat(shared, "m_blockPower", "m_blockPowerPerLevel", item.m_quality), BlockForce = GetScaledFloat(shared, "m_deflectionForce", "m_deflectionForcePerLevel", item.m_quality), ParryBonus = GetFloatField(shared, "m_timedBlockBonus"), Knockback = GetFloatField(shared, "m_attackForce"), BackstabBonus = GetFloatField(shared, "m_backstabBonus"), MovementModifier = GetFloatField(shared, "m_movementModifier"), AttackStaminaModifier = GetFloatField(shared, "m_attackStaminaModifier"), BlockStaminaModifier = GetFloatField(shared, "m_blockStaminaModifier"), EquipEffectName = equipEffectName, EquipEffectText = equipEffectText, SetName = setName, SetSize = setSize, SetEffectText = setEffectText, DamageModifiers = GetDamageModifiers(shared), Damages = GetDamageStats(shared, item.m_quality) }; } private static List GetDamageModifiers(object? shared) { List list = new List(); if (!(GetFieldValue(shared, "m_damageModifiers") is IEnumerable enumerable)) { return list; } foreach (object item in enumerable) { if (item != null) { string text = SafeText(GetFieldValue(item, "m_type")?.ToString()); string text2 = SafeText(GetFieldValue(item, "m_modifier")?.ToString()); if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2) && text2 != "Normal") { list.Add(new DamageModifierSnapshot { Type = text, Modifier = text2 }); } } } return list; } private static List GetDamageStats(object? shared, int quality) { object fieldValue = GetFieldValue(shared, "m_damages"); object fieldValue2 = GetFieldValue(shared, "m_damagesPerLevel"); List list = new List(); AddDamage(list, "Blunt", fieldValue, fieldValue2, "m_blunt", quality); AddDamage(list, "Slash", fieldValue, fieldValue2, "m_slash", quality); AddDamage(list, "Pierce", fieldValue, fieldValue2, "m_pierce", quality); AddDamage(list, "Fire", fieldValue, fieldValue2, "m_fire", quality); AddDamage(list, "Frost", fieldValue, fieldValue2, "m_frost", quality); AddDamage(list, "Lightning", fieldValue, fieldValue2, "m_lightning", quality); AddDamage(list, "Poison", fieldValue, fieldValue2, "m_poison", quality); AddDamage(list, "Spirit", fieldValue, fieldValue2, "m_spirit", quality); return list; } private static (string Name, string Text) GetEquipEffect(object? shared, int quality) { return GetStatusEffectInfo(GetFieldValue(shared, "m_equipStatusEffect"), quality); } private static (string Name, int Size, string Text) GetSetEffect(object? shared, int quality) { object? fieldValue = GetFieldValue(shared, "m_setStatusEffect"); (string Name, string Text) statusEffectInfo = GetStatusEffectInfo(fieldValue, quality); string item = statusEffectInfo.Name; string item2 = statusEffectInfo.Text; int item3 = ((fieldValue != null) ? GetIntField(shared, "m_setSize") : 0); return (Name: item, Size: item3, Text: item2); } private static (string Name, string Text) GetStatusEffectInfo(object? statusEffect, int quality) { if (statusEffect == null) { return (Name: "", Text: ""); } string displayName = GetDisplayName(GetStringField(statusEffect, "m_name")); string text = StripColorTags(Localize(InvokeTooltipString(statusEffect, quality))).Trim(); string text2 = StripColorTags(Localize(GetStringField(statusEffect, "m_tooltip"))).Trim(); string item = (string.IsNullOrEmpty(text) ? text2 : ((string.IsNullOrEmpty(text2) || text.Contains(text2)) ? text : (text2 + "\n" + text))); return (Name: displayName, Text: item); } private static int GetRepairStationLevel(object? shared, ItemData item) { try { ObjectDB instance = ObjectDB.instance; Recipe val = (((Object)(object)instance != (Object)null) ? instance.GetRecipe(item) : null); if ((Object)(object)val != (Object)null) { int intField = GetIntField(val, "m_minStationLevel"); if (intField > 0) { return intField; } } } catch { } return GetIntField(shared, "m_minStationLevel"); } private static string InvokeTooltipString(object statusEffect, int quality) { try { MethodInfo method = statusEffect.GetType().GetMethod("GetTooltipString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null) { return ""; } object[] parameters = ((method.GetParameters().Length == 0) ? new object[0] : new object[1] { quality }); return (method.Invoke(statusEffect, parameters) as string) ?? ""; } catch { return ""; } } private static string Localize(string value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } if (Localization.instance == null) { return value; } return Localization.instance.Localize(value); } private static string StripColorTags(string value) { return Regex.Replace(value, "]*>", "", RegexOptions.IgnoreCase); } private static float GetArmorValue(ItemData item) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Invalid comparison between Unknown and I4 //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Invalid comparison between Unknown and I4 if (item.m_shared == null) { return 0f; } ItemType itemType = item.m_shared.m_itemType; if (itemType - 6 <= 1 || (int)itemType == 11 || (int)itemType == 17) { return item.GetArmor(); } return 0f; } private static void AddDamage(List stats, string label, object? damages, object? perLevel, string fieldName, int quality) { float num = GetFloatField(damages, fieldName) + GetFloatField(perLevel, fieldName) * (float)Math.Max(0, quality - 1); if (num > 0.001f) { stats.Add(new DamageStatSnapshot { Type = label, Value = num }); } } private static float GetScaledFloat(object? source, string baseField, string perLevelField, int quality) { return GetFloatField(source, baseField) + GetFloatField(source, perLevelField) * (float)Math.Max(0, quality - 1); } private static string GetStringField(object? source, string fieldName) { return (GetFieldValue(source, fieldName) as string) ?? ""; } private static int GetIntField(object? source, string fieldName) { object fieldValue = GetFieldValue(source, fieldName); if (fieldValue is int) { return (int)fieldValue; } return 0; } private static float GetFloatField(object? source, string fieldName) { object fieldValue = GetFieldValue(source, fieldName); if (fieldValue is float) { return (float)fieldValue; } return 0f; } private static object? GetFieldValue(object? source, string fieldName) { return source?.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(source); } private static float InvokeFloat(object? source, string methodName) { try { return ((source?.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null))?.Invoke(source, null) is float num) ? num : 0f; } catch { return 0f; } } private string ExportIcon(ItemData item, string internalName) { Sprite icon = item.GetIcon(); if ((Object)(object)icon == (Object)null) { return ""; } string text = SanitizeFileName(internalName) + ".png"; string path = Path.Combine(_iconDirectory, text); if (File.Exists(path)) { return "icons/" + text; } Directory.CreateDirectory(_iconDirectory); Texture2D obj = CopySpriteTexture(icon); byte[] bytes = ImageConversion.EncodeToPNG(obj); Object.Destroy((Object)(object)obj); File.WriteAllBytes(path, bytes); return "icons/" + text; } private static Texture2D CopySpriteTexture(Sprite sprite) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown Rect textureRect = sprite.textureRect; Texture2D texture = sprite.texture; Texture2D val = new Texture2D((int)((Rect)(ref textureRect)).width, (int)((Rect)(ref textureRect)).height, (TextureFormat)4, false); RenderTexture active = RenderTexture.active; RenderTexture temporary = RenderTexture.GetTemporary(((Texture)texture).width, ((Texture)texture).height, 0, (RenderTextureFormat)7, (RenderTextureReadWrite)1); Graphics.Blit((Texture)(object)texture, temporary); RenderTexture.active = temporary; val.ReadPixels(textureRect, 0, 0); val.Apply(); RenderTexture.active = active; RenderTexture.ReleaseTemporary(temporary); return val; } private static string SanitizeFileName(string value) { string text = Regex.Replace(value, "[^A-Za-z0-9_.-]+", "_"); if (!string.IsNullOrWhiteSpace(text)) { return text; } return "item"; } private static string GetDisplayName(string value) { if (string.IsNullOrWhiteSpace(value)) { return value; } string text = ((Localization.instance != null) ? Localization.instance.Localize(value) : value); if (!string.IsNullOrWhiteSpace(text) && !text.StartsWith("$", StringComparison.Ordinal)) { return text; } string input = (value.StartsWith("$", StringComparison.Ordinal) ? value.Substring(1) : value); input = Regex.Replace(input, "^(item|piece|enemy|skill|se)_", "", RegexOptions.IgnoreCase); input = input.Replace("_", " "); return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(input); } private static string SafeText(string? value) { if (!string.IsNullOrWhiteSpace(value)) { return value; } return ""; } } internal sealed class EquipmentIconExportResult { public int PrefabsScanned { get; set; } public int ItemDropsScanned { get; set; } public int EquipmentItemsFound { get; set; } public int IconsAvailable { get; set; } public int FailedItems { get; set; } } internal sealed class ArmourySnapshot { public string CharacterName { get; set; } = ""; public string WorldName { get; set; } = ""; public string UpdatedAt { get; set; } = ""; public float TotalArmor { get; set; } public EquipmentSnapshot Equipment { get; set; } = new EquipmentSnapshot(); public List InventoryEquipment { get; set; } = new List(); public List Skills { get; set; } = new List(); public string ToJson() { StringBuilder stringBuilder = new StringBuilder(2048); stringBuilder.AppendLine("{"); JsonProp(stringBuilder, "characterName", CharacterName, 1, comma: true); JsonProp(stringBuilder, "worldName", WorldName, 1, comma: true); JsonProp(stringBuilder, "updatedAt", UpdatedAt, 1, comma: true); JsonProp(stringBuilder, "totalArmor", TotalArmor, 1, comma: true); stringBuilder.AppendLine(" \"equipment\": {"); JsonItem(stringBuilder, "helmet", Equipment.Helmet, comma: true); JsonItem(stringBuilder, "chest", Equipment.Chest, comma: true); JsonItem(stringBuilder, "legs", Equipment.Legs, comma: true); JsonItem(stringBuilder, "cape", Equipment.Cape, comma: true); JsonItem(stringBuilder, "mainHand", Equipment.MainHand, comma: true); JsonItem(stringBuilder, "offHand", Equipment.OffHand, comma: false); stringBuilder.AppendLine(" },"); stringBuilder.AppendLine(" \"inventoryEquipment\": ["); for (int i = 0; i < InventoryEquipment.Count; i++) { JsonItemObject(stringBuilder, InventoryEquipment[i], 2, i < InventoryEquipment.Count - 1); } stringBuilder.AppendLine(" ],"); stringBuilder.AppendLine(" \"skills\": ["); for (int j = 0; j < Skills.Count; j++) { JsonSkill(stringBuilder, Skills[j], j < Skills.Count - 1); } stringBuilder.AppendLine(" ]"); stringBuilder.AppendLine("}"); return stringBuilder.ToString(); } public string EquipmentKey() { StringBuilder stringBuilder = new StringBuilder(512); AppendItemKey(stringBuilder, Equipment.Helmet); AppendItemKey(stringBuilder, Equipment.Chest); AppendItemKey(stringBuilder, Equipment.Legs); AppendItemKey(stringBuilder, Equipment.Cape); AppendItemKey(stringBuilder, Equipment.MainHand); AppendItemKey(stringBuilder, Equipment.OffHand); return stringBuilder.ToString(); } private static void AppendItemKey(StringBuilder key, ArmouryItemSnapshot? item) { if (item == null) { key.Append("empty;"); } else { key.Append(item.Slot).Append('|').Append(item.InternalName) .Append('|') .Append(item.Quality) .Append('|') .Append(item.Armor.ToString("0.###", CultureInfo.InvariantCulture)) .Append(';'); } } private static void JsonItem(StringBuilder json, string key, ArmouryItemSnapshot? item, bool comma) { json.Append(" \"").Append(key).Append("\": "); if (item == null) { json.Append("null"); json.AppendLine(comma ? "," : ""); return; } json.AppendLine("{"); JsonItemProperties(json, item, 3); json.Append(" }"); json.AppendLine(comma ? "," : ""); } private static void JsonItemObject(StringBuilder json, ArmouryItemSnapshot item, int indent, bool comma) { json.Append(' ', indent * 2).AppendLine("{"); JsonItemProperties(json, item, indent + 1); json.Append(' ', indent * 2).AppendLine(comma ? "}," : "}"); } private static void JsonItemProperties(StringBuilder json, ArmouryItemSnapshot item, int indent) { JsonProp(json, "slot", item.Slot, indent, comma: true); JsonProp(json, "name", item.Name, indent, comma: true); JsonProp(json, "internalName", item.InternalName, indent, comma: true); JsonProp(json, "iconPath", item.IconPath, indent, comma: true); JsonProp(json, "itemType", item.ItemType, indent, comma: true); JsonProp(json, "description", item.Description, indent, comma: true); JsonProp(json, "craftedBy", item.CraftedBy, indent, comma: true); JsonProp(json, "quality", item.Quality, indent, comma: true); JsonProp(json, "durability", item.Durability, indent, comma: true); JsonProp(json, "maxDurability", item.MaxDurability, indent, comma: true); JsonProp(json, "durabilityPercent", item.DurabilityPercent, indent, comma: true); JsonProp(json, "armor", item.Armor, indent, comma: true); JsonProp(json, "weight", item.Weight, indent, comma: true); JsonProp(json, "repairStationLevel", item.RepairStationLevel, indent, comma: true); JsonProp(json, "useStamina", item.UseStamina, indent, comma: true); JsonProp(json, "blockArmor", item.BlockArmor, indent, comma: true); JsonProp(json, "blockForce", item.BlockForce, indent, comma: true); JsonProp(json, "parryBonus", item.ParryBonus, indent, comma: true); JsonProp(json, "knockback", item.Knockback, indent, comma: true); JsonProp(json, "backstabBonus", item.BackstabBonus, indent, comma: true); JsonProp(json, "movementModifier", item.MovementModifier, indent, comma: true); JsonProp(json, "attackStaminaModifier", item.AttackStaminaModifier, indent, comma: true); JsonProp(json, "blockStaminaModifier", item.BlockStaminaModifier, indent, comma: true); JsonProp(json, "equipEffectName", item.EquipEffectName, indent, comma: true); JsonProp(json, "equipEffectText", item.EquipEffectText, indent, comma: true); JsonProp(json, "setName", item.SetName, indent, comma: true); JsonProp(json, "setSize", item.SetSize, indent, comma: true); JsonProp(json, "setEffectText", item.SetEffectText, indent, comma: true); JsonDamageModifiers(json, item.DamageModifiers, indent, comma: true); JsonDamageStats(json, item.Damages, indent, comma: false); } private static void JsonDamageModifiers(StringBuilder json, List modifiers, int indent, bool comma) { json.Append(' ', indent * 2).AppendLine("\"damageModifiers\": ["); for (int i = 0; i < modifiers.Count; i++) { DamageModifierSnapshot damageModifierSnapshot = modifiers[i]; json.Append(' ', (indent + 1) * 2).AppendLine("{"); JsonProp(json, "type", damageModifierSnapshot.Type, indent + 2, comma: true); JsonProp(json, "modifier", damageModifierSnapshot.Modifier, indent + 2, comma: false); json.Append(' ', (indent + 1) * 2).AppendLine((i < modifiers.Count - 1) ? "}," : "}"); } json.Append(' ', indent * 2).AppendLine(comma ? "]," : "]"); } private static void JsonDamageStats(StringBuilder json, List damages, int indent, bool comma) { json.Append(' ', indent * 2).AppendLine("\"damages\": ["); for (int i = 0; i < damages.Count; i++) { DamageStatSnapshot damageStatSnapshot = damages[i]; json.Append(' ', (indent + 1) * 2).AppendLine("{"); JsonProp(json, "type", damageStatSnapshot.Type, indent + 2, comma: true); JsonProp(json, "value", damageStatSnapshot.Value, indent + 2, comma: false); json.Append(' ', (indent + 1) * 2).AppendLine((i < damages.Count - 1) ? "}," : "}"); } json.Append(' ', indent * 2).AppendLine(comma ? "]," : "]"); } private static void JsonSkill(StringBuilder json, SkillSnapshot skill, bool comma) { json.AppendLine(" {"); JsonProp(json, "skill", skill.Skill, 3, comma: true); JsonProp(json, "level", skill.Level, 3, comma: true); JsonProp(json, "progress", skill.Progress, 3, comma: false); json.AppendLine(comma ? " }," : " }"); } private static void JsonProp(StringBuilder json, string key, string value, int indent, bool comma) { json.Append(' ', indent * 2).Append('"').Append(key) .Append("\": \"") .Append(Escape(value)) .Append('"') .AppendLine(comma ? "," : ""); } private static void JsonProp(StringBuilder json, string key, int value, int indent, bool comma) { json.Append(' ', indent * 2).Append('"').Append(key) .Append("\": ") .Append(value.ToString(CultureInfo.InvariantCulture)) .AppendLine(comma ? "," : ""); } private static void JsonProp(StringBuilder json, string key, float value, int indent, bool comma) { json.Append(' ', indent * 2).Append('"').Append(key) .Append("\": ") .Append(value.ToString("0.###", CultureInfo.InvariantCulture)) .AppendLine(comma ? "," : ""); } private static string Escape(string value) { return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("\t", "\\t"); } } internal sealed class EquipmentSnapshot { public ArmouryItemSnapshot? Helmet { get; set; } public ArmouryItemSnapshot? Chest { get; set; } public ArmouryItemSnapshot? Legs { get; set; } public ArmouryItemSnapshot? Cape { get; set; } public ArmouryItemSnapshot? MainHand { get; set; } public ArmouryItemSnapshot? OffHand { get; set; } } internal sealed class ArmouryItemSnapshot { public string Slot { get; set; } = ""; public string Name { get; set; } = ""; public string InternalName { get; set; } = ""; public string IconPath { get; set; } = ""; public string ItemType { get; set; } = ""; public string Description { get; set; } = ""; public string CraftedBy { get; set; } = ""; public int Quality { get; set; } public float Durability { get; set; } public float MaxDurability { get; set; } public float DurabilityPercent { get; set; } public float Armor { get; set; } public float Weight { get; set; } public int RepairStationLevel { get; set; } public float UseStamina { get; set; } public float BlockArmor { get; set; } public float BlockForce { get; set; } public float ParryBonus { get; set; } public float Knockback { get; set; } public float BackstabBonus { get; set; } public float MovementModifier { get; set; } public float AttackStaminaModifier { get; set; } public float BlockStaminaModifier { get; set; } public string EquipEffectName { get; set; } = ""; public string EquipEffectText { get; set; } = ""; public string SetName { get; set; } = ""; public int SetSize { get; set; } public string SetEffectText { get; set; } = ""; public List DamageModifiers { get; set; } = new List(); public List Damages { get; set; } = new List(); } internal sealed class SkillSnapshot { public string Skill { get; set; } = ""; public int Level { get; set; } public int Progress { get; set; } } internal sealed class DamageModifierSnapshot { public string Type { get; set; } = ""; public string Modifier { get; set; } = ""; } internal sealed class DamageStatSnapshot { public string Type { get; set; } = ""; public float Value { get; set; } } internal sealed class ArmouryUploader { private readonly ManualLogSource _log; private readonly Func _backendUrl; private readonly Func _uploadToken; private readonly HttpClient _http = new HttpClient(); private string _lastUploadedJson = ""; private string _lastUploadedEquipmentKey = ""; private DateTime _nextPeriodicUploadUtc = DateTime.MinValue; private bool _isUploading; public ArmouryUploader(ManualLogSource log, Func backendUrl, Func uploadToken) { _log = log; _backendUrl = backendUrl; _uploadToken = uploadToken; } public void UploadIfDue(ArmourySnapshot snapshot, TimeSpan periodicInterval) { string token = _uploadToken(); if (string.IsNullOrWhiteSpace(token) || _isUploading) { return; } string json = snapshot.ToJson(); string equipmentKey = snapshot.EquipmentKey(); bool num = equipmentKey != _lastUploadedEquipmentKey; bool flag = DateTime.UtcNow >= _nextPeriodicUploadUtc; if ((!num && !flag) || (json == _lastUploadedJson && !flag)) { return; } _isUploading = true; Task.Run(async delegate { _ = 1; try { string requestUri = CombineUrl(_backendUrl(), "/api/armoury"); using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); using HttpResponseMessage response = await _http.SendAsync(request).ConfigureAwait(continueOnCapturedContext: false); string arg = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false); if (response.IsSuccessStatusCode) { _lastUploadedJson = json; _lastUploadedEquipmentKey = equipmentKey; _nextPeriodicUploadUtc = DateTime.UtcNow + periodicInterval; } else { _log.LogWarning((object)$"Armoury upload failed: {(int)response.StatusCode} {arg}"); } } catch (Exception ex) { _log.LogWarning((object)("Armoury upload failed: " + ex.Message)); } finally { _isUploading = false; } }); } private static string CombineUrl(string baseUrl, string path) { return baseUrl.TrimEnd('/') + path; } } internal sealed class OverlayWriter { private readonly ManualLogSource _log; private readonly string _outputPath; private string _lastJson = ""; public OverlayWriter(ManualLogSource log, string outputPath) { _log = log; _outputPath = outputPath; } public void WriteIfChanged(ArmourySnapshot snapshot) { string text = snapshot.ToJson(); if (!(text == _lastJson)) { string directoryName = Path.GetDirectoryName(_outputPath); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } string text2 = _outputPath + ".tmp"; File.WriteAllText(text2, text); if (File.Exists(_outputPath)) { File.Delete(_outputPath); } File.Move(text2, _outputPath); _lastJson = text; _log.LogDebug((object)("Wrote armoury overlay data to " + _outputPath)); } } } [BepInPlugin("com.valheimarmoury.twitch", "Valheim Twitch Armoury", "0.1.3")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.valheimarmoury.twitch"; public const string PluginName = "Valheim Twitch Armoury"; public const string PluginVersion = "0.1.3"; private ConfigEntry _exportEnabled; private ConfigEntry _outputPath; private ConfigEntry _exportIntervalSeconds; private ConfigEntry _syncEnabled; private ConfigEntry _syncPeriodicIntervalSeconds; private ConfigEntry _backendUrl; private ConfigEntry _twitchClientId; private ConfigEntry _oauthRedirectPort; private ConfigEntry _uploadToken; private ConfigEntry _channelId; private ConfigEntry _twitchLogin; private ConfigEntry _loginShortcut; private ConfigEntry _autoExportAllIcons; private ConfigEntry _exportAllIconsShortcut; private ArmouryExporter _exporter; private OverlayWriter _writer; private ArmouryUploader _uploader; private TwitchOAuthLogin _oauthLogin; private float _nextExportAt; private bool _exportedAllIcons; public static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } public bool IsTwitchAuthenticated => !string.IsNullOrWhiteSpace(_uploadToken?.Value); public string TwitchButtonText { get { if (!IsTwitchAuthenticated) { return "Armoury Login"; } return "Armoury: " + _twitchLogin.Value; } } private void Awake() { //IL_0205: Unknown result type (might be due to invalid IL or missing references) //IL_0255: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; string text = Path.Combine(Paths.PluginPath, "ValheimTwitchArmoury", "overlay", "data", "armoury.json"); string text2 = Path.Combine(Paths.PluginPath, "ValheimTwitchArmoury", "overlay", "icons"); _exportEnabled = ((BaseUnityPlugin)this).Config.Bind("Overlay", "ExportEnabled", true, "Write current character gear to the overlay JSON file."); _outputPath = ((BaseUnityPlugin)this).Config.Bind("Overlay", "OutputPath", text, "Path to the generated armoury JSON file."); _exportIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind("Overlay", "ExportIntervalSeconds", 1f, "How often to refresh overlay data while in-game."); _syncEnabled = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "Enabled", false, "Upload armoury data to the Cloudflare Worker backend."); _syncPeriodicIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "PeriodicUploadIntervalSeconds", 60f, "Upload even without equipment changes after this many seconds."); _backendUrl = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "BackendUrl", "https://valheim-twitch-armoury.sbraeunlein.workers.dev", "Cloudflare Worker backend URL."); _twitchClientId = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "TwitchClientId", "21d5enqxycndhl70g0ea54ynizodwh", "Twitch application client ID."); _oauthRedirectPort = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "OAuthRedirectPort", 8718, "Local OAuth callback port. Twitch app redirect URL must match this."); _uploadToken = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "UploadToken", "", "Generated after Twitch authorization. Keep private."); _channelId = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "ChannelId", "", "Twitch channel ID generated after authorization."); _twitchLogin = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "TwitchLogin", "", "Twitch login generated after authorization."); _loginShortcut = ((BaseUnityPlugin)this).Config.Bind("Twitch Sync", "LoginShortcut", new KeyboardShortcut((KeyCode)289, Array.Empty()), "Keyboard shortcut to start Twitch authorization."); _autoExportAllIcons = ((BaseUnityPlugin)this).Config.Bind("Assets", "ExportAllEquipableIcons", false, "Export all equipable item icons once per game session when ObjectDB is loaded. Keep disabled for normal use."); _exportAllIconsShortcut = ((BaseUnityPlugin)this).Config.Bind("Assets", "ExportAllEquipableIconsShortcut", new KeyboardShortcut((KeyCode)290, Array.Empty()), "Keyboard shortcut to export all equipable item icons again."); if (string.IsNullOrWhiteSpace(_twitchClientId.Value)) { _twitchClientId.Value = "21d5enqxycndhl70g0ea54ynizodwh"; ((BaseUnityPlugin)this).Config.Save(); } _exporter = new ArmouryExporter(text2); _writer = new OverlayWriter(Log, _outputPath.Value); _uploader = new ArmouryUploader(Log, () => _backendUrl.Value, () => _uploadToken.Value); _oauthLogin = new TwitchOAuthLogin(Log, () => _backendUrl.Value, () => _twitchClientId.Value, () => _oauthRedirectPort.Value, OnTwitchAuthenticated); Log.LogInfo((object)"Valheim Twitch Armoury 0.1.3 loaded."); Log.LogInfo((object)("Armoury overlay data path: " + _outputPath.Value)); Log.LogInfo((object)("Armoury icon export path: " + text2)); } public void StartTwitchLogin() { _oauthLogin.Start(); } private void OnTwitchAuthenticated(string channelId, string login, string uploadToken) { _channelId.Value = channelId; _twitchLogin.Value = login; _uploadToken.Value = uploadToken; _syncEnabled.Value = true; ((BaseUnityPlugin)this).Config.Save(); } private void Update() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = _loginShortcut.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { StartTwitchLogin(); } if (_autoExportAllIcons.Value) { value = _exportAllIconsShortcut.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { ExportAllEquipableIcons(); } } if (_autoExportAllIcons.Value && !_exportedAllIcons && (Object)(object)ObjectDB.instance != (Object)null && ObjectDB.instance.m_items.Count > 0) { ExportAllEquipableIcons(); } if (!_exportEnabled.Value || Time.time < _nextExportAt) { return; } _nextExportAt = Time.time + Mathf.Max(0.25f, _exportIntervalSeconds.Value); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } try { ArmourySnapshot snapshot = _exporter.CreateSnapshot(localPlayer); _writer.WriteIfChanged(snapshot); if (_syncEnabled.Value) { _uploader.UploadIfDue(snapshot, TimeSpan.FromSeconds(Mathf.Max(5f, _syncPeriodicIntervalSeconds.Value))); } } catch (Exception ex) { Log.LogWarning((object)("Failed to write armoury overlay data: " + ex.Message)); } } private void ExportAllEquipableIcons() { try { EquipmentIconExportResult equipmentIconExportResult = _exporter.ExportAllEquipmentIcons(); _exportedAllIcons = equipmentIconExportResult.PrefabsScanned > 0; Log.LogInfo((object)("Equipable icon export scanned " + $"{equipmentIconExportResult.PrefabsScanned} prefabs, " + $"{equipmentIconExportResult.ItemDropsScanned} item drops, " + $"{equipmentIconExportResult.EquipmentItemsFound} equipable items, " + $"{equipmentIconExportResult.IconsAvailable} icons available, " + $"{equipmentIconExportResult.FailedItems} failed items.")); } catch (Exception ex) { Log.LogWarning((object)("Failed to export all equipable item icons: " + ex.Message)); } } private void OnGUI() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown if (ShouldShowLoginGui()) { Rect val = new Rect((float)(Screen.width - 260 - 24), 24f, 260f, 44f); GUI.depth = -100; GUIStyle val2 = new GUIStyle(GUI.skin.button) { fontSize = 16, fontStyle = (FontStyle)1 }; if (GUI.Button(val, TwitchButtonText + " (F8)", val2)) { StartTwitchLogin(); } } } private static bool ShouldShowLoginGui() { if ((Object)(object)Player.m_localPlayer == (Object)null) { return (Object)(object)Object.FindObjectOfType() != (Object)null; } return false; } } internal sealed class TwitchOAuthLogin { private readonly struct AuthResult { public string ChannelId { get; } public string Login { get; } public string UploadToken { get; } public AuthResult(string channelId, string login, string uploadToken) { ChannelId = channelId; Login = login; UploadToken = uploadToken; } } private readonly ManualLogSource _log; private readonly Func _backendUrl; private readonly Func _clientId; private readonly Func _redirectPort; private readonly Action _onAuthenticated; private readonly HttpClient _http = new HttpClient(); private bool _isRunning; public TwitchOAuthLogin(ManualLogSource log, Func backendUrl, Func clientId, Func redirectPort, Action onAuthenticated) { _log = log; _backendUrl = backendUrl; _clientId = clientId; _redirectPort = redirectPort; _onAuthenticated = onAuthenticated; } public void Start() { if (_isRunning) { return; } string clientId = _clientId(); if (string.IsNullOrWhiteSpace(clientId)) { _log.LogWarning((object)"TwitchClientId is empty. Add it to the BepInEx config before authorizing Twitch sync."); return; } _isRunning = true; Task.Run(() => RunAsync(clientId)); } private async Task RunAsync(string clientId) { HttpListener listener = null; try { int num = _redirectPort(); string redirectUri = $"http://localhost:{num}/callback"; string state = Guid.NewGuid().ToString("N"); string url = "https://id.twitch.tv/oauth2/authorize?client_id=" + Uri.EscapeDataString(clientId) + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&response_type=code&scope=" + Uri.EscapeDataString("user:read:email") + "&state=" + Uri.EscapeDataString(state); listener = new HttpListener(); listener.Prefixes.Add($"http://localhost:{num}/"); listener.Start(); OpenBrowser(url); _log.LogInfo((object)"Opened Twitch authorization in browser."); HttpListenerContext context = await listener.GetContextAsync().ConfigureAwait(continueOnCapturedContext: false); string error = context.Request.QueryString["error_description"] ?? context.Request.QueryString["error"]; string text = context.Request.QueryString["code"]; string text2 = context.Request.QueryString["state"]; if (!string.IsNullOrWhiteSpace(error)) { await SendHtml(context, "Authentication denied", "

Authentication denied

" + EscapeHtml(error) + "

").ConfigureAwait(continueOnCapturedContext: false); _log.LogWarning((object)("Twitch authorization denied: " + error)); return; } if (string.IsNullOrWhiteSpace(text) || text2 != state) { await SendHtml(context, "Authentication failed", "

Authentication failed

Invalid OAuth response.

").ConfigureAwait(continueOnCapturedContext: false); _log.LogWarning((object)"Twitch authorization failed: missing code or invalid state."); return; } AuthResult result = await ExchangeCode(text, redirectUri).ConfigureAwait(continueOnCapturedContext: false); _onAuthenticated(result.ChannelId, result.Login, result.UploadToken); await SendHtml(context, "Authentication successful", "

Authentication successful

You can close this tab and return to Valheim.

").ConfigureAwait(continueOnCapturedContext: false); _log.LogInfo((object)("Twitch authorization complete for " + result.Login + " (" + result.ChannelId + ").")); } catch (Exception ex) { _log.LogWarning((object)("Twitch authorization failed: " + ex.Message)); } finally { listener?.Close(); _isRunning = false; } } private async Task ExchangeCode(string code, string redirectUri) { string content = "{\"code\":\"" + JsonEscape(code) + "\",\"redirectUri\":\"" + JsonEscape(redirectUri) + "\"}"; using StringContent content2 = new StringContent(content, Encoding.UTF8, "application/json"); using HttpResponseMessage response = await _http.PostAsync(CombineUrl(_backendUrl(), "/oauth/exchange"), content2).ConfigureAwait(continueOnCapturedContext: false); string text = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false); if (!response.IsSuccessStatusCode) { throw new InvalidOperationException($"OAuth exchange failed: {(int)response.StatusCode} {text}"); } return new AuthResult(ExtractJsonString(text, "channelId"), ExtractJsonString(text, "login"), ExtractJsonString(text, "uploadToken")); } private static void OpenBrowser(string url) { Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); } private static async Task SendHtml(HttpListenerContext context, string title, string body) { byte[] bytes = Encoding.UTF8.GetBytes("" + EscapeHtml(title) + "" + body + ""); context.Response.StatusCode = 200; context.Response.ContentType = "text/html; charset=utf-8"; context.Response.ContentLength64 = bytes.Length; await context.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(continueOnCapturedContext: false); context.Response.Close(); } private static string ExtractJsonString(string json, string key) { Match match = Regex.Match(json, "\"" + Regex.Escape(key) + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\""); if (!match.Success) { throw new InvalidOperationException("Missing " + key + " in OAuth response."); } return Regex.Unescape(match.Groups[1].Value); } private static string CombineUrl(string baseUrl, string path) { return baseUrl.TrimEnd('/') + path; } private static string JsonEscape(string value) { return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n"); } private static string EscapeHtml(string value) { return value.Replace("&", "&").Replace("<", "<").Replace(">", ">") .Replace("\"", """); } } }