Decompiled source of Beastwhispering v0.2.0

Beastwhispering/Beastwhispering.Core.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using CompanionKit.Core;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]
[assembly: AssemblyCompany("Beastwhispering.Core")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+c48b2eb6460f1b993d9ddb914cbdc5ff6f1d22c6")]
[assembly: AssemblyProduct("Beastwhispering.Core")]
[assembly: AssemblyTitle("Beastwhispering.Core")]
[assembly: AssemblyMetadata("BuildStamp", "c48b2eb 2026-07-30")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Beastwhispering.Core
{
	public enum CounterVerdict
	{
		Inactive,
		Expired,
		AlreadyCountered,
		Counter
	}
	public enum BraceHitVerdict
	{
		Counter,
		NoDealer,
		SelfHit,
		StatusTick,
		NotHostile
	}
	public struct BraceHitInputs
	{
		public string DealerUid;

		public string SelfUid;

		public bool SourceIsStatus;

		public bool DealerHostile;
	}
	public sealed class BraceState
	{
		private readonly HashSet<string> _countered = new HashSet<string>(StringComparer.Ordinal);

		public bool Open { get; private set; }

		public double ExpireAt { get; private set; }

		public IReadOnlyCollection<string> CounteredUids => _countered;

		public int CounterCount { get; private set; }

		public void Begin(double now, double windowSeconds)
		{
			Open = true;
			ExpireAt = now + ((windowSeconds > 0.0) ? windowSeconds : 0.0);
			_countered.Clear();
			CounterCount = 0;
		}

		public bool Active(double now)
		{
			if (Open)
			{
				return now < ExpireAt;
			}
			return false;
		}

		public double RemainingSeconds(double now)
		{
			if (!Active(now))
			{
				return 0.0;
			}
			return ExpireAt - now;
		}

		public void End()
		{
			Open = false;
			_countered.Clear();
		}

		public CounterVerdict TryCounter(string attackerUid, double now, bool perAttackerOnce)
		{
			if (!Open)
			{
				return CounterVerdict.Inactive;
			}
			if (now >= ExpireAt)
			{
				return CounterVerdict.Expired;
			}
			if (perAttackerOnce && !string.IsNullOrEmpty(attackerUid) && _countered.Contains(attackerUid))
			{
				return CounterVerdict.AlreadyCountered;
			}
			if (perAttackerOnce && !string.IsNullOrEmpty(attackerUid))
			{
				_countered.Add(attackerUid);
			}
			CounterCount++;
			return CounterVerdict.Counter;
		}
	}
	public static class BraceRules
	{
		public static BraceHitVerdict Eligible(in BraceHitInputs hit)
		{
			if (string.IsNullOrEmpty(hit.DealerUid))
			{
				return BraceHitVerdict.NoDealer;
			}
			if (!string.IsNullOrEmpty(hit.SelfUid) && string.Equals(hit.DealerUid, hit.SelfUid, StringComparison.Ordinal))
			{
				return BraceHitVerdict.SelfHit;
			}
			if (hit.SourceIsStatus)
			{
				return BraceHitVerdict.StatusTick;
			}
			if (!hit.DealerHostile)
			{
				return BraceHitVerdict.NotHostile;
			}
			return BraceHitVerdict.Counter;
		}
	}
	public enum BuffFoodKind
	{
		Damage,
		DecayRider
	}
	public sealed class BuffFoodDef
	{
		public string Key = "";

		public int? ItemId;

		public BuffFoodKind Kind;

		public double PercentPerLevel;

		public double? DurationSeconds;

		public string Toast = "";
	}
	public sealed class BuffFoodEntry
	{
		public string Species = "";

		public List<BuffFoodDef> Foods = new List<BuffFoodDef>();
	}
	public static class BuffFoods
	{
		public static Dictionary<string, BuffFoodEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "species", "an object of species → buff-food arrays", ParseEntry);
		}

		private static BuffFoodEntry ParseEntry(string species, object value, Action<string> warn)
		{
			if (!(value is List<object> list))
			{
				new FieldReader("'" + species + "'", warn).Say("value must be an ARRAY of buff-food objects — species skipped.");
				return null;
			}
			List<BuffFoodDef> list2 = new List<BuffFoodDef>();
			for (int i = 0; i < list.Count; i++)
			{
				BuffFoodDef buffFoodDef = ParseRow(species, i, list[i], warn);
				if (buffFoodDef != null)
				{
					list2.Add(buffFoodDef);
				}
			}
			return new BuffFoodEntry
			{
				Species = species,
				Foods = list2
			};
		}

		private static BuffFoodDef ParseRow(string species, int index, object value, Action<string> warn)
		{
			FieldReader fieldReader = new FieldReader($"'{species}' buff food #{index + 1}", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				fieldReader.Say("must be an object — entry skipped.");
				return null;
			}
			BuffFoodDef buffFoodDef = new BuffFoodDef
			{
				Kind = BuffFoodKind.Damage
			};
			bool flag = false;
			double? num = null;
			ItemKey val = default(ItemKey);
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "item":
					if (ItemKey.TryRead(item.Value, ref val))
					{
						buffFoodDef.Key = ((ItemKey)(ref val)).Key;
						buffFoodDef.ItemId = ((ItemKey)(ref val)).ItemId;
						flag = true;
					}
					else
					{
						fieldReader.Wrong("item", "a number (ItemID) or non-empty string (display name)");
					}
					break;
				case "kind":
				{
					if (item.Value is string name && TryParseKind(name, out var kind))
					{
						buffFoodDef.Kind = kind;
						break;
					}
					fieldReader.Say($"unknown kind '{item.Value}' (valid: damage, decayRider) — entry skipped.");
					return null;
				}
				case "percentperlevel":
				{
					if (!fieldReader.Num("percentPerLevel", item.Value, out var result, "a number > 0", "entry skipped"))
					{
						return null;
					}
					if (!(result > 0.0))
					{
						fieldReader.Wrong("percentPerLevel", "a number > 0", "entry skipped");
						return null;
					}
					num = result;
					break;
				}
				case "durationseconds":
				{
					if (!fieldReader.Num("durationSeconds", item.Value, out var result2, "a number > 0 (seconds)", "entry skipped"))
					{
						return null;
					}
					if (!(result2 > 0.0))
					{
						fieldReader.Wrong("durationSeconds", "a number > 0 (seconds)", "entry skipped");
						return null;
					}
					buffFoodDef.DurationSeconds = result2;
					break;
				}
				case "toast":
					if (item.Value is string toast)
					{
						buffFoodDef.Toast = toast;
					}
					else
					{
						fieldReader.Wrong("toast", "a string", "ignored");
					}
					break;
				default:
					fieldReader.Unknown(item.Key, "item, kind, percentPerLevel, durationSeconds, toast");
					break;
				}
			}
			if (!flag)
			{
				fieldReader.Say("missing/invalid required 'item' — entry skipped.");
				return null;
			}
			if (!num.HasValue)
			{
				fieldReader.Say("missing required 'percentPerLevel' (> 0) — entry skipped.");
				return null;
			}
			buffFoodDef.PercentPerLevel = num.Value;
			return buffFoodDef;
		}

		private static bool TryParseKind(string name, out BuffFoodKind kind)
		{
			if (string.Equals(name, "damage", StringComparison.OrdinalIgnoreCase))
			{
				kind = BuffFoodKind.Damage;
				return true;
			}
			if (string.Equals(name, "decayRider", StringComparison.OrdinalIgnoreCase))
			{
				kind = BuffFoodKind.DecayRider;
				return true;
			}
			kind = BuffFoodKind.Damage;
			return false;
		}

		public static Dictionary<string, BuffFoodEntry> Merge(Dictionary<string, BuffFoodEntry> builtIn, Dictionary<string, BuffFoodEntry> overrides)
		{
			return SpeciesTable.Merge<BuffFoodEntry>(builtIn, overrides);
		}

		public static BuffFoodEntry Resolve(Dictionary<string, BuffFoodEntry> table, string speciesId)
		{
			BuffFoodEntry result = default(BuffFoodEntry);
			if (!SpeciesTable.TryResolve<BuffFoodEntry>(table, speciesId, ref result, (string)null))
			{
				return null;
			}
			return result;
		}

		public static BuffFoodDef Match(BuffFoodEntry entry, int itemId, string itemName)
		{
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			if (entry?.Foods == null)
			{
				return null;
			}
			foreach (BuffFoodDef food in entry.Foods)
			{
				ItemKey val = new ItemKey(food.Key, food.ItemId);
				if (((ItemKey)(ref val)).Matches(itemId, itemName))
				{
					return food;
				}
			}
			return null;
		}

		public static BuffFoodDef DefFor(BuffFoodEntry entry, string slotKey)
		{
			if (entry?.Foods == null || string.IsNullOrEmpty(slotKey))
			{
				return null;
			}
			foreach (BuffFoodDef food in entry.Foods)
			{
				if (string.Equals(food.Key, slotKey, StringComparison.OrdinalIgnoreCase))
				{
					return food;
				}
			}
			return null;
		}

		public static double Percent(BuffFoodDef def, LoyaltyTier tier)
		{
			if (def != null)
			{
				return def.PercentPerLevel * (double)PetBuffs.Level(tier);
			}
			return 0.0;
		}

		public static float DamageFactor(BuffFoodDef def, LoyaltyTier tier, double secondsLeft)
		{
			if (def == null || def.Kind != BuffFoodKind.Damage || !(secondsLeft > 0.0))
			{
				return 1f;
			}
			return (float)(1.0 + Percent(def, tier) / 100.0);
		}

		public static float DecayFraction(BuffFoodDef def, LoyaltyTier tier, double secondsLeft)
		{
			if (def == null || def.Kind != BuffFoodKind.DecayRider || !(secondsLeft > 0.0))
			{
				return 0f;
			}
			return (float)(Percent(def, tier) / 100.0);
		}

		public static double ResolveDuration(BuffFoodDef def, double hungerSecondsPerDay)
		{
			return def?.DurationSeconds ?? hungerSecondsPerDay;
		}

		public static string Describe(BuffFoodDef def, LoyaltyTier tier, double secondsLeft)
		{
			if (def == null)
			{
				return "no buff food";
			}
			double num = Percent(def, tier);
			string text = ((def.Kind == BuffFoodKind.DecayRider) ? string.Format(CultureInfo.InvariantCulture, "+{0:F1}% of total damage as Decay", num) : string.Format(CultureInfo.InvariantCulture, "+{0:F1}% pet damage", num));
			return string.Format(CultureInfo.InvariantCulture, "{0}: {1} ({2} lvl {3}, {4:F0}s left)", def.Key, text, tier, PetBuffs.Level(tier), secondsLeft);
		}
	}
	public enum BwIdRegistry
	{
		ItemId,
		EffectPreset
	}
	public readonly struct BwIdRange
	{
		public int Start { get; }

		public int End { get; }

		public BwIdRange(int start, int end)
		{
			Start = start;
			End = end;
		}

		public bool Contains(int id)
		{
			if (id >= Start)
			{
				return id <= End;
			}
			return false;
		}

		public override string ToString()
		{
			return $"{Start}-{End}";
		}
	}
	public readonly struct BwIdEntry
	{
		public int Id { get; }

		public string Key { get; }

		public string Family { get; }

		public BwIdRegistry Registry { get; }

		public string Name { get; }

		public BwIdEntry(int id, string key, string family, BwIdRegistry registry, string name)
		{
			Id = id;
			Key = key;
			Family = family;
			Registry = registry;
			Name = name;
		}

		public override string ToString()
		{
			return $"{Id} {Key} ({Family}, {Registry})";
		}
	}
	public static class BwIds
	{
		public const int SkillHealPet = 87000;

		public const int SkillHuntAsOne = 87001;

		public const int SkillPetCommand = 87002;

		public const int SkillReleasePet = 87003;

		public const int SkillPetGift = 87004;

		public const int SkillForTheKill = 87005;

		public const int SkillScatology = 87006;

		public const int SkillBeastOfBurden = 87007;

		public const int SkillWildUnknown = 87008;

		public const int SkillCommunion = 87009;

		public const int StatusPetHungry = 87050;

		public const int StatusPetStarving = 87051;

		public const int StatusBondFraying = 87052;

		public const int StatusBondBroken = 87053;

		public const int StatusBondSteady = 87054;

		public const int StatusBondDevoted = 87055;

		public const int StatusPetCold = 87056;

		public const int StatusPetFreezing = 87057;

		public const int StatusPetHot = 87058;

		public const int StatusPetOverheating = 87059;

		public const int StatusPetScent = 87060;

		public const int StatusSynergy = 87061;

		public const int StatusKillFavor = 87062;

		public const int StatusCommunionBroken = 87063;

		public const int StatusCommunionFraying = 87064;

		public const int StatusCommunionSteady = 87065;

		public const int StatusCommunionDevoted = 87066;

		public const int StatusBuffCrystalPowder = 87067;

		public const int StatusBuffAmbraine = 87068;

		public const int StatusBuffGaberryWine = 87069;

		public const int StatusBuffDarkStone = 87070;

		public const int BlanketHeatingBlanket = 87100;

		public const int BlanketCoolingBlanket = 87101;

		public const int SigilFire = 87150;

		public const int SigilFrost = 87151;

		public const int SigilAir = 87152;

		public const int SigilBlood = 87153;

		public const int FeatherTattered = 87200;

		public const int FeatherRuffled = 87201;

		public const int FeatherSleek = 87202;

		public const int FeatherResplendent = 87203;

		public const int FletchTatteredPhysical = 87220;

		public const int FletchRuffledPhysical = 87221;

		public const int FletchSleekPhysical = 87222;

		public const int FletchResplendentRaw = 87223;

		public const int FletchTatteredEthereal = 87224;

		public const int FletchTatteredDecay = 87225;

		public const int FletchTatteredElectric = 87226;

		public const int FletchTatteredFrost = 87227;

		public const int FletchTatteredFire = 87228;

		public const int FletchRuffledEthereal = 87229;

		public const int FletchRuffledDecay = 87230;

		public const int FletchRuffledElectric = 87231;

		public const int FletchRuffledFrost = 87232;

		public const int FletchRuffledFire = 87233;

		public const int FletchSleekEthereal = 87234;

		public const int FletchSleekDecay = 87235;

		public const int FletchSleekElectric = 87236;

		public const int FletchSleekFrost = 87237;

		public const int FletchSleekFire = 87238;

		public const int TamingHyenaChow = 87300;

		public const int TamingHyenaScroll = 87301;

		public const int TamingPearlbirdChow = 87302;

		public const int TamingPearlbirdScroll = 87303;

		public const int TamingVeaberChow = 87304;

		public const int TamingVeaberScroll = 87305;

		public static readonly BwIdEntry[] All = new BwIdEntry[66]
		{
			new BwIdEntry(87000, "skill.heal-pet", "skills", BwIdRegistry.ItemId, "Heal Pet"),
			new BwIdEntry(87001, "skill.hunt-as-one", "skills", BwIdRegistry.ItemId, "Hunt as One"),
			new BwIdEntry(87002, "skill.pet-command", "skills", BwIdRegistry.ItemId, "Command Pet"),
			new BwIdEntry(87003, "skill.release-pet", "skills", BwIdRegistry.ItemId, "Release Pet"),
			new BwIdEntry(87004, "skill.pet-gift", "skills", BwIdRegistry.ItemId, "Gift of the Wild"),
			new BwIdEntry(87005, "skill.for-the-kill", "skills", BwIdRegistry.ItemId, "For the Kill"),
			new BwIdEntry(87006, "skill.scatology", "skills", BwIdRegistry.ItemId, "Scatology"),
			new BwIdEntry(87007, "skill.beast-of-burden", "skills", BwIdRegistry.ItemId, "Beast of Burden"),
			new BwIdEntry(87008, "skill.wild-unknown", "skills", BwIdRegistry.ItemId, "Wild Unknown"),
			new BwIdEntry(87009, "skill.communion", "skills", BwIdRegistry.ItemId, "Communion"),
			new BwIdEntry(87050, "status.pet-hungry", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87051, "status.pet-starving", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87052, "status.bond-fraying", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87053, "status.bond-broken", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87054, "status.bond-steady", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87055, "status.bond-devoted", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87056, "status.pet-cold", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87057, "status.pet-freezing", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87058, "status.pet-hot", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87059, "status.pet-overheating", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87060, "status.pet-scent", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87061, "status.synergy", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87062, "status.kill-favor", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87063, "status.communion-broken", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87064, "status.communion-fraying", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87065, "status.communion-steady", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87066, "status.communion-devoted", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87067, "status.buff-crystal-powder", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87068, "status.buff-ambraine", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87069, "status.buff-gaberry-wine", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87070, "status.buff-dark-stone", "statusPresets", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87100, "blanket.heating-blanket", "blankets", BwIdRegistry.ItemId, "Heating Blanket"),
			new BwIdEntry(87101, "blanket.cooling-blanket", "blankets", BwIdRegistry.ItemId, "Cooling Blanket"),
			new BwIdEntry(87150, "sigil.fire", "sigils", BwIdRegistry.ItemId, "Pet Fire Sigil"),
			new BwIdEntry(87151, "sigil.frost", "sigils", BwIdRegistry.ItemId, "Pet Frost Sigil"),
			new BwIdEntry(87152, "sigil.air", "sigils", BwIdRegistry.ItemId, "Pet Wind Sigil"),
			new BwIdEntry(87153, "sigil.blood", "sigils", BwIdRegistry.ItemId, "Pet Blood Sigil"),
			new BwIdEntry(87200, "feather.tattered", "feathers", BwIdRegistry.ItemId, "Tattered Pearlbird Feather"),
			new BwIdEntry(87201, "feather.ruffled", "feathers", BwIdRegistry.ItemId, "Ruffled Pearlbird Feather"),
			new BwIdEntry(87202, "feather.sleek", "feathers", BwIdRegistry.ItemId, "Sleek Pearlbird Feather"),
			new BwIdEntry(87203, "feather.resplendent", "feathers", BwIdRegistry.ItemId, "Resplendent Pearlbird Feather"),
			new BwIdEntry(87220, "fletch.tattered-physical", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87221, "fletch.ruffled-physical", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87222, "fletch.sleek-physical", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87223, "fletch.resplendent-raw", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87224, "fletch.tattered-ethereal", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87225, "fletch.tattered-decay", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87226, "fletch.tattered-electric", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87227, "fletch.tattered-frost", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87228, "fletch.tattered-fire", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87229, "fletch.ruffled-ethereal", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87230, "fletch.ruffled-decay", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87231, "fletch.ruffled-electric", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87232, "fletch.ruffled-frost", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87233, "fletch.ruffled-fire", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87234, "fletch.sleek-ethereal", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87235, "fletch.sleek-decay", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87236, "fletch.sleek-electric", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87237, "fletch.sleek-frost", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87238, "fletch.sleek-fire", "fletchEnchantments", BwIdRegistry.EffectPreset, null),
			new BwIdEntry(87300, "taming.hyena.chow", "taming", BwIdRegistry.ItemId, "Hyena Chow"),
			new BwIdEntry(87301, "taming.hyena.scroll", "taming", BwIdRegistry.ItemId, "Recipe: Hyena Chow"),
			new BwIdEntry(87302, "taming.pearlbird.chow", "taming", BwIdRegistry.ItemId, "Pearlbird Chow"),
			new BwIdEntry(87303, "taming.pearlbird.scroll", "taming", BwIdRegistry.ItemId, "Recipe: Pearlbird Chow"),
			new BwIdEntry(87304, "taming.veaber.chow", "taming", BwIdRegistry.ItemId, "Veaber Chow"),
			new BwIdEntry(87305, "taming.veaber.scroll", "taming", BwIdRegistry.ItemId, "Recipe: Veaber Chow")
		};

		public static readonly BwIdRange[] Pool = new BwIdRange[1]
		{
			new BwIdRange(87000, 87999)
		};

		public static bool TryGet(string key, out int id)
		{
			BwIdEntry[] all = All;
			for (int i = 0; i < all.Length; i++)
			{
				BwIdEntry bwIdEntry = all[i];
				if (bwIdEntry.Key == key)
				{
					id = bwIdEntry.Id;
					return true;
				}
			}
			id = 0;
			return false;
		}

		public static bool TryFind(int id, out BwIdEntry entry)
		{
			BwIdEntry[] all = All;
			for (int i = 0; i < all.Length; i++)
			{
				BwIdEntry bwIdEntry = all[i];
				if (bwIdEntry.Id == id)
				{
					entry = bwIdEntry;
					return true;
				}
			}
			entry = default(BwIdEntry);
			return false;
		}

		public static bool InPool(int id)
		{
			BwIdRange[] pool = Pool;
			foreach (BwIdRange bwIdRange in pool)
			{
				if (bwIdRange.Contains(id))
				{
					return true;
				}
			}
			return false;
		}

		public static bool IsOurs(int id)
		{
			BwIdEntry entry;
			return TryFind(id, out entry);
		}

		public static IEnumerable<string> Lines()
		{
			BwIdRange[] pool = Pool;
			foreach (BwIdRange bwIdRange in pool)
			{
				yield return $"pool {bwIdRange}";
			}
			BwIdEntry[] all = All;
			for (int i = 0; i < all.Length; i++)
			{
				BwIdEntry bwIdEntry = all[i];
				yield return bwIdEntry.ToString();
			}
		}
	}
	public enum ComfortGate
	{
		NeverSampled,
		Ok,
		SystemOff,
		NoPetState,
		NoPlayer,
		NoPetSim,
		GameplayPaused,
		NoEnvironment,
		NoSampleTransform
	}
	public enum AmbientSource
	{
		None,
		Environment,
		Rediscovered
	}
	public static class ComfortGates
	{
		public static ComfortGate Evaluate(bool systemEnabled, bool hasPetState, bool hasEnvironment, bool hasSampleTransform)
		{
			if (!systemEnabled)
			{
				return ComfortGate.SystemOff;
			}
			if (!hasPetState)
			{
				return ComfortGate.NoPetState;
			}
			if (!hasEnvironment)
			{
				return ComfortGate.NoEnvironment;
			}
			if (!hasSampleTransform)
			{
				return ComfortGate.NoSampleTransform;
			}
			return ComfortGate.Ok;
		}

		public static string Code(ComfortGate gate)
		{
			return gate switch
			{
				ComfortGate.Ok => "ok", 
				ComfortGate.SystemOff => "system-off", 
				ComfortGate.NoPetState => "no-pet-state", 
				ComfortGate.NoPlayer => "no-player", 
				ComfortGate.NoPetSim => "no-pet-sim", 
				ComfortGate.GameplayPaused => "gameplay-paused", 
				ComfortGate.NoEnvironment => "no-env", 
				ComfortGate.NoSampleTransform => "no-transform", 
				_ => "never-sampled", 
			};
		}

		public static bool IsFault(ComfortGate gate)
		{
			if (gate != ComfortGate.Ok && gate != ComfortGate.SystemOff)
			{
				return gate != ComfortGate.GameplayPaused;
			}
			return false;
		}

		public static AmbientSource PreferredAmbientSource(bool envAvailable, bool rediscoveredAvailable)
		{
			if (envAvailable)
			{
				return AmbientSource.Environment;
			}
			if (rediscoveredAvailable)
			{
				return AmbientSource.Rediscovered;
			}
			return AmbientSource.None;
		}

		public static string SourceLabel(AmbientSource source)
		{
			return source switch
			{
				AmbientSource.Environment => "environment", 
				AmbientSource.Rediscovered => "rediscovered-environment", 
				_ => "none", 
			};
		}
	}
	public static class CommunionBadge
	{
		public const string NoBenefit = "no benefit yet — deepen the bond";

		public static string Describe(IReadOnlyList<ResolvedBuff> benefits)
		{
			if (benefits == null || benefits.Count == 0)
			{
				return "no benefit yet — deepen the bond";
			}
			return string.Join(", ", from b in benefits
				orderby (int)b.Stat
				select "+" + b.Amount.ToString("0.##", CultureInfo.InvariantCulture) + (PetBuffs.IsFlat(b.Stat) ? " " : "% ") + StatName(b.Stat));
		}

		public static string Description(LoyaltyTier tier, string benefits)
		{
			object obj = tier switch
			{
				LoyaltyTier.Gone => "The communion is all but severed.", 
				LoyaltyTier.Broken => "The communion is all but severed.", 
				LoyaltyTier.Fraying => "The communion is fraying.", 
				LoyaltyTier.Steady => "The communion holds steady.", 
				LoyaltyTier.Devoted => "The communion runs deep.", 
				_ => "The communion endures.", 
			};
			string text = ((string.IsNullOrEmpty(benefits) || benefits == "no benefit yet — deepen the bond") ? "No benefit yet — deepen the bond." : ("The bond grants you " + benefits + "."));
			return (string?)obj + " " + text;
		}

		private static string StatName(PetBuffStat stat)
		{
			return PetBuffs.DisplayName(stat);
		}
	}
	internal static class EnumRead
	{
		public static bool TryName<T>(string s, out T value) where T : struct
		{
			value = default(T);
			if (string.IsNullOrEmpty(s))
			{
				return false;
			}
			if (!Enum.TryParse<T>(s, ignoreCase: true, out var result))
			{
				return false;
			}
			if (!Enum.IsDefined(typeof(T), result))
			{
				return false;
			}
			value = result;
			return true;
		}
	}
	public sealed class TemperatureExposure
	{
		private double _timer;

		private int _dir;

		public double EscalateSeconds { get; }

		public double RecoverSeconds { get; }

		public ComfortStage Stage { get; private set; }

		public TemperatureExposure(double escalateSeconds = 30.0, double recoverSeconds = 15.0, ComfortStage startStage = ComfortStage.Comfortable)
		{
			EscalateSeconds = escalateSeconds;
			RecoverSeconds = recoverSeconds;
			Stage = startStage;
		}

		public ComfortStage Tick(int stepsOutside, double dt, double escalateMult = 1.0)
		{
			ComfortStage comfortStage = Temperature.Stage(stepsOutside);
			int num = comfortStage.CompareTo(Stage);
			if (num == 0)
			{
				_timer = 0.0;
				_dir = 0;
				return Stage;
			}
			if (num != _dir)
			{
				_timer = 0.0;
				_dir = num;
			}
			_timer += dt;
			double num2 = ((num > 0) ? (EscalateSeconds * Math.Max(1.0, escalateMult)) : RecoverSeconds);
			while (_timer >= num2 && comfortStage.CompareTo(Stage) == num)
			{
				_timer -= num2;
				Stage += num;
			}
			if (Stage == comfortStage)
			{
				_timer = 0.0;
				_dir = 0;
			}
			return Stage;
		}

		public ComfortStage Tick(TempStep temp, ComfortBand band, double dt)
		{
			return Tick(Temperature.StepsOutside(temp, band), dt);
		}
	}
	public sealed class HungerTimer
	{
		private int _daysApplied;

		public double SecondsPerDay { get; }

		public double SecondsSinceFed { get; private set; }

		public HungerTimer(double secondsPerDay, double secondsSinceFed = 0.0)
		{
			SecondsPerDay = secondsPerDay;
			SecondsSinceFed = secondsSinceFed;
			_daysApplied = ((secondsPerDay > 0.0) ? ((int)(secondsSinceFed / secondsPerDay)) : 0);
		}

		public void Feed()
		{
			SecondsSinceFed = 0.0;
			_daysApplied = 0;
		}

		public void Seed(double secondsSinceFed)
		{
			SecondsSinceFed = ((secondsSinceFed < 0.0) ? 0.0 : secondsSinceFed);
			_daysApplied = ((SecondsPerDay > 0.0) ? ((int)(SecondsSinceFed / SecondsPerDay)) : 0);
		}

		public int Tick(double dt)
		{
			SecondsSinceFed += dt;
			if (SecondsPerDay <= 0.0)
			{
				return 0;
			}
			int num = (int)(SecondsSinceFed / SecondsPerDay);
			int result = num - _daysApplied;
			_daysApplied = num;
			return result;
		}
	}
	public sealed class FletchEntry
	{
		public string Key;

		public string ArrowKey;

		public int? ArrowId;
	}
	public sealed class FeatherQuality
	{
		public string Quality;

		public LoyaltyTier Tier;

		public int Percent;

		public int FlatX;

		public int ItemId;

		public int EnchantmentId;

		public string ItemName => Quality + " Pearlbird Feather";
	}
	public static class FeatherFletching
	{
		public struct StackSlot
		{
			public string Key;

			public int FreeSpace;

			public StackSlot(string key, int freeSpace)
			{
				Key = key ?? string.Empty;
				FreeSpace = freeSpace;
			}
		}

		public const int PhysicalDamageType = 0;

		public const int RawDamageType = 8;

		public const int ElementalVariantBase = 87224;

		private const int ElementalVariantCount = 15;

		public static readonly FeatherQuality[] Qualities = new FeatherQuality[4]
		{
			new FeatherQuality
			{
				Quality = "Tattered",
				Tier = LoyaltyTier.Broken,
				Percent = 10,
				FlatX = 2,
				ItemId = 87200,
				EnchantmentId = 87220
			},
			new FeatherQuality
			{
				Quality = "Ruffled",
				Tier = LoyaltyTier.Fraying,
				Percent = 20,
				FlatX = 3,
				ItemId = 87201,
				EnchantmentId = 87221
			},
			new FeatherQuality
			{
				Quality = "Sleek",
				Tier = LoyaltyTier.Steady,
				Percent = 30,
				FlatX = 5,
				ItemId = 87202,
				EnchantmentId = 87222
			},
			new FeatherQuality
			{
				Quality = "Resplendent",
				Tier = LoyaltyTier.Devoted,
				Percent = 40,
				FlatX = 7,
				ItemId = 87203,
				EnchantmentId = 87223
			}
		};

		public static FeatherQuality QualityFor(int loyalty)
		{
			LoyaltyTier loyaltyTier = Loyalty.Tier(loyalty);
			for (int num = Qualities.Length - 1; num > 0; num--)
			{
				if (loyaltyTier >= Qualities[num].Tier)
				{
					return Qualities[num];
				}
			}
			return Qualities[0];
		}

		public static int? PercentForItemId(int itemId)
		{
			FeatherQuality[] qualities = Qualities;
			foreach (FeatherQuality featherQuality in qualities)
			{
				if (featherQuality.ItemId == itemId)
				{
					return featherQuality.Percent;
				}
			}
			return null;
		}

		public static FeatherQuality QualityForItemId(int itemId)
		{
			FeatherQuality[] qualities = Qualities;
			foreach (FeatherQuality featherQuality in qualities)
			{
				if (featherQuality.ItemId == itemId)
				{
					return featherQuality;
				}
			}
			return null;
		}

		private static int IndexOf(FeatherQuality q)
		{
			if (q == null)
			{
				return -1;
			}
			for (int i = 0; i < Qualities.Length; i++)
			{
				if (Qualities[i] == q || Qualities[i].EnchantmentId == q.EnchantmentId)
				{
					return i;
				}
			}
			return -1;
		}

		public static int EnchantmentIdFor(FeatherQuality q, int damageType)
		{
			int num = IndexOf(q);
			if (num < 0)
			{
				return 0;
			}
			if (num == Qualities.Length - 1)
			{
				return q.EnchantmentId;
			}
			if (damageType <= 0 || damageType > 5)
			{
				return q.EnchantmentId;
			}
			return 87224 + num * 5 + (damageType - 1);
		}

		public static FeatherQuality QualityForEnchantmentId(int enchantmentId)
		{
			FeatherQuality[] qualities = Qualities;
			foreach (FeatherQuality featherQuality in qualities)
			{
				if (featherQuality.EnchantmentId == enchantmentId)
				{
					return featherQuality;
				}
			}
			if (enchantmentId >= 87224 && enchantmentId < 87239)
			{
				int num = (enchantmentId - 87224) / 5;
				if (num >= 0 && num < Qualities.Length)
				{
					return Qualities[num];
				}
			}
			return null;
		}

		public static int? FlatTypeForEnchantmentId(int enchantmentId)
		{
			FeatherQuality featherQuality = Qualities[Qualities.Length - 1];
			if (enchantmentId == featherQuality.EnchantmentId)
			{
				return 8;
			}
			for (int i = 0; i < Qualities.Length - 1; i++)
			{
				if (Qualities[i].EnchantmentId == enchantmentId)
				{
					return 0;
				}
			}
			if (enchantmentId >= 87224 && enchantmentId < 87239)
			{
				return (enchantmentId - 87224) % 5 + 1;
			}
			return null;
		}

		public static int PickFlatType(IList<int> presentTypes, double roll)
		{
			if (presentTypes == null || presentTypes.Count == 0)
			{
				return 0;
			}
			List<int> list = new List<int>(presentTypes.Count);
			foreach (int presentType in presentTypes)
			{
				if (presentType >= 0 && presentType <= 5)
				{
					list.Add(presentType);
				}
			}
			if (list.Count == 0)
			{
				return 0;
			}
			if (list.Count == 1)
			{
				return list[0];
			}
			int num = (int)(roll * (double)list.Count);
			if (num < 0)
			{
				num = 0;
			}
			if (num >= list.Count)
			{
				num = list.Count - 1;
			}
			return list[num];
		}

		public static bool IsFletchEnchantment(int enchantmentId)
		{
			return QualityForEnchantmentId(enchantmentId) != null;
		}

		public static bool CanUpgradeFletch(int? existingPercent, int newPercent)
		{
			if (existingPercent.HasValue)
			{
				return newPercent > existingPercent.Value;
			}
			return true;
		}

		public static string DisplayNameFor(string baseName, int percent)
		{
			if (percent > 0)
			{
				return $"{baseName} (+{ClampPercent(percent)}%)";
			}
			return baseName;
		}

		public static Dictionary<string, FletchEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "arrow", "an object of arrow → fletch objects", ParseEntry);
		}

		private static FletchEntry ParseEntry(string key, object value, Action<string> warn)
		{
			FieldReader fieldReader = new FieldReader("'" + key + "'", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				fieldReader.Say("value must be an object — arrow skipped.");
				return null;
			}
			FletchEntry fletchEntry = new FletchEntry
			{
				Key = key
			};
			ItemKey val = default(ItemKey);
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				if (item.Key.ToLowerInvariant() == "arrow")
				{
					if (ItemKey.TryRead(item.Value, ref val))
					{
						fletchEntry.ArrowKey = ((ItemKey)(ref val)).Key;
						fletchEntry.ArrowId = ((ItemKey)(ref val)).ItemId;
					}
					else
					{
						fieldReader.Wrong("arrow", "a number (ItemID) or non-empty string (display name)");
					}
				}
				else
				{
					fieldReader.Unknown(item.Key, "arrow");
				}
			}
			if (fletchEntry.ArrowKey == null)
			{
				fieldReader.Say("missing required 'arrow' — arrow skipped.");
				return null;
			}
			return fletchEntry;
		}

		public static Dictionary<string, FletchEntry> Merge(Dictionary<string, FletchEntry> builtIn, Dictionary<string, FletchEntry> overrides)
		{
			return SpeciesTable.Merge<FletchEntry>(builtIn, overrides);
		}

		public static int ClampPercent(int percent)
		{
			if (percent >= 0)
			{
				if (percent <= 100)
				{
					return percent;
				}
				return 100;
			}
			return 0;
		}

		public static string EnchantmentSetKey(IEnumerable<int> enchantmentIds)
		{
			if (enchantmentIds == null)
			{
				return string.Empty;
			}
			List<int> list = new List<int>(enchantmentIds);
			if (list.Count == 0)
			{
				return string.Empty;
			}
			list.Sort();
			StringBuilder stringBuilder = new StringBuilder();
			for (int i = 0; i < list.Count; i++)
			{
				if (i > 0)
				{
					stringBuilder.Append(';');
				}
				stringBuilder.Append(list[i].ToString(CultureInfo.InvariantCulture));
			}
			return stringBuilder.ToString();
		}

		public static bool ShouldMergeEnchantedStack(string incomingKey, IEnumerable<StackSlot> sameIdStacks, out bool anyDifferentPartial, out int sameKeyCapacity)
		{
			anyDifferentPartial = false;
			sameKeyCapacity = 0;
			incomingKey = incomingKey ?? string.Empty;
			bool result = false;
			if (sameIdStacks != null)
			{
				foreach (StackSlot sameIdStack in sameIdStacks)
				{
					if (sameIdStack.FreeSpace > 0)
					{
						if (string.Equals(sameIdStack.Key ?? string.Empty, incomingKey, StringComparison.Ordinal))
						{
							result = true;
							sameKeyCapacity += sameIdStack.FreeSpace;
						}
						else
						{
							anyDifferentPartial = true;
						}
					}
				}
			}
			return result;
		}
	}
	public static class FeedLadder
	{
		public const string Relic = "relic";

		public const string BuffFood = "bufffood";

		public const string Meal = "meal";

		public static readonly IReadOnlyList<string> Order = new string[3] { "relic", "bufffood", "meal" };

		public static string Describe(IReadOnlyList<string> order = null)
		{
			IReadOnlyList<string> readOnlyList = order ?? Order;
			List<string> list = new List<string>(readOnlyList.Count);
			for (int i = 0; i < readOnlyList.Count; i++)
			{
				list.Add($"{i + 1} {readOnlyList[i]}");
			}
			return string.Join(" -> ", list.ToArray());
		}

		public static string Mismatch(IReadOnlyList<string> registered)
		{
			if (registered == null)
			{
				return "the feed registry is null";
			}
			if (registered.Count == Order.Count)
			{
				bool flag = true;
				for (int i = 0; i < Order.Count; i++)
				{
					if (!string.Equals(registered[i], Order[i], StringComparison.Ordinal))
					{
						flag = false;
						break;
					}
				}
				if (flag)
				{
					return null;
				}
			}
			return "registered [" + Describe(registered) + "] but the declared ladder is [" + Describe() + "]";
		}
	}
	public readonly struct DualListedFood
	{
		public string Species { get; }

		public string BuffKey { get; }

		public string DietKey { get; }

		public bool ByCategory { get; }

		public DualListedFood(string species, string buffKey, string dietKey, bool byCategory)
		{
			Species = species;
			BuffKey = buffKey;
			DietKey = dietKey;
			ByCategory = byCategory;
		}
	}
	public readonly struct FeedPlan
	{
		public bool FeedsAsMeal { get; }

		public bool AppliesBuff { get; }

		public bool IsMealPlusBuff
		{
			get
			{
				if (FeedsAsMeal)
				{
					return AppliesBuff;
				}
				return false;
			}
		}

		public int ConsumesOnAccept
		{
			get
			{
				if (!FeedsAsMeal && !AppliesBuff)
				{
					return 0;
				}
				return 1;
			}
		}

		public FeedPlan(bool feedsAsMeal, bool appliesBuff)
		{
			FeedsAsMeal = feedsAsMeal;
			AppliesBuff = appliesBuff;
		}
	}
	public static class FeedPrecedence
	{
		public static FoodEntry MatchingDietRow(IReadOnlyList<FoodEntry> diet, int itemId, string itemName, IReadOnlyCollection<string> itemCategories)
		{
			if (diet == null)
			{
				return null;
			}
			foreach (FoodEntry item in diet)
			{
				if (item != null && PetDiet.Matches(item, itemId, itemName, itemCategories))
				{
					return item;
				}
			}
			return null;
		}

		public static bool IsMeal(IReadOnlyList<FoodEntry> diet, int itemId, string itemName, IReadOnlyCollection<string> itemCategories)
		{
			return MatchingDietRow(diet, itemId, itemName, itemCategories) != null;
		}

		public static FeedPlan Plan(bool hasBuffRow, FoodEntry dietRow)
		{
			return new FeedPlan(dietRow != null, hasBuffRow);
		}

		public static bool BuffFoodApplies(bool hasBuffRow, FoodEntry dietRow)
		{
			return hasBuffRow;
		}

		public static List<DualListedFood> DualListed(BuffFoodEntry entry, IReadOnlyList<FoodEntry> diet, Func<BuffFoodDef, IReadOnlyCollection<string>> categoriesOf)
		{
			List<DualListedFood> list = new List<DualListedFood>();
			if (entry?.Foods == null || diet == null)
			{
				return list;
			}
			foreach (BuffFoodDef food in entry.Foods)
			{
				if (food != null)
				{
					IReadOnlyCollection<string> itemCategories = categoriesOf?.Invoke(food);
					FoodEntry foodEntry = MatchingDietRow(diet, food.ItemId.GetValueOrDefault(), food.Key, itemCategories);
					if (foodEntry != null)
					{
						list.Add(new DualListedFood(entry.Species, food.Key, foodEntry.Key, foodEntry.Category != null));
					}
				}
			}
			return list;
		}
	}
	public readonly struct FieldReader
	{
		private readonly string _context;

		private readonly Action<string> _warn;

		public string Context => _context;

		public Action<string> Sink => _warn;

		public FieldReader(string context, Action<string> warn)
		{
			_context = context;
			_warn = warn;
		}

		public void Say(string message)
		{
			if (_warn != null)
			{
				_warn(string.IsNullOrEmpty(_context) ? message : (_context + ": " + message));
			}
		}

		public void Unknown(string key, string valid)
		{
			Say("unknown key '" + key + "' (valid: " + valid + ").");
		}

		public void Wrong(string field, string expected, string fallback = null)
		{
			Say("'" + field + "' must be " + expected + (string.IsNullOrEmpty(fallback) ? "" : (" — " + fallback)) + ".");
		}

		public bool Num(string field, object value, out double result, string expected = null, string fallback = null)
		{
			if (value is double num)
			{
				result = num;
				return true;
			}
			result = 0.0;
			Wrong(field, expected ?? "a number", fallback);
			return false;
		}

		public bool Int(string field, object value, out int result, string expected = null, string fallback = null)
		{
			result = 0;
			if (!Num(field, value, out var result2, expected, fallback))
			{
				return false;
			}
			result = (int)result2;
			return true;
		}

		public bool Str(string field, object value, out string result, string expected = null, string fallback = null)
		{
			if (value is string text && text.Trim().Length > 0)
			{
				result = text.Trim();
				return true;
			}
			result = null;
			Wrong(field, expected ?? "a non-empty string", fallback);
			return false;
		}

		public bool Bool(string field, object value, out bool result, string expected = null, string fallback = null)
		{
			if (value is bool flag)
			{
				result = flag;
				return true;
			}
			result = false;
			Wrong(field, expected ?? "true or false", fallback);
			return false;
		}

		public bool Items(string field, object value, out List<object> result, string expected = null, string fallback = null)
		{
			if (value is List<object> list)
			{
				result = list;
				return true;
			}
			result = null;
			Wrong(field, expected ?? "an ARRAY", fallback);
			return false;
		}

		public bool Obj(string field, object value, out Dictionary<string, object> result, string expected = null, string fallback = null)
		{
			if (value is Dictionary<string, object> dictionary)
			{
				result = dictionary;
				return true;
			}
			result = null;
			Wrong(field, expected ?? "an object", fallback);
			return false;
		}

		public bool Enum<T>(string field, object value, out T result, string expected = null, string fallback = null) where T : struct
		{
			if (value is string text && EnumRead.TryName<T>(text.Trim(), out result))
			{
				return true;
			}
			result = default(T);
			Wrong(field, expected ?? ("one of " + string.Join(", ", System.Enum.GetNames(typeof(T)))), fallback);
			return false;
		}
	}
	public static class FoodCategories
	{
		public const string Meat = "Meat";

		public const string Fish = "Fish";

		public const string Vegetable = "Vegetable";

		public const string Egg = "Egg";

		public const string Bread = "Bread";

		public const string Mushroom = "Mushroom";

		public const string RationIngredient = "RationIngredient";

		public const string Water = "Water";

		public static readonly string[] All = new string[8] { "Meat", "Fish", "Vegetable", "Egg", "Bread", "Mushroom", "RationIngredient", "Water" };

		public static bool TryCanonical(string raw, out string canonical)
		{
			canonical = null;
			if (string.IsNullOrEmpty(raw))
			{
				return false;
			}
			string text = Squash(raw);
			if (text.Length == 0)
			{
				return false;
			}
			string[] all = All;
			foreach (string text2 in all)
			{
				if (string.Equals(text, Squash(text2), StringComparison.OrdinalIgnoreCase))
				{
					canonical = text2;
					return true;
				}
			}
			return false;
		}

		public static string Label(string category)
		{
			return "any " + Spaced(category);
		}

		public static string Spaced(string category)
		{
			if (string.IsNullOrEmpty(category))
			{
				return category;
			}
			StringBuilder stringBuilder = new StringBuilder(category.Length + 4);
			for (int i = 0; i < category.Length; i++)
			{
				if (i > 0 && char.IsUpper(category[i]) && !char.IsUpper(category[i - 1]))
				{
					stringBuilder.Append(' ');
				}
				stringBuilder.Append(category[i]);
			}
			return stringBuilder.ToString();
		}

		public static string ValidList()
		{
			return string.Join(", ", All);
		}

		private static string Squash(string s)
		{
			StringBuilder stringBuilder = new StringBuilder(s.Length);
			foreach (char c in s)
			{
				if (c != ' ' && c != '-' && c != '_')
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}
	}
	public sealed class FoodHexMeal
	{
		public string Key;

		public int? ItemId;

		public string HexId;
	}
	public sealed class FoodHexEntry
	{
		public string Species;

		public double? BuildUpPercent;

		public int? Window;

		public List<FoodHexMeal> Meals = new List<FoodHexMeal>();
	}
	public struct HexBuildUp
	{
		public string HexId;

		public float Percent;

		public int MealCount;
	}
	public static class FoodHexes
	{
		public const int MaxWindow = 20;

		public const int DefaultWindow = 3;

		public const float DefaultBuildUpPercent = 20f;

		public static Dictionary<string, FoodHexEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "species", "an object of species → food-hex objects", ParseEntry);
		}

		private static FoodHexEntry ParseEntry(string species, object value, Action<string> warn)
		{
			FieldReader f = new FieldReader("'" + species + "'", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("value must be an object — species skipped.");
				return null;
			}
			FoodHexEntry foodHexEntry = new FoodHexEntry
			{
				Species = species
			};
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "builduppercent":
				{
					if (f.Num("buildUpPercent", item.Value, out var result2, "a number > 0", "using the config default"))
					{
						if (result2 > 0.0)
						{
							foodHexEntry.BuildUpPercent = result2;
						}
						else
						{
							f.Wrong("buildUpPercent", "a number > 0", "using the config default");
						}
					}
					break;
				}
				case "window":
				{
					if (!f.Num("window", item.Value, out var result3, $"a number 1..{20}", "using the config default"))
					{
						break;
					}
					if (result3 >= 1.0)
					{
						foodHexEntry.Window = (int)result3;
						if (foodHexEntry.Window > 20)
						{
							f.Say($"'window' {foodHexEntry.Window} exceeds the hard max {20} — clamped.");
							foodHexEntry.Window = 20;
						}
					}
					else
					{
						f.Wrong("window", $"a number 1..{20}", "using the config default");
					}
					break;
				}
				case "meals":
				{
					if (!f.Obj("meals", item.Value, out Dictionary<string, object> result, "an OBJECT of food-key → hex-name pairs"))
					{
						break;
					}
					foreach (KeyValuePair<string, object> item2 in result)
					{
						FoodHexMeal foodHexMeal = ParseMeal(f, item2.Key, item2.Value);
						if (foodHexMeal != null)
						{
							foodHexEntry.Meals.Add(foodHexMeal);
						}
					}
					break;
				}
				default:
					f.Unknown(item.Key, "buildUpPercent, window, meals");
					break;
				}
			}
			if (foodHexEntry.Meals.Count == 0)
			{
				f.Say("missing/empty required 'meals' — species skipped.");
				return null;
			}
			return foodHexEntry;
		}

		private static FoodHexMeal ParseMeal(FieldReader f, string key, object value)
		{
			string text = (key ?? "").Trim();
			if (text.Length == 0)
			{
				f.Say("empty meal key — entry skipped.");
				return null;
			}
			if (text.IndexOf('|') >= 0 || text.IndexOf('\t') >= 0 || text.IndexOf('\n') >= 0 || text.IndexOf('\r') >= 0)
			{
				f.Say("meal key '" + text + "' contains a reserved character ('|', tab, or newline) — entry skipped.");
				return null;
			}
			if (!(value is string text2) || text2.Trim().Length == 0)
			{
				f.Say("meal '" + text + "' must map to a non-empty status-effect name string — entry skipped.");
				return null;
			}
			ItemKey val = default(ItemKey);
			ItemKey.TryRead((object)text, ref val);
			return new FoodHexMeal
			{
				Key = ((ItemKey)(ref val)).Key,
				HexId = text2.Trim(),
				ItemId = ((ItemKey)(ref val)).ItemId
			};
		}

		public static Dictionary<string, FoodHexEntry> Merge(Dictionary<string, FoodHexEntry> builtIn, Dictionary<string, FoodHexEntry> overrides)
		{
			return SpeciesTable.Merge<FoodHexEntry>(builtIn, overrides);
		}

		public static FoodHexEntry Resolve(Dictionary<string, FoodHexEntry> table, string speciesId)
		{
			FoodHexEntry result = default(FoodHexEntry);
			if (!SpeciesTable.TryResolve<FoodHexEntry>(table, speciesId, ref result, (string)null))
			{
				return null;
			}
			return result;
		}

		public static string MatchMeal(FoodHexEntry entry, int itemId, string itemName)
		{
			//IL_0027: 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)
			if (entry == null)
			{
				return null;
			}
			foreach (FoodHexMeal meal in entry.Meals)
			{
				ItemKey val = new ItemKey(meal.Key, meal.ItemId);
				if (((ItemKey)(ref val)).Matches(itemId, itemName))
				{
					return meal.Key;
				}
			}
			return null;
		}

		public static int ClampWindow(int window)
		{
			if (window >= 1)
			{
				if (window <= 20)
				{
					return window;
				}
				return 20;
			}
			return 1;
		}

		public static void RecordMeal(List<string> history, string mealKey)
		{
			if (history != null && !string.IsNullOrEmpty(mealKey))
			{
				history.Add(mealKey);
				while (history.Count > 20)
				{
					history.RemoveAt(0);
				}
			}
		}

		public static List<HexBuildUp> ComputeBuildups(FoodHexEntry entry, IReadOnlyList<string> history, int configWindow, float configPercent)
		{
			List<HexBuildUp> list = new List<HexBuildUp>();
			if (entry == null || history == null || history.Count == 0)
			{
				return list;
			}
			int num = ClampWindow(entry.Window ?? configWindow);
			float num2 = (float)(entry.BuildUpPercent ?? ((double)configPercent));
			if (num2 <= 0f)
			{
				return list;
			}
			List<string> list2 = new List<string>();
			Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
			int num3 = 0;
			int num4 = history.Count - 1;
			while (num4 >= 0 && num3 < num)
			{
				string text = HexFor(entry, history[num4]);
				if (text != null)
				{
					num3++;
					if (dictionary.TryGetValue(text, out var value))
					{
						dictionary[text] = value + 1;
					}
					else
					{
						dictionary[text] = 1;
						list2.Add(text);
					}
				}
				num4--;
			}
			foreach (string item in list2)
			{
				list.Add(new HexBuildUp
				{
					HexId = item,
					Percent = (float)dictionary[item] * num2,
					MealCount = dictionary[item]
				});
			}
			return list;
		}

		private static string HexFor(FoodHexEntry entry, string mealKey)
		{
			if (string.IsNullOrEmpty(mealKey))
			{
				return null;
			}
			foreach (FoodHexMeal meal in entry.Meals)
			{
				if (string.Equals(meal.Key, mealKey, StringComparison.OrdinalIgnoreCase))
				{
					return meal.HexId;
				}
			}
			return null;
		}
	}
	public enum KillFavorStat
	{
		StaminaCostReduction,
		ManaCostReduction,
		Protection
	}
	public sealed class KillFavorDef
	{
		public KillFavorStat Stat;

		public float[] AmountByTier;
	}
	public sealed class ForTheKillEntry
	{
		public string Species;

		public string StatusName;

		public double? BuildupPercent;

		public KillFavorDef KillBuff;
	}
	public static class KillFavor
	{
		public const string StatusIdentifier = "BW_KillFavor";

		public const int StatusNumId = 87062;

		public static float Amount(KillFavorDef def, LoyaltyTier tier)
		{
			if (def?.AmountByTier == null || tier == LoyaltyTier.Gone)
			{
				return 0f;
			}
			int num = (int)(tier - 1);
			if (num < 0 || num >= def.AmountByTier.Length)
			{
				return 0f;
			}
			return def.AmountByTier[num];
		}
	}
	public static class ForTheKill
	{
		public const double MinBuildup = 1.0;

		public const double MaxBuildup = 100.0;

		public static Dictionary<string, ForTheKillEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "species", "an object of species → forTheKill objects", ParseEntry);
		}

		private static ForTheKillEntry ParseEntry(string speciesKey, object value, Action<string> warn)
		{
			FieldReader f = new FieldReader("'" + speciesKey + "'", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("value must be an object — species skipped.");
				return null;
			}
			ForTheKillEntry forTheKillEntry = new ForTheKillEntry
			{
				Species = speciesKey
			};
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "status":
				{
					if (f.Str("status", item.Value, out string result2, "a non-empty status name string"))
					{
						forTheKillEntry.StatusName = result2;
					}
					break;
				}
				case "buildup":
				{
					if (f.Num("buildup", item.Value, out var result, null, "ignored (direct apply)"))
					{
						if (result < 1.0)
						{
							f.Say($"'buildup' {result} below {1.0} — clamped.");
							result = 1.0;
						}
						if (result > 100.0)
						{
							f.Say($"'buildup' {result} above {100.0} — clamped.");
							result = 100.0;
						}
						forTheKillEntry.BuildupPercent = result;
					}
					break;
				}
				case "killbuff":
					forTheKillEntry.KillBuff = ParseKillBuff(f, item.Value);
					break;
				default:
					f.Unknown(item.Key, "status, buildup, killBuff");
					break;
				}
			}
			if (string.IsNullOrEmpty(forTheKillEntry.StatusName))
			{
				f.Say("missing required 'status' — species skipped.");
				return null;
			}
			return forTheKillEntry;
		}

		private static KillFavorDef ParseKillBuff(FieldReader f, object value)
		{
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Wrong("killBuff", "an object", "ignored (strike only)");
				return null;
			}
			KillFavorStat? killFavorStat = null;
			float[] array = null;
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				string text = item.Key.ToLowerInvariant();
				KillFavorStat value2;
				if (!(text == "stat"))
				{
					if (text == "amounts")
					{
						if (item.Value is List<object> { Count: 4 } list)
						{
							array = new float[4];
							for (int i = 0; i < 4; i++)
							{
								array[i] = ((list[i] is double num) ? ((float)num) : 0f);
							}
						}
						else
						{
							f.Say("killBuff 'amounts' must be a 4-number array (Broken, Fraying, Steady, Devoted) — ignored.");
						}
					}
					else
					{
						f.Say("unknown killBuff key '" + item.Key + "' (valid: stat, amounts).");
					}
				}
				else if (item.Value is string text2 && EnumRead.TryName<KillFavorStat>(text2.Trim(), out value2))
				{
					killFavorStat = value2;
				}
				else
				{
					f.Say($"killBuff 'stat' '{item.Value}' unknown " + "(valid: " + string.Join(", ", Enum.GetNames(typeof(KillFavorStat))) + ").");
				}
			}
			if (!killFavorStat.HasValue || array == null)
			{
				f.Say("killBuff needs both 'stat' and a 4-number 'amounts' — ignored (strike only).");
				return null;
			}
			return new KillFavorDef
			{
				Stat = killFavorStat.Value,
				AmountByTier = array
			};
		}

		public static Dictionary<string, ForTheKillEntry> Merge(Dictionary<string, ForTheKillEntry> builtIn, Dictionary<string, ForTheKillEntry> overrides)
		{
			return SpeciesTable.Merge<ForTheKillEntry>(builtIn, overrides);
		}

		public static ForTheKillEntry Resolve(Dictionary<string, ForTheKillEntry> table, string speciesId)
		{
			ForTheKillEntry result = default(ForTheKillEntry);
			if (!SpeciesTable.TryResolve<ForTheKillEntry>(table, speciesId, ref result, (string)null))
			{
				return null;
			}
			return result;
		}
	}
	public static class HealthRecovery
	{
		public const int MinLevel = 1;

		public const int MaxLevel = 5;

		public const int DefaultLevel = 1;

		public const double DurationSeconds = 600.0;

		private static readonly double[] Rates = new double[5] { 0.2, 0.25, 0.3, 0.4, 0.5 };

		public static double RatePerSecond(int level)
		{
			if (level > 0)
			{
				return Rates[((level > 5) ? 5 : level) - 1];
			}
			return 0.0;
		}

		public static int ClampLevel(int level)
		{
			if (level > 0)
			{
				if (level >= 1)
				{
					if (level <= 5)
					{
						return level;
					}
					return 5;
				}
				return 1;
			}
			return 0;
		}

		public static int EffectiveLevel(int? foodLevel, bool isTamingFood)
		{
			if (!isTamingFood)
			{
				return ClampLevel(foodLevel ?? 1);
			}
			return 5;
		}

		public static double TickHeal(int level, double dt)
		{
			if (!(dt > 0.0))
			{
				return 0.0;
			}
			return RatePerSecond(level) * dt;
		}

		public static double TotalHeal(int level)
		{
			return RatePerSecond(level) * 600.0;
		}

		public static string Describe(int level, double secondsLeft)
		{
			return string.Format(CultureInfo.InvariantCulture, "level {0} (+{1} HP/s, {2}s left)", level, RatePerSecond(level), Math.Round(secondsLeft));
		}
	}
	public static class HuntCooldown
	{
		public const float DivergenceEpsilon = 0.25f;

		public static float WantSkillCooldown(bool syncEnabled, bool hasSpeciesRow, float speciesCooldownSeconds, float baseCooldownSeconds)
		{
			float result = AtLeastZero(baseCooldownSeconds);
			if (!syncEnabled || !hasSpeciesRow)
			{
				return result;
			}
			return AtLeastZero(speciesCooldownSeconds);
		}

		public static float AdoptedCooldownSeconds(float committedRealCooldown, float tableCooldownSeconds)
		{
			if (!(committedRealCooldown > 0f))
			{
				return AtLeastZero(tableCooldownSeconds);
			}
			return committedRealCooldown;
		}

		public static bool Diverged(float skillRemainingSeconds, float petRemainingSeconds, float epsilon = 0.25f)
		{
			return Math.Abs(skillRemainingSeconds - petRemainingSeconds) > AtLeastZero(epsilon);
		}

		public static double Drain(double secondsLeft, double dt)
		{
			if (secondsLeft <= 0.0)
			{
				return 0.0;
			}
			if (dt <= 0.0)
			{
				return secondsLeft;
			}
			double num = secondsLeft - dt;
			if (!(num > 0.0))
			{
				return 0.0;
			}
			return num;
		}

		public static double EffectiveRemainder(bool syncEnabled, double savedSecondsLeft)
		{
			if (!syncEnabled || !(savedSecondsLeft > 0.0))
			{
				return 0.0;
			}
			return savedSecondsLeft;
		}

		private static float AtLeastZero(float v)
		{
			if (!(v > 0f))
			{
				return 0f;
			}
			return v;
		}
	}
	public static class JsonTable
	{
		public static string DuplicateMessage(string key, string keyNoun)
		{
			return "'" + key + "': duplicate " + keyNoun + " key — the later entry wins.";
		}

		public static string RootMessage(string rootShape)
		{
			return "root must be " + rootShape + ".";
		}

		public static Dictionary<string, T> Read<T>(string json, Action<string> warn, string keyNoun, string rootShape, Func<string, object, Action<string>, T> entryFn) where T : class
		{
			Dictionary<string, T> dictionary = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
			if (!TryReadRoot(json, warn, keyNoun, rootShape, out Dictionary<string, object> root))
			{
				return dictionary;
			}
			ReadInto(root, dictionary, warn, keyNoun, null, entryFn);
			return dictionary;
		}

		public static bool TryReadRoot(string json, Action<string> warn, string keyNoun, string rootShape, out Dictionary<string, object> root)
		{
			root = null;
			if (string.IsNullOrEmpty(json) || json.Trim().Length == 0)
			{
				return false;
			}
			object obj;
			try
			{
				obj = Json.Parse(json, (Action<string>)delegate(string k)
				{
					warn?.Invoke(DuplicateMessage(k, keyNoun));
				});
			}
			catch (FormatException ex)
			{
				warn?.Invoke(ex.Message);
				return false;
			}
			if (!(obj is Dictionary<string, object> dictionary))
			{
				warn?.Invoke(RootMessage(rootShape));
				return false;
			}
			root = dictionary;
			return true;
		}

		public static void ReadInto<T>(Dictionary<string, object> source, Dictionary<string, T> dest, Action<string> warn, string keyNoun, string ctxPrefix, Func<string, object, Action<string>, T> entryFn) where T : class
		{
			if (source == null || dest == null || entryFn == null)
			{
				return;
			}
			foreach (KeyValuePair<string, object> item in source)
			{
				T val = entryFn(item.Key, item.Value, warn);
				if (val != null)
				{
					if (dest.ContainsKey(item.Key))
					{
						warn?.Invoke(DuplicateMessage(string.IsNullOrEmpty(ctxPrefix) ? item.Key : (ctxPrefix + "." + item.Key), keyNoun));
					}
					dest[item.Key] = val;
				}
			}
		}
	}
	public enum LanternAction
	{
		None,
		Create,
		Destroy
	}
	public static class LanternRules
	{
		public static LanternAction Mirror(bool playerHas, bool bodyAlive, bool cloneAlive)
		{
			if (playerHas && bodyAlive && !cloneAlive)
			{
				return LanternAction.Create;
			}
			if (cloneAlive && (!playerHas || !bodyAlive))
			{
				return LanternAction.Destroy;
			}
			return LanternAction.None;
		}
	}
	public enum LoyaltyTier
	{
		Gone,
		Broken,
		Fraying,
		Steady,
		Devoted
	}
	public enum LoyaltyEvent
	{
		FeedPreferred,
		FeedBondFood,
		DayWithoutFeeding,
		PetCriticallyHurt,
		PetDowned,
		DefeatedNearbyEnemy,
		CrossedRegionBonded
	}
	public static class Loyalty
	{
		public const int Min = 0;

		public const int Max = 100;

		public const double DefaultGainScale = 0.05;

		public static LoyaltyTier Tier(int value)
		{
			if (value <= 39)
			{
				if (value > 0)
				{
					if (value <= 14)
					{
						return LoyaltyTier.Broken;
					}
					return LoyaltyTier.Fraying;
				}
				return LoyaltyTier.Gone;
			}
			if (value <= 74)
			{
				return LoyaltyTier.Steady;
			}
			return LoyaltyTier.Devoted;
		}

		public static int DeltaFor(LoyaltyEvent ev, int speciesDailyDecay = 15)
		{
			return ev switch
			{
				LoyaltyEvent.FeedPreferred => 10, 
				LoyaltyEvent.FeedBondFood => 20, 
				LoyaltyEvent.DayWithoutFeeding => -speciesDailyDecay, 
				LoyaltyEvent.PetCriticallyHurt => -10, 
				LoyaltyEvent.PetDowned => -20, 
				LoyaltyEvent.DefeatedNearbyEnemy => 5, 
				LoyaltyEvent.CrossedRegionBonded => 5, 
				_ => 0, 
			};
		}

		public static int Apply(int value, LoyaltyEvent ev, int speciesDailyDecay = 15)
		{
			return Clamp(value + DeltaFor(ev, speciesDailyDecay));
		}

		public static int ApplyDelta(int value, int delta)
		{
			return Clamp(value + delta);
		}

		public static bool NextTier(int value, out LoyaltyTier next, out int atValue)
		{
			switch (Tier(value))
			{
			case LoyaltyTier.Gone:
				next = LoyaltyTier.Broken;
				atValue = 1;
				return true;
			case LoyaltyTier.Broken:
				next = LoyaltyTier.Fraying;
				atValue = 15;
				return true;
			case LoyaltyTier.Fraying:
				next = LoyaltyTier.Steady;
				atValue = 40;
				return true;
			case LoyaltyTier.Steady:
				next = LoyaltyTier.Devoted;
				atValue = 75;
				return true;
			default:
				next = LoyaltyTier.Devoted;
				atValue = 100;
				return false;
			}
		}

		public static int Clamp(int value)
		{
			if (value >= 0)
			{
				if (value <= 100)
				{
					return value;
				}
				return 100;
			}
			return 0;
		}

		public static int BankGain(double scaledGain, ref double carry)
		{
			if (carry < 0.0 || double.IsNaN(carry) || double.IsInfinity(carry))
			{
				carry = 0.0;
			}
			if (!(scaledGain > 0.0) || double.IsNaN(scaledGain) || double.IsInfinity(scaledGain))
			{
				return 0;
			}
			double num = scaledGain + carry;
			if (num >= 100.0)
			{
				carry = 0.0;
				return 100;
			}
			int num2 = (int)Math.Floor(num);
			carry = num - (double)num2;
			return num2;
		}
	}
	public enum PetBuffStat
	{
		PhysicalDamage,
		EtherealDamage,
		DecayDamage,
		ElectricDamage,
		FrostDamage,
		FireDamage,
		MovementSpeed,
		BagCapacity
	}
	public sealed class PetBuffDef
	{
		public PetBuffStat Stat;

		public float AmountPerLevel;

		public float MaxAmount;
	}
	public readonly struct ResolvedBuff
	{
		public PetBuffStat Stat { get; }

		public float Amount { get; }

		public ResolvedBuff(PetBuffStat stat, float amount)
		{
			Stat = stat;
			Amount = amount;
		}
	}
	public static class PetBuffs
	{
		public static int Level(LoyaltyTier tier)
		{
			return (int)tier;
		}

		public static bool IsFlat(PetBuffStat stat)
		{
			return stat == PetBuffStat.BagCapacity;
		}

		public static string DisplayName(PetBuffStat stat)
		{
			return stat switch
			{
				PetBuffStat.PhysicalDamage => "Physical damage", 
				PetBuffStat.EtherealDamage => "Ethereal damage", 
				PetBuffStat.DecayDamage => "Decay damage", 
				PetBuffStat.ElectricDamage => "Electric damage", 
				PetBuffStat.FrostDamage => "Frost damage", 
				PetBuffStat.FireDamage => "Fire damage", 
				PetBuffStat.MovementSpeed => "movement speed", 
				PetBuffStat.BagCapacity => "bag capacity", 
				_ => stat.ToString(), 
			};
		}

		public static float Amount(PetBuffDef def, LoyaltyTier tier)
		{
			if (def == null)
			{
				return 0f;
			}
			int num = Level(tier);
			if (IsFlat(def.Stat))
			{
				if (num <= 0)
				{
					return 0f;
				}
				float amountPerLevel = def.AmountPerLevel;
				float num2 = ((float.IsPositiveInfinity(def.MaxAmount) || def.MaxAmount < amountPerLevel) ? amountPerLevel : def.MaxAmount);
				if (num < Level(LoyaltyTier.Devoted))
				{
					return amountPerLevel + (float)(num - 1) * ((num2 - amountPerLevel) / 4f);
				}
				return num2;
			}
			float num3 = def.AmountPerLevel * (float)num;
			if (!(num3 > def.MaxAmount))
			{
				return num3;
			}
			return def.MaxAmount;
		}

		public static List<ResolvedBuff> Resolve(Dictionary<string, List<PetBuffDef>> table, string speciesId, LoyaltyTier tier)
		{
			List<ResolvedBuff> list = new List<ResolvedBuff>();
			if (!TryGet(table, speciesId, out List<PetBuffDef> defs))
			{
				return list;
			}
			foreach (PetBuffDef item in defs)
			{
				float num = Amount(item, tier);
				if (num > 0f)
				{
					list.Add(new ResolvedBuff(item.Stat, num));
				}
			}
			return list;
		}

		public static float BagCapacityTotal(float speciesAmount, bool speciesEnabled, float beastOfBurdenAmount, bool beastOfBurdenLearned)
		{
			float num = 0f;
			if (speciesEnabled && speciesAmount > 0f)
			{
				num += speciesAmount;
			}
			if (beastOfBurdenLearned && beastOfBurdenAmount > 0f)
			{
				num += beastOfBurdenAmount;
			}
			return num;
		}

		public static Dictionary<string, List<PetBuffDef>> Parse(string text, Action<string> warn = null)
		{
			//IL_001f: 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_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: 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_00d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0106: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			Dictionary<string, List<PetBuffDef>> dictionary = new Dictionary<string, List<PetBuffDef>>(StringComparer.OrdinalIgnoreCase);
			foreach (TableLine item2 in SpeciesTable.Lines(text, '\n'))
			{
				string text2 = item2.Fields[0].Trim();
				if (text2.Length == 0)
				{
					continue;
				}
				if (!EnumRead.TryName<PetBuffStat>(text2, out var value))
				{
					warn?.Invoke("'" + item2.Key + "': unknown buff stat '" + text2 + "' (valid: " + string.Join(", ", Enum.GetNames(typeof(PetBuffStat))) + ").");
				}
				else
				{
					PetBuffDef item = new PetBuffDef
					{
						Stat = value,
						AmountPerLevel = SpeciesTable.FloatOr(item2.Fields, 1, 1f, warn, item2.Key),
						MaxAmount = SpeciesTable.FloatOr(item2.Fields, 2, float.PositiveInfinity, warn, item2.Key)
					};
					if (!dictionary.TryGetValue(item2.Key, out var value2))
					{
						value2 = (dictionary[item2.Key] = new List<PetBuffDef>());
					}
					value2.Add(item);
				}
			}
			return dictionary;
		}

		public static Dictionary<string, List<PetBuffDef>> Merge(Dictionary<string, List<PetBuffDef>> builtIn, Dictionary<string, List<PetBuffDef>> overrides)
		{
			return SpeciesTable.Merge<List<PetBuffDef>>(builtIn, overrides);
		}

		public static bool TryGet(Dictionary<string, List<PetBuffDef>> table, string speciesId, out List<PetBuffDef> defs)
		{
			return SpeciesTable.TryResolve<List<PetBuffDef>>(table, speciesId, ref defs, (string)null);
		}
	}
	public enum ComfortSide
	{
		None,
		Cold,
		Hot
	}
	public sealed class ComfortRelief
	{
		public ComfortSide Side;

		public int ReliefSteps;

		public double EscalateMult;

		public bool Immune;
	}
	public readonly struct ComfortReading
	{
		public int StepsOutside { get; }

		public ComfortSide Side { get; }

		public double EscalateMult { get; }

		public int RawStepsOutside { get; }

		public bool Immune { get; }

		public static ComfortReading InBand => new ComfortReading(0, ComfortSide.None, 1.0, 0);

		public ComfortReading(int stepsOutside, ComfortSide side, double escalateMult, int rawStepsOutside, bool immune = false)
		{
			StepsOutside = stepsOutside;
			Side = side;
			EscalateMult = escalateMult;
			RawStepsOutside = rawStepsOutside;
			Immune = immune;
		}
	}
	public static class PetComfort
	{
		public static ComfortSide SideOf(TempStep ambient, ComfortBand band)
		{
			if (ambient < band.Min)
			{
				return ComfortSide.Cold;
			}
			if (ambient > band.Max)
			{
				return ComfortSide.Hot;
			}
			return ComfortSide.None;
		}

		public static ComfortReading Evaluate(TempStep ambient, ComfortBand band, ComfortRelief relief = null)
		{
			return Evaluate(ambient, band, (relief == null) ? null : new ComfortRelief[1] { relief });
		}

		public static ComfortReading Evaluate(TempStep ambient, ComfortBand band, IReadOnlyList<ComfortRelief> reliefs)
		{
			int num = Temperature.StepsOutside(ambient, band);
			if (num <= 0)
			{
				return ComfortReading.InBand;
			}
			ComfortSide comfortSide = SideOf(ambient, band);
			if (reliefs != null)
			{
				for (int i = 0; i < reliefs.Count; i++)
				{
					if (reliefs[i] != null && reliefs[i].Immune)
					{
						return new ComfortReading(0, comfortSide, 1.0, num, immune: true);
					}
				}
			}
			int num2 = 0;
			double num3 = 1.0;
			if (reliefs != null)
			{
				for (int j = 0; j < reliefs.Count; j++)
				{
					ComfortRelief comfortRelief = reliefs[j];
					if (comfortRelief != null && comfortRelief.Side == comfortSide && comfortRelief.ReliefSteps > 0)
					{
						num2 += comfortRelief.ReliefSteps;
						if (comfortRelief.EscalateMult > num3)
						{
							num3 = comfortRelief.EscalateMult;
						}
					}
				}
			}
			int num4 = Math.Max(0, num - num2);
			double escalateMult = ((num4 > 0 && num2 > 0) ? num3 : 1.0);
			return new ComfortReading(num4, comfortSide, escalateMult, num);
		}
	}
	public static class SpeciesComfort
	{
		public const string DefaultKey = "Default";

		public static Dictionary<string, ComfortBand> Parse(string text, Action<string> warn = null)
		{
			//IL_001f: 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_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0122: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			Dictionary<string, ComfortBand> dictionary = new Dictionary<string, ComfortBand>(StringComparer.OrdinalIgnoreCase);
			foreach (TableLine item in SpeciesTable.Lines(text, '\n'))
			{
				string[] fields = item.Fields;
				TempStep step;
				TempStep step2;
				if (fields.Length < 2)
				{
					warn?.Invoke("'" + item.Raw + "': expected Species=MinStep,MaxStep.");
				}
				else if (!TryParseStep(fields[0], out step) || !TryParseStep(fields[1], out step2))
				{
					warn?.Invoke("'" + item.Key + "': unknown step name (valid: " + string.Join(", ", Enum.GetNames(typeof(TempStep))) + ").");
				}
				else if (step > step2)
				{
					warn?.Invoke("'" + item.Key + "': inverted band " + fields[0].Trim() + ">" + fields[1].Trim() + " — line skipped.");
				}
				else
				{
					dictionary[item.Key] = new ComfortBand(step, step2);
				}
			}
			return dictionary;
		}

		public static Dictionary<string, ComfortBand> Merge(Dictionary<string, ComfortBand> builtIn, Dictionary<string, ComfortBand> overrides)
		{
			return SpeciesTable.Merge<ComfortBand>(builtIn, overrides);
		}

		public static ComfortBand Resolve(Dictionary<string, ComfortBand> table, string speciesId, ComfortBand fallback)
		{
			ComfortBand result = default(ComfortBand);
			if (!SpeciesTable.TryResolve<ComfortBand>(table, speciesId, ref result, "Default"))
			{
				return fallback;
			}
			return result;
		}

		private static bool TryParseStep(string s, out TempStep step)
		{
			return EnumRead.TryName<TempStep>(s.Trim(), out step);
		}
	}
	public sealed class BlanketDef
	{
		public string Key = "";

		public ComfortSide Side;

		public int ReliefSteps;

		public double EscalateMult;

		public double DurationSeconds;

		public List<string> Ingredients = new List<string>();

		public ComfortRelief ToRelief()
		{
			return new ComfortRelief
			{
				Side = Side,
				ReliefSteps = ReliefSteps,
				EscalateMult = EscalateMult
			};
		}
	}
	public static class Blankets
	{
		public static Dictionary<string, BlanketDef> Parse(string text, Action<string> warn = null)
		{
			//IL_001f: 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_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			//IL_018a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0172: Unknown result type (might be due to invalid IL or missing references)
			Dictionary<string, BlanketDef> dictionary = new Dictionary<string, BlanketDef>(StringComparer.OrdinalIgnoreCase);
			foreach (TableLine item in SpeciesTable.Lines(text, '\n'))
			{
				string[] fields = item.Fields;
				if (fields.Length < 5)
				{
					warn?.Invoke("'" + item.Raw + "': expected Name=Side,ReliefSteps,EscalateMult,DurationSeconds,Ingredient….");
					continue;
				}
				string text2 = fields[0].Trim();
				if (!TryParseSide(text2, out var side) || side == ComfortSide.None)
				{
					warn?.Invoke("'" + item.Key + "': side must be Cold or Hot, got '" + text2 + "'.");
					continue;
				}
				BlanketDef blanketDef = new BlanketDef
				{
					Key = item.Key,
					Side = side,
					ReliefSteps = (int)SpeciesTable.DoubleOr(fields, 1, 3.0, warn, item.Key),
					EscalateMult = SpeciesTable.DoubleOr(fields, 2, 3.0, warn, item.Key),
					DurationSeconds = SpeciesTable.DoubleOr(fields, 3, 900.0, warn, item.Key)
				};
				for (int i = 4; i < fields.Length; i++)
				{
					string text3 = fields[i].Trim();
					if (text3.Length > 0)
					{
						blanketDef.Ingredients.Add(text3);
					}
				}
				if (blanketDef.Ingredients.Count == 0)
				{
					warn?.Invoke("'" + item.Key + "': no ingredients — line skipped (an uncraftable blanket helps no one).");
				}
				else
				{
					dictionary[item.Key] = blanketDef;
				}
			}
			return dictionary;
		}

		public static Dictionary<string, BlanketDef> Merge(Dictionary<string, BlanketDef> builtIn, Dictionary<string, BlanketDef> overrides)
		{
			return SpeciesTable.Merge<BlanketDef>(builtIn, overrides);
		}

		private static bool TryParseSide(string s, out ComfortSide side)
		{
			return EnumRead.TryName<ComfortSide>(s, out side);
		}
	}
	public enum FoodKind
	{
		None,
		Preferred,
		Bond
	}
	public readonly struct FoodMatch
	{
		public static readonly FoodMatch None = new FoodMatch(FoodKind.None, 0);

		public FoodKind Kind { get; }

		public int LoyaltyGain { get; }

		public bool IsMatch => Kind != FoodKind.None;

		public FoodMatch(FoodKind kind, int loyaltyGain)
		{
			Kind = kind;
			LoyaltyGain = loyaltyGain;
		}
	}
	public enum FeedOutcome
	{
		Satiated,
		NotInterested,
		Fed
	}
	public sealed class FoodEntry
	{
		public string Key;

		public int? ItemId;

		public string Category;

		public FoodKind Kind;

		public int LoyaltyGain;

		public float? Heal;

		public int? HealthRecovery;
	}
	public sealed class DietEntry
	{
		public double? HungerSecondsPerDay;

		public List<FoodEntry> Foods;

		public DietEntry(List<FoodEntry> foods = null, double? hungerSecondsPerDay = null)
		{
			Foods = foods ?? new List<FoodEntry>();
			HungerSecondsPerDay = hungerSecondsPerDay;
		}
	}
	public readonly struct FeedDecision
	{
		public FeedOutcome Outcome { get; }

		public FoodKind Kind { get; }

		public int LoyaltyGain { get; }

		public float Heal { get; }

		public int HealthRecoveryLevel { get; }

		public string MatchedKey { get; }

		public FeedDecision(FeedOutcome outcome, FoodKind kind, int loyaltyGain, float heal, int healthRecoveryLevel, string matchedKey)
		{
			Outcome = outcome;
			Kind = kind;
			LoyaltyGain = loyaltyGain;
			Heal = heal;
			HealthRecoveryLevel = healthRecoveryLevel;
			MatchedKey = matchedKey;
		}

		public FoodMatch ToFoodMatch()
		{
			if (Outcome != FeedOutcome.Fed)
			{
				return FoodMatch.None;
			}
			return new FoodMatch(Kind, LoyaltyGain);
		}
	}
	public static class PetDiet
	{
		public const string DefaultKey = "Default";

		public const int DefaultPreferredLoyalty = 10;

		public const int DefaultBondLoyalty = 20;

		public static Dictionary<string, DietEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "species", "an object of species → food arrays", ParseDiet);
		}

		private static DietEntry ParseDiet(string species, object value, Action<string> warn)
		{
			if (value is List<object> foods)
			{
				return new DietEntry(ParseFoods(species, foods, warn));
			}
			FieldReader fieldReader = new FieldReader("'" + species + "'", warn);
			if (value is Dictionary<string, object> dictionary)
			{
				double? hungerSecondsPerDay = null;
				List<object> list = null;
				foreach (KeyValuePair<string, object> item in dictionary)
				{
					string text = item.Key.ToLowerInvariant();
					double result2;
					if (!(text == "hungersecondsperday"))
					{
						if (text == "foods")
						{
							if (fieldReader.Items("foods", item.Value, out List<object> result, "an ARRAY of food objects", "species skipped"))
							{
								list = result;
							}
						}
						else
						{
							fieldReader.Unknown(item.Key, "hungerSecondsPerDay, foods");
						}
					}
					else if (fieldReader.Num("hungerSecondsPerDay", item.Value, out result2, null, "using the global default"))
					{
						hungerSecondsPerDay = result2;
						if (result2 <= 0.0)
						{
							fieldReader.Say($"hungerSecondsPerDay {result2} disables hunger and decay — this pet stays permanently feedable and never loses loyalty to hunger.");
						}
					}
				}
				if (list == null)
				{
					fieldReader.Say("object form requires a 'foods' array — species skipped.");
					return null;
				}
				return new DietEntry(ParseFoods(species, list, warn), hungerSecondsPerDay);
			}
			fieldReader.Say("value must be a food ARRAY or an OBJECT with a 'foods' array — species skipped.");
			return null;
		}

		private static List<FoodEntry> ParseFoods(string species, List<object> foods, Action<string> warn)
		{
			List<FoodEntry> list = new List<FoodEntry>();
			for (int i = 0; i < foods.Count; i++)
			{
				FoodEntry foodEntry = ParseFood(species, i, foods[i], warn);
				if (foodEntry != null)
				{
					list.Add(foodEntry);
				}
			}
			return list;
		}

		private static FoodEntry ParseFood(string species, int index, object value, Action<string> warn)
		{
			FieldReader f = new FieldReader($"'{species}' food #{index + 1}", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("must be an object — entry skipped.");
				return null;
			}
			FoodEntry foodEntry = new FoodEntry
			{
				Kind = FoodKind.Preferred
			};
			bool flag = false;
			bool flag2 = false;
			int? num = null;
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "item":
					flag = TryReadItem(f, item.Value, foodEntry);
					break;
				case "category":
					flag2 = TryReadCategory(f, item.Value, foodEntry);
					break;
				case "kind":
				{
					if (item.Value is string name && TryParseKind(name, out var kind))
					{
						foodEntry.Kind = kind;
					}
					else
					{
						f.Say($"unknown kind '{item.Value}' (valid: Preferred, Bond) — using Preferred.");
					}
					break;
				}
				case "loyalty":
				{
					if (f.Int("loyalty", item.Value, out var result2, null, "using the kind default"))
					{
						num = result2;
					}
					break;
				}
				case "heal":
				{
					if (f.Num("heal", item.Value, out var result3, null, "using the global fallback"))
					{
						foodEntry.Heal = (float)result3;
					}
					break;
				}
				case "healthrecovery":
				{
					if (f.Int("healthRecovery", item.Value, out var result, $"a number {1}-{5}", $"using the default ({1})"))
					{
						int num2 = ((result < 1) ? 1 : ((result > 5) ? 5 : result));
						if (num2 != result)
						{
							f.Say($"'healthRecovery' {result} out of range {1}-{5} — clamped to {num2}.");
						}
						foodEntry.HealthRecovery = num2;
					}
					break;
				}
				default:
					f.Unknown(item.Key, "item, category, kind, loyalty, heal, healthRecovery");
					break;
				}
			}
			if (flag && flag2)
			{
				f.Say("has BOTH 'item' and 'category' — ambiguous, entry skipped (write two foods).");
				return null;
			}
			if (!flag && !flag2)
			{
				f.Say("missing/invalid required 'item' (or 'category') — entry skipped.");
				return null;
			}
			foodEntry.LoyaltyGain = num ?? ((foodEntry.Kind == FoodKind.Bond) ? 20 : 10);
			return foodEntry;
		}

		private static bool TryReadItem(FieldReader f, object value, FoodEntry entry)
		{
			ItemKey val = default(ItemKey);
			if (ItemKey.TryRead(value, ref val))
			{
				entry.Key = ((ItemKey)(ref val)).Key;
				entry.ItemId = ((ItemKey)(ref val)).ItemId;
				return true;
			}
			f.Wrong("item", "a number (ItemID) or non-empty string (display name)");
			return false;
		}

		private static bool TryReadCategory(FieldReader f, object value, FoodEntry entry)
		{
			if (!(value is string text) || text.Trim().Length == 0)
			{
				f.Wrong("category", "a non-empty string (valid: " + FoodCategories.ValidList() + ")");
				return false;
			}
			if (FoodCategories.TryCanonical(text, out string canonical))
			{
				entry.Category = canonical;
			}
			else
			{
				entry.Category = text.Trim();
				f.Say("unknown category '" + text.Trim() + "' (valid: " + FoodCategories.ValidList() + ") — the entry will never match.");
			}
			entry.Key = FoodCategories.Label(entry.Category);
			return true;
		}

		private static bool TryParseKind(string name, out FoodKind kind)
		{
			if (string.Equals(name, "Preferred", StringComparison.OrdinalIgnoreCase))
			{
				kind = FoodKind.Preferred;
				return true;
			}
			if (string.Equals(name, "Bond", StringComparison.OrdinalIgnoreCase))
			{
				kind = FoodKind.Bond;
				return true;
			}
			kind = FoodKind.Preferred;
			return false;
		}

		public static Dictionary<string, DietEntry> Merge(Dictionary<string, DietEntry> builtIn, Dictionary<string, DietEntry> overrides)
		{
			return SpeciesTable.Merge<DietEntry>(builtIn, overrides);
		}

		public static IReadOnlyList<FoodEntry> Resolve(Dictionary<string, DietEntry> table, string speciesId)
		{
			DietEntry dietEntry = default(DietEntry);
			if (!SpeciesTable.TryResolve<DietEntry>(table, speciesId, ref dietEntry, "Default") || dietEntry?.Foods == null)
			{
				return Array.Empty<FoodEntry>();
			}
			return dietEntry.Foods;
		}

		public static double? HungerSecondsPerDay(Dictionary<string, DietEntry> table, string speciesId)
		{
			DietEntry dietEntry = default(DietEntry);
			if (!SpeciesTable.TryResolve<DietEntry>(table, speciesId, ref dietEntry, "Default"))
			{
				return null;
			}
			return dietEntry?.HungerSecondsPerDay;
		}

		public static FeedDecision Decide(bool satiated, IReadOnlyList<FoodEntry> diet, int itemId, string itemName, float fallbackHeal)
		{
			return Decide(satiated, diet, itemId, itemName, null, fallbackHeal);
		}

		public static FeedDecision Decide(bool satiated, IReadOnlyList<FoodEntry> diet, int itemId, string itemName, IReadOnlyCollection<string> itemCategories, float fallbackHeal)
		{
			if (satiated)
			{
				return new FeedDecision(FeedOutcome.Satiated, FoodKind.None, 0, 0f, 0, null);
			}
			FoodEntry foodEntry = null;
			if (diet != null)
			{
				foreach (FoodEntry item in diet)
				{
					if (item != null && Matches(item, itemId, itemName, itemCategories))
					{
						if (item.Kind == FoodKind.Bond)
						{
							foodEntry = item;
							break;
						}
						if (foodEntry == null)
						{
							foodEntry = item;
						}
					}
				}
			}
			if (foodEntry == null)
			{
				return new FeedDecision(FeedOutcome.NotInterested, FoodKind.None, 0, 0f, 0, null);
			}
			return new FeedDecision(FeedOutcome.Fed, foodEntry.Kind, foodEntry.LoyaltyGain, foodEntry.Heal ?? fallbackHeal, foodEntry.HealthRecovery ?? 1, foodEntry.Key);
		}

		public static bool Matches(FoodEntry entry, int itemId, string itemName)
		{
			return Matches(entry, itemId, itemName, null);
		}

		public static bool Matches(FoodEntry entry, int itemId, string itemName, IReadOnlyCollection<string> itemCategories)
		{
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			if (entry == null)
			{
				return false;
			}
			if (entry.Category != null)
			{
				if (itemCategories == null)
				{
					return false;
				}
				foreach (string itemCategory in itemCategories)
				{
					if (string.Equals(itemCategory, entry.Category, StringComparison.OrdinalIgnoreCase))
					{
						return true;
					}
				}
				return false;
			}
			ItemKey val = new ItemKey(entry.Key, entry.ItemId);
			return ((ItemKey)(ref val)).Matches(itemId, itemName);
		}
	}
	public enum GearSlot
	{
		Any = -1,
		Helmet,
		Chest,
		Legs,
		Foot,
		Hands,
		RightHand,
		LeftHand,
		Back,
		Quiver
	}
	public enum GearEffectKind
	{
		LoyaltyGainMult,
		LoyaltyDecayMult,
		PetHealthMult,
		PetDamageMult,
		PetDefenseMult,
		PetSpeedMult
	}
	public sealed class GearEffectDef
	{
		public GearEffectKind Kind;

		public float Value;
	}
	public sealed class GearEntry
	{
		public string ItemKey;

		public int? ItemId;

		public GearSlot Slot = GearSlot.Any;

		public string Species;

		public List<GearEffectDef> Effects = new List<GearEffectDef>();
	}
	public readonly struct EquippedItem
	{
		public readonly int ItemId;

		public readonly string Name;

		public readonly GearSlot Slot;

		public EquippedItem(int itemId, string name, GearSlot slot)
		{
			ItemId = itemId;
			Name = name;
			Slot = slot;
		}
	}
	public sealed class GearEffects
	{
		public static readonly GearEffects None = new GearEffects(1f, 1f, new StatModifiers());

		public float LoyaltyGainMult { get; }

		public float LoyaltyDecayMult { get; }

		public StatModifiers PetStats { get; }

		public bool IsIdentity
		{
			get
			{
				if (LoyaltyGainMult == 1f && LoyaltyDecayMult == 1f && PetStats.Health == 1f && PetStats.Damage == 1f && PetStats.Defense == 1f)
				{
					return PetStats.Speed == 1f;
				}
				return false;
			}
		}

		public GearEffects(float loyaltyGainMult, float loyaltyDecayMult, StatModifiers petStats)
		{
			LoyaltyGainMult = loyaltyGainMult;
			LoyaltyDecayMult = loyaltyDecayMult;
			PetStats = petStats ?? new StatModifiers();
		}
	}
	public static class PetGear
	{
		public static Dictionary<string, GearEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "item", "an object of item → gear-effect objects", ParseEntry);
		}

		private static GearEntry ParseEntry(string itemKey, object value, Action<string> warn)
		{
			string text = (itemKey ?? "").Trim();
			if (text.Length == 0)
			{
				warn?.Invoke("empty item key — entry skipped.");
				return null;
			}
			FieldReader f = new FieldReader("'" + text + "'", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("value must be an object — entry skipped.");
				return null;
			}
			ItemKey val = default(ItemKey);
			ItemKey.TryRead((object)text, ref val);
			GearEntry gearEntry = new GearEntry
			{
				ItemKey = ((ItemKey)(ref val)).Key,
				ItemId = ((ItemKey)(ref val)).ItemId
			};
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "slot":
				{
					if (!f.Enum<GearSlot>("slot", item.Value, out var result2, "name a slot (" + string.Join(", ", Enum.GetNames(typeof(GearSlot))) + ")", "entry skipped"))
					{
						return null;
					}
					gearEntry.Slot = result2;
					break;
				}
				case "species":
				{
					if (!f.Str("species", item.Value, out string result3, null, "entry skipped"))
					{
						return null;
					}
					gearEntry.Species = result3;
					break;
				}
				case "effects":
				{
					if (!f.Items("effects", item.Value, out List<object> result, "an ARRAY of {kind, value} objects"))
					{
						break;
					}
					foreach (object item2 in result)
					{
						GearEffectDef gearEffectDef = ParseEffect(f, item2);
						if (gearEffectDef != null)
						{
							gearEntry.Effects.Add(gearEffectDef);
						}
					}
					break;
				}
				default:
					f.Unknown(item.Key, "slot, species, effects");
					break;
				}
			}
			if (gearEntry.Effects.Count == 0)
			{
				f.Say("missing/empty required 'effects' — entry skipped.");
				return null;
			}
			return gearEntry;
		}

		private static GearEffectDef ParseEffect(FieldReader f, object value)
		{
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("each effect must be an object {kind, value} — one skipped.");
				return null;
			}
			if (!dictionary.TryGetValue("kind", out var value2) || !(value2 is string text))
			{
				f.Say("an effect is missing a string 'kind' — skipped.");
				return null;
			}
			if (!EnumRead.TryName<GearEffectKind>(text.Trim(), out var value3))
			{
				f.Say("unknown effect kind '" + text + "' (valid: " + string.Join(", ", Enum.GetNames(typeof(GearEffectKind))) + ") — skipped (a table for a future build stays loadable).");
				return null;
			}
			if (!dictionary.TryGetValue("value", out var value4) || !(value4 is double num) || num <= 0.0)
			{
				f.Say($"effect '{value3}' needs a numeric 'value' > 0 (a multiplier; 1.5 = +50%) — skipped.");
				return null;
			}
			return new GearEffectDef
			{
				Kind = value3,
				Value = (float)num
			};
		}

		public static Dictionary<string, GearEntry> Merge(Dictionary<string, GearEntry> builtIn, Dictionary<string, GearEntry> overrides)
		{
			return SpeciesTable.Merge<GearEntry>(builtIn, overrides);
		}

		public static GearEffects Resolve(Dictionary<string, GearEntry> table, IReadOnlyList<EquippedItem> worn, string speciesId, Action<string> log = null)
		{
			if (table == null || table.Count == 0 || worn == null || worn.Count == 0)
			{
				return GearEffects.None;
			}
			float num = 1f;
			float num2 = 1f;
			StatModifiers statModifiers = new StatModifiers();
			bool flag = false;
			foreach (GearEntry value in table.Values)
			{
				if (!MatchesWorn(value, worn, out var hit) || !SpeciesMatches(value.Species, speciesId))
				{
					continue;
				}
				flag = true;
				foreach (GearEffectDef effect in value.Effects)
				{
					switch (effect.Kind)
					{
					case GearEffectKind.LoyaltyGainMult:
						num *= effect.Value;
						break;
					case GearEffectKind.LoyaltyDecayMult:
						num2 *= effect.Value;
						break;
					case GearEffectKind.PetHealthMult:
						statModifiers.Health *= effect.Value;
						break;
					case GearEffectKind.PetDamageMult:
						statModifiers.Damage *= effect.Value;
						break;
					case GearEffectKind.PetDefenseMult:
						statModifiers.Defense *= effect.Value;
						break;
					case GearEffectKind.PetSpeedMult:
						statModifiers.Speed *= effect.Value;
						break;
					}
				}
				log?.Invoke($"{value.ItemKey} (slot {hit.Slot}) -> {Describe(value)}");
			}
			if (!flag)
			{
				return GearEffects.None;
			}
			return new GearEffects(num, num2, statModifiers);
		}

		private static bool MatchesWorn(GearEntry entry, IReadOnlyList<EquippedItem> worn, out EquippedItem hit)
		{
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			for (int i = 0; i < worn.Count; i++)
			{
				EquippedItem equippedItem = worn[i];
				ItemKey val = new ItemKey(entry.ItemKey, entry.ItemId);
				if (((ItemKey)(ref val)).Matches(equippedItem.ItemId, equippedItem.Name) && (entry.Slot == GearSlot.Any || entry.Slot == equippedItem.Slot))
				{
					hit = equippedItem;
					return true;
				}
			}
			hit = default(EquippedItem);
			return false;
		}

		public static bool SpeciesMatches(string filter, string speciesId)
		{
			return Species.NameMatches(speciesId, filter);
		}

		private static string Describe(GearEntry entry)
		{
			List<string> list = new List<string>();
			foreach (GearEffectDef effect in entry.Effects)
			{
				list.Add($"{effect.Kind} x{effect.Value:0.###}");
			}
			return string.Join(", ", list);
		}
	}
	public sealed class GiftDrop
	{
		public string Key;

		public int? ItemId;

		public int Qty = 1;

		public double Chance;

		public double ChanceAt100;
	}
	public sealed class PetGiftEntry
	{
		public string Species;

		public double NothingChance;

		public double NothingChanceAt100;

		public GiftDrop Default;

		public List<GiftDrop> Drops = new List<GiftDrop>();
	}
	public sealed class GiftMix
	{
		public double[] DropChances;

		public double DefaultChance;

		public double NothingChance;
	}
	public sealed class GiftAuditFinding
	{
		public bool Error;

		public string Message;
	}
	public static class PetGifts
	{
		public const string DefaultKey = "Default";

		private const double Epsilon = 1E-09;

		public static Dictionary<string, PetGiftEntry> Parse(string json, Action<string> warn = null)
		{
			return JsonTable.Read(json, warn, "species", "an object of species → gift objects", ParseEntry);
		}

		private static PetGiftEntry ParseEntry(string speciesKey, object value, Action<string> warn)
		{
			FieldReader f = new FieldReader("'" + speciesKey + "'", warn);
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("value must be an object — species skipped.");
				return null;
			}
			PetGiftEntry petGiftEntry = new PetGiftEntry
			{
				Species = speciesKey
			};
			bool flag = false;
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "default":
					petGiftEntry.Default = ParseDefault(f, item.Value);
					break;
				case "drops":
				{
					if (!f.Items("drops", item.Value, out List<object> result, "an ARRAY of drop objects"))
					{
						break;
					}
					foreach (object item2 in result)
					{
						GiftDrop giftDrop = ParseDrop(f, speciesKey, item2);
						if (giftDrop != null)
						{
							petGiftEntry.Drops.Add(giftDrop);
						}
					}
					break;
				}
				case "nothingchance":
					petGiftEntry.NothingChance = ReadChance(f, "nothingChance", item.Value, 0.0);
					break;
				case "nothingchanceat100":
					petGiftEntry.NothingChanceAt100 = ReadChance(f, "nothingChanceAt100", item.Value, 0.0);
					flag = true;
					break;
				default:
					f.Unknown(item.Key, "default, drops, nothingChance, nothingChanceAt100");
					break;
				}
			}
			if (!flag)
			{
				petGiftEntry.NothingChanceAt100 = petGiftEntry.NothingChance;
			}
			if (petGiftEntry.Default == null)
			{
				f.Say("missing required 'default' drop — species skipped.");
				return null;
			}
			foreach (GiftAuditFinding item3 in Audit(petGiftEntry))
			{
				f.Say(item3.Message);
			}
			return petGiftEntry;
		}

		private static GiftDrop ParseDefault(FieldReader f, object value)
		{
			if (value is Dictionary<string, object> dictionary)
			{
				GiftDrop giftDrop = new GiftDrop();
				foreach (KeyValuePair<string, object> item in dictionary)
				{
					string text = item.Key.ToLowerInvariant();
					if (!(text == "item"))
					{
						if (text == "qty")
						{
							giftDrop.Qty = ReadQty(f, "default", item.Value);
						}
						else
						{
							f.Say("unknown default key '" + item.Key + "' (valid: item, qty — the default's chance is computed, never authored).");
						}
					}
					else
					{
						ReadItem(f, "default", item.Value, giftDrop);
					}
				}
				if (giftDrop.Key == null)
				{
					f.Say("'default' object missing a usable 'item'.");
					return null;
				}
				return giftDrop;
			}
			GiftDrop giftDrop2 = new GiftDrop();
			if (!ReadItem(f, "default", value, giftDrop2))
			{
				return null;
			}
			return giftDrop2;
		}

		private static GiftDrop ParseDrop(FieldReader f, string speciesKey, object value)
		{
			if (!(value is Dictionary<string, object> dictionary))
			{
				f.Say("each drop must be an object — drop skipped.");
				return null;
			}
			GiftDrop giftDrop = new GiftDrop();
			bool flag = false;
			bool flag2 = false;
			foreach (KeyValuePair<string, object> item in dictionary)
			{
				switch (item.Key.ToLowerInvariant())
				{
				case "item":
					ReadItem(f, "drop", item.Value, giftDrop);
					break;
				case "chance":
				{
					if (f.Num("chance", item.Value, out var result, null, "drop skipped"))
					{
						giftDrop.Chance = result;
						flag = true;
					}
					break;
				}
				case "chanceat100":
					giftDrop.ChanceAt100 = ReadChance(f, "chanceAt100", item.Value, 0.0);
					flag2 = true;
					break;
				case "qty":
					giftDrop.Qty = ReadQty(f, "drop", item.Value);
					break;
				default:
					f.Say("unknown drop key '" + item.Key + "' (valid: item, chance, chanceAt100, qty).");
					break;
				}
			}
			if (giftDrop.Key == null)
			{
				f.Say("drop missing required 'item' — drop skipped.");
				return null;
			}
			FieldReader fieldReader = new FieldReader("'" + speciesKey + "/" + giftDrop.Key + "'", f.Sink);
			if (!flag)
			{
				fieldReader.Say("drop missing required 'chance' — drop skipped.");
				return null;
			}
			if (!(giftDrop.Chance > 0.0) || giftDrop.Chance > 1.0)
			{
				fieldReader.Wrong("chance", "in (0,1]", "drop skipped");
				return null;
			}
			if (!flag2)
			{
				giftDrop.ChanceAt100 = giftDrop.Chance;
			}
			return giftDrop;
		}

		private static bool ReadItem(FieldReader f, string where, object value, GiftDrop drop)
		{
			ItemKey val = default(ItemKey);
			if (ItemKey.TryRead(value, ref val))
			{
				drop.Key = ((ItemKey)(ref val)).Key;
				drop.ItemId = ((ItemKey)(ref val)).ItemId;
				return true;
			}
			f.Say(where + " item must be a number or an item-name string.");
			return false;
		}

		private static double ReadChance(FieldReader f, string name, object value, double fallback)
		{
			if (f.Num(name, value, out var result, null, "using " + fallback.ToString("0.###", CultureInfo.InvariantCulture)))
			{
				if (result < 0.0 || result > 1.0)
				{
					f.Say("'" + name + "' " + result.ToString("0.###", CultureInfo.InvariantCulture) + " outside [0,1] — clamped.");
					if (!(result < 0.0))
					{
						return 1.0;
					}
					return 0.0;
				}
				return result;
			}
			return fallback;
		}

		private static int ReadQty(Fiel

Beastwhispering/Beastwhispering.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using AggroKit;
using Beastwhispering.Core;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using CompanionKit;
using CompanionKit.Core;
using ForgeKit;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using NetKit;
using NetKit.Core;
using Rewired;
using SideLoader;
using SideLoader.Model;
using SkillKit;
using StoryKit;
using StoryKit.Core;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("Beastwhispering")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+c48b2eb6460f1b993d9ddb914cbdc5ff6f1d22c6")]
[assembly: AssemblyProduct("Beastwhispering")]
[assembly: AssemblyTitle("Beastwhispering")]
[assembly: AssemblyMetadata("BuildStamp", "c48b2eb 2026-07-30")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.2.0.0")]
[module: UnverifiableCode]
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 Beastwhispering
{
	internal static class AggroStage
	{
		internal static bool ForceTarget(CharacterAI enemy, Character target)
		{
			return AggroTools.ForceTarget(enemy, target);
		}

		internal static bool Calm(CharacterAI enemy)
		{
			return AggroTools.Calm(enemy);
		}

		internal static IEnumerable<CharacterAI> AisInRange(Vector3 center, float radius, Character exclude)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			return AggroTools.AisInRange(center, radius, true, exclude);
		}

		internal static CharacterAI Find(string namePart, Vector3 center, float radius, Character exclude)
		{
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			CharacterAI result = null;
			float num = float.MaxValue;
			foreach (CharacterAI item in AisInRange(center, radius, exclude))
			{
				Character character = ((CharacterControl)item).Character;
				if (string.IsNullOrEmpty(namePart) || (character.Name != null && character.Name.IndexOf(namePart, StringComparison.OrdinalIgnoreCase) >= 0))
				{
					float num2 = Vector3.Distance(center, ((Component)character).transform.position);
					if (num2 < num)
					{
						num = num2;
						result = item;
					}
				}
			}
			return result;
		}
	}
	internal static class BlanketSetup
	{
		internal static readonly Dictionary<string, int> Registered = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);

		private const string DonorName = "Bandages";

		private static bool _done;

		internal static void Init()
		{
			SL.OnPacksLoaded += Setup;
		}

		private static void Setup()
		{
			if (!_done)
			{
				_done = true;
				SlFeatureSetup.Run("[BLANKET]", Plugin.EnableTemperatureSystem, "EnableTemperatureSystem=false — no blanket items/recipes registered.", PetComfortTable.Blankets.Values, (BlanketDef d) => d.Key, RegisterOne, () => Registered.Count, "blankets");
			}
		}

		private static void RegisterOne(BlanketDef def)
		{
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Invalid comparison between Unknown and I4
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_0168: Invalid comparison between Unknown and I4
			int num = ResolveId(def.Key);
			if (num < 0)
			{
				return;
			}
			List<int> list = new List<int>();
			foreach (string ingredient in def.Ingredients)
			{
				int? num2 = ResolveByName(ingredient, def.Key, "ingredient");
				if (!num2.HasValue)
				{
					return;
				}
				list.Add(num2.Value);
			}
			int num3 = ResolveByName("Bandages", def.Key, "donor") ?? list[0];
			string text = (((int)def.Side == 1) ? "cold" : "heat");
			Item val = SlConsumables.RegisterUsable("[BLANKET]", def.Key, num3, num, def.Key, $"Wrap your companion against the {text}. Relieves up to {def.ReliefSteps} steps of {text} for " + $"{def.DurationSeconds / 60.0:0} minutes — and where the {text} runs deeper than the blanket can counter, " + "it still slows the toll. One wrap at a time; useless against the opposite extreme.", delegate(GameObject host)
			{
				host.AddComponent<BlanketWrapEffect>().BlanketKey = def.Key;
			}, ((int)def.Side == 1) ? "HeatingBlanket.png" : "CoolingBlanket.png", (SpriteBorderTypes)1);
			if (!((Object)(object)val == (Object)null))
			{
				string text2 = "bw.blanket." + def.Key.Trim().ToLowerInvariant().Replace(' ', '_');
				Recipe val2 = SlConsumables.RegisterRecipe("[BLANKET]", def.Key, text2, (CraftingType)2, list, num, 1);
				if (!((Object)(object)val2 == (Object)null))
				{
					Registered[def.Key] = num;
					Plugin.Log.LogMessage((object)($"[BLANKET] '{def.Key}': item {num} (donor {num3}), recipe '{text2}' " + string.Format("(Survival: {0}), relieves {1} {2} steps for {3:0}s.", string.Join(" + ", def.Ingredients), def.ReliefSteps, text, def.DurationSeconds)));
				}
			}
		}

		internal static BlanketDef ForItemId(int itemId)
		{
			foreach (KeyValuePair<string, int> item in Registered)
			{
				if (item.Value == itemId && PetComfortTable.Blankets.TryGetValue(item.Key, out var value))
				{
					return value;
				}
			}
			return null;
		}

		private static int ResolveId(string key)
		{
			int num = default(int);
			if (!BwIds.TryGet("blanket." + key.Trim().ToLowerInvariant().Replace(' ', '-'), ref num))
			{
				Plugin.Log.LogWarning((object)("[BLANKET] '" + key + "': no ItemID is allocated for this blanket, so it cannot be registered — a made-up id would land on some other mod's item. Allocate one with `bwspecies ids alloc --family blankets --key blanket.<key> --owner Beastwhispering/BlanketSetup --name \"<display name>\"`, then `bwspecies ids gen`."));
				return -1;
			}
			if (Registered.ContainsValue(num))
			{
				Plugin.Log.LogWarning((object)$"[BLANKET] '{key}': ItemID {num} is already registered this session — skipped.");
				return -1;
			}
			return num;
		}

		private static int? ResolveByName(string name, string key, string field)
		{
			if (ItemNameIndex.TryResolve(name, out var itemId))
			{
				Plugin.Log.LogMessage((object)$"[BLANKET] '{key}': {field} '{name}' resolved to ItemID {itemId}.");
				return itemId;
			}
			Plugin.Log.LogWarning((object)("[BLANKET] '" + key + "': " + field + " '" + name + "' matches no item display name on this locale — blanket skipped."));
			return null;
		}
	}
	internal sealed class BlanketUseVeto : IUseVeto
	{
		public string Tag => "[BLANKET]";

		public bool Owns(int itemId)
		{
			return BlanketSetup.ForItemId(itemId) != null;
		}

		public UseVeto Check(Item item, Character user)
		{
			BlanketDef val = BlanketSetup.ForItemId(item.ItemID);
			if (val == null)
			{
				return null;
			}
			PetSave val2 = Plugin.Instance?.ActivePet?.State;
			if (val2 == null)
			{
				return new UseVeto("You have no companion to wrap.", "[BLANKET] use of '" + val.Key + "' vetoed: no active pet.");
			}
			if (val2.BlanketKey == val.Key && val2.BlanketSecondsLeft >= val.DurationSeconds - 1.0)
			{
				return new UseVeto("Your companion is already snugly wrapped.", "[BLANKET] use of '" + val.Key + "' vetoed: same wrap already at full duration.");
			}
			return null;
		}
	}
	internal class BlanketWrapEffect : Effect
	{
		public string BlanketKey;

		public override void ActivateLocally(Character _affectedCharacter, object[] _infos)
		{
			try
			{
				if (!Plugin.EnableTemperatureSystem.Value)
				{
					if ((Object)(object)_affectedCharacter != (Object)null && _affectedCharacter.IsLocalPlayer)
					{
						Notify.Player(_affectedCharacter, "The temperature system is disabled — the blanket was not consumed.");
					}
					Plugin.Log.LogMessage((object)"[BLANKET] wrap ignored — [Temperature] EnableTemperatureSystem is off (the blanket was not consumed; flip it back + `reloadcfg` to re-enable).");
					return;
				}
				BlanketDef value;
				BlanketDef val = (PetComfortTable.Blankets.TryGetValue(BlanketKey ?? "", out value) ? value : null);
				PetSave val2 = Plugin.Instance?.ActivePet?.State;
				if (val != null && val2 != null && !((Object)(object)_affectedCharacter == (Object)null))
				{
					bool flag = !string.IsNullOrEmpty(val2.BlanketKey) && val2.BlanketKey != val.Key;
					val2.BlanketKey = val.Key;
					val2.BlanketSecondsLeft = val.DurationSeconds;
					Plugin.Instance.PersistPet();
					Item parentItem = ((Effect)this).ParentItem;
					if ((Object)(object)parentItem != (Object)null)
					{
						parentItem.RemoveQuantity(1);
					}
					else
					{
						Plugin.Log.LogWarning((object)"[BLANKET] no ParentItem to consume — wrap landed, blanket NOT consumed (bug, report).");
					}
					string text = Plugin.Instance.ActivePet.SpeciesId ?? "your companion";
					Notify.Player(_affectedCharacter, "You wrap " + text + " in the " + val.Key + "." + (flag ? " The old wrap falls away." : ""));
					Plugin.Log.LogMessage((object)($"[BLANKET] '{val.Key}' consumed → buff {val.DurationSeconds:0}s on '{text}'" + (flag ? " (replaced the previous wrap)" : "") + "."));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[BLANKET] wrap-on-use failed: " + ex));
			}
		}
	}
	internal static class BraceDriver
	{
		private static readonly BraceState State = new BraceState();

		private static SpecialAttackDef _def;

		private static CompanionBody _body;

		private static CompanionCombat _combat;

		private static bool _synergyOpened;

		private static int _negated;

		private static string _lastWindow = "never braced";

		private static string _lastTaunt = "none this session";

		private static readonly List<Character> _pendingRipostes = new List<Character>();

		internal static bool Active => State.Active((double)Time.time);

		internal static string Forensics
		{
			get
			{
				if (!Active)
				{
					return _lastWindow;
				}
				return string.Format("OPEN {0:F1}s left, countered [{1}], {2} negated", State.RemainingSeconds((double)Time.time), string.Join(", ", CounteredNames()), _negated);
			}
		}

		private static List<string> CounteredNames()
		{
			return new List<string>(State.CounteredUids);
		}

		internal static void Enter(CompanionBody body, CompanionCombat combat, SpecialAttackDef def)
		{
			_body = body;
			_combat = combat;
			_def = def;
			_synergyOpened = false;
			_negated = 0;
			_pendingRipostes.Clear();
			State.Begin((double)Time.time, (double)Plugin.BraceWindowSeconds.Value);
			SetBlockPose(on: true);
			PlayEnterCue();
			Plugin.Log.LogMessage((object)($"[BRACE] '{_body?.SpeciesId}' braced for {Plugin.BraceWindowSeconds.Value:F1}s " + $"(perAttackerOnce={Plugin.BracePerAttackerOnce.Value}, negate={Plugin.BraceNegateCounteredHit.Value})."));
			MaybeTaunt();
		}

		private static void MaybeTaunt()
		{
			if (!Plugin.BraceEnableTaunt.Value)
			{
				_lastTaunt = "skipped ([Brace] EnableTaunt=false)";
				return;
			}
			Pet pet = Plugin.Instance?.ActivePet;
			int? obj;
			if (pet == null)
			{
				obj = null;
			}
			else
			{
				PetSimulation sim = pet.Sim;
				obj = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue);
			}
			int? num = obj;
			int valueOrDefault = num.GetValueOrDefault();
			float num2 = SpecialAttackTable.TauntSeconds(_def, valueOrDefault);
			if (num2 <= 0f)
			{
				_lastTaunt = "none ('" + _body?.SpeciesId + "' has no taunt axis on its SpeciesSpecialAttacks row)";
				return;
			}
			object obj2;
			if (pet == null)
			{
				obj2 = null;
			}
			else
			{
				CompanionCombat combat = ((Companion)pet).Combat;
				obj2 = ((combat != null) ? combat.SpecialAttackTarget : null);
			}
			Character val = (Character)obj2;
			if ((Object)(object)val == (Object)null)
			{
				Plugin.Log.LogMessage((object)"[TAUNT] no braced-stance target to taunt — nothing pinned.");
				_lastTaunt = $"no target (would have been {num2:F1}s at loyalty {valueOrDefault})";
				return;
			}
			object anchor;
			if (pet == null)
			{
				anchor = null;
			}
			else
			{
				CompanionAnchor anchor2 = ((Companion)pet).Anchor;
				anchor = ((anchor2 != null) ? anchor2.Current : null);
			}
			TauntController.Begin(val, (Character)anchor, num2);
			Character localPlayerCharacter = Plugin.LocalPlayerCharacter;
			if ((Object)(object)localPlayerCharacter != (Object)null)
			{
				Notify.Player(localPlayerCharacter, pet.SpeciesId + " draws the enemy's fury with an infernal growl!");
			}
			_lastTaunt = $"'{val.Name}' for {num2:F1}s at loyalty {valueOrDefault}";
		}

		internal static bool OnAnchorHit(Character dealer, Object damageSource, Character anchor)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0094: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: Invalid comparison between Unknown and I4
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Invalid comparison between Unknown and I4
			//IL_0100: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			BraceHitInputs val = new BraceHitInputs
			{
				DealerUid = (((Object)(object)dealer != (Object)null) ? UID.op_Implicit(dealer.UID) : null),
				SelfUid = (((Object)(object)anchor != (Object)null) ? UID.op_Implicit(anchor.UID) : null),
				SourceIsStatus = (damageSource is StatusEffect),
				DealerHostile = ((Object)(object)dealer != (Object)null && dealer.Alive && dealer.IsAI && (int)dealer.Faction != 1)
			};
			string text = DescribeSource(damageSource);
			BraceHitVerdict val2 = BraceRules.Eligible(ref val);
			if ((int)val2 != 0)
			{
				Plugin.Log.LogMessage((object)string.Format("[BRACE] hit from '{0}' via {1} not counter-eligible ({2}) — lands normally.", ((dealer != null) ? dealer.Name : null) ?? "nothing", text, val2));
				return false;
			}
			CounterVerdict val3 = State.TryCounter(val.DealerUid, (double)Time.time, Plugin.BracePerAttackerOnce.Value);
			if ((int)val3 != 3)
			{
				Plugin.Log.LogMessage((object)$"[BRACE] hit from '{dealer.Name}' via {text} not countered ({val3}) — lands normally.");
				return false;
			}
			_pendingRipostes.Add(dealer);
			if (Plugin.BraceNegateCounteredHit.Value)
			{
				_negated++;
			}
			Plugin.Log.LogMessage((object)("[BRACE] COUNTER: '" + dealer.Name + "' struck the braced pet via " + text + " — " + (Plugin.BraceNegateCounteredHit.Value ? "hit negated, " : "") + "riposte next frame."));
			return Plugin.BraceNegateCounteredHit.Value;
		}

		private static string DescribeSource(Object src)
		{
			if (src == (Object)null)
			{
				return "src=none";
			}
			string text = ((src is ProjectileWeapon) ? " (missile)" : ((src is Weapon) ? " (melee)" : ((src is StatusEffect) ? " (status tick)" : ((((object)src).GetType().Name.IndexOf("Blast", StringComparison.OrdinalIgnoreCase) >= 0 || src.name.IndexOf("Blast", StringComparison.OrdinalIgnoreCase) >= 0 || src.name.IndexOf("AoE", StringComparison.OrdinalIgnoreCase) >= 0) ? " (AoE?)" : ((src.name.IndexOf("Projectile", StringComparison.OrdinalIgnoreCase) >= 0 || src.name.IndexOf("Bolt", StringComparison.OrdinalIgnoreCase) >= 0) ? " (missile?)" : "")))));
			return "src=" + ((object)src).GetType().Name + ":'" + src.name + "'" + text;
		}

		internal static void Tick()
		{
			if (!State.Open)
			{
				return;
			}
			if (!Plugin.EnableBrace.Value)
			{
				Exit("kill-switch");
			}
			else if ((Object)(object)_body == (Object)null || (Object)(object)((Companion)(Plugin.Instance?.ActivePet?)).Body != (Object)(object)_body)
			{
				Exit("body gone");
			}
			else if (!State.Active((double)Time.time) && _pendingRipostes.Count == 0)
			{
				Exit("expiry");
			}
			else
			{
				if (_pendingRipostes.Count <= 0)
				{
					return;
				}
				List<Character> list = new List<Character>(_pendingRipostes);
				_pendingRipostes.Clear();
				foreach (Character item in list)
				{
					Riposte(item);
				}
			}
		}

		private static void Riposte(Character attacker)
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a4: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)attacker == (Object)null || !attacker.Alive)
			{
				Plugin.Log.LogMessage((object)"[BRACE] riposte target died before the counter landed — nothing dealt.");
			}
			else if (!((Object)(object)_combat == (Object)null) && !((Object)(object)_body == (Object)null))
			{
				Vector3 val = ((Component)attacker).transform.position - ((Component)_body).transform.position;
				Vector3 normalized = ((Vector3)(ref val)).normalized;
				float num = _combat.BaseDamage * Mathf.Max(0f, _def.DamageMultiplier);
				_combat.DealDamage(attacker, num, normalized, Plugin.BraceRiposteImpact.Value);
				try
				{
					Global.AudioManager.PlaySoundAtPosition((Sounds)12930, ((Component)_body).transform, 0f, 1f, 1f, 1f, 1f);
				}
				catch
				{
				}
				Plugin.Log.LogMessage((object)($"[BRACE] riposte hit '{attacker.Name}' for {num:F0} " + $"(base {_combat.BaseDamage:F0} x{_def.DamageMultiplier:F2}, impact {Plugin.BraceRiposteImpact.Value:F0})."));
				Pet pet = Plugin.Instance?.ActivePet;
				SpecialHitContext specialHitContext = new SpecialHitContext
				{
					Pet = pet,
					Def = _def,
					Target = attacker
				};
				object dealer;
				if (pet == null)
				{
					dealer = null;
				}
				else
				{
					CompanionAnchor anchor = ((Companion)pet).Anchor;
					dealer = ((anchor != null) ? anchor.Current : null);
				}
				specialHitContext.Dealer = (Character)dealer;
				specialHitContext.ActiveSigils = (Plugin.EnableSigilSynergies.Value ? SigilSense.ActiveKeysAt(((Component)_body).transform.position) : SigilSense.None);
				SpecialHitContext ctx = specialHitContext;
				SpeciesRegistry.For(_body.SpeciesId).OnSpecialAttackHit(in ctx);
				Character localPlayerCharacter = Plugin.LocalPlayerCharacter;
				if (!_synergyOpened && Plugin.EnableHuntAsOne.Value && (Object)(object)localPlayerCharacter != (Object)null)
				{
					_synergyOpened = true;
					HuntSynergy.Open(attacker);
					HuntSynergy.ReportPet((StrikeOutcome)1);
					HuntAsOnePlayer.SyncedStrike(localPlayerCharacter, attacker);
					HuntAsOnePlayer.OnSpecialAttackFired(localPlayerCharacter);
				}
			}
		}

		private static void Exit(string reason)
		{
			_lastWindow = $"closed ({reason}): countered {State.CounterCount} attacker(s), {_negated} hit(s) negated";
			Plugin.Log.LogMessage((object)("[BRACE] " + _lastWindow + "."));
			State.End();
			_pendingRipostes.Clear();
			SetBlockPose(on: false);
			_def = null;
			_combat = null;
			_body = null;
		}

		private static void PlayEnterCue()
		{
			float num = Mathf.Clamp01(Plugin.BraceCueVolume.Value);
			if ((Object)(object)_body == (Object)null || num <= 0f)
			{
				return;
			}
			try
			{
				Global.AudioManager.PlaySoundAtPosition((Sounds)12910, ((Component)_body).transform, 0f, num, num, 1f, 1f);
			}
			catch
			{
			}
		}

		internal static void ForceExit(string reason)
		{
			if (State.Open)
			{
				Exit(reason);
			}
		}

		private static void SetBlockPose(bool on)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Invalid comparison between Unknown and I4
			Animator val = (((Object)(object)_body != (Object)null) ? ((Component)_body).GetComponent<Animator>() : null);
			if ((Object)(object)val == (Object)null)
			{
				return;
			}
			AnimatorControllerParameter[] parameters = val.parameters;
			foreach (AnimatorControllerParameter val2 in parameters)
			{
				if ((int)val2.type == 4 && val2.name == "Block")
				{
					val.SetBool("Block", on);
					break;
				}
			}
		}

		internal static void ForceBrace(Plugin p)
		{
			CompanionBody val = ((Companion)(p.ActivePet?)).Body;
			PetSpecialAttack petSpecialAttack = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent<PetSpecialAttack>() : null);
			if ((Object)(object)petSpecialAttack == (Object)null)
			{
				Plugin.Log.LogWarning((object)"[BRACE] brace: no strike-capable pet body.");
				return;
			}
			PetSpecialAttack.FireOverrides ov = PetSpecialAttack.FireOverrides.Default;
			ov.BypassCooldown = true;
			ov.ArmCooldown = false;
			ov.Tag = "[BRACE]";
			PetSpecialAttack.SpecialFireReport specialFireReport = petSpecialAttack.Fire(null, ov);
			Plugin.Log.LogMessage((object)("[BRACE] brace -> " + specialFireReport.Summary + ((specialFireReport.Started && !specialFireReport.SynergyDeferred) ? " (NB not a Brace species — that was a normal strike)" : "")));
		}

		internal static void BraceDump(Plugin p)
		{
			//IL_0101: Unknown result type (might be due to invalid IL or missing references)
			//IL_0122: Unknown result type (might be due to invalid IL or missing references)
			//IL_0127: 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_014c: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Log.LogMessage((object)"[BRACE] ── bracedump ──");
			Plugin.Log.LogMessage((object)($"[BRACE] config: EnableBrace={Plugin.EnableBrace.Value} WindowSeconds={Plugin.BraceWindowSeconds.Value:F1} " + $"PerAttackerOnce={Plugin.BracePerAttackerOnce.Value} NegateCounteredHit={Plugin.BraceNegateCounteredHit.Value} " + $"RiposteImpact={Plugin.BraceRiposteImpact.Value:F0} CueVolume={Plugin.BraceCueVolume.Value:F2} " + $"EnableTaunt={Plugin.BraceEnableTaunt.Value}"));
			Plugin.Log.LogMessage((object)$"[BRACE] ReceiveHit prefix attached: {BraceReceiveHit.Attached}");
			Pet activePet = p.ActivePet;
			if (activePet == null)
			{
				Plugin.Log.LogMessage((object)"[BRACE] no active pet.");
				return;
			}
			string speciesId = activePet.SpeciesId;
			SpecialAttackDef val = default(SpecialAttackDef);
			if (SpecialAttackTable.TryGet(SpecialAttacks.Table, speciesId, ref val))
			{
				AttackKind kind = val.Kind;
				bool value = Plugin.EnableBrace.Value;
				CompanionAnchor anchor = ((Companion)activePet).Anchor;
				BraceRoute val2 = SpecialAttackTable.RouteBrace(kind, value, anchor != null && anchor.HasLiveAnchor);
				Plugin.Log.LogMessage((object)(string.Format("[BRACE] '{0}': kind={1} route={2} statuses='{3}' ", speciesId, val.Kind, val2, val.StatusEffectId ?? "none") + $"riposte x{val.DamageMultiplier:F2} cooldown={val.CooldownSeconds:F0}s buildup={val.BuildupPercent:F0}"));
				PetSimulation sim = activePet.Sim;
				int num = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue) ?? (-1);
				Plugin.Log.LogMessage((object)((val.TauntMinSeconds > 0f || val.TauntMaxSeconds > 0f) ? ($"[BRACE] taunt axis: {val.TauntMinSeconds:F1}s at loyalty 0 → {val.TauntMaxSeconds:F1}s at 100; " + $"loyalty {num} → {SpecialAttackTable.TauntSeconds(val, (num >= 0) ? num : 0):F1}s") : ("[BRACE] '" + speciesId + "' has no taunt axis — its Hunt as One never taunts.")));
			}
			else
			{
				Plugin.Log.LogMessage((object)("[BRACE] '" + speciesId + "' has no SpeciesSpecialAttacks row."));
			}
			Plugin.Log.LogMessage((object)("[BRACE] window: " + Forensics));
			Plugin.Log.LogMessage((object)("[BRACE] last taunt: " + _lastTaunt));
		}
	}
	[HarmonyPatch(typeof(Character), "ReceiveHit", new Type[]
	{
		typeof(Object),
		typeof(DamageList),
		typeof(Vector3),
		typeof(Vector3),
		typeof(float),
		typeof(float),
		typeof(Character),
		typeof(float),
		typeof(bool)
	})]
	internal static class BraceReceiveHit
	{
		internal static bool Attached;

		[HarmonyPrefix]
		private static bool Prefix(Character __instance, ref DamageList __result, Object _damageSource, DamageList _damage, Character _dealerChar)
		{
			if (!BraceDriver.Active)
			{
				return true;
			}
			if (!CompanionAnchor.IsAnchor(__instance))
			{
				return true;
			}
			Plugin instance = Plugin.Instance;
			object obj;
			if (instance == null)
			{
				obj = null;
			}
			else
			{
				Pet activePet = instance.ActivePet;
				if (activePet == null)
				{
					obj = null;
				}
				else
				{
					CompanionAnchor anchor = ((Companion)activePet).Anchor;
					obj = ((anchor != null) ? anchor.Current : null);
				}
			}
			Character val = (Character)obj;
			if ((Object)(object)val == (Object)null || (Object)(object)__instance != (Object)(object)val)
			{
				return true;
			}
			if (!BraceDriver.OnAnchorHit(_dealerChar, _damageSource, val))
			{
				return true;
			}
			DamageList val2 = _damage.Clone();
			for (int i = 0; i < val2.Count; i++)
			{
				val2[i].Damage = 0f;
			}
			__result = val2;
			return false;
		}
	}
	internal static class BuffFoodTable
	{
		private static readonly TableLoader<BuffFoodEntry> _loader = new TableLoader<BuffFoodEntry>("BuffFoods.json", "[BUFFFOOD]", "species buff food", "species buff foods", (Func<string, Action<string>, Dictionary<string, BuffFoodEntry>>)BuffFoods.Parse, (Func<Dictionary<string, BuffFoodEntry>, Dictionary<string, BuffFoodEntry>, Dictionary<string, BuffFoodEntry>>)BuffFoods.Merge, "replaces per-species", (Func<Dictionary<string, BuffFoodEntry>, Action<string>, Dictionary<string, BuffFoodEntry>>)null, (Func<Dictionary<string, BuffFoodEntry>, string>)null, "");

		private static readonly DataAxis<Dictionary<string, BuffFoodEntry>> _axis = BwAxis.Table(_loader, "[BUFFFOOD]", Validate);

		internal static Dictionary<string, BuffFoodEntry> Table => _axis.Table;

		internal static void Init()
		{
			_axis.Init();
		}

		internal static void Reload()
		{
			_axis.Reload();
		}

		internal static BuffFoodDef Match(string speciesId, int itemId, string itemName)
		{
			if (!Plugin.EnableBuffFoods.Value)
			{
				return null;
			}
			BuffFoodEntry val = BuffFoods.Resolve(Table, speciesId);
			return BuffFoods.Match(val, itemId, itemName);
		}

		internal static BuffFoodDef ActiveDef(Pet pet)
		{
			if (!Plugin.EnableBuffFoods.Value)
			{
				return null;
			}
			PetSave val = pet?.State;
			if (val == null || string.IsNullOrEmpty(val.BuffFoodKey) || val.BuffFoodSecondsLeft <= 0.0)
			{
				return null;
			}
			BuffFoodEntry val2 = BuffFoods.Resolve(Table, pet.SpeciesId);
			return BuffFoods.DefFor(val2, val.BuffFoodKey);
		}

		internal static float DamageFactor(Pet pet, LoyaltyTier tier)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			return BuffFoods.DamageFactor(ActiveDef(pet), tier, (pet?.State?.BuffFoodSecondsLeft).GetValueOrDefault());
		}

		internal static float DecayFraction(Pet pet, LoyaltyTier tier)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			return BuffFoods.DecayFraction(ActiveDef(pet), tier, (pet?.State?.BuffFoodSecondsLeft).GetValueOrDefault());
		}

		internal static string DisplayName(BuffFoodDef def)
		{
			if (def == null)
			{
				return "";
			}
			if (def.ItemId.HasValue)
			{
				ResourcesPrefabManager instance = ResourcesPrefabManager.Instance;
				Item val = ((instance != null) ? instance.GetItemPrefab(def.ItemId.Value) : null);
				if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(val.Name))
				{
					return val.Name;
				}
			}
			return def.Key;
		}

		internal static string StatusSummary(Pet pet)
		{
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: 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_00cc: Unknown result type (might be due to invalid IL or missing references)
			if (pet?.State == null)
			{
				return "no pet state.";
			}
			PetSave state = pet.State;
			if (string.IsNullOrEmpty(state.BuffFoodKey) || state.BuffFoodSecondsLeft <= 0.0)
			{
				return "no buff running" + (Plugin.EnableBuffFoods.Value ? "." : " (EnableBuffFoods OFF).");
			}
			BuffFoodEntry val = BuffFoods.Resolve(Table, pet.SpeciesId);
			BuffFoodDef val2 = BuffFoods.DefFor(val, state.BuffFoodKey);
			if (val2 == null)
			{
				return $"'{state.BuffFoodKey}' — KEY NOT IN TABLE (override removed it?): {state.BuffFoodSecondsLeft:F0}s left, ticking but inert.";
			}
			LoyaltyTier loyalty = pet.Sim.Status().Loyalty;
			return "'" + DisplayName(val2) + "' " + BuffFoods.Describe(val2, loyalty, state.BuffFoodSecondsLeft) + (Plugin.EnableBuffFoods.Value ? "" : " (EnableBuffFoods OFF — inert)");
		}

		private static void Validate()
		{
			//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d6: Invalid comparison between Unknown and I4
			TableValidator.CheckItemTable("[BUFFFOOD]", "BuffFoods.json", "entry", ItemRefs(), out var _, out var misses);
			int num = 0;
			foreach (BuffFoodEntry value in Table.Values)
			{
				foreach (BuffFoodDef food in value.Foods)
				{
					num++;
					int num2;
					if (food.ItemId.HasValue)
					{
						ResourcesPrefabManager instance = ResourcesPrefabManager.Instance;
						num2 = (((Object)(object)((instance != null) ? instance.GetItemPrefab(food.ItemId.Value) : null) != (Object)null) ? 1 : 0);
					}
					else
					{
						num2 = 1;
					}
					bool flag = (byte)num2 != 0;
					Plugin.Log.LogMessage((object)("[BUFFFOOD] '" + value.Species + "': '" + food.Key + "' (" + (((int)food.Kind == 1) ? "decay rider" : "damage") + ", " + $"+{food.PercentPerLevel:F0}%/level, " + (food.DurationSeconds.HasValue ? $"{food.DurationSeconds.Value:F0}s" : "one hunger-day") + ", " + (flag ? "armed" : "INERT — item not in registry") + ")."));
				}
			}
			int num3 = ReportDualListed();
			Plugin.Log.LogMessage((object)($"[BUFFFOOD] boot check: {num} buff food(s) across {Table.Count} species, " + $"{misses} unresolved item key(s), {num3} dual-listed row(s) (meal + buff), " + $"EnableBuffFoods={Plugin.EnableBuffFoods.Value}."));
		}

		private static int ReportDualListed()
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			int num = 0;
			foreach (BuffFoodEntry value in Table.Values)
			{
				IReadOnlyList<FoodEntry> readOnlyList = PetDietTable.Resolve(value.Species);
				foreach (DualListedFood item in FeedPrecedence.DualListed(value, readOnlyList, (Func<BuffFoodDef, IReadOnlyCollection<string>>)CategoriesOf))
				{
					DualListedFood current2 = item;
					num++;
					Plugin.Log.LogMessage((object)("[BUFFFOOD] '" + ((DualListedFood)(ref current2)).Species + "': buff food '" + ((DualListedFood)(ref current2)).BuffKey + "' is ALSO a diet food ('" + ((DualListedFood)(ref current2)).DietKey + "'" + (((DualListedFood)(ref current2)).ByCategory ? " — a food-CATEGORY row" : "") + ") — DUAL-LISTED: feeding it runs the meal AND applies the buff, one item consumed (intended; the diet and buff axes are orthogonal since 2026-07-29)."));
				}
			}
			return num;
		}

		private static IReadOnlyCollection<string> CategoriesOf(BuffFoodDef def)
		{
			object obj;
			if (!def.ItemId.HasValue)
			{
				if (!ItemNameIndex.TryResolveCatalog(def.Key, out var itemId, out var _))
				{
					obj = null;
				}
				else
				{
					ResourcesPrefabManager instance = ResourcesPrefabManager.Instance;
					obj = ((instance != null) ? instance.GetItemPrefab(itemId) : null);
				}
			}
			else
			{
				ResourcesPrefabManager instance2 = ResourcesPrefabManager.Instance;
				obj = ((instance2 != null) ? instance2.GetItemPrefab(def.ItemId.Value) : null);
			}
			Item val = (Item)obj;
			if (!((Object)(object)val != (Object)null))
			{
				return null;
			}
			return FoodCategoryTags.CategoriesOf(val);
		}

		private static IEnumerable<ItemRef> ItemRefs()
		{
			foreach (BuffFoodEntry value in Table.Values)
			{
				foreach (BuffFoodDef food in value.Foods)
				{
					yield return new ItemRef(food.ItemId, food.Key, $"buff food ItemID {food.ItemId}", "buff food '" + food.Key + "'");
				}
			}
		}

		internal static void BuffFoodDump(Plugin p)
		{
			//IL_0222: Unknown result type (might be due to invalid IL or missing references)
			//IL_0227: Unknown result type (might be due to invalid IL or missing references)
			//IL_022c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0126: Unknown result type (might be due to invalid IL or missing references)
			//IL_012c: Invalid comparison between Unknown and I4
			//IL_02a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_02be: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ca: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Log.LogMessage((object)$"[BUFFFOOD] flags: EnableBuffFoods={Plugin.EnableBuffFoods.Value}");
			Plugin.Log.LogMessage((object)$"[BUFFFOOD] ── table ({Table.Count} species; 'reloadbufffoods' re-reads the override) ──");
			foreach (KeyValuePair<string, BuffFoodEntry> item in Table)
			{
				foreach (BuffFoodDef food in item.Value.Foods)
				{
					object obj;
					if (!food.ItemId.HasValue)
					{
						obj = "(by display name)";
					}
					else
					{
						ResourcesPrefabManager instance = ResourcesPrefabManager.Instance;
						Item val = ((instance != null) ? instance.GetItemPrefab(food.ItemId.Value) : null);
						obj = ((val != null) ? ("'" + val.Name + "'") : "UNKNOWN ITEM");
					}
					string text = (string)obj;
					Plugin.Log.LogMessage((object)("[BUFFFOOD]   '" + item.Key + "' <- '" + food.Key + "' " + text + ": " + string.Format("{0} +{1:F0}%/level, ", ((int)food.Kind == 1) ? "decay rider" : "damage", food.PercentPerLevel) + (food.DurationSeconds.HasValue ? $"{food.DurationSeconds.Value:F0}s" : "one hunger-day") + (string.IsNullOrEmpty(food.Toast) ? "" : (", toast=\"" + food.Toast + "\""))));
				}
			}
			Pet activePet = p.ActivePet;
			if (activePet?.State == null)
			{
				Plugin.Log.LogMessage((object)"[BUFFFOOD] no active pet.");
				return;
			}
			PetSave state = activePet.State;
			LoyaltyTier loyalty = activePet.Sim.Status().Loyalty;
			BuffFoodDef val2 = ActiveDef(activePet);
			if (string.IsNullOrEmpty(state.BuffFoodKey))
			{
				Plugin.Log.LogMessage((object)"[BUFFFOOD] active slot: none.");
			}
			else if (val2 == null)
			{
				Plugin.Log.LogMessage((object)($"[BUFFFOOD] active slot: '{state.BuffFoodKey}', {state.BuffFoodSecondsLeft:F0}s left — " + (Plugin.EnableBuffFoods.Value ? "KEY NOT IN TABLE / expired (override removed it?): ticking but inert." : "EnableBuffFoods OFF: ticking but inert.")));
			}
			else
			{
				Plugin.Log.LogMessage((object)("[BUFFFOOD] active slot: " + BuffFoods.Describe(val2, loyalty, state.BuffFoodSecondsLeft) + " " + $"(damage x{DamageFactor(activePet, loyalty):F3}, decay +{DecayFraction(activePet, loyalty) * 100f:F0}% of total)."));
			}
			Plugin.Log.LogMessage((object)("[BUFFFOOD] " + PetSystems.PowerSummary(p)));
		}
	}
	internal static class BwAxis
	{
		internal static DataAxis<Dictionary<string, T>> Table<T>(TableLoader<T> loader, string tag, Action validate = null)
		{
			return new DataAxis<Dictionary<string, T>>((ITableSource<Dictionary<string, T>>)(object)loader, (Action<Action>)delegate(Action h)
			{
				SL.OnPacksLoaded += h;
			}, (Func<bool>)(() => TableValidator.RegistryReady(tag)), validate);
		}

		internal static DataAxis<TTable> Composite<TTable>(Func<TTable> load, Action<TTable> announceReload, string tag, Action validate = null) where TTable : class
		{
			return new DataAxis<TTable>((ITableSource<TTable>)(object)new DelegateTableSource<TTable>(load, announceReload), (Action<Action>)delegate(Action h)
			{
				SL.OnPacksLoaded += h;
			}, (Func<bool>)(() => TableValidator.RegistryReady(tag)), validate);
		}
	}
	internal sealed class BwCompanionSettings : ICompanionSettings
	{
		public float AttackDamage => Plugin.AttackDamage.Value;

		public float AttackInterval => Plugin.AttackInterval.Value;

		public float AggroRange => Plugin.AggroRange.Value;

		public float AttackRange => Plugin.AttackRange.Value;

		public float CombatLeashDistance => Plugin.CombatLeashDistance.Value;

		public float DisengageRunHomeSeconds => Plugin.DisengageRunHomeSeconds.Value;

		public bool AttackVocals => Plugin.PetAttackVocals.Value;

		public bool AnchorInvisible => Plugin.AnchorInvisible.Value;

		public bool AnchorShowHealthBar => Plugin.AnchorShowHealthBar.Value;

		public bool AnchorLinkSummonSlot => Plugin.AnchorLinkSummonSlot.Value;

		public bool AnchorHideSummonIcon => Plugin.HideSummonIcon.Value;

		public float AnchorLeashDistance => Plugin.AnchorLeashDistance.Value;

		public float AnchorRespawnSeconds => Plugin.AnchorRespawnSeconds.Value;

		public bool AnchorDealsDamage => Plugin.AnchorDealsDamage.Value;

		public float CritHealthFraction => 0.2f;

		public float CritRearmFraction => 0.5f;

		public bool SpeciesVoice => Plugin.AnchorSpeciesVoice.Value;

		public AnchorGlueMode GlueMode => Plugin.GlueMode.Value;

		public float GlueOffsetBehind => Plugin.GlueOffsetBehind.Value;

		public bool UnifyTargets => Plugin.UnifyTargets.Value;

		public AnchorCollisionMode AnchorPlayerCollision => Plugin.AnchorPlayerCollision.Value;

		public string GhostPrefabName => "NewGhostOneHandedAlly";

		public bool AnchorEnabled => true;

		public bool SuppressLeashWarp => false;

		public BodilessAnchorPolicy BodilessAnchor => (BodilessAnchorPolicy)0;

		public float ModelYawOffset => Plugin.ModelYawOffset.Value;

		public float LoafDistanceMin => Plugin.LoafDistanceMin.Value;

		public float LoafDistanceMax => Plugin.LoafDistanceMax.Value;

		public float LoafRepickDistance => Plugin.LoafDistanceRepick.Value;

		public string LogTagSuffix => null;
	}
	internal static class BwConfig
	{
		internal static class Keys
		{
			public static ConfigEntry<KeyboardShortcut> TameKey;

			public static ConfigEntry<KeyboardShortcut> FeedKey;

			public static ConfigEntry<KeyboardShortcut> SelfTestKey;

			public static ConfigEntry<KeyboardShortcut> RecallKey;

			public static ConfigEntry<KeyboardShortcut> DiagKey;

			internal static void Bind(ConfigFile cfg)
			{
				//IL_0015: Unknown result type (might be due to invalid IL or missing references)
				//IL_003e: 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_0090: Unknown result type (might be due to invalid IL or missing references)
				//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
				TameKey = cfg.Bind<KeyboardShortcut>("Keys", "TameKey", new KeyboardShortcut((KeyCode)288, Array.Empty<KeyCode>()), "Tame the nearest wild creature.");
				FeedKey = cfg.Bind<KeyboardShortcut>("Keys", "FeedKey", new KeyboardShortcut((KeyCode)289, Array.Empty<KeyCode>()), "Feed the pet the first inventory item its diet accepts (same ruling as the right-click Feed action).");
				SelfTestKey = cfg.Bind<KeyboardShortcut>("Keys", "SelfTestKey", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Run the Core self-test now.");
				RecallKey = cfg.Bind<KeyboardShortcut>("Keys", "RecallKey", new KeyboardShortcut((KeyCode)290, Array.Empty<KeyCode>()), "Recall the pet to your feet right now (also re-forms a bodiless pet). The in-game remedy for any stuck/misplaced pet.");
				DiagKey = cfg.Bind<KeyboardShortcut>("Keys", "DiagKey", new KeyboardShortcut((KeyCode)293, Array.Empty<KeyCode>()), "Dump a '[DIAG]' snapshot (location, player vitals, combat state, nearby AI, pet status) to the log right now.");
				Keybinds.Claim("Beastwhispering", "tame the nearest creature", TameKey);
				Keybinds.Claim("Beastwhispering", "feed the pet", FeedKey);
				Keybinds.Claim("Beastwhispering", "run the self-test", SelfTestKey);
				Keybinds.Claim("Beastwhispering", "recall the pet", RecallKey);
				Keybinds.Claim("Beastwhispering", "dump a [DIAG] snapshot", DiagKey);
			}
		}

		internal static class Pet
		{
			public static ConfigEntry<float> TameRange;

			public static ConfigEntry<float> ModelYawOffset;

			public static ConfigEntry<string> SpeciesYawOffsets;

			public static ConfigEntry<float> FollowSpeed;

			public static ConfigEntry<float> MinFollowSpeed;

			public static ConfigEntry<bool> RestHealsPet;

			internal static void Bind(ConfigFile cfg)
			{
				TameRange = cfg.Bind<float>("Pet", "TameRange", 15f, "Search radius (m) to tame / re-form from.");
				ModelYawOffset = cfg.Bind<float>("Pet", "ModelYawOffset", 180f, "Degrees the creature model is rotated from the transform's forward (hyena ~180; tune if a species faces sideways).");
				SpeciesYawOffsets = cfg.Bind<string>("Pet", "SpeciesYawOffsets", "", "Per-species yaw overrides for rigs authored facing differently (bug 7: several donor rigs walk backward under the default). This is now the USER OVERRIDE layer only: the shipped defaults ride an EMBEDDED table (SpeciesYawOffsets.txt, generated by bwspecies from the manifests' yaw axis; F16), so the default here is empty and anything you set WINS per-key over that table (a live 'yaw <degrees>' tune wins over both, for the current session). 'Term=degrees' comma-separated, substring match against the species (e.g. Tuanosaur=0,Pearlbird=90). Tune live with the 'yaw <degrees>' dev command, then persist the value here.");
				FollowSpeed = cfg.Bind<float>("Pet", "FollowSpeed", 4.5f, "Pet base move speed (m/s) at full responsiveness. Lower to match a creature's natural gait; raise to keep up with a sprinting player. The creature's natural speed is logged as [PUPPET] on spawn.");
				MinFollowSpeed = cfg.Bind<float>("Pet", "MinFollowSpeed", 4.5f, "Species-stats mode only: while merely FOLLOWING you (not fighting), the pet never moves slower than this (m/s) so a naturally slow species can't lose a sprinting player. Combat chases run the true species speed. Matches the old FollowSpeed default.");
				RestHealsPet = cfg.Bind<bool>("Pet", "RestHealsPet", true, "When you finish a rest/sleep, heal the pet in proportion to the SLEEP hours (a full night = full heal; a short nap heals partially; a rest with no sleep heals nothing). Heals through the anchor's non-death HP seam, never past its loyalty-scaled max.");
			}
		}

		internal static class Follow
		{
			public static ConfigEntry<float> LoafDistanceMin;

			public static ConfigEntry<float> LoafDistanceMax;

			public static ConfigEntry<float> LoafDistanceRepick;

			internal static void Bind(ConfigFile cfg)
			{
				LoafDistanceMin = cfg.Bind<float>("Follow", "LoafDistanceMin", 2f, "Nearest distance (m) a following pet settles from you when it 'loafs' nearby instead of standing on you.");
				LoafDistanceMax = cfg.Bind<float>("Follow", "LoafDistanceMax", 4f, "Farthest distance (m) a following pet settles from you. 0 = loafing OFF: the pet settles on you (exact pre-feature behavior).");
				LoafDistanceRepick = cfg.Bind<float>("Follow", "LoafDistanceRepick", 3f, "How far (m) you must move from where the pet's loaf spot was chosen before it picks a new one. Larger = the pet re-settles less often (never orbits you); 0 falls back to the 3 m default.");
			}
		}

		internal static class Diag
		{
			public static ConfigEntry<float> DiagIntervalSeconds;

			public static ConfigEntry<float> DiagRadius;

			public static ConfigEntry<bool> CastDiagPatches;

			public static ConfigEntry<bool> MusicReconPatches;

			internal static void Bind(ConfigFile cfg)
			{
				DiagIntervalSeconds = cfg.Bind<float>("Diag", "DiagIntervalSeconds", 60f, "Auto-dump a '[DIAG]' snapshot on this cadence (real/unscaled seconds, keeps ticking through menus and pause) -- the breadcrumb trail a Steam Deck session you can't watch live still needs. 0 = auto-dump off; DiagKey/'diag' always works on demand.");
				DiagRadius = cfg.Bind<float>("Diag", "DiagRadius", 30f, "Radius (m) the 'diag' snapshot scans for nearby AI beyond whatever's already listed as engaged in combat.");
				CastDiagPatches = cfg.Bind<bool>("Diag", "CastDiagPatches", false, "Re-arm the Bug-19 cast-pipeline tracer (HuntAsOneCastDiag): 8 Harmony taps on hot vanilla paths (Item.TryQuickSlotUse/Use, Skill.HasAllRequirements/SkillStarted, Character.CastSpell/SendPerformSpellCastItem/CastDone, EffectSynchronizer.RegisterEffect) logging [CASTDIAG]. Bug 19 is CLOSED (HuntAsOnePlayer.cs) so this is OFF by default and the taps stay un-patched; only turn it on to re-investigate a native-cast regression. Patch application is decided in Awake, so a change needs a relaunch.");
				MusicReconPatches = cfg.Bind<bool>("Diag", "MusicReconPatches", false, "Re-arm the Bug-12/Bug-4 music PASSIVE taps (MusicRecon): 11 Harmony taps on GlobalAudioManager/GlobalCombatManager (combat start/end, music start/stop/queue, Update-gate, level-clear) logging the [MUSIC-TAP] timeline. OFF by default keeps GlobalAudioManager.Update et al. un-patched. The musiccheck/musicdump verb live-reads GAM state directly and still gives a full snapshot WITHOUT the taps (it degrades with a note) -- only the passive [MUSIC-TAP] timeline needs this on. Patch application is decided in Awake, so a change needs a relaunch.");
			}
		}

		internal static class Harvest
		{
			public static ConfigEntry<UnloadAssetsMode> UnloadUnusedAssetsMode;

			public static ConfigEntry<int> UnloadEveryNHarvests;

			public static ConfigEntry<bool> FlushTerrainAfterPurge;

			internal static void Bind(ConfigFile cfg)
			{
				UnloadUnusedAssetsMode = cfg.Bind<UnloadAssetsMode>("Harvest", "UnloadUnusedAssetsMode", (UnloadAssetsMode)1, "When to run the post-harvest Resources.UnloadUnusedAssets() purge (docs/terrain-hole-plan.md). Always = the pre-2026-07-09 behaviour (after every harvest — the render-hole trigger). EveryN = only every UnloadEveryNHarvests-th harvest (default; normal play rarely harvests, so it essentially never purges mid-session, while a heavy SpawnKit sweep still gets periodic relief). Off = never purge — donor textures/meshes stay resident until the next full zone change (trades the render-hole for donor-asset memory residency; only for chasing a different memory issue).");
				UnloadEveryNHarvests = cfg.Bind<int>("Harvest", "UnloadEveryNHarvests", 5, "With UnloadUnusedAssetsMode=EveryN: purge only on every Nth completed harvest (5,10,15,...). <=1 collapses to Always. Bigger = fewer purges = smaller render-hole risk but more resident donor assets between purges.");
				FlushTerrainAfterPurge = cfg.Bind<bool>("Harvest", "FlushTerrainAfterPurge", true, "After a purge, call Terrain.Flush() on every active-scene Unity terrain to re-arm any basemap/patch render data the purge may have evicted (the secondary render-hole theory). No-op when nothing needs rebuilding, so it is safe on.");
			}
		}

		internal static class Expedition
		{
			public static ConfigEntry<bool> CaptureOnSceneEntry;

			public static ConfigEntry<string> AutoWarmAtBoot;

			public static ConfigEntry<string> AlwaysWarmSpecies;

			internal static void Bind(ConfigFile cfg)
			{
				CaptureOnSceneEntry = cfg.Bind<bool>("Expedition", "CaptureOnSceneEntry", true, "MOVED to DonorKit ([Expedition] in cobalt.donorkit.cfg, via CompanionKit 2026-07-11 → DonorKit lane 5E-2) — this legacy key is read once to migrate a customized value, then ignored. Edit DonorKit's cfg instead. EXPIRES: this carrier bind exists only so one already-migrated install can be detected; DELETE it (and its migration lane) after the next release.");
				AutoWarmAtBoot = cfg.Bind<string>("Expedition", "AutoWarmAtBoot", "needed", "MOVED to DonorKit ([Expedition] in cobalt.donorkit.cfg, via CompanionKit 2026-07-11 → DonorKit lane 5E-2) — this legacy key is read once to migrate a customized value, then ignored. Edit DonorKit's cfg instead. EXPIRES: this carrier bind exists only so one already-migrated install can be detected; DELETE it (and its migration lane) after the next release.");
				AlwaysWarmSpecies = cfg.Bind<string>("Expedition", "AlwaysWarmSpecies", "", "MOVED to DonorKit ([Expedition] in cobalt.donorkit.cfg, via CompanionKit 2026-07-11 → DonorKit lane 5E-2) — this legacy key is read once to migrate a customized value, then ignored. Edit DonorKit's cfg instead. EXPIRES: this carrier bind exists only so one already-migrated install can be detected; DELETE it (and its migration lane) after the next release.");
			}

			internal static void MigrateToCompanionKit()
			{
				int num = 0;
				num += MigrateKey<bool>(CaptureOnSceneEntry, Expedition.CaptureOnSceneEntry);
				num += MigrateKey<string>(AutoWarmAtBoot, Expedition.AutoWarmAtBoot);
				num += MigrateKey<string>(AlwaysWarmSpecies, Expedition.AlwaysWarmSpecies);
				if (num > 0)
				{
					((ConfigEntryBase)Expedition.AutoWarmAtBoot).ConfigFile.Save();
					Plugin.Log.LogWarning((object)($"[EXPEDITION] config migration: {num} customized [Expedition] value(s) copied from " + "'" + ((ConfigEntryBase)CaptureOnSceneEntry).ConfigFile.ConfigFilePath + "' into '" + ((ConfigEntryBase)Expedition.AutoWarmAtBoot).ConfigFile.ConfigFilePath + "' (DonorKit owns this section now — lane 5E-2). The legacy keys are no longer read — edit DonorKit's cfg from here on."));
				}
			}

			private static int MigrateKey<T>(ConfigEntry<T> legacy, ConfigEntry<T> ck)
			{
				//IL_0022: Unknown result type (might be due to invalid IL or missing references)
				//IL_0027: Unknown result type (might be due to invalid IL or missing references)
				//IL_0028: Unknown result type (might be due to invalid IL or missing references)
				//IL_002a: Invalid comparison between Unknown and I4
				//IL_002c: Unknown result type (might be due to invalid IL or missing references)
				//IL_002e: Invalid comparison between Unknown and I4
				Outcome val = ExpeditionConfigMigration.Decide<T>(legacy.Value, (T)((ConfigEntryBase)legacy).DefaultValue, ck.Value, (T)((ConfigEntryBase)ck).DefaultValue);
				if ((int)val != 2)
				{
					if ((int)val == 3)
					{
						ck.Value = legacy.Value;
						return 1;
					}
					return 0;
				}
				Plugin.Log.LogWarning((object)("[EXPEDITION] config migration: '" + ((ConfigEntryBase)legacy).Definition.Key + "' is customized in BOTH " + $"Beastwhispering's legacy [Expedition] ('{legacy.Value}') and DonorKit's cfg ('{ck.Value}') — " + "DonorKit's wins; the legacy key is ignored."));
				return 0;
			}
		}

		internal static class Taming
		{
			public static ConfigEntry<bool> EnableTamingFoods;

			public static ConfigEntry<float> TameRadius;

			public static ConfigEntry<float> TameRecheckGraceMult;

			public static ConfigEntry<float> RecipeDropChance;

			internal static void Bind(ConfigFile cfg)
			{
				EnableTamingFoods = cfg.Bind<bool>("Taming", "EnableTamingFoods", true, "The player taming loop (docs/taming-food-plan.md): tamable creatures drop their taming-food recipe scroll, the cooked food used near a wild one tames it. OFF = no items/recipes registered, no drops, no use-hook (dev F7/'tame' still works).");
				TameRadius = cfg.Bind<float>("Taming", "TameRadius", 15f, "How close (game meters) a wild creature of the right species must be for USING its taming food to tame it. The spec says '15 ft' — Outward units are meters and 15 matches the dev TameRange default, so this errs friendly; tune down if it feels too generous.");
				TameRecheckGraceMult = cfg.Bind<float>("Taming", "TameRecheckGraceMult", 1.5f, "Slack on the SECOND target check. The chow is checked twice: once before the eat animation (at TameRadius) and once after it, and a skittish species can drift a few metres during those 1-2 seconds — which refused the tame outright (2026-07-25 live, Pearlbird). The effect-time re-check uses TameRadius x this. 1 = the old identical-radius behavior. FLOORED AT 1 in code — this is a grace, never a penalty (a value below 1 would let the animation play and then fail the narrower re-check on a creature that never moved).");
				RecipeDropChance = cfg.Bind<float>("Taming", "RecipeDropChance", 1f, "Global chance (0-1) a tamable creature drops its taming-food recipe scroll on death, multiplied by the per-species dropChance in TamingFoods.json. 1 = guaranteed (the current deliberately-generous default while the loop is play-tested).");
			}
		}

		internal static class SelfTest
		{
			public static ConfigEntry<bool> RunSelfTestOnLoad;

			internal static void Bind(ConfigFile cfg)
			{
				RunSelfTestOnLoad = cfg.Bind<bool>("SelfTest", "RunSelfTestOnLoad", false, "Run the Core self-test on load.");
			}
		}

		internal static class Systems
		{
			public static ConfigEntry<int> InitialLoyalty;

			public static ConfigEntry<float> HungerSecondsPerDay;

			public static ConfigEntry<float> TempEscalateSeconds;

			public static ConfigEntry<float> TempRecoverSeconds;

			public static ConfigEntry<int> SpeciesDailyDecay;

			public static ConfigEntry<float> LoyaltyGainPercent;

			public static ConfigEntry<float> SimTickSeconds;

			public static ConfigEntry<float> CastWatchdogWarnSeconds;

			public static ConfigEntry<float> CastWatchdogClearSeconds;

			public static ConfigEntry<float> FeedHealAmount;

			public static ConfigEntry<bool> EnableFoodHealthRecovery;

			public static ConfigEntry<float> SatiationFraction;

			public static ConfigEntry<bool> EnablePassiveBuffs;

			public static ConfigEntry<bool> EnableBagPerk;

			public static ConfigEntry<bool> ShowPetStatusIcons;

			public static ConfigEntry<float> HungryIconFraction;

			public static ConfigEntry<bool> UseSpeciesStats;

			public static ConfigEntry<bool> PersistPetHealth;

			public static ConfigEntry<float> HealthLoyaltyFactor0;

			public static ConfigEntry<float> HealthLoyaltyFactor100;

			public static ConfigEntry<float> DamageLoyaltyFactor0;

			public static ConfigEntry<float> DamageLoyaltyFactor100;

			public static ConfigEntry<float> DefenseLoyaltyFactor0;

			public static ConfigEntry<float> DefenseLoyaltyFactor100;

			public static ConfigEntry<float> SpeedLoyaltyFactor0;

			public static ConfigEntry<float> SpeedLoyaltyFactor100;

			public static ConfigEntry<int> ReleaseConfirmLoyaltyThreshold;

			internal static void Bind(ConfigFile cfg)
			{
				InitialLoyalty = cfg.Bind<int>("Systems", "InitialLoyalty", 50, "Loyalty a freshly tamed pet starts with (0-100).");
				HungerSecondsPerDay = cfg.Bind<float>("Systems", "HungerSecondsPerDay", 1200f, "Game-seconds without feeding per 'day' of loyalty decay. 0 or negative DISABLES hunger and decay entirely — the pet then stays permanently feedable and never loses loyalty to hunger (a footgun, not a feature; almost always a typo).");
				TempEscalateSeconds = cfg.Bind<float>("Systems", "TempEscalateSeconds", 30f, "Sustained seconds out-of-band to worsen one comfort stage.");
				TempRecoverSeconds = cfg.Bind<float>("Systems", "TempRecoverSeconds", 15f, "Sustained seconds back-in-band to recover one comfort stage.");
				SpeciesDailyDecay = cfg.Bind<int>("Systems", "SpeciesDailyDecay", 15, "Loyalty lost per day without feeding.");
				LoyaltyGainPercent = cfg.Bind<float>("Systems", "LoyaltyGainPercent", 5f, "Percent of every POSITIVE loyalty gain (feeding, kill credit, first region crossing) that actually lands — the 'slow, hard-won bond' balance lever. 5 = a preferred meal is worth +0.5 instead of +10, so a bond takes roughly 20x as much care to build. Nothing is lost to rounding: the leftover fraction is BANKED per pet (saved with it) and the next gain adds onto it, so twenty preferred meals still deliver exactly the ten loyalty one unscaled meal used to. LOSSES are deliberately NOT scaled by this — hunger decay, the pet being hurt or downed, and abandonment all keep their full face value. 100 = the original (pre-2026-07-25) fast-bonding rates. Retunes live via reloadcfg; forensics: the 'gain' fragment in petstatus.");
				SimTickSeconds = cfg.Bind<float>("Systems", "SimTickSeconds", 2f, "How often (game-seconds) the pet systems advance.");
				CastWatchdogWarnSeconds = cfg.Bind<float>("Systems", "CastWatchdogWarnSeconds", 5f, "Log (once per cast) when the player's IsCasting has been open this many seconds — pure diagnostics, nothing is touched. Bug-20 history: the watchdog used to FORCE-CLEAR at 1s, which killed every legitimate long vanilla cast (taming-food eating, flint-and-steel campfires).");
				CastWatchdogClearSeconds = cfg.Bind<float>("Systems", "CastWatchdogClearSeconds", 30f, "Force-clear a wedged IsCasting after this many seconds (bug-16 class: the animation event never fires, so the flag would stay stuck FOREVER and silently block Hunt as One / other casts). Must be comfortably longer than the slowest legitimate vanilla cast. 0 = never auto-clear (the 'castclear' verb remains the manual remedy).");
				FeedHealAmount = cfg.Bind<float>("Systems", "FeedHealAmount", 10f, "Pet HP healed by a successful feed when the food's PetDiets.json entry has no 'heal' of its own.");
				EnableFoodHealthRecovery = cfg.Bind<bool>("Systems", "EnableFoodHealthRecovery", true, "Feeding the pet ALSO starts a vanilla-style Health Recovery regen on it (on top of the instant feed heal): level 1-5 per the food's PetDiets.json 'healthRecovery' (default 1; the vanilla player ladder — 0.2/0.25/0.3/0.4/0.5 HP per second for 600s). A taming food (species chow) always grants level 5, including the tame itself. A new feed REPLACES the running regen (fresh 600s, even at a lower level — vanilla semantics). OFF = feeding grants no regen and no regen HP is applied (a running clock still ticks down inert, so a live re-enable resumes it). Flip works live via reloadcfg; forensics: the [REGEN] log lines + petstatus.");
				SatiationFraction = cfg.Bind<float>("Systems", "SatiationFraction", 0.25f, "A pet fed within this fraction of its hunger 'day' (HungerSecondsPerDay, or the species override) is completely satiated and refuses ALL food. 0 = pets never refuse.");
				EnablePassiveBuffs = cfg.Bind<bool>("Systems", "EnablePassiveBuffs", true, "Grant the bond's passive buff to the PLAYER (per-species stat + percent in SpeciesBuffs.txt, scaled by the pet's loyalty tier: Gone=0 ... Devoted=4 levels).");
				EnableBagPerk = cfg.Bind<bool>("Systems", "EnableBagPerk", true, "Grant the bond's flat backpack-capacity gift to the PLAYER's equipped bag (BagCapacity lines in SpeciesBuffs.txt, stepped min..max over the loyalty tiers). Follows the BOND, not the body: a downed pet keeps carrying for you; release/abandonment/permanent death withdraw it. Flip works live via reloadcfg.");
				ShowPetStatusIcons = cfg.Bind<bool>("Systems", "ShowPetStatusIcons", true, "Show the pet's hunger/bond state as status-effect icons on the player HUD (indicator-only statuses; flip works live via reloadcfg).");
				HungryIconFraction = cfg.Bind<float>("Systems", "HungryIconFraction", 0.75f, "Hunger fraction (of the pet's hunger 'day') at which the Hungry icon appears; the Starving icon appears at a full day (1.0).");
				UseSpeciesStats = cfg.Bind<bool>("Systems", "UseSpeciesStats", true, "The pet keeps the tamed creature's OWN combat stats (docs/pet-stats-plan.md): resistances/protection/impact-res/barrier on the anchor, its natural per-type attack damage + impact, its max health, and its movement speed — all captured at tame time, saved with the pet, and re-tuned live by the loyalty factors below. OFF = the pre-feature config numbers (AnchorBaseHealth / AttackDamage / FollowSpeed) apply everywhere.");
				PersistPetHealth = cfg.Bind<bool>("Systems", "PersistPetHealth", true, "Persist the pet's current-HP FRACTION across loading screens and reloads. Fixes the bug where a fresh combat anchor healed to FULL on a zone transition that respawned it (e.g. the pet reads 150/150 in one town but the correct 53/150 in the field). OFF = the pre-fix behaviour (a re-formed/reloaded pet loads at full HP; the save column is written as full). A genuine downed-death still returns the pet at full either way. Flip works live via reloadcfg; forensics: the [PERSIST]/[STATS] log lines.");
				HealthLoyaltyFactor0 = cfg.Bind<float>("Systems", "HealthLoyaltyFactor0", 0.5f, "Pet max-HP multiplier at loyalty 0 (lerps linearly to HealthLoyaltyFactor100 at loyalty 100). Applies to the species HP when UseSpeciesStats, else to AnchorBaseHealth — the defaults reproduce the original 0.5x..1.5x curve.");
				HealthLoyaltyFactor100 = cfg.Bind<float>("Systems", "HealthLoyaltyFactor100", 1.5f, "Pet max-HP multiplier at loyalty 100.");
				DamageLoyaltyFactor0 = cfg.Bind<float>("Systems", "DamageLoyaltyFactor0", 1f, "Pet attack-damage (and impact) multiplier at loyalty 0. Default 1/1 = loyalty does not scale damage (responsiveness already does); raise the 100 end to make devotion hit harder.");
				DamageLoyaltyFactor100 = cfg.Bind<float>("Systems", "DamageLoyaltyFactor100", 1f, "Pet attack-damage (and impact) multiplier at loyalty 100.");
				DefenseLoyaltyFactor0 = cfg.Bind<float>("Systems", "DefenseLoyaltyFactor0", 1f, "Pet defense multiplier (resistances/protection/barrier/impact-res) at loyalty 0. Scaled resistances clamp at 100%.");
				DefenseLoyaltyFactor100 = cfg.Bind<float>("Systems", "DefenseLoyaltyFactor100", 1f, "Pet defense multiplier at loyalty 100.");
				SpeedLoyaltyFactor0 = cfg.Bind<float>("Systems", "SpeedLoyaltyFactor0", 1f, "Pet movement-speed multiplier at loyalty 0.");
				SpeedLoyaltyFactor100 = cfg.Bind<float>("Systems", "SpeedLoyaltyFactor100", 1f, "Pet movement-speed multiplier at loyalty 100.");
				ReleaseConfirmLoyaltyThreshold = cfg.Bind<int>("Systems", "ReleaseConfirmLoyaltyThreshold", 0, "Releasing a pet is IRREVERSIBLE (the bond ends and that pet's save file is deleted), so at or above this loyalty a release must be CONFIRMED: the first Release Pet cast — or the first 'release' verb — only warns and arms a 30s window; a second cast/call inside that window, or 'release confirm', actually does it. 0 (the default) = ALWAYS confirm; a NEGATIVE value disables the gate entirely for scripted runs. History: on 2026-07-27 a single unconfirmed 'release' destroyed a real loyalty-35 bond built over a whole session (docs/hunt-synergy-testplan.md V20) — a value you have to guess protects nobody, which is why the default guards every pet.");
			}
		}

		internal static class Temperature
		{
			public static ConfigEntry<bool> EnableTemperatureSystem;

			public static ConfigEntry<bool> EnableWeatherFoods;

			public static ConfigEntry<PetDeathMode> PetDeathModeConfig;

			public static ConfigEntry<float> SufferingDrainPerMinute;

			public static ConfigEntry<float> CriticalDrainPerMinute;

			public static ConfigEntry<float> UneasyDecayMult;

			public static ConfigEntry<float> SufferingDecayMult;

			internal static void Bind(ConfigFile cfg)
			{
				EnableTemperatureSystem = cfg.Bind<bool>("Temperature", "EnableTemperatureSystem", true, "Master switch for the pet temperature/comfort system (ambient sampling, discomfort stages, drain, death, blankets). OFF = exact pre-feature behavior: the pet always reads as comfortable.");
				EnableWeatherFoods = cfg.Bind<bool>("Temperature", "EnableWeatherFoods", true, "Feeding the pet a weather-resist consumable (warming/cooling potions, teas, water — WeatherFoods.json) grants it the player's hot/cold relief; all pets may drink water. OFF = feeding ignores the table entirely (a running drink buff keeps ticking but grants nothing, so a live re-enable resumes it).");
				PetDeathModeConfig = cfg.Bind<PetDeathMode>("Temperature", "PetDeathMode", (PetDeathMode)0, "What happens when temperature exposure drains the pet to zero: Permanent = it dies, bond deleted; KnockedOut = it collapses (the existing downed/re-form path); Disabled = it holds at 1 HP, suffering but unkillable by climate.");
				SufferingDrainPerMinute = cfg.Bind<float>("Temperature", "SufferingDrainPerMinute", 2f, "Pet HP drained per minute (as PERCENT of its max) while Suffering (2 steps out of band, sustained).");
				CriticalDrainPerMinute = cfg.Bind<float>("Temperature", "CriticalDrainPerMinute", 6f, "Pet HP drained per minute (percent of max) while Critical (3+ steps out, sustained). At zero the PetDeathMode above decides.");
				UneasyDecayMult = cfg.Bind<float>("Temperature", "UneasyDecayMult", 1.5f, "Daily loyalty-decay multiplier while the pet is Uneasy (1 step out of band).");
				SufferingDecayMult = cfg.Bind<float>("Temperature", "SufferingDecayMult", 2f, "Daily loyalty-decay multiplier while Suffering or Critical.");
			}
		}

		internal static class Combat
		{
			public static ConfigEntry<bool> EnableCombat;

			public static ConfigEntry<float> AttackDamage;

			public static ConfigEntry<float> AttackInterval;

			public static ConfigEntry<float> AggroRange;

			public static ConfigEntry<float> AttackRange;

			public static ConfigEntry<float> CombatLeashDistance;

			public static ConfigEntry<bool> EnableSpecialAttack;

			public static ConfigEntry<float> EngageRange;

			public static ConfigEntry<float> EngageConeDegrees;

			public static ConfigEntry<float> DisengageRunHomeSeconds;

			public static ConfigEntry<bool> PetAttackVocals;

			public static ConfigEntry<float> PetDamageScalePercent;

			public static ConfigEntry<string> SpeciesDamageScales;

			public static ConfigEntry<float> UngovernedPetDamagePercent;

			internal static void Bind(ConfigFile cfg)
			{
				EnableCombat = cfg.Bind<bool>("Combat", "EnableCombat", true, "Let the pet attack what you're fighting.");
				AttackDamage = cfg.Bind<float>("Combat", "AttackDamage", 25f, "Base damage per pet hit (then scaled by responsiveness).");
				AttackInterval = cfg.Bind<float>("Combat", "AttackInterval", 1.4f, "Seconds between pet attacks.");
				AggroRange = cfg.Bind<float>("Combat", "AggroRange", 12f, "How far the pet will engage an enemy you're fighting.");
				AttackRange = cfg.Bind<float>("Combat", "AttackRange", 2.6f, "Melee reach for a pet attack.");
				CombatLeashDistance = cfg.Bind<float>("Combat", "CombatLeashDistance", 40f, "While actively fighting, the pet's and anchor's leashes relax to this (m) so they can actually reach enemies engaged at range. The tighter follow leashes apply only out of combat (round-9 Finding 10: a 14-20m leash yanked the pet home right as it reached anything fought at a distance, forever).");
				EnableSpecialAttack = cfg.Bind<bool>("Combat", "EnableSpecialAttack", true, "Let the player fire the pet's 'Hunt as One' signature attack — cast from the quickslot skill (or the 'special' dev verb for headless testing; the old SpecialAttackKey bind was DELETED per Bug 26). Requires EnableCombat.");
				EngageRange = cfg.Bind<float>("Combat", "EngageRange", 20f, "How far (m) the Command Pet skill's ENGAGE scans for an enemy in front of you when you have no locked target (your locked target is honored at any range).");
				EngageConeDegrees = cfg.Bind<float>("Combat", "EngageConeDegrees", 120f, "Total width (degrees) of the 'in front of you' cone the ENGAGE scan uses when you have no locked target.");
				DisengageRunHomeSeconds = cfg.Bind<float>("Combat", "DisengageRunHomeSeconds", 12f, "Session-9 finding: when a fight ends (DISENGAGE order or the enemy dies) beyond the tight follow leash, the pet warp-TELEPORTED home on the next tick. For this many seconds after any fight ends, the relaxed CombatLeashDistance keeps applying so the pet visibly RUNS back; if it still hasn't made it home when the window closes, the normal leash warp fires as the stuck-backstop. 0 = the old instant-warp behavior.");
				PetDamageScalePercent = cfg.Bind<float>("Combat", "PetDamageScalePercent", 100f, "Scale every hit the PET itself deals, as a percent of its normal damage. 100 = unchanged. Covers the auto-attack, Hunt as One, the ranged bolt and For the Kill (they all multiply one number, CompanionCombat.BaseDamage), in both UseSpeciesStats modes. Does NOT touch the player's own synced bonus strike ([HuntAsOne] BonusDamage), knockback/impact, or the un-multiplied [Anchor] AnchorDealsDamage path (off by default). Clamped to 0-1000 (0 = a pet that deals nothing; the ceiling is a typo guard). Retune live with `reloadcfg`; forensics: `statdump`.");
				SpeciesDamageScales = cfg.Bind<string>("Combat", "SpeciesDamageScales", "", "Per-species overrides of PetDamageScalePercent, as 'Species=percent' pairs — e.g. 'Hyena=60, Pearlbird=25'. An exact species name wins; otherwise the LONGEST matching name does, so 'Hyena=60' also covers 'Armored Hyena' unless that has its own entry. Empty = every species uses the global. Malformed entries are skipped with a warning, never fatal.");
				UngovernedPetDamagePercent = cfg.Bind<float>("Combat", "UngovernedPetDamagePercent", 50f, "While the pet-bond's owner has NOT learned the Wild Unknown breakthrough, every hit the pet deals is scaled to this percent of its potential. 100 = no penalty (full damage even without the breakthrough). Composes multiplicatively with PetDamageScalePercent and covers all four pet damage paths (auto-attack, Hunt as One, the ranged bolt, For the Kill). Independent of [Skills] EnableWildUnknownGate. Clamped to 0-1000. Retune live with `reloadcfg`; forensics: `statdump`.");
			}

			internal static void BindVocals(ConfigFile cfg)
			{
				PetAttackVocals = cfg.Bind<bool>("Combat", "PetAttackVocals", true, "Play the species attack sound from the visible body when its bite animation fires. ON by default — session-verified as the pet's working attack voice (the anchor ghost has no sound manager to vocalize through — Bug 14). Toggle live with the `vocals` verb.");
			}
		}

		internal static class HuntAsOne
		{
			public static ConfigEntry<bool> EnableHuntAsOne;

			public static ConfigEntry<string> HuntAsOneMeleeAnimation;

			public static ConfigEntry<string> HuntAsOneBowAnimation;

			public static ConfigEntry<float> HuntAsOneBonusDamage;

			public static ConfigEntry<float> HuntAsOneBowShotDelay;

			public static ConfigEntry<bool> EnableFoodHexes;

			public static ConfigEntry<int> HexMealWindow;

			public static ConfigEntry<float> HexBuildupPercent;

			public static ConfigEntry<bool> EnableRangedSpecial;

			public static ConfigEntry<float> BoltMaxFlightSeconds;

			public static ConfigEntry<bool> HonestHits;

			public static ConfigEntry<bool> SyncSkillCooldownToPet;

			public static ConfigEntry<float> BaseSkillCooldownSeconds;

			internal static void Bind(ConfigFile cfg)
			{
				EnableHuntAsOne = cfg.Bind<bool>("HuntAsOne", "EnableHuntAsOne", true, "When the pet's special attack fires, ALSO play the player's own synced strike (animation + bonus damage) on the same target. Requires EnableSpecialAttack.");
				HuntAsOneMeleeAnimation = cfg.Bind<string>("HuntAsOne", "MeleeAnimation", "Probe", "Character.SpellCastType name for the player's flourish when a melee weapon (or none) is equipped. HuntAsOneCastSync stamps this (with CastModifier=Attack) onto the skill's own CastType/CastModifier fields every frame, so Outward's NATIVE cast pipeline plays a REAL weapon swing via AttackInput -- no CastSpell override needed (docs/bug19-native-cast-plan.md). 'Probe' is a Polearm weapon-skill anim; swap for any other SpellCastType name, no relaunch needed.");
				HuntAsOneBowAnimation = cfg.Bind<string>("HuntAsOne", "BowAnimation", "PowerShot", "Character.SpellCastType name for the player's flourish when a Bow is equipped (bows have no 'Probe' clip). Stamped with CastModifier=Immobilized (NEVER Attack for bow -- that's Bug 13 Round 1's AttackInput charge-path crash) -- same native-cast mechanism as MeleeAnimation. 'PowerShot' is the closest-named candidate for the real Sniper Shot skill's look (Outward has no literal 'SniperShot' entry); if it's wrong, try PierceShot/BloodShot/StrafingShot or fall back to 'ShootProjectile' (the plain bow-fire anim, confirmed generic) -- no relaunch needed.");
				HuntAsOneBonusDamage = cfg.Bind<float>("HuntAsOne", "BonusDamage", 20f, "Flat bonus Physical damage the player's synced strike deals to the pet's target (docs/taming-handoff.md Part 12's 'bonus Impact'). 0 = animation only, no damage.");
				HuntAsOneBowShotDelay = cfg.Bind<float>("HuntAsOne", "BowShotDelay", 0.5f, "Bug 13 fix (2026-07-04): seconds to wait after the bow's draw animation starts before InstantLoadWeapon()+ForceShoot() actually fire the real arrow. Decompiling RangeAttackSkill showed the REAL Sniper Shot skill fires these as TWO SEPARATE Animation Events baked into the draw clip's timeline (ShootProjectileCastInit near the start, ShootProjectileCast — the real ForceShoot() call — at the release frame), not back-to-back in the same frame. This flat delay approximates that Init/Cast gap since we have no animation-event hook of our own to hang the real timing off of -- tune to taste live (no relaunch needed) if the arrow fires before/after the visual draw completes.");
				EnableFoodHexes = cfg.Bind<bool>("HuntAsOne", "EnableFoodHexes", true, "Food-driven hex build-up (docs/food-hex-plan.md): a species with a FoodHexes.json entry inflicts hex BUILD-UP on its Hunt-as-One hit, chosen by the pet's last HexMealWindow mapped meals (same food stacks: 3x Oil = 3x the percent of Scorch). Gates BOTH meal recording and application — off = exact pre-feature behavior (the saved meal history is kept but frozen).");
				HexMealWindow = cfg.Bind<int>("HuntAsOne", "HexMealWindow", 3, $"How many of the pet's most recent MAPPED meals drive the hex mix. Unmapped foods are never recorded and never displace a mapped meal. Clamped 1..{20}; a per-species 'window' in FoodHexes.json overrides this.");
				HexBuildupPercent = cfg.Bind<float>("HuntAsOne", "HexBuildupPercent", 20f, "Hex build-up percent contributed per mapped meal (the game's gauge: a status procs at 100, spill-over kept, scaled by the target's resistances). A per-species 'buildUpPercent' in FoodHexes.json overrides this.");
			}

			internal static void BindRanged(ConfigFile cfg)
			{
				HonestHits = cfg.Bind<bool>("HuntAsOne", "HonestHits", true, "Hunt-as-One strikes are FAILABLE (docs/hunt-synergy-plan.md): the pet's hit and the player's synced strike are judged with the engine's own connect rules before dealing — a blocking target (frontal, 60°/80°-shield), a dodge-roll's i-frames, or an out-of-reach/behind-you target means that half deals NOTHING (a frontal block plays the real block thunk instead). OFF = the pre-feature guaranteed strikes; Synergy then counts every resolve as landed. Live via reloadcfg.");
				EnableRangedSpecial = cfg.Bind<bool>("HuntAsOne", "EnableRangedSpecial", true, "Ranged signature attacks (docs/mantis-bolt-plan.md): a species whose SpeciesSpecialAttacks.txt row says Kind=Ranged (Mantis Shrimp) fires its OWN captured projectile on Hunt-as-One, resolving damage/status/food-hexes when the bolt actually LANDS (an honest miss lands nothing; cooldown stands). Off = every Kind is treated as Melee — exact pre-feature behavior. Gates FIRING only: the projectile capture at tame/re-form always runs for Ranged rows, so a live reloadcfg re-enable works without a re-tame.");
				BoltMaxFlightSeconds = cfg.Bind<float>("HuntAsOne", "BoltMaxFlightSeconds", 6f, "Backstop ceiling (seconds) on waiting for a fired bolt's impact before ruling a timeout-miss. The projectile's own Lifespan expiry usually reports the miss first (the shorter of the two wins); this only guards a bolt destroyed mid-flight without reporting. Read live — reloadcfg applies it to the next shot.");
				SyncSkillCooldownToPet = cfg.Bind<bool>("HuntAsOne", "SyncSkillCooldownToPet", true, "Bug 30 — the ONE kill-switch for the conflicting-cooldowns fix. Hunt as One used to have TWO clocks: the SKILL's own cooldown (1s, and the only one the hotbar radial draws) and the PET SPECIAL's (SpeciesSpecialAttacks CooldownSeconds, 8-12s). The icon therefore said READY while the ability could not fire, and a press in that window spent stamina, swung the sword, burned the skill cooldown, and did nothing (Cobalt saw it 5x in one fight). ON = the skill advertises the active species' REAL cooldown, so the engine's own on-cooldown gate refuses an early press for free (no stamina, no swing) and the radial shows the true countdown; the pet then adopts the cooldown the engine actually committed, so your cooldown-REDUCTION gear shortens the pet's signature attack too (Cobalt's ruling). It also arms the up-front veto that greys the button when the pet has nothing to strike. OFF = exact pre-fix behaviour, and the base cooldown below is re-stamped back onto the skill. Live via reloadcfg, both ways.");
				BaseSkillCooldownSeconds = cfg.Bind<float>("HuntAsOne", "BaseSkillCooldownSeconds", 1f, "The Hunt-as-One skill's own action-economy cooldown (seconds) used when there is NO species cooldown to sync to — no pet, a species with no SpeciesSpecialAttacks row, or SyncSkillCooldownToPet=false. This is HuntAsOne.xml's original <Cooldown>1</Cooldown>: a double-press debounce, not a real gate. Outward has no global cooldown, so 0 = none.");
			}
		}

		internal static class Brace
		{
			public static ConfigEntry<bool> EnableBrace;

			public static ConfigEntry<float> WindowSeconds;

			public static ConfigEntry<bool> PerAttackerOnce;

			public static ConfigEntry<bool> NegateCounteredHit;

			public static ConfigEntry<float> RiposteImpact;

			public static ConfigEntry<float> CueVolume;

			public static ConfigEntry<bool> EnableTaunt;

			internal static void Bind(ConfigFile cfg)
			{
				EnableBrace = cfg.Bind<bool>("Brace", "EnableBrace", true, "Brace-kind signature attacks (the Armored Hyena's counterattack stance): Hunt-as-One on a Kind=Brace species plants the pet for WindowSeconds; every enemy blow that lands on it (melee, missile, AoE — vanilla-Brace coverage) is countered: the hit is negated and the attacker eats a heavy-impact riposte plus one of the row's statuses at random. OFF = a Brace row runs the ordinary melee strike — exact pre-feature behavior. Live via reloadcfg (an open window closes on the next tick).");
				WindowSeconds = cfg.Bind<float>("Brace", "WindowSeconds", 5f, "How long the braced window stays open (seconds). The cooldown that gates re-entry is the species' own SpeciesSpecialAttacks CooldownSeconds, exactly like every other signature attack.");
				PerAttackerOnce = cfg.Bind<bool>("Brace", "PerAttackerOnce", true, "Counter each unique attacker only ONCE per window (its later hits land normally on the pet's armor — a mob of three still gets three counters, but one fast attacker can't be stonewalled all window). false = counter EVERY eligible hit (closer to vanilla Brace, effectively brief invulnerability). Cobalt's A/B knob — live via reloadcfg.");
				NegateCounteredHit = cfg.Bind<bool>("Brace", "NegateCounteredHit", true, "A countered hit deals ZERO damage to the pet (the vanilla counter rule — the whole hit is consumed by the counter). false = the counter still riposts but the hit ALSO lands, for a softer tank.");
				RiposteImpact = cfg.Bind<float>("Brace", "RiposteImpact", 60f, "Impact (knockback) the riposte carries — the 'heavy impact damage' half of the counter; vanilla heavy attacks run ~50-80, enough to stagger most humanoids. The riposte's DAMAGE is the row's mult x BaseDamage.");
				CueVolume = cfg.Bind<float>("Brace", "CueVolume", 0.5f, "Volume (0..1) of the parry-window cue (SFX_SKILL_Brace), played ONCE at brace-enter. Cobalt's 2026-07-14 session-2 tuning: the original every-2s repeat read as loud/annoying — the repeat is gone and the single play defaults to half volume. 0 = no cue at all (the pet's growl still marks the stance opening). Live via reloadcfg.");
				EnableTaunt = cfg.Bind<bool>("Brace", "EnableTaunt", true, "The Hunt-as-One taunt (Cobalt's 2026-07-29 ruling — it used to fire from For the Kill instead): a species whose SpeciesSpecialAttacks.txt row carries the two taunt columns locks its CURRENT target's aggro onto the pet's anchor when it enters the stance — ONE provocation per cast, fired at Brace entry, and a provocation not a payload (it pins whether or not a counter ever lands). The duration is LOYALTY-SCALED: the row's min at loyalty 0 rising linearly to its max at 100 (the Armored Hyena ships 1s→5s), so a devoted tank holds a fight where a fraying one only interrupts it. Held by a 0.3s re-assert loop; released on expiry, death, or beyond [Combat] CombatLeashDistance. OFF = the stance still counters and riposts, it just never pulls aggro. Live via reloadcfg (an active taunt releases on its next tick). Forensics: `bracedump` (the axis + what it resolves to at this pet's loyalty) and `tauntdump` (the hold + the aggro census).");
			}
		}

		internal static class SkillEcho
		{
			public static ConfigEntry<bool> EnableSkillEcho;

			public static ConfigEntry<float> EchoDamageMult;

			public static ConfigEntry<float> EchoImpactMult;

			public static ConfigEntry<float> EchoWindupSeconds;

			public static ConfigEntry<float> EchoCooldownSeconds;

			public static ConfigEntry<float> EchoRangeMeters;

			public static ConfigEntry<bool> EchoWhilePassive;

			public static ConfigEntry<bool> CueOnPet;

			public static ConfigEntry<string> CueStatusName;

			public static ConfigEntry<float> CueSeconds;

			internal static void Bind(ConfigFile cfg)
			{
				EnableSkillEcho = cfg.Bind<bool>("SkillEcho", "EnableSkillEcho", true, "The skill-echo system: casting one of the classic weapon skills (Puncture, Moon Swipe, Pommel Counter & co — the fixed 10-skill roster, run `echodump` for the list) makes the active pet strike your target with a small echo attack. OFF = the cast hook bails instantly, exact pre-feature behavior. Live via reloadcfg.");
				EchoDamageMult = cfg.Bind<float>("SkillEcho", "EchoDamageMult", 1.25f, "Default echo damage as a multiplier of the pet's own BaseDamage (every roster skill, every species). A species' SkillEchoes.json entry can override it per skill. Live via reloadcfg.");
				EchoImpactMult = cfg.Bind<float>("SkillEcho", "EchoImpactMult", 1.25f, "Default echo impact (knockback) as a multiplier of the pet's own attack impact. Per-skill overridable like the damage default. Live via reloadcfg.");
				EchoWindupSeconds = cfg.Bind<float>("SkillEcho", "EchoWindupSeconds", 0.4f, "Seconds between the echo's attack animation starting and the hit resolving (the pet's brain-stripped body has no animation-event hit frame — the timer fakes one, same as the signature attack).");
				EchoCooldownSeconds = cfg.Bind<float>("SkillEcho", "EchoCooldownSeconds", 1f, "Minimum seconds between echoes — the anti-spam debounce for CHAINED different skills (each vanilla skill already gates its own re-cast). 0 = every roster cast echoes.");
				EchoRangeMeters = cfg.Bind<float>("SkillEcho", "EchoRangeMeters", 10f, "The pet must be within this many meters of the picked target or the echo skips (no magic hit landing on a locked target across the map). Generous by default — the pet is normally already in the fight.");
				EchoWhilePassive = cfg.Bind<bool>("SkillEcho", "EchoWhilePassive", false, "true = the echo fires even while the pet is in Disengage (passive) stance — one strike, then it stays passive. Default false per Cobalt's 2026-07-15 call: a disengaged pet stays fully passive, no echo, no cue.");
				CueOnPet = cfg.Bind<bool>("SkillEcho", "CueOnPet", true, "Where the red-lines cue VFX plays: true = on the pet's body as it strikes (Cobalt's 2026-07-15 call), false = on the player character.");
				CueStatusName = cfg.Bind<string>("SkillEcho", "CueStatusName", "Rage", "The status effect whose FX prefab is borrowed for the echo cue — the Enrage boon's red lines. The status is NEVER applied, only its visual is flashed. 'Rage' is the best-candidate IdentifierName for the Enrage boon (unverified offline — testplan V6); a name that doesn't resolve falls back through the built-in candidate ladder, then warns once and skips the cue (the echo itself is unaffected). Empty = no cue.");
				CueSeconds = cfg.Bind<float>("SkillEcho", "CueSeconds", 2f, "How long the cue VFX lives before it is destroyed (seconds). 0 = no cue.");
			}
		}

		internal static class Synergy
		{
			public static ConfigEntry<bool> EnableSynergy;

			public static ConfigEntry<float> PercentPerStack;

			public static ConfigEntry<int> MaxStacks;

			public static ConfigEntry<float> DurationSeconds;

			public static ConfigEntry<float> WindowSeconds;

			public static ConfigEntry<bool> RequireSameTarget;

			public static ConfigEntry<float> PlayerMeleeRangeMeters;

			public static ConfigEntry<float> PlayerMeleeConeDegrees;

			public static ConfigEntry<float> PetMeleeRangeMeters;

			public static ConfigEntry<bool> ObserverPatch;

			internal static void Bind(ConfigFile cfg)
			{
				EnableSynergy = cfg.Bind<bool>("Synergy", "EnableSynergy", true, "The Hunt-as-One Synergy buff (docs/hunt-synergy-plan.md): a cast whose pet strike AND player strike both LAND grants a stack of Synergy — +PercentPerStack% ALL damage for player and pet per stack, up to MaxStacks, shown as a stacking status on the HUD. OFF = no attempts, no grants, the multipliers converge to 1.0 and a leftover status clears on the next tick. Live via reloadcfg; forensics: `synergydump`.");
				PercentPerStack = cfg.Bind<float>("Synergy", "PercentPerStack", 0.5f, "ALL-damage bonus percent per Synergy stack, for BOTH the player (multiplier stat stack per damage type) and the pet (CompanionCombat damage fold). 4 stacks at 0.5 = +2%.");
				MaxStacks = cfg.Bind<int>("Synergy", "MaxStacks", 4, "Stack cap, enforced at grant time (the status family itself is uncapped so this retunes live). A grant at the cap still refreshes every running stack's clock.");
				DurationSeconds = cfg.Bind<float>("Synergy", "DurationSeconds", 60f, "Lifespan of the Synergy stacks. Every grant refreshes ALL stacks to this, so the set expires together. Applies to the NEXT grant/refresh after a reloadcfg.");
				WindowSeconds = cfg.Bind<float>("Synergy", "WindowSeconds", 6f, "How long after a Hunt-as-One cast the two halves may land and still count (covers the pet's windup + a bolt/arrow's flight). A half still pending when it closes reads TimedOut — no stack.");
				RequireSameTarget = cfg.Bind<bool>("Synergy", "RequireSameTarget", false, "When true, the player's landed hit must be on the PET's target to count. Default false: the auto-fired arrow legitimately strikes whatever it strikes in the pile.");
				PlayerMeleeRangeMeters = cfg.Bind<float>("Synergy", "PlayerMeleeRangeMeters", 3.5f, "Melee: how close (m) the player must be to the pet's target for the synced strike to CONNECT. Beyond it the strike whiffs — no damage, no stack.");
				PlayerMeleeConeDegrees = cfg.Bind<float>("Synergy", "PlayerMeleeConeDegrees", 100f, "Melee: total width (degrees) of the player's front cone the target must be inside for the synced strike to connect — you have to be facing the fight. 0 or 360 disables the facing check.");
				PetMeleeRangeMeters = cfg.Bind<float>("Synergy", "PetMeleeRangeMeters", 4.5f, "How close (m) the pet must be to its target at the windup's impact frame for its melee special to CONNECT (the fake-windup strike used to land from any distance). Ranged bolts judge at physical impact instead.");
				ObserverPatch = cfg.Bind<bool>("Synergy", "ObserverPatch", false, "Diagnostic (not a gate): a Character.HasHit postfix logging the player's REAL weapon connects ([SYNERGY] observer lines) while a synergy attempt is open — evidence to compare the judged verdict against the Probe swing's actual physics (plan risk R1). Patch application is decided in Awake, so a change needs a relaunch.");
			}
		}

		internal static class ForTheKill
		{
			public static ConfigEntry<bool> EnableForTheKill;

			public static ConfigEntry<float> CooldownSeconds;

			public static ConfigEntry<float> BaseDamageMult;

			public static ConfigEntry<float> DamagePerStackPercent;

			public static ConfigEntry<bool> EnableKillFavor;

			public static ConfigEntry<float> KillFavorDurationSeconds;

			internal static void Bind(ConfigFile cfg)
			{
				EnableForTheKill = cfg.Bind<bool>("ForTheKill", "EnableForTheKill", true, "The For the Kill skill (docs/hunt-synergy-plan.md): consume ALL Synergy stacks for one joint pet+player strike at BaseDamageMult x (1 + DamagePerStackPercent% per stack), plus the species' ForTheKill.json debuff. Gates the CAST BODY only — the skill stays learnable, so a live reloadcfg flip works both ways. Forensics: `ftkdump`; retune the debuff table live: config-override ForTheKill.json + `reloadftk`.");
				CooldownSeconds = cfg.Bind<float>("ForTheKill", "CooldownSeconds", 60f, "For the Kill cooldown in seconds (Cobalt's 2026-07-12 retune: 180 -> 15; 2026-07-16 retune: 15 -> 60). Stamped onto the skill prefab + every learned instance each sim tick and on reloadcfg, so a .cfg edit retunes the LIVE skill. EVERY cast burns the full cooldown (Cobalt's live session-1 ruling — the swing has already played, a wasted swing with a free cooldown reads as a bug): no companion, no target, zero stacks, dodged/blocked — all burn. Stacks are only consumed when the strike actually starts.");
				BaseDamageMult = cfg.Bind<float>("ForTheKill", "BaseDamageMult", 1.5f, "Base multiplier on the joint strike (applies to the pet's signature damage AND the player's synced bonus), before the per-stack scaling. A 0-stack cast still hits at this.");
				DamagePerStackPercent = cfg.Bind<float>("ForTheKill", "DamagePerStackPercent", 35f, "Extra damage percent per Synergy stack consumed: total = BaseDamageMult x (1 + this/100 x stacks). Defaults 1.5/35 -> x3.6 at 4 stacks.");
				EnableKillFavor = cfg.Bind<bool>("ForTheKill", "EnableKillFavor", true, "The killing-favor buff: when a For the Kill execute (pet's blow OR the player's synced strike) actually KILLS its target, the species' ForTheKill.json 'killBuff' entry grants the player a timed stat buff scaled by the pet's loyalty tier. Off = strike/debuff only, no buff check. Forensics: `killfavordump`.");
				KillFavorDurationSeconds = cfg.Bind<float>("ForTheKill", "KillFavorDurationSeconds", 300f, "How long the killing-favor buff lasts once granted, in seconds. Retunes live via reloadcfg (re-stamps the status prefab; a running buff's countdown updates on its next refresh).");
			}
		}

		internal static class Sigils
		{
			public static ConfigEntry<bool> EnableSigilSynergies;

			public static ConfigEntry<bool> EnablePetSigils;

			public static ConfigEntry<float> PetSigilScale;

			public static ConfigEntry<float> PetSigilCooldownSeconds;

			internal static void Bind(ConfigFile cfg)
			{
				EnableSigilSynergies = cfg.Bind<bool>("Sigils", "EnableSigilSynergies", true, "Pet ↔ sigil synergies (docs/sigil-synergy-plan.md): a species with a Sigils.json entry changes its Hunt-as-One hit while the PET stands inside a registered mage sigil (seed: Veaber in a Blood Sigil applies all seven hexes at 30% build-up, superseding the food mix). Gates the per-hit detection query AND the composition — off = exact pre-feature behavior. Retune live: config-override Sigils.json + `reloadsigils`; forensics: `sigildump`.");
				EnablePetSigils = cfg.Bind<bool>("Sigils", "EnablePetSigils", true, "Pet-cast sigils (docs/pet-sigil-testplan.md, SPIKE): the pet can drop its own 0.75x sigil circle (dev verb `petsigil <key>`; player-facing triggers come later). Gates the cast funnel only — the four cloned circle items still register at pack-load either way (SL templates can't be un-applied live), they just can never be spawned. Off = exact pre-feature behavior at cast time.");
				PetSigilScale = cfg.Bind<float>("Sigils", "PetSigilScale", 0.75f, "Uniform scale baked into the PET sigil circle prefabs at pack-load, relative to the vanilla circles (visual AND trigger-collider extent shrink together; every co-op client bakes its own prefab, so guests see the same size with no sync). Change needs a RELAUNCH to re-bake cleanly (the hook stores the original scale, so a reloadcfg re-apply is absolute, not compounding — but items already standing in the world keep the scale they spawned with).");
				PetSigilCooldownSeconds = cfg.Bind<float>("Sigils", "PetSigilCooldownSeconds", 60f, "Cooldown armed on the pet after a (non-dev) pet sigil cast, drained by the sim tick on the aggregate clock (the Bug-30 shape; `petsigil` bypasses and does not arm). Priced so the pet cannot keep a circle up permanently — keep it at or above the circle's lifespan.");
			}
		}

		internal static class Gear
		{
			public static ConfigEntry<bool> EnableGearEffects;

			internal static void Bind(ConfigFile cfg)
			{
				EnableGearEffects = cfg.Bind<bool>("Gear", "EnableGearEffects", true, "Equipped-gear -> pet effects (docs/pet-gear-plan.md): a worn item carrying a PetGear.json entry alters the active pet while equipped (seed: a Pearlbird Mask makes every loyalty GAIN worth 1.5x; other kinds buff the pet's HP/damage/defense/speed). Off = the player's equipment never touches the pet (no equipment reads) — exact pre-feature behavior. Retune live: config-override PetGear.json + `reloadgear`; forensics: `geardump`.");
			}
		}

		internal static class Gifts
		{
			public static ConfigEntry<bool> EnableGiftSkill;

			public static ConfigEntry<float> GiftCooldownSeconds;

			public static ConfigEntry<bool> EnableFeatherFletching;

			public static ConfigEntry<bool> FletchNameSuffix;

			internal static void Bind(ConfigFile cfg)
			{
				EnableGiftSkill = cfg.Bind<bool>("Gifts", "EnableGiftSkill", true, "The Pet Gift skill (docs/pet-gift-plan.md): cast it and the pet gives the player a species-defined gift from PetGifts.json — one DEFAULT drop whose probability is the remainder, explicit drops with loyalty-lerped chances (chance -> chanceAt100), and a nothing-chance. Gates the CAST BODY only — the skill stays registered/learnable, so a live reloadcfg flip works both ways. Retune live: config-override PetGifts.json + `reloadgifts`; forensics: `giftdump`.");
				GiftCooldownSeconds = cfg.Bind<float>("Gifts", "GiftCooldownSeconds", 600f, "Pet Gift skill cooldown in seconds (default 600 = 10 min). Stamped onto the skill prefab + every learned instance each sim tick and on `reloadcfg`, so a .cfg edit retunes the LIVE skill — no relaunch. A cast with NO companion refunds the cooldown; a \"nothing to give\" roll burns it (that is the gamble the nothing-chance defines).");
				EnableFeatherFletching = cfg.Bind<bool>("Gifts", "EnableFeatherFletching", true, "Feather fletching, v3 (docs/feather-fletch-plan.md): the Pearlbird gifts a QUALITY-TIERED feather (Tattered/Ruffled/Sleek/Resplendent = the pet's loyalty tier, stackable to 999 by quality); one feather + a stack of any FeatherFletch.json arrow (survival crafting, any stack size) ENCHANTS that stack IN PLACE with 'Pearl Fletching' at the quality's +10/20/30/40% of the arrow's own damage — arrows keep their ItemID/name/icon/effects, the enchantment glint + tooltip panel + save/sync are all vanilla, and differently-fletched same-arrow stacks never merge. Re-fletching is UPGRADE-ONLY (equal/lower refuses, feather kept). Gates item/enchantment/recipe REGISTRATION (boot-time) and the craft/stack patches (live); OFF with fletched arrows in a save = they load as plain arrows, no crash. Retune: config-override FeatherFletch.json + `reloadfletch` (existing rows only); forensics: `fletchdump`, test spawn: `givefeather [loyalty|quality] [qty]`.");
				FletchNameSuffix = cfg.Bind<bool>("Gifts", "FletchNameSuffix", true, "Fletched arrow stacks show their bonus in the DISPLAY NAME — \"Iron Arrow (+20%)\" — on top of the enchantment glint/tooltip. This is the only at-a-glance way to tell tiers apart in the inventory grid (the glint is the same generic mark for every enchantment). Names are not game-saved; the suffix is restamped at craft/split/load. Off = pure vanilla names, tiers distinguishable only via the tooltip's enchantment panel.");
			}
		}

		internal static class BuffFoods
		{
			public static ConfigEntry<bool> EnableBuffFoods;

			internal static void Bind(ConfigFile cfg)
			{
				EnableBuffFoods = cfg.Bind<bool>("BuffFoods", "EnableBuffFoods", true, "Feeding the pet a buff food (BuffFoods.json — e.g. Crystal Powder / Ambraine / Gaberry Wine for +damage, Dark Stone for a Decay rider) grants a TEMPORARY damage buff scaled by the pet's loyalty level. These are NOT meals: no satiety, no loyalty, no heal — one shared temp-buff slot (same item refreshes, a different one replaces). OFF = feeding ignores the table entirely (a running buff keeps ticking but grants nothing, so a live re-enable resumes it).");
			}
		}

		internal static class Scent
		{
			public static ConfigEntry<bool> EnableScentSense;

			public static ConfigEntry<float> SniffIntervalSeconds;

			public static ConfigEntry<bool> SkipEmptyGatherables;

			public static ConfigEntry<float> NorthYawOffsetDegrees;

			public static ConfigEntry<bool> IndicatePoint;

			public static ConfigEntry<float> PointSeconds;

			public static ConfigEntry<bool> IndicateStatusIcon;

			public static ConfigEntry<bool> IndicateToast;

			internal static void Bind(ConfigFile cfg)
			{
				EnableScentSense = cfg.Bind<bool>("Scent", "EnableScentSense", true, "Pet environment sense (docs/pet-scent-plan.md): a species with a PetSenses.json entry periodically noses out nearby spawns of its 'interesting items' (seed: Hyena → Unidentified Molepig den) and indicates the scent to the player. Off = no scan, no dispatch, the held scent clears — exact pre-feature behavior. Retune live: config-override PetSenses.json + `reloadscents`; forensics: `scentdump`, force a scan with `sniff`.");
				SniffIntervalSeconds = cfg.Bind<float>("Scent", "SniffIntervalSeconds", 5f, "Seconds between scent scans (one OverlapSphere at the pet per scan). Own cadence, deliberately decoupled from SimTickSeconds — the sniff query is bigger than anything on the sim tick.");
				SkipEmptyGatherables = cfg.Bind<bool>("Scent", "SkipEmptyGatherables", true, "Already-harvested gathering spots don't smell like anything (the game's own visual-empty condition: container empty + nothing left to gather). Turn off if a node type ever reads empty wrongly — `scentdump` shows the per-hit reject reason.");
				NorthYawOffsetDegrees = cfg.Bind<float>("Scent", "NorthYawOffsetDegrees", 0f, "Compass correction for the [SCENT] direction bucketing: degrees to rotate the world frame if the in-game compass's north turns out not to be world +Z. 0 until live evidence says otherwise.");
				IndicatePoint = cfg.Bind<bool>("Scent", "IndicatePoint", true, "Scent indication channel: on a fresh alert the pet stands still, turns to face the smelled spawn (the hunting-dog 'point') for PointSeconds, and barks its species vocal. Combat cancels the point instantly.");
				PointSeconds = cfg.Bind<float>("Scent", "PointSeconds", 3f, "How long (seconds) the pet holds the point toward a fresh scent.");
				IndicateStatusIcon = cfg.Bind<bool>("Scent", "IndicateStatusIcon", true, "Scent indication channel: a 'Scent Trail' buff icon on the player HUD while the pet holds a scent (clears when the nose loses it). Also needs [Systems] ShowPetStatusIcons.");
				IndicateToast = cfg.Bind<bool>("Scent", "IndicateToast", true, "Scent indication channel (caravanner spike, docs/caravanner-spike-plan.md): on a fresh CHARACTER-kind alert an on-screen toast names the sense and