Decompiled source of MimirsWell v0.5.4

MimirsWell.Core.dll

Decompiled 20 hours ago
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using MimirsWell.Core.Domain;
using MimirsWell.Core.Indexing;
using MimirsWell.Core.Planning;
using MimirsWell.Core.Resolution;
using MimirsWell.Core.Services;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("MimirsWell.Core")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Pure domain, indexing, interpretation, and query logic for Mimir's Well.")]
[assembly: AssemblyFileVersion("0.5.4.0")]
[assembly: AssemblyInformationalVersion("0.5.4+e9ead6b8be5d9ac71509b88d6e713cfa6d095cc3")]
[assembly: AssemblyProduct("MimirsWell.Core")]
[assembly: AssemblyTitle("MimirsWell.Core")]
[assembly: AssemblyVersion("0.5.4.0")]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace MimirsWell.Core.Services
{
	internal enum ComparisonMetricId
	{
		ListedDamage,
		Blunt,
		Slash,
		Pierce,
		Fire,
		Frost,
		Lightning,
		Poison,
		Spirit,
		PrimaryAttackStamina,
		SecondaryAttackStamina,
		Durability,
		Block,
		ParryBonus,
		Armor,
		FoodHealth,
		FoodStamina,
		FoodEitr,
		FoodDuration,
		FoodRegeneration,
		MovementModifier,
		Weight
	}
	internal sealed class ComparisonMetricDefinition
	{
		public ComparisonMetricId Id { get; }

		public string Name { get; }

		public bool HigherRanksFirst { get; }

		public string Suffix { get; }

		public string Caveat { get; }

		public string DirectionLabel
		{
			get
			{
				if (!HigherRanksFirst)
				{
					return "lower ranks first";
				}
				return "higher ranks first";
			}
		}

		public ComparisonMetricDefinition(ComparisonMetricId id, string name, bool higherRanksFirst, string suffix, string caveat)
		{
			Id = id;
			Name = name;
			HigherRanksFirst = higherRanksFirst;
			Suffix = suffix;
			Caveat = caveat;
		}
	}
	internal static class ComparisonMetricCatalog
	{
		private const string DamageCaveat = "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS.";

		private const string StaminaCaveat = "Not considered: damage per attack, attack timing or chains, regeneration, skill scaling, or real DPS.";

		private const string DefenseCaveat = "Not considered: resistances, stagger behavior, enemy attack mix, player skill, or effective survivability.";

		private const string FoodCaveat = "Not considered: meal combinations, acquisition difficulty, progression, or effects outside the selected loaded field.";

		private const string SingleFieldCaveat = "This is a single loaded field, not an overall item score.";

		private static readonly IReadOnlyDictionary<string, ComparisonMetricId> Aliases = new Dictionary<string, ComparisonMetricId>(StringComparer.OrdinalIgnoreCase)
		{
			["damage"] = ComparisonMetricId.ListedDamage,
			["listed damage"] = ComparisonMetricId.ListedDamage,
			["total damage"] = ComparisonMetricId.ListedDamage,
			["attack damage"] = ComparisonMetricId.ListedDamage,
			["listed combat damage"] = ComparisonMetricId.ListedDamage,
			["blunt"] = ComparisonMetricId.Blunt,
			["blunt damage"] = ComparisonMetricId.Blunt,
			["slash"] = ComparisonMetricId.Slash,
			["slash damage"] = ComparisonMetricId.Slash,
			["pierce"] = ComparisonMetricId.Pierce,
			["pierce damage"] = ComparisonMetricId.Pierce,
			["fire"] = ComparisonMetricId.Fire,
			["fire damage"] = ComparisonMetricId.Fire,
			["frost"] = ComparisonMetricId.Frost,
			["frost damage"] = ComparisonMetricId.Frost,
			["lightning"] = ComparisonMetricId.Lightning,
			["lightning damage"] = ComparisonMetricId.Lightning,
			["poison"] = ComparisonMetricId.Poison,
			["poison damage"] = ComparisonMetricId.Poison,
			["spirit"] = ComparisonMetricId.Spirit,
			["spirit damage"] = ComparisonMetricId.Spirit,
			["primary stamina"] = ComparisonMetricId.PrimaryAttackStamina,
			["primary attack stamina"] = ComparisonMetricId.PrimaryAttackStamina,
			["attack stamina"] = ComparisonMetricId.PrimaryAttackStamina,
			["stamina cost"] = ComparisonMetricId.PrimaryAttackStamina,
			["secondary stamina"] = ComparisonMetricId.SecondaryAttackStamina,
			["secondary attack stamina"] = ComparisonMetricId.SecondaryAttackStamina,
			["durability"] = ComparisonMetricId.Durability,
			["block"] = ComparisonMetricId.Block,
			["block power"] = ComparisonMetricId.Block,
			["parry"] = ComparisonMetricId.ParryBonus,
			["parry bonus"] = ComparisonMetricId.ParryBonus,
			["armor"] = ComparisonMetricId.Armor,
			["armour"] = ComparisonMetricId.Armor,
			["health"] = ComparisonMetricId.FoodHealth,
			["food health"] = ComparisonMetricId.FoodHealth,
			["eitr"] = ComparisonMetricId.FoodEitr,
			["food eitr"] = ComparisonMetricId.FoodEitr,
			["duration"] = ComparisonMetricId.FoodDuration,
			["food duration"] = ComparisonMetricId.FoodDuration,
			["regeneration"] = ComparisonMetricId.FoodRegeneration,
			["regen"] = ComparisonMetricId.FoodRegeneration,
			["food regeneration"] = ComparisonMetricId.FoodRegeneration,
			["movement"] = ComparisonMetricId.MovementModifier,
			["movement modifier"] = ComparisonMetricId.MovementModifier,
			["movement penalty"] = ComparisonMetricId.MovementModifier,
			["weight"] = ComparisonMetricId.Weight
		};

		public static string SupportedSummary => "damage, a listed combat-damage type, primary/secondary stamina, durability, block, parry, armor, food health/stamina/eitr/duration/regeneration, movement, or weight";

		public static bool TryResolve(string metricText, WikiItem left, WikiItem right, out ComparisonMetricDefinition? metric, out string diagnostic)
		{
			string text = TextNormalizer.Normalize(metricText);
			if (text == "stamina")
			{
				if (left.Kind == WikiItemKind.Food && right.Kind == WikiItemKind.Food)
				{
					return Resolved(ComparisonMetricId.FoodStamina, out metric, out diagnostic);
				}
				if (left.Kind == WikiItemKind.Weapon && right.Kind == WikiItemKind.Weapon)
				{
					return Resolved(ComparisonMetricId.PrimaryAttackStamina, out metric, out diagnostic);
				}
				metric = null;
				diagnostic = "'stamina' is contextual: compare two foods for food stamina or two weapons for primary attack stamina.";
				return false;
			}
			if (!Aliases.TryGetValue(text, out var value))
			{
				metric = null;
				diagnostic = "Unsupported comparison metric '" + metricText + "'. Supported metrics: " + SupportedSummary + ". DPS and composite best scores are not supported.";
				return false;
			}
			return Resolved(value, out metric, out diagnostic);
		}

		public static bool TryValue(ComparisonMetricDefinition metric, WikiItem item, WikiQualityStats stats, out double value)
		{
			double? num = null;
			switch (metric.Id)
			{
			case ComparisonMetricId.ListedDamage:
				if (item.Kind == WikiItemKind.Weapon)
				{
					num = stats.Damage?.ListedCombatTotal;
				}
				break;
			case ComparisonMetricId.Blunt:
				num = Damage(item, stats, DamageKind.Blunt);
				break;
			case ComparisonMetricId.Slash:
				num = Damage(item, stats, DamageKind.Slash);
				break;
			case ComparisonMetricId.Pierce:
				num = Damage(item, stats, DamageKind.Pierce);
				break;
			case ComparisonMetricId.Fire:
				num = Damage(item, stats, DamageKind.Fire);
				break;
			case ComparisonMetricId.Frost:
				num = Damage(item, stats, DamageKind.Frost);
				break;
			case ComparisonMetricId.Lightning:
				num = Damage(item, stats, DamageKind.Lightning);
				break;
			case ComparisonMetricId.Poison:
				num = Damage(item, stats, DamageKind.Poison);
				break;
			case ComparisonMetricId.Spirit:
				num = Damage(item, stats, DamageKind.Spirit);
				break;
			case ComparisonMetricId.PrimaryAttackStamina:
				if (item.Kind == WikiItemKind.Weapon)
				{
					num = stats.PrimaryAttackStamina;
				}
				break;
			case ComparisonMetricId.SecondaryAttackStamina:
				if (item.Kind == WikiItemKind.Weapon)
				{
					num = stats.SecondaryAttackStamina;
				}
				break;
			case ComparisonMetricId.Durability:
				num = stats.Durability;
				break;
			case ComparisonMetricId.Block:
			{
				WikiItemKind kind = item.Kind;
				if ((uint)(kind - 1) <= 1u)
				{
					num = stats.Block;
				}
				break;
			}
			case ComparisonMetricId.ParryBonus:
			{
				WikiItemKind kind = item.Kind;
				if ((uint)(kind - 1) <= 1u)
				{
					num = item.ParryBonus;
				}
				break;
			}
			case ComparisonMetricId.Armor:
				if (item.Kind == WikiItemKind.Armor)
				{
					num = stats.Armor;
				}
				break;
			case ComparisonMetricId.FoodHealth:
				if (item.Kind == WikiItemKind.Food)
				{
					num = item.FoodHealth;
				}
				break;
			case ComparisonMetricId.FoodStamina:
				if (item.Kind == WikiItemKind.Food)
				{
					num = item.FoodStamina;
				}
				break;
			case ComparisonMetricId.FoodEitr:
				if (item.Kind == WikiItemKind.Food)
				{
					num = item.FoodEitr;
				}
				break;
			case ComparisonMetricId.FoodDuration:
				if (item.Kind == WikiItemKind.Food)
				{
					num = item.FoodDurationSeconds;
				}
				break;
			case ComparisonMetricId.FoodRegeneration:
				if (item.Kind == WikiItemKind.Food)
				{
					num = item.FoodRegeneration;
				}
				break;
			case ComparisonMetricId.MovementModifier:
				num = item.MovementModifier * 100.0;
				break;
			case ComparisonMetricId.Weight:
				num = item.Weight;
				break;
			}
			value = num.GetValueOrDefault();
			if (num.HasValue && !double.IsNaN(value))
			{
				return !double.IsInfinity(value);
			}
			return false;
		}

		public static int Compare(ComparisonMetricDefinition metric, double left, double right)
		{
			int num = left.CompareTo(right);
			if (!metric.HigherRanksFirst)
			{
				return -num;
			}
			return num;
		}

		public static double Improvement(ComparisonMetricDefinition metric, double candidate, double baseline)
		{
			if (!metric.HigherRanksFirst)
			{
				return baseline - candidate;
			}
			return candidate - baseline;
		}

		private static bool Resolved(ComparisonMetricId id, out ComparisonMetricDefinition? metric, out string diagnostic)
		{
			metric = Definition(id);
			diagnostic = string.Empty;
			return true;
		}

		private static double? Damage(WikiItem item, WikiQualityStats stats, DamageKind kind)
		{
			if (item.Kind != WikiItemKind.Weapon || stats.Damage == null)
			{
				return null;
			}
			return stats.Damage.Get(kind);
		}

		private static ComparisonMetricDefinition Definition(ComparisonMetricId id)
		{
			return id switch
			{
				ComparisonMetricId.ListedDamage => new ComparisonMetricDefinition(id, "Listed combat damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Blunt => new ComparisonMetricDefinition(id, "Blunt damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Slash => new ComparisonMetricDefinition(id, "Slash damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Pierce => new ComparisonMetricDefinition(id, "Pierce damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Fire => new ComparisonMetricDefinition(id, "Fire damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Frost => new ComparisonMetricDefinition(id, "Frost damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Lightning => new ComparisonMetricDefinition(id, "Lightning damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Poison => new ComparisonMetricDefinition(id, "Poison damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.Spirit => new ComparisonMetricDefinition(id, "Spirit damage", higherRanksFirst: true, string.Empty, "Not considered: enemy resistance, status effects, skill scaling, attack timing or chains, stamina efficiency, or real DPS."), 
				ComparisonMetricId.PrimaryAttackStamina => new ComparisonMetricDefinition(id, "Primary attack stamina", higherRanksFirst: false, string.Empty, "Not considered: damage per attack, attack timing or chains, regeneration, skill scaling, or real DPS."), 
				ComparisonMetricId.SecondaryAttackStamina => new ComparisonMetricDefinition(id, "Secondary attack stamina", higherRanksFirst: false, string.Empty, "Not considered: damage per attack, attack timing or chains, regeneration, skill scaling, or real DPS."), 
				ComparisonMetricId.Durability => new ComparisonMetricDefinition(id, "Durability", higherRanksFirst: true, string.Empty, "This is a single loaded field, not an overall item score."), 
				ComparisonMetricId.Block => new ComparisonMetricDefinition(id, "Block", higherRanksFirst: true, string.Empty, "Not considered: resistances, stagger behavior, enemy attack mix, player skill, or effective survivability."), 
				ComparisonMetricId.ParryBonus => new ComparisonMetricDefinition(id, "Parry bonus", higherRanksFirst: true, "x", "Not considered: resistances, stagger behavior, enemy attack mix, player skill, or effective survivability."), 
				ComparisonMetricId.Armor => new ComparisonMetricDefinition(id, "Armor", higherRanksFirst: true, string.Empty, "Not considered: resistances, stagger behavior, enemy attack mix, player skill, or effective survivability."), 
				ComparisonMetricId.FoodHealth => new ComparisonMetricDefinition(id, "Food health", higherRanksFirst: true, string.Empty, "Not considered: meal combinations, acquisition difficulty, progression, or effects outside the selected loaded field."), 
				ComparisonMetricId.FoodStamina => new ComparisonMetricDefinition(id, "Food stamina", higherRanksFirst: true, string.Empty, "Not considered: meal combinations, acquisition difficulty, progression, or effects outside the selected loaded field."), 
				ComparisonMetricId.FoodEitr => new ComparisonMetricDefinition(id, "Food eitr", higherRanksFirst: true, string.Empty, "Not considered: meal combinations, acquisition difficulty, progression, or effects outside the selected loaded field."), 
				ComparisonMetricId.FoodDuration => new ComparisonMetricDefinition(id, "Food duration", higherRanksFirst: true, " s", "Not considered: meal combinations, acquisition difficulty, progression, or effects outside the selected loaded field."), 
				ComparisonMetricId.FoodRegeneration => new ComparisonMetricDefinition(id, "Food regeneration", higherRanksFirst: true, string.Empty, "Not considered: meal combinations, acquisition difficulty, progression, or effects outside the selected loaded field."), 
				ComparisonMetricId.MovementModifier => new ComparisonMetricDefinition(id, "Movement modifier", higherRanksFirst: true, "%", "This is a single loaded field, not an overall item score."), 
				_ => new ComparisonMetricDefinition(id, "Weight", higherRanksFirst: false, string.Empty, "This is a single loaded field, not an overall item score."), 
			};
		}
	}
	public enum WikiContextKind
	{
		Held,
		Equipped,
		ActiveFood,
		CurrentStation
	}
	public sealed class WikiContextItem
	{
		public string ItemId { get; }

		public string Role { get; }

		public double? CurrentHealth { get; }

		public double? CurrentStamina { get; }

		public double? CurrentEitr { get; }

		public double? RemainingSeconds { get; }

		public WikiContextItem(string itemId, string role, double? currentHealth = null, double? currentStamina = null, double? currentEitr = null, double? remainingSeconds = null)
		{
			ItemId = itemId ?? string.Empty;
			Role = (string.IsNullOrWhiteSpace(role) ? "context item" : role);
			CurrentHealth = Finite(currentHealth);
			CurrentStamina = Finite(currentStamina);
			CurrentEitr = Finite(currentEitr);
			RemainingSeconds = Finite(remainingSeconds, nonNegative: true);
		}

		private static double? Finite(double? value, bool nonNegative = false)
		{
			if (value.HasValue && !double.IsNaN(value.Value) && !double.IsInfinity(value.Value) && (!nonNegative || !(value.Value < 0.0)))
			{
				return value;
			}
			return null;
		}
	}
	public sealed class WikiContextSnapshot
	{
		private readonly IReadOnlyList<WikiContextItem> items;

		public WikiContextKind Kind { get; }

		public bool PlayerContextAvailable { get; }

		public IReadOnlyList<WikiContextItem> Items => items;

		public string? StationName { get; }

		public string? StationInternalName { get; }

		public int IsolatedErrors { get; }

		public WikiContextSnapshot(WikiContextKind kind, bool playerContextAvailable, IEnumerable<WikiContextItem>? items = null, string? stationName = null, string? stationInternalName = null, int isolatedErrors = 0)
		{
			Kind = kind;
			PlayerContextAvailable = playerContextAvailable;
			this.items = Array.AsReadOnly((items ?? Array.Empty<WikiContextItem>()).OfType<WikiContextItem>().ToArray());
			StationName = (string.IsNullOrWhiteSpace(stationName) ? null : stationName);
			StationInternalName = (string.IsNullOrWhiteSpace(stationInternalName) ? null : stationInternalName);
			IsolatedErrors = Math.Max(0, isolatedErrors);
		}
	}
	public sealed class WikiEngineOptions
	{
		public bool ShowInternalNames { get; set; }

		public bool ShowSourceMod { get; set; } = true;

		public bool ShowComparisonAssumptions { get; set; } = true;

		public int MaximumAlternativeResults { get; set; } = 5;

		public bool FuzzyMatchingEnabled { get; set; } = true;

		public double FuzzyMatchThreshold { get; set; } = 0.82;

		public double AmbiguityMargin { get; set; } = 0.05;
	}
	public sealed class WikiEngine
	{
		private sealed class FilterExpression
		{
			private static readonly Dictionary<string, string> Categories = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
			{
				["sword"] = "sword",
				["swords"] = "sword",
				["axe"] = "axe",
				["axes"] = "axe",
				["bow"] = "bow",
				["bows"] = "bow",
				["spear"] = "spear",
				["spears"] = "spear",
				["mace"] = "club",
				["maces"] = "club",
				["club"] = "club",
				["clubs"] = "club",
				["knife"] = "knife",
				["knives"] = "knife",
				["polearm"] = "polearm",
				["polearms"] = "polearm"
			};

			private static readonly Dictionary<string, WikiItemKind> Kinds = new Dictionary<string, WikiItemKind>(StringComparer.OrdinalIgnoreCase)
			{
				["weapon"] = WikiItemKind.Weapon,
				["weapons"] = WikiItemKind.Weapon,
				["shield"] = WikiItemKind.Shield,
				["shields"] = WikiItemKind.Shield,
				["armor"] = WikiItemKind.Armor,
				["armour"] = WikiItemKind.Armor,
				["food"] = WikiItemKind.Food,
				["foods"] = WikiItemKind.Food,
				["material"] = WikiItemKind.Material,
				["materials"] = WikiItemKind.Material,
				["tool"] = WikiItemKind.Tool,
				["tools"] = WikiItemKind.Tool
			};

			private static readonly Dictionary<string, DamageKind> Damages = (from DamageKind kind in Enum.GetValues(typeof(DamageKind))
				where kind != DamageKind.Chop && kind != DamageKind.Pickaxe
				select kind).ToDictionary<DamageKind, string, DamageKind>((DamageKind kind) => kind.ToString(), (DamageKind kind) => kind, StringComparer.OrdinalIgnoreCase);

			public bool IsSupported { get; private set; }

			public string Diagnostic { get; private set; } = string.Empty;

			public WikiItemKind? Kind { get; private set; }

			public string? Category { get; private set; }

			public DamageKind? Damage { get; private set; }

			public string? NumericField { get; private set; }

			public string? Operator { get; private set; }

			public double? NumericValue { get; private set; }

			public static FilterExpression Parse(string expression)
			{
				FilterExpression filterExpression = new FilterExpression();
				string text = TextNormalizer.Normalize(expression);
				string[] array = text.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				foreach (string key in array)
				{
					if (Categories.TryGetValue(key, out string value))
					{
						filterExpression.Category = value;
					}
					if (Kinds.TryGetValue(key, out var value2))
					{
						filterExpression.Kind = value2;
					}
					if (Damages.TryGetValue(key, out var value3))
					{
						filterExpression.Damage = value3;
					}
				}
				Match match = Regex.Match(text, "(?<op>above|below|at least|at most)\\s+(?<value>-?[0-9]+(?:\\.[0-9]+)?)\\s+(?<field>primary stamina|secondary stamina|total damage|listed damage|health|stamina|eitr|duration|regeneration|regen|armor|block|durability|weight|movement|blunt|slash|pierce|fire|frost|lightning|poison|spirit)\\b", RegexOptions.IgnoreCase);
				if (!match.Success)
				{
					match = Regex.Match(text, "(?<field>primary stamina|secondary stamina|total damage|listed damage|health|stamina|eitr|duration|regeneration|regen|armor|block|durability|weight|movement|blunt|slash|pierce|fire|frost|lightning|poison|spirit)\\s+(?<op>above|below|at least|at most)\\s+(?<value>-?[0-9]+(?:\\.[0-9]+)?)\\b", RegexOptions.IgnoreCase);
				}
				if (match.Success && double.TryParse(match.Groups["value"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					filterExpression.NumericField = NormalizeNumericField(match.Groups["field"].Value);
					filterExpression.Operator = match.Groups["op"].Value.ToLowerInvariant();
					filterExpression.NumericValue = result;
					if (!filterExpression.Kind.HasValue && filterExpression.Category == null)
					{
						bool flag;
						switch (filterExpression.NumericField)
						{
						case "health":
						case "eitr":
						case "duration":
						case "regeneration":
							flag = true;
							break;
						default:
							flag = false;
							break;
						}
						if (flag)
						{
							filterExpression.Kind = WikiItemKind.Food;
						}
						switch (filterExpression.NumericField)
						{
						case "poison":
						case "spirit":
						case "damage":
						case "pierce":
						case "blunt":
						case "frost":
						case "slash":
						case "primary stamina":
						case "secondary stamina":
						case "fire":
						case "lightning":
							flag = true;
							break;
						default:
							flag = false;
							break;
						}
						if (flag)
						{
							filterExpression.Kind = WikiItemKind.Weapon;
						}
						if (filterExpression.NumericField == "armor")
						{
							filterExpression.Kind = WikiItemKind.Armor;
						}
					}
				}
				filterExpression.IsSupported = filterExpression.Kind.HasValue || filterExpression.Category != null || filterExpression.Damage.HasValue || filterExpression.NumericValue.HasValue;
				filterExpression.Diagnostic = (filterExpression.IsSupported ? "Supported filter parsed." : "Supported filters are item category, combat damage type, and loaded numeric fields such as damage, stamina, armor, block, durability, weight, movement, health, eitr, duration, or regeneration using above, below, at least, or at most.");
				return filterExpression;
			}

			private static string NormalizeNumericField(string field)
			{
				string text = TextNormalizer.Normalize(field);
				if ((text == "total damage" || text == "listed damage") ? true : false)
				{
					return "damage";
				}
				if (text == "regen")
				{
					return "regeneration";
				}
				return text;
			}
		}

		private sealed class StationResolution
		{
			public ResolutionStatus Status { get; }

			public IReadOnlyList<string> Names { get; }

			public string Diagnostic { get; }

			public StationResolution(ResolutionStatus status, IEnumerable<string> names, string diagnostic)
			{
				Status = status;
				Names = Array.AsReadOnly(names.ToArray());
				Diagnostic = diagnostic;
			}
		}

		private sealed class SourceEntityResolution
		{
			public ResolutionStatus Status { get; }

			public IReadOnlyList<string> Names { get; }

			public SourceEntityResolution(ResolutionStatus status, IEnumerable<string> names)
			{
				Status = status;
				Names = Array.AsReadOnly(names.ToArray());
			}
		}

		private sealed class SourceDisplayGroup
		{
			public WikiAcquisitionSource Source { get; }

			public int VariantCount { get; }

			public SourceDisplayGroup(WikiAcquisitionSource source, int variantCount)
			{
				Source = source;
				VariantCount = Math.Max(1, variantCount);
			}
		}

		private const int MaximumContextEntries = 16;

		private const int MaximumDiagnosticRows = 20;

		private static readonly DamageKind[] CombatDamageOrder = new DamageKind[8]
		{
			DamageKind.Blunt,
			DamageKind.Slash,
			DamageKind.Pierce,
			DamageKind.Fire,
			DamageKind.Frost,
			DamageKind.Lightning,
			DamageKind.Poison,
			DamageKind.Spirit
		};

		private readonly EntityResolver resolver;

		private readonly WikiEngineOptions options;

		public WikiEngine(EntityResolver resolver, WikiEngineOptions options)
		{
			this.resolver = resolver ?? throw new ArgumentNullException("resolver");
			this.options = options ?? throw new ArgumentNullException("options");
		}

		public WikiAnswer Execute(WikiIndex index, WikiQuery query)
		{
			return Execute(index, query, WikiVisibilitySnapshot.AllLoaded);
		}

		public WikiAnswer Execute(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (index == null)
			{
				throw new ArgumentNullException("index");
			}
			if (query == null)
			{
				throw new ArgumentNullException("query");
			}
			if (visibility == null)
			{
				throw new ArgumentNullException("visibility");
			}
			if (visibility.IsRestricted && !visibility.CharacterStateAvailable && query.Intent != WikiIntent.Help && query.Intent != WikiIntent.Status)
			{
				return new WikiAnswer("Character knowledge unavailable", new string[2]
				{
					$"{visibility.Policy} requires an active local character.",
					"The complete runtime index remains loaded, but this query was not allowed to read it."
				}, null, new string[1] { "Enter a world with a character or set SpoilerPolicy = AllLoaded." });
			}
			switch (query.Intent)
			{
			case WikiIntent.Help:
				return Help(visibility);
			case WikiIntent.Status:
				return Status(index, visibility);
			case WikiIntent.ItemInfo:
				return WithItem(index, query, visibility, (WikiItem item) => ItemInfo(index, item, query.Quality ?? 1));
			case WikiIntent.RecipeLookup:
				return WithItem(index, query, visibility, (WikiItem item) => Recipes(index, item, visibility));
			case WikiIntent.UpgradeLookup:
				return WithItem(index, query, visibility, (WikiItem item) => Upgrades(index, item, visibility));
			case WikiIntent.MaterialUsage:
				return WithItem(index, query, visibility, (WikiItem item) => Uses(index, item, visibility));
			case WikiIntent.CraftingPlan:
				return WithItem(index, query, visibility, (WikiItem item) => Plan(index, item, query.Quantity ?? 1, query.Quality ?? 1, visibility));
			case WikiIntent.AcquisitionSource:
				return WithItem(index, query, visibility, (WikiItem item) => AcquisitionSources(index, item, SourceKindFilter(query), visibility));
			case WikiIntent.SourceInventory:
				return SourceInventory(index, query, visibility);
			case WikiIntent.CraftingStation:
				return StationQuery(index, query, visibility);
			case WikiIntent.StrongerAlternatives:
				return WithItem(index, query, visibility, (WikiItem item) => Stronger(index, item, query, visibility));
			case WikiIntent.ItemComparison:
				return Compare(index, query, visibility);
			case WikiIntent.CategoryRanking:
				return RankCategory(index, query, visibility);
			case WikiIntent.ComparisonExplanation:
				return ExplainComparison(index, query, visibility);
			case WikiIntent.Find:
				return Find(index, query, visibility);
			case WikiIntent.Diagnostics:
				return Diagnostics(index, visibility);
			case WikiIntent.ModReport:
				return ModReport(index, visibility);
			case WikiIntent.UnsupportedReport:
				return UnsupportedReport(index, visibility);
			case WikiIntent.ContextHeld:
			case WikiIntent.ContextEquipped:
			case WikiIntent.ContextFood:
			case WikiIntent.ContextStation:
				return new WikiAnswer("Context adapter required", new string[1] { "This shortcut requires the read-only in-game context adapter." });
			case WikiIntent.More:
				return new WikiAnswer("Pagination", new string[1] { "No pagination context was supplied to the query engine." });
			case WikiIntent.Reindex:
				return new WikiAnswer("Reindex", new string[1] { "Reindexing is handled by the game lifecycle adapter." });
			case WikiIntent.Reload:
				return new WikiAnswer("Reload", new string[1] { "Configuration reload is handled by the plugin adapter." });
			default:
				return new WikiAnswer("Unsupported question", new string[1] { query.Diagnostic }, null, new string[1] { "Use /wiki help for supported forms." });
			}
		}

		public WikiAnswer ExecuteContext(WikiIndex index, WikiQuery query, WikiContextSnapshot context, WikiVisibilitySnapshot visibility)
		{
			if (index == null)
			{
				throw new ArgumentNullException("index");
			}
			if (query == null)
			{
				throw new ArgumentNullException("query");
			}
			if (context == null)
			{
				throw new ArgumentNullException("context");
			}
			if (visibility == null)
			{
				throw new ArgumentNullException("visibility");
			}
			if (!IsContextIntent(query.Intent))
			{
				return new WikiAnswer("Invalid context query", new string[1] { "The supplied query is not a context shortcut." });
			}
			if (visibility.IsRestricted && !visibility.CharacterStateAvailable)
			{
				return new WikiAnswer("Character knowledge unavailable", new string[1] { $"{visibility.Policy} requires an active local character." });
			}
			if (!context.PlayerContextAvailable)
			{
				return new WikiAnswer("Player context unavailable", new string[1] { "Enter a world with an active local character before using this shortcut." });
			}
			if (!ContextKindMatches(query.Intent, context.Kind))
			{
				return new WikiAnswer("Context mismatch", new string[1] { "The runtime context did not match the requested shortcut; no fallback lookup was attempted." });
			}
			return query.Intent switch
			{
				WikiIntent.ContextHeld => HeldContext(index, context, visibility), 
				WikiIntent.ContextEquipped => ItemContextList(index, context, visibility, "Equipped items"), 
				WikiIntent.ContextFood => FoodContext(index, context, visibility), 
				_ => StationContext(index, context, visibility), 
			};
		}

		private WikiAnswer HeldContext(WikiIndex index, WikiContextSnapshot context, WikiVisibilitySnapshot visibility)
		{
			if (context.Items.Count == 0)
			{
				return new WikiAnswer("Held item", new string[1] { "No item is reported in either direct hand slot or among equipped hand-capable items." });
			}
			WikiContextItem wikiContextItem = context.Items[0];
			if (!index.Items.TryGetValue(wikiContextItem.ItemId, out WikiItem value))
			{
				return ContextUnavailableItem("Held item", context, "The current held item is not represented in the runtime wiki index.");
			}
			if (!visibility.IsVisible(value))
			{
				return ContextUnavailableItem("Held item", context, $"The current held item is hidden by {visibility.Policy}.");
			}
			WikiAnswer wikiAnswer = ItemInfo(index, value, 1);
			string[] warnings = wikiAnswer.Warnings.Concat(ContextWarnings(context)).ToArray();
			return new WikiAnswer("Held: " + wikiAnswer.Title, new string[1] { "Context: " + wikiContextItem.Role + "." }.Concat(wikiAnswer.Lines), warnings, wikiAnswer.Suggestions);
		}

		private WikiAnswer ItemContextList(WikiIndex index, WikiContextSnapshot context, WikiVisibilitySnapshot visibility, string title)
		{
			List<(WikiContextItem, WikiItem)> list = new List<(WikiContextItem, WikiItem)>();
			int num = 0;
			int num2 = 0;
			foreach (WikiContextItem item in context.Items.Take(16))
			{
				if (!index.Items.TryGetValue(item.ItemId, out WikiItem value))
				{
					num2++;
				}
				else if (!visibility.IsVisible(value))
				{
					num++;
				}
				else
				{
					list.Add((item, value));
				}
			}
			List<string> list2 = list.Select<(WikiContextItem, WikiItem), string>(((WikiContextItem Entry, WikiItem Item) tuple) => "- " + Label(tuple.Item) + " | " + tuple.Item.Category + " | " + tuple.Entry.Role).ToList();
			if (context.Items.Count == 0)
			{
				list2.Add("No equipped items are reported by the active character inventory.");
			}
			if (num > 0)
			{
				list2.Add(string.Format("{0} equipped item{1} hidden by {2}.", num, (num == 1) ? " is" : "s are", visibility.Policy));
			}
			if (num2 > 0)
			{
				list2.Add(string.Format("{0} equipped item{1} not represented in the runtime wiki index.", num2, (num2 == 1) ? " is" : "s are"));
			}
			if (context.Items.Count > 16)
			{
				list2.Add($"Only the first {16} context entries were evaluated.");
			}
			list2.AddRange(ContextWarnings(context));
			return new WikiAnswer($"{title} ({list.Count} visible)", list2);
		}

		private WikiAnswer FoodContext(WikiIndex index, WikiContextSnapshot context, WikiVisibilitySnapshot visibility)
		{
			List<(WikiContextItem, WikiItem)> list = new List<(WikiContextItem, WikiItem)>();
			int num = 0;
			int num2 = 0;
			foreach (WikiContextItem item in context.Items.Take(16))
			{
				if (!index.Items.TryGetValue(item.ItemId, out WikiItem value))
				{
					num2++;
				}
				else if (!visibility.IsVisible(value))
				{
					num++;
				}
				else
				{
					list.Add((item, value));
				}
			}
			List<string> list2 = list.Select<(WikiContextItem, WikiItem), string>(delegate((WikiContextItem Entry, WikiItem Item) tuple)
			{
				List<string> list3 = new List<string>();
				if (tuple.Entry.CurrentHealth.HasValue)
				{
					list3.Add("health " + Number(tuple.Entry.CurrentHealth.Value));
				}
				if (tuple.Entry.CurrentStamina.HasValue)
				{
					list3.Add("stamina " + Number(tuple.Entry.CurrentStamina.Value));
				}
				if (tuple.Entry.CurrentEitr.HasValue)
				{
					list3.Add("eitr " + Number(tuple.Entry.CurrentEitr.Value));
				}
				if (tuple.Entry.RemainingSeconds.HasValue)
				{
					list3.Add("remaining " + Number(tuple.Entry.RemainingSeconds.Value / 60.0) + " min");
				}
				return "- " + Label(tuple.Item) + ": " + ((list3.Count == 0) ? "current contribution unavailable" : string.Join(" | ", list3));
			}).ToList();
			if (context.Items.Count == 0)
			{
				list2.Add("No active consumed foods are reported by the current character.");
			}
			if (num > 0)
			{
				list2.Add(string.Format("{0} active food item{1} hidden by {2}.", num, (num == 1) ? " is" : "s are", visibility.Policy));
			}
			if (num2 > 0)
			{
				list2.Add(string.Format("{0} active food item{1} not represented in the runtime wiki index.", num2, (num2 == 1) ? " is" : "s are"));
			}
			list2.AddRange(ContextWarnings(context));
			return new WikiAnswer($"Active food ({list.Count} visible)", list2, new string[1] { "Values are the character's loaded current food contributions and remaining time; no food state is changed." });
		}

		private WikiAnswer StationContext(WikiIndex index, WikiContextSnapshot context, WikiVisibilitySnapshot visibility)
		{
			if (string.IsNullOrWhiteSpace(context.StationName))
			{
				return new WikiAnswer("Current station", new string[2] { "No current crafting-station interaction is active.", "For this shortcut, 'nearby' means the station currently being used; Mimir's Well does not scan the scene or infer distance." }.Concat(ContextWarnings(context)));
			}
			WikiAnswer wikiAnswer = StationContents(index, context.StationName, visibility);
			return new WikiAnswer("Current station: " + wikiAnswer.Title, wikiAnswer.Lines, wikiAnswer.Warnings.Concat(ContextWarnings(context)), wikiAnswer.Suggestions);
		}

		private static WikiAnswer ContextUnavailableItem(string title, WikiContextSnapshot context, string message)
		{
			return new WikiAnswer(title, new string[1] { message }.Concat(ContextWarnings(context)));
		}

		private static IEnumerable<string> ContextWarnings(WikiContextSnapshot context)
		{
			if (context.IsolatedErrors > 0)
			{
				yield return string.Format("{0} malformed context entr{1} isolated.", context.IsolatedErrors, (context.IsolatedErrors == 1) ? "y was" : "ies were");
			}
		}

		private static bool IsContextIntent(WikiIntent intent)
		{
			if ((uint)(intent - 15) <= 3u)
			{
				return true;
			}
			return false;
		}

		private static bool ContextKindMatches(WikiIntent intent, WikiContextKind kind)
		{
			if ((intent != WikiIntent.ContextHeld || kind != WikiContextKind.Held) && (intent != WikiIntent.ContextEquipped || kind != WikiContextKind.Equipped) && (intent != WikiIntent.ContextFood || kind != WikiContextKind.ActiveFood))
			{
				if (intent == WikiIntent.ContextStation)
				{
					return kind == WikiContextKind.CurrentStation;
				}
				return false;
			}
			return true;
		}

		private WikiAnswer WithItem(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility, Func<WikiItem, WikiAnswer> answer)
		{
			if (query.EntityTexts.Count == 0)
			{
				return new WikiAnswer("Missing item", new string[1] { "Please provide an item name." });
			}
			ResolutionResult resolutionResult = resolver.Resolve(index, query.EntityTexts[0], visibility.IsVisible);
			if (resolutionResult.Status == ResolutionStatus.Resolved && resolutionResult.Item != null)
			{
				return answer(resolutionResult.Item);
			}
			return ResolutionAnswer(query.EntityTexts[0], resolutionResult);
		}

		private static WikiAnswer Help(WikiVisibilitySnapshot visibility)
		{
			return new WikiAnswer("Commands", new string[16]
			{
				"/wiki <item> or /wiki info <item>", "/wiki recipe <item> | upgrades <item> | uses <material>", "/wiki plan <item> | plan <quantity> <item> quality <n>", "/wiki source <item> | what drops <item>", "/wiki drops <creature> | what does <trader> sell", "/wiki mine <item> | fish <item> | what produces <item>", "/wiki station <item or station> | compare <item> and <item>", "/wiki compare <item> and <item> by <metric> quality <n>", "/wiki compare foods by stamina | compare shields by block", "/wiki why <item> is stronger than <item> by <metric>",
				"/wiki stronger <weapon> | find <category/filter>", "/wiki held | equipped | food | station nearby", "/wiki diagnostics | mods | unsupported", "/wiki status | more | reload | reindex", "Natural example: /wiki how do I make an iron sword", "Quote an exact name when needed: /wiki recipe \"Iron Sword\""
			}, new string[2]
			{
				"Coverage includes crafting, build pieces, processing conversions, reliable direct sources, deterministic environmental drops, and direct fish pickup. Acquisition sources remain separate from recipe methods.",
				$"Knowledge visibility: {visibility.Policy} ({visibility.Description})."
			});
		}

		private static WikiAnswer Plan(WikiIndex index, WikiItem item, long quantity, int quality, WikiVisibilitySnapshot visibility)
		{
			ProductionPlan productionPlan = new ProductionPlanner().Plan(index, item, quantity, quality, visibility);
			return new WikiAnswer(productionPlan.Title, productionPlan.Lines, null, null, isPaginated: true);
		}

		private static WikiAnswer Status(WikiIndex index, WikiVisibilitySnapshot visibility)
		{
			int num = (visibility.IsRestricted ? index.Items.Values.Count(visibility.IsVisible) : index.Items.Count);
			return new WikiAnswer("Index ready", new string[12]
			{
				$"Items: {index.Items.Count} | recipes: {index.Recipes.Count} | conversions: {index.Conversions.Count} | build pieces: {index.Pieces.Count}",
				string.Format("Knowledge visibility: {0} | visible items: {1}/{2} | character state: {3} | isolated evaluation errors: {4}.", visibility.Policy, num, index.Items.Count, visibility.CharacterStateAvailable ? "available" : "unavailable", visibility.EvaluationErrors),
				"The complete loaded index is immutable; visibility is applied only while answering each query.",
				$"Diagnostics: {index.Diagnostics.Count} retained issue records | {index.OmittedDiagnostics} omitted by the 256-record bound.",
				$"Sources: {index.Sources.Count} | creature: {index.Sources.Count((WikiAcquisitionSource source) => source.Kind == WikiSourceKind.CreatureDrop)} | purchased: {index.Sources.Count((WikiAcquisitionSource source) => source.Kind == WikiSourceKind.Purchased)} | passive: {index.Sources.Count((WikiAcquisitionSource source) => source.Kind == WikiSourceKind.PassivelyProduced)} | harvested: {index.Sources.Count((WikiAcquisitionSource source) => source.Kind == WikiSourceKind.Harvested)}",
				$"Environmental: {index.Sources.Count((WikiAcquisitionSource source) => source.Kind == WikiSourceKind.MinedOrDestroyed)} mined/destroyed ({index.Sources.Count((WikiAcquisitionSource source) => source.QuantitySemantics == WikiQuantitySemantics.ExactLoadedRange)} guaranteed-variable) | fishing: {index.Sources.Count((WikiAcquisitionSource source) => source.Kind == WikiSourceKind.Fishing)}",
				"Planner: enabled | bounds: depth 12, nodes 256, alternatives 8, target 1,000,000, computed 1,000,000,000, lines 200, time 50 ms.",
				"Comparisons: deterministic explicit metrics and explanations enabled; no DPS or composite best score.",
				$"Language: {index.Language} | build: {index.BuildDuration.TotalMilliseconds:0} ms",
				$"Skipped entries: {index.SkippedEntries} | unavailable optional stats: {index.UnavailableStatistics}",
				index.WorldContextAvailable ? "Runtime calculators: world context available." : "Runtime calculators: menu context; a world-entry refresh is pending.",
				index.CoverageNote
			}.Where((string line) => !string.IsNullOrWhiteSpace(line)));
		}

		private WikiAnswer ItemInfo(WikiIndex index, WikiItem item, int requestedQuality)
		{
			int num = ClampQuality(item, requestedQuality);
			WikiQualityStats wikiQualityStats = item.StatsAt(num);
			List<string> list = new List<string>
			{
				$"Category: {item.Category} | quality: {num}/{item.MaxQuality}",
				$"Weight: {Number(item.Weight)} | stack: {item.MaxStack}"
			};
			if (item.Kind == WikiItemKind.Weapon)
			{
				if (wikiQualityStats.Damage != null)
				{
					list.Add("Listed total attack damage: " + Number(wikiQualityStats.Damage.ListedCombatTotal) + " (" + DamageSummary(wikiQualityStats.Damage) + ")");
				}
				else
				{
					list.Add("Listed total attack damage: unavailable for this quality.");
				}
				AddNullable(list, "Primary attack stamina", wikiQualityStats.PrimaryAttackStamina);
				AddNullable(list, "Secondary attack stamina", wikiQualityStats.SecondaryAttackStamina);
				AddNullable(list, "Durability", wikiQualityStats.Durability);
				AddNullable(list, "Block", wikiQualityStats.Block);
				AddNullable(list, "Parry bonus", item.ParryBonus, "x");
			}
			else if (item.Kind == WikiItemKind.Armor || item.Kind == WikiItemKind.Shield)
			{
				AddNullable(list, "Armor", wikiQualityStats.Armor);
				AddNullable(list, "Block", wikiQualityStats.Block);
				AddNullable(list, "Durability", wikiQualityStats.Durability);
				AddNullable(list, "Parry bonus", item.ParryBonus, "x");
			}
			else if (item.Kind == WikiItemKind.Food)
			{
				AddNullable(list, "Food health", item.FoodHealth);
				AddNullable(list, "Food stamina", item.FoodStamina);
				AddNullable(list, "Food eitr", item.FoodEitr);
				AddNullable(list, "Duration", item.FoodDurationSeconds, " s");
				AddNullable(list, "Regeneration", item.FoodRegeneration);
			}
			if (Math.Abs(item.MovementModifier) > 0.0001)
			{
				list.Add("Movement modifier: " + Number(item.MovementModifier * 100.0) + "%");
			}
			int count = index.RecipesForOutput(item.Id).Count;
			int count2 = index.ConversionsForOutput(item.Id).Count;
			int count3 = index.SourcesForOutput(item.Id).Count;
			list.Add((count == 0 && count2 == 0 && count3 == 0) ? "Production/acquisition: no indexed method found." : string.Format("Production/acquisition: {0} crafting recipe{1}, {2} processing method{3}, {4} direct source{5}.", count, (count == 1) ? string.Empty : "s", count2, (count2 == 1) ? string.Empty : "s", count3, (count3 == 1) ? string.Empty : "s"));
			AddIdentity(list, item);
			return new WikiAnswer(item.DisplayName, list, (requestedQuality == num) ? null : new string[1] { $"Requested quality {requestedQuality} was clamped to {num}." });
		}

		private static WikiAnswer Recipes(WikiIndex index, WikiItem item, WikiVisibilitySnapshot visibility)
		{
			IReadOnlyList<WikiRecipe> readOnlyList = index.RecipesForOutput(item.Id);
			IReadOnlyList<WikiConversion> readOnlyList2 = index.ConversionsForOutput(item.Id);
			if (readOnlyList.Count == 0 && readOnlyList2.Count == 0)
			{
				int count = index.SourcesForOutput(item.Id).Count;
				return new WikiAnswer(item.DisplayName, new string[1] { "No indexed crafting or input-processing method was found." }, (count <= 0) ? new string[1] { "The item may come from a currently unsupported source system." } : new string[1] { string.Format("{0} direct acquisition source{1} indexed; use /wiki source \"{2}\".", count, (count == 1) ? " is" : "s are", item.DisplayName) });
			}
			List<string> list = new List<string>();
			if (readOnlyList.Count > 0 && readOnlyList2.Count > 0)
			{
				list.Add("Crafting:");
			}
			for (int i = 0; i < readOnlyList.Count; i++)
			{
				WikiRecipe wikiRecipe = readOnlyList[i];
				if (readOnlyList.Count > 1)
				{
					list.Add($"Recipe {i + 1}: {wikiRecipe.Id}");
				}
				list.Add(CraftAt(wikiRecipe, 1));
				list.Add("Initial cost: " + Requirements(wikiRecipe.Requirements, 1, wikiRecipe.RequireOnlyOneIngredient, visibility));
				list.Add($"Output: {wikiRecipe.OutputAmount} | maximum quality: {item.MaxQuality}");
			}
			if (readOnlyList2.Count > 0)
			{
				if (readOnlyList.Count > 0)
				{
					list.Add("Processing:");
				}
				for (int j = 0; j < readOnlyList2.Count; j++)
				{
					WikiConversion wikiConversion = readOnlyList2[j];
					if (readOnlyList2.Count > 1)
					{
						list.Add($"Method {j + 1}: {wikiConversion.StationName} ({ProcessLabel(wikiConversion.Kind)})");
					}
					else
					{
						list.Add("Process at: " + wikiConversion.StationName + " (" + ProcessLabel(wikiConversion.Kind) + ")");
					}
					list.Add(ConversionRequirements(wikiConversion, visibility));
					string arg = (wikiConversion.DurationSeconds.HasValue ? (" | time: " + Number(wikiConversion.DurationSeconds.Value) + " s") : string.Empty);
					list.Add($"Output: {wikiConversion.OutputAmount} {wikiConversion.OutputName}{arg}");
				}
			}
			string[] suggestions = ((item.MaxQuality <= 1) ? null : new string[1] { "Use /wiki upgrades \"" + item.DisplayName + "\" for per-quality costs." });
			return new WikiAnswer(item.DisplayName, list, null, suggestions);
		}

		private static WikiAnswer Upgrades(WikiIndex index, WikiItem item, WikiVisibilitySnapshot visibility)
		{
			if (item.MaxQuality <= 1)
			{
				return new WikiAnswer(item.DisplayName, new string[1] { "This item has no meaningful quality progression." });
			}
			IReadOnlyList<WikiRecipe> readOnlyList = index.RecipesForOutput(item.Id);
			if (readOnlyList.Count == 0)
			{
				return new WikiAnswer(item.DisplayName, new string[1] { "No enabled ObjectDB recipe is available for upgrade costs." });
			}
			List<string> list = new List<string>();
			for (int i = 0; i < readOnlyList.Count; i++)
			{
				WikiRecipe wikiRecipe = readOnlyList[i];
				if (readOnlyList.Count > 1)
				{
					list.Add($"Recipe {i + 1}: {wikiRecipe.Id}");
				}
				for (int j = 2; j <= item.MaxQuality; j++)
				{
					list.Add($"To quality {j}: {Requirements(wikiRecipe.Requirements, j, wikiRecipe.RequireOnlyOneIngredient, visibility)} | {CraftAt(wikiRecipe, j)}");
				}
			}
			return new WikiAnswer(item.DisplayName + " upgrades", list, new string[1] { "Costs are per target quality, calculated by Valheim's loaded Requirement.GetAmount data." });
		}

		private static WikiAnswer Uses(WikiIndex index, WikiItem material, WikiVisibilitySnapshot visibility)
		{
			WikiRecipe[] array = (from recipe in index.RecipesUsing(material.Id)
				where visibility.IsVisible(recipe.OutputItemId)
				select recipe).OrderBy<WikiRecipe, string>((WikiRecipe recipe) => recipe.OutputName, StringComparer.CurrentCultureIgnoreCase).ToArray();
			WikiPiece[] array2 = index.PiecesUsing(material.Id).Where(visibility.IsVisible).OrderBy<WikiPiece, string>((WikiPiece piece) => piece.DisplayName, StringComparer.CurrentCultureIgnoreCase)
				.ToArray();
			WikiConversion[] array3 = (from conversion in index.ConversionsUsing(material.Id)
				where visibility.IsVisible(conversion.OutputItemId)
				select conversion).OrderBy<WikiConversion, string>((WikiConversion conversion) => conversion.OutputName, StringComparer.CurrentCultureIgnoreCase).ToArray();
			List<string> list = new List<string>();
			if (array.Length != 0)
			{
				list.Add("Crafting:");
				list.AddRange(array.Select((WikiRecipe recipe) => $"- {recipe.OutputName}: {RequirementAmount(recipe, material.Id, 1)} {material.DisplayName} | {StationText(recipe.StationName, recipe.StationLevelForTargetQuality(1))}"));
			}
			if (array2.Length != 0)
			{
				list.Add("Building:");
				list.AddRange(array2.Select((WikiPiece piece) => $"- {piece.DisplayName}: {piece.Requirements.First((WikiPieceRequirement requirement) => requirement.ItemId.Equals(material.Id, StringComparison.OrdinalIgnoreCase)).Amount} {material.DisplayName}"));
			}
			if (array3.Length != 0)
			{
				list.Add("Processing:");
				list.AddRange(array3.Select((WikiConversion conversion) => "- " + conversion.OutputName + ": " + ConversionUse(conversion, material.Id) + " | " + conversion.StationName + " (" + ProcessLabel(conversion.Kind) + ")"));
			}
			if (list.Count == 0)
			{
				list.Add("No indexed crafting recipe, build piece, or input-processing method consumes this item.");
			}
			return new WikiAnswer("Uses for " + material.DisplayName, list);
		}

		private WikiAnswer AcquisitionSources(WikiIndex index, WikiItem item, WikiSourceKind? sourceKind, WikiVisibilitySnapshot visibility)
		{
			WikiAcquisitionSource[] source = (from wikiAcquisitionSource in index.SourcesForOutput(item.Id)
				where !sourceKind.HasValue || wikiAcquisitionSource.Kind == sourceKind.Value
				orderby wikiAcquisitionSource.Kind
				select wikiAcquisitionSource).ThenBy<WikiAcquisitionSource, string>((WikiAcquisitionSource wikiAcquisitionSource) => wikiAcquisitionSource.SourceName, StringComparer.CurrentCultureIgnoreCase).ThenBy<WikiAcquisitionSource, string>((WikiAcquisitionSource wikiAcquisitionSource) => wikiAcquisitionSource.Id, StringComparer.OrdinalIgnoreCase).ToArray();
			WikiRecipe[] array = (sourceKind.HasValue ? Array.Empty<WikiRecipe>() : index.RecipesForOutput(item.Id).ToArray());
			WikiConversion[] array2 = (sourceKind.HasValue ? Array.Empty<WikiConversion>() : index.ConversionsForOutput(item.Id).ToArray());
			List<string> list = new List<string>();
			if (array.Length != 0)
			{
				list.Add("Crafted:");
				list.AddRange(array.Select((WikiRecipe recipe) => $"- {StationText(recipe.StationName, recipe.StationLevelForTargetQuality(1))}: {Requirements(recipe.Requirements, 1, recipe.RequireOnlyOneIngredient, visibility)} -> {recipe.OutputAmount} {item.DisplayName}"));
			}
			if (array2.Length != 0)
			{
				list.Add("Processed:");
				list.AddRange(array2.Select((WikiConversion conversion) => "- " + conversion.StationName + " (" + ProcessLabel(conversion.Kind) + "): " + ConversionSummary(conversion, visibility)));
			}
			foreach (IGrouping<WikiSourceKind, WikiAcquisitionSource> item2 in from wikiAcquisitionSource in source
				group wikiAcquisitionSource by wikiAcquisitionSource.Kind)
			{
				list.Add(SourceKindLabel(item2.Key) + ":");
				list.AddRange(from display in CollapseSourceVariants(item2)
					select "- " + SourceLabel(display.Source) + ": " + SourceDetails(display.Source, includeOutputName: false, visibility, display.VariantCount));
			}
			if (list.Count == 0)
			{
				string text = (sourceKind.HasValue ? SourceKindLabel(sourceKind.Value).ToLowerInvariant() : "acquisition");
				list.Add("No indexed " + text + " source was found.");
			}
			string[] warnings = ((!sourceKind.HasValue) ? new string[1] { "Chance-based, weighted, multi-output, or zero-minimum environmental tables, fish extra drops/bait probabilities, containers, locations, biome inference, and procedural loot are excluded." } : new string[1] { "Only " + SourceKindLabel(sourceKind.Value).ToLowerInvariant() + " sources were requested; other acquisition categories were not shown." });
			return new WikiAnswer("Sources for " + item.DisplayName, list, warnings);
		}

		private WikiAnswer SourceInventory(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (query.EntityTexts.Count == 0)
			{
				return new WikiAnswer("Missing source", new string[1] { "Please provide a creature or trader name." });
			}
			WikiSourceKind? kind = SourceKindFilter(query);
			WikiAcquisitionSource[] array = (from source in index.Sources
				where visibility.IsVisible(source.OutputItemId)
				where !kind.HasValue || source.Kind == kind.Value
				select source).ToArray();
			if (array.Length == 0)
			{
				return new WikiAnswer("No indexed sources", new string[1] { "No source entries of the requested category are present in the runtime index." });
			}
			string text = query.EntityTexts[0];
			string text2 = TextNormalizer.Normalize(text);
			if (kind == WikiSourceKind.Purchased)
			{
				switch (text2)
				{
				case "trader":
				case "traders":
				case "all traders":
					return SourceInventoryAnswer("Trader goods", array, visibility);
				}
			}
			SourceEntityResolution resolution = ResolveSourceEntity(array, text);
			if (resolution.Status == ResolutionStatus.Resolved)
			{
				WikiAcquisitionSource[] values = array.Where((WikiAcquisitionSource source) => string.Equals(source.SourceName, resolution.Names[0], StringComparison.CurrentCultureIgnoreCase)).ToArray();
				return SourceInventoryAnswer(resolution.Names[0] + " sources", values, visibility);
			}
			if (resolution.Status == ResolutionStatus.Ambiguous)
			{
				return new WikiAnswer("Several possible sources", resolution.Names.Select((string name, int indexValue) => $"{indexValue + 1}. {name}"), null, new string[1] { "Try an exact source name, for example /wiki " + ((kind == WikiSourceKind.Purchased) ? "sells" : "drops") + " \"" + resolution.Names[0] + "\"." });
			}
			return new WikiAnswer("Source not found", new string[1] { "No confident source match for '" + text + "'." }, null, new string[1] { (kind == WikiSourceKind.Purchased) ? "Try /wiki what does the trader sell." : "Check the creature name or use /wiki source <item>." });
		}

		private WikiAnswer SourceInventoryAnswer(string title, IEnumerable<WikiAcquisitionSource> values, WikiVisibilitySnapshot visibility)
		{
			IReadOnlyList<SourceDisplayGroup> readOnlyList = CollapseSourceVariants(values.OrderBy<WikiAcquisitionSource, string>((WikiAcquisitionSource source) => source.SourceName, StringComparer.CurrentCultureIgnoreCase).ThenBy<WikiAcquisitionSource, string>((WikiAcquisitionSource source) => source.OutputName, StringComparer.CurrentCultureIgnoreCase).ToArray());
			List<string> list = new List<string>();
			foreach (IGrouping<string, SourceDisplayGroup> item in readOnlyList.GroupBy<SourceDisplayGroup, string>((SourceDisplayGroup display) => display.Source.SourceName, StringComparer.CurrentCultureIgnoreCase))
			{
				if (readOnlyList.Select((SourceDisplayGroup display) => display.Source.SourceName).Distinct<string>(StringComparer.CurrentCultureIgnoreCase).Skip(1)
					.Any())
				{
					list.Add(SourceLabel(item.First().Source) + ":");
				}
				list.AddRange(item.Select((SourceDisplayGroup display) => "- " + SourceDetails(display.Source, includeOutputName: true, visibility, display.VariantCount)));
			}
			return new WikiAnswer($"{title} ({readOnlyList.Count})", list);
		}

		private SourceEntityResolution ResolveSourceEntity(IEnumerable<WikiAcquisitionSource> values, string text)
		{
			bool wasQuoted;
			string query = TextNormalizer.Unquote(text, out wasQuoted);
			string normalized = TextNormalizer.Normalize(query);
			var source = (from @group in values.GroupBy<WikiAcquisitionSource, string>((WikiAcquisitionSource wikiAcquisitionSource) => wikiAcquisitionSource.SourceName, StringComparer.CurrentCultureIgnoreCase)
				select new
				{
					Name = @group.Key,
					InternalNames = @group.Select((WikiAcquisitionSource wikiAcquisitionSource) => wikiAcquisitionSource.SourceInternalName).Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray()
				}).OrderBy(entity => entity.Name, StringComparer.CurrentCultureIgnoreCase).ToArray();
			string[] array = (from entity in source
				where string.Equals(entity.Name, query, StringComparison.CurrentCultureIgnoreCase) || TextNormalizer.Normalize(entity.Name) == normalized || entity.InternalNames.Any((string name) => string.Equals(name, query, StringComparison.OrdinalIgnoreCase) || TextNormalizer.Normalize(name) == normalized)
				select entity.Name).ToArray();
			if (array.Length != 0)
			{
				return new SourceEntityResolution((array.Length != 1) ? ResolutionStatus.Ambiguous : ResolutionStatus.Resolved, array);
			}
			if (wasQuoted || !options.FuzzyMatchingEnabled)
			{
				return new SourceEntityResolution(ResolutionStatus.NotFound, Array.Empty<string>());
			}
			var array2 = (from entity in source
				select new
				{
					Name = entity.Name,
					Score = new double[1] { FuzzyMatcher.Similarity(query, entity.Name) }.Concat(entity.InternalNames.Select((string name) => FuzzyMatcher.Similarity(query, name))).Max()
				} into candidate
				where candidate.Score >= options.FuzzyMatchThreshold
				orderby candidate.Score descending
				select candidate).ThenBy(candidate => candidate.Name, StringComparer.CurrentCultureIgnoreCase).Take(5).ToArray();
			if (array2.Length == 0)
			{
				return new SourceEntityResolution(ResolutionStatus.NotFound, Array.Empty<string>());
			}
			bool flag = array2.Length > 1 && array2[0].Score - array2[1].Score <= options.AmbiguityMargin;
			int status = (flag ? 1 : 0);
			IEnumerable<string> names;
			if (!flag)
			{
				IEnumerable<string> enumerable = new string[1] { array2[0].Name };
				names = enumerable;
			}
			else
			{
				names = array2.Select(candidate => candidate.Name);
			}
			return new SourceEntityResolution((ResolutionStatus)status, names);
		}

		private string SourceLabel(WikiAcquisitionSource source)
		{
			if (!options.ShowSourceMod || string.IsNullOrWhiteSpace(source.SourceMod))
			{
				return source.SourceName;
			}
			return source.SourceName + " [" + source.SourceMod + "]";
		}

		private static string SourceDetails(WikiAcquisitionSource source, bool includeOutputName, WikiVisibilitySnapshot visibility, int variantCount = 1)
		{
			string text = ((source.MinimumAmount == source.MaximumAmount) ? source.MinimumAmount.ToString(CultureInfo.InvariantCulture) : $"{source.MinimumAmount}-{source.MaximumAmount}");
			string text2 = (includeOutputName ? (" " + source.OutputName) : string.Empty);
			List<string> list = new List<string>();
			switch (source.Kind)
			{
			case WikiSourceKind.Purchased:
			{
				string text4 = (source.Price.HasValue ? string.Format(" for {0} {1}", source.Price.Value, (source.CostItemId == null) ? (source.CostItemName ?? "currency") : visibility.ItemName(source.CostItemId, source.CostItemName ?? "currency")) : string.Empty);
				list.Add(text + text2 + text4);
				break;
			}
			case WikiSourceKind.PassivelyProduced:
			{
				string text3 = (source.ProductionIntervalSeconds.HasValue ? (" per " + Number(source.ProductionIntervalSeconds.Value) + " s") : string.Empty);
				list.Add(text + text2 + text3);
				break;
			}
			case WikiSourceKind.Harvested:
				list.Add(source.WorldScaled ? ("base yield " + text + text2) : (text + text2));
				break;
			case WikiSourceKind.MinedOrDestroyed:
				list.Add(source.ConfiguredMinimumAmount.HasValue ? ("loaded yield " + text + text2) : (source.WorldScaled ? ("base yield " + text + text2) : (text + text2)));
				break;
			case WikiSourceKind.Fishing:
				list.Add(text + text2);
				break;
			default:
				list.Add(text + text2);
				break;
			}
			if (source.Chance.HasValue)
			{
				list.Add("chance " + Number(source.Chance.Value * 100.0) + "%");
			}
			if (source.Capacity.HasValue)
			{
				list.Add($"capacity {source.Capacity.Value}");
			}
			if (source.RespawnSeconds.HasValue)
			{
				list.Add("respawn " + Number(source.RespawnSeconds.Value / 60.0) + " min");
			}
			if (source.ConfiguredMinimumAmount.HasValue && source.ConfiguredMaximumAmount.HasValue)
			{
				string text5 = ((source.ConfiguredMinimumAmount.Value == source.ConfiguredMaximumAmount.Value) ? source.ConfiguredMinimumAmount.Value.ToString(CultureInfo.InvariantCulture) : $"{source.ConfiguredMinimumAmount.Value}-{source.ConfiguredMaximumAmount.Value}");
				if (source.ConfiguredMinimumAmount.Value != source.MinimumAmount || source.ConfiguredMaximumAmount.Value != source.MaximumAmount)
				{
					list.Add("configured base " + text5 + text2);
				}
				list.Add((source.QuantitySemantics == WikiQuantitySemantics.ExactLoadedRange) ? "classification: guaranteed variable" : "classification: guaranteed fixed");
			}
			if (source.LoadedResourceRate.HasValue && Math.Abs(source.LoadedResourceRate.Value - 1.0) > 0.0001)
			{
				list.Add("loaded resource rate " + Number(source.LoadedResourceRate.Value) + "x");
			}
			if (!string.IsNullOrWhiteSpace(source.UnlockRequirement))
			{
				list.Add("requires global key " + source.UnlockRequirement);
			}
			if (source.OnePerPlayer)
			{
				list.Add("one per player");
			}
			if (!string.IsNullOrWhiteSpace(source.Method))
			{
				list.Add("method: " + source.Method);
			}
			if (source.MinimumToolTier.HasValue)
			{
				list.Add($"minimum tool tier {source.MinimumToolTier.Value}");
			}
			if (source.LevelScaled)
			{
				list.Add("quantity can scale with creature level");
			}
			if (source.WorldScaled)
			{
				list.Add("world-rate scaled");
			}
			if (variantCount > 1)
			{
				list.Add($"{variantCount} loaded prefab variants");
			}
			list.Add("certainty: " + SourceCertaintyLabel(source.Certainty));
			return string.Join(" | ", list);
		}

		private static IReadOnlyList<SourceDisplayGroup> CollapseSourceVariants(IEnumerable<WikiAcquisitionSource> sources)
		{
			return (from @group in sources.GroupBy<WikiAcquisitionSource, string>(SourceDisplayKey, StringComparer.Ordinal)
				select new SourceDisplayGroup(@group.First(), @group.Count())).ToArray();
		}

		private static string SourceDisplayKey(WikiAcquisitionSource source)
		{
			return string.Join("\u001f", source.Kind.ToString(), source.Certainty.ToString(), source.Adapter, source.SourceName, source.SourceMod ?? string.Empty, source.OutputItemId, source.OutputName, source.MinimumAmount.ToString(CultureInfo.InvariantCulture), source.MaximumAmount.ToString(CultureInfo.InvariantCulture), NullableNumber(source.Chance), source.CostItemId ?? string.Empty, source.CostItemName ?? string.Empty, source.Price?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, source.UnlockRequirement ?? string.Empty, NullableNumber(source.ProductionIntervalSeconds), source.Capacity?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, NullableNumber(source.RespawnSeconds), source.OnePerPlayer.ToString(), source.LevelScaled.ToString(), source.WorldScaled.ToString(), source.Method ?? string.Empty, source.MinimumToolTier?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, source.QuantitySemantics.ToString(), source.ConfiguredMinimumAmount?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, source.ConfiguredMaximumAmount?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, NullableNumber(source.LoadedResourceRate));
		}

		private static string NullableNumber(double? value)
		{
			if (!value.HasValue)
			{
				return string.Empty;
			}
			return value.Value.ToString("R", CultureInfo.InvariantCulture);
		}

		private static WikiSourceKind? SourceKindFilter(WikiQuery query)
		{
			if (!query.Filters.TryGetValue("sourceKind", out string value))
			{
				return null;
			}
			if (!Enum.TryParse<WikiSourceKind>(value, ignoreCase: true, out var result))
			{
				return null;
			}
			return result;
		}

		private static string SourceKindLabel(WikiSourceKind kind)
		{
			return kind switch
			{
				WikiSourceKind.CreatureDrop => "Creature drop", 
				WikiSourceKind.Purchased => "Purchased", 
				WikiSourceKind.PassivelyProduced => "Passively produced", 
				WikiSourceKind.Harvested => "Harvested", 
				WikiSourceKind.MinedOrDestroyed => "Mined or destroyed", 
				_ => "Fishing", 
			};
		}

		private static string SourceCertaintyLabel(WikiSourceCertainty certainty)
		{
			return certainty switch
			{
				WikiSourceCertainty.Chance => "chance", 
				WikiSourceCertainty.Conditional => "conditional", 
				_ => "guaranteed", 
			};
		}

		private static WikiAnswer Station(WikiIndex index, WikiItem item)
		{
			IReadOnlyList<WikiRecipe> recipes = index.RecipesForOutput(item.Id);
			IReadOnlyList<WikiConversion> readOnlyList = index.ConversionsForOutput(item.Id);
			if (recipes.Count == 0 && readOnlyList.Count == 0)
			{
				return new WikiAnswer(item.DisplayName, new string[1] { "No indexed crafting or input-processing method was found." });
			}
			IEnumerable<string> lines = recipes.Select((WikiRecipe recipe, int indexValue) => (recipes.Count != 1) ? $"Recipe {indexValue + 1}: {CraftAt(recipe, 1)}" : CraftAt(recipe, 1)).Concat(readOnlyList.Select((WikiConversion conversion) => "Process at: " + conversion.StationName + " (" + ProcessLabel(conversion.Kind) + ")"));
			return new WikiAnswer("How to make " + item.DisplayName, lines);
		}

		private WikiAnswer StationQuery(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (query.EntityTexts.Count == 0)
			{
				return new WikiAnswer("Missing item or station", new string[1] { "Please provide an item or crafting-station name." });
			}
			string text = query.EntityTexts[0];
			StationResolution stationResolution = ResolveStation(index, text, allowFuzzy: false, visibility);
			if (stationResolution.Status != ResolutionStatus.NotFound)
			{
				return StationResolutionAnswer(index, stationResolution, visibility);
			}
			ResolutionResult resolutionResult = resolver.Resolve(index, text, visibility.IsVisible);
			if (resolutionResult.Status == ResolutionStatus.Resolved && resolutionResult.Item != null)
			{
				return Station(index, resolutionResult.Item);
			}
			if (resolutionResult.Status == ResolutionStatus.Ambiguous)
			{
				return ResolutionAnswer(text, resolutionResult);
			}
			StationResolution stationResolution2 = ResolveStation(index, text, allowFuzzy: true, visibility);
			if (stationResolution2.Status != ResolutionStatus.NotFound)
			{
				return StationResolutionAnswer(index, stationResolution2, visibility);
			}
			return new WikiAnswer("Item or station not found", new string[1] { "No confident item or crafting-station match for '" + text + "'." }, null, new string[1] { "Try /wiki station forge or /wiki station iron sword." });
		}

		private static WikiAnswer StationContents(WikiIndex index, string stationName, WikiVisibilitySnapshot visibility)
		{
			WikiRecipe[] array = (from @group in (from recipe in index.Recipes
					where string.Equals(recipe.StationName, stationName, StringComparison.CurrentCultureIgnoreCase)
					where visibility.IsVisible(recipe.OutputItemId)
					select recipe).GroupBy<WikiRecipe, string>((WikiRecipe recipe) => $"{recipe.OutputItemId}\u001f{recipe.StationLevelForTargetQuality(1)}", StringComparer.OrdinalIgnoreCase)
				select @group.First()).OrderBy<WikiRecipe, string>((WikiRecipe recipe) => recipe.OutputName, StringComparer.CurrentCultureIgnoreCase).ToArray();
			WikiPiece[] array2 = (from @group in index.Pieces.Where((WikiPiece piece) => string.Equals(piece.StationName, stationName, StringComparison.CurrentCultureIgnoreCase)).Where(visibility.IsVisible).GroupBy<WikiPiece, string>((WikiPiece piece) => piece.Id, StringComparer.OrdinalIgnoreCase)
				select @group.First()).OrderBy<WikiPiece, string>((WikiPiece piece) => piece.DisplayName, StringComparer.CurrentCultureIgnoreCase).ToArray();
			WikiConversion[] array3 = (from @group in (from conversion in index.Conversions
					where string.Equals(conversion.StationName, stationName, StringComparison.CurrentCultureIgnoreCase)
					where visibility.IsVisible(conversion.OutputItemId)
					select conversion).GroupBy<WikiConversion, string>((WikiConversion conversion) => conversion.Id, StringComparer.OrdinalIgnoreCase)
				select @group.First()).OrderBy<WikiConversion, string>((WikiConversion conversion) => conversion.OutputName, StringComparer.CurrentCultureIgnoreCase).ToArray();
			List<string> list = new List<string>();
			if (array.Length != 0)
			{
				list.Add("Crafting:");
				list.AddRange(array.Select((WikiRecipe recipe) => $"- {recipe.OutputName} (level {recipe.StationLevelForTargetQuality(1)})"));
			}
			if (array2.Length != 0)
			{
				list.Add("Building:");
				list.AddRange(array2.Select((WikiPiece piece) => "- " + piece.DisplayName));
			}
			if (array3.Length != 0)
			{
				list.Add("Processing:");
				list.AddRange(array3.Select((WikiConversion conversion) => "- " + ConversionSummary(conversion, visibility)));
			}
			if (list.Count == 0)
			{
				list.Add("No indexed recipe, build piece, or processing conversion uses this station.");
			}
			return new WikiAnswer($"{stationName} uses ({array.Length + array2.Length + array3.Length})", list, (array.Length == 0) ? null : new string[1] { "Crafting levels shown are the minimum for quality 1." });
		}

		private StationResolution ResolveStation(WikiIndex index, string text, bool allowFuzzy, WikiVisibilitySnapshot visibility)
		{
			bool wasQuoted;
			string query = TextNormalizer.Unquote(text, out wasQuoted);
			string normalized = TextNormalizer.Normalize(query);
			string[] source = (from name in (from recipe in index.Recipes
					where visibility.IsVisible(recipe.OutputItemId)
					select recipe.StationName).Concat<string>(from piece in index.Pieces.Where(visibility.IsVisible)
					select piece.StationName).Concat(from conversion in index.Conversions
					where visibility.IsVisible(conversion.OutputItemId)
					select conversion.StationName)
				where !string.IsNullOrWhiteSpace(name)
				select (name)).Distinct<string>(StringComparer.CurrentCultureIgnoreCase).OrderBy<string, string>((string name) => name, StringComparer.CurrentCultureIgnoreCase).ToArray();
			string[] array = source.Where((string name) => string.Equals(name, query, StringComparison.CurrentCultureIgnoreCase)).ToArray();
			if (array.Length == 0)
			{
				array = source.Where((string name) => TextNormalizer.Normalize(name) == normalized).ToArray();
			}
			if (array.Length != 0)
			{
				return new StationResolution((array.Length != 1) ? ResolutionStatus.Ambiguous : ResolutionStatus.Resolved, array, "exact station name");
			}
			if (!allowFuzzy || wasQuoted || !options.FuzzyMatchingEnabled)
			{
				return new StationResolution(ResolutionStatus.NotFound, Array.Empty<string>(), "no exact station match");
			}
			var array2 = (from name in source
				select new
				{
					Name = name,
					Score = FuzzyMatcher.Similarity(query, name)
				} into candidate
				where candidate.Score >= options.FuzzyMatchThreshold
				orderby candidate.Score descending
				select candidate).ThenBy(candidate => candidate.Name, StringComparer.CurrentCultureIgnoreCase).Take(5).ToArray();
			if (array2.Length == 0)
			{
				return new StationResolution(ResolutionStatus.NotFound, Array.Empty<string>(), "no fuzzy station match");
			}
			bool flag = array2.Length > 1 && array2[0].Score - array2[1].Score <= options.AmbiguityMargin;
			int status = (flag ? 1 : 0);
			IEnumerable<string> names;
			if (!flag)
			{
				IEnumerable<string> enumerable = new string[1] { array2[0].Name };
				names = enumerable;
			}
			else
			{
				names = array2.Select(candidate => candidate.Name);
			}
			return new StationResolution((ResolutionStatus)status, names, flag ? "station matches are within the ambiguity margin" : "fuzzy station name");
		}

		private static WikiAnswer StationResolutionAnswer(WikiIndex index, StationResolution resolution, WikiVisibilitySnapshot visibility)
		{
			if (resolution.Status == ResolutionStatus.Resolved)
			{
				return StationContents(index, resolution.Names[0], visibility);
			}
			return new WikiAnswer("Several possible stations", resolution.Names.Select((string name, int candidateIndex) => $"{candidateIndex + 1}. {name}"), null, new string[1] { "Try an exact station name, for example /wiki station \"" + resolution.Names[0] + "\"." });
		}

		private WikiAnswer Compare(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (query.EntityTexts.Count != 2)
			{
				return new WikiAnswer("Comparison", new string[1] { "Provide exactly two item names." });
			}
			ResolutionResult resolutionResult = resolver.Resolve(index, query.EntityTexts[0], visibility.IsVisible);
			if (resolutionResult.Status != ResolutionStatus.Resolved || resolutionResult.Item == null)
			{
				return ResolutionAnswer(query.EntityTexts[0], resolutionResult);
			}
			ResolutionResult resolutionResult2 = resolver.Resolve(index, query.EntityTexts[1], visibility.IsVisible);
			if (resolutionResult2.Status != ResolutionStatus.Resolved || resolutionResult2.Item == null)
			{
				return ResolutionAnswer(query.EntityTexts[1], resolutionResult2);
			}
			WikiItem item = resolutionResult.Item;
			WikiItem item2 = resolutionResult2.Item;
			int num = query.Quality ?? 1;
			int num2 = ClampQuality(item, num);
			int num3 = ClampQuality(item2, num);
			WikiQualityStats leftStats = item.StatsAt(num2);
			WikiQualityStats rightStats = item2.StatsAt(num3);
			if (query.Filters.TryGetValue("metric", out string value))
			{
				return CompareByMetric(item, item2, leftStats, rightStats, num2, num3, num, value);
			}
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			if (item.Kind == WikiItemKind.Weapon && item2.Kind == WikiItemKind.Weapon)
			{
				if (leftStats.Damage != null && rightStats.Damage != null)
				{
					list.Add(Pair("Listed total attack damage", leftStats.Damage.ListedCombatTotal, rightStats.Damage.ListedCombatTotal, item, item2));
					foreach (DamageKind item3 in CombatDamageOrder.Where((DamageKind kind) => leftStats.Damage.HasPositive(kind) || rightStats.Damage.HasPositive(kind)))
					{
						list.Add(Pair(item3.ToString(), leftStats.Damage.Get(item3), rightStats.Damage.Get(item3), item, item2));
					}
				}
				else
				{
					list2.Add("Loaded damage data was unavailable for one or both requested qualities, so no damage ranking is shown.");
				}
				AddPair(list, "Primary attack stamina", leftStats.PrimaryAttackStamina, rightStats.PrimaryAttackStamina, item, item2);
				AddPair(list, "Secondary attack stamina", leftStats.SecondaryAttackStamina, rightStats.SecondaryAttackStamina, item, item2);
				AddPair(list, "Durability", leftStats.Durability, rightStats.Durability, item, item2);
				AddPair(list, "Block", leftStats.Block, rightStats.Block, item, item2);
				AddPair(list, "Parry bonus", item.ParryBonus, item2.ParryBonus, item, item2, "x");
				list.Add(Pair("Movement modifier %", item.MovementModifier * 100.0, item2.MovementModifier * 100.0, item, item2));
				if (options.ShowComparisonAssumptions)
				{
					list2.Add("Listed damage excludes chop/pickaxe and does not model resistances, status effects, skill scaling, attack chains, or real DPS.");
				}
			}
			else if (item.Kind == WikiItemKind.Armor && item2.Kind == WikiItemKind.Armor)
			{
				AddPair(list, "Armor", leftStats.Armor, rightStats.Armor, item, item2);
				AddPair(list, "Durability", leftStats.Durability, rightStats.Durability, item, item2);
				list.Add(Pair("Movement modifier %", item.MovementModifier * 100.0, item2.MovementModifier * 100.0, item, item2));
				list.Add(Pair("Weight", item.Weight, item2.Weight, item, item2));
			}
			else if (item.Kind == WikiItemKind.Shield && item2.Kind == WikiItemKind.Shield)
			{
				AddPair(list, "Block", leftStats.Block, rightStats.Block, item, item2);
				AddPair(list, "Durability", leftStats.Durability, rightStats.Durability, item, item2);
				AddPair(list, "Parry bonus", item.ParryBonus, item2.ParryBonus, item, item2, "x");
				list.Add(Pair("Movement modifier %", item.MovementModifier * 100.0, item2.MovementModifier * 100.0, item, item2));
				list.Add(Pair("Weight", item.Weight, item2.Weight, item, item2));
			}
			else if (item.Kind == WikiItemKind.Food && item2.Kind == WikiItemKind.Food)
			{
				AddPair(list, "Health", item.FoodHealth, item2.FoodHealth, item, item2);
				AddPair(list, "Stamina", item.FoodStamina, item2.FoodStamina, item, item2);
				AddPair(list, "Eitr", item.FoodEitr, item2.FoodEitr, item, item2);
				AddPair(list, "Duration seconds", item.FoodDurationSeconds, item2.FoodDurationSeconds, item, item2);
				AddPair(list, "Regeneration", item.FoodRegeneration, item2.FoodRegeneration, item, item2);
			}
			else
			{
				list.Add(Pair("Weight", item.Weight, item2.Weight, item, item2));
				list.Add("Category: " + item.DisplayName + " " + item.Category + " | " + item2.DisplayName + " " + item2.Category);
				list2.Add("These items do not share a supported comparison profile, so no overall ranking is given.");
			}
			list.Add($"Quality assumption: {item.DisplayName} q{num2} | {item2.DisplayName} q{num3}");
			if (num2 != num || num3 != num)
			{
				list2.Add("Requested quality was clamped independently to each item's maximum quality.");
			}
			return new WikiAnswer(item.DisplayName + " vs " + item2.DisplayName, list, list2);
		}

		private static WikiAnswer CompareByMetric(WikiItem left, WikiItem right, WikiQualityStats leftStats, WikiQualityStats rightStats, int leftQuality, int rightQuality, int requestedQuality, string metricText)
		{
			if (!ComparisonMetricCatalog.TryResolve(metricText, left, right, out ComparisonMetricDefinition metric, out string diagnostic) || metric == null)
			{
				return UnsupportedMetric(diagnostic);
			}
			if (!ComparisonMetricCatalog.TryValue(metric, left, leftStats, out var value) || !ComparisonMetricCatalog.TryValue(metric, right, rightStats, out var value2))
			{
				return new WikiAnswer(left.DisplayName + " vs " + right.DisplayName, new string[1] { metric.Name + " is unavailable or inapplicable for one or both loaded items at the requested quality." }, null, new string[1] { "Supported metrics: " + ComparisonMetricCatalog.SupportedSummary + "." });
			}
			int num = ComparisonMetricCatalog.Compare(metric, value, value2);
			string text = ((num == 0) ? ("Result: tied on " + metric.Name.ToLowerInvariant() + ".") : ("Result: " + ((num > 0) ? left.DisplayName : right.DisplayName) + " ranks higher on this selected metric by " + Number(Math.Abs(value - value2)) + metric.Suffix + "."));
			List<string> list = new List<string> { metric.Caveat };
			if (leftQuality != requestedQuality || rightQuality != requestedQuality)
			{
				list.Add("Requested quality was clamped independently to each item's maximum quality.");
			}
			return new WikiAnswer(left.DisplayName + " vs " + right.DisplayName, new string[4]
			{
				"Selected metric: " + metric.Name + " (" + metric.DirectionLabel + ").",
				Pair(metric.Name, value, value2, left, right, metric.Suffix),
				text,
				$"Quality assumption: {left.DisplayName} q{leftQuality} | {right.DisplayName} q{rightQuality}"
			}, list);
		}

		private WikiAnswer ExplainComparison(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (query.EntityTexts.Count != 2)
			{
				return new WikiAnswer("Why comparison", new string[1] { "Provide exactly two item names." });
			}
			ResolutionResult resolutionResult = resolver.Resolve(index, query.EntityTexts[0], visibility.IsVisible);
			if (resolutionResult.Status != ResolutionStatus.Resolved || resolutionResult.Item == null)
			{
				return ResolutionAnswer(query.EntityTexts[0], resolutionResult);
			}
			ResolutionResult resolutionResult2 = resolver.Resolve(index, query.EntityTexts[1], visibility.IsVisible);
			if (resolutionResult2.Status != ResolutionStatus.Resolved || resolutionResult2.Item == null)
			{
				return ResolutionAnswer(query.EntityTexts[1], resolutionResult2);
			}
			WikiItem item = resolutionResult.Item;
			WikiItem item2 = resolutionResult2.Item;
			int num = query.Quality ?? 1;
			int num2 = ClampQuality(item, num);
			int num3 = ClampQuality(item2, num);
			if (!ComparisonMetricCatalog.TryResolve(query.Filters.TryGetValue("metric", out string value) ? value : "damage", item, item2, out ComparisonMetricDefinition metric, out string diagnostic) || metric == null)
			{
				return UnsupportedMetric(diagnostic);
			}
			if (!ComparisonMetricCatalog.TryValue(metric, item, item.StatsAt(num2), out var value2) || !ComparisonMetricCatalog.TryValue(metric, item2, item2.StatsAt(num3), out var value3))
			{
				return new WikiAnswer("Comparison explanation unavailable", new string[1] { metric.Name + " is unavailable or inapplicable for one or both loaded items." });
			}
			int num4 = ComparisonMetricCatalog.Compare(metric, value2, value3);
			string text = ((num4 > 0) ? ("The loaded values support the premise: " + item.DisplayName + " ranks higher on this metric by " + Number(Math.Abs(value2 - value3)) + metric.Suffix + ".") : ((num4 >= 0) ? ("The loaded values do not support a stronger claim: both items are tied on " + metric.Name.ToLowerInvariant() + ".") : ("The loaded values do not support the premise: " + item2.DisplayName + " ranks higher on this metric by " + Number(Math.Abs(value2 - value3)) + metric.Suffix + ".")));
			List<string> list = new List<string> { metric.Caveat };
			if (num2 != num || num3 != num)
			{
				list.Add("Requested quality was clamped independently to each item's maximum quality.");
			}
			return new WikiAnswer("Why " + item.DisplayName + " versus " + item2.DisplayName, new string[5]
			{
				"Selected metric: " + metric.Name + " (" + metric.DirectionLabel + ").",
				$"{item.DisplayName} q{num2}: {Number(value2)}{metric.Suffix}",
				$"{item2.DisplayName} q{num3}: {Number(value3)}{metric.Suffix}",
				text,
				"Explanation source: deterministic loaded runtime fields only."
			}, list);
		}

		private WikiAnswer RankCategory(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (!query.Filters.TryGetValue("category", out string value) || !query.Filters.TryGetValue("metric", out string value2))
			{
				return new WikiAnswer("Category comparison", new string[1] { "Provide a supported category and explicit metric." });
			}
			WikiItem[] array = CategoryCandidates(index, value).Where(visibility.IsVisible).ToArray();
			if (array.Length == 0)
			{
				return new WikiAnswer("Unsupported comparison category", new string[1] { "No supported loaded category matched '" + value + "'." }, null, new string[1] { "Try foods, shields, armor, weapons, swords, axes, bows, spears, clubs, knives, or polearms." });
			}
			if (!ComparisonMetricCatalog.TryResolve(value2, array[0], array[0], out ComparisonMetricDefinition metric, out string diagnostic) || metric == null)
			{
				return UnsupportedMetric(diagnostic);
			}
			int requestedQuality = query.Quality ?? 1;
			var array2 = (from anon in array.Select((WikiItem item) => new
				{
					Item = item,
					Quality = ClampQuality(item, requestedQuality)
				}).Select(anon =>
				{
					double value3;
					bool available = ComparisonMetricCatalog.TryValue(metric, anon.Item, anon.Item.StatsAt(anon.Quality), out value3);
					return new
					{
						Item = anon.Item,
						Quality = anon.Quality,
						Available = available,
						Value = value3
					};
				})
				where anon.Available
				select anon).ToArray();
			if (array2.Length == 0)
			{
				return new WikiAnswer(value + " by " + metric.Name, new string[1] { "The selected metric is unavailable or inapplicable for every loaded item in this category." });
			}
			var source = (metric.HigherRanksFirst ? array2.OrderByDescending(anon => anon.Value).ThenBy(anon => anon.Item.DisplayName, StringComparer.CurrentCultureIgnoreCase) : array2.OrderBy(anon => anon.Value).ThenBy(anon => anon.Item.DisplayName, StringComparer.CurrentCultureIgnoreCase));
			int count = Math.Max(1, options.MaximumAlternativeResults);
			var array3 = source.Take(count).ToArray();
			List<string> list = new List<string>();
			list.Add($"Metric: {metric.Name}; {metric.DirectionLabel}. Showing {array3.Length} of {array2.Length} loaded items with an available value.");
			list.Add(metric.Caveat);
			List<string> list2 = list;
			if (array3.Any(anon => anon.Quality != requestedQuality))
			{
				list2.Add("Requested quality was clamped independently to each item's loaded maximum quality.");
			}
			return new WikiAnswer("Top " + value + " by " + metric.Name, array3.Select((anon, int rank) => $"{rank + 1}. {Label(anon.Item)} q{anon.Quality}: {Number(anon.Value)}{metric.Suffix}"), list2);
		}

		private static IEnumerable<WikiItem> CategoryCandidates(WikiIndex index, string categoryText)
		{
			string text = TextNormalizer.Normalize(categoryText);
			switch (text)
			{
			case "foods":
			case "food":
				return index.Items.Values.Where((WikiItem item) => item.Kind == WikiItemKind.Food);
			case "shield":
			case "shields":
				return index.Items.Values.Where((WikiItem item) => item.Kind == WikiItemKind.Shield);
			case "armor":
			case "armour":
				return index.Items.Values.Where((WikiItem item) => item.Kind == WikiItemKind.Armor);
			case "weapon":
			case "weapons":
				return index.Items.Values.Where((WikiItem item) => item.Kind == WikiItemKind.Weapon);
			default:
			{
				string category = text.TrimEnd(new char[1] { 's' });
				if (category == "mace")
				{
					category = "club";
				}
				bool flag;
				switch (category)
				{
				case "knife":
				case "spear":
				case "sword":
				case "axe":
				case "bow":
				case "club":
				case "polearm":
					flag = true;
					break;
				default:
					flag = false;
					break;
				}
				if (!flag)
				{
					return Array.Empty<WikiItem>();
				}
				return index.Items.Values.Where((WikiItem item) => item.Kind == WikiItemKind.Weapon && CategoryMatches(item.Category, category));
			}
			}
		}

		private WikiAnswer Stronger(WikiIndex index, WikiItem baseline, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			string value;
			bool flag = query.Filters.TryGetValue("metric", out value);
			if (!flag && baseline.Kind != WikiItemKind.Weapon)
			{
				return new WikiAnswer("Stronger than " + baseline.DisplayName, new string[1] { "Unqualified stronger-than is defined only for weapons; select a supported metric for other item profiles." });
			}
			int requestedQuality = query.Quality ?? 1;
			int num = ClampQuality(baseline, requestedQuality);
			if (!ComparisonMetricCatalog.TryResolve(flag ? value : "damage", baseline, baseline, out ComparisonMetricDefinition metric, out string diagnostic) || metric == null)
			{
				return UnsupportedMetric(diagnostic);
			}
			if (!ComparisonMetricCatalog.TryValue(metric, baseline, baseline.StatsAt(num), out var baselineValue))
			{
				return new WikiAnswer("Alternatives to " + baseline.DisplayName, new string[1] { metric.Name + " is unavailable or inapplicable for the loaded baseline item." });
			}
			var array = (from anon in (from item in index.Items.Values.Where(visibility.IsVisible)
					where !item.Id.Equals(baseline.Id, StringComparison.OrdinalIgnoreCase)
					where ComparableAlternative(baseline, item)
					select new
					{
						Item = item,
						Quality = ClampQuality(item, requestedQuality)
					}).Select(anon =>
				{
					double value2;
					bool available = ComparisonMetricCatalog.TryValue(metric, anon.Item, anon.Item.StatsAt(anon.Quality), out value2);
					return new
					{
						Item = anon.Item,
						Quality = anon.Quality,
						Available = available,
						Value = value2
					};
				})
				where anon.Available
				select new
				{
					Item = anon.Item,
					Quality = anon.Quality,
					Value = anon.Value,
					Improvement = ComparisonMetricCatalog.Improvement(metric, anon.Value, baselineValue)
				} into anon
				where anon.Improvement > 0.0
				orderby anon.Improvement
				select anon).ThenBy(anon => anon.Item.DisplayName, StringComparer.CurrentCultureIgnoreCase).Take(Math.Max(1, options.MaximumAlternativeResults)).ToArray();
			string[] lines = ((array.Length != 0) ? array.Select(anon => $"- {Label(anon.Item)} q{anon.Quality}: {Number(anon.Value)}{metric.Suffix} (improves by {Number(anon.Improvement)}{metric.Suffix})").ToArray() : new string[1] { "No comparable indexed item strictly improves on " + Number(baselineValue) + metric.Suffix + " " + metric.Name.ToLowerInvariant() + "." });
			List<string> list = new List<string>();
			list.Add(string.Format("Metric: {0} at requested quality {1}; {2}. {3}", metric.Name, requestedQuality, metric.DirectionLabel, (baseline.Kind == WikiItemKind.Weapon) ? "Weapon alternatives stay in the same category." : "Alternatives stay in the same item profile."));
			list.Add(metric.Caveat);
			List<string> list2 = list;
			if (num != requestedQuality || array.Any(anon => anon.Quality != requestedQuality))
			{
				list2.Add("Requested quality was clamped independently to loaded maximum quality.");
			}
			if (!flag && requestedQuality == 1)
			{
				list2.Add("Default stronger metric: quality-1 listed combat damage in the same weapon category.");
			}
			return new WikiAnswer("Alternatives by " + metric.Name + " for " + baseline.DisplayName, lines, list2);
		}

		private static bool ComparableAlternative(WikiItem baseline, WikiItem candidate)
		{
			if (baseline.Kind == candidate.Kind)
			{
				if (baseline.Kind == WikiItemKind.Weapon)
				{
					return string.Equals(baseline.Category, candidate.Category, StringComparison.OrdinalIgnoreCase);
				}
				return true;
			}
			return false;
		}

		private static WikiAnswer UnsupportedMetric(string diagnostic)
		{
			return new WikiAnswer("Unsupported comparison metric", new string[1] { diagnostic }, null, new string[1] { "Try: by slash, by stamina, by block, by armor, or by weight. Supported metrics: " + ComparisonMetricCatalog.SupportedSummary + "." });
		}

		private WikiAnswer Find(WikiIndex index, WikiQuery query, WikiVisibilitySnapshot visibility)
		{
			if (!query.Filters.TryGetValue("expression", out string value) || string.IsNullOrWhiteSpace(value))
			{
				return new WikiAnswer("Find", new string[1] { "Provide a category or supported filter." });
			}
			FilterExpression parsed = FilterExpression.Parse(value);
			if (!parsed.IsSupported)
			{
				return new WikiAnswer("Unsupported filter", new string[1] { parsed.Diagnostic }, null, new string[3] { "Try: /wiki find swords", "/wiki foods above 70 health", "/wiki find weapons with frost" });
			}
			IEnumerable<WikiItem> source = index.Items.Values.Where(visibility.IsVisible);
			if (parsed.Kind.HasValue)
			{
				source = source.Where((WikiItem item) => item.Kind == parsed.Kind.Value);
			}
			string category = parsed.Category;
			if (!string.IsNullOrEmpty(category))
			{
				source = source.Where((WikiItem item) => CategoryMatches(item.Category, category));
			}
			if (parsed.Damage.HasValue)
			{
				source = source.Where((WikiItem item) => item.StatsAt(1).Damage?.HasPositive(parsed.Damage.Value) ?? false);
			}
			if (parsed.NumericField != null && parsed.NumericValue.HasValue && parsed.Operator != null)
			{
				source = source.Where((WikiItem item) => NumericFilter(GetNumeric(item, parsed.NumericField), parsed.Operator, parsed.NumericValue.Value));
			}
			WikiItem[] array = source.OrderBy<WikiItem, string>((WikiItem item) => item.DisplayName, StringComparer.CurrentCultureIgnoreCase).ToArray();
			if (array.Length == 0)
			{
				return new WikiAnswer("Find results", new string[1] { "No indexed items matched the supported filter." });
			}
			return new WikiAnswer($"Find results ({array.Length})", array.Select((WikiItem item) => (parsed.NumericField != null) ? ("- " + Label(item) + " | " + item.Category + " | " + NumericFieldLabel(parsed.NumericField, item) + " " + Number(GetNumeric(item, parsed.NumericField).Value)) : ("- " + Label(item) + " | " + item.Category)));
		}

		private WikiAnswer Diagnostics(WikiIndex index, WikiVisibilitySnapshot visibility)
		{
			WikiItem[] array = index.Items.Values.Where(visibility.IsVisible).ToArray();
			WikiPiece[] array2 = index.Pieces.Where(visibility.IsVisible).ToArray();
			IGrouping<string, WikiItem>[] array3 = (from @group in array.GroupBy<WikiItem, string>((WikiItem item) => item.DisplayName, StringComparer.CurrentCultureIgnoreCase)
				where @group.Count() > 1
				select @group).OrderBy<IGrouping<string, WikiItem>, string>((IGrouping<string, WikiItem> group) => group.Key, StringComparer.CurrentCultureIgnoreCase).ToArray();
			int num = CountDanglingEdges(index);
			int num2 = index.Recipes.Count((WikiRecipe recipe) => recipe.Requirements.Count == 0);
			int num3 = index.Conversions.Count((WikiConversion conversion) => conversion.Requirements.Count == 0);
			int num4 = array.Count((WikiItem item) => !string.IsNullOrWhiteSpace(item.SourceMod));
			List<string> list = new List<string>();
			list.Add($"Visibility: {visibility.Policy} | items {array.Length}/{index.Items.Count} | pieces {array2.Length}/{index.Pieces.Count}.");
			list.Add($"Build isolation: {index.SkippedEntries} skipped entries | {index.UnavailableStatistics} unavailable optional statistics.");
			list.Add($"Retained issue records: {index.Diagnostics.Count} | omitted after hard bound: {index.OmittedDiagnostics}.");
			list.Add(string.Format("Relationship integrity: {0} dangling item reference{1} | recipes with no listed requirements: {2} | conversions with no listed inputs: {3}.", num, (num == 1) ? string.Empty : "s", num2, num3));
			list.Add(string.Format("Localized-name ambiguity: {0} visible duplicate group{1}.", array3.Length, (array3.Length == 1) ? string.Empty : "s"));
			list.Add($"Item mod attribution: {num4}/{array.Length} visible items attributed; unattributed can mean vanilla or startup detection disabled.");
			list.Add($"Knowledge/context isolation: {visibility.EvaluationErrors} visibility evaluation errors in this query snapshot.");
			List<string> list2 = list;
			list2.AddRange(from @group in array3.Take(5)
				select $"- Duplicate name: {@group.Key} ({@group.Count()} visible item records)");
			if (array3.Length > 5)
			{
				list2.Add($"{array3.Length - 5} additional duplicate-name groups are available through paginated diagnostics in a future release; this summary is bounded to five.");
			}
			return new WikiAnswer("Diagnostics", list2, new string[1] { "Counts describe the immutable loaded index and current query visibility. Use /wiki unsupported for bounded issue details and /wiki mods for attribution groups." });
		}

		private WikiAnswer ModReport(WikiIndex index, WikiVisibilitySnapshot visibility)
		{
			WikiItem[] source = index.Items.Values.Where(visibility.IsVisible).ToArray();
			WikiAcquisitionSource[] source2 = index.Sources.Where((WikiAcquisitionSource wikiAcquisitionSource) => visibility.IsVisible(wikiAcquisitionSource.OutputItemId)).ToArray();
			var array = (from @group in source.GroupBy<WikiItem, string>((WikiItem item) => (!string.IsNullOrWhiteSpace(item.SourceMod)) ? item.SourceMod : "Unattributed (vanilla or detection disabled)", StringComparer.CurrentCultureIgnoreCase)
				select new
				{
					Name = @group.Key,
					Items = @group.Count()
				} into @group
				orderby @group.Items descending
				select @group).ThenBy(group => group.Name, StringComparer.CurrentCultureIgnoreCase).ToArray();
			Dictionary<string, int> sourceGroups = source2.Where((WikiAcquisitionSource wikiAcquisitionSource) => !string.IsNullOrWhiteSpace(wikiAcquisitionSource.SourceMod)).GroupBy<WikiAcquisitionSource, string>((WikiAcquisitionSource wikiAcquisitionSource) => wikiAcquisitionSource.SourceMod, StringComparer.CurrentCultureIgnoreCase).ToDictionary<IGrouping<string, WikiAcquisitionSource>, string, int>((IGrouping<string, WikiAcquisitionSource> group) => group.Key, (IGrouping<string, WikiAcquisitionSource> group) => group.Count(), StringComparer.CurrentCultureIgnoreCase);
			var array2 = array.Take(20).ToArray();
			int value;
			List<string> list = array2.Select(group => string.Format("- {0}: {1} item{2}{3}", group.Name, group.Items, (group.Items == 1) ? string.Empty : "s", sourceGroups.TryGetValue(group.Name, out value) ? string.Format(" | {0} direct source edge{1}", value, (value == 1) ? string.Empty : "s") : string.Empty)).ToList();
			if (array.Length == 0)
			{
				list.Add("No visible item records are available under the current policy.");
			}
			if (array.Length > array2.Length)
			{
				list.Add($"{array.Length - array2.Length} additional attribution groups were omitted by the {20}-row bound.");
			}
			if (!source.Any((WikiItem item) => !string.IsNullOrWhiteSpace(item.SourceMod)))
			{
				list.Add("No item source-mod labels are available. EnableModSourceDetection must be enabled before startup for best-effort Jotunn attribution.");
			}
			return new WikiAnswer($"Indexed mods ({array.Length} groups)", list, new string[1] { $"Visibility: {visibility.Policy}. Attribution is best effort and does not change indexed facts." }, null, isPaginated: true);
		}

		private WikiAnswer UnsupportedReport(WikiIndex index, WikiVisibilitySnapshot visibility)
		{
			IGrouping<string, WikiDiagnosticIssue>[] array = (from @group in index.Diagnostics.Where((WikiDiagnosticIssue issue) => issue.Kind == WikiDiagnosticKind.SkippedEntry).GroupBy<WikiDiagnosticIssue, string>((WikiDiagnosticIssue issue) => issue.Category, StringComparer.CurrentCultureIgnoreCase)
				orderby @group.Count() descending
				select @group).ThenBy<IGrouping<string, WikiDiagnosticIssue>, string>((IGrouping<string, WikiDiagnosticIssue> group) => group.Key, StringComparer.CurrentCultureIgnoreCase).ToArray();
			WikiDiagnosticIssue[] array2 = index.Diagnostics.Where((WikiDiagnosticIssue issue) => !visibility.IsRestricted || (issue.RelatedItemId != null && visibility.IsVisible(issue.RelatedItemId))).Take(20).ToArray();
			List<string> list = new List<string>
			{
				$"Full-index isolation totals: {index.SkippedEntries} skipped entries | {index.UnavailableStatistics} unavailable optional statistics.",
				$"Retained issue records: {index.Diagnostics.Count} | omitted after 256: {index.OmittedDiagnostics}."
			};
			if (array.Length != 0)
			{
				list.Add("Skipped categories: " + string.Join(", ", from @group in array.Take(10)
					select $"{@group.Key} {@group.Count()}") + ((array.Length > 10) ? $"; {array.Length - 10} more categories" : string.Empty) + ".");
			}
			WikiDiagnosticIssue[] array3 = array2;
			foreach (WikiDiagnosticIssue wikiDiagnosticIssue in array3)
			{
				if (wikiDiagnosticIssue.Kind == WikiDiagnosticKind.UnavailableStatistic && wikiDiagnosticIssue.RelatedItemId != null && index.Items.TryGetValue(wikiDiagnosticIssue.RelatedItemId, out WikiItem value))
				{
					list.Add("- Optional statistic '" + wikiDiagnosticIssue.Category + "' unavailable for " + Label(value) + " (" + wikiDiagnosticIssue.ReasonCode + ").");
				}
				else if (!visibility.IsRestricted)
				{
					list.Add("- Skipped " + wikiDiagnosticIssue.Category + ": " + wikiDiagnosticIssue.EntityId + " (" + wikiDiagnosticIssue.ReasonCode + ").");
				}
			}
			if (visibility.IsRestricted && index.Diagnostics.Any((WikiDiagnosticIssue issue) => issue.RelatedItemId == null))
			{
				list.Add("Unassociated skipped-prefab identifiers are not listed under a character-knowledge policy.");
			}
			if (index.Diagnostics.Count > array2.Length)
			{
				list.Add($"Issue detail output is bounded to {20} eligible records.");
			}
			list.Add("Intentionally unsupported: chance/weighted/multi-output/zero-minimum environmental tables; fish extra drops and bait probabilities; containers; locations; biome inference; random pickables; procedural loot.");
			list.Add("Intentionally unsupported: DPS, resistance-adjusted damage, attack timing, skill simulation, composite best-item scores, scene-distance station scans, inventory mutation, and progression mutation.");
			return new WikiAnswer("Unsupported and isolated content", list, new string[1] { $"Visibility: {visibility

MimirsWell.dll

Decompiled 20 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using MimirsWell.Core.Domain;
using MimirsWell.Core.Formatting;
using MimirsWell.Core.Indexing;
using MimirsWell.Core.Interpretation;
using MimirsWell.Core.Resolution;
using MimirsWell.Core.Services;
using MimirsWell.Indexing;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("MimirsWell")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("An offline, read-only, runtime-indexed Valheim wiki.")]
[assembly: AssemblyFileVersion("0.5.4.0")]
[assembly: AssemblyInformationalVersion("0.5.4+e9ead6b8be5d9ac71509b88d6e713cfa6d095cc3")]
[assembly: AssemblyProduct("MimirsWell")]
[assembly: AssemblyTitle("MimirsWell")]
[assembly: AssemblyMetadata("ValheimVersion", "0.221.12")]
[assembly: AssemblyMetadata("ValheimNetworkVersion", "36")]
[assembly: AssemblyMetadata("UnityVersion", "6000.0.61f1")]
[assembly: AssemblyMetadata("BepInExPackVersion", "5.4.2333")]
[assembly: AssemblyMetadata("BepInExCoreVersion", "5.4.23.3")]
[assembly: AssemblyMetadata("JotunnVersion", "2.29.1")]
[assembly: AssemblyVersion("0.5.4.0")]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace MimirsWell
{
	[BepInPlugin("com.braxt.mimirswell", "Mimir's Well", "0.5.4")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class MimirsWellPlugin : BaseUnityPlugin
	{
		private WikiRuntime? runtime;

		private void Awake()
		{
			//IL_008b: Unknown result type (might be due to invalid IL or missing references)
			WikiConfiguration wikiConfiguration = new WikiConfiguration(((BaseUnityPlugin)this).Config);
			if (wikiConfiguration.EnableModSourceDetection.Value)
			{
				ModQuery.Enable();
			}
			runtime = new WikiRuntime(this, wikiConfiguration, ((BaseUnityPlugin)this).Logger);
			runtime.Attach();
			CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new WikiCommand(runtime));
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded with Jotunn {2}. Command: {3}. Offline/read-only; knowledge visibility: {4}; immutable index: all loaded content.", "Mimir's Well", "0.5.4", "2.29.1", runtime.CommandName, wikiConfiguration.EffectiveKnowledgePolicy));
		}

		private void OnDestroy()
		{
			runtime?.Dispose();
			runtime = null;
		}

		private void Update()
		{
			runtime?.Tick();
		}
	}
	internal static class PluginInfo
	{
		internal const string Guid = "com.braxt.mimirswell";

		internal const string Name = "Mimir's Well";

		internal const string Version = "0.5.4";
	}
	internal sealed class WikiCommand : ConsoleCommand
	{
		private readonly WikiRuntime runtime;

		public override string Name => runtime.CommandName;

		public override string Help => "Ask about loaded Valheim knowledge, current held/equipped/food context, or bounded diagnostics.";

		public override bool IsNetwork => false;

		internal WikiCommand(WikiRuntime runtime)
		{
			this.runtime = runtime ?? throw new ArgumentNullException("runtime");
		}

		public override void Run(string[] args, Terminal context)
		{
			if (!((Object)(object)context == (Object)null))
			{
				string input = string.Join(" ", args ?? Array.Empty<string>());
				ChatPage val = runtime.Execute(input);
				runtime.LogAcceptanceTranscript(input, val);
				for (int i = 0; i < val.Lines.Count; i++)
				{
					string text = ((i == 0) ? (runtime.ResponsePrefix + " ") : "  ");
					context.AddString(text + val.Lines[i]);
				}
			}
		}
	}
	internal sealed class WikiConfiguration
	{
		private readonly ConfigFile file;

		internal ConfigEntry<string> CommandName { get; }

		internal ConfigEntry<string> ResponsePrefix { get; }

		internal ConfigEntry<bool> EnableNaturalLanguagePatterns { get; }

		internal ConfigEntry<bool> EnableBuildPieces { get; }

		internal ConfigEntry<bool> EnableModSourceDetection { get; }

		internal ConfigEntry<bool> FuzzyMatchingEnabled { get; }

		internal ConfigEntry<double> FuzzyMatchThreshold { get; }

		internal ConfigEntry<double> AmbiguityMargin { get; }

		internal ConfigEntry<int> MaxLinesPerPage { get; }

		internal ConfigEntry<int> MaxResultsPerPage { get; }

		internal ConfigEntry<int> PaginationTimeoutSeconds { get; }

		internal ConfigEntry<bool> ShowInternalNames { get; }

		internal ConfigEntry<bool> ShowSourceMod { get; }

		internal ConfigEntry<bool> ShowComparisonAssumptions { get; }

		internal ConfigEntry<string> SpoilerPolicy { get; }

		internal ConfigEntry<string> LogLevel { get; }

		internal ConfigEntry<bool> WriteIndexSnapshot { get; }

		internal ConfigEntry<bool> LogAcceptanceTranscript { get; }

		internal bool DebugLogging => string.Equals(LogLevel.Value, "Debug", StringComparison.OrdinalIgnoreCase);

		internal WikiKnowledgePolicy EffectiveKnowledgePolicy
		{
			get
			{
				//IL_0016: Unknown result type (might be due to invalid IL or missing references)
				if (!TryKnowledgePolicy(SpoilerPolicy.Value, out var policy))
				{
					return (WikiKnowledgePolicy)3;
				}
				return policy;
			}
		}

		internal bool KnowledgePolicyIsValid
		{
			get
			{
				WikiKnowledgePolicy policy;
				return TryKnowledgePolicy(SpoilerPolicy.Value, out policy);
			}
		}

		internal WikiConfiguration(ConfigFile file)
		{
			//IL_0102: Unknown result type (might be due to invalid IL or missing references)
			//IL_010c: Expected O, but got Unknown
			//IL_0147: Unknown result type (might be due to invalid IL or missing references)
			//IL_0151: Expected O, but got Unknown
			//IL_0175: Unknown result type (might be due to invalid IL or missing references)
			//IL_017f: Expected O, but got Unknown
			//IL_01a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ad: Expected O, but got Unknown
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e0: Expected O, but got Unknown
			this.file = file ?? throw new ArgumentNullException("file");
			CommandName = file.Bind<string>("General", "CommandName", "wiki", "Command name. Startup-only; changing it requires a game restart.");
			ResponsePrefix = file.Bind<string>("General", "ResponsePrefix", "[Mimir's Well]", "Short prefix added to command responses.");
			EnableNaturalLanguagePatterns = file.Bind<bool>("General", "EnableNaturalLanguagePatterns", true, "Enable deterministic natural-language patterns after explicit commands.");
			EnableBuildPieces = file.Bind<bool>("General", "EnableBuildPieces", true, "Include build-piece material requirements in the runtime index.");
			EnableModSourceDetection = file.Bind<bool>("General", "EnableModSourceDetection", false, "Enable Jotunn ModQuery at startup. Startup-only because it changes loading instrumentation.");
			FuzzyMatchingEnabled = file.Bind<bool>("Matching", "FuzzyMatchingEnabled", true, "Allow minor misspellings after exact matching fails.");
			FuzzyMatchThreshold = file.Bind<double>("Matching", "FuzzyMatchThreshold", 0.82, new ConfigDescription("Minimum fuzzy similarity.", (AcceptableValueBase)(object)new AcceptableValueRange<double>(0.5, 1.0), Array.Empty<object>()));
			AmbiguityMargin = file.Bind<double>("Matching", "AmbiguityMargin", 0.05, new ConfigDescription("Return candidates when top scores are this close.", (AcceptableValueBase)(object)new AcceptableValueRange<double>(0.0, 0.25), Array.Empty<object>()));
			MaxLinesPerPage = file.Bind<int>("Display", "MaxLinesPerPage", 8, new ConfigDescription("Maximum content lines per page.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(3, 30), Array.Empty<object>()));
			MaxResultsPerPage = file.Bind<int>("Display", "MaxResultsPerPage", 6, new ConfigDescription("Maximum result rows targeted per page.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(2, 20), Array.Empty<object>()));
			PaginationTimeoutSeconds = file.Bind<int>("Display", "PaginationTimeoutSeconds", 120, new ConfigDescription("Seconds before /wiki more expires.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(15, 600), Array.Empty<object>()));
			ShowInternalNames = file.Bind<bool>("Display", "ShowInternalNames", false, "Show prefab/internal names in item information.");
			ShowSourceMod = file.Bind<bool>("Display", "ShowSourceMod", true, "Show source-mod labels when startup detection is enabled and attribution is available.");
			ShowComparisonAssumptions = file.Bind<bool>("Display", "ShowComparisonAssumptions", true, "Show the limitations of comparison metrics.");
			SpoilerPolicy = file.Bind<string>("Knowledge", "SpoilerPolicy", "AllLoaded", "Query visibility: AllLoaded, KnownItems, KnownRecipes, or KnownToCharacter. The complete runtime index is always retained.");
			LogLevel = file.Bind<string>("Diagnostics", "LogLevel", "Info", "Mimir's Well diagnostic verbosity: Info or Debug.");
			WriteIndexSnapshot = file.Bind<bool>("Diagnostics", "WriteIndexSnapshot", false, "Write metadata-only index counts to the BepInEx config folder.");
			LogAcceptanceTranscript = file.Bind<bool>("Diagnostics", "LogAcceptanceTranscript", false, "Opt in to logging only wiki commands and their displayed response pages for local acceptance testing. Diagnostic bundles remove these lines.");
		}

		internal void Reload()
		{
			file.Reload();
		}

		private static bool TryKnowledgePolicy(string value, out WikiKnowledgePolicy policy)
		{
			if (Enum.TryParse<WikiKnowledgePolicy>(value?.Trim(), ignoreCase: true, out policy))
			{
				return Enum.IsDefined(typeof(WikiKnowledgePolicy), policy);
			}
			return false;
		}
	}
	internal sealed class WikiRuntime : IDisposable
	{
		private static readonly WikiIndex EmptyIndex = new WikiIndex((IEnumerable<WikiItem>)Array.Empty<WikiItem>(), (IEnumerable<WikiRecipe>)Array.Empty<WikiRecipe>(), (IEnumerable<WikiPiece>)Array.Empty<WikiPiece>(), DateTimeOffset.MinValue, TimeSpan.Zero, "unknown", 0, string.Empty, 0, false, (IEnumerable<WikiConversion>)null, (IEnumerable<WikiAcquisitionSource>)null, (IEnumerable<WikiDiagnosticIssue>)null, 0);

		private readonly MimirsWellPlugin host;

		private readonly WikiConfiguration configuration;

		private readonly ManualLogSource log;

		private readonly RuleBasedQuestionInterpreter interpreter = new RuleBasedQuestionInterpreter();

		private readonly ChatPaginator paginator = new ChatPaginator();

		private readonly string startupCommandName;

		private readonly bool startupSourceDetection;

		private WikiIndex? index;

		private bool itemsRegistered;

		private bool piecesRegistered;

		private bool prefabsRegistered;

		private bool creaturesRegistered;

		private bool buildQueued;

		private int lifecycleGeneration;

		private bool disposed;

		internal string ResponsePrefix
		{
			get
			{
				if (!string.IsNullOrWhiteSpace(configuration.ResponsePrefix.Value))
				{
					return configuration.ResponsePrefix.Value.Trim();
				}
				return "[Mimir's Well]";
			}
		}

		internal string CommandName => startupCommandName;

		internal WikiRuntime(MimirsWellPlugin host, WikiConfiguration configuration, ManualLogSource log)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Expected O, but got Unknown
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Expected O, but got Unknown
			this.host = host ?? throw new ArgumentNullException("host");
			this.configuration = configuration ?? throw new ArgumentNullException("configuration");
			this.log = log ?? throw new ArgumentNullException("log");
			startupCommandName = EffectiveCommandName(configuration.CommandName.Value);
			startupSourceDetection = configuration.EnableModSourceDetection.Value;
		}

		internal void Attach()
		{
			if (!configuration.KnowledgePolicyIsValid)
			{
				log.LogWarning((object)("SpoilerPolicy '" + configuration.SpoilerPolicy.Value + "' is invalid; failing closed to KnownToCharacter."));
			}
			ItemManager.OnItemsRegistered += OnItemsRegistered;
			ItemManager.OnItemsRegisteredFejd += OnItemsRegisteredFejd;
			PieceManager.OnPiecesRegistered += OnPiecesRegistered;
			PrefabManager.OnPrefabsRegistered += OnPrefabsRegistered;
			CreatureManager.OnCreaturesRegistered += OnCreaturesRegistered;
			Localization.OnLanguageChange = (Action)Delegate.Combine(Localization.OnLanguageChange, new Action(OnLanguageChanged));
		}

		internal ChatPage Execute(string input)
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Expected O, but got Unknown
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Invalid comparison between Unknown and I4
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a8: Invalid comparison between Unknown and I4
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Invalid comparison between Unknown and I4
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d6: Invalid comparison between Unknown and I4
			//IL_016d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Invalid comparison between Unknown and I4
			//IL_0161: Unknown result type (might be due to invalid IL or missing references)
			//IL_016b: Expected O, but got Unknown
			//IL_01c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_019c: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Expected O, but got Unknown
			//IL_01e0: Unknown result type (might be due to invalid IL or missing references)
			WikiQuery val = interpreter.Interpret(input, new InterpretationContext(CurrentLanguage(), configuration.EnableNaturalLanguagePatterns.Value));
			if (configuration.DebugLogging)
			{
				log.LogDebug((object)$"Intent={val.Intent}; confidence={val.Confidence:0.00}; diagnostic={val.Diagnostic}");
			}
			if ((int)val.Intent == 22)
			{
				return paginator.More(configuration.MaxLinesPerPage.Value, DateTimeOffset.Now, configuration.MaxResultsPerPage.Value);
			}
			if ((int)val.Intent == 25)
			{
				return ReloadConfiguration();
			}
			if ((int)val.Intent == 24)
			{
				return RequestManualReindex();
			}
			WikiIndex val2 = Volatile.Read(in index);
			if ((int)val.Intent == 23 && val2 == null)
			{
				return Page(new WikiAnswer("Index not ready", (IEnumerable<string>)new string[6]
				{
					$"Items registered: {itemsRegistered}",
					$"Pieces registered: {piecesRegistered}",
					$"Prefabs registered: {prefabsRegistered}",
					$"Creatures registered: {creaturesRegistered}",
					KnowledgePolicyStatus(),
					"Waiting for the required Jotunn registration phases."
				}, (IEnumerable<string>)null, (IEnumerable<string>)null, false));
			}
			if ((int)val.Intent != 1 && val2 == null)
			{
				return Page(new WikiAnswer("Index not ready", (IEnumerable<string>)new string[1] { "Mimir's Well is waiting for the active ObjectDB and Jotunn registration events." }, (IEnumerable<string>)null, (IEnumerable<string>)new string[1] { "Try /wiki status." }, false));
			}
			WikiEngine val3 = CreateEngine();
			WikiIndex val4 = val2 ?? EmptyIndex;
			WikiVisibilitySnapshot val5 = CreateVisibilitySnapshot(val4);
			WikiAnswer answer = (IsContextIntent(val.Intent) ? val3.ExecuteContext(val4, val, CaptureContext(val.Intent, val4), val5) : val3.Execute(val4, val, val5));
			return Page(answer);
		}

		internal void LogAcceptanceTranscript(string input, ChatPage page)
		{
			if (configuration.LogAcceptanceTranscript.Value && page != null)
			{
				string text = SingleLine(input, 512);
				log.LogInfo((object)("[AcceptanceTranscript] Command: /" + CommandName + ((text.Length > 0) ? (" " + text) : string.Empty)));
				for (int i = 0; i < page.Lines.Count; i++)
				{
					string text2 = ((i == 0) ? (ResponsePrefix + " ") : "  ");
					log.LogInfo((object)("[AcceptanceTranscript] Response: " + text2 + SingleLine(page.Lines[i], 2048)));
				}
			}
		}

		internal void Tick()
		{
			WikiIndex val = Volatile.Read(in index);
			if (!disposed && !buildQueued && val != null && !val.WorldContextAvailable && (Object)(object)Game.instance != (Object)null && (Object)(object)Player.m_localPlayer != (Object)null && Object.op_Implicit((Object)(object)ObjectDB.instance))
			{
				log.LogInfo((object)"World context became available; scheduling one calculator-complete index refresh.");
				QueueBuild(force: true);
			}
		}

		public void Dispose()
		{
			if (!disposed)
			{
				disposed = true;
				ItemManager.OnItemsRegistered -= OnItemsRegistered;
				ItemManager.OnItemsRegisteredFejd -= OnItemsRegisteredFejd;
				PieceManager.OnPiecesRegistered -= OnPiecesRegistered;
				PrefabManager.OnPrefabsRegistered -= OnPrefabsRegistered;
				CreatureManager.OnCreaturesRegistered -= OnCreaturesRegistered;
				Localization.OnLanguageChange = (Action)Delegate.Remove(Localization.OnLanguageChange, new Action(OnLanguageChanged));
				paginator.Clear();
				Volatile.Write(ref index, null);
			}
		}

		private void OnItemsRegistered()
		{
			itemsRegistered = true;
			QueueLifecycleBuildIfReady();
		}

		private void OnPiecesRegistered()
		{
			piecesRegistered = true;
			QueueLifecycleBuildIfReady();
		}

		private void OnPrefabsRegistered()
		{
			prefabsRegistered = true;
			QueueLifecycleBuildIfReady();
		}

		private void OnCreaturesRegistered()
		{
			creaturesRegistered = true;
			QueueLifecycleBuildIfReady();
		}

		private void OnItemsRegisteredFejd()
		{
			lifecycleGeneration++;
			itemsRegistered = false;
			piecesRegistered = false;
			prefabsRegistered = false;
			creaturesRegistered = false;
			buildQueued = false;
			paginator.Clear();
			Volatile.Write(ref index, null);
			log.LogInfo((object)"Invalidated the wiki index for the Fejd/main-menu ObjectDB lifecycle.");
		}

		private void OnLanguageChanged()
		{
			if (Volatile.Read(in index) != null && Object.op_Implicit((Object)(object)ObjectDB.instance))
			{
				log.LogInfo((object)"Language changed; scheduling a localized index rebuild.");
				QueueBuild(force: true);
			}
		}

		private void QueueLifecycleBuildIfReady()
		{
			if (LifecycleRegistrationsReady())
			{
				QueueBuild(force: false);
			}
		}

		private void QueueBuild(bool force)
		{
			if (!disposed && !buildQueued && (force || LifecycleRegistrationsReady()))
			{
				buildQueued = true;
				int generation = lifecycleGeneration;
				((MonoBehaviour)host).StartCoroutine(BuildOnNextFrame(generation));
			}
		}

		private IEnumerator BuildOnNextFrame(int generation)
		{
			yield return null;
			buildQueued = false;
			if (disposed || generation != lifecycleGeneration || !Object.op_Implicit((Object)(object)ObjectDB.instance))
			{
				yield break;
			}
			try
			{
				log.LogInfo((object)"Building runtime wiki index from loaded game data.");
				AliasRepository aliases = AliasRepository.Load(Path.Combine(Paths.ConfigPath, "MimirsWell.aliases.json"), log);
				WikiIndex val = new GameDataIndexer(log, aliases).Build(configuration.EnableBuildPieces.Value, startupSourceDetection);
				Volatile.Write(ref index, val);
				paginator.Clear();
				log.LogInfo((object)$"Wiki index ready in {val.BuildDuration.TotalMilliseconds:0} ms: {val.Items.Count} items, {val.Recipes.Count} recipes, {val.Conversions.Count} conversions, {val.Sources.Count} acquisition sources, {val.Pieces.Count} pieces, {val.SkippedEntries} skipped, {val.UnavailableStatistics} optional statistics unavailable.");
				log.LogInfo((object)$"Acquisition source categories: creature {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 0)}, purchased {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 1)}, passive {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 2)}, harvested {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 3)}, mined/destroyed {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 4)}, fishing {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 5)}.");
				log.LogInfo((object)string.Format("Acquisition source adapters: MineRock {0}, MineRock5 {1}, TreeBase {2}, TreeLog {3}, DropOnDestroyed {4}, Fish {5}.", val.Sources.Count((WikiAcquisitionSource source) => source.Adapter == "MineRock"), val.Sources.Count((WikiAcquisitionSource source) => source.Adapter == "MineRock5"), val.Sources.Count((WikiAcquisitionSource source) => source.Adapter == "TreeBase"), val.Sources.Count((WikiAcquisitionSource source) => source.Adapter == "TreeLog"), val.Sources.Count((WikiAcquisitionSource source) => source.Adapter == "DropOnDestroyed"), val.Sources.Count((WikiAcquisitionSource source) => source.Adapter == "Fish")));
				log.LogInfo((object)$"Environmental quantity semantics: fixed {val.Sources.Count((WikiAcquisitionSource source) => (int)source.Kind == 4 && (int)source.QuantitySemantics == 0)}, guaranteed-variable {val.Sources.Count((WikiAcquisitionSource source) => (int)source.QuantitySemantics == 1)}.");
				log.LogInfo((object)$"Diagnostics retained: {val.Diagnostics.Count}; omitted after bound: {val.OmittedDiagnostics}.");
				WriteMetadataSnapshot(val);
			}
			catch (Exception arg)
			{
				Volatile.Write(ref index, null);
				log.LogError((object)$"Wiki index build failed: {arg}");
			}
		}

		private ChatPage ReloadConfiguration()
		{
			//IL_0017: 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_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Expected O, but got Unknown
			//IL_0111: Unknown result type (might be due to invalid IL or missing references)
			//IL_0118: Unknown result type (might be due to invalid IL or missing references)
			//IL_012b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0196: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a0: Expected O, but got Unknown
			bool value = configuration.EnableBuildPieces.Value;
			WikiKnowledgePolicy effectiveKnowledgePolicy = configuration.EffectiveKnowledgePolicy;
			try
			{
				configuration.Reload();
			}
			catch (Exception ex)
			{
				log.LogError((object)$"Configuration reload failed: {ex}");
				return Page(new WikiAnswer("Reload failed", (IEnumerable<string>)new string[1] { ex.Message }, (IEnumerable<string>)null, (IEnumerable<string>)null, false));
			}
			List<string> list = new List<string> { "Runtime-safe settings reloaded." };
			if (!string.Equals(EffectiveCommandName(configuration.CommandName.Value), startupCommandName, StringComparison.OrdinalIgnoreCase))
			{
				list.Add("CommandName remains '" + startupCommandName + "' until the game restarts.");
			}
			if (configuration.EnableModSourceDetection.Value != startupSourceDetection)
			{
				list.Add($"EnableModSourceDetection remains '{startupSourceDetection}' until the game restarts.");
			}
			if (value != configuration.EnableBuildPieces.Value)
			{
				list.Add("Build-piece coverage changed; an index rebuild was scheduled.");
				QueueBuild(force: true);
			}
			if (effectiveKnowledgePolicy != configuration.EffectiveKnowledgePolicy)
			{
				list.Add($"Knowledge visibility changed to {configuration.EffectiveKnowledgePolicy}; the immutable runtime index was not rebuilt.");
			}
			if (!configuration.KnowledgePolicyIsValid)
			{
				list.Add("SpoilerPolicy '" + configuration.SpoilerPolicy.Value + "' is invalid; failing closed to KnownToCharacter.");
			}
			paginator.Clear();
			log.LogInfo((object)"Runtime-safe configuration settings reloaded.");
			return Page(new WikiAnswer("Configuration reloaded", (IEnumerable<string>)list, (IEnumerable<string>)null, (IEnumerable<string>)null, false));
		}

		private ChatPage RequestManualReindex()
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Expected O, but got Unknown
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Expected O, but got Unknown
			if (!Object.op_Implicit((Object)(object)ObjectDB.instance) || !Object.op_Implicit((Object)(object)ZNetScene.instance) || !LifecycleRegistrationsReady())
			{
				return Page(new WikiAnswer("Reindex unavailable", (IEnumerable<string>)new string[1] { "A fully registered active world is required; main-menu data would produce an incomplete index." }, (IEnumerable<string>)null, (IEnumerable<string>)null, false));
			}
			log.LogInfo((object)"Manual reindex requested; scheduling a fresh runtime index build.");
			QueueBuild(force: true);
			return Page(new WikiAnswer("Reindex scheduled", (IEnumerable<string>)new string[1] { "The loaded game data will be copied into a new index on the next frame." }, (IEnumerable<string>)null, (IEnumerable<string>)null, false));
		}

		private WikiEngine CreateEngine()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Expected O, but got Unknown
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00db: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_010c: Expected O, but got Unknown
			//IL_010c: Expected O, but got Unknown
			//IL_0107: Unknown result type (might be due to invalid IL or missing references)
			//IL_010d: Expected O, but got Unknown
			return new WikiEngine(new EntityResolver(new EntityResolverOptions
			{
				FuzzyMatchingEnabled = configuration.FuzzyMatchingEnabled.Value,
				FuzzyMatchThreshold = configuration.FuzzyMatchThreshold.Value,
				AmbiguityMargin = configuration.AmbiguityMargin.Value,
				MaximumCandidates = configuration.MaxResultsPerPage.Value
			}), new WikiEngineOptions
			{
				ShowInternalNames = configuration.ShowInternalNames.Value,
				ShowSourceMod = configuration.ShowSourceMod.Value,
				ShowComparisonAssumptions = configuration.ShowComparisonAssumptions.Value,
				MaximumAlternativeResults = Math.Min(5, configuration.MaxResultsPerPage.Value),
				FuzzyMatchingEnabled = configuration.FuzzyMatchingEnabled.Value,
				FuzzyMatchThreshold = configuration.FuzzyMatchThreshold.Value,
				AmbiguityMargin = configuration.AmbiguityMargin.Value
			});
		}

		private WikiContextSnapshot CaptureContext(WikiIntent intent, WikiIndex current)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//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_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Expected O, but got Unknown
			//IL_0222: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Expected I4, but got Unknown
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			//IL_0246: Unknown result type (might be due to invalid IL or missing references)
			//IL_024c: Expected O, but got Unknown
			//IL_01f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0200: Expected O, but got Unknown
			//IL_017a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0184: Expected O, but got Unknown
			WikiContextKind val = ContextKind(intent);
			Player localPlayer = Player.m_localPlayer;
			if (Object.op_Implicit((Object)(object)localPlayer))
			{
				List<WikiContextItem> list = new List<WikiContextItem>();
				int errors = 0;
				try
				{
					switch ((int)val)
					{
					case 0:
					{
						ItemData val2 = ((Humanoid)localPlayer).GetRightItem();
						string role = "right hand";
						if (val2 == null)
						{
							val2 = ((Humanoid)localPlayer).GetLeftItem();
							role = "left-hand fallback";
						}
						if (val2 == null)
						{
							val2 = EquippedHandFallback(localPlayer, ref errors);
							role = "equipped hand-item fallback (direct hand slots unavailable)";
						}
						AddContextItem(list, val2, role, current, ref errors);
						break;
					}
					case 1:
					{
						Inventory inventory = ((Humanoid)localPlayer).GetInventory();
						foreach (ItemData item in ((inventory != null) ? inventory.GetEquippedItems() : null) ?? new List<ItemData>())
						{
							AddContextItem(list, item, "equipped", current, ref errors);
						}
						break;
					}
					case 2:
						foreach (Food item2 in localPlayer.GetFoods() ?? new List<Food>())
						{
							try
							{
								if (item2?.m_item == null)
								{
									errors++;
									continue;
								}
								string text3 = ContextItemId(item2.m_item, current);
								if (string.IsNullOrWhiteSpace(text3))
								{
									errors++;
								}
								else
								{
									list.Add(new WikiContextItem(text3, "active food", (double?)item2.m_health, (double?)item2.m_stamina, (double?)item2.m_eitr, (double?)item2.m_time));
								}
							}
							catch
							{
								errors++;
							}
						}
						break;
					case 3:
					{
						CraftingStation currentCraftingStation = localPlayer.GetCurrentCraftingStation();
						string text = (Object.op_Implicit((Object)(object)currentCraftingStation) ? ContextDisplayName(currentCraftingStation.m_name, ((Object)((Component)currentCraftingStation).gameObject).name) : null);
						string text2 = (Object.op_Implicit((Object)(object)currentCraftingStation) ? ((Object)((Component)currentCraftingStation).gameObject).name : null);
						return new WikiContextSnapshot(val, true, (IEnumerable<WikiContextItem>)null, text, text2, errors);
					}
					}
				}
				catch (Exception ex)
				{
					errors++;
					if (configuration.DebugLogging)
					{
						log.LogDebug((object)$"Context shortcut isolated a {val} read failure: {ex.GetType().Name}.");
					}
				}
				return new WikiContextSnapshot(val, true, (IEnumerable<WikiContextItem>)list, (string)null, (string)null, errors);
			}
			return new WikiContextSnapshot(val, false, (IEnumerable<WikiContextItem>)null, (string)null, (string)null, 0);
		}

		private static ItemData? EquippedHandFallback(Player player, ref int errors)
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				ItemData result = null;
				int num = int.MaxValue;
				Inventory inventory = ((Humanoid)player).GetInventory();
				foreach (ItemData item in ((inventory != null) ? inventory.GetEquippedItems() : null) ?? new List<ItemData>())
				{
					try
					{
						if (item?.m_shared != null)
						{
							int num2 = HandItemPriority(item.m_shared.m_itemType);
							if (num2 < num)
							{
								result = item;
								num = num2;
							}
						}
					}
					catch
					{
						errors++;
					}
				}
				return result;
			}
			catch
			{
				errors++;
				return null;
			}
		}

		private static int HandItemPriority(ItemType type)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0004: Invalid comparison between Unknown and I4
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Invalid comparison between Unknown and I4
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Expected I4, but got Unknown
			if (type - 3 > 1)
			{
				if ((int)type == 5)
				{
					return 3;
				}
				switch (type - 14)
				{
				case 0:
				case 6:
				case 8:
					break;
				case 5:
					return 1;
				case 1:
					return 2;
				default:
					return int.MaxValue;
				}
			}
			return 0;
		}

		private static void AddContextItem(ICollection<WikiContextItem> output, ItemData? item, string role, WikiIndex current, ref int errors)
		{
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Expected O, but got Unknown
			if (item == null)
			{
				return;
			}
			try
			{
				string text = ContextItemId(item, current);
				if (string.IsNullOrWhiteSpace(text))
				{
					errors++;
				}
				else
				{
					output.Add(new WikiContextItem(text, role, (double?)null, (double?)null, (double?)null, (double?)null));
				}
			}
			catch
			{
				errors++;
			}
		}

		private static string ContextItemId(ItemData item, WikiIndex current)
		{
			if (Object.op_Implicit((Object)(object)item.m_dropPrefab))
			{
				return ((Object)item.m_dropPrefab).name;
			}
			string key = item.m_shared?.m_name;
			if (string.IsNullOrWhiteSpace(key))
			{
				return string.Empty;
			}
			WikiItem? obj = current.Items.Values.FirstOrDefault((Func<WikiItem, bool>)((WikiItem candidate) => string.Equals(candidate.KnowledgeKey, key, StringComparison.Ordinal)));
			return ((obj != null) ? obj.Id : null) ?? string.Empty;
		}

		private static string ContextDisplayName(string? token, string fallback)
		{
			if (string.IsNullOrWhiteSpace(token))
			{
				return PrettifyContextName(fallback);
			}
			string text = ((Localization.instance != null) ? Localization.instance.Localize(token) : token);
			if (string.IsNullOrWhiteSpace(text) || (text == token && token.StartsWith("$", StringComparison.Ordinal)))
			{
				return PrettifyContextName(fallback);
			}
			string text2 = Regex.Replace(text, "<[^>]+>", string.Empty).Trim();
			if (!string.IsNullOrWhiteSpace(text2))
			{
				return text2;
			}
			return PrettifyContextName(fallback);
		}

		private static string PrettifyContextName(string value)
		{
			string text = TextNormalizer.Normalize(value);
			if (!string.IsNullOrWhiteSpace(text))
			{
				return string.Join(" ", from word in text.Split(new char[1] { ' ' })
					select char.ToUpperInvariant(word[0]) + word.Substring(1));
			}
			return value;
		}

		private static bool IsContextIntent(WikiIntent intent)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Invalid comparison between Unknown and I4
			if (intent - 15 <= 3)
			{
				return true;
			}
			return false;
		}

		private static WikiContextKind ContextKind(WikiIntent intent)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Expected I4, but got Unknown
			return (WikiContextKind)((intent - 15) switch
			{
				0 => 0, 
				1 => 1, 
				2 => 2, 
				_ => 3, 
			});
		}

		private WikiVisibilitySnapshot CreateVisibilitySnapshot(WikiIndex current)
		{
			//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_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Expected O, but got Unknown
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Invalid comparison between Unknown and I4
			//IL_011d: Unknown result type (might be due to invalid IL or missing references)
			//IL_011f: Invalid comparison between Unknown and I4
			//IL_0194: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a0: Expected O, but got Unknown
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Invalid comparison between Unknown and I4
			WikiKnowledgePolicy effectiveKnowledgePolicy = configuration.EffectiveKnowledgePolicy;
			if ((int)effectiveKnowledgePolicy == 0)
			{
				return WikiVisibilitySnapshot.AllLoaded;
			}
			Player localPlayer = Player.m_localPlayer;
			if (Object.op_Implicit((Object)(object)localPlayer))
			{
				HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				int num = 0;
				foreach (WikiItem value in current.Items.Values)
				{
					try
					{
						bool flag = (int)effectiveKnowledgePolicy != 2 && localPlayer.IsKnownMaterial(value.KnowledgeKey);
						bool flag2 = (int)effectiveKnowledgePolicy != 1 && localPlayer.IsRecipeKnown(value.KnowledgeKey);
						if (flag || flag2)
						{
							hashSet.Add(value.Id);
						}
					}
					catch (Exception ex)
					{
						num++;
						if (configuration.DebugLogging)
						{
							log.LogDebug((object)("Knowledge visibility isolated one indexed-item check: " + ex.GetType().Name + "."));
						}
					}
				}
				foreach (WikiPiece piece in current.Pieces)
				{
					try
					{
						if ((int)effectiveKnowledgePolicy != 1 && localPlayer.IsRecipeKnown(piece.KnowledgeKey))
						{
							hashSet2.Add(piece.Id);
						}
					}
					catch (Exception ex2)
					{
						num++;
						if (configuration.DebugLogging)
						{
							log.LogDebug((object)("Knowledge visibility isolated one indexed-piece check: " + ex2.GetType().Name + "."));
						}
					}
				}
				return new WikiVisibilitySnapshot(effectiveKnowledgePolicy, (IEnumerable<string>)hashSet, true, num, (IEnumerable<string>)hashSet2);
			}
			return new WikiVisibilitySnapshot(effectiveKnowledgePolicy, (IEnumerable<string>)Array.Empty<string>(), false, 0, (IEnumerable<string>)null);
		}

		private string KnowledgePolicyStatus()
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			string arg = (configuration.KnowledgePolicyIsValid ? string.Empty : (" (invalid configured value '" + configuration.SpoilerPolicy.Value + "' failed closed)"));
			return $"Knowledge visibility: {configuration.EffectiveKnowledgePolicy}{arg}.";
		}

		private ChatPage Page(WikiAnswer answer)
		{
			return paginator.Begin(answer, configuration.MaxLinesPerPage.Value, TimeSpan.FromSeconds(configuration.PaginationTimeoutSeconds.Value), DateTimeOffset.Now, configuration.MaxResultsPerPage.Value);
		}

		private void WriteMetadataSnapshot(WikiIndex built)
		{
			if (!configuration.WriteIndexSnapshot.Value)
			{
				return;
			}
			try
			{
				string path = Path.Combine(Paths.ConfigPath, "MimirsWell.index-metadata.json");
				string contents = new StringBuilder().AppendLine("{").Append("  \"pluginVersion\": \"").Append("0.5.4")
					.AppendLine("\",")
					.Append("  \"builtAt\": \"")
					.Append(built.BuiltAt.ToString("O"))
					.AppendLine("\",")
					.Append("  \"language\": \"")
					.Append(JsonEscape(built.Language))
					.AppendLine("\",")
					.Append("  \"items\": ")
					.Append(built.Items.Count)
					.AppendLine(",")
					.Append("  \"recipes\": ")
					.Append(built.Recipes.Count)
					.AppendLine(",")
					.Append("  \"conversions\": ")
					.Append(built.Conversions.Count)
					.AppendLine(",")
					.Append("  \"sources\": ")
					.Append(built.Sources.Count)
					.AppendLine(",")
					.Append("  \"pieces\": ")
					.Append(built.Pieces.Count)
					.AppendLine(",")
					.Append("  \"skippedEntries\": ")
					.Append(built.SkippedEntries)
					.AppendLine(",")
					.Append("  \"unavailableStatistics\": ")
					.Append(built.UnavailableStatistics)
					.AppendLine(",")
					.Append("  \"diagnosticsRetained\": ")
					.Append(built.Diagnostics.Count)
					.AppendLine(",")
					.Append("  \"diagnosticsOmitted\": ")
					.Append(built.OmittedDiagnostics)
					.AppendLine(",")
					.Append("  \"worldContextAvailable\": ")
					.Append(built.WorldContextAvailable ? "true" : "false")
					.AppendLine()
					.AppendLine("}")
					.ToString();
				File.WriteAllText(path, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
				log.LogInfo((object)"Wrote metadata-only index snapshot to the BepInEx config folder.");
			}
			catch (Exception ex)
			{
				log.LogWarning((object)("Could not write the optional index metadata snapshot: " + ex.Message));
			}
		}

		private static string JsonEscape(string value)
		{
			return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
		}

		private bool LifecycleRegistrationsReady()
		{
			if (itemsRegistered && prefabsRegistered && creaturesRegistered)
			{
				if (configuration.EnableBuildPieces.Value)
				{
					return piecesRegistered;
				}
				return true;
			}
			return false;
		}

		private static string SingleLine(string? value, int maximumLength)
		{
			string text = (value ?? string.Empty).Replace('\r', ' ').Replace('\n', ' ').Trim();
			if (text.Length > maximumLength)
			{
				return text.Substring(0, maximumLength) + "…";
			}
			return text;
		}

		private static string EffectiveCommandName(string value)
		{
			if (!string.IsNullOrWhiteSpace(value))
			{
				return value.Trim().TrimStart(new char[1] { '/' });
			}
			return "wiki";
		}

		private static string CurrentLanguage()
		{
			Localization instance = Localization.instance;
			return ((instance != null) ? instance.GetSelectedLanguage() : null) ?? "unknown";
		}
	}
}
namespace MimirsWell.Indexing
{
	internal sealed class AcquisitionSourceExtractor
	{
		private readonly bool includeModSources;

		private readonly Action<string, string, Exception> skip;

		internal AcquisitionSourceExtractor(bool includeModSources, Action<string, string, Exception> skip)
		{
			this.includeModSources = includeModSources;
			this.skip = skip ?? throw new ArgumentNullException("skip");
		}

		internal IReadOnlyList<RawAcquisitionSource> Scan(IEnumerable<GameObject> prefabs)
		{
			List<RawAcquisitionSource> list = new List<RawAcquisitionSource>();
			foreach (GameObject item in prefabs ?? Array.Empty<GameObject>())
			{
				if (Object.op_Implicit((Object)(object)item))
				{
					ExtractCreatureDrops(item, list);
					ExtractTraderInventory(item, list);
					ExtractBeehive(item, list);
					ExtractSapCollector(item, list);
					ExtractPickable(item, list);
					ExtractPickableItem(item, list);
					ExtractMineRock(item, list);
					ExtractMineRock5(item, list);
					ExtractTreeBase(item, list);
					ExtractTreeLog(item, list);
					ExtractDropOnDestroyed(item, list);
					ExtractFish(item, list);
				}
			}
			return (from @group in list.GroupBy<RawAcquisitionSource, string>((RawAcquisitionSource source) => source.Id, StringComparer.OrdinalIgnoreCase)
				select @group.First()).ToArray();
		}

		internal IReadOnlyList<WikiAcquisitionSource> Build(IEnumerable<RawAcquisitionSource> rawSources, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0108: Unknown result type (might be due to invalid IL or missing references)
			//IL_0112: Expected O, but got Unknown
			List<WikiAcquisitionSource> list = new List<WikiAcquisitionSource>();
			foreach (RawAcquisitionSource rawSource in rawSources)
			{
				try
				{
					if (!items.TryGetValue(rawSource.OutputItemId, out WikiItem value))
					{
						throw new InvalidOperationException("Output item '" + rawSource.OutputItemId + "' is not present in the published item index.");
					}
					string text = (string.IsNullOrWhiteSpace(rawSource.CostItemId) ? null : ItemName(rawSource.CostItemId, items, rawItems));
					list.Add(new WikiAcquisitionSource(rawSource.Id, rawSource.Kind, rawSource.Certainty, rawSource.Adapter, rawSource.SourceInternalName, rawSource.SourceName, rawSource.SourceMod, rawSource.OutputItemId, value.DisplayName, rawSource.MinimumAmount, rawSource.MaximumAmount, rawSource.Chance, rawSource.CostItemId, text, rawSource.Price, rawSource.UnlockRequirement, rawSource.ProductionIntervalSeconds, rawSource.Capacity, rawSource.RespawnSeconds, rawSource.OnePerPlayer, rawSource.LevelScaled, rawSource.WorldScaled, rawSource.Method, rawSource.MinimumToolTier, rawSource.QuantitySemantics, rawSource.ConfiguredMinimumAmount, rawSource.ConfiguredMaximumAmount, rawSource.LoadedResourceRate));
				}
				catch (Exception arg)
				{
					skip("acquisition source", rawSource.Id, arg);
				}
			}
			return list;
		}

		private void ExtractCreatureDrops(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				CharacterDrop component = prefab.GetComponent<CharacterDrop>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				Character component2 = prefab.GetComponent<Character>();
				string sourceName = Localize(Object.op_Implicit((Object)(object)component2) ? component2.m_name : null, Prettify(((Object)prefab).name));
				List<Drop> list = component.m_drops ?? new List<Drop>();
				for (int i = 0; i < list.Count; i++)
				{
					Drop val = list[i];
					if ((Object)(object)val?.m_prefab == (Object)null || val.m_chance <= 0f)
					{
						continue;
					}
					try
					{
						if (Object.op_Implicit((Object)(object)val.m_prefab.GetComponent<ItemDrop>()))
						{
							if (float.IsNaN(val.m_chance) || float.IsInfinity(val.m_chance) || val.m_chance > 1f)
							{
								throw new InvalidOperationException($"Drop chance '{val.m_chance}' is outside the supported 0..1 range.");
							}
							if (val.m_amountMin < 0 || val.m_amountMax < val.m_amountMin)
							{
								throw new InvalidOperationException($"Drop quantity range '{val.m_amountMin}..{val.m_amountMax}' is invalid.");
							}
							string name = ((Object)val.m_prefab).name;
							string id = $"{((Object)prefab).name}:CharacterDrop:{i}:{name}";
							bool num = !(val.m_chance >= 1f);
							string name2 = ((Object)prefab).name;
							string? sourceMod = SourceMod(((Object)prefab).name);
							int amountMin = val.m_amountMin;
							int amountMax = val.m_amountMax;
							double? chance = val.m_chance;
							bool onePerPlayer = val.m_onePerPlayer;
							bool levelMultiplier = val.m_levelMultiplier;
							bool worldScaled = !val.m_dontScale;
							sources.Add(new RawAcquisitionSource(id, (WikiSourceKind)0, (WikiSourceCertainty)num, "CharacterDrop", name2, sourceName, sourceMod, name, amountMin, amountMax, chance, null, null, null, null, null, null, onePerPlayer, levelMultiplier, worldScaled, null, null, (WikiQuantitySemantics)0));
						}
					}
					catch (Exception arg)
					{
						skip("creature-drop entry", $"{((Object)prefab).name}[{i}]", arg);
					}
				}
			}
			catch (Exception arg2)
			{
				skip("creature-drop component", SafeName((Object?)(object)prefab), arg2);
			}
		}

		private void ExtractTraderInventory(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				Trader component = prefab.GetComponent<Trader>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				string sourceName = Localize(component.m_name, Prettify(((Object)prefab).name));
				List<TradeItem> list = component.m_items ?? new List<TradeItem>();
				for (int i = 0; i < list.Count; i++)
				{
					TradeItem val = list[i];
					if ((Object)(object)val?.m_prefab == (Object)null)
					{
						continue;
					}
					try
					{
						if (val.m_stack <= 0 || val.m_price < 0)
						{
							throw new InvalidOperationException($"Trader stack/price '{val.m_stack}/{val.m_price}' is invalid.");
						}
						string name = ((Object)((Component)val.m_prefab).gameObject).name;
						string id = $"{((Object)prefab).name}:Trader:{i}:{name}";
						int num = ((!string.IsNullOrWhiteSpace(val.m_requiredGlobalKey)) ? 2 : 0);
						string name2 = ((Object)prefab).name;
						string? sourceMod = SourceMod(((Object)prefab).name);
						int stack = val.m_stack;
						int stack2 = val.m_stack;
						int? price = val.m_price;
						string requiredGlobalKey = val.m_requiredGlobalKey;
						sources.Add(new RawAcquisitionSource(id, (WikiSourceKind)1, (WikiSourceCertainty)num, "Trader", name2, sourceName, sourceMod, name, stack, stack2, null, "Coins", price, requiredGlobalKey, null, null, null, onePerPlayer: false, levelScaled: false, worldScaled: false, null, null, (WikiQuantitySemantics)0));
					}
					catch (Exception arg)
					{
						skip("trader entry", $"{((Object)prefab).name}[{i}]", arg);
					}
				}
			}
			catch (Exception arg2)
			{
				skip("trader component", SafeName((Object?)(object)prefab), arg2);
			}
		}

		private void ExtractBeehive(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				Beehive component = prefab.GetComponent<Beehive>();
				if (Object.op_Implicit((Object)(object)component) && Object.op_Implicit((Object)(object)component.m_honeyItem))
				{
					string name = ((Object)((Component)component.m_honeyItem).gameObject).name;
					string id = ((Object)prefab).name + ":Beehive:" + name;
					string name2 = ((Object)prefab).name;
					string sourceName = ComponentName(prefab, component.m_name);
					string? sourceMod = SourceMod(((Object)prefab).name);
					double? productionIntervalSeconds = Positive(component.m_secPerUnit);
					int? capacity = NonNegative(component.m_maxHoney);
					sources.Add(new RawAcquisitionSource(id, (WikiSourceKind)2, (WikiSourceCertainty)2, "Beehive", name2, sourceName, sourceMod, name, 1, 1, null, null, null, null, productionIntervalSeconds, capacity, null, onePerPlayer: false, levelScaled: false, worldScaled: false, null, null, (WikiQuantitySemantics)0));
				}
			}
			catch (Exception arg)
			{
				skip("beehive component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractSapCollector(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				SapCollector component = prefab.GetComponent<SapCollector>();
				if (Object.op_Implicit((Object)(object)component) && Object.op_Implicit((Object)(object)component.m_spawnItem))
				{
					string name = ((Object)((Component)component.m_spawnItem).gameObject).name;
					string id = ((Object)prefab).name + ":SapCollector:" + name;
					string name2 = ((Object)prefab).name;
					string sourceName = ComponentName(prefab, component.m_name);
					string? sourceMod = SourceMod(((Object)prefab).name);
					double? productionIntervalSeconds = Positive(component.m_secPerUnit);
					int? capacity = NonNegative(component.m_maxLevel);
					sources.Add(new RawAcquisitionSource(id, (WikiSourceKind)2, (WikiSourceCertainty)2, "SapCollector", name2, sourceName, sourceMod, name, 1, 1, null, null, null, null, productionIntervalSeconds, capacity, null, onePerPlayer: false, levelScaled: false, worldScaled: false, null, null, (WikiQuantitySemantics)0));
				}
			}
			catch (Exception arg)
			{
				skip("sap-collector component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractPickable(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				Pickable component = prefab.GetComponent<Pickable>();
				if (Object.op_Implicit((Object)(object)component) && Object.op_Implicit((Object)(object)component.m_itemPrefab))
				{
					if (component.m_amount <= 0)
					{
						throw new InvalidOperationException($"Pickable base amount '{component.m_amount}' is not positive.");
					}
					string name = ((Object)component.m_itemPrefab).name;
					string id = ((Object)prefab).name + ":Pickable:" + name;
					string name2 = ((Object)prefab).name;
					string sourceName = Localize(component.m_overrideName, Prettify(((Object)prefab).name));
					string? sourceMod = SourceMod(((Object)prefab).name);
					int amount = component.m_amount;
					int amount2 = component.m_amount;
					double? respawnSeconds = ((component.m_respawnTimeMinutes > 0f) ? new double?((double)component.m_respawnTimeMinutes * 60.0) : ((double?)null));
					bool worldScaled = !component.m_dontScale;
					sources.Add(new RawAcquisitionSource(id, (WikiSourceKind)3, (WikiSourceCertainty)0, "Pickable", name2, sourceName, sourceMod, name, amount, amount2, null, null, null, null, null, null, respawnSeconds, onePerPlayer: false, levelScaled: false, worldScaled, null, null, (WikiQuantitySemantics)0));
				}
			}
			catch (Exception arg)
			{
				skip("pickable component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractPickableItem(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				PickableItem component = prefab.GetComponent<PickableItem>();
				if (Object.op_Implicit((Object)(object)component) && (component.m_randomItemPrefabs == null || !component.m_randomItemPrefabs.Any((RandomItem entry) => (Object)(object)entry.m_itemPrefab != (Object)null)) && Object.op_Implicit((Object)(object)component.m_itemPrefab))
				{
					if (component.m_stack <= 0)
					{
						throw new InvalidOperationException($"Pickable-item stack '{component.m_stack}' is not positive.");
					}
					string name = ((Object)((Component)component.m_itemPrefab).gameObject).name;
					sources.Add(new RawAcquisitionSource(((Object)prefab).name + ":PickableItem:" + name, (WikiSourceKind)3, (WikiSourceCertainty)0, "PickableItem", ((Object)prefab).name, Prettify(((Object)prefab).name), SourceMod(((Object)prefab).name), name, component.m_stack, component.m_stack, null, null, null, null, null, null, null, onePerPlayer: false, levelScaled: false, worldScaled: false, null, null, (WikiQuantitySemantics)0));
				}
			}
			catch (Exception arg)
			{
				skip("pickable-item component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractMineRock(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				MineRock component = prefab.GetComponent<MineRock>();
				if (Object.op_Implicit((Object)(object)component))
				{
					ExtractDeterministicDropTable(prefab, component.m_dropItems, "MineRock", Localize(component.m_name, Prettify(((Object)prefab).name)), "mine", component.m_minToolTier, sources);
				}
			}
			catch (Exception arg)
			{
				skip("mine-rock component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractMineRock5(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				MineRock5 component = prefab.GetComponent<MineRock5>();
				if (Object.op_Implicit((Object)(object)component))
				{
					ExtractDeterministicDropTable(prefab, component.m_dropItems, "MineRock5", Localize(component.m_name, Prettify(((Object)prefab).name)), "mine", component.m_minToolTier, sources);
				}
			}
			catch (Exception arg)
			{
				skip("mine-rock-5 component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractTreeBase(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				TreeBase component = prefab.GetComponent<TreeBase>();
				if (Object.op_Implicit((Object)(object)component))
				{
					ExtractDeterministicDropTable(prefab, component.m_dropWhenDestroyed, "TreeBase", Prettify(((Object)prefab).name), "chop", component.m_minToolTier, sources);
				}
			}
			catch (Exception arg)
			{
				skip("tree-base component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractTreeLog(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				TreeLog component = prefab.GetComponent<TreeLog>();
				if (Object.op_Implicit((Object)(object)component))
				{
					ExtractDeterministicDropTable(prefab, component.m_dropWhenDestroyed, "TreeLog", Prettify(((Object)prefab).name), "chop", component.m_minToolTier, sources);
				}
			}
			catch (Exception arg)
			{
				skip("tree-log component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractDropOnDestroyed(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				DropOnDestroyed component = prefab.GetComponent<DropOnDestroyed>();
				Destructible component2 = prefab.GetComponent<Destructible>();
				if (Object.op_Implicit((Object)(object)component) && Object.op_Implicit((Object)(object)component2) && !Object.op_Implicit((Object)(object)prefab.GetComponent<Piece>()))
				{
					ExtractDeterministicDropTable(prefab, component.m_dropWhenDestroyed, "DropOnDestroyed", Prettify(((Object)prefab).name), "destroy", component2.m_minToolTier, sources);
				}
			}
			catch (Exception arg)
			{
				skip("drop-on-destroyed component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractFish(GameObject prefab, ICollection<RawAcquisitionSource> sources)
		{
			try
			{
				Fish component = prefab.GetComponent<Fish>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				GameObject val = component.m_pickupItem;
				if (!Object.op_Implicit((Object)(object)val) && Object.op_Implicit((Object)(object)prefab.GetComponent<ItemDrop>()))
				{
					val = prefab;
				}
				if (Object.op_Implicit((Object)(object)val) && Object.op_Implicit((Object)(object)val.GetComponent<ItemDrop>()))
				{
					if (component.m_pickupItemStackSize <= 0)
					{
						throw new InvalidOperationException($"Fish pickup stack '{component.m_pickupItemStackSize}' is not positive.");
					}
					string name = ((Object)val).name;
					sources.Add(new RawAcquisitionSource(((Object)prefab).name + ":Fish:" + name, (WikiSourceKind)5, (WikiSourceCertainty)2, "Fish", ((Object)prefab).name, Localize(component.m_name, Prettify(((Object)prefab).name)), SourceMod(((Object)prefab).name), name, component.m_pickupItemStackSize, component.m_pickupItemStackSize, null, null, null, null, null, null, null, onePerPlayer: false, levelScaled: false, worldScaled: false, "catch or collect", null, (WikiQuantitySemantics)0));
				}
			}
			catch (Exception arg)
			{
				skip("fish component", SafeName((Object?)(object)prefab), arg);
			}
		}

		private void ExtractDeterministicDropTable(GameObject prefab, DropTable? table, string adapter, string sourceName, string method, int minimumToolTier, ICollection<RawAcquisitionSource> sources)
		{
			//IL_012a: Unknown result type (might be due to invalid IL or missing references)
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Unknown result type (might be due to invalid IL or missing references)
			//IL_014d: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0203: Unknown result type (might be due to invalid IL or missing references)
			//IL_0209: Unknown result type (might be due to invalid IL or missing references)
			//IL_0215: Unknown result type (might be due to invalid IL or missing references)
			//IL_023c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0242: Unknown result type (might be due to invalid IL or missing references)
			//IL_024e: Unknown result type (might be due to invalid IL or missing references)
			//IL_025d: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0312: Unknown result type (might be due to invalid IL or missing references)
			//IL_036b: Unknown result type (might be due to invalid IL or missing references)
			if (table == null || table.m_drops == null || table.m_drops.Count == 0)
			{
				return;
			}
			if (table.m_drops.Count > 128)
			{
				throw new InvalidOperationException($"Drop table contains {table.m_drops.Count} entries; the P1 limit is 128.");
			}
			if (float.IsNaN(table.m_dropChance) || float.IsInfinity(table.m_dropChance) || table.m_dropChance < 0f || table.m_dropChance > 1f)
			{
				throw new InvalidOperationException($"Drop-table chance '{table.m_dropChance}' is outside the supported 0..1 range.");
			}
			if (table.m_dropChance < 1f)
			{
				return;
			}
			if (table.m_dropMin < 0 || table.m_dropMax < table.m_dropMin)
			{
				throw new InvalidOperationException($"Drop-table roll range '{table.m_dropMin}..{table.m_dropMax}' is invalid.");
			}
			if (table.m_dropMin == 0)
			{
				return;
			}
			DropData[] array = table.m_drops.Where((DropData entry) => Object.op_Implicit((Object)(object)entry.m_item)).ToArray();
			DropData[] array2 = array;
			foreach (DropData val in array2)
			{
				if (float.IsNaN(val.m_weight) || float.IsInfinity(val.m_weight) || val.m_weight < 0f)
				{
					throw new InvalidOperationException($"Drop-table weight '{val.m_weight}' is invalid.");
				}
			}
			if (array.Length != 1 || array[0].m_weight <= 0f)
			{
				return;
			}
			DropData val2 = array[0];
			if (Object.op_Implicit((Object)(object)val2.m_item.GetComponent<ItemDrop>()))
			{
				if (val2.m_stackMin <= 0 || val2.m_stackMax < val2.m_stackMin)
				{
					throw new InvalidOperationException($"Drop-table stack range '{val2.m_stackMin}..{val2.m_stackMax}' is invalid.");
				}
				EnvironmentalQuantityBounds val3 = EnvironmentalQuantityCalculator.CalculateGuaranteedSingleOutput(table.m_dropMin, table.m_dropMax, val2.m_stackMin, val2.m_stackMax, table.m_oneOfEach, val2.m_dontScale, 1.0);
				float resourceRate = Game.m_resourceRate;
				EnvironmentalQuantityBounds val4 = EnvironmentalQuantityCalculator.CalculateGuaranteedSingleOutput(table.m_dropMin, table.m_dropMax, val2.m_stackMin, val2.m_stackMax, table.m_oneOfEach, val2.m_dontScale, (double)resourceRate);
				string name = ((Object)val2.m_item).name;
				string id = ((Object)prefab).name + ":" + adapter + ":" + name;
				string name2 = ((Object)prefab).name;
				string? sourceMod = SourceMod(((Object)prefab).name);
				int minimum = val4.Minimum;
				int maximum = val4.Maximum;
				bool worldScaled = !val2.m_dontScale;
				int? minimumToolTier2 = ((minimumToolTier >= 0) ? new int?(minimumToolTier) : ((int?)null));
				WikiQuantitySemantics quantitySemantics = (WikiQuantitySemantics)(val4.IsVariable ? 1 : 0);
				int? configuredMinimumAmount = val3.Minimum;
				int? configuredMaximumAmount = val3.Maximum;
				double? loadedResourceRate = ((!val2.m_dontScale) ? new double?(resourceRate) : ((double?)null));
				sources.Add(new RawAcquisitionSource(id, (WikiSourceKind)4, (WikiSourceCertainty)0, adapter, name2, sourceName, sourceMod, name, minimum, maximum, null, null, null, null, null, null, null, onePerPlayer: false, levelScaled: false, worldScaled, method, minimumToolTier2, quantitySemantics, configuredMinimumAmount, configuredMaximumAmount, loadedResourceRate));
			}
		}

		private string? SourceMod(string prefabName)
		{
			if (!includeModSources)
			{
				return null;
			}
			IModPrefab prefab = ModQuery.GetPrefab(prefabName);
			if (prefab == null)
			{
				return null;
			}
			BepInPlugin sourceMod = prefab.SourceMod;
			if (sourceMod == null)
			{
				return null;
			}
			return sourceMod.Name;
		}

		private static string ComponentName(GameObject prefab, string? componentName)
		{
			Piece component = prefab.GetComponent<Piece>();
			if (Object.op_Implicit((Object)(object)component) && !string.IsNullOrWhiteSpace(component.m_name))
			{
				return Localize(component.m_name, Prettify(((Object)prefab).name));
			}
			return Localize(componentName, Prettify(((Object)prefab).name));
		}

		private static string ItemName(string itemId, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			if (items.TryGetValue(itemId, out WikiItem value))
			{
				return value.DisplayName;
			}
			if (!rawItems.TryGetValue(itemId, out ItemDrop value2))
			{
				return Prettify(itemId);
			}
			return Localize(value2.m_itemData.m_shared.m_name, itemId);
		}

		private static double? Positive(float value)
		{
			if (!(value > 0f) || float.IsNaN(value) || float.IsInfinity(value))
			{
				return null;
			}
			return value;
		}

		private static int? NonNegative(int value)
		{
			if (value < 0)
			{
				return null;
			}
			return value;
		}

		private static string Localize(string? token, string fallback)
		{
			if (string.IsNullOrWhiteSpace(token))
			{
				return fallback;
			}
			string text = ((Localization.instance != null) ? Localization.instance.Localize(token) : token) ?? string.Empty;
			if (string.IsNullOrWhiteSpace(text) || (text == token && token.StartsWith("$", StringComparison.Ordinal)))
			{
				return fallback;
			}
			return StripMarkup(text);
		}

		private static string StripMarkup(string value)
		{
			string text = Regex.Replace(value, "<[^>]+>", string.Empty).Trim();
			if (!string.IsNullOrWhiteSpace(text))
			{
				return text;
			}
			return value;
		}

		private static string Prettify(string value)
		{
			string text = TextNormalizer.Normalize(value);
			if (!string.IsNullOrWhiteSpace(text))
			{
				return string.Join(" ", from word in text.Split(new char[1] { ' ' })
					select char.ToUpperInvariant(word[0]) + word.Substring(1));
			}
			return value;
		}

		private static string SafeName(Object? value)
		{
			if (!Object.op_Implicit(value))
			{
				return "<null>";
			}
			return value.name;
		}
	}
	internal sealed class RawAcquisitionSource
	{
		internal string Id { get; }

		internal WikiSourceKind Kind { get; }

		internal WikiSourceCertainty Certainty { get; }

		internal string Adapter { get; }

		internal string SourceInternalName { get; }

		internal string SourceName { get; }

		internal string? SourceMod { get; }

		internal string OutputItemId { get; }

		internal int MinimumAmount { get; }

		internal int MaximumAmount { get; }

		internal double? Chance { get; }

		internal string? CostItemId { get; }

		internal int? Price { get; }

		internal string? UnlockRequirement { get; }

		internal double? ProductionIntervalSeconds { get; }

		internal int? Capacity { get; }

		internal double? RespawnSeconds { get; }

		internal bool OnePerPlayer { get; }

		internal bool LevelScaled { get; }

		internal bool WorldScaled { get; }

		internal string? Method { get; }

		internal int? MinimumToolTier { get; }

		internal WikiQuantitySemantics QuantitySemantics { get; }

		internal int? ConfiguredMinimumAmount { get; }

		internal int? ConfiguredMaximumAmount { get; }

		internal double? LoadedResourceRate { get; }

		internal RawAcquisitionSource(string id, WikiSourceKind kind, WikiSourceCertainty certainty, string adapter, string sourceInternalName, string sourceName, string? sourceMod, string outputItemId, int minimumAmount, int maximumAmount, double? chance = null, string? costItemId = null, int? price = null, string? unlockRequirement = null, double? productionIntervalSeconds = null, int? capacity = null, double? respawnSeconds = null, bool onePerPlayer = false, bool levelScaled = false, bool worldScaled = false, string? method = null, int? minimumToolTier = null, WikiQuantitySemantics quantitySemantics = (WikiQuantitySemantics)0, int? configuredMinimumAmount = null, int? configuredMaximumAmount = null, double? loadedResourceRate = null)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: 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_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			Id = id;
			Kind = kind;
			Certainty = certainty;
			Adapter = adapter;
			SourceInternalName = sourceInternalName;
			SourceName = sourceName;
			SourceMod = sourceMod;
			OutputItemId = outputItemId;
			MinimumAmount = minimumAmount;
			MaximumAmount = maximumAmount;
			Chance = chance;
			CostItemId = costItemId;
			Price = price;
			UnlockRequirement = unlockRequirement;
			ProductionIntervalSeconds = productionIntervalSeconds;
			Capacity = capacity;
			RespawnSeconds = respawnSeconds;
			OnePerPlayer = onePerPlayer;
			LevelScaled = levelScaled;
			WorldScaled = worldScaled;
			Method = method;
			MinimumToolTier = minimumToolTier;
			QuantitySemantics = quantitySemantics;
			ConfiguredMinimumAmount = configuredMinimumAmount;
			ConfiguredMaximumAmount = configuredMaximumAmount;
			LoadedResourceRate = loadedResourceRate;
		}
	}
	internal sealed class AliasRepository
	{
		[DataContract]
		private sealed class AliasFile
		{
			[DataMember(Name = "aliases")]
			public Dictionary<string, string>? Aliases { get; set; }
		}

		private readonly IReadOnlyDictionary<string, IReadOnlyList<string>> aliasesByTarget;

		private AliasRepository(IReadOnlyDictionary<string, IReadOnlyList<string>> aliasesByTarget)
		{
			this.aliasesByTarget = aliasesByTarget;
		}

		internal static AliasRepository Load(string path, ManualLogSource log)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Expected O, but got Unknown
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Expected O, but got Unknown
			if (!File.Exists(path))
			{
				return Empty();
			}
			try
			{
				DataContractJsonSerializer val = new DataContractJsonSerializer(typeof(AliasFile), new DataContractJsonSerializerSettings
				{
					UseSimpleDictionaryFormat = true
				});
				using FileStream fileStream = File.OpenRead(path);
				Dictionary<string, IReadOnlyList<string>> dictionary = ((((XmlObjectSerializer)val).ReadObject((Stream)fileStream) as AliasFile)?.Aliases ?? new Dictionary<string, string>()).Where<KeyValuePair<string, string>>((KeyValuePair<string, string> pair) => !string.IsNullOrWhiteSpace(pair.Key) && !string.IsNullOrWhiteSpace(pair.Value)).GroupBy<KeyValuePair<string, string>, string, string>((KeyValuePair<string, string> pair) => pair.Value.Trim(), (KeyValuePair<string, string> pair) => pair.Key.Trim(), StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, string>, string, IReadOnlyList<string>>((IGrouping<string, string> group) => group.Key, (IGrouping<string, string> group) => Array.AsReadOnly(group.Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray()), StringComparer.OrdinalIgnoreCase);
				log.LogInfo((object)$"Loaded {dictionary.Sum((KeyValuePair<string, IReadOnlyList<string>> pair) => pair.Value.Count)} local item aliases from the BepInEx config folder.");
				return new AliasRepository(new ReadOnlyDictionary<string, IReadOnlyList<string>>(dictionary));
			}
			catch (Exception ex)
			{
				log.LogWarning((object)("Could not load local alias JSON '" + Path.GetFileName(path) + "': " + ex.Message));
				return Empty();
			}
		}

		internal IReadOnlyList<string> For(string internalName, string displayName)
		{
			List<string> list = new List<string>();
			if (aliasesByTarget.TryGetValue(internalName, out IReadOnlyList<string> value))
			{
				list.AddRange(value);
			}
			if (aliasesByTarget.TryGetValue(displayName, out IReadOnlyList<string> value2))
			{
				list.AddRange(value2);
			}
			return Array.AsReadOnly(list.Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray());
		}

		private static AliasRepository Empty()
		{
			return new AliasRepository(new ReadOnlyDictionary<string, IReadOnlyList<string>>(new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase)));
		}
	}
	internal sealed class GameDataIndexer
	{
		private const int BaseWorldLevel = 0;

		private readonly ManualLogSource log;

		private readonly AliasRepository aliases;

		private int skipped;

		private int unavailableStatistics;

		private readonly HashSet<string> unavailableStatisticKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		private readonly List<WikiDiagnosticIssue> diagnostics = new List<WikiDiagnosticIssue>();

		private int omittedDiagnostics;

		internal GameDataIndexer(ManualLogSource log, AliasRepository aliases)
		{
			this.log = log ?? throw new ArgumentNullException("log");
			this.aliases = aliases ?? throw new ArgumentNullException("aliases");
		}

		internal WikiIndex Build(bool includeBuildPieces, bool includeModSources)
		{
			//IL_0281: Unknown result type (might be due to invalid IL or missing references)
			//IL_0287: Expected O, but got Unknown
			Stopwatch stopwatch = Stopwatch.StartNew();
			skipped = 0;
			unavailableStatistics = 0;
			unavailableStatisticKeys.Clear();
			diagnostics.Clear();
			omittedDiagnostics = 0;
			ObjectDB instance = ObjectDB.instance;
			if (!Object.op_Implicit((Object)(object)instance))
			{
				throw new InvalidOperationException("ObjectDB is unavailable.");
			}
			List<GameObject> runtimePrefabs = GetRuntimePrefabs();
			List<GameObject> processorPrefabs = GetProcessorPrefabs(runtimePrefabs);
			AcquisitionSourceExtractor acquisitionSourceExtractor = new AcquisitionSourceExtractor(includeModSources, Skip);
			IReadOnlyList<RawAcquisitionSource> readOnlyList = acquisitionSourceExtractor.Scan(runtimePrefabs);
			Dictionary<string, ItemDrop> dictionary = new Dictionary<string, ItemDrop>(StringComparer.OrdinalIgnoreCase);
			foreach (GameObject item in instance.m_items ?? new List<GameObject>())
			{
				try
				{
					if (Object.op_Implicit((Object)(object)item))
					{
						ItemDrop component = item.GetComponent<ItemDrop>();
						if (component?.m_itemData?.m_shared != null)
						{
							dictionary[((Object)item).name] = component;
						}
					}
				}
				catch (Exception exception)
				{
					Skip("item prefab", SafeName((Object?)(object)item), exception);
				}
			}
			HashSet<string> hashSet = CollectRecipeRelationships(instance);
			hashSet.UnionWith(CollectConversionRelationships(processorPrefabs));
			hashSet.UnionWith(readOnlyList.Select((RawAcquisitionSource source) => source.OutputItemId));
			Dictionary<string, WikiItem> dictionary2 = new Dictionary<string, WikiItem>(StringComparer.OrdinalIgnoreCase);
			foreach (KeyValuePair<string, ItemDrop> item2 in dictionary)
			{
				try
				{
					if (IsPlayerFacing(item2.Value, hashSet.Contains(item2.Key)))
					{
						dictionary2[item2.Key] = ExtractItem(item2.Key, item2.Value, includeModSources);
					}
				}
				catch (Exception exception2)
				{
					Skip("item", item2.Key, exception2);
				}
			}
			List<WikiRecipe> list = ExtractRecipes(instance, dictionary2, dictionary);
			List<WikiPiece> list2 = (List<WikiPiece>)(includeBuildPieces ? ((IList)ExtractPieces(dictionary2, dictionary)) : ((IList)new List<WikiPiece>()));
			List<WikiConversion> list3 = ExtractConversions(processorPrefabs, dictionary2, dictionary);
			IReadOnlyList<WikiAcquisitionSource> readOnlyList2 = acquisitionSourceExtractor.Build(readOnlyList, dictionary2, dictionary);
			stopwatch.Stop();
			return new WikiIndex((IEnumerable<WikiItem>)dictionary2.Values, (IEnumerable<WikiRecipe>)list, (IEnumerable<WikiPiece>)list2, DateTimeOffset.Now, stopwatch.Elapsed, CurrentLanguage(), skipped, includeBuildPieces ? "Coverage: enabled ObjectDB recipes, registered build pieces, processing conversions, reliable direct sources, guaranteed fixed or exact-range mineable/destructible/tree drops, and direct fish pickup. Chance/weighted/multi-output/zero-minimum environmental tables, fish extra drops, bait probabilities, containers, locations, biome inference, and procedural loot excluded. The immutable index contains all loaded content; the configured query visibility is applied separately." : "Coverage: enabled ObjectDB recipes, processing conversions, reliable direct sources, guaranteed fixed or exact-range environmental drops, and direct fish pickup; build pieces and chance/weighted/location sources excluded. The immutable index contains all loaded content; the configured query visibility is applied separately.", unavailableStatistics, WorldCalculatorContextAvailable(), (IEnumerable<WikiConversion>)list3, (IEnumerable<WikiAcquisitionSource>)readOnlyList2, (IEnumerable<WikiDiagnosticIssue>)diagnostics, omittedDiagnostics);
		}

		private List<GameObject> GetRuntimePrefabs()
		{
			Dictionary<string, GameObject> dictionary = new Dictionary<string, GameObject>(StringComparer.OrdinalIgnoreCase);
			ZNetScene instance = ZNetScene.instance;
			if (!Object.op_Implicit((Object)(object)instance))
			{
				log.LogWarning((object)"ZNetScene is unavailable; processing conversions and acquisition sources cannot be indexed in this lifecycle.");
				return new List<GameObject>();
			}
			foreach (GameObject item in (instance.m_prefabs ?? new List<GameObject>()).Concat(instance.m_nonNetViewPrefabs ?? new List<GameObject>()))
			{
				if (Object.op_Implicit((Object)(object)item))
				{
					dictionary[((Object)item).name] = item;
				}
			}
			return dictionary.Values.ToList();
		}

		private List<GameObject> GetProcessorPrefabs(IEnumerable<GameObject> runtimePrefabs)
		{
			List<GameObject> list = new List<GameObject>();
			foreach (GameObject runtimePrefab in runtimePrefabs)
			{
				try
				{
					if (Object.op_Implicit((Object)(object)runtimePrefab.GetComponent<Smelter>()) || Object.op_Implicit((Object)(object)runtimePrefab.GetComponent<CookingStation>()) || Object.op_Implicit((Object)(object)runtimePrefab.GetComponent<Fermenter>()) || Object.op_Implicit((Object)(object)runtimePrefab.GetComponent<Incinerator>()))
					{
						list.Add(runtimePrefab);
					}
				}
				catch (Exception exception)
				{
					Skip("processor prefab", SafeName((Object?)(object)runtimePrefab), exception);
				}
			}
			return list;
		}

		private HashSet<string> CollectRecipeRelationships(ObjectDB objectDb)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (Recipe item in objectDb.m_recipes ?? new List<Recipe>())
			{
				try
				{
					if (!Object.op_Implicit((Object)(object)item) || !item.m_enabled)
					{
						continue;
					}
					if (Object.op_Implicit((Object)(object)item.m_item))
					{
						hashSet.Add(((Object)((Component)item.m_item).gameObject).name);
					}
					Requirement[] array = item.m_resources ?? Array.Empty<Requirement>();
					foreach (Requirement val in array)
					{
						if (Object.op_Implicit((Object)(object)val?.m_resItem))
						{
							hashSet.Add(((Object)((Component)val.m_resItem).gameObject).name);
						}
					}
				}
				catch (Exception exception)
				{
					Skip("recipe relationship", SafeName((Object?)(object)item), exception);
				}
			}
			return hashSet;
		}

		private HashSet<string> CollectConversionRelationships(IEnumerable<GameObject> processorPrefabs)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (GameObject processorPrefab in processorPrefabs)
			{
				try
				{
					Smelter component = processorPrefab.GetComponent<Smelter>();
					if (Object.op_Implicit((Object)(object)component))
					{
						AddItem(hashSet, component.m_fuelItem);
						foreach (ItemConversion item in component.m_conversion ?? new List<ItemConversion>())
						{
							if (item != null)
							{
								AddItem(hashSet, item.m_from);
								AddItem(hashSet, item.m_to);
							}
						}
					}
					CookingStation component2 = processorPrefab.GetComponent<CookingStation>();
					if (Object.op_Implicit((Object)(object)component2))
					{
						AddItem(hashSet, component2.m_fuelItem);
						foreach (ItemConversion item2 in component2.m_conversion ?? new List<ItemConversion>())
						{
							if (item2 != null)
							{
								AddItem(hashSet, item2.m_from);
								AddItem(hashSet, item2.m_to);
							}
						}
					}
					Fermenter component3 = processorPrefab.GetComponent<Fermenter>();
					if (Object.op_Implicit((Object)(object)component3))
					{
						foreach (ItemConversion item3 in component3.m_conversion ?? new List<ItemConversion>())
						{
							if (item3 != null)
							{
								AddItem(hashSet, item3.m_from);
								AddItem(hashSet, item3.m_to);
							}
						}
					}
					Incinerator component4 = processorPrefab.GetComponent<Incinerator>();
					if (!Object.op_Implicit((Object)(object)component4))
					{
						continue;
					}
					foreach (IncineratorConversion item4 in component4.m_conversions ?? new List<IncineratorConversion>())
					{
						if (item4 == null)
						{
							continue;
						}
						AddItem(hashSet, item4.m_result);
						foreach (Requirement item5 in item4.m_requirements ?? new List<Requirement>())
						{
							if (item5 != null)
							{
								AddItem(hashSet, item5.m_resItem);
							}
						}
					}
				}
				catch (Exception exception)
				{
					Skip("conversion relationship", SafeName((Object?)(object)processorPrefab), exception);
				}
			}
			return hashSet;
		}

		private WikiItem ExtractItem(string id, ItemDrop drop, bool includeModSources)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: 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)
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Invalid comparison between Unknown and I4
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b7: Invalid comparison between Unknown and I4
			//IL_01ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f0: Invalid comparison between Unknown and I4
			//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Invalid comparison between Unknown and I4
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Invalid comparison between Unknown and I4
			//IL_0133: Unknown result type (might be due to invalid IL or missing references)
			//IL_0135: Invalid comparison between Unknown and I4
			//IL_018c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0196: Expected O, but got Unknown
			//IL_028c: Unknown result type (might be due to invalid IL or missing references)
			//IL_028e: Invalid comparison between Unknown and I4
			//IL_0290: Unknown result type (might be due to invalid IL or missing references)
			//IL_0292: Invalid comparison between Unknown and I4
			//IL_02b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ba: Expected O, but got Unknown
			ItemData data = drop.m_itemData;
			SharedData shared = data.m_shared;
			string text = Localize(shared.m_name, id);
			WikiItemKind val = Classify(shared);
			string text2 = Category(shared, val);
			int num = Math.Max(1, shared.m_maxQuality);
			Attack attack = shared.m_attack;
			Attack secondaryAttack = shared.m_secondaryAttack;
			List<WikiQualityStats> list = new List<WikiQualityStats>(num);
			int quality;
			for (quality = 1; quality <= num; quality++)
			{
				list.Add(new WikiQualityStats(quality, TryDamage(id, data, shared, quality), ((int)val == 3) ? TryArmor(id, data, quality) : ((double?)null), shared.m_useDurability ? TryStatistic(id, "durability", () => data.GetMaxDurability(quality)) : ((double?)null), ((int)val == 1 || (int)val == 2) ? TryStatistic(id, "block", () => data.GetBaseBlockPower(quality)) : ((double?)null), ((int)val == 1 || (int)val == 2) ? TryStatistic(id, "deflection", () => data.GetDeflectionForce(quality)) : ((double?)null), TryAttackStamina(id, "primary attack stamina", attack), TryAttackStamina(id, "secondary attack stamina", secondaryAttack)));
			}
			bool flag = (int)val == 4;
			return new WikiItem(id, id, text, Localize(shared.m_description, string.Empty), val, text2, (IEnumerable<string>)aliases.For(id, text), includeModSources ? SourceMod(id) : null, shared.m_maxStackSize, (double)shared.m_weight, num, (double)shared.m_movementModifier, flag ? new double?(shared.m_food) : ((double?)null), flag ? new double?(shared.m_foodStamina) : ((double?)null), flag ? new double?(shared.m_foodEitr) : ((double?)null), flag ? new double?(shared.m_foodBurnTime) : ((double?)null), flag ? new double?(shared.m_foodRegen) : ((double?)null), ((int)val == 1 || (int)val == 2) ? new double?(shared.m_timedBlockBonus) : ((double?)null), (IEnumerable<WikiQualityStats>)list, shared.m_name);
		}

		private List<WikiRecipe> ExtractRecipes(ObjectDB objectDb, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			//IL_01f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fd: Expected O, but got Unknown
			//IL_0185: Unknown result type (might be due to invalid IL or missing references)
			//IL_018f: Expected O, but got Unknown
			List<WikiRecipe> list = new List<WikiRecipe>();
			foreach (Recipe recipe in objectDb.m_recipes ?? new List<Recipe>())
			{
				try
				{
					if (!Object.op_Implicit((Object)(object)recipe) || !recipe.m_enabled || !Object.op_Implicit((Object)(object)recipe.m_item))
					{
						continue;
					}
					string name = ((Object)((Component)recipe.m_item).gameObject).name;
					if (!items.TryGetValue(name, out WikiItem value))
					{
						continue;
					}
					IEnumerable<KeyValuePair<int, int>> enumerable = from quality in Enumerable.Range(1, value.MaxQuality)
						select new KeyValuePair<int, int>(quality, recipe.GetRequiredStationLevel(quality));
					List<WikiRequirement> list2 = new List<WikiRequirement>();
					Requirement[] array = recipe.m_resources ?? Array.Empty<Requirement>();
					foreach (Requirement requirement in array)
					{
						if (!((Object)(object)requirement?.m_resItem == (Object)null))
						{
							string name2 = ((Object)((Component)requirement.m_resItem).gameObject).name;
							WikiItem value2;
							ItemDrop value3;
							string text = (items.TryGetValue(name2, out value2) ? value2.DisplayName : (rawItems.TryGetValue(name2, out value3) ? Localize(value3.m_itemData.m_shared.m_name, name2) : Prettify(name2)));
							IEnumerable<KeyValuePair<int, int>> enumerable2 = from quality in Enumerable.Range(1, value.MaxQuality)
								select new KeyValuePair<int, int>(quality, requirement.GetAmount(quality));
							list2.Add(new WikiRequirement(name2, text, enumerable2));
						}
					}
					list.Add(new WikiRecipe(string.IsNullOrWhiteSpace(((Object)recipe).name) ? name : ((Object)recipe).name, name, value.DisplayName, recipe.m_amount, StationName(recipe.m_craftingStation), enumerable, recipe.m_requireOnlyOneIngredient, (IEnumerable<WikiRequirement>)list2));
				}
				catch (Exception exception)
				{
					Skip("recipe", SafeName((Object?)(object)recipe), exception);
				}
			}
			return list;
		}

		private List<WikiPiece> ExtractPieces(IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			//IL_01ae: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Expected O, but got Unknown
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_013b: Expected O, but got Unknown
			List<WikiPiece> list = new List<WikiPiece>();
			foreach (PieceTable item in PieceManager.Instance.GetPieceTables() ?? new List<PieceTable>())
			{
				if (!Object.op_Implicit((Object)(object)item))
				{
					continue;
				}
				try
				{
					foreach (GameObject item2 in item.m_pieces ?? new List<GameObject>())
					{
						try
						{
							if (!Object.op_Implicit((Object)(object)item2))
							{
								continue;
							}
							Piece component = item2.GetComponent<Piece>();
							if (!Object.op_Implicit((Object)(object)component) || !component.m_enabled)
							{
								continue;
							}
							List<WikiPieceRequirement> list2 = new List<WikiPieceRequirement>();
							Requirement[] array = component.m_resources ?? Array.Empty<Requirement>();
							foreach (Requirement val in array)
							{
								if (!((Object)(object)val?.m_resItem == (Object)null))
								{
									string name = ((Object)((Component)val.m_resItem).gameObject).name;
									WikiItem value;
									ItemDrop value2;
									string text = (items.TryGetValue(name, out value) ? value.DisplayName : (rawItems.TryGetValue(name, out value2) ? Localize(value2.m_itemData.m_shared.m_name, name) : Prettify(name)));
									list2.Add(new WikiPieceRequirement(name, text, Math.Max(0, val.m_amount)));
								}
							}
							list.Add(new WikiPiece(((Object)((Component)item).gameObject).name + ":" + ((Object)item2).name, Localize(component.m_name, ((Object)item2).name), ((object)Unsafe.As<PieceCategory, PieceCategory>(ref component.m_category)/*cast due to .constrained prefix*/).ToString(), ((Object)((Component)item).gameObject).name, StationName(component.m_craftingStation), (IEnumerable<WikiPieceRequirement>)list2, component.m_name));
						}
						catch (Exception exception)
						{
							Skip("piece", SafeName((Object?)(object)item2), exception);
						}
					}
				}
				catch (Exception exception2)
				{
					Skip("piece table", SafeName((Object?)(object)item), exception2);
				}
			}
			return list;
		}

		private List<WikiConversion> ExtractConversions(IEnumerable<GameObject> processorPrefabs, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			List<WikiConversion> list = new List<WikiConversion>();
			foreach (GameObject processorPrefab in processorPrefabs)
			{
				ExtractSmelterConversions(processorPrefab, list, items, rawItems);
				ExtractCookingConversions(processorPrefab, list, items, rawItems);
				ExtractFermenterConversions(processorPrefab, list, items, rawItems);
				ExtractIncineratorConversions(processorPrefab, list, items, rawItems);
			}
			return (from @group in list.GroupBy<WikiConversion, string>((WikiConversion conversion) => conversion.Id, StringComparer.OrdinalIgnoreCase)
				select @group.First()).ToList();
		}

		private void ExtractSmelterConversions(GameObject prefab, ICollection<WikiConversion> conversions, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			try
			{
				Smelter component = prefab.GetComponent<Smelter>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				string stationName = Localize(component.m_name, Prettify(((Object)prefab).name));
				foreach (ItemConversion item in component.m_conversion ?? new List<ItemConversion>())
				{
					if ((Object)(object)item?.m_from == (Object)null || (Object)(object)item.m_to == (Object)null)
					{
						continue;
					}
					try
					{
						List<WikiConversionRequirement> list = new List<WikiConversionRequirement> { ConversionRequirement(item.m_from, 1.0, "Input", items, rawItems) };
						if (Object.op_Implicit((Object)(object)component.m_fuelItem) && component.m_fuelPerProduct > 0)
						{
							list.Add(ConversionRequirement(component.m_fuelItem, component.m_fuelPerProduct, "Fuel", items, rawItems));
						}
						conversions.Add(CreateConversion(((Object)prefab).name + ":processing:" + ((Object)((Component)item.m_from).gameObject).name + ":" + ((Object)((Component)item.m_to).gameObject).name, (WikiProcessKind)0, ((Object)prefab).name, stationName, item.m_to, 1, list, (component.m_secPerProduct > 0f) ? new double?(component.m_secPerProduct) : ((double?)null), requireOnlyOneIngredient: false, items, rawItems));
					}
					catch (Exception exception)
					{
						Skip("smelter conversion", SafeName((Object?)(object)prefab), exception);
					}
				}
			}
			catch (Exception exception2)
			{
				Skip("smelter component", SafeName((Object?)(object)prefab), exception2);
			}
		}

		private void ExtractCookingConversions(GameObject prefab, ICollection<WikiConversion> conversions, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			try
			{
				CookingStation component = prefab.GetComponent<CookingStation>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				string stationName = Localize(component.m_name, Prettify(((Object)prefab).name));
				foreach (ItemConversion item in component.m_conversion ?? new List<ItemConversion>())
				{
					if ((Object)(object)item?.m_from == (Object)null || (Object)(object)item.m_to == (Object)null)
					{
						continue;
					}
					try
					{
						List<WikiConversionRequirement> list = new List<WikiConversionRequirement> { ConversionRequirement(item.m_from, 1.0, "Input", items, rawItems) };
						if (component.m_useFuel && Object.op_Implicit((Object)(object)component.m_fuelItem))
						{
							list.Add(ConversionRequirement(component.m_fuelItem, null, "Fuel", items, rawItems));
						}
						conversions.Add(CreateConversion(((Object)prefab).name + ":cooking:" + ((Object)((Component)item.m_from).gameObject).name + ":" + ((Object)((Component)item.m_to).gameObject).name, (WikiProcessKind)1, ((Object)prefab).name, stationName, item.m_to, 1, list, (item.m_cookTime > 0f) ? new double?(item.m_cookTime) : ((double?)null), requireOnlyOneIngredient: false, items, rawItems));
					}
					catch (Exception exception)
					{
						Skip("cooking conversion", SafeName((Object?)(object)prefab), exception);
					}
				}
			}
			catch (Exception exception2)
			{
				Skip("cooking component", SafeName((Object?)(object)prefab), exception2);
			}
		}

		private void ExtractFermenterConversions(GameObject prefab, ICollection<WikiConversion> conversions, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			try
			{
				Fermenter component = prefab.GetComponent<Fermenter>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				string stationName = Localize(component.m_name, Prettify(((Object)prefab).name));
				foreach (ItemConversion item in component.m_conversion ?? new List<ItemConversion>())
				{
					if (!((Object)(object)item?.m_from == (Object)null) && !((Object)(object)item.m_to == (Object)null))
					{
						try
						{
							conversions.Add(CreateConversion(((Object)prefab).name + ":fermenting:" + ((Object)((Component)item.m_from).gameObject).name + ":" + ((Object)((Component)item.m_to).gameObject).name, (WikiProcessKind)2, ((Object)prefab).name, stationName, item.m_to, item.m_producedItems, (IEnumerable<WikiConversionRequirement>)(object)new WikiConversionRequirement[1] { ConversionRequirement(item.m_from, 1.0, "Input", items, rawItems) }, (component.m_fermentationDuration > 0f) ? new double?(component.m_fermentationDuration) : ((double?)null), requireOnlyOneIngredient: false, items, rawItems));
						}
						catch (Exception exception)
						{
							Skip("fermenter conversion", SafeName((Object?)(object)prefab), exception);
						}
					}
				}
			}
			catch (Exception exception2)
			{
				Skip("fermenter component", SafeName((Object?)(object)prefab), exception2);
			}
		}

		private void ExtractIncineratorConversions(GameObject prefab, ICollection<WikiConversion> conversions, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			try
			{
				Incinerator component = prefab.GetComponent<Incinerator>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				Piece component2 = prefab.GetComponent<Piece>();
				string stationName = (Object.op_Implicit((Object)(object)component2) ? Localize(component2.m_name, Prettify(((Object)prefab).name)) : Prettify(((Object)prefab).name));
				foreach (IncineratorConversion item in component.m_conversions ?? new List<IncineratorConversion>())
				{
					if ((Object)(object)item?.m_result == (Object)null)
					{
						continue;
					}
					try
					{
						WikiConversionRequirement[] requirements = (from requirement in item.m_requirements ?? new List<Requirement>()
							where (Object)(object)requirement?.m_resItem != (Object)null
							select ConversionRequirement(requirement.m_resItem, requirement.m_amount, "Input", items, rawItems)).ToArray();
						conversions.Add(CreateConversion($"{((Object)prefab).name}:incinerating:{((Object)((Component)item.m_result).gameObject).name}:{item.m_priority}", (WikiProcessKind)3, ((Object)prefab).name, stationName, item.m_result, item.m_resultAmount, requirements, null, item.m_requireOnlyOneIngredient, items, rawItems));
					}
					catch (Exception exception)
					{
						Skip("incinerator conversion", SafeName((Object?)(object)prefab), exception);
					}
				}
			}
			catch (Exception exception2)
			{
				Skip("incinerator component", SafeName((Object?)(object)prefab), exception2);
			}
		}

		private static WikiConversion CreateConversion(string id, WikiProcessKind kind, string stationInternalName, string stationName, ItemDrop output, int outputAmount, IEnumerable<WikiConversionRequirement> requirements, double? durationSeconds, bool requireOnlyOneIngredient, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Expected O, but got Unknown
			string name = ((Object)((Component)output).gameObject).name;
			return new WikiConversion(id, kind, stationInternalName, stationName, name, ItemName(name, items, rawItems), outputAmount, requirements, durationSeconds, requireOnlyOneIngredient);
		}

		private static WikiConversionRequirement ConversionRequirement(ItemDrop item, double? amount, string role, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Expected O, but got Unknown
			string name = ((Object)((Component)item).gameObject).name;
			return new WikiConversionRequirement(name, ItemName(name, items, rawItems), amount, role);
		}

		private static string ItemName(string itemId, IReadOnlyDictionary<string, WikiItem> items, IReadOnlyDictionary<string, ItemDrop> rawItems)
		{
			if (items.TryGetValue(itemId, out WikiItem value))
			{
				return value.DisplayName;
			}
			if (!rawItems.TryGetValue(itemId, out ItemDrop value2))
			{
				return Prettify(itemId);
			}
			return Localize(value2.m_itemData.m_shared.m_name, itemId);
		}

		private static void AddItem(ISet<string> related, ItemDrop? item)
		{
			if (Object.op_Implicit((Object)(object)item))
			{
				related.Add(((Object)((Component)it