Decompiled source of Beastwhispering v0.2.18
plugins/Beastwhispering.Core.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
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("0.2.18.0")] [assembly: AssemblyInformationalVersion("0.2.18+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")] [assembly: AssemblyProduct("Beastwhispering.Core")] [assembly: AssemblyTitle("Beastwhispering.Core")] [assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")] [assembly: AssemblyVersion("0.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 static class ArrowAdoption { public const double ArmToleranceSeconds = 0.05; public static bool FlightStartUnknown(double projectileShootTime) { return projectileShootTime <= 0.0; } public static bool ShouldAdopt(bool ownerMatch, double armTime, double projectileShootTime, double toleranceSeconds = 0.05) { if (!ownerMatch) { return false; } if (FlightStartUnknown(projectileShootTime)) { return true; } return projectileShootTime + toleranceSeconds >= armTime; } } public enum CounterVerdict { Inactive, Expired, AlreadyCountered, WindowSpent, 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 int MaxCounters { get; private set; } public void Begin(double now, double windowSeconds, int maxCounters = 0) { Open = true; ExpireAt = now + ((windowSeconds > 0.0) ? windowSeconds : 0.0); _countered.Clear(); CounterCount = 0; MaxCounters = ((maxCounters > 0) ? maxCounters : 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(); CounterCount = 0; MaxCounters = 0; } public CounterVerdict TryCounter(string attackerUid, double now, bool perAttackerOnce) { if (!Open) { return CounterVerdict.Inactive; } if (now >= ExpireAt) { return CounterVerdict.Expired; } if (MaxCounters > 0 && CounterCount >= MaxCounters) { return CounterVerdict.WindowSpent; } 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) * 0.5; } 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 StatusPetThirsty = 87071; public const int StatusPetDehydrated = 87072; public const int BlanketHeatingBlanket = 87100; public const int BlanketCoolingBlanket = 87101; public const int BlanketHeatingBlanketScroll = 87102; public const int BlanketCoolingBlanketScroll = 87103; 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 ArmorLeather = 87250; 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[71] { 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(87071, "status.pet-thirsty", "statusPresets", BwIdRegistry.EffectPreset, "Thirsty Companion"), new BwIdEntry(87072, "status.pet-dehydrated", "statusPresets", BwIdRegistry.EffectPreset, "Dehydrated Companion"), new BwIdEntry(87100, "blanket.heating-blanket", "blankets", BwIdRegistry.ItemId, "Heating Blanket"), new BwIdEntry(87101, "blanket.cooling-blanket", "blankets", BwIdRegistry.ItemId, "Cooling Blanket"), new BwIdEntry(87102, "blanket.heating-blanket-scroll", "blankets", BwIdRegistry.ItemId, "Recipe: Heating Blanket"), new BwIdEntry(87103, "blanket.cooling-blanket-scroll", "blankets", BwIdRegistry.ItemId, "Recipe: 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(87250, "armor.leather", "petArmor", BwIdRegistry.ItemId, "Pet Leather Armor"), 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 severed.", LoyaltyTier.Broken => "The communion is broken.", LoyaltyTier.Fraying => "The communion is fraying.", LoyaltyTier.Guarded => "The communion is guarded.", LoyaltyTier.Cautious => "The communion is cautious.", LoyaltyTier.Steady => "The communion is steady.", LoyaltyTier.Trusting => "The communion is trusting.", LoyaltyTier.Devoted => "The communion is devoted.", LoyaltyTier.Unshaken => "The communion is unshaken.", LoyaltyTier.Boundless => "The communion is boundless.", LoyaltyTier.Sworn => "The communion is sworn.", LoyaltyTier.Fierce => "The communion is fierce.", LoyaltyTier.Mythic => "The communion is mythic.", LoyaltyTier.Eternal => "The communion is eternal.", _ => "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); } } public enum ConvergePinMode { None, Planted, Converge } public struct ConvergePinInputs { public bool PinEnabled; public bool ConvergeEnabled; public bool HasPetBody; public bool StyleOpposite; public string? StyleName; public bool HasStation; public bool Planted; public bool TauntEnabled; public bool HasLiveTarget; public string? EnemyName; public float Dist; public float AttackRange; public float ConvergeReachMultiplier; public bool Guest; public bool HasLiveAnchor; public bool LockedOnAnchor; public string? LockedName; public float Now; public float LastPinAt; public float LastConvergePinAt; public float PlantedSeconds; public float PlantedCooldown; public float ConvergeSeconds; public float ConvergeCooldown; } public struct ConvergePinVerdict { public ConvergePinMode Mode; public string Why; public float Seconds; public float Reach; public string LockedOn; public bool Armed => Mode != ConvergePinMode.None; } public static class ConvergePin { public const float DefaultReachMultiplier = 3f; public const float DefaultConvergeSeconds = 1.5f; public const float DefaultConvergeCooldown = 5f; public const string GuestLockWording = "unknown (guest: lock state is master-side)"; public const string MovingToStation = "moving to station"; public static ConvergePinVerdict Should(in ConvergePinInputs i) { if (!i.PinEnabled) { return Held("off ([Combat] StationPin=false)"); } if (!i.HasPetBody) { return Held("no pet body"); } if (!i.StyleOpposite) { return Held("style " + i.StyleName + " (pin is Opposite-only)"); } if (!i.HasStation) { return Held("no station yet"); } if (!i.Planted) { if (!i.ConvergeEnabled) { return Held("moving to station"); } ConvergePinVerdict result = Tail(in i, ConvergePinMode.Converge, i.AttackRange * i.ConvergeReachMultiplier, i.ConvergeSeconds, i.ConvergeCooldown); if (result.Armed) { return result; } return Held("moving to station (converge: " + result.Why + ")"); } return Tail(in i, ConvergePinMode.Planted, i.AttackRange + 1f, i.PlantedSeconds, i.PlantedCooldown); } private static ConvergePinVerdict Tail(in ConvergePinInputs i, ConvergePinMode mode, float reach, float seconds, float cooldown) { if (!i.TauntEnabled) { return Held("[Brace] EnableTaunt is off"); } if (!i.HasLiveTarget) { return Held("no live target"); } if (i.Dist > reach) { return Held("target " + F1(i.Dist) + "m > reach " + F1(reach) + "m"); } if (!i.Guest && !i.HasLiveAnchor) { return Held("no live anchor"); } if (!i.Guest && i.LockedOnAnchor) { return Held("mob already locked on the bird"); } float num = cooldown - (i.Now - ((mode == ConvergePinMode.Converge) ? i.LastConvergePinAt : i.LastPinAt)); if (num > 0f) { return Held("cooldown (" + F1(num) + "s left)"); } string text = (i.Guest ? "unknown (guest: lock state is master-side)" : ((i.LockedName == null) ? "nothing" : ("'" + i.LockedName + "'"))); return new ConvergePinVerdict { Mode = mode, Why = "armed: '" + i.EnemyName + "' " + F1(i.Dist) + "m, locked on " + text, Seconds = seconds, Reach = reach, LockedOn = text }; } private static ConvergePinVerdict Held(string why) { return new ConvergePinVerdict { Mode = ConvergePinMode.None, Why = why }; } private static string F1(float v) { return v.ToString("F1", CultureInfo.InvariantCulture); } } public enum DevTameKeyPress { Tame, RefuseAndExplain, RefuseSilently } public static class DevTameKeyGate { public const string RefusalMessage = "The tame key is a DEV tool and is disabled: set [Taming] EnableDevTameKey = true in cobalt.beastwhispering.cfg to use it. Taming foods are unaffected, and the 'tame' dev verb still works."; public static DevTameKeyPress Decide(bool enabled, bool alreadyExplained) { if (enabled) { return DevTameKeyPress.Tame; } if (!alreadyExplained) { return DevTameKeyPress.RefuseAndExplain; } return DevTameKeyPress.RefuseSilently; } public static bool Explained(DevTameKeyPress press) { return press != DevTameKeyPress.Tame; } } public sealed class DotAuraSpec { public string Key; public int Slot; public string FxStatusName; public string Label; public string[] Statuses; public bool Matches(string statusName) { if (string.IsNullOrEmpty(statusName) || Statuses == null) { return false; } for (int i = 0; i < Statuses.Length; i++) { if (string.Equals(Statuses[i], statusName, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } } public static class DotAuras { public const string BurningKey = "aura.dotburning"; public const string PoisonKey = "aura.dotpoison"; public const string BleedKey = "aura.dotbleed"; public const string PlagueKey = "aura.dotplague"; public static readonly DotAuraSpec[] Roster = new DotAuraSpec[4] { new DotAuraSpec { Key = "aura.dotburning", Slot = 102, FxStatusName = "Burning", Label = "burning", Statuses = new string[3] { "Burning", "Burn", "Immolate" } }, new DotAuraSpec { Key = "aura.dotpoison", Slot = 103, FxStatusName = "Poisoned", Label = "poisoned", Statuses = new string[7] { "Poisoned", "Poisoned +", "Hallowed Marsh Poison Lvl1", "Hallowed Marsh Poison Lvl2", "SulphurPoison", "Food Poisoned", "Food Poisoned +" } }, new DotAuraSpec { Key = "aura.dotbleed", Slot = 104, FxStatusName = "Bleeding", Label = "bleeding", Statuses = new string[2] { "Bleeding", "Bleeding +" } }, new DotAuraSpec { Key = "aura.dotplague", Slot = 105, FxStatusName = "Plague", Label = "plagued", Statuses = new string[1] { "Plague" } } }; public static DotAuraSpec Find(string key) { if (string.IsNullOrEmpty(key)) { return null; } for (int i = 0; i < Roster.Length; i++) { if (string.Equals(Roster[i].Key, key, StringComparison.OrdinalIgnoreCase)) { return Roster[i]; } } return null; } public static List<string> ActiveKeys(IEnumerable<string> liveStatusNames, bool enabled) { List<string> list = new List<string>(); if (!enabled || liveStatusNames == null) { return list; } for (int i = 0; i < Roster.Length; i++) { DotAuraSpec dotAuraSpec = Roster[i]; foreach (string liveStatusName in liveStatusNames) { if (dotAuraSpec.Matches(liveStatusName)) { list.Add(dotAuraSpec.Key); break; } } } return list; } public static string Explain(IEnumerable<string> liveStatusNames, bool enabled, bool hasAnchor, bool proxied = false) { if (!enabled) { return "off: [DotAuras] Enable is false"; } if (!hasAnchor && !proxied) { return "idle: no live anchor to read statuses from and no proxied status set (bodiless bond, or a GUEST whose master streams no ck.proxy.status — old master build or no announced row)"; } string text = (proxied ? "proxied status set (ck.proxy.status)" : "anchor"); List<string> list = ActiveKeys(liveStatusNames, enabled: true); if (list.Count == 0) { return "clear: the " + text + " carries no covered DoT status"; } return "active: " + string.Join(", ", list.ToArray()) + (proxied ? " (proxied)" : ""); } } 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 ExecuteCreditWindow { private struct Entry { public double At; public int Stacks; } public const double DefaultWindowSeconds = 12.0; private const int PruneAtCount = 16; private readonly Dictionary<string, Entry> _armed = new Dictionary<string, Entry>(StringComparer.Ordinal); private readonly double _window; public double WindowSeconds => _window; public int Count => _armed.Count; public ExecuteCreditWindow(double windowSeconds = 12.0) { _window = ((windowSeconds > 0.0) ? windowSeconds : 12.0); } public void Arm(string uid, double now) { Arm(uid, now, 0); } public void Arm(string uid, double now, int stacks) { if (!string.IsNullOrEmpty(uid)) { if (_armed.Count >= 16) { Prune(now); } if (stacks < 0) { stacks = 0; } if (TryGetArmed(uid, now, out var stacks2) && stacks2 > stacks) { stacks = stacks2; } _armed[uid] = new Entry { At = now, Stacks = stacks }; } } public bool IsArmed(string uid, double now) { int stacks; return TryGetArmed(uid, now, out stacks); } public bool TryGetArmed(string uid, double now, out int stacks) { stacks = 0; if (string.IsNullOrEmpty(uid)) { return false; } if (!_armed.TryGetValue(uid, out var value)) { return false; } double num = now - value.At; if (num < 0.0 || num > _window) { return false; } stacks = value.Stacks; return true; } public void Clear() { _armed.Clear(); } private void Prune(double now) { List<string> list = null; foreach (KeyValuePair<string, Entry> item in _armed) { double num = now - item.Value.At; if (num < 0.0 || num > _window) { (list ?? (list = new List<string>())).Add(item.Key); } } if (list != null) { for (int i = 0; i < list.Count; i++) { _armed.Remove(list[i]); } } } } 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; } if (dt > 0.0) { _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 double Stretch { get; private set; } = 1.0; public double EffectiveSecondsPerDay => SecondsPerDay * Stretch; public HungerTimer(double secondsPerDay, double secondsSinceFed = 0.0, double stretch = 1.0) { Stretch = ((!(stretch > 0.0)) ? 1.0 : stretch); SecondsPerDay = secondsPerDay; SecondsSinceFed = ((!(secondsSinceFed > 0.0)) ? 0.0 : secondsSinceFed); _daysApplied = ((SecondsPerDay > 0.0) ? ((int)(SecondsSinceFed / EffectiveSecondsPerDay)) : 0); } public void Feed(double stretch = 1.0) { Stretch = ((!(stretch > 0.0)) ? 1.0 : stretch); SecondsSinceFed = 0.0; _daysApplied = 0; } public void Seed(double secondsSinceFed) { SecondsSinceFed = ((!(secondsSinceFed > 0.0)) ? 0.0 : secondsSinceFed); _daysApplied = ((SecondsPerDay > 0.0) ? ((int)(SecondsSinceFed / EffectiveSecondsPerDay)) : 0); } public int Tick(double dt) { if (dt <= 0.0) { return 0; } SecondsSinceFed += dt; if (SecondsPerDay <= 0.0) { return 0; } int num = (int)(SecondsSinceFed / EffectiveSecondsPerDay); 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.Guarded, Percent = 30, FlatX = 5, ItemId = 87202, EnchantmentId = 87222 }, new FeatherQuality { Quality = "Resplendent", Tier = LoyaltyTier.Steady, 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 int PlaceableAmount(int shotCount, int maxStack) { if (shotCount <= 0 || maxStack <= 0) { return 0; } if (shotCount >= maxStack) { return maxStack; } return shotCount; } 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 Cure = "cure"; public const string Relic = "relic"; public const string BuffFood = "bufffood"; public const string Heal = "heal"; public const string Meal = "meal"; public static readonly IReadOnlyList<string> Order = new string[5] { "cure", "relic", "bufffood", "heal", "meal" }; public static int IndexOf(string rung) { for (int i = 0; i < Order.Count; i++) { if (string.Equals(rung, Order[i], StringComparison.Ordinal)) { return i; } } return -1; } public static bool IsTerminal(string rung) { if (Order.Count > 0) { return string.Equals(rung, Order[Order.Count - 1], StringComparison.Ordinal); } return false; } public static bool VerdictIsAClaim(string rung, bool wouldConsume) { if (!IsTerminal(rung)) { return true; } return wouldConsume; } 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, MovementSpeed, AllResistances, CooldownReduction } public sealed class KillFavorDef { public KillFavorStat Stat; public float[] AmountByTier; public float PerStack; } 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 || def.AmountByTier.Length == 0 || tier == LoyaltyTier.Gone) { return 0f; } int num = Loyalty.PayoutLevel(tier); if (num <= 0) { return 0f; } float[] array = new float[def.AmountByTier.Length + 1]; array[0] = 0f; for (int i = 0; i < def.AmountByTier.Length; i++) { array[i + 1] = def.AmountByTier[i]; } return LoyaltyCurve.Sample(array, num, 8); } public static float Amount(KillFavorDef def, LoyaltyTier tier, int stacksSpent) { if (def == null || tier == LoyaltyTier.Gone) { return 0f; } if (def.PerStack > 0f) { if (stacksSpent > 0) { return def.PerStack * (float)stacksSpent; } return 0f; } return Amount(def, tier); } } public static class ExecuteHeal { public enum Verdict { AlreadyPaid, NothingToPay, GuestNoOp, NoAnchor, Pay } public static float Amount(int loyaltyPayoutLevel, int stacks, float perTierPerStack) { if (loyaltyPayoutLevel <= 0 || stacks <= 0 || perTierPerStack <= 0f) { return 0f; } return (float)((loyaltyPayoutLevel > 8) ? 8 : loyaltyPayoutLevel) * 0.5f * (float)stacks * perTierPerStack; } public static Verdict Decide(bool alreadyPaid, float heal, bool isGuest, bool hasAnchor) { if (alreadyPaid) { return Verdict.AlreadyPaid; } if (heal <= 0f) { return Verdict.NothingToPay; } if (isGuest) { return Verdict.GuestNoOp; } if (!hasAnchor) { return Verdict.NoAnchor; } return Verdict.Pay; } public static bool SettlesLedger(Verdict verdict, bool healApplied) { if (verdict != Verdict.NothingToPay) { return verdict == Verdict.Pay && healApplied; } return true; } } 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; float num = 0f; foreach (KeyValuePair<string, object> item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "stat": { if (item.Value is string text && EnumRead.TryName<KillFavorStat>(text.Trim(), out var value2)) { killFavorStat = value2; } else { f.Say($"killBuff 'stat' '{item.Value}' unknown " + "(valid: " + string.Join(", ", Enum.GetNames(typeof(KillFavorStat))) + ")."); } break; } case "amounts": if (item.Value is List<object> { Count: 4 } list) { array = new float[4]; for (int i = 0; i < 4; i++) { if (list[i] is double num2) { array[i] = (float)num2; continue; } array[i] = 0f; f.Say($"killBuff 'amounts'[{i}] is not a number — that tier grants 0."); } } else { f.Say("killBuff 'amounts' must be a 4-number array (payout bands 1–4) — ignored."); } break; case "perstack": { if (f.Num("perStack", item.Value, out var result, null, "ignored")) { if (result <= 0.0) { f.Say($"killBuff 'perStack' {result} must be > 0 — ignored."); } else { num = (float)result; } } break; } default: f.Say("unknown killBuff key '" + item.Key + "' (valid: stat, amounts, perStack)."); break; } } if (!killFavorStat.HasValue || (array == null && num <= 0f)) { f.Say("killBuff needs 'stat' plus either a 4-number 'amounts' or a positive 'perStack' — ignored (strike only)."); return null; } if (array != null && num > 0f) { f.Say("killBuff has BOTH 'amounts' and 'perStack' — author exactly one; ignored (strike only)."); return null; } return new KillFavorDef { Stat = killFavorStat.Value, AmountByTier = array, PerStack = num }; } 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 Hostility { public const double DefaultRecencySeconds = 1.0; public static readonly string[] FriendlyFactions = new string[3] { "Player", "Merchants", "NONE" }; public static readonly string[] AmbiguousFactions = new string[1] { "Merchants" }; public static bool IsAmbiguousFaction(string faction) { if (string.IsNullOrEmpty(faction)) { return false; } for (int i = 0; i < AmbiguousFactions.Length; i++) { if (string.Equals(AmbiguousFactions[i], faction, StringComparison.Ordinal)) { return true; } } return false; } public static bool IsFriendlyFaction(string faction) { return IsFriendlyFaction(faction, lockedByCompanionAi: false); } public static bool IsFriendlyFaction(string faction, bool lockedByCompanionAi) { if (string.IsNullOrEmpty(faction)) { return true; } for (int i = 0; i < FriendlyFactions.Length; i++) { if (string.Equals(FriendlyFactions[i], faction, StringComparison.Ordinal)) { if (lockedByCompanionAi) { return !IsAmbiguousFaction(faction); } return true; } } return false; } public static bool IsHostileTarget(bool isAI, bool isSelf, bool isOwnAnchor, string faction, bool aliveNow, double secondsSinceDeath, bool requireAlive, double recencySeconds = 1.0, bool lockedByCompanionAi = false) { if (!IsHostileIdentity(isAI, isSelf, isOwnAnchor, faction, lockedByCompanionAi)) { return false; } if (aliveNow) { return true; } if (requireAlive) { return false; } if (secondsSinceDeath > 0.0) { return secondsSinceDeath <= recencySeconds; } return false; } public static bool IsHostileIdentity(bool isAI, bool isSelf, bool isOwnAnchor, string faction, bool lockedByCompanionAi = false) { if (!isAI || isSelf || isOwnAnchor) { return false; } return !IsFriendlyFaction(faction, lockedByCompanionAi); } } 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 enum InfuseElement { None, Fire, Frost } public struct InfuseStatus { public InfuseElement Element; public double Expiry; } public static class InfuseRider { public const int InfuseFireSkillId = 8200103; public const int InfuseFrostSkillId = 8200102; public const float DefaultRiderFraction = 0.25f; public const float DefaultDurationSeconds = 180f; public static InfuseStatus None => new InfuseStatus { Element = InfuseElement.None, Expiry = 0.0 }; public static bool IsInfuseSkill(int skillId) { if (skillId != 8200103) { return skillId == 8200102; } return true; } public static InfuseElement ElementFor(int skillId) { return skillId switch { 8200103 => InfuseElement.Fire, 8200102 => InfuseElement.Frost, _ => InfuseElement.None, }; } public static string Token(InfuseElement element) { return element switch { InfuseElement.Fire => "Fire", InfuseElement.Frost => "Frost", _ => string.Empty, }; } public static InfuseStatus Cast(InfuseStatus current, int skillId, double now, float durationSeconds) { InfuseElement infuseElement = ElementFor(skillId); if (infuseElement == InfuseElement.None) { return current; } return new InfuseStatus { Element = infuseElement, Expiry = now + (double)((durationSeconds > 0f) ? durationSeconds : 0f) }; } public static InfuseStatus Retime(InfuseStatus current, double now, float secondsLeft) { if (current.Element == InfuseElement.None || current.Expiry <= now) { return current; } return new InfuseStatus { Element = current.Element, Expiry = now + (double)((secondsLeft > 0f) ? secondsLeft : 0f) }; } public static InfuseElement ActiveElement(InfuseStatus status, double now) { if (status.Element == InfuseElement.None || !(now < status.Expiry)) { return InfuseElement.None; } return status.Element; } public static float ActiveFraction(InfuseStatus status, double now, float fraction, bool enabled) { if (!enabled || fraction <= 0f) { return 0f; } if (ActiveElement(status, now) != InfuseElement.None) { return fraction; } return 0f; } public static string Explain(InfuseStatus status, double now, float fraction, bool enabled) { if (!enabled) { return "zeroed: [InfuseEcho] Enable is false"; } if (fraction <= 0f) { return "zeroed: [InfuseEcho] RiderFraction is 0 or negative"; } if (status.Element == InfuseElement.None) { return "zeroed: no infusion has been cast (the cast gate never armed one)"; } if (status.Expiry <= now) { return "zeroed: the infusion lapsed"; } return "active: " + status.Element.ToString() + " at fraction " + fraction.ToString("0.###", CultureInfo.InvariantCulture) + ", " + SecondsRemaining(status, now).ToString("0.#", CultureInfo.InvariantCulture) + "s left"; } public static double SecondsRemaining(InfuseStatus status, double now) { if (status.Element == InfuseElement.None || status.Expiry <= now) { return 0.0; } return status.Expiry - now; } } public static class ItemHeals { public const float MinHeal = 0.5f; public const float FullFraction = 0.999f; public const float MinLandedFraction = 0.25f; public const float AiSentinel = -99999f; public static float Quantity(bool isModifier, float quantity, float quantityOnAI, bool isAi, float maxHealth) { float num = ((quantityOnAI != -99999f && isAi) ? quantityOnAI : quantity); if (!isModifier) { return num; } return num * 0.01f * maxHealth; } public static float Total(IReadOnlyList<float> quantities) { if (quantities == null) { return 0f; } float num = 0f; for (int i = 0; i < quantities.Count; i++) { if (quantities[i] > 0f) { num += quantities[i]; } } return num; } public static bool CarriesHeal(bool enabled, float total) { if (enabled) { return total >= 0.5f; } return false; } public static bool WouldHeal(bool enabled, float total, bool hasAnchor, float current, float max, bool lowerRungClaims) { if (!CarriesHeal(enabled, total)) { return false; } if (!hasAnchor || max <= 0f) { return false; } if (current / max >= 0.999f) { return false; } float num = Landed(total, current, max); if (num < 0.5f || num < total * 0.25f) { return false; } return !lowerRungClaims; } public static float Landed(float total, float current, float max) { if (total <= 0f || max <= 0f) { return 0f; } float num = max - current; if (num <= 0f) { return 0f; } if (!(total < num)) { return num; } return total; } public static bool RefusedForWaste(bool enabled, float total, bool hasAnchor, float current, float max) { if (!CarriesHeal(enabled, total)) { return false; } if (!hasAnchor || max <= 0f) { return false; } if (current / max >= 0.999f) { return true; } float num = Landed(total, current, max); if (!(num < 0.5f)) { return num < total * 0.25f; } return true; } public static string Explain(bool enabled, float total, bool hasAnchor, float current, float max, bool lowerRungClaims) { if (!enabled) { return "off: [Systems] EnableItemHealing is false"; } if (total < 0.5f) { if (!(total <= 0f)) { return string.Format(CultureInfo.InvariantCulture, "inert: this item's own health restore is {0:0.##} HP — below the {1} HP floor", total, 0.5f); } return "inert: this item carries no health restore of its own"; } if (!hasAnchor || max <= 0f) { return "idle: no live anchor to heal (bodiless bond, a DOWNED pet, or a GUEST — the anchor lives on the master, so a healing item can only be applied there)"; } float num = current / max; if (num >= 0.999f) { return string.Format(CultureInfo.InvariantCulture, "nothing to heal: the pet is at {0:P0} health — the offer is refused WITHOUT consuming", num); } float num2 = Landed(total, current, max); if (num2 < 0.5f || num2 < total * 0.25f) { return string.Format(CultureInfo.InvariantCulture, "too little to heal: only {0:0.##} of the item's {1:0.##} HP could land ({2:F0}/{3:F0}) — refused WITHOUT consuming, so a smaller remedy can be spent instead", num2, total, current, max); } if (lowerRungClaims) { return "not ours: a lower rung claims this offer (a diet food that also heals is fed as a MEAL and heals by its authored per-food value, not by the item's)"; } return string.Format(CultureInfo.InvariantCulture, "healing: {0:0.##} HP from the item's own vanilla effect ({1:0.##} of it can land)", total, num2); } public static string Toast(string species, float healed) { return string.Format(CultureInfo.InvariantCulture, "{0} laps up the draught and steadies ({1:0} health).", string.IsNullOrEmpty(species) ? "Your pet" : species, Math.Round(healed)); } } 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 static class LeapArc { public static double Height(double t, double apexHeight) { if (t <= 0.0 || t >= 1.0) { return 0.0; } return 4.0 * apexHeight * t * (1.0 - t); } public static void Sample(double sx, double sy, double sz, double ex, double ey, double ez, double apexHeight, double t, out double x, out double y, out double z) { if (t < 0.0) { t = 0.0; } if (t > 1.0) { t = 1.0; } x = sx + (ex - sx) * t; y = sy + (ey - sy) * t + Height(t, apexHeight); z = sz + (ez - sz) * t; } } public static class LeapGate { public const double MinLeapMeters = 1.0; public static bool ShouldLeap(bool enabled, bool hasTarget, double distanceMeters, double leapRangeMeters) { if (!enabled || !hasTarget) { return false; } if (leapRangeMeters <= 0.0) { return false; } if (distanceMeters >= 1.0) { return distanceMeters <= leapRangeMeters; } return false; } } public struct LeapMsg { public bool PlayerLeaps; public double PlayerStartX; public double PlayerStartY; public double PlayerStartZ; public double PlayerEndX; public double PlayerEndY; public double PlayerEndZ; public bool PetLeaps; public double PetStartX; public double PetStartY; public double PetStartZ; public double PetEndX; public double PetEndY; public double PetEndZ; } public static class LeapProtocol { private const string Ver = "v1"; public static string Build(in LeapMsg m) { return string.Join("|", "v1", m.PlayerLeaps ? "1" : "0", Triple(m.PlayerStartX, m.PlayerStartY, m.PlayerStartZ), Triple(m.PlayerEndX, m.PlayerEndY, m.PlayerEndZ), m.PetLeaps ? "1" : "0", Triple(m.PetStartX, m.PetStartY, m.PetStartZ), Triple(m.PetEndX, m.PetEndY, m.PetEndZ)); } public static bool TryParse(string payload, out LeapMsg m) { m = default(LeapMsg); if (string.IsNullOrEmpty(payload)) { return false; } string[] array = payload.Split(new char[1] { '|' }); if (array.Length != 7 || array[0] != "v1") { return false; } if (TryFlag(array[1], out m.PlayerLeaps) && TryTriple(array[2], out m.PlayerStartX, out m.PlayerStartY, out m.PlayerStartZ) && TryTriple(array[3], out m.PlayerEndX, out m.PlayerEndY, out m.PlayerEndZ) && TryFlag(array[4], out m.PetLeaps) && TryTriple(array[5], out m.PetStartX, out m.PetStartY, out m.PetStartZ)) { return TryTriple(array[6], out m.PetEndX, out m.PetEndY, out m.PetEndZ); } return false; } private static string Triple(double x, double y, double z) { return x.ToString("0.##", CultureInfo.InvariantCulture) + "," + y.ToString("0.##", CultureInfo.InvariantCulture) + "," + z.ToString("0.##", CultureInfo.InvariantCulture); } private static bool TryFlag(string s, out bool v) { v = s == "1"; if (!(s == "1")) { return s == "0"; } return true; } private static bool TryTriple(string s, out double x, out double y, out double z) { return LeapNumbers.TryTriple(s, out x, out y, out z); } } internal static class LeapNumbers { public const double MaxAbsMeters = 100000.0; public static bool IsSane(double v) { if (!double.IsNaN(v) && !double.IsInfinity(v)) { return Math.Abs(v) < 100000.0; } return false; } public static bool TryTriple(string s, out double x, out double y, out double z) { x = (y = (z = 0.0)); if (string.IsNullOrEmpty(s)) { return false; } string[] array = s.Split(new char[1] { ',' }); if (array.Length != 3) { return false; } if (!double.TryParse(array[0], NumberStyles.Float, CultureInfo.InvariantCulture, out x) || !double.TryParse(array[1], NumberStyles.Float, CultureInfo.InvariantCulture, out y) || !double.TryParse(array[2], NumberStyles.Float, CultureInfo.InvariantCulture, out z)) { return false; } if (IsSane(x) && IsSane(y) && IsSane(z)) { return true; } x = (y = (z = 0.0)); return false; } } public static class StompProtocol { private const string Ver = "v1"; public static string Build(double x, double y, double z) { return "v1|" + x.ToString("0.##", CultureInfo.InvariantCulture) + "," + y.ToString("0.##", CultureInfo.InvariantCulture) + "," + z.ToString("0.##", CultureInfo.InvariantCulture); } public static bool TryParse(string payload, out double x, out double y, out double z) { x = (y = (z = 0.0)); if (string.IsNullOrEmpty(payload)) { return false; } string[] array = payload.Split(new char[1] { '|' }); if (array.Length != 2 || array[0] != "v1") { return false; } return LeapNumbers.TryTriple(array[1], out x, out y, out z); } } public enum LoyaltyTier { Gone, Broken, Fraying, Guarded, Cautious, Steady, Trusting, Devoted, Unshaken, Boundless, Sworn, Fierce, Mythic, Eternal } public enum LoyaltyEvent { FeedPreferred, FeedBondFood, DayWithoutFeeding, DayWithoutDrinking, PetCriticallyHurt, PetDowned, DefeatedNearbyEnemy, CrossedRegionBonded, SynergyGranted, ForTheKillExecuteKill } public static class Loyalty { public const int Min = 0; public const int Max = 250; public const int FractionCap = 100; public const int MaxPayoutLevel = 8; public const int LegacyMaxPayoutLevel = 4; public const float PayoutUnit = 0.5f; public const int CrossedRegionPoints = 5; public const int CrossedRegionFace = 100; public const double DefaultGainScale = 0.05; public static LoyaltyTier Tier(int value) { return LoyaltyLadder.TierFor(value); } public static int PayoutLevel(LoyaltyTier tier) { return LoyaltyLadder.Payout(tier); } public static double LadderFraction(int value) { return (double)PayoutLevel(Tier(value)) / 8.0; } public static int DeltaFor(LoyaltyEvent ev, int speciesDailyDecay = 15) { return ev switch { LoyaltyEvent.FeedPreferred => 10, LoyaltyEvent.FeedBondFood => 20, LoyaltyEvent.DayWithoutFeeding => -(int)Math.Round((double)speciesDailyDecay * 0.5, MidpointRounding.ToEven), LoyaltyEvent.DayWithoutDrinking => -(int)Math.Round((double)speciesDailyDecay * 0.5, MidpointRounding.ToEven), LoyaltyEvent.PetCriticallyHurt => -10, LoyaltyEvent.PetDowned => -20, LoyaltyEvent.DefeatedNearbyEnemy => 20, LoyaltyEvent.CrossedRegionBonded => 100, LoyaltyEvent.SynergyGranted => 20, LoyaltyEvent.ForTheKillExecuteKill => 60, _ => 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) { LoyaltyTier loyaltyTier = Tier(value); LoyaltyTier top = LoyaltyLadder.Top; if (loyaltyTier >= top) { next = top; atValue = TierFloor(top); return false; } next = loyaltyTier + 1; atValue = TierFloor(next); return true; } public static bool NextPayoutTier(int value, out LoyaltyTier next, out int atValue) { LoyaltyTier loyaltyTier = Tier(value); int num = PayoutLevel(loyaltyTier); LoyaltyTier top = LoyaltyLadder.Top; while (loyaltyTier < top) { loyaltyTier++; if (PayoutLevel(loyaltyTier) > num) { next = loyaltyTier; atValue = TierFloor(loyaltyTier); return true; } } next = top; atValue = TierFloor(top); return false; } public static int TierFloor(LoyaltyTier tier) { return LoyaltyLadder.Floor(tier); } public static int InitialLoyaltyFor(int configInitial, bool wildUnknownLearned, int tierBonus = 2) { int num = Clamp(configInitial); if (!wildUnknownLearned || tierBonus <= 0) { return num; } LoyaltyTier loyaltyTier = Tier(num) + tierBonus; if (loyaltyTier > LoyaltyTier.Trusting) { loyaltyTier = LoyaltyTier.Trusting; } int num2 = TierFloor(loyaltyTier); if (num2 <= num) { return num; } return num2; } public static int Clamp(int value) { if (value >= 0) { if (value <= 250) { return value; } return 250; } return 0; } public static double Fraction(int value) { if (value > 0) { if (value < 100) { return (double)value / 100.0; } return 1.0; } return 0.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 >= 250.0) { carry = 0.0; return 250; } int num2 = (int)Math.Floor(num); carry = num - (double)num2; return num2; } } public static class LoyaltyCurve { public static float Sample(float[] anchors, int level, int max) { if (anchors == null || anchors.Length == 0) { return 0f; } if (anchors.Length == 1 || max <= 0) { return anchors[0]; } if (level <= 0) { return anchors[0]; } if (level >= max) { return anchors[^1]; } double num = (double)level / (double)max * (double)(anchors.Length - 1); int num2 = (int)Math.Floor(num); if (num2 >= anchors.Length - 1) { return anchors[^1]; } double num3 = num - (double)num2; return (float)((double)anchors[num2] + (double)(anchors[num2 + 1] - anchors[num2]) * num3); } public static int SampleInt(int[] anchors, int level, int max) { if (anchors == null || anchors.Length == 0) { return 0; } float[] array = new float[anchors.Length]; for (int i = 0; i < anchors.Length; i++) { array[i] = anchors[i]; } return (int)Math.Round(Sample(array, level, max), MidpointRounding.AwayFromZero); } } public sealed class LadderRow { public LoyaltyTier Tier; public string Name; public int Floor; public int Payout; public bool Lock; public int PowerStep; public LadderRow(LoyaltyTier tier, string name, int floor, int payout, bool @lock, int powerStep) { Tier = tier; Name = name; Floor = floor; Payout = payout; Lock = @lock; PowerStep = powerStep; } public LadderRow Clone() { return new LadderRow(Tier, Name, Floor, Payout, Lock, PowerStep); } } public static class LoyaltyLadder { public const int TierCount = 14; public static readonly LadderRow[] Default = new LadderRow[14] { new LadderRow(LoyaltyTier.Gone, "Gone", 0, 0, @lock: false, 0), new LadderRow(LoyaltyTier.Broken, "Broken", 1, 1, @lock: false, 0), new LadderRow(LoyaltyTier.Fraying, "Fraying", 15, 2, @lock: false, 0), new LadderRow(LoyaltyTier.Guarded, "Guarded", 40, 3, @lock: false, 0), new LadderRow(LoyaltyTier.Cautious, "Cautious", 55, 3, @lock: true, 0), new LadderRow(LoyaltyTier.Steady, "Steady", 75, 4, @lock: false, 0), new LadderRow(LoyaltyTier.Trusting, "Trusting", 90, 4, @lock: true, 0), new LadderRow(LoyaltyTier.Devoted, "Devoted", 100, 4, @lock: false, 0), new LadderRow(LoyaltyTier.Unshaken, "Unshaken", 125, 5, @lock: true, 1), new LadderRow(LoyaltyTier.Boundless, "Boundless", 150, 5, @lock: false, 2), new LadderRow(LoyaltyTier.Sworn, "Sworn", 175, 6, @lock: true, 3), new LadderRow(LoyaltyTier.Fierce, "Fierce", 200, 7, @lock: false, 4), new LadderRow(LoyaltyTier.Mythic, "Mythic", 225, 7, @lock: true, 5), new LadderRow(LoyaltyTier.Eternal, "Eternal", 250, 8, @lock: true, 6) }; private static LadderRow[] _rows = Default; public static LadderRow[] Rows => _rows; public static bool IsOverridden => _rows != Default; public static LoyaltyTier Top => _rows[_rows.Length - 1].Tier; public static void Reset() { _rows = Default; } public static LadderRow Row(LoyaltyTier tier) { int num = (int)tier; if (num < 0) { num = 0; } if (num >= _rows.Length) { num = _rows.Length - 1; } return _rows[num]; } public static LoyaltyTier TierFor(int value) { for (int num = _rows.Length - 1; num > 0; num--) { if (value >= _rows[num].Floor) { return _rows[num].Tier; } } return _rows[0].Tier; } public static int Floor(LoyaltyTier tier) { return Row(tier).Floor; } public static int Payout(LoyaltyTier tier) { return Row(tier).Payout; } public static int PowerStep(LoyaltyTier tier) { return Row(tier).PowerStep; } public static bool IsLock(LoyaltyTier tier) { return Row(tier).Lock; } public static string Name(LoyaltyTier tier) { return Row(tier).Name; } public static int LockFloorAtOrBelow(int value) { for (int num = _rows.Length - 1; num > 0; num--) { if (_rows[num].Lock && value >= _rows[num].Floor) { return _rows[num].Floor; } } return 0; } public static LoyaltyTier RepresentativeTier(int payout) { if (payout <= 0) { return _rows[0].Tier; } for (int i = 0; i < _rows.Length; i++) { if (_rows[i].Payout >= payout) { return _rows[i].Tier; } } return Top; } public static bool TryParseOverride(string spec, out LadderRow[] rows, out string error) { rows = Default; error = null; if (string.IsNullOrWhiteSpace(spec)) { return true; } string[] array = spec.Split(new char[1] { ',' }); if (array.Length != 14) { error = $"LadderOverride needs exactly {14} comma-separated rows, got {array.Length}."; return false; } LadderRow[] array2 = new LadderRow[14]; for (int i = 0; i < 14; i++) { LadderRow ladderRow = Default[i].Clone(); string[] array3 = array[i].Trim().Split(new char[1] { ':' }); if (array3.Length < 1 || array3.Length > 4 || !int.TryParse(array3[0].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out ladderRow.Floor)) { error = $"LadderOverride row {i} ('{array[i].Trim()}') is not floor[:payout[:lock[:power]]]."; return false; } if (array3.Length > 1 && array3[1].Trim().Length > 0 && !int.TryParse(array3[1].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out ladderRow.Payout)) { error = $"LadderOverride row {i}: payout '{array3[1].Trim()}' is not an integer."; return false; } if (array3.Length > 2 && array3[2].Trim().Length > 0) { string text = array3[2].Trim(); if (text == "1") { ladderRow.Lock = true; } else { if (!(text == "0")) { error = $"LadderOverride row {i}: lock '{text}' must be 0 or 1."; return false; } ladderRow.Lock = false; } } if (array3.Length > 3 && array3[3].Trim().Length > 0 && !int.TryParse(array3[3].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out ladderRow.PowerStep)) { error = $"LadderOverride row {i}: power '{array3[3].Trim()}' is not an integer."; return false; } array2[i] = ladderRow; } if (array2[0].Floor != 0) { error = "LadderOverride row 0 (Gone) must have floor 0."; return false; } for (int j = 1; j < 14; j++) { if (array2[j].Floor <= array2[j - 1].Floor) { error = $"LadderOverride floors must strictly ascend (row {j}: {array2[j].Floor} <= {array2[j - 1].Floor})."; return false; } if (array2[j].Payout < array2[j - 1].Payout) { error = $"LadderOverride payouts must not fall (row {j}: {array2[j].Payout} < {array2[j - 1].Payout})."; return false; } } for (int k = 0; k < 14; k++) { if (array2[k].Payout < 0 || array2[k].Payout > 8) { error = $"LadderOverride row {k}: payout {array2[k].Payout} outside 0..{8}.";
plugins/Beastwhispering.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; 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 System.Text.RegularExpressions; using AggroKit; using Beastwhispering.Core; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CompanionKit; using CompanionKit.Core; using DonorKit; 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.EventSystems; using UnityEngine.Events; 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.18.0")] [assembly: AssemblyInformationalVersion("0.2.18+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")] [assembly: AssemblyProduct("Beastwhispering")] [assembly: AssemblyTitle("Beastwhispering")] [assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.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_0053: 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) CharacterAI result = null; float num = float.MaxValue; foreach (CharacterAI item in AisInRange(center, radius, exclude)) { Character character = ((CharacterControl)item).Character; if (!((Object)(object)character == (Object)null) && (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 ArmorAction { internal const int EquipActionId = 910072; internal const int RepairActionId = 910073; internal static bool Eligible(ItemDisplayOptionPanel panel, out Item item) { item = (((Object)(object)panel != (Object)null) ? panel.m_pendingItem : null); if ((Object)(object)item == (Object)null) { return false; } if (!Plugin.EnablePetArmor.Value) { return false; } if (!PetArmorService.IsArmor(item)) { return false; } if ((Object)(object)((EffectSynchronizer)item).OwnerCharacter != (Object)null) { return (Object)(object)((EffectSynchronizer)item).OwnerCharacter == (Object)(object)((UIElement)panel).LocalCharacter; } return false; } } [HarmonyPatch(typeof(ItemDisplayOptionPanel), "GetActiveActions")] internal static class ArmorAction_GetActiveActions { internal static void Postfix(ItemDisplayOptionPanel __instance, List<int> __result) { try { if (__result != null && ArmorAction.Eligible(__instance, out var item)) { if (Plugin.Pet?.Sim != null) { __result.Add(910072); } PetArmorDef val = PetArmorTable.ForItemId(item.ItemID); if (val != null && !string.IsNullOrEmpty(val.RepairReagent) && item.DurabilityRatio < 1f) { __result.Add(910073); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ARMOR] GetActiveActions patch error: " + ex)); } } } [HarmonyPatch(typeof(ItemDisplayOptionPanel), "GetActionText")] internal static class ArmorAction_GetActionText { internal static bool Prefix(ItemDisplayOptionPanel __instance, int _actionID, ref string __result) { if (_actionID != 910072 && _actionID != 910073) { return true; } try { if (_actionID == 910072) { string text = Plugin.Pet?.DisplayName; __result = (string.IsNullOrEmpty(text) ? "Equip on companion" : ("Equip to " + text)); } else { Item val = (((Object)(object)__instance != (Object)null) ? __instance.m_pendingItem : null); __result = PetArmorService.RepairLabel(PetArmorTable.ForItemId(((Object)(object)val != (Object)null) ? val.ItemID : 0)); } return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ARMOR] GetActionText patch error (using a plain label): " + ex)); __result = ((_actionID == 910072) ? "Equip on companion" : "Repair"); return false; } } } [HarmonyPatch(typeof(ItemDisplayOptionPanel), "ActionHasBeenPressed")] internal static class ArmorAction_ActionHasBeenPressed { internal static bool Prefix(ItemDisplayOptionPanel __instance, int _actionID) { if (_actionID != 910072 && _actionID != 910073) { return true; } ItemDisplay activatedItemDisplay = __instance.m_activatedItemDisplay; Item pendingItem = __instance.m_pendingItem; try { Character localCharacter = ((UIElement)__instance).LocalCharacter; if (_actionID == 910072) { PetArmorService.TryEquip(pendingItem, localCharacter); } else { PetArmorService.TryRepairItem(pendingItem, localCharacter); } } catch (Exception ex) { Plugin.Log.LogError((object)("[ARMOR] armor action failed: " + ex)); } RestoreFocusIfItemAlive(__instance, activatedItemDisplay, pendingItem); return true; } private static void RestoreFocusIfItemAlive(ItemDisplayOptionPanel panel, ItemDisplay src, Item item) { try { if (!((Object)(object)item == (Object)null) && !item.DestroyWanted && !((Object)(object)src == (Object)null) && !((Object)(object)((UIElement)panel).m_characterUI == (Object)null)) { ((UIElement)panel).m_characterUI.SetSelectedGameObject(((Component)src).gameObject); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ARMOR] focus restore after armor action failed: " + ex)); } } } internal static class AuraSetup { internal const string FireKey = "aura.infusefire"; internal const string FrostKey = "aura.infusefrost"; private const int FireSlot = 100; private const int FrostSlot = 101; internal static void RegisterAll() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: 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_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown //IL_0062: Expected O, but got Unknown //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: 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_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00c5: Expected O, but got Unknown CompanionAura.Register(new AuraRecipe { Key = "aura.infusefire", Slot = 100, Recipe = new FxRecipe { Source = (FxSource)4, SpeciesKey = "Burning Man", SubtreeFilter = "fire,flame,burn,ember,torch", Attach = (FxAttach)1, Offset = Vector3.zero, Scale = 1f } }); CompanionAura.Register(new AuraRecipe { Key = "aura.infusefrost", Slot = 101, Recipe = new FxRecipe { Source = (FxSource)4, SpeciesKey = "Wendigo", SubtreeFilter = "frost,ice,snow,cold,freez,breath", Attach = (FxAttach)1, Offset = Vector3.zero, Scale = 1f } }); RegisterDotAuras(); } private static void RegisterDotAuras() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //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) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown //IL_006b: Expected O, but got Unknown DotAuraSpec[] roster = DotAuras.Roster; foreach (DotAuraSpec val in roster) { CompanionAura.Register(new AuraRecipe { Key = val.Key, Slot = val.Slot, Recipe = new FxRecipe { Source = (FxSource)1, StatusName = val.FxStatusName, Attach = (FxAttach)1, Offset = Vector3.zero, Scale = 1f } }); } } } internal static class BackpedalTable { private static readonly TableLoader<float> _loader = new TableLoader<float>("SpeciesBackpedal.txt", "[BACKPEDALTABLE]", "backpedal entry", "backpedal entries", (Func<string, Action<string>, Dictionary<string, float>>)SpeciesBackpedal.ParseTable, (Func<Dictionary<string, float>, Dictionary<string, float>, Dictionary<string, float>>)SpeciesTable.Merge<float>, "replaces per-species", (Func<Dictionary<string, float>, Action<string>, Dictionary<string, float>>)null, (Func<Dictionary<string, float>, string>)null, ""); private static readonly DataAxis<Dictionary<string, float>> _axis = BwAxis.Table(_loader, "[BACKPEDALTABLE]", Validate); internal static Dictionary<string, float> Table => _axis.Table; internal static List<KeyValuePair<string, float>> Pairs => Table.ToList(); internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = TableValidator.CheckSpeciesKeys("[BACKPEDALTABLE]", "SpeciesBackpedal.txt", Table.Keys); Plugin.Log.LogMessage((object)string.Format("[BACKPEDALTABLE] boot check: {0} backpedal entr{1}, {2} unknown species key(s).", Table.Count, (Table.Count == 1) ? "y" : "ies", num)); } } internal static class ScrollDrop { internal static bool Into(ItemContainer target, int scrollId, string tag, string label) { Item val = ItemManager.Instance.GenerateItemNetwork(scrollId); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)$"{tag} GenerateItemNetwork({scrollId}) returned null — '{label}' not dropped."); return false; } val.ChangeParent(((Component)target).transform); return true; } } internal static class BlanketScrollDrops { private static readonly HashSet<int> ChestAndOtherIds = new HashSet<int> { 1000000, 1000010, 1000040, 1000050, 1000120, 1000060, 1000110, 1000070, 1000130, 1000080, 1001000 }; private static readonly HashSet<string> _rolled = new HashSet<string>(); internal static void ResetForScene() { _rolled.Clear(); } internal static bool IsChestOrOther(ItemContainer c) { if ((Object)(object)c != (Object)null) { return ChestAndOtherIds.Contains(((Item)c).ItemID); } return false; } internal static void Roll(ItemContainer target, string dedupKey, string what) { if (!Plugin.EnableTemperatureSystem.Value) { return; } float value = Plugin.BlanketRecipeDropChance.Value; if (value <= 0f || BlanketSetup.RegisteredScrolls.Count == 0 || !_rolled.Add(dedupKey)) { return; } foreach (KeyValuePair<string, int> registeredScroll in BlanketSetup.RegisteredScrolls) { double num = Random.value; if (!TamingFoods.ShouldDrop(1.0, (double)value, num)) { Plugin.Log.LogMessage((object)$"[BLANKETDROP] {what} rolled {num:0.000} vs {value:0.000} for '{registeredScroll.Key}' — no scroll."); } else if (ScrollDrop.Into(target, registeredScroll.Value, "[BLANKETDROP]", "Recipe: " + registeredScroll.Key)) { Plugin.Log.LogMessage((object)$"[BLANKETDROP] {what} dropped 'Recipe: {registeredScroll.Key}' (roll {num:0.000} vs {value:0.000})."); } } } } [HarmonyPatch(typeof(SelfFilledItemContainer), "GenerateContents")] internal static class BlanketScrollContainerPatch { internal static void Prefix(SelfFilledItemContainer __instance, out bool __state) { TreasureChest val = (TreasureChest)(object)((__instance is TreasureChest) ? __instance : null); __state = val != null && val.HasGeneratedContent; } internal static void Postfix(SelfFilledItemContainer __instance, bool __state) { try { if (!__state && !((Object)(object)__instance == (Object)null) && !PhotonNetwork.isNonMasterClientInRoom && BlanketScrollDrops.IsChestOrOther((ItemContainer)(object)__instance)) { BlanketScrollDrops.Roll((ItemContainer)(object)__instance, ((Item)__instance).UID.ToString(), $"container {((Item)__instance).ItemID}/{((Item)__instance).UID}"); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BLANKETDROP] container postfix error: " + ex)); } } } [HarmonyPatch(typeof(TreasureChest), "ShowContent")] internal static class BlanketScrollStashPatch { private static readonly FieldInfo LootedStashField = AccessTools.Field(typeof(Character), "LootedStash"); private static bool _warnedNoLootedStash; private static bool _warnedStashReadFailed; internal static void Prefix(TreasureChest __instance, Character _character, out bool __state) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Invalid comparison between Unknown and I4 __state = false; try { if ((Object)(object)__instance == (Object)null || (Object)(object)_character == (Object)null || (int)((ItemContainer)__instance).SpecialType != 4) { return; } if (LootedStashField == null) { if (!_warnedNoLootedStash) { _warnedNoLootedStash = true; Plugin.Log.LogWarning((object)"[BLANKETDROP] Character.LootedStash not found — stash containers will not roll for blanket recipe scrolls (chests and the 'other' containers are unaffected)."); } } else { List<string> list = LootedStashField.GetValue(_character) as List<string>; __state = list != null && !list.Contains(SceneManagerHelper.ActiveSceneName); } } catch (Exception ex) { __state = false; if (!_warnedStashReadFailed) { _warnedStashReadFailed = true; Plugin.Log.LogWarning((object)("[BLANKETDROP] reading Character.LootedStash threw — stash containers will not roll for blanket recipe scrolls this session (chests are unaffected): " + ex)); } } } internal static void Postfix(TreasureChest __instance, Character _character, bool __state) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) try { if (__state && !PhotonNetwork.isNonMasterClientInRoom) { ItemContainer val = (((Object)(object)_character != (Object)null) ? _character.Stash : null); if (!((Object)(object)val == (Object)null)) { BlanketScrollDrops.Roll(val, $"{((Item)__instance).UID}|{_character.UID}", "stash " + ((Item)__instance).UID + " for '" + _character.Name + "'"); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BLANKETDROP] stash postfix error: " + ex)); } } } internal static class BlanketSetup { internal static readonly Dictionary<string, int> Registered = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); internal static readonly Dictionary<string, int> RegisteredScrolls = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private const int ScrollVendorSilver = 1; 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_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Invalid comparison between Unknown and I4 //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: 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"); int num4; if (num3.HasValue) { num4 = num3.Value; } else { if (list.Count <= 0) { Plugin.Log.LogWarning((object)("[BLANKET] '" + def.Key + "': donor 'Bandages' did not resolve and the row lists NO ingredients to fall back on — blanket skipped.")); return; } num4 = list[0]; Plugin.Log.LogMessage((object)("[BLANKET] '" + def.Key + "': donor 'Bandages' did not resolve, but the blanket is NOT " + $"skipped — falling back to its first ingredient (ItemID {num4}) as the clone donor.")); } string text = (((int)def.Side == 1) ? "cold" : "heat"); Item val = SlConsumables.RegisterUsable("[BLANKET]", def.Key, num4, 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 {num4}), recipe '{text2}' " + string.Format("(Survival: {0}), relieves {1} {2} steps for {3:0}s.", string.Join(" + ", def.Ingredients), def.ReliefSteps, text, def.DurationSeconds))); RegisterScroll(def, text2, val2); } } } private static void RegisterScroll(BlanketDef def, string recipeUid, Recipe applied) { //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Expected O, but got Unknown int num = default(int); if (!BwIds.TryGet(LedgerKey(def.Key) + "-scroll", ref num)) { Plugin.Log.LogWarning((object)("[BLANKET] '" + def.Key + "': no ItemID is allocated for this blanket's RECIPE SCROLL, so the recipe can only be found by crafting, never as loot. Allocate one with `bwspecies ids alloc --family blankets --key " + LedgerKey(def.Key) + "-scroll --owner Beastwhispering/BlanketSetup --name \"Recipe: <display name>\"`, then `bwspecies ids gen`.")); return; } if (RegisteredScrolls.ContainsValue(num)) { Plugin.Log.LogWarning((object)$"[BLANKET] '{def.Key}': scroll ItemID {num} is already registered this session — scroll skipped."); return; } int? num2 = TamingFoodSetup.FindVanillaRecipeScroll(); if (!num2.HasValue) { Plugin.Log.LogWarning((object)("[BLANKET] '" + def.Key + "': no vanilla RecipeItem to clone the scroll from — scroll skipped.")); } else if (IdPool.Claim(num, "[BLANKET] recipe scroll for '" + def.Key + "'", (Action<string>)delegate(string m) { Plugin.Log.LogError((object)m); })) { SL_RecipeItem val = new SL_RecipeItem { Target_ItemID = num2.Value, New_ItemID = num, Name = "Recipe: " + def.Key, Description = "Teaches how to sew a " + def.Key + " — the wrap that keeps a companion working where the weather would otherwise break it.", RecipeUID = recipeUid }; ((ContentTemplate)val).ApplyTemplate(); Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(num); RecipeItem val2 = (RecipeItem)(object)((itemPrefab is RecipeItem) ? itemPrefab : null); if (val2 == null) { Plugin.Log.LogWarning((object)$"[BLANKET] '{def.Key}': scroll prefab {num} missing/not a RecipeItem after ApplyTemplate — scroll skipped."); return; } val2.Recipe = applied; SlConsumables.SetVendorValue("[BLANKET]", def.Key, (Item)(object)val2, 1); RegisteredScrolls[def.Key] = num; Plugin.Log.LogMessage((object)($"[BLANKET] '{def.Key}': recipe scroll {num} 'Recipe: {def.Key}' (donor {num2}) " + "teaching '" + recipeUid + "'.")); } } private static string LedgerKey(string key) { return "blanket." + key.Trim().ToLowerInvariant().Replace(' ', '-'); } 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(LedgerKey(key), 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 PlaysScene => true; 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.Pet?.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) { //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)_affectedCharacter == (Object)null) { return; } if (!_affectedCharacter.IsLocalPlayer) { Plugin.Log.LogMessage((object)("[BLANKET] replicated wrap by remote '" + _affectedCharacter.Name + "' ignored on this machine (the actor's machine owns the wrap).")); return; } 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.Pet?.State; if (val == null || val2 == null || (Object)(object)_affectedCharacter == (Object)null) { return; } 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) { ConsumeResult val3 = Inventories.ConsumeOne(parentItem, 1); if (!((ConsumeResult)(ref val3)).Consumed) { Plugin.Log.LogWarning((object)("[BLANKET] consume read-back MISMATCH — '" + parentItem.Name + "' likely NOT consumed (" + ((ConsumeResult)(ref val3)).Describe() + "); report (BUG-CHOWNOTCONSUMED class).")); } } else { Plugin.Log.LogWarning((object)"[BLANKET] no ParentItem to consume — wrap landed, blanket NOT consumed (bug, report)."); } string text = Plugin.Instance.ActivePet.DisplayName ?? "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 bool _fromPlayerCast; private static string _lastDiscipline = "none this session"; 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>(); private static readonly NameCandidates _discipline = new NameCandidates("Discipline", (Func<string>)(() => Plugin.BraceEchoDisciplineNames.Value), (Func<string, bool>)NameCandidates.StatusPrefabExists, (Action<string>)delegate(string cand) { Plugin.Log.LogMessage((object)("[BRACEECHO] resolved Discipline status: '" + cand + "'.")); }, (Action<string>)delegate(string raw) { Plugin.Log.LogWarning((object)("[BRACEECHO] no Discipline status resolved from candidates '" + raw + "' — fix [PetBraceEcho] DisciplineStatusNames (discovery: `statusdump`).")); }); private static readonly HashSet<string> _cueFailures = new HashSet<string>(); 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) { EnterCore(body, combat, def, Plugin.BraceWindowSeconds.Value, 0, fromPlayerCast: false); MaybeTaunt(); } internal static void EnterFromPlayerBrace(Character player) { if (State.Open) { Plugin.Log.LogMessage((object)"[BRACEECHO] player cast Brace but a brace window is already open — pet keeps bracing."); return; } Pet pet = Plugin.Pet; CompanionBody val = ((Companion)(pet?)).Body; CompanionCombat val2 = ((pet != null) ? ((Companion)pet).Combat : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val2 == (Object)null)) { CompanionAnchor anchor = ((Companion)pet).Anchor; if (anchor != null && anchor.HasLiveAnchor) { SpecialAttackDef val3 = default(SpecialAttackDef); SpecialAttackTable.TryGet(SpecialAttacks.Table, val.SpeciesId, ref val3); SpecialAttackDef def = PetBraceEcho.ResolveDef(val3, Plugin.BraceEchoRiposteMult.Value, (Action<string>)delegate(string w) { Plugin.Log.LogWarning((object)("[BRACEECHO] " + w)); }); EnterCore(val, val2, def, Plugin.BraceEchoWindowSeconds.Value, 1, fromPlayerCast: true); return; } } Plugin.Log.LogMessage((object)"[BRACEECHO] player cast Brace but there is no live pet body/anchor — nothing echoes."); } private static void EnterCore(CompanionBody body, CompanionCombat combat, SpecialAttackDef def, float windowSeconds, int maxCounters, bool fromPlayerCast) { _body = body; _combat = combat; _def = def; _synergyOpened = false; _fromPlayerCast = fromPlayerCast; _negated = 0; _pendingRipostes.Clear(); State.Begin((double)Time.time, (double)windowSeconds, maxCounters); SetBlockPose(on: true); PlayEnterCue(); string arg = (((Object)(object)_body != (Object)null) ? _body.SpeciesId : null); string arg2 = (fromPlayerCast ? "[BRACEECHO]" : "[BRACE]"); Plugin.Log.LogMessage((object)($"{arg2} '{arg}' braced for {windowSeconds:F1}s " + (fromPlayerCast ? $"(player-cast echo, one counter total, riposte x{def.DamageMultiplier:F2})." : $"(perAttackerOnce={Plugin.BracePerAttackerOnce.Value}, negate={Plugin.BraceNegateCounteredHit.Value})."))); } private static void MaybeTaunt() { if (!Plugin.BraceEnableTaunt.Value) { _lastTaunt = "skipped ([Brace] EnableTaunt=false)"; return; } Pet pet = Plugin.Pet; 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) { string text = (((Object)(object)_body != (Object)null) ? _body.SpeciesId : null); _lastTaunt = "none ('" + text + "' has no taunt axis on its SpeciesSpecialAttacks row)"; return; } CompanionCombat val = ((pet != null) ? ((Companion)pet).Combat : null); Character val2 = (((Object)(object)val != (Object)null) ? val.SpecialAttackTarget : null); if ((Object)(object)val2 == (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(val2, (Character)anchor, num2); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter != (Object)null) { Notify.Player(localPlayerCharacter, pet.DisplayName + " draws the enemy's fury with an infernal growl!"); } _lastTaunt = $"'{val2.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_00d4: 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_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: 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_0105: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: 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) { string arg = (((Object)(object)dealer != (Object)null) ? dealer.Name : "nothing"); Plugin.Log.LogMessage((object)$"[BRACE] hit from '{arg}' via {text} not counter-eligible ({val2}) — lands normally."); return false; } CounterVerdict val3 = State.TryCounter(val.DealerUid, (double)Time.time, Plugin.BracePerAttackerOnce.Value); if ((int)val3 != 4) { 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 (_fromPlayerCast ? (!Plugin.EnableBraceEcho.Value) : (!Plugin.EnableBrace.Value)) { Exit("kill-switch"); } else if ((Object)(object)_body == (Object)null || (Object)(object)((Companion)(Plugin.Pet?)).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_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0082: 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_008b: 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_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: 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 ('" + (((Object)(object)attacker != (Object)null) ? attacker.Name : "<destroyed>") + "') died before the counter landed — nothing dealt.")); if (_fromPlayerCast) { GrantDiscipline(); } } else { if ((Object)(object)_combat == (Object)null || (Object)(object)_body == (Object)null) { return; } 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); PlayCue((Sounds)12930, "riposte"); 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.Pet; 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); if (_fromPlayerCast) { GrantDiscipline(); return; } 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) { string text = (_fromPlayerCast ? "player-cast echo" : "signature stance"); _lastWindow = $"closed ({reason}, {text}): countered {State.CounterCount} attacker(s), {_negated} hit(s) negated"; Plugin.Log.LogMessage((object)((_fromPlayerCast ? "[BRACEECHO]" : "[BRACE]") + " " + _lastWindow + ".")); State.End(); _pendingRipostes.Clear(); SetBlockPose(on: false); _fromPlayerCast = false; _def = null; _combat = null; _body = null; } private static void GrantDiscipline() { if (!Plugin.BraceEchoGrantDiscipline.Value) { _lastDiscipline = "skipped ([PetBraceEcho] GrantDiscipline=false)"; return; } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null || (Object)(object)localPlayerCharacter.StatusEffectMngr == (Object)null) { _lastDiscipline = "no player at counter time"; return; } string text = _discipline.Resolve(); if (text == null) { _lastDiscipline = "UNRESOLVED status name"; return; } localPlayerCharacter.StatusEffectMngr.AddStatusEffect(text); Notify.Player(localPlayerCharacter, (Plugin.Pet?.DisplayName ?? "Your companion") + "'s stand steels your discipline!"); _lastDiscipline = "granted '" + text + "'"; Plugin.Log.LogMessage((object)("[BRACEECHO] pet counter succeeded — player granted '" + text + "'.")); } private static void PlayEnterCue() { float num = Mathf.Clamp01(Plugin.BraceCueVolume.Value); if (!((Object)(object)_body == (Object)null) && !(num <= 0f)) { PlayCue((Sounds)12910, "enter cue", num); } } private static void PlayCue(Sounds sound, string what, float vol = -1f) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_body == (Object)null) { return; } try { if (vol < 0f) { Global.AudioManager.PlaySoundAtPosition(sound, ((Component)_body).transform, 0f, 1f, 1f, 1f, 1f); } else { Global.AudioManager.PlaySoundAtPosition(sound, ((Component)_body).transform, 0f, vol, vol, 1f, 1f); } } catch (Exception ex) { if (_cueFailures.Add(what)) { Plugin.Log.LogWarning((object)($"[BRACE] {what} sound ({sound}) failed to play — brace is now silent " + "for this session; the counter itself is unaffected: " + ex.Message)); } } } 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_01f0: Unknown result type (might be due to invalid IL or missing references) //IL_0211: Unknown result type (might be due to invalid IL or missing references) //IL_0216: Unknown result type (might be due to invalid IL or missing references) //IL_022e: Unknown result type (might be due to invalid IL or missing references) //IL_023b: 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] echo config: Enable={Plugin.EnableBraceEcho.Value} WindowSeconds={Plugin.BraceEchoWindowSeconds.Value:F1} " + $"RiposteMult={Plugin.BraceEchoRiposteMult.Value:F2} GrantDiscipline={Plugin.BraceEchoGrantDiscipline.Value} " + "DisciplineNames='" + Plugin.BraceEchoDisciplineNames.Value + "' (resolved: '" + (_discipline.Resolve() ?? "UNRESOLVED") + "')")); Plugin.Log.LogMessage((object)("[BRACE] last echo Discipline: " + _lastDiscipline + (State.Open ? (" | open window origin: " + (_fromPlayerCast ? "player-cast echo" : "signature stance")) : ""))); 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, ref DamageList _damage, Character _dealerChar) { try { return Decide(__instance, ref __result, _damageSource, ref _damage, _dealerChar); } catch (Exception arg) { Plugin.Log.LogWarning((object)("[BRACE] ReceiveHit prefix threw on '" + (((Object)(object)__instance != (Object)null) ? __instance.Name : "?") + "' " + string.Format("(dealer '{0}') — running the hit vanilla: {1}", ((Object)(object)_dealerChar != (Object)null) ? _dealerChar.Name : "none", arg))); return true; } } private static bool Decide(Character __instance, ref DamageList __result, Object _damageSource, ref DamageList _damage, Character _dealerChar) { bool active = BraceDriver.Active; bool active2 = UnerringDriver.Active; bool armed = PetInvincible.Armed; bool wearArmed = PetArmorService.WearArmed; bool armed2 = EvadeDriver.Armed; if (!active && !active2 && !armed && !wearArmed && !armed2) { return true; } if (!CompanionAnchor.IsAnchor(__instance)) { return true; } Pet pet = Plugin.Pet; object obj; if (pet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)pet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; bool flag = (Object)(object)val != (Object)null && (Object)(object)__instance == (Object)(object)val; if (!flag && !armed2) { return true; } bool flag2 = flag && active && BraceDriver.OnAnchorHit(_dealerChar, _damageSource, val); if (!flag2 && flag && active2) { flag2 = UnerringDriver.OnAnchorHit(_dealerChar, _damageSource, val); } if (!flag2 && armed2) { flag2 = TryEvade(__instance, flag, _dealerChar, _damageSource, _damage); } if (!flag2) { if (!flag) { return true; } if (wearArmed) { PetArmorService.OnAnchorHit(_damage); } if (armed) { PetInvincible.ClampLethal(val, ref _damage, _dealerChar); } return true; } if (_damage == null) { Plugin.Log.LogWarning((object)("[BRACE] negated a hit from '" + (((Object)(object)_dealerChar != (Object)null) ? _dealerChar.Name : "none") + "' that carried NO damage list — running it vanilla instead of returning a null result.")); return true; } DamageList val2 = _damage.Clone(); for (int i = 0; i < val2.Count; i++) { val2[i].Damage = 0f; } __result = val2; return false; } private static bool TryEvade(Character victim, bool isLocalPetAnchor, Character dealer, Object damageSource, DamageList damage) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) float rawDamage = ((damage != null) ? damage.TotalDamage : float.NaN); string text = default(string); if (!AnchorSentinel.TryParseOwner(UID.op_Implicit(victim.UID), ref text)) { return false; } Character localPlayer = Plugin.LocalPlayer; string speciesId; int loyalty; if ((Object)(object)localPlayer != (Object)null && string.Equals(text, UID.op_Implicit(localPlayer.UID), StringComparison.Ordinal)) { if (!isLocalPetAnchor) { return false; } Pet pet = Plugin.Pet; if (pet == null) { return false; } speciesId = pet.SpeciesId; PetSimulation sim = pet.Sim; loyalty = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue).GetValueOrDefault(); } else { ProxyInfo val = default(ProxyInfo); if (!ProxyPets.TryGetProxyInfo(text, ref val)) { return false; } speciesId = val.SpeciesKey; loyalty = val.Loyalty; } return EvadeDriver.OnAnchorHit(dealer, damageSource, victim, text, speciesId, loyalty, rawDamage); } } 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_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: 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."; } if (pet.Sim == null) { return $"'{state.BuffFoodKey}' — {state.BuffFoodSecondsLeft:F0}s left; no simulation on the pet yet, so no tier to describe."; } 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_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Invalid comparison between Unknown and I4 //IL_023a: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0244: Unknown result type (might be due to invalid IL or missing references) //IL_02bf: Unknown result type (might be due to invalid IL or missing references) //IL_02d6: Unknown result type (might be due to invalid IL or missing references) //IL_02e2: 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; } if (activePet.Sim == null) { Plugin.Log.LogMessage((object)"[BUFFFOOD] the pet has no simulation yet — no tier to report."); 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 BundleBodies { private enum State { Untried, Registered, Missing, Failed } internal static ConfigEntry<bool> Enabled; private static readonly Dictionary<string, (string file, string prefab)> Species = new Dictionary<string, (string, string)>(StringComparer.OrdinalIgnoreCase) { { "Pearlbird", ("bw_bodies.pearlbird", "PearlBird_v") }, { "Veaber", ("bw_bodies.veaber", "Veaber_v") }, { "Hyena", ("bw_bodies.hyena", "Hyena_v") }, { "Armored Hyena", ("bw_bodies.armoredhyena", "ArmoredHyena_v") } }; private static readonly Dictionary<string, State> _state = new Dictionary<string, State>(StringComparer.OrdinalIgnoreCase); private static string Dir => Path.Combine(Path.GetDirectoryName(typeof(Plugin).Assembly.Location) ?? ".", "AssetBundles"); internal static void Bind(ConfigFile cfg) { Enabled = cfg.Bind<bool>("BundleBodies", "Enabled", true, "Register shipped AssetBundle rigs (AssetBundles/ beside the plugin DLL) as resident body templates at scene load, so those species re-form with no donor-scene load or expedition. Off (or a missing/failed bundle) falls back to the normal harvest/expedition path."); } internal static void EnsureRegistered() { if (Enabled == null || !Enabled.Value) { return; } foreach (KeyValuePair<string, (string, string)> item in Species) { BodyTemplate resident = BodyTemplateStore.GetResident(item.Key); if (resident != null) { if (resident.Origin == "prebuilt") { continue; } GameObject dormant = resident.Dormant; BodyTemplateStore.Drop(item.Key); if ((Object)(object)dormant != (Object)null) { Object.Destroy((Object)(object)dormant); } Plugin.Log.LogMessage((object)("[BUNDLE] '" + item.Key + "': a harvested template was resident (origin=" + (resident.Origin ?? "harvest") + ") — replaced by the shipped bundle (bundle tier takes precedence).")); } _state.TryGetValue(item.Key, out var value); if (value != State.Missing && value != State.Failed) { _state[item.Key] = Register(item.Key, item.Value.Item1, item.Value.Item2); } } } private static State Register(string key, string file, string prefabName) { string text = Path.Combine(Dir, file); if (!File.Exists(text)) { Plugin.Log.LogInfo((object)("[BUNDLE] no bundle for '" + key + "' at " + text + " — the expedition/harvest path covers it as before.")); return State.Missing; } AssetBundle val = null; try { val = AssetBundle.LoadFromFile(text); if ((Object)(object)val == (Object)null) { throw new IOException("AssetBundle.LoadFromFile returned null (wrong Unity version or corrupt file?)"); } GameObject val2 = val.LoadAsset<GameObject>(prefabName); if ((Object)(object)val2 == (Object)null) { throw new IOException("no GameObject asset '" + prefabName + "' in the bundle (assets: " + string.Join(", ", val.GetAllAssetNames()) + ")"); } BodyTemplate val3 = BodyTemplateStore.RegisterPrebuilt(key, val2, SpeciesStats.Lookup(key), key); if (val3 == null) { throw new InvalidOperationException("RegisterPrebuilt refused the body (see [TEMPLATE] error above)"); } RemapShaders(val3.Dormant, key); Plugin.Log.LogMessage((object)("[BUNDLE] '" + key + "' body registered from " + file + " — no expedition needed for this species" + ((val3.Captured != null) ? " (stats from the species ledger)." : " (NO stats yet: the first real harvest of this species will record them)."))); return State.Registered; } catch (Exception ex) { Plugin.Log.LogError((object)("[BUNDLE] loading '" + key + "' from " + text + " FAILED — the expedition/harvest path takes over: " + ex.Message)); return State.Failed; } finally { if ((Object)(object)val != (Object)null) { val.Unload(false); } } } private static void RemapShaders(GameObject root, string key) { int num = 0; int num2 = 0; Renderer[] componentsInChildren = root.GetComponentsInChildren<Renderer>(true); foreach (Renderer val in componentsInChildren) { Material[] sharedMaterials = val.sharedMaterials; foreach (Material val2 in sharedMaterials) { if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2.shader == (Object)null)) { Shader val3 = Shader.Find(((Object)val2.shader).name); if ((Object)(object)val3 != (Object)null && val3 != val2.shader) { val2.shader = val3; num++; } else if ((Object)(object)val3 == (Object)null) { num2++; } } } } if (num > 0 || num2 > 0) { Plugin.Log.LogMessage((object)($"[BUNDLE] '{key}' shader remap: {num} material(s) swapped to live game shaders" + ((num2 > 0) ? $", {num2} UNRESOLVED (name not in the loaded game — expect pink; ship the name in the material)" : "") + ".")); } } internal static string Verb(string[] parts) { bool flag = false; if (parts != null) { foreach (string a in parts) { if (string.Equals(a, "reload", StringComparison.OrdinalIgnoreCase)) { flag = true; } } } if (flag) { foreach (string key in Species.Keys) { if (BodyTemplateStore.GetResident(key) != null) { BodyTemplateStore.Drop(key); } _state.Remove(key); } EnsureRegistered(); } StringBuilder stringBuilder = new StringBuilder(string.Format("[BUNDLE] {0}enabled={1} dir={2}", flag ? "reloaded — " : "", Enabled?.Value ?? false, Dir)); foreach (KeyValuePair<string, (string, string)> item in Species) { _state.TryGetValue(item.Key, out var value); bool flag2 = BodyTemplateStore.GetResident(item.Key) != null; stringBuilder.Append($"\n '{item.Key}' <- {item.Value.Item1} ({item.Value.Item2}): {value}, resident={flag2}"); } return stringBuilder.ToString(); } } 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 bool AssistOnOwnerHit => Plugin.AssistOnOwnerHit.Value; public float OwnerFocusRange => Plugin.OwnerFocusRange.Value; public float LeashDistance => Plugin.LeashDistance.Value; public float CatchUpSpeed => Plugin.CatchUpSpeed.Value; public float DisengageRunHomeSeconds => Plugin.DisengageRunHomeSeconds.Value; public bool AttackVocals => Plugin.PetAttackVocals.Value; public float StationRingFraction => Plugin.StationRingFraction.Value; public float StationLineAngleDeg => Plugin.StationLineAngleDeg.Value; public float StationRestationMeters => Plugin.StationRestationMeters.Value; public float StationRestationSeconds => Plugin.StationRestationSeconds.Value; public float StationArriveMeters => Plugin.StationArriveMeters.Value; public int StationMaxRestations => Plugin.StationMaxRestations.Value; public float StationFarMeters => Plugin.StationFarMeters.Value; public float StationProgressMeters => Plugin.StationProgressMeters.Value; public float StationEnemyFastMetersPerSecond => Plugin.StationEnemyFastMetersPerSecond.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 bool SlopeTiltEnabled => false; 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<string> SpeciesCarryWeights; public static ConfigEntry<string> SpeciesSynergyResistMultipliers; public static ConfigEntry<float> FollowSpeed; public static ConfigEntry<float> MinFollowSpeed; public static ConfigEntry<float> LeashDistance; public static ConfigEntry<float> CatchUpSpeed; 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)."); SpeciesCarryWeights = cfg.Bind<string>("Pet", "SpeciesCarryWeights", "", "Per-species pet-inventory carry-weight overrides (docs/pet-inventory-plan.md). The shipped defaults ride an EMBEDDED table (SpeciesCarry.txt, generated by bwspecies from the manifests' carry axis); anything you set here WINS per-key over that table, and a species NO layer names takes [Systems] PetCarryWeightDefault. 'Term=weight' comma-separated, substring match against the species (e.g. Phytosaur=12,Hyena=5 — resolved on its own BEFORE the embedded table, so 'Hyena' covers 'Armored Hyena' too; an exact key here beats a longer substring here); weight must be a finite value > 0 (a 0 refuses every item, a negative would mean UNLIMITED to the engine — both are refused with a [CARRY] warning and fall to the default). Applies on the next sim tick — no relaunch; forensics: carrydump."); SpeciesSynergyResistMultipliers = cfg.Bind<string>("Pet", "SpeciesSynergyResistMultipliers", "", "Per-species Synergy resist-multiplier overrides (docs/synergy-resist-plan.md). The shipped defaults ride an EMBEDDED table (SpeciesSynergyResist.txt, generated by bwspecies from the manifests' synergyResist axis); anything you set here WINS per-key over that table, and a species NO layer names takes [Synergy] ResistMultiplierDefault. 'Term=multiplier' comma-separated, substring match against the species (e.g. Pearlbird=3,Hyena=2 — 'Hyena' covers 'Armored Hyena' too). This string is resolved on its own BEFORE the embedded table (within it an exact key beats a longer substring, which is what keeps 'Armored Hyena' off a 'Hyena' row when you list both; the table only answers for species this string does not name). The pet's anchor gains loyaltyTier(0-4) x synergyStacks x multiplier all-resistances while Synergy is up, so a multiplier of 2 is +64 at the ceiling. Must be a finite value > 0 (refused with a [SYNRESIST] warning, falling to the default). Applies on the next sim tick — no relaunch; forensics: synergydump."); 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."); LeashDistance = cfg.Bind<float>("Pet", "LeashDistance", 84f, "Out-of-combat leash (m): when the pet falls further behind you than this, it teleports to the nearest OFF-SCREEN spot behind the camera (12.5-22.5 m back, whatever the camera can't see) and walks the rest in. Was a fixed 14 before 2026-08-20 — the 'yank'. Vanilla's cosmetic pets use 40. Raised 30 -> 42 -> 84 on 2026-08-25: Cobalt's ruling is ONE number in and out of combat, so this now MATCHES [Combat] CombatLeashDistance (84) and the pet is effectively never warped for merely being behind — the warp is a stuck-backstop, not a leash you feel. This deliberately sits ABOVE the old 'keep it under 50' guidance: 50 is CompanionKit's zone-change re-place floor, and the re-place trigger simply rides this value up with it (FollowPolicy.Decide). The one exception is the coverage-gap backstop for a body whose goal is PINNED away from you (Stay spot / scene spot / combat target after an area change) — that keeps the flat 50 m floor so a stranded pet still gets rescued. Lowering this is safe at any value; raising it further only lengthens the rope."); CatchUpSpeed = cfg.Bind<float>("Pet", "CatchUpSpeed", 8f, "Catch-up ceiling (m/s): while merely FOLLOWING, a pet more than ~5 m behind you ramps up from its base speed to this over the next ~4 m, the way vanilla cosmetic pets do (they run 8-12 m/s when behind), so it closes the gap on foot instead of hitting the leash. This is also the FOLLOW speed ceiling: captured species speeds often exceed it (a Veaber records 18), and 8 is the rigs' run-animation ceiling — above it the feet skate and the pet reads as teleporting. Scales with the F2 speedmult dev slider (a 3x player gets a 24 m/s pet). Combat chases run the true species speed. 0 = the old flat, unclamped follow speed."); 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", 3f, "Nearest distance (m) a following pet settles from you when it 'loafs' nearby instead of standing on you."); LoafDistanceMax = cfg.Bind<float>("Follow", "LoafDistanceMax", 5f, "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 Watchdog { public static ConfigEntry<bool> EnableStuckBodyWatchdog; public static ConfigEntry<float> StuckSeconds; public static ConfigEntry<float> StuckDistanceMeters; public static ConfigEntry<int> MaxRebuildAttempts; internal static void Bind(ConfigFile cfg) { EnableStuckBodyWatchdog = cfg.Bind<bool>("Watchdog", "EnableStuckBodyWatchdog", true, "Watch the pet's body for the 'pinned in place' failure and rebuild it when it cannot recover on its own (docs/mp-session-2026-08-04-handoff.md). The rebuild keeps the bond, the save file, the pet's cargo, its buffs and its anchor -- it destroys and re-forms only the visible body, exactly as a quit-to-menu reload would. false = no automatic detection and no automatic rebuild (nothing is measured, so this is exactly pre-feature behaviour); the MANUAL 'petunstick' verb still works on request, alongside the 'recall' key (F9) and a main-menu reload."); StuckSeconds = cfg.Bind<float>("Watchdog", "StuckSeconds", 20f, "How many continuous real seconds the body must be far from you, agent-less AND making no progress toward you before the watchdog acts. Lower reacts sooner but risks acting on a merely blocked pet; the rebuild is cheap but not free (the pet blinks out and re-forms)."); StuckDistanceMeters = cfg.Bind<float>("Watchdog", "StuckDistanceMeters", 25f, "How far (m) from its goal the body must be to be a watchdog candidate at all. A body stuck CLOSE to you is a different, quieter failure and is deliberately out of scope -- recall (F9) covers it."); MaxRebuildAttempts = cfg.Bind<int>("Watchdog", "MaxRebuildAttempts", 2, "Most rebuilds the watchdog will attempt before giving up and telling you to reload (per area; the count also resets after a minute of healthy following). A cap exists because a rebuild that keeps producing stuck bodies means the CAUSE is upstream of the body, and looping on it would just churn."); } } 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; public static ConfigEntry<bool> TameRollEnabled; public static ConfigEntry<float> TameSuccessChance; public static ConfigEntry<float> TamePassiveChance; public static ConfigEntry<float> StealthTameSuccessChance; public static ConfigEntry<float> StealthTamePassiveChance; public static ConfigEntry<float> TamePassiveSeconds; public static ConfigEntry<bool> EnableDevTameKey; internal static void Bind(ConfigFile cfg) { EnableDevTameKey = cfg.Bind<bool>("Taming", "EnableDevTameKey", false, "DEV ONLY. Enables the [Keys] TameKey bind (default F7), an instant full taming attempt on the nearest wild tamable with no cooldown, no cost and no confirmation. OFF (default) = the key does nothing and taming happens through taming foods, as intended for play. The 'tame' dev command verb works regardless of this setting. NOTE: BepInEx never migrates a changed default into an existing cobalt.beastwhispering.cfg."); TameRollEnabled = cfg.Bind<bool>("Taming", "TameRollEnabled", true, "Every real taming attempt ROLLS: success, fail-but-the-beast-lets-you-be (a truce), or fail-and-it-attacks. OFF = the pre-2026-08-21 certainty (every attempt that passes the refusal ladder bonds). Dev cheats (tameany/tamecached, 'tame force') never roll either way."); TameSuccessChance = cfg.Bind<float>("Taming", "TameSuccessChance", 1f / 3f, "Chance (0-1) a NON-stealth attempt succeeds. The attack band is 1 - success - passive (never below 0; if the two exceed 1 they are scaled down to fit). Default: an even third each. NOTE: BepInEx never migrates a changed default into an existing cobalt.beastwhispering.cfg."); TamePassiveChance = cfg.Bind<float>("Taming", "TamePassiveChance", 1f / 3f, "Chance (0-1) a NON-stealth attempt fails but the beast tolerates you for TamePassiveSeconds (it won't come for you unless you or your pet strike it)."); StealthTameSuccessChance = cfg.Bind<float>("Taming", "StealthTameSuccessChance", 0.5f, "Chance (0-1) an attempt FROM STEALTH succeeds. From stealth = you are sneaking AND the beast has not noticed you (not locked on you, not in combat). Default 1/2."); StealthTamePassiveChance = cfg.Bind<float>("Taming", "StealthTamePassiveChance", 0.25f, "Chance (0-1) an attempt FROM STEALTH fails with the beast tolerating you. Default 1/4 (leaving 1/4 for an attack)."); TamePassiveSeconds = cfg.Bind<float>("Taming", "TamePassiveSeconds", 60f, "How long (seconds) a fail-passive beast tolerates you: it will not acquire you by sight/hearing or squad hand-out, and is calmed once if it already had you. Ends early the moment you (or your pet) hit it — then it fights exactly as vanilla would. Its squadmates were never party to the truce."); 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", 0.33f, "Global chance (0-1) a tamable creature drops its taming-food recipe scroll on death, multiplied by the per-species dropChance in TamingFoods.json. 0.33 = roughly one scroll per three tamable kills; 1 = guaranteed (the old deliberately-generous play-test default). NOTE: BepInEx never migrates a changed default into an existing cobalt.beastwhispering.cfg — an install made before this change keeps whatever number is already written in the file."); } } 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<int> WildUnknownTameTierBonus; public static ConfigEntry<float> HungerSecondsPerDay; public static ConfigEntry<float> ThirstSecondsPerDay; 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<bool> EnableItemHealing; public static ConfigEntry<float> SatiationFraction; public static ConfigEntry<float> ChowSatietyMultiplier; public static ConfigEntry<bool> EnablePassiveBuffs; public static ConfigEntry<bool> EnableBagPerk; public static ConfigEntry<bool> EnablePetInventory; public static ConfigEntry<float> PetCarryWeightDefault; public static ConfigEntry<bool> ShowPetStatusIcons; public static ConfigEntry<float> HungryIconFraction; public static ConfigEntry<float> ThirstyIconFraction; 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; public static ConfigEntry<bool> EnableCombatLoyalty; public static ConfigEntry<float> KillCreditRadius; public static