Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimTwitchArmoury v0.1.3
ValheimTwitchArmoury.dll
Decompiled 4 days agousing 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<string> seen = new HashSet<string>(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<GameObject>? prefabs, HashSet<string> 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<ItemDrop>()?.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<ArmouryItemSnapshot> GetInventoryEquipment(Player player, EquipmentSnapshot equipment) { List<ArmouryItemSnapshot> list = new List<ArmouryItemSnapshot>(); 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<SkillSnapshot> GetSkills(Player player) { List<SkillSnapshot> list = new List<SkillSnapshot>(); 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<ItemType, ItemType>(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<DamageModifierSnapshot> GetDamageModifiers(object? shared) { List<DamageModifierSnapshot> list = new List<DamageModifierSnapshot>(); 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<DamageStatSnapshot> GetDamageStats(object? shared, int quality) { object fieldValue = GetFieldValue(shared, "m_damages"); object fieldValue2 = GetFieldValue(shared, "m_damagesPerLevel"); List<DamageStatSnapshot> list = new List<DamageStatSnapshot>(); 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, "</?color[^>]*>", "", 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<DamageStatSnapshot> 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<ArmouryItemSnapshot> InventoryEquipment { get; set; } = new List<ArmouryItemSnapshot>(); public List<SkillSnapshot> Skills { get; set; } = new List<SkillSnapshot>(); 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<DamageModifierSnapshot> 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<DamageStatSnapshot> 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<DamageModifierSnapshot> DamageModifiers { get; set; } = new List<DamageModifierSnapshot>(); public List<DamageStatSnapshot> Damages { get; set; } = new List<DamageStatSnapshot>(); } 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<string> _backendUrl; private readonly Func<string> _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<string> backendUrl, Func<string> 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<bool> _exportEnabled; private ConfigEntry<string> _outputPath; private ConfigEntry<float> _exportIntervalSeconds; private ConfigEntry<bool> _syncEnabled; private ConfigEntry<float> _syncPeriodicIntervalSeconds; private ConfigEntry<string> _backendUrl; private ConfigEntry<string> _twitchClientId; private ConfigEntry<int> _oauthRedirectPort; private ConfigEntry<string> _uploadToken; private ConfigEntry<string> _channelId; private ConfigEntry<string> _twitchLogin; private ConfigEntry<KeyboardShortcut> _loginShortcut; private ConfigEntry<bool> _autoExportAllIcons; private ConfigEntry<KeyboardShortcut> _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<bool>("Overlay", "ExportEnabled", true, "Write current character gear to the overlay JSON file."); _outputPath = ((BaseUnityPlugin)this).Config.Bind<string>("Overlay", "OutputPath", text, "Path to the generated armoury JSON file."); _exportIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Overlay", "ExportIntervalSeconds", 1f, "How often to refresh overlay data while in-game."); _syncEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Twitch Sync", "Enabled", false, "Upload armoury data to the Cloudflare Worker backend."); _syncPeriodicIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Twitch Sync", "PeriodicUploadIntervalSeconds", 60f, "Upload even without equipment changes after this many seconds."); _backendUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch Sync", "BackendUrl", "https://valheim-twitch-armoury.sbraeunlein.workers.dev", "Cloudflare Worker backend URL."); _twitchClientId = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch Sync", "TwitchClientId", "21d5enqxycndhl70g0ea54ynizodwh", "Twitch application client ID."); _oauthRedirectPort = ((BaseUnityPlugin)this).Config.Bind<int>("Twitch Sync", "OAuthRedirectPort", 8718, "Local OAuth callback port. Twitch app redirect URL must match this."); _uploadToken = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch Sync", "UploadToken", "", "Generated after Twitch authorization. Keep private."); _channelId = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch Sync", "ChannelId", "", "Twitch channel ID generated after authorization."); _twitchLogin = ((BaseUnityPlugin)this).Config.Bind<string>("Twitch Sync", "TwitchLogin", "", "Twitch login generated after authorization."); _loginShortcut = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Twitch Sync", "LoginShortcut", new KeyboardShortcut((KeyCode)289, Array.Empty<KeyCode>()), "Keyboard shortcut to start Twitch authorization."); _autoExportAllIcons = ((BaseUnityPlugin)this).Config.Bind<bool>("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<KeyboardShortcut>("Assets", "ExportAllEquipableIconsShortcut", new KeyboardShortcut((KeyCode)290, Array.Empty<KeyCode>()), "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<FejdStartup>() != (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<string> _backendUrl; private readonly Func<string> _clientId; private readonly Func<int> _redirectPort; private readonly Action<string, string, string> _onAuthenticated; private readonly HttpClient _http = new HttpClient(); private bool _isRunning; public TwitchOAuthLogin(ManualLogSource log, Func<string> backendUrl, Func<string> clientId, Func<int> redirectPort, Action<string, string, string> 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", "<h1>Authentication denied</h1><p>" + EscapeHtml(error) + "</p>").ConfigureAwait(continueOnCapturedContext: false); _log.LogWarning((object)("Twitch authorization denied: " + error)); return; } if (string.IsNullOrWhiteSpace(text) || text2 != state) { await SendHtml(context, "Authentication failed", "<h1>Authentication failed</h1><p>Invalid OAuth response.</p>").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", "<h1>Authentication successful</h1><p>You can close this tab and return to Valheim.</p>").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<AuthResult> 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("<!doctype html><html><head><title>" + EscapeHtml(title) + "</title></head><body>" + body + "</body></html>"); 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("\"", """); } } }