Decompiled source of ValheimTwitchArmoury v0.1.3

ValheimTwitchArmoury.dll

Decompiled 4 days ago
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<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("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;")
				.Replace("\"", "&quot;");
		}
	}
}