using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using AggroKit; using Beastwhispering.Core; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CompanionKit; using CompanionKit.Core; using ForgeKit; using HarmonyLib; using Microsoft.CodeAnalysis; using NetKit; using NetKit.Core; using Rewired; using SideLoader; using SideLoader.Model; using SkillKit; using StoryKit; using StoryKit.Core; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Beastwhispering")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.0.0")] [assembly: AssemblyInformationalVersion("0.2.0+c48b2eb6460f1b993d9ddb914cbdc5ff6f1d22c6")] [assembly: AssemblyProduct("Beastwhispering")] [assembly: AssemblyTitle("Beastwhispering")] [assembly: AssemblyMetadata("BuildStamp", "c48b2eb 2026-07-30")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.2.0.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace Beastwhispering { internal static class AggroStage { internal static bool ForceTarget(CharacterAI enemy, Character target) { return AggroTools.ForceTarget(enemy, target); } internal static bool Calm(CharacterAI enemy) { return AggroTools.Calm(enemy); } internal static IEnumerable AisInRange(Vector3 center, float radius, Character exclude) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return AggroTools.AisInRange(center, radius, true, exclude); } internal static CharacterAI Find(string namePart, Vector3 center, float radius, Character exclude) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) CharacterAI result = null; float num = float.MaxValue; foreach (CharacterAI item in AisInRange(center, radius, exclude)) { Character character = ((CharacterControl)item).Character; if (string.IsNullOrEmpty(namePart) || (character.Name != null && character.Name.IndexOf(namePart, StringComparison.OrdinalIgnoreCase) >= 0)) { float num2 = Vector3.Distance(center, ((Component)character).transform.position); if (num2 < num) { num = num2; result = item; } } } return result; } } internal static class BlanketSetup { internal static readonly Dictionary Registered = new Dictionary(StringComparer.OrdinalIgnoreCase); private const string DonorName = "Bandages"; private static bool _done; internal static void Init() { SL.OnPacksLoaded += Setup; } private static void Setup() { if (!_done) { _done = true; SlFeatureSetup.Run("[BLANKET]", Plugin.EnableTemperatureSystem, "EnableTemperatureSystem=false — no blanket items/recipes registered.", PetComfortTable.Blankets.Values, (BlanketDef d) => d.Key, RegisterOne, () => Registered.Count, "blankets"); } } private static void RegisterOne(BlanketDef def) { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Invalid comparison between Unknown and I4 //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Invalid comparison between Unknown and I4 int num = ResolveId(def.Key); if (num < 0) { return; } List list = new List(); foreach (string ingredient in def.Ingredients) { int? num2 = ResolveByName(ingredient, def.Key, "ingredient"); if (!num2.HasValue) { return; } list.Add(num2.Value); } int num3 = ResolveByName("Bandages", def.Key, "donor") ?? list[0]; string text = (((int)def.Side == 1) ? "cold" : "heat"); Item val = SlConsumables.RegisterUsable("[BLANKET]", def.Key, num3, num, def.Key, $"Wrap your companion against the {text}. Relieves up to {def.ReliefSteps} steps of {text} for " + $"{def.DurationSeconds / 60.0:0} minutes — and where the {text} runs deeper than the blanket can counter, " + "it still slows the toll. One wrap at a time; useless against the opposite extreme.", delegate(GameObject host) { host.AddComponent().BlanketKey = def.Key; }, ((int)def.Side == 1) ? "HeatingBlanket.png" : "CoolingBlanket.png", (SpriteBorderTypes)1); if (!((Object)(object)val == (Object)null)) { string text2 = "bw.blanket." + def.Key.Trim().ToLowerInvariant().Replace(' ', '_'); Recipe val2 = SlConsumables.RegisterRecipe("[BLANKET]", def.Key, text2, (CraftingType)2, list, num, 1); if (!((Object)(object)val2 == (Object)null)) { Registered[def.Key] = num; Plugin.Log.LogMessage((object)($"[BLANKET] '{def.Key}': item {num} (donor {num3}), recipe '{text2}' " + string.Format("(Survival: {0}), relieves {1} {2} steps for {3:0}s.", string.Join(" + ", def.Ingredients), def.ReliefSteps, text, def.DurationSeconds))); } } } internal static BlanketDef ForItemId(int itemId) { foreach (KeyValuePair item in Registered) { if (item.Value == itemId && PetComfortTable.Blankets.TryGetValue(item.Key, out var value)) { return value; } } return null; } private static int ResolveId(string key) { int num = default(int); if (!BwIds.TryGet("blanket." + key.Trim().ToLowerInvariant().Replace(' ', '-'), ref num)) { Plugin.Log.LogWarning((object)("[BLANKET] '" + key + "': no ItemID is allocated for this blanket, so it cannot be registered — a made-up id would land on some other mod's item. Allocate one with `bwspecies ids alloc --family blankets --key blanket. --owner Beastwhispering/BlanketSetup --name \"\"`, then `bwspecies ids gen`.")); return -1; } if (Registered.ContainsValue(num)) { Plugin.Log.LogWarning((object)$"[BLANKET] '{key}': ItemID {num} is already registered this session — skipped."); return -1; } return num; } private static int? ResolveByName(string name, string key, string field) { if (ItemNameIndex.TryResolve(name, out var itemId)) { Plugin.Log.LogMessage((object)$"[BLANKET] '{key}': {field} '{name}' resolved to ItemID {itemId}."); return itemId; } Plugin.Log.LogWarning((object)("[BLANKET] '" + key + "': " + field + " '" + name + "' matches no item display name on this locale — blanket skipped.")); return null; } } internal sealed class BlanketUseVeto : IUseVeto { public string Tag => "[BLANKET]"; public bool Owns(int itemId) { return BlanketSetup.ForItemId(itemId) != null; } public UseVeto Check(Item item, Character user) { BlanketDef val = BlanketSetup.ForItemId(item.ItemID); if (val == null) { return null; } PetSave val2 = Plugin.Instance?.ActivePet?.State; if (val2 == null) { return new UseVeto("You have no companion to wrap.", "[BLANKET] use of '" + val.Key + "' vetoed: no active pet."); } if (val2.BlanketKey == val.Key && val2.BlanketSecondsLeft >= val.DurationSeconds - 1.0) { return new UseVeto("Your companion is already snugly wrapped.", "[BLANKET] use of '" + val.Key + "' vetoed: same wrap already at full duration."); } return null; } } internal class BlanketWrapEffect : Effect { public string BlanketKey; public override void ActivateLocally(Character _affectedCharacter, object[] _infos) { try { if (!Plugin.EnableTemperatureSystem.Value) { if ((Object)(object)_affectedCharacter != (Object)null && _affectedCharacter.IsLocalPlayer) { Notify.Player(_affectedCharacter, "The temperature system is disabled — the blanket was not consumed."); } Plugin.Log.LogMessage((object)"[BLANKET] wrap ignored — [Temperature] EnableTemperatureSystem is off (the blanket was not consumed; flip it back + `reloadcfg` to re-enable)."); return; } BlanketDef value; BlanketDef val = (PetComfortTable.Blankets.TryGetValue(BlanketKey ?? "", out value) ? value : null); PetSave val2 = Plugin.Instance?.ActivePet?.State; if (val != null && val2 != null && !((Object)(object)_affectedCharacter == (Object)null)) { bool flag = !string.IsNullOrEmpty(val2.BlanketKey) && val2.BlanketKey != val.Key; val2.BlanketKey = val.Key; val2.BlanketSecondsLeft = val.DurationSeconds; Plugin.Instance.PersistPet(); Item parentItem = ((Effect)this).ParentItem; if ((Object)(object)parentItem != (Object)null) { parentItem.RemoveQuantity(1); } else { Plugin.Log.LogWarning((object)"[BLANKET] no ParentItem to consume — wrap landed, blanket NOT consumed (bug, report)."); } string text = Plugin.Instance.ActivePet.SpeciesId ?? "your companion"; Notify.Player(_affectedCharacter, "You wrap " + text + " in the " + val.Key + "." + (flag ? " The old wrap falls away." : "")); Plugin.Log.LogMessage((object)($"[BLANKET] '{val.Key}' consumed → buff {val.DurationSeconds:0}s on '{text}'" + (flag ? " (replaced the previous wrap)" : "") + ".")); } } catch (Exception ex) { Plugin.Log.LogError((object)("[BLANKET] wrap-on-use failed: " + ex)); } } } internal static class BraceDriver { private static readonly BraceState State = new BraceState(); private static SpecialAttackDef _def; private static CompanionBody _body; private static CompanionCombat _combat; private static bool _synergyOpened; private static int _negated; private static string _lastWindow = "never braced"; private static string _lastTaunt = "none this session"; private static readonly List _pendingRipostes = new List(); 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 CounteredNames() { return new List(State.CounteredUids); } internal static void Enter(CompanionBody body, CompanionCombat combat, SpecialAttackDef def) { _body = body; _combat = combat; _def = def; _synergyOpened = false; _negated = 0; _pendingRipostes.Clear(); State.Begin((double)Time.time, (double)Plugin.BraceWindowSeconds.Value); SetBlockPose(on: true); PlayEnterCue(); Plugin.Log.LogMessage((object)($"[BRACE] '{_body?.SpeciesId}' braced for {Plugin.BraceWindowSeconds.Value:F1}s " + $"(perAttackerOnce={Plugin.BracePerAttackerOnce.Value}, negate={Plugin.BraceNegateCounteredHit.Value}).")); MaybeTaunt(); } private static void MaybeTaunt() { if (!Plugin.BraceEnableTaunt.Value) { _lastTaunt = "skipped ([Brace] EnableTaunt=false)"; return; } Pet pet = Plugin.Instance?.ActivePet; int? obj; if (pet == null) { obj = null; } else { PetSimulation sim = pet.Sim; obj = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue); } int? num = obj; int valueOrDefault = num.GetValueOrDefault(); float num2 = SpecialAttackTable.TauntSeconds(_def, valueOrDefault); if (num2 <= 0f) { _lastTaunt = "none ('" + _body?.SpeciesId + "' has no taunt axis on its SpeciesSpecialAttacks row)"; return; } object obj2; if (pet == null) { obj2 = null; } else { CompanionCombat combat = ((Companion)pet).Combat; obj2 = ((combat != null) ? combat.SpecialAttackTarget : null); } Character val = (Character)obj2; if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)"[TAUNT] no braced-stance target to taunt — nothing pinned."); _lastTaunt = $"no target (would have been {num2:F1}s at loyalty {valueOrDefault})"; return; } object anchor; if (pet == null) { anchor = null; } else { CompanionAnchor anchor2 = ((Companion)pet).Anchor; anchor = ((anchor2 != null) ? anchor2.Current : null); } TauntController.Begin(val, (Character)anchor, num2); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter != (Object)null) { Notify.Player(localPlayerCharacter, pet.SpeciesId + " draws the enemy's fury with an infernal growl!"); } _lastTaunt = $"'{val.Name}' for {num2:F1}s at loyalty {valueOrDefault}"; } internal static bool OnAnchorHit(Character dealer, Object damageSource, Character anchor) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Invalid comparison between Unknown and I4 //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Invalid comparison between Unknown and I4 //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) BraceHitInputs val = new BraceHitInputs { DealerUid = (((Object)(object)dealer != (Object)null) ? UID.op_Implicit(dealer.UID) : null), SelfUid = (((Object)(object)anchor != (Object)null) ? UID.op_Implicit(anchor.UID) : null), SourceIsStatus = (damageSource is StatusEffect), DealerHostile = ((Object)(object)dealer != (Object)null && dealer.Alive && dealer.IsAI && (int)dealer.Faction != 1) }; string text = DescribeSource(damageSource); BraceHitVerdict val2 = BraceRules.Eligible(ref val); if ((int)val2 != 0) { Plugin.Log.LogMessage((object)string.Format("[BRACE] hit from '{0}' via {1} not counter-eligible ({2}) — lands normally.", ((dealer != null) ? dealer.Name : null) ?? "nothing", text, val2)); return false; } CounterVerdict val3 = State.TryCounter(val.DealerUid, (double)Time.time, Plugin.BracePerAttackerOnce.Value); if ((int)val3 != 3) { Plugin.Log.LogMessage((object)$"[BRACE] hit from '{dealer.Name}' via {text} not countered ({val3}) — lands normally."); return false; } _pendingRipostes.Add(dealer); if (Plugin.BraceNegateCounteredHit.Value) { _negated++; } Plugin.Log.LogMessage((object)("[BRACE] COUNTER: '" + dealer.Name + "' struck the braced pet via " + text + " — " + (Plugin.BraceNegateCounteredHit.Value ? "hit negated, " : "") + "riposte next frame.")); return Plugin.BraceNegateCounteredHit.Value; } private static string DescribeSource(Object src) { if (src == (Object)null) { return "src=none"; } string text = ((src is ProjectileWeapon) ? " (missile)" : ((src is Weapon) ? " (melee)" : ((src is StatusEffect) ? " (status tick)" : ((((object)src).GetType().Name.IndexOf("Blast", StringComparison.OrdinalIgnoreCase) >= 0 || src.name.IndexOf("Blast", StringComparison.OrdinalIgnoreCase) >= 0 || src.name.IndexOf("AoE", StringComparison.OrdinalIgnoreCase) >= 0) ? " (AoE?)" : ((src.name.IndexOf("Projectile", StringComparison.OrdinalIgnoreCase) >= 0 || src.name.IndexOf("Bolt", StringComparison.OrdinalIgnoreCase) >= 0) ? " (missile?)" : ""))))); return "src=" + ((object)src).GetType().Name + ":'" + src.name + "'" + text; } internal static void Tick() { if (!State.Open) { return; } if (!Plugin.EnableBrace.Value) { Exit("kill-switch"); } else if ((Object)(object)_body == (Object)null || (Object)(object)((Companion)(Plugin.Instance?.ActivePet?)).Body != (Object)(object)_body) { Exit("body gone"); } else if (!State.Active((double)Time.time) && _pendingRipostes.Count == 0) { Exit("expiry"); } else { if (_pendingRipostes.Count <= 0) { return; } List list = new List(_pendingRipostes); _pendingRipostes.Clear(); foreach (Character item in list) { Riposte(item); } } } private static void Riposte(Character attacker) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)attacker == (Object)null || !attacker.Alive) { Plugin.Log.LogMessage((object)"[BRACE] riposte target died before the counter landed — nothing dealt."); } else if (!((Object)(object)_combat == (Object)null) && !((Object)(object)_body == (Object)null)) { Vector3 val = ((Component)attacker).transform.position - ((Component)_body).transform.position; Vector3 normalized = ((Vector3)(ref val)).normalized; float num = _combat.BaseDamage * Mathf.Max(0f, _def.DamageMultiplier); _combat.DealDamage(attacker, num, normalized, Plugin.BraceRiposteImpact.Value); try { Global.AudioManager.PlaySoundAtPosition((Sounds)12930, ((Component)_body).transform, 0f, 1f, 1f, 1f, 1f); } catch { } Plugin.Log.LogMessage((object)($"[BRACE] riposte hit '{attacker.Name}' for {num:F0} " + $"(base {_combat.BaseDamage:F0} x{_def.DamageMultiplier:F2}, impact {Plugin.BraceRiposteImpact.Value:F0}).")); Pet pet = Plugin.Instance?.ActivePet; SpecialHitContext specialHitContext = new SpecialHitContext { Pet = pet, Def = _def, Target = attacker }; object dealer; if (pet == null) { dealer = null; } else { CompanionAnchor anchor = ((Companion)pet).Anchor; dealer = ((anchor != null) ? anchor.Current : null); } specialHitContext.Dealer = (Character)dealer; specialHitContext.ActiveSigils = (Plugin.EnableSigilSynergies.Value ? SigilSense.ActiveKeysAt(((Component)_body).transform.position) : SigilSense.None); SpecialHitContext ctx = specialHitContext; SpeciesRegistry.For(_body.SpeciesId).OnSpecialAttackHit(in ctx); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if (!_synergyOpened && Plugin.EnableHuntAsOne.Value && (Object)(object)localPlayerCharacter != (Object)null) { _synergyOpened = true; HuntSynergy.Open(attacker); HuntSynergy.ReportPet((StrikeOutcome)1); HuntAsOnePlayer.SyncedStrike(localPlayerCharacter, attacker); HuntAsOnePlayer.OnSpecialAttackFired(localPlayerCharacter); } } } private static void Exit(string reason) { _lastWindow = $"closed ({reason}): countered {State.CounterCount} attacker(s), {_negated} hit(s) negated"; Plugin.Log.LogMessage((object)("[BRACE] " + _lastWindow + ".")); State.End(); _pendingRipostes.Clear(); SetBlockPose(on: false); _def = null; _combat = null; _body = null; } private static void PlayEnterCue() { float num = Mathf.Clamp01(Plugin.BraceCueVolume.Value); if ((Object)(object)_body == (Object)null || num <= 0f) { return; } try { Global.AudioManager.PlaySoundAtPosition((Sounds)12910, ((Component)_body).transform, 0f, num, num, 1f, 1f); } catch { } } internal static void ForceExit(string reason) { if (State.Open) { Exit(reason); } } private static void SetBlockPose(bool on) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 Animator val = (((Object)(object)_body != (Object)null) ? ((Component)_body).GetComponent() : 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() : null); if ((Object)(object)petSpecialAttack == (Object)null) { Plugin.Log.LogWarning((object)"[BRACE] brace: no strike-capable pet body."); return; } PetSpecialAttack.FireOverrides ov = PetSpecialAttack.FireOverrides.Default; ov.BypassCooldown = true; ov.ArmCooldown = false; ov.Tag = "[BRACE]"; PetSpecialAttack.SpecialFireReport specialFireReport = petSpecialAttack.Fire(null, ov); Plugin.Log.LogMessage((object)("[BRACE] brace -> " + specialFireReport.Summary + ((specialFireReport.Started && !specialFireReport.SynergyDeferred) ? " (NB not a Brace species — that was a normal strike)" : ""))); } internal static void BraceDump(Plugin p) { //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)"[BRACE] ── bracedump ──"); Plugin.Log.LogMessage((object)($"[BRACE] config: EnableBrace={Plugin.EnableBrace.Value} WindowSeconds={Plugin.BraceWindowSeconds.Value:F1} " + $"PerAttackerOnce={Plugin.BracePerAttackerOnce.Value} NegateCounteredHit={Plugin.BraceNegateCounteredHit.Value} " + $"RiposteImpact={Plugin.BraceRiposteImpact.Value:F0} CueVolume={Plugin.BraceCueVolume.Value:F2} " + $"EnableTaunt={Plugin.BraceEnableTaunt.Value}")); Plugin.Log.LogMessage((object)$"[BRACE] ReceiveHit prefix attached: {BraceReceiveHit.Attached}"); Pet activePet = p.ActivePet; if (activePet == null) { Plugin.Log.LogMessage((object)"[BRACE] no active pet."); return; } string speciesId = activePet.SpeciesId; SpecialAttackDef val = default(SpecialAttackDef); if (SpecialAttackTable.TryGet(SpecialAttacks.Table, speciesId, ref val)) { AttackKind kind = val.Kind; bool value = Plugin.EnableBrace.Value; CompanionAnchor anchor = ((Companion)activePet).Anchor; BraceRoute val2 = SpecialAttackTable.RouteBrace(kind, value, anchor != null && anchor.HasLiveAnchor); Plugin.Log.LogMessage((object)(string.Format("[BRACE] '{0}': kind={1} route={2} statuses='{3}' ", speciesId, val.Kind, val2, val.StatusEffectId ?? "none") + $"riposte x{val.DamageMultiplier:F2} cooldown={val.CooldownSeconds:F0}s buildup={val.BuildupPercent:F0}")); PetSimulation sim = activePet.Sim; int num = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue) ?? (-1); Plugin.Log.LogMessage((object)((val.TauntMinSeconds > 0f || val.TauntMaxSeconds > 0f) ? ($"[BRACE] taunt axis: {val.TauntMinSeconds:F1}s at loyalty 0 → {val.TauntMaxSeconds:F1}s at 100; " + $"loyalty {num} → {SpecialAttackTable.TauntSeconds(val, (num >= 0) ? num : 0):F1}s") : ("[BRACE] '" + speciesId + "' has no taunt axis — its Hunt as One never taunts."))); } else { Plugin.Log.LogMessage((object)("[BRACE] '" + speciesId + "' has no SpeciesSpecialAttacks row.")); } Plugin.Log.LogMessage((object)("[BRACE] window: " + Forensics)); Plugin.Log.LogMessage((object)("[BRACE] last taunt: " + _lastTaunt)); } } [HarmonyPatch(typeof(Character), "ReceiveHit", new Type[] { typeof(Object), typeof(DamageList), typeof(Vector3), typeof(Vector3), typeof(float), typeof(float), typeof(Character), typeof(float), typeof(bool) })] internal static class BraceReceiveHit { internal static bool Attached; [HarmonyPrefix] private static bool Prefix(Character __instance, ref DamageList __result, Object _damageSource, DamageList _damage, Character _dealerChar) { if (!BraceDriver.Active) { return true; } if (!CompanionAnchor.IsAnchor(__instance)) { return true; } Plugin instance = Plugin.Instance; object obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } } Character val = (Character)obj; if ((Object)(object)val == (Object)null || (Object)(object)__instance != (Object)(object)val) { return true; } if (!BraceDriver.OnAnchorHit(_dealerChar, _damageSource, val)) { return true; } DamageList val2 = _damage.Clone(); for (int i = 0; i < val2.Count; i++) { val2[i].Damage = 0f; } __result = val2; return false; } } internal static class BuffFoodTable { private static readonly TableLoader _loader = new TableLoader("BuffFoods.json", "[BUFFFOOD]", "species buff food", "species buff foods", (Func, Dictionary>)BuffFoods.Parse, (Func, Dictionary, Dictionary>)BuffFoods.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[BUFFFOOD]", Validate); internal static Dictionary Table => _axis.Table; internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } internal static BuffFoodDef Match(string speciesId, int itemId, string itemName) { if (!Plugin.EnableBuffFoods.Value) { return null; } BuffFoodEntry val = BuffFoods.Resolve(Table, speciesId); return BuffFoods.Match(val, itemId, itemName); } internal static BuffFoodDef ActiveDef(Pet pet) { if (!Plugin.EnableBuffFoods.Value) { return null; } PetSave val = pet?.State; if (val == null || string.IsNullOrEmpty(val.BuffFoodKey) || val.BuffFoodSecondsLeft <= 0.0) { return null; } BuffFoodEntry val2 = BuffFoods.Resolve(Table, pet.SpeciesId); return BuffFoods.DefFor(val2, val.BuffFoodKey); } internal static float DamageFactor(Pet pet, LoyaltyTier tier) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return BuffFoods.DamageFactor(ActiveDef(pet), tier, (pet?.State?.BuffFoodSecondsLeft).GetValueOrDefault()); } internal static float DecayFraction(Pet pet, LoyaltyTier tier) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return BuffFoods.DecayFraction(ActiveDef(pet), tier, (pet?.State?.BuffFoodSecondsLeft).GetValueOrDefault()); } internal static string DisplayName(BuffFoodDef def) { if (def == null) { return ""; } if (def.ItemId.HasValue) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(def.ItemId.Value) : null); if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(val.Name)) { return val.Name; } } return def.Key; } internal static string StatusSummary(Pet pet) { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) if (pet?.State == null) { return "no pet state."; } PetSave state = pet.State; if (string.IsNullOrEmpty(state.BuffFoodKey) || state.BuffFoodSecondsLeft <= 0.0) { return "no buff running" + (Plugin.EnableBuffFoods.Value ? "." : " (EnableBuffFoods OFF)."); } BuffFoodEntry val = BuffFoods.Resolve(Table, pet.SpeciesId); BuffFoodDef val2 = BuffFoods.DefFor(val, state.BuffFoodKey); if (val2 == null) { return $"'{state.BuffFoodKey}' — KEY NOT IN TABLE (override removed it?): {state.BuffFoodSecondsLeft:F0}s left, ticking but inert."; } LoyaltyTier loyalty = pet.Sim.Status().Loyalty; return "'" + DisplayName(val2) + "' " + BuffFoods.Describe(val2, loyalty, state.BuffFoodSecondsLeft) + (Plugin.EnableBuffFoods.Value ? "" : " (EnableBuffFoods OFF — inert)"); } private static void Validate() { //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Invalid comparison between Unknown and I4 TableValidator.CheckItemTable("[BUFFFOOD]", "BuffFoods.json", "entry", ItemRefs(), out var _, out var misses); int num = 0; foreach (BuffFoodEntry value in Table.Values) { foreach (BuffFoodDef food in value.Foods) { num++; int num2; if (food.ItemId.HasValue) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; num2 = (((Object)(object)((instance != null) ? instance.GetItemPrefab(food.ItemId.Value) : null) != (Object)null) ? 1 : 0); } else { num2 = 1; } bool flag = (byte)num2 != 0; Plugin.Log.LogMessage((object)("[BUFFFOOD] '" + value.Species + "': '" + food.Key + "' (" + (((int)food.Kind == 1) ? "decay rider" : "damage") + ", " + $"+{food.PercentPerLevel:F0}%/level, " + (food.DurationSeconds.HasValue ? $"{food.DurationSeconds.Value:F0}s" : "one hunger-day") + ", " + (flag ? "armed" : "INERT — item not in registry") + ").")); } } int num3 = ReportDualListed(); Plugin.Log.LogMessage((object)($"[BUFFFOOD] boot check: {num} buff food(s) across {Table.Count} species, " + $"{misses} unresolved item key(s), {num3} dual-listed row(s) (meal + buff), " + $"EnableBuffFoods={Plugin.EnableBuffFoods.Value}.")); } private static int ReportDualListed() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) int num = 0; foreach (BuffFoodEntry value in Table.Values) { IReadOnlyList readOnlyList = PetDietTable.Resolve(value.Species); foreach (DualListedFood item in FeedPrecedence.DualListed(value, readOnlyList, (Func>)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 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 ItemRefs() { foreach (BuffFoodEntry value in Table.Values) { foreach (BuffFoodDef food in value.Foods) { yield return new ItemRef(food.ItemId, food.Key, $"buff food ItemID {food.ItemId}", "buff food '" + food.Key + "'"); } } } internal static void BuffFoodDump(Plugin p) { //IL_0222: Unknown result type (might be due to invalid IL or missing references) //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Invalid comparison between Unknown and I4 //IL_02a7: Unknown result type (might be due to invalid IL or missing references) //IL_02be: Unknown result type (might be due to invalid IL or missing references) //IL_02ca: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)$"[BUFFFOOD] flags: EnableBuffFoods={Plugin.EnableBuffFoods.Value}"); Plugin.Log.LogMessage((object)$"[BUFFFOOD] ── table ({Table.Count} species; 'reloadbufffoods' re-reads the override) ──"); foreach (KeyValuePair item in Table) { foreach (BuffFoodDef food in item.Value.Foods) { object obj; if (!food.ItemId.HasValue) { obj = "(by display name)"; } else { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(food.ItemId.Value) : null); obj = ((val != null) ? ("'" + val.Name + "'") : "UNKNOWN ITEM"); } string text = (string)obj; Plugin.Log.LogMessage((object)("[BUFFFOOD] '" + item.Key + "' <- '" + food.Key + "' " + text + ": " + string.Format("{0} +{1:F0}%/level, ", ((int)food.Kind == 1) ? "decay rider" : "damage", food.PercentPerLevel) + (food.DurationSeconds.HasValue ? $"{food.DurationSeconds.Value:F0}s" : "one hunger-day") + (string.IsNullOrEmpty(food.Toast) ? "" : (", toast=\"" + food.Toast + "\"")))); } } Pet activePet = p.ActivePet; if (activePet?.State == null) { Plugin.Log.LogMessage((object)"[BUFFFOOD] no active pet."); return; } PetSave state = activePet.State; LoyaltyTier loyalty = activePet.Sim.Status().Loyalty; BuffFoodDef val2 = ActiveDef(activePet); if (string.IsNullOrEmpty(state.BuffFoodKey)) { Plugin.Log.LogMessage((object)"[BUFFFOOD] active slot: none."); } else if (val2 == null) { Plugin.Log.LogMessage((object)($"[BUFFFOOD] active slot: '{state.BuffFoodKey}', {state.BuffFoodSecondsLeft:F0}s left — " + (Plugin.EnableBuffFoods.Value ? "KEY NOT IN TABLE / expired (override removed it?): ticking but inert." : "EnableBuffFoods OFF: ticking but inert."))); } else { Plugin.Log.LogMessage((object)("[BUFFFOOD] active slot: " + BuffFoods.Describe(val2, loyalty, state.BuffFoodSecondsLeft) + " " + $"(damage x{DamageFactor(activePet, loyalty):F3}, decay +{DecayFraction(activePet, loyalty) * 100f:F0}% of total).")); } Plugin.Log.LogMessage((object)("[BUFFFOOD] " + PetSystems.PowerSummary(p))); } } internal static class BwAxis { internal static DataAxis> Table(TableLoader loader, string tag, Action validate = null) { return new DataAxis>((ITableSource>)(object)loader, (Action)delegate(Action h) { SL.OnPacksLoaded += h; }, (Func)(() => TableValidator.RegistryReady(tag)), validate); } internal static DataAxis Composite(Func load, Action announceReload, string tag, Action validate = null) where TTable : class { return new DataAxis((ITableSource)(object)new DelegateTableSource(load, announceReload), (Action)delegate(Action h) { SL.OnPacksLoaded += h; }, (Func)(() => TableValidator.RegistryReady(tag)), validate); } } internal sealed class BwCompanionSettings : ICompanionSettings { public float AttackDamage => Plugin.AttackDamage.Value; public float AttackInterval => Plugin.AttackInterval.Value; public float AggroRange => Plugin.AggroRange.Value; public float AttackRange => Plugin.AttackRange.Value; public float CombatLeashDistance => Plugin.CombatLeashDistance.Value; public float DisengageRunHomeSeconds => Plugin.DisengageRunHomeSeconds.Value; public bool AttackVocals => Plugin.PetAttackVocals.Value; public bool AnchorInvisible => Plugin.AnchorInvisible.Value; public bool AnchorShowHealthBar => Plugin.AnchorShowHealthBar.Value; public bool AnchorLinkSummonSlot => Plugin.AnchorLinkSummonSlot.Value; public bool AnchorHideSummonIcon => Plugin.HideSummonIcon.Value; public float AnchorLeashDistance => Plugin.AnchorLeashDistance.Value; public float AnchorRespawnSeconds => Plugin.AnchorRespawnSeconds.Value; public bool AnchorDealsDamage => Plugin.AnchorDealsDamage.Value; public float CritHealthFraction => 0.2f; public float CritRearmFraction => 0.5f; public bool SpeciesVoice => Plugin.AnchorSpeciesVoice.Value; public AnchorGlueMode GlueMode => Plugin.GlueMode.Value; public float GlueOffsetBehind => Plugin.GlueOffsetBehind.Value; public bool UnifyTargets => Plugin.UnifyTargets.Value; public AnchorCollisionMode AnchorPlayerCollision => Plugin.AnchorPlayerCollision.Value; public string GhostPrefabName => "NewGhostOneHandedAlly"; public bool AnchorEnabled => true; public bool SuppressLeashWarp => false; public BodilessAnchorPolicy BodilessAnchor => (BodilessAnchorPolicy)0; public float ModelYawOffset => Plugin.ModelYawOffset.Value; public float LoafDistanceMin => Plugin.LoafDistanceMin.Value; public float LoafDistanceMax => Plugin.LoafDistanceMax.Value; public float LoafRepickDistance => Plugin.LoafDistanceRepick.Value; public string LogTagSuffix => null; } internal static class BwConfig { internal static class Keys { public static ConfigEntry TameKey; public static ConfigEntry FeedKey; public static ConfigEntry SelfTestKey; public static ConfigEntry RecallKey; public static ConfigEntry 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("Keys", "TameKey", new KeyboardShortcut((KeyCode)288, Array.Empty()), "Tame the nearest wild creature."); FeedKey = cfg.Bind("Keys", "FeedKey", new KeyboardShortcut((KeyCode)289, Array.Empty()), "Feed the pet the first inventory item its diet accepts (same ruling as the right-click Feed action)."); SelfTestKey = cfg.Bind("Keys", "SelfTestKey", new KeyboardShortcut((KeyCode)291, Array.Empty()), "Run the Core self-test now."); RecallKey = cfg.Bind("Keys", "RecallKey", new KeyboardShortcut((KeyCode)290, Array.Empty()), "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("Keys", "DiagKey", new KeyboardShortcut((KeyCode)293, Array.Empty()), "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 TameRange; public static ConfigEntry ModelYawOffset; public static ConfigEntry SpeciesYawOffsets; public static ConfigEntry FollowSpeed; public static ConfigEntry MinFollowSpeed; public static ConfigEntry RestHealsPet; internal static void Bind(ConfigFile cfg) { TameRange = cfg.Bind("Pet", "TameRange", 15f, "Search radius (m) to tame / re-form from."); ModelYawOffset = cfg.Bind("Pet", "ModelYawOffset", 180f, "Degrees the creature model is rotated from the transform's forward (hyena ~180; tune if a species faces sideways)."); SpeciesYawOffsets = cfg.Bind("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 ' 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 ' dev command, then persist the value here."); FollowSpeed = cfg.Bind("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("Pet", "MinFollowSpeed", 4.5f, "Species-stats mode only: while merely FOLLOWING you (not fighting), the pet never moves slower than this (m/s) so a naturally slow species can't lose a sprinting player. Combat chases run the true species speed. Matches the old FollowSpeed default."); RestHealsPet = cfg.Bind("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 LoafDistanceMin; public static ConfigEntry LoafDistanceMax; public static ConfigEntry LoafDistanceRepick; internal static void Bind(ConfigFile cfg) { LoafDistanceMin = cfg.Bind("Follow", "LoafDistanceMin", 2f, "Nearest distance (m) a following pet settles from you when it 'loafs' nearby instead of standing on you."); LoafDistanceMax = cfg.Bind("Follow", "LoafDistanceMax", 4f, "Farthest distance (m) a following pet settles from you. 0 = loafing OFF: the pet settles on you (exact pre-feature behavior)."); LoafDistanceRepick = cfg.Bind("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 DiagIntervalSeconds; public static ConfigEntry DiagRadius; public static ConfigEntry CastDiagPatches; public static ConfigEntry MusicReconPatches; internal static void Bind(ConfigFile cfg) { DiagIntervalSeconds = cfg.Bind("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("Diag", "DiagRadius", 30f, "Radius (m) the 'diag' snapshot scans for nearby AI beyond whatever's already listed as engaged in combat."); CastDiagPatches = cfg.Bind("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("Diag", "MusicReconPatches", false, "Re-arm the Bug-12/Bug-4 music PASSIVE taps (MusicRecon): 11 Harmony taps on GlobalAudioManager/GlobalCombatManager (combat start/end, music start/stop/queue, Update-gate, level-clear) logging the [MUSIC-TAP] timeline. OFF by default keeps GlobalAudioManager.Update et al. un-patched. The musiccheck/musicdump verb live-reads GAM state directly and still gives a full snapshot WITHOUT the taps (it degrades with a note) -- only the passive [MUSIC-TAP] timeline needs this on. Patch application is decided in Awake, so a change needs a relaunch."); } } internal static class Harvest { public static ConfigEntry UnloadUnusedAssetsMode; public static ConfigEntry UnloadEveryNHarvests; public static ConfigEntry FlushTerrainAfterPurge; internal static void Bind(ConfigFile cfg) { UnloadUnusedAssetsMode = cfg.Bind("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("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("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 CaptureOnSceneEntry; public static ConfigEntry AutoWarmAtBoot; public static ConfigEntry AlwaysWarmSpecies; internal static void Bind(ConfigFile cfg) { CaptureOnSceneEntry = cfg.Bind("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("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("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(CaptureOnSceneEntry, Expedition.CaptureOnSceneEntry); num += MigrateKey(AutoWarmAtBoot, Expedition.AutoWarmAtBoot); num += MigrateKey(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(ConfigEntry legacy, ConfigEntry 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(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 EnableTamingFoods; public static ConfigEntry TameRadius; public static ConfigEntry TameRecheckGraceMult; public static ConfigEntry RecipeDropChance; internal static void Bind(ConfigFile cfg) { EnableTamingFoods = cfg.Bind("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("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("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("Taming", "RecipeDropChance", 1f, "Global chance (0-1) a tamable creature drops its taming-food recipe scroll on death, multiplied by the per-species dropChance in TamingFoods.json. 1 = guaranteed (the current deliberately-generous default while the loop is play-tested)."); } } internal static class SelfTest { public static ConfigEntry RunSelfTestOnLoad; internal static void Bind(ConfigFile cfg) { RunSelfTestOnLoad = cfg.Bind("SelfTest", "RunSelfTestOnLoad", false, "Run the Core self-test on load."); } } internal static class Systems { public static ConfigEntry InitialLoyalty; public static ConfigEntry HungerSecondsPerDay; public static ConfigEntry TempEscalateSeconds; public static ConfigEntry TempRecoverSeconds; public static ConfigEntry SpeciesDailyDecay; public static ConfigEntry LoyaltyGainPercent; public static ConfigEntry SimTickSeconds; public static ConfigEntry CastWatchdogWarnSeconds; public static ConfigEntry CastWatchdogClearSeconds; public static ConfigEntry FeedHealAmount; public static ConfigEntry EnableFoodHealthRecovery; public static ConfigEntry SatiationFraction; public static ConfigEntry EnablePassiveBuffs; public static ConfigEntry EnableBagPerk; public static ConfigEntry ShowPetStatusIcons; public static ConfigEntry HungryIconFraction; public static ConfigEntry UseSpeciesStats; public static ConfigEntry PersistPetHealth; public static ConfigEntry HealthLoyaltyFactor0; public static ConfigEntry HealthLoyaltyFactor100; public static ConfigEntry DamageLoyaltyFactor0; public static ConfigEntry DamageLoyaltyFactor100; public static ConfigEntry DefenseLoyaltyFactor0; public static ConfigEntry DefenseLoyaltyFactor100; public static ConfigEntry SpeedLoyaltyFactor0; public static ConfigEntry SpeedLoyaltyFactor100; public static ConfigEntry ReleaseConfirmLoyaltyThreshold; internal static void Bind(ConfigFile cfg) { InitialLoyalty = cfg.Bind("Systems", "InitialLoyalty", 50, "Loyalty a freshly tamed pet starts with (0-100)."); HungerSecondsPerDay = cfg.Bind("Systems", "HungerSecondsPerDay", 1200f, "Game-seconds without feeding per 'day' of loyalty decay. 0 or negative DISABLES hunger and decay entirely — the pet then stays permanently feedable and never loses loyalty to hunger (a footgun, not a feature; almost always a typo)."); TempEscalateSeconds = cfg.Bind("Systems", "TempEscalateSeconds", 30f, "Sustained seconds out-of-band to worsen one comfort stage."); TempRecoverSeconds = cfg.Bind("Systems", "TempRecoverSeconds", 15f, "Sustained seconds back-in-band to recover one comfort stage."); SpeciesDailyDecay = cfg.Bind("Systems", "SpeciesDailyDecay", 15, "Loyalty lost per day without feeding."); LoyaltyGainPercent = cfg.Bind("Systems", "LoyaltyGainPercent", 5f, "Percent of every POSITIVE loyalty gain (feeding, kill credit, first region crossing) that actually lands — the 'slow, hard-won bond' balance lever. 5 = a preferred meal is worth +0.5 instead of +10, so a bond takes roughly 20x as much care to build. Nothing is lost to rounding: the leftover fraction is BANKED per pet (saved with it) and the next gain adds onto it, so twenty preferred meals still deliver exactly the ten loyalty one unscaled meal used to. LOSSES are deliberately NOT scaled by this — hunger decay, the pet being hurt or downed, and abandonment all keep their full face value. 100 = the original (pre-2026-07-25) fast-bonding rates. Retunes live via reloadcfg; forensics: the 'gain' fragment in petstatus."); SimTickSeconds = cfg.Bind("Systems", "SimTickSeconds", 2f, "How often (game-seconds) the pet systems advance."); CastWatchdogWarnSeconds = cfg.Bind("Systems", "CastWatchdogWarnSeconds", 5f, "Log (once per cast) when the player's IsCasting has been open this many seconds — pure diagnostics, nothing is touched. Bug-20 history: the watchdog used to FORCE-CLEAR at 1s, which killed every legitimate long vanilla cast (taming-food eating, flint-and-steel campfires)."); CastWatchdogClearSeconds = cfg.Bind("Systems", "CastWatchdogClearSeconds", 30f, "Force-clear a wedged IsCasting after this many seconds (bug-16 class: the animation event never fires, so the flag would stay stuck FOREVER and silently block Hunt as One / other casts). Must be comfortably longer than the slowest legitimate vanilla cast. 0 = never auto-clear (the 'castclear' verb remains the manual remedy)."); FeedHealAmount = cfg.Bind("Systems", "FeedHealAmount", 10f, "Pet HP healed by a successful feed when the food's PetDiets.json entry has no 'heal' of its own."); EnableFoodHealthRecovery = cfg.Bind("Systems", "EnableFoodHealthRecovery", true, "Feeding the pet ALSO starts a vanilla-style Health Recovery regen on it (on top of the instant feed heal): level 1-5 per the food's PetDiets.json 'healthRecovery' (default 1; the vanilla player ladder — 0.2/0.25/0.3/0.4/0.5 HP per second for 600s). A taming food (species chow) always grants level 5, including the tame itself. A new feed REPLACES the running regen (fresh 600s, even at a lower level — vanilla semantics). OFF = feeding grants no regen and no regen HP is applied (a running clock still ticks down inert, so a live re-enable resumes it). Flip works live via reloadcfg; forensics: the [REGEN] log lines + petstatus."); SatiationFraction = cfg.Bind("Systems", "SatiationFraction", 0.25f, "A pet fed within this fraction of its hunger 'day' (HungerSecondsPerDay, or the species override) is completely satiated and refuses ALL food. 0 = pets never refuse."); EnablePassiveBuffs = cfg.Bind("Systems", "EnablePassiveBuffs", true, "Grant the bond's passive buff to the PLAYER (per-species stat + percent in SpeciesBuffs.txt, scaled by the pet's loyalty tier: Gone=0 ... Devoted=4 levels)."); EnableBagPerk = cfg.Bind("Systems", "EnableBagPerk", true, "Grant the bond's flat backpack-capacity gift to the PLAYER's equipped bag (BagCapacity lines in SpeciesBuffs.txt, stepped min..max over the loyalty tiers). Follows the BOND, not the body: a downed pet keeps carrying for you; release/abandonment/permanent death withdraw it. Flip works live via reloadcfg."); ShowPetStatusIcons = cfg.Bind("Systems", "ShowPetStatusIcons", true, "Show the pet's hunger/bond state as status-effect icons on the player HUD (indicator-only statuses; flip works live via reloadcfg)."); HungryIconFraction = cfg.Bind("Systems", "HungryIconFraction", 0.75f, "Hunger fraction (of the pet's hunger 'day') at which the Hungry icon appears; the Starving icon appears at a full day (1.0)."); UseSpeciesStats = cfg.Bind("Systems", "UseSpeciesStats", true, "The pet keeps the tamed creature's OWN combat stats (docs/pet-stats-plan.md): resistances/protection/impact-res/barrier on the anchor, its natural per-type attack damage + impact, its max health, and its movement speed — all captured at tame time, saved with the pet, and re-tuned live by the loyalty factors below. OFF = the pre-feature config numbers (AnchorBaseHealth / AttackDamage / FollowSpeed) apply everywhere."); PersistPetHealth = cfg.Bind("Systems", "PersistPetHealth", true, "Persist the pet's current-HP FRACTION across loading screens and reloads. Fixes the bug where a fresh combat anchor healed to FULL on a zone transition that respawned it (e.g. the pet reads 150/150 in one town but the correct 53/150 in the field). OFF = the pre-fix behaviour (a re-formed/reloaded pet loads at full HP; the save column is written as full). A genuine downed-death still returns the pet at full either way. Flip works live via reloadcfg; forensics: the [PERSIST]/[STATS] log lines."); HealthLoyaltyFactor0 = cfg.Bind("Systems", "HealthLoyaltyFactor0", 0.5f, "Pet max-HP multiplier at loyalty 0 (lerps linearly to HealthLoyaltyFactor100 at loyalty 100). Applies to the species HP when UseSpeciesStats, else to AnchorBaseHealth — the defaults reproduce the original 0.5x..1.5x curve."); HealthLoyaltyFactor100 = cfg.Bind("Systems", "HealthLoyaltyFactor100", 1.5f, "Pet max-HP multiplier at loyalty 100."); DamageLoyaltyFactor0 = cfg.Bind("Systems", "DamageLoyaltyFactor0", 1f, "Pet attack-damage (and impact) multiplier at loyalty 0. Default 1/1 = loyalty does not scale damage (responsiveness already does); raise the 100 end to make devotion hit harder."); DamageLoyaltyFactor100 = cfg.Bind("Systems", "DamageLoyaltyFactor100", 1f, "Pet attack-damage (and impact) multiplier at loyalty 100."); DefenseLoyaltyFactor0 = cfg.Bind("Systems", "DefenseLoyaltyFactor0", 1f, "Pet defense multiplier (resistances/protection/barrier/impact-res) at loyalty 0. Scaled resistances clamp at 100%."); DefenseLoyaltyFactor100 = cfg.Bind("Systems", "DefenseLoyaltyFactor100", 1f, "Pet defense multiplier at loyalty 100."); SpeedLoyaltyFactor0 = cfg.Bind("Systems", "SpeedLoyaltyFactor0", 1f, "Pet movement-speed multiplier at loyalty 0."); SpeedLoyaltyFactor100 = cfg.Bind("Systems", "SpeedLoyaltyFactor100", 1f, "Pet movement-speed multiplier at loyalty 100."); ReleaseConfirmLoyaltyThreshold = cfg.Bind("Systems", "ReleaseConfirmLoyaltyThreshold", 0, "Releasing a pet is IRREVERSIBLE (the bond ends and that pet's save file is deleted), so at or above this loyalty a release must be CONFIRMED: the first Release Pet cast — or the first 'release' verb — only warns and arms a 30s window; a second cast/call inside that window, or 'release confirm', actually does it. 0 (the default) = ALWAYS confirm; a NEGATIVE value disables the gate entirely for scripted runs. History: on 2026-07-27 a single unconfirmed 'release' destroyed a real loyalty-35 bond built over a whole session (docs/hunt-synergy-testplan.md V20) — a value you have to guess protects nobody, which is why the default guards every pet."); } } internal static class Temperature { public static ConfigEntry EnableTemperatureSystem; public static ConfigEntry EnableWeatherFoods; public static ConfigEntry PetDeathModeConfig; public static ConfigEntry SufferingDrainPerMinute; public static ConfigEntry CriticalDrainPerMinute; public static ConfigEntry UneasyDecayMult; public static ConfigEntry SufferingDecayMult; internal static void Bind(ConfigFile cfg) { EnableTemperatureSystem = cfg.Bind("Temperature", "EnableTemperatureSystem", true, "Master switch for the pet temperature/comfort system (ambient sampling, discomfort stages, drain, death, blankets). OFF = exact pre-feature behavior: the pet always reads as comfortable."); EnableWeatherFoods = cfg.Bind("Temperature", "EnableWeatherFoods", true, "Feeding the pet a weather-resist consumable (warming/cooling potions, teas, water — WeatherFoods.json) grants it the player's hot/cold relief; all pets may drink water. OFF = feeding ignores the table entirely (a running drink buff keeps ticking but grants nothing, so a live re-enable resumes it)."); PetDeathModeConfig = cfg.Bind("Temperature", "PetDeathMode", (PetDeathMode)0, "What happens when temperature exposure drains the pet to zero: Permanent = it dies, bond deleted; KnockedOut = it collapses (the existing downed/re-form path); Disabled = it holds at 1 HP, suffering but unkillable by climate."); SufferingDrainPerMinute = cfg.Bind("Temperature", "SufferingDrainPerMinute", 2f, "Pet HP drained per minute (as PERCENT of its max) while Suffering (2 steps out of band, sustained)."); CriticalDrainPerMinute = cfg.Bind("Temperature", "CriticalDrainPerMinute", 6f, "Pet HP drained per minute (percent of max) while Critical (3+ steps out, sustained). At zero the PetDeathMode above decides."); UneasyDecayMult = cfg.Bind("Temperature", "UneasyDecayMult", 1.5f, "Daily loyalty-decay multiplier while the pet is Uneasy (1 step out of band)."); SufferingDecayMult = cfg.Bind("Temperature", "SufferingDecayMult", 2f, "Daily loyalty-decay multiplier while Suffering or Critical."); } } internal static class Combat { public static ConfigEntry EnableCombat; public static ConfigEntry AttackDamage; public static ConfigEntry AttackInterval; public static ConfigEntry AggroRange; public static ConfigEntry AttackRange; public static ConfigEntry CombatLeashDistance; public static ConfigEntry EnableSpecialAttack; public static ConfigEntry EngageRange; public static ConfigEntry EngageConeDegrees; public static ConfigEntry DisengageRunHomeSeconds; public static ConfigEntry PetAttackVocals; public static ConfigEntry PetDamageScalePercent; public static ConfigEntry SpeciesDamageScales; public static ConfigEntry UngovernedPetDamagePercent; internal static void Bind(ConfigFile cfg) { EnableCombat = cfg.Bind("Combat", "EnableCombat", true, "Let the pet attack what you're fighting."); AttackDamage = cfg.Bind("Combat", "AttackDamage", 25f, "Base damage per pet hit (then scaled by responsiveness)."); AttackInterval = cfg.Bind("Combat", "AttackInterval", 1.4f, "Seconds between pet attacks."); AggroRange = cfg.Bind("Combat", "AggroRange", 12f, "How far the pet will engage an enemy you're fighting."); AttackRange = cfg.Bind("Combat", "AttackRange", 2.6f, "Melee reach for a pet attack."); CombatLeashDistance = cfg.Bind("Combat", "CombatLeashDistance", 40f, "While actively fighting, the pet's and anchor's leashes relax to this (m) so they can actually reach enemies engaged at range. The tighter follow leashes apply only out of combat (round-9 Finding 10: a 14-20m leash yanked the pet home right as it reached anything fought at a distance, forever)."); EnableSpecialAttack = cfg.Bind("Combat", "EnableSpecialAttack", true, "Let the player fire the pet's 'Hunt as One' signature attack — cast from the quickslot skill (or the 'special' dev verb for headless testing; the old SpecialAttackKey bind was DELETED per Bug 26). Requires EnableCombat."); EngageRange = cfg.Bind("Combat", "EngageRange", 20f, "How far (m) the Command Pet skill's ENGAGE scans for an enemy in front of you when you have no locked target (your locked target is honored at any range)."); EngageConeDegrees = cfg.Bind("Combat", "EngageConeDegrees", 120f, "Total width (degrees) of the 'in front of you' cone the ENGAGE scan uses when you have no locked target."); DisengageRunHomeSeconds = cfg.Bind("Combat", "DisengageRunHomeSeconds", 12f, "Session-9 finding: when a fight ends (DISENGAGE order or the enemy dies) beyond the tight follow leash, the pet warp-TELEPORTED home on the next tick. For this many seconds after any fight ends, the relaxed CombatLeashDistance keeps applying so the pet visibly RUNS back; if it still hasn't made it home when the window closes, the normal leash warp fires as the stuck-backstop. 0 = the old instant-warp behavior."); PetDamageScalePercent = cfg.Bind("Combat", "PetDamageScalePercent", 100f, "Scale every hit the PET itself deals, as a percent of its normal damage. 100 = unchanged. Covers the auto-attack, Hunt as One, the ranged bolt and For the Kill (they all multiply one number, CompanionCombat.BaseDamage), in both UseSpeciesStats modes. Does NOT touch the player's own synced bonus strike ([HuntAsOne] BonusDamage), knockback/impact, or the un-multiplied [Anchor] AnchorDealsDamage path (off by default). Clamped to 0-1000 (0 = a pet that deals nothing; the ceiling is a typo guard). Retune live with `reloadcfg`; forensics: `statdump`."); SpeciesDamageScales = cfg.Bind("Combat", "SpeciesDamageScales", "", "Per-species overrides of PetDamageScalePercent, as 'Species=percent' pairs — e.g. 'Hyena=60, Pearlbird=25'. An exact species name wins; otherwise the LONGEST matching name does, so 'Hyena=60' also covers 'Armored Hyena' unless that has its own entry. Empty = every species uses the global. Malformed entries are skipped with a warning, never fatal."); UngovernedPetDamagePercent = cfg.Bind("Combat", "UngovernedPetDamagePercent", 50f, "While the pet-bond's owner has NOT learned the Wild Unknown breakthrough, every hit the pet deals is scaled to this percent of its potential. 100 = no penalty (full damage even without the breakthrough). Composes multiplicatively with PetDamageScalePercent and covers all four pet damage paths (auto-attack, Hunt as One, the ranged bolt, For the Kill). Independent of [Skills] EnableWildUnknownGate. Clamped to 0-1000. Retune live with `reloadcfg`; forensics: `statdump`."); } internal static void BindVocals(ConfigFile cfg) { PetAttackVocals = cfg.Bind("Combat", "PetAttackVocals", true, "Play the species attack sound from the visible body when its bite animation fires. ON by default — session-verified as the pet's working attack voice (the anchor ghost has no sound manager to vocalize through — Bug 14). Toggle live with the `vocals` verb."); } } internal static class HuntAsOne { public static ConfigEntry EnableHuntAsOne; public static ConfigEntry HuntAsOneMeleeAnimation; public static ConfigEntry HuntAsOneBowAnimation; public static ConfigEntry HuntAsOneBonusDamage; public static ConfigEntry HuntAsOneBowShotDelay; public static ConfigEntry EnableFoodHexes; public static ConfigEntry HexMealWindow; public static ConfigEntry HexBuildupPercent; public static ConfigEntry EnableRangedSpecial; public static ConfigEntry BoltMaxFlightSeconds; public static ConfigEntry HonestHits; public static ConfigEntry SyncSkillCooldownToPet; public static ConfigEntry BaseSkillCooldownSeconds; internal static void Bind(ConfigFile cfg) { EnableHuntAsOne = cfg.Bind("HuntAsOne", "EnableHuntAsOne", true, "When the pet's special attack fires, ALSO play the player's own synced strike (animation + bonus damage) on the same target. Requires EnableSpecialAttack."); HuntAsOneMeleeAnimation = cfg.Bind("HuntAsOne", "MeleeAnimation", "Probe", "Character.SpellCastType name for the player's flourish when a melee weapon (or none) is equipped. HuntAsOneCastSync stamps this (with CastModifier=Attack) onto the skill's own CastType/CastModifier fields every frame, so Outward's NATIVE cast pipeline plays a REAL weapon swing via AttackInput -- no CastSpell override needed (docs/bug19-native-cast-plan.md). 'Probe' is a Polearm weapon-skill anim; swap for any other SpellCastType name, no relaunch needed."); HuntAsOneBowAnimation = cfg.Bind("HuntAsOne", "BowAnimation", "PowerShot", "Character.SpellCastType name for the player's flourish when a Bow is equipped (bows have no 'Probe' clip). Stamped with CastModifier=Immobilized (NEVER Attack for bow -- that's Bug 13 Round 1's AttackInput charge-path crash) -- same native-cast mechanism as MeleeAnimation. 'PowerShot' is the closest-named candidate for the real Sniper Shot skill's look (Outward has no literal 'SniperShot' entry); if it's wrong, try PierceShot/BloodShot/StrafingShot or fall back to 'ShootProjectile' (the plain bow-fire anim, confirmed generic) -- no relaunch needed."); HuntAsOneBonusDamage = cfg.Bind("HuntAsOne", "BonusDamage", 20f, "Flat bonus Physical damage the player's synced strike deals to the pet's target (docs/taming-handoff.md Part 12's 'bonus Impact'). 0 = animation only, no damage."); HuntAsOneBowShotDelay = cfg.Bind("HuntAsOne", "BowShotDelay", 0.5f, "Bug 13 fix (2026-07-04): seconds to wait after the bow's draw animation starts before InstantLoadWeapon()+ForceShoot() actually fire the real arrow. Decompiling RangeAttackSkill showed the REAL Sniper Shot skill fires these as TWO SEPARATE Animation Events baked into the draw clip's timeline (ShootProjectileCastInit near the start, ShootProjectileCast — the real ForceShoot() call — at the release frame), not back-to-back in the same frame. This flat delay approximates that Init/Cast gap since we have no animation-event hook of our own to hang the real timing off of -- tune to taste live (no relaunch needed) if the arrow fires before/after the visual draw completes."); EnableFoodHexes = cfg.Bind("HuntAsOne", "EnableFoodHexes", true, "Food-driven hex build-up (docs/food-hex-plan.md): a species with a FoodHexes.json entry inflicts hex BUILD-UP on its Hunt-as-One hit, chosen by the pet's last HexMealWindow mapped meals (same food stacks: 3x Oil = 3x the percent of Scorch). Gates BOTH meal recording and application — off = exact pre-feature behavior (the saved meal history is kept but frozen)."); HexMealWindow = cfg.Bind("HuntAsOne", "HexMealWindow", 3, $"How many of the pet's most recent MAPPED meals drive the hex mix. Unmapped foods are never recorded and never displace a mapped meal. Clamped 1..{20}; a per-species 'window' in FoodHexes.json overrides this."); HexBuildupPercent = cfg.Bind("HuntAsOne", "HexBuildupPercent", 20f, "Hex build-up percent contributed per mapped meal (the game's gauge: a status procs at 100, spill-over kept, scaled by the target's resistances). A per-species 'buildUpPercent' in FoodHexes.json overrides this."); } internal static void BindRanged(ConfigFile cfg) { HonestHits = cfg.Bind("HuntAsOne", "HonestHits", true, "Hunt-as-One strikes are FAILABLE (docs/hunt-synergy-plan.md): the pet's hit and the player's synced strike are judged with the engine's own connect rules before dealing — a blocking target (frontal, 60°/80°-shield), a dodge-roll's i-frames, or an out-of-reach/behind-you target means that half deals NOTHING (a frontal block plays the real block thunk instead). OFF = the pre-feature guaranteed strikes; Synergy then counts every resolve as landed. Live via reloadcfg."); EnableRangedSpecial = cfg.Bind("HuntAsOne", "EnableRangedSpecial", true, "Ranged signature attacks (docs/mantis-bolt-plan.md): a species whose SpeciesSpecialAttacks.txt row says Kind=Ranged (Mantis Shrimp) fires its OWN captured projectile on Hunt-as-One, resolving damage/status/food-hexes when the bolt actually LANDS (an honest miss lands nothing; cooldown stands). Off = every Kind is treated as Melee — exact pre-feature behavior. Gates FIRING only: the projectile capture at tame/re-form always runs for Ranged rows, so a live reloadcfg re-enable works without a re-tame."); BoltMaxFlightSeconds = cfg.Bind("HuntAsOne", "BoltMaxFlightSeconds", 6f, "Backstop ceiling (seconds) on waiting for a fired bolt's impact before ruling a timeout-miss. The projectile's own Lifespan expiry usually reports the miss first (the shorter of the two wins); this only guards a bolt destroyed mid-flight without reporting. Read live — reloadcfg applies it to the next shot."); SyncSkillCooldownToPet = cfg.Bind("HuntAsOne", "SyncSkillCooldownToPet", true, "Bug 30 — the ONE kill-switch for the conflicting-cooldowns fix. Hunt as One used to have TWO clocks: the SKILL's own cooldown (1s, and the only one the hotbar radial draws) and the PET SPECIAL's (SpeciesSpecialAttacks CooldownSeconds, 8-12s). The icon therefore said READY while the ability could not fire, and a press in that window spent stamina, swung the sword, burned the skill cooldown, and did nothing (Cobalt saw it 5x in one fight). ON = the skill advertises the active species' REAL cooldown, so the engine's own on-cooldown gate refuses an early press for free (no stamina, no swing) and the radial shows the true countdown; the pet then adopts the cooldown the engine actually committed, so your cooldown-REDUCTION gear shortens the pet's signature attack too (Cobalt's ruling). It also arms the up-front veto that greys the button when the pet has nothing to strike. OFF = exact pre-fix behaviour, and the base cooldown below is re-stamped back onto the skill. Live via reloadcfg, both ways."); BaseSkillCooldownSeconds = cfg.Bind("HuntAsOne", "BaseSkillCooldownSeconds", 1f, "The Hunt-as-One skill's own action-economy cooldown (seconds) used when there is NO species cooldown to sync to — no pet, a species with no SpeciesSpecialAttacks row, or SyncSkillCooldownToPet=false. This is HuntAsOne.xml's original 1: a double-press debounce, not a real gate. Outward has no global cooldown, so 0 = none."); } } internal static class Brace { public static ConfigEntry EnableBrace; public static ConfigEntry WindowSeconds; public static ConfigEntry PerAttackerOnce; public static ConfigEntry NegateCounteredHit; public static ConfigEntry RiposteImpact; public static ConfigEntry CueVolume; public static ConfigEntry EnableTaunt; internal static void Bind(ConfigFile cfg) { EnableBrace = cfg.Bind("Brace", "EnableBrace", true, "Brace-kind signature attacks (the Armored Hyena's counterattack stance): Hunt-as-One on a Kind=Brace species plants the pet for WindowSeconds; every enemy blow that lands on it (melee, missile, AoE — vanilla-Brace coverage) is countered: the hit is negated and the attacker eats a heavy-impact riposte plus one of the row's statuses at random. OFF = a Brace row runs the ordinary melee strike — exact pre-feature behavior. Live via reloadcfg (an open window closes on the next tick)."); WindowSeconds = cfg.Bind("Brace", "WindowSeconds", 5f, "How long the braced window stays open (seconds). The cooldown that gates re-entry is the species' own SpeciesSpecialAttacks CooldownSeconds, exactly like every other signature attack."); PerAttackerOnce = cfg.Bind("Brace", "PerAttackerOnce", true, "Counter each unique attacker only ONCE per window (its later hits land normally on the pet's armor — a mob of three still gets three counters, but one fast attacker can't be stonewalled all window). false = counter EVERY eligible hit (closer to vanilla Brace, effectively brief invulnerability). Cobalt's A/B knob — live via reloadcfg."); NegateCounteredHit = cfg.Bind("Brace", "NegateCounteredHit", true, "A countered hit deals ZERO damage to the pet (the vanilla counter rule — the whole hit is consumed by the counter). false = the counter still riposts but the hit ALSO lands, for a softer tank."); RiposteImpact = cfg.Bind("Brace", "RiposteImpact", 60f, "Impact (knockback) the riposte carries — the 'heavy impact damage' half of the counter; vanilla heavy attacks run ~50-80, enough to stagger most humanoids. The riposte's DAMAGE is the row's mult x BaseDamage."); CueVolume = cfg.Bind("Brace", "CueVolume", 0.5f, "Volume (0..1) of the parry-window cue (SFX_SKILL_Brace), played ONCE at brace-enter. Cobalt's 2026-07-14 session-2 tuning: the original every-2s repeat read as loud/annoying — the repeat is gone and the single play defaults to half volume. 0 = no cue at all (the pet's growl still marks the stance opening). Live via reloadcfg."); EnableTaunt = cfg.Bind("Brace", "EnableTaunt", true, "The Hunt-as-One taunt (Cobalt's 2026-07-29 ruling — it used to fire from For the Kill instead): a species whose SpeciesSpecialAttacks.txt row carries the two taunt columns locks its CURRENT target's aggro onto the pet's anchor when it enters the stance — ONE provocation per cast, fired at Brace entry, and a provocation not a payload (it pins whether or not a counter ever lands). The duration is LOYALTY-SCALED: the row's min at loyalty 0 rising linearly to its max at 100 (the Armored Hyena ships 1s→5s), so a devoted tank holds a fight where a fraying one only interrupts it. Held by a 0.3s re-assert loop; released on expiry, death, or beyond [Combat] CombatLeashDistance. OFF = the stance still counters and riposts, it just never pulls aggro. Live via reloadcfg (an active taunt releases on its next tick). Forensics: `bracedump` (the axis + what it resolves to at this pet's loyalty) and `tauntdump` (the hold + the aggro census)."); } } internal static class SkillEcho { public static ConfigEntry EnableSkillEcho; public static ConfigEntry EchoDamageMult; public static ConfigEntry EchoImpactMult; public static ConfigEntry EchoWindupSeconds; public static ConfigEntry EchoCooldownSeconds; public static ConfigEntry EchoRangeMeters; public static ConfigEntry EchoWhilePassive; public static ConfigEntry CueOnPet; public static ConfigEntry CueStatusName; public static ConfigEntry CueSeconds; internal static void Bind(ConfigFile cfg) { EnableSkillEcho = cfg.Bind("SkillEcho", "EnableSkillEcho", true, "The skill-echo system: casting one of the classic weapon skills (Puncture, Moon Swipe, Pommel Counter & co — the fixed 10-skill roster, run `echodump` for the list) makes the active pet strike your target with a small echo attack. OFF = the cast hook bails instantly, exact pre-feature behavior. Live via reloadcfg."); EchoDamageMult = cfg.Bind("SkillEcho", "EchoDamageMult", 1.25f, "Default echo damage as a multiplier of the pet's own BaseDamage (every roster skill, every species). A species' SkillEchoes.json entry can override it per skill. Live via reloadcfg."); EchoImpactMult = cfg.Bind("SkillEcho", "EchoImpactMult", 1.25f, "Default echo impact (knockback) as a multiplier of the pet's own attack impact. Per-skill overridable like the damage default. Live via reloadcfg."); EchoWindupSeconds = cfg.Bind("SkillEcho", "EchoWindupSeconds", 0.4f, "Seconds between the echo's attack animation starting and the hit resolving (the pet's brain-stripped body has no animation-event hit frame — the timer fakes one, same as the signature attack)."); EchoCooldownSeconds = cfg.Bind("SkillEcho", "EchoCooldownSeconds", 1f, "Minimum seconds between echoes — the anti-spam debounce for CHAINED different skills (each vanilla skill already gates its own re-cast). 0 = every roster cast echoes."); EchoRangeMeters = cfg.Bind("SkillEcho", "EchoRangeMeters", 10f, "The pet must be within this many meters of the picked target or the echo skips (no magic hit landing on a locked target across the map). Generous by default — the pet is normally already in the fight."); EchoWhilePassive = cfg.Bind("SkillEcho", "EchoWhilePassive", false, "true = the echo fires even while the pet is in Disengage (passive) stance — one strike, then it stays passive. Default false per Cobalt's 2026-07-15 call: a disengaged pet stays fully passive, no echo, no cue."); CueOnPet = cfg.Bind("SkillEcho", "CueOnPet", true, "Where the red-lines cue VFX plays: true = on the pet's body as it strikes (Cobalt's 2026-07-15 call), false = on the player character."); CueStatusName = cfg.Bind("SkillEcho", "CueStatusName", "Rage", "The status effect whose FX prefab is borrowed for the echo cue — the Enrage boon's red lines. The status is NEVER applied, only its visual is flashed. 'Rage' is the best-candidate IdentifierName for the Enrage boon (unverified offline — testplan V6); a name that doesn't resolve falls back through the built-in candidate ladder, then warns once and skips the cue (the echo itself is unaffected). Empty = no cue."); CueSeconds = cfg.Bind("SkillEcho", "CueSeconds", 2f, "How long the cue VFX lives before it is destroyed (seconds). 0 = no cue."); } } internal static class Synergy { public static ConfigEntry EnableSynergy; public static ConfigEntry PercentPerStack; public static ConfigEntry MaxStacks; public static ConfigEntry DurationSeconds; public static ConfigEntry WindowSeconds; public static ConfigEntry RequireSameTarget; public static ConfigEntry PlayerMeleeRangeMeters; public static ConfigEntry PlayerMeleeConeDegrees; public static ConfigEntry PetMeleeRangeMeters; public static ConfigEntry ObserverPatch; internal static void Bind(ConfigFile cfg) { EnableSynergy = cfg.Bind("Synergy", "EnableSynergy", true, "The Hunt-as-One Synergy buff (docs/hunt-synergy-plan.md): a cast whose pet strike AND player strike both LAND grants a stack of Synergy — +PercentPerStack% ALL damage for player and pet per stack, up to MaxStacks, shown as a stacking status on the HUD. OFF = no attempts, no grants, the multipliers converge to 1.0 and a leftover status clears on the next tick. Live via reloadcfg; forensics: `synergydump`."); PercentPerStack = cfg.Bind("Synergy", "PercentPerStack", 0.5f, "ALL-damage bonus percent per Synergy stack, for BOTH the player (multiplier stat stack per damage type) and the pet (CompanionCombat damage fold). 4 stacks at 0.5 = +2%."); MaxStacks = cfg.Bind("Synergy", "MaxStacks", 4, "Stack cap, enforced at grant time (the status family itself is uncapped so this retunes live). A grant at the cap still refreshes every running stack's clock."); DurationSeconds = cfg.Bind("Synergy", "DurationSeconds", 60f, "Lifespan of the Synergy stacks. Every grant refreshes ALL stacks to this, so the set expires together. Applies to the NEXT grant/refresh after a reloadcfg."); WindowSeconds = cfg.Bind("Synergy", "WindowSeconds", 6f, "How long after a Hunt-as-One cast the two halves may land and still count (covers the pet's windup + a bolt/arrow's flight). A half still pending when it closes reads TimedOut — no stack."); RequireSameTarget = cfg.Bind("Synergy", "RequireSameTarget", false, "When true, the player's landed hit must be on the PET's target to count. Default false: the auto-fired arrow legitimately strikes whatever it strikes in the pile."); PlayerMeleeRangeMeters = cfg.Bind("Synergy", "PlayerMeleeRangeMeters", 3.5f, "Melee: how close (m) the player must be to the pet's target for the synced strike to CONNECT. Beyond it the strike whiffs — no damage, no stack."); PlayerMeleeConeDegrees = cfg.Bind("Synergy", "PlayerMeleeConeDegrees", 100f, "Melee: total width (degrees) of the player's front cone the target must be inside for the synced strike to connect — you have to be facing the fight. 0 or 360 disables the facing check."); PetMeleeRangeMeters = cfg.Bind("Synergy", "PetMeleeRangeMeters", 4.5f, "How close (m) the pet must be to its target at the windup's impact frame for its melee special to CONNECT (the fake-windup strike used to land from any distance). Ranged bolts judge at physical impact instead."); ObserverPatch = cfg.Bind("Synergy", "ObserverPatch", false, "Diagnostic (not a gate): a Character.HasHit postfix logging the player's REAL weapon connects ([SYNERGY] observer lines) while a synergy attempt is open — evidence to compare the judged verdict against the Probe swing's actual physics (plan risk R1). Patch application is decided in Awake, so a change needs a relaunch."); } } internal static class ForTheKill { public static ConfigEntry EnableForTheKill; public static ConfigEntry CooldownSeconds; public static ConfigEntry BaseDamageMult; public static ConfigEntry DamagePerStackPercent; public static ConfigEntry EnableKillFavor; public static ConfigEntry KillFavorDurationSeconds; internal static void Bind(ConfigFile cfg) { EnableForTheKill = cfg.Bind("ForTheKill", "EnableForTheKill", true, "The For the Kill skill (docs/hunt-synergy-plan.md): consume ALL Synergy stacks for one joint pet+player strike at BaseDamageMult x (1 + DamagePerStackPercent% per stack), plus the species' ForTheKill.json debuff. Gates the CAST BODY only — the skill stays learnable, so a live reloadcfg flip works both ways. Forensics: `ftkdump`; retune the debuff table live: config-override ForTheKill.json + `reloadftk`."); CooldownSeconds = cfg.Bind("ForTheKill", "CooldownSeconds", 60f, "For the Kill cooldown in seconds (Cobalt's 2026-07-12 retune: 180 -> 15; 2026-07-16 retune: 15 -> 60). Stamped onto the skill prefab + every learned instance each sim tick and on reloadcfg, so a .cfg edit retunes the LIVE skill. EVERY cast burns the full cooldown (Cobalt's live session-1 ruling — the swing has already played, a wasted swing with a free cooldown reads as a bug): no companion, no target, zero stacks, dodged/blocked — all burn. Stacks are only consumed when the strike actually starts."); BaseDamageMult = cfg.Bind("ForTheKill", "BaseDamageMult", 1.5f, "Base multiplier on the joint strike (applies to the pet's signature damage AND the player's synced bonus), before the per-stack scaling. A 0-stack cast still hits at this."); DamagePerStackPercent = cfg.Bind("ForTheKill", "DamagePerStackPercent", 35f, "Extra damage percent per Synergy stack consumed: total = BaseDamageMult x (1 + this/100 x stacks). Defaults 1.5/35 -> x3.6 at 4 stacks."); EnableKillFavor = cfg.Bind("ForTheKill", "EnableKillFavor", true, "The killing-favor buff: when a For the Kill execute (pet's blow OR the player's synced strike) actually KILLS its target, the species' ForTheKill.json 'killBuff' entry grants the player a timed stat buff scaled by the pet's loyalty tier. Off = strike/debuff only, no buff check. Forensics: `killfavordump`."); KillFavorDurationSeconds = cfg.Bind("ForTheKill", "KillFavorDurationSeconds", 300f, "How long the killing-favor buff lasts once granted, in seconds. Retunes live via reloadcfg (re-stamps the status prefab; a running buff's countdown updates on its next refresh)."); } } internal static class Sigils { public static ConfigEntry EnableSigilSynergies; public static ConfigEntry EnablePetSigils; public static ConfigEntry PetSigilScale; public static ConfigEntry PetSigilCooldownSeconds; internal static void Bind(ConfigFile cfg) { EnableSigilSynergies = cfg.Bind("Sigils", "EnableSigilSynergies", true, "Pet ↔ sigil synergies (docs/sigil-synergy-plan.md): a species with a Sigils.json entry changes its Hunt-as-One hit while the PET stands inside a registered mage sigil (seed: Veaber in a Blood Sigil applies all seven hexes at 30% build-up, superseding the food mix). Gates the per-hit detection query AND the composition — off = exact pre-feature behavior. Retune live: config-override Sigils.json + `reloadsigils`; forensics: `sigildump`."); EnablePetSigils = cfg.Bind("Sigils", "EnablePetSigils", true, "Pet-cast sigils (docs/pet-sigil-testplan.md, SPIKE): the pet can drop its own 0.75x sigil circle (dev verb `petsigil `; player-facing triggers come later). Gates the cast funnel only — the four cloned circle items still register at pack-load either way (SL templates can't be un-applied live), they just can never be spawned. Off = exact pre-feature behavior at cast time."); PetSigilScale = cfg.Bind("Sigils", "PetSigilScale", 0.75f, "Uniform scale baked into the PET sigil circle prefabs at pack-load, relative to the vanilla circles (visual AND trigger-collider extent shrink together; every co-op client bakes its own prefab, so guests see the same size with no sync). Change needs a RELAUNCH to re-bake cleanly (the hook stores the original scale, so a reloadcfg re-apply is absolute, not compounding — but items already standing in the world keep the scale they spawned with)."); PetSigilCooldownSeconds = cfg.Bind("Sigils", "PetSigilCooldownSeconds", 60f, "Cooldown armed on the pet after a (non-dev) pet sigil cast, drained by the sim tick on the aggregate clock (the Bug-30 shape; `petsigil` bypasses and does not arm). Priced so the pet cannot keep a circle up permanently — keep it at or above the circle's lifespan."); } } internal static class Gear { public static ConfigEntry EnableGearEffects; internal static void Bind(ConfigFile cfg) { EnableGearEffects = cfg.Bind("Gear", "EnableGearEffects", true, "Equipped-gear -> pet effects (docs/pet-gear-plan.md): a worn item carrying a PetGear.json entry alters the active pet while equipped (seed: a Pearlbird Mask makes every loyalty GAIN worth 1.5x; other kinds buff the pet's HP/damage/defense/speed). Off = the player's equipment never touches the pet (no equipment reads) — exact pre-feature behavior. Retune live: config-override PetGear.json + `reloadgear`; forensics: `geardump`."); } } internal static class Gifts { public static ConfigEntry EnableGiftSkill; public static ConfigEntry GiftCooldownSeconds; public static ConfigEntry EnableFeatherFletching; public static ConfigEntry FletchNameSuffix; internal static void Bind(ConfigFile cfg) { EnableGiftSkill = cfg.Bind("Gifts", "EnableGiftSkill", true, "The Pet Gift skill (docs/pet-gift-plan.md): cast it and the pet gives the player a species-defined gift from PetGifts.json — one DEFAULT drop whose probability is the remainder, explicit drops with loyalty-lerped chances (chance -> chanceAt100), and a nothing-chance. Gates the CAST BODY only — the skill stays registered/learnable, so a live reloadcfg flip works both ways. Retune live: config-override PetGifts.json + `reloadgifts`; forensics: `giftdump`."); GiftCooldownSeconds = cfg.Bind("Gifts", "GiftCooldownSeconds", 600f, "Pet Gift skill cooldown in seconds (default 600 = 10 min). Stamped onto the skill prefab + every learned instance each sim tick and on `reloadcfg`, so a .cfg edit retunes the LIVE skill — no relaunch. A cast with NO companion refunds the cooldown; a \"nothing to give\" roll burns it (that is the gamble the nothing-chance defines)."); EnableFeatherFletching = cfg.Bind("Gifts", "EnableFeatherFletching", true, "Feather fletching, v3 (docs/feather-fletch-plan.md): the Pearlbird gifts a QUALITY-TIERED feather (Tattered/Ruffled/Sleek/Resplendent = the pet's loyalty tier, stackable to 999 by quality); one feather + a stack of any FeatherFletch.json arrow (survival crafting, any stack size) ENCHANTS that stack IN PLACE with 'Pearl Fletching' at the quality's +10/20/30/40% of the arrow's own damage — arrows keep their ItemID/name/icon/effects, the enchantment glint + tooltip panel + save/sync are all vanilla, and differently-fletched same-arrow stacks never merge. Re-fletching is UPGRADE-ONLY (equal/lower refuses, feather kept). Gates item/enchantment/recipe REGISTRATION (boot-time) and the craft/stack patches (live); OFF with fletched arrows in a save = they load as plain arrows, no crash. Retune: config-override FeatherFletch.json + `reloadfletch` (existing rows only); forensics: `fletchdump`, test spawn: `givefeather [loyalty|quality] [qty]`."); FletchNameSuffix = cfg.Bind("Gifts", "FletchNameSuffix", true, "Fletched arrow stacks show their bonus in the DISPLAY NAME — \"Iron Arrow (+20%)\" — on top of the enchantment glint/tooltip. This is the only at-a-glance way to tell tiers apart in the inventory grid (the glint is the same generic mark for every enchantment). Names are not game-saved; the suffix is restamped at craft/split/load. Off = pure vanilla names, tiers distinguishable only via the tooltip's enchantment panel."); } } internal static class BuffFoods { public static ConfigEntry EnableBuffFoods; internal static void Bind(ConfigFile cfg) { EnableBuffFoods = cfg.Bind("BuffFoods", "EnableBuffFoods", true, "Feeding the pet a buff food (BuffFoods.json — e.g. Crystal Powder / Ambraine / Gaberry Wine for +damage, Dark Stone for a Decay rider) grants a TEMPORARY damage buff scaled by the pet's loyalty level. These are NOT meals: no satiety, no loyalty, no heal — one shared temp-buff slot (same item refreshes, a different one replaces). OFF = feeding ignores the table entirely (a running buff keeps ticking but grants nothing, so a live re-enable resumes it)."); } } internal static class Scent { public static ConfigEntry EnableScentSense; public static ConfigEntry SniffIntervalSeconds; public static ConfigEntry SkipEmptyGatherables; public static ConfigEntry NorthYawOffsetDegrees; public static ConfigEntry IndicatePoint; public static ConfigEntry PointSeconds; public static ConfigEntry IndicateStatusIcon; public static ConfigEntry IndicateToast; internal static void Bind(ConfigFile cfg) { EnableScentSense = cfg.Bind("Scent", "EnableScentSense", true, "Pet environment sense (docs/pet-scent-plan.md): a species with a PetSenses.json entry periodically noses out nearby spawns of its 'interesting items' (seed: Hyena → Unidentified Molepig den) and indicates the scent to the player. Off = no scan, no dispatch, the held scent clears — exact pre-feature behavior. Retune live: config-override PetSenses.json + `reloadscents`; forensics: `scentdump`, force a scan with `sniff`."); SniffIntervalSeconds = cfg.Bind("Scent", "SniffIntervalSeconds", 5f, "Seconds between scent scans (one OverlapSphere at the pet per scan). Own cadence, deliberately decoupled from SimTickSeconds — the sniff query is bigger than anything on the sim tick."); SkipEmptyGatherables = cfg.Bind("Scent", "SkipEmptyGatherables", true, "Already-harvested gathering spots don't smell like anything (the game's own visual-empty condition: container empty + nothing left to gather). Turn off if a node type ever reads empty wrongly — `scentdump` shows the per-hit reject reason."); NorthYawOffsetDegrees = cfg.Bind("Scent", "NorthYawOffsetDegrees", 0f, "Compass correction for the [SCENT] direction bucketing: degrees to rotate the world frame if the in-game compass's north turns out not to be world +Z. 0 until live evidence says otherwise."); IndicatePoint = cfg.Bind("Scent", "IndicatePoint", true, "Scent indication channel: on a fresh alert the pet stands still, turns to face the smelled spawn (the hunting-dog 'point') for PointSeconds, and barks its species vocal. Combat cancels the point instantly."); PointSeconds = cfg.Bind("Scent", "PointSeconds", 3f, "How long (seconds) the pet holds the point toward a fresh scent."); IndicateStatusIcon = cfg.Bind("Scent", "IndicateStatusIcon", true, "Scent indication channel: a 'Scent Trail' buff icon on the player HUD while the pet holds a scent (clears when the nose loses it). Also needs [Systems] ShowPetStatusIcons."); IndicateToast = cfg.Bind("Scent", "IndicateToast", true, "Scent indication channel (caravanner spike, docs/caravanner-spike-plan.md): on a fresh CHARACTER-kind alert an on-screen toast names the sense and its 8-way compass direction ('Your companion scents Caravanner to the north-east'). Character senses only — gatherable sense keys are code ids, not prose. Cooldown-limited like every alert — one toast per target per cooldownSeconds."); } } internal static class Scavenge { public static ConfigEntry EnableScavengeBonus; internal static void Bind(ConfigFile cfg) { EnableScavengeBonus = cfg.Bind("Scavenge", "EnableScavengeBonus", true, "Pet scavenge bonus (docs/scavenge-bonus-plan.md): a species with a PetScavenge.json entry (seed: Veaber → Junk Pile / Broken Tent / Hollowed Trunk) makes those containers roll their OWN existing loot tables extra times on first open, scaled by loyalty tier (default 0/1/1/2/3 for Gone..Devoted). Each extra slot is one more honest roll of the container's table — the empty chance still applies. Off = no container check at all, exact pre-feature behavior. Retune live: config-override PetScavenge.json + `reloadscavenge`; forensics: `scavengedump`."); } } internal static class Skills { public static ConfigEntry ScatologyGatesScrolls; public static ConfigEntry BeastOfBurdenCapacity; public static ConfigEntry EnableWildUnknownGate; public static ConfigEntry EnableCommunionGate; internal static void Bind(ConfigFile cfg) { ScatologyGatesScrolls = cfg.Bind("Skills", "ScatologyGatesScrolls", true, "Taming-recipe scrolls only drop when someone in the session has learned the Scatology skill (ANY player character counts, not just the host — co-op rule). OFF = pre-overhaul behavior: scrolls drop for everyone, skill or not."); BeastOfBurdenCapacity = cfg.Bind("Skills", "BeastOfBurdenCapacity", 5f, "Flat bag capacity the Beast of Burden passive adds to the player's equipped backpack while they have a pet (the bond, not the body — a downed pet still counts). Stacks additively with any species BagCapacity buff (SpeciesBuffs.txt). 0 = the skill grants nothing."); EnableWildUnknownGate = cfg.Bind("Skills", "EnableWildUnknownGate", true, "Region travel needs the Wild Unknown breakthrough: ration-consuming travel (the long-distance kind, never doors/dungeons) with an active pet bond warns at the travel-confirm menu and BREAKS the bond on arrival unless the bond's owner has learned Wild Unknown; with the skill, the first crossing of each region pair grants +5 loyalty. OFF = pre-overhaul behavior: pets travel freely, no toast, no bonus. Forensics: `travelcheck`; headless drive: `travelcross`."); EnableCommunionGate = cfg.Bind("Skills", "EnableCommunionGate", true, "The bond's passive player buffs (SpeciesBuffs.txt stat buffs AND the BagCapacity bag perk) require the Communion skill; once learned, the tier-colored Communion badge replaces the bond icons and reads out the actual benefit. OFF = pre-overhaul behavior: buffs for everyone, classic bond icons, no Communion badge. NOTE this default is a deliberate balance change for existing saves — a pet grants nothing until Communion is bought from Maren."); } } internal static class Anchor { public static ConfigEntry EnableAnchor; public static ConfigEntry AnchorInvisible; public static ConfigEntry AnchorShowHealthBar; public static ConfigEntry AnchorLinkSummonSlot; public static ConfigEntry HideSummonIcon; public static ConfigEntry AnchorLeashDistance; public static ConfigEntry AnchorRespawnSeconds; public static ConfigEntry AnchorBaseHealth; public static ConfigEntry AnchorDealsDamage; public static ConfigEntry AnchorSpeciesVoice; public static ConfigEntry GlueMode; public static ConfigEntry GlueOffsetBehind; public static ConfigEntry UnifyTargets; public static ConfigEntry AnchorPlayerCollision; internal static void Bind(ConfigFile cfg) { EnableAnchor = cfg.Bind("Anchor", "EnableAnchor", true, "Spawn an invisible ally Character alongside the pet so enemies can target, fight, and damage the pet (pet-aggro)."); AnchorInvisible = cfg.Bind("Anchor", "AnchorInvisible", true, "Hide the anchor's ghost model (renderers off). Turn OFF to watch the ally directly while debugging."); AnchorShowHealthBar = cfg.Bind("Anchor", "AnchorShowHealthBar", false, "Show the anchor's floating health bar (the pet's effective combat HP)."); AnchorLinkSummonSlot = cfg.Bind("Anchor", "AnchorLinkSummonSlot", true, "Register the anchor as the player's summon (vanilla-proven path; rides owner teleports). Conflicts with a real Cabal Hermit ghost — casting Conjure destroys the pet's anchor and vice versa."); HideSummonIcon = cfg.Bind("Anchor", "HideSummonIcon", true, "Hide the vanilla 'Summoned Ghost' buff icon the HUD synthesizes for the summon-linked anchor (Bug 35 — it is not a real status effect, just the HUD reading player.CurrentSummon; the character-sheet row is hidden too). A real Cabal Hermit Conjure ghost keeps its icon either way. WARNING: false also re-exposes a per-frame CombatHUD NullReferenceException (its status-timer pass chokes on the anchor's infinite lifetime). Fully live via reloadcfg, both directions."); AnchorLeashDistance = cfg.Bind("Anchor", "AnchorLeashDistance", 20f, "Teleport the anchor to the player when it falls further behind than this (zone changes, ledges). Out-of-combat only — while fighting, [Combat] CombatLeashDistance applies instead."); AnchorRespawnSeconds = cfg.Bind("Anchor", "AnchorRespawnSeconds", 60f, "How long a downed pet stays gone: the anchor respawn cooldown AND the delay before the retreated body re-forms."); AnchorBaseHealth = cfg.Bind("Anchor", "AnchorBaseHealth", 110f, "The pet's combat HP at loyalty 100 is 1.5x this; at loyalty 0, 0.5x (see PetSystems.MaxHealthFactor)."); AnchorDealsDamage = cfg.Bind("Anchor", "AnchorDealsDamage", false, "Let the anchor's own (invisible) weapon deal damage. OFF by default: CompanionCombat owns pet damage (tuned, responsiveness-scaled, CombatHUD-visible, kill-credited); the anchor's zero-damage swings still hold aggro."); AnchorSpeciesVoice = cfg.Bind("Anchor", "AnchorSpeciesVoice", true, "Species-correct pet voice + ghost silencing (post-Bug-14 shape): mutes every audio source under the invisible anchor (its sword whoosh & co — the 'skeleton audio'; the ghost has NO vanilla sound manager, so muting its tree IS the silencer), and plays the species HURT cry when the pet takes a hit and the species DEATH cry when it dies, from the body's own sound presets. Turn OFF to hear the raw ghost (debug). Takes effect on the next anchor spawn if changed live (resummon)."); Combat.BindVocals(cfg); GlueMode = cfg.Bind("Anchor", "GlueMode", (AnchorGlueMode)2, "How the anchor is position-coupled to the visible body. Always (default) = the body is the single movement brain and the anchor is welded to it every frame — enemies aim at what you actually see (fixes the puppet/ghost drift). Combat = weld only while fighting; the anchor leads out of combat (legacy topology). Off = no weld at all; the legacy 2s combat pin."); GlueOffsetBehind = cfg.Bind("Anchor", "GlueOffsetBehind", 0.3f, "Weld offset (m) behind the body along its facing, keeping the ghost's capsule at the body's rear rather than inside its visual center (enemy hit-VFX read better on the body). 0 = weld dead-center."); UnifyTargets = cfg.Bind("Anchor", "UnifyTargets", true, "Keep the anchor's combat lock asserted onto the pet's own combat target so the two bodies never fight different enemies. The anchor's natural self-defense lock (someone attacked the pet) still takes priority in target picking."); AnchorPlayerCollision = cfg.Bind("Anchor", "AnchorPlayerCollision", (AnchorCollisionMode)1, "How the pet's invisible anchor collides with PLAYERS (Bug 24, 'anchor shoving' — the pet wedging against you and pushing you around). PassPlayer (default) = the fix: the game's own player-vs-player recipe (Physics.IgnoreCollision on the anchor/player collider pairs) extended to the anchor, which vanilla skips because it bails on AI. The pet can no longer move you; it still BLOCKS enemies, so they can't stand inside its model. Block = pre-fix behavior, the anchor shoves you (the A/B control). Phantom = escalation if PassPlayer somehow isn't enough: mute the anchor's blocking volumes entirely, so NOTHING is blocked by it (enemies may then overlap the pet's model). All three are fully live via reloadcfg, in both directions."); } } internal static class Hud { public static ConfigEntry EnableHealthHud; internal static void Bind(ConfigFile cfg) { EnableHealthHud = cfg.Bind("HUD", "EnableHealthHud", true, "Show a simple pet-health bar overlay (top-left) whenever a live pet anchor exists."); } } internal static class PetPanel { public static ConfigEntry EnableControllerNav; public static ConfigEntry StickScrollSpeed; public static ConfigEntry StickDeadzone; internal static void Bind(ConfigFile cfg) { EnableControllerNav = cfg.Bind("PetPanel", "EnableControllerNav", true, "Right stick vertical scrolls the active half of the Companion-tab panel; left/right trigger switches which half (left/right) is active. Off = mouse wheel/drag only."); StickScrollSpeed = cfg.Bind("PetPanel", "StickScrollSpeed", 1.2f, "How fast the right stick scrolls the active column, in normalized-scroll-position per second at full deflection."); StickDeadzone = cfg.Bind("PetPanel", "StickDeadzone", 0.2f, "Right stick vertical must exceed this magnitude (0-1) before it scrolls — filters controller drift/noise."); } } internal static class Wards { public static ConfigEntry EnableWardShare; public static ConfigEntry WardPollSeconds; public static ConfigEntry ManaWardStatusNames; public static ConfigEntry GiftOfBloodStatusNames; public static ConfigEntry GiftOfBloodAllyStatusNames; internal static void Bind(ConfigFile cfg) { EnableWardShare = cfg.Bind("Wards", "EnableWardShare", true, "Player-ward sharing (docs/ward-share-plan.md): while the player has Mana Ward's Force Bubble, the pet's anchor carries the SAME vanilla status (invulnerable to all but raw-class damage, more prone to impact — exactly the player behavior); a Gift of Blood cast (Blood Sigil + Mana Ward) grants the anchor the vanilla 10s Gift of Blood Ally regen once per cast. Off = no status reads or writes; an already-mirrored bubble simply expires on its own (≤4s). Forensics: `warddump`."); WardPollSeconds = cfg.Bind("Wards", "WardPollSeconds", 0.25f, "How often the player's ward statuses are polled, in seconds. Force Bubble lasts only 4s, so this runs much faster than the ~2s sim tick — the mirror lands within one poll of the cast and clears within one poll of expiry."); ManaWardStatusNames = cfg.Bind("Wards", "ManaWardStatusNames", "Force Bubble,ForceBubble", "Status identifier candidates for Mana Ward's protective status, comma-separated, first that resolves wins (identifiers are asset data — unknowable offline; `statusdump` lists every loadable one if the shipped candidates miss on some install/locale)."); GiftOfBloodStatusNames = cfg.Bind("Wards", "GiftOfBloodStatusNames", "Gift of Blood,GiftOfBlood", "Status identifier candidates for the caster-side Gift of Blood status (the 60s self buff whose APPEARANCE triggers the ally grant)."); GiftOfBloodAllyStatusNames = cfg.Bind("Wards", "GiftOfBloodAllyStatusNames", "Gift of Blood Ally,GiftOfBloodAlly", "Status identifier candidates for the ally-side regen status granted to the anchor (vanilla: 2 HP/s for 10s)."); } } internal static class Bandage { public static ConfigEntry EnableBandageHealing; public static ConfigEntry BandageItemIds; public static ConfigEntry BandageStatusNames; internal static void Bind(ConfigFile cfg) { EnableBandageHealing = cfg.Bind("Bandage", "EnableBandageHealing", true, "Let the player apply a Bandage to the pet (docs/pet-bandage-plan.md): on a bandage item the right-click 'Feed ' action becomes 'Bandage ' and puts the vanilla healing status on the pet's anchor — the pet heals-over-time exactly as a bandaged player does; the bandage is consumed and the player gets no buff. Off = a bandage is just an ordinary item again (the action falls back to 'Feed', which a bandage refuses — it's in no diet). Forensics: `bandagedump`."); BandageItemIds = cfg.Bind("Bandage", "BandageItemIds", "4400010", "ItemIDs treated as a 'bandage' for the apply-to-pet action, comma-separated (vanilla Bandages = 4400010). Add a modded bandage's id here to make it applicable to the pet too."); BandageStatusNames = cfg.Bind("Bandage", "BandageStatusNames", "Bandage", "Status identifier candidates for the healing status a bandage grants, comma-separated, first that resolves wins (identifiers are asset data — unknowable offline; `statusdump` lists every loadable one, `bandagedump` shows what resolved). This is the SAME status a bandage applies to a player, so the pet heal is player-identical by construction."); } } internal static class Lantern { public static ConfigEntry EnableLanternShare; public static ConfigEntry LanternPollSeconds; public static ConfigEntry LanternStatusNames; public static ConfigEntry LanternUpOffset; public static ConfigEntry LanternForwardOffset; internal static void Bind(ConfigFile cfg) { EnableLanternShare = cfg.Bind("Lantern", "EnableLanternShare", true, "Runic-Lantern sharing (docs/lantern-share-plan.md): while the player carries the Runic Lantern status, an identical light (a logic-stripped clone of the status's own FX) floats above the pet and dies exactly when the player's lantern does — duration, the Rune Sage breakthrough extension and recast-refresh are all inherited from the vanilla status. Off = the clone is removed within one poll and never re-created. Forensics: `lanterndump`."); LanternPollSeconds = cfg.Bind("Lantern", "LanternPollSeconds", 0.25f, "How often the player's statuses are polled for the Runic Lantern, in seconds (the WardPollSeconds cadence — the mirror appears/disappears within one poll of the vanilla lantern)."); LanternStatusNames = cfg.Bind("Lantern", "LanternStatusNames", "Runic Lantern Amplified,Runic Lantern,RunicLantern", "Status identifier candidates for the Runic Lantern status, comma-separated. EVERY candidate is checked against the player's live statuses each poll, first carried wins — the base spell and the Rune Sage amplification are SEPARATE statuses (session-2 live finding), so both ship here. If a variant still misses, cast the lantern and run `lanterndump` — its status census names the real identifier. NB an existing .cfg keeps its old value (BepInEx never migrates defaults) — sync or hand-add 'Runic Lantern Amplified' on old installs."); LanternUpOffset = cfg.Bind("Lantern", "LanternUpOffset", 0.5f, "How far above the top of the pet's body the mirrored lantern floats, in meters (the body's skinned-mesh bounds top + this). Live: edit + `reloadcfg` moves an active clone."); LanternForwardOffset = cfg.Bind("Lantern", "LanternForwardOffset", 0.3f, "How far ahead of the pet the mirrored lantern floats, in meters (vanilla uses 0.7 on the player). Live: edit + `reloadcfg` moves an active clone."); } } internal static class MP { public static ConfigEntry AllowConsumeTameInRoom; internal static void Bind(ConfigFile cfg) { AllowConsumeTameInRoom = cfg.Bind("MP", "AllowConsumeTameInRoom", true, "Allow the MASTER's consume-tame (F7 / 'tame' / taming chow) while OTHER PLAYERS are connected. ON is the shipped default (Cobalt's 2026-07-21 ruling: both players taming one pet each is the milestone — the host must be able to tame in co-op). KNOWN COST, accepted: the consume path destroys the wild creature's PhotonView in the same frame it strips the clone, so its removal never replicates and every GUEST keeps a frozen STATUE of the creature until their next area load (health-review 2.5b; a replicated remove is future work). Guests' own tames ride the non-consume path and are unaffected; offline/solo play is unaffected. Set false to restore the old refusal toast. NB BepInEx never migrates a changed default into an existing cfg — installs generated before 2026-07-21 keep false until patched (config/ overlay pins it on the dev boxes)."); } } } internal static class AnchorSpike { private static readonly CompanionAnchor Rig = new CompanionAnchor((ICompanionSettings)null); private static Vector3 _lastWatchPos; internal static bool Spawn(Character player) { //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) if (!Rig.Spawn(player, (MonoBehaviour)null, false)) { return false; } _lastWatchPos = ((Component)Rig.Current).transform.position; return true; } internal static IEnumerator WatchTimeline() { yield return null; LogOneShotInitFacts(); for (int tick = 1; tick <= 24; tick++) { if (!((Object)(object)Rig.Current != (Object)null)) { break; } Character current = Rig.Current; if ((Object)(object)current == (Object)null || !current.Alive) { Plugin.Log.LogMessage((object)"[ANCHOR][watch] anchor died/destroyed — watch ended."); yield break; } CharacterAI component = ((Component)current).GetComponent(); NavMeshAgent component2 = ((Component)current).GetComponent(); Animator componentInChildren = ((Component)current).GetComponentInChildren(); float num = Vector3.Distance(((Component)current).transform.position, _lastWatchPos); _lastWatchPos = ((Component)current).transform.position; Plugin.Log.LogMessage((object)($"[ANCHOR][watch] t={(float)tick * 0.5f:F1}s moved={num:F2}" + " spawnAnimDone=" + ReadBool(current, "SpawnAnimDone") + " closeToPlayer=" + ReadBool(component, "CloseToPlayer") + " ai=" + (((Object)(object)component != (Object)null && ((Behaviour)component).enabled) ? "T" : "F") + " agent=" + (((Object)(object)component2 != (Object)null && ((Behaviour)component2).enabled) ? "T" : "F") + " navmesh=" + (((Object)(object)component2 != (Object)null && ((Behaviour)component2).enabled && component2.isOnNavMesh) ? "T" : "F") + " path=" + (((Object)(object)component2 != (Object)null && ((Behaviour)component2).enabled && component2.isOnNavMesh && component2.hasPath) ? "T" : "F") + " rootMotion=" + ((!((Object)(object)componentInChildren != (Object)null)) ? "?" : (componentInChildren.applyRootMotion ? "T" : "F")) + string.Format(" fwd={0:F2}", GetAnimFloat(componentInChildren, "moveForward")) + " state=" + (((component == null) ? null : ((object)component.CurrentAiState)?.GetType().Name) ?? "?"))); yield return (object)new WaitForSeconds(0.5f); } Plugin.Log.LogMessage((object)"[ANCHOR][watch] done."); } private static void LogOneShotInitFacts() { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) Character current = Rig.Current; if (!((Object)(object)current == (Object)null)) { bool flag = false; try { flag = (Object)(object)CharacterManager.Instance.GetCharacter(UID.op_Implicit(current.UID)) != (Object)null; } catch { } object obj2 = ReadMember(CharacterManager.Instance.GetFirstLocalCharacter(), "CurrentSummon"); Plugin.Log.LogMessage((object)("[ANCHOR][watch] init facts: name='" + ((Object)current).name + "' uid=" + UID.op_Implicit(current.UID) + $" registeredInCharacterManager={flag}" + $" playerCurrentSummonIsAnchor={obj2 == current}")); } } internal static void Status() { //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Unknown result type (might be due to invalid IL or missing references) Character current = Rig.Current; if ((Object)(object)current == (Object)null) { Plugin.Log.LogMessage((object)"[ANCHOR] none active."); return; } if (!current.Alive) { Plugin.Log.LogMessage((object)"[ANCHOR] no longer alive (dead or destroyed)."); return; } CharacterAI component = ((Component)current).GetComponent(); NavMeshAgent component2 = ((Component)current).GetComponent(); Animator componentInChildren = ((Component)current).GetComponentInChildren(); Character val = (((Object)(object)component != (Object)null && (Object)(object)component.TargetingSystem != (Object)null) ? component.TargetingSystem.LockedCharacter : null); bool flag = (Object)(object)component2 != (Object)null && ((Behaviour)component2).enabled && component2.isOnNavMesh; ManualLogSource log = Plugin.Log; string[] obj = new string[13] { $"[ANCHOR] alive hp={current.Stats.CurrentHealth:F0}/{current.Stats.MaxHealth:F0}", string.Format(" state={0} aiEnabled={1}", ((component == null) ? null : ((object)component.CurrentAiState)?.GetType().Name) ?? "?", (Object)(object)component != (Object)null && ((Behaviour)component).enabled), $" agentEnabled={(Object)(object)component2 != (Object)null && ((Behaviour)component2).enabled} onNavMesh={(Object)(object)component2 != (Object)null && component2.isOnNavMesh}", $" hasPath={flag && component2.hasPath} remaining={((flag && component2.hasPath) ? component2.remainingDistance : (-1f)):F1}", null, null, null, null, null, null, null, null, null }; float num; if (!flag) { num = 0f; } else { Vector3 desiredVelocity = component2.desiredVelocity; num = ((Vector3)(ref desiredVelocity)).magnitude; } obj[4] = $" desiredVel={num:F2}"; obj[5] = " spawnAnimDone="; obj[6] = ReadBool(current, "SpawnAnimDone"); obj[7] = " closeToPlayer="; obj[8] = ReadBool(component, "CloseToPlayer"); obj[9] = string.Format(" rootMotion={0} fwd={1:F2}", (!((Object)(object)componentInChildren != (Object)null)) ? "?" : (componentInChildren.applyRootMotion ? "T" : "F"), GetAnimFloat(componentInChildren, "moveForward")); obj[10] = " target="; obj[11] = (((Object)(object)val != (Object)null) ? val.Name : "-"); obj[12] = $" pos={((Component)current).transform.position:F1} inCombat={current.InCombat}"; log.LogMessage((object)string.Concat(obj)); } internal static void ForceLegacyGates() { Character current = Rig.Current; if ((Object)(object)current == (Object)null) { Plugin.Log.LogMessage((object)"[ANCHOR] none active."); return; } CharacterAI component = ((Component)current).GetComponent(); NavMeshAgent component2 = ((Component)current).GetComponent(); if ((Object)(object)component2 != (Object)null) { ((Behaviour)component2).enabled = true; } if ((Object)(object)component != (Object)null) { ((Behaviour)component).enabled = true; } bool flag = TrySetBool(component, "IgnoreSpawnAnim", value: true); bool flag2 = TrySetBool(component, "CloseToPlayer", value: true); Plugin.Log.LogMessage((object)$"[ANCHOR] legacy gate-forces applied: agent/ai enabled, ignoreSpawnAnim={flag}, closeToPlayer={flag2}."); } internal static void Clear() { Rig.DestroyCurrent(); } private static string ReadBool(object obj, string name) { object obj2 = ReadMember(obj, name); if (!(obj2 is bool)) { return "?"; } if (!(bool)obj2) { return "F"; } return "T"; } private static object ReadMember(object obj, string name) { if (obj == null) { return null; } try { Type type = obj.GetType(); PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(obj, null); } return type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj); } catch { return null; } } private static bool TrySetBool(object obj, string name, bool value) { if (obj == null) { return false; } try { FieldInfo field = obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public); if (field == null) { Plugin.Log.LogWarning((object)("[ANCHOR] " + name + " field not found via reflection.")); return false; } field.SetValue(obj, value); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ANCHOR] " + name + " reflection set failed: " + ex.Message)); return false; } } private static float GetAnimFloat(Animator anim, string param) { if ((Object)(object)anim == (Object)null || !anim.isInitialized) { return -1f; } try { return anim.GetFloat(param); } catch { return -1f; } } } internal static class BwDiagnostics { internal static void Diag(Plugin p) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_021f: Unknown result type (might be due to invalid IL or missing references) //IL_0224: Unknown result type (might be due to invalid IL or missing references) //IL_02db: Unknown result type (might be due to invalid IL or missing references) //IL_02ee: Unknown result type (might be due to invalid IL or missing references) //IL_02fa: Unknown result type (might be due to invalid IL or missing references) //IL_042b: Unknown result type (might be due to invalid IL or missing references) //IL_043e: Unknown result type (might be due to invalid IL or missing references) //IL_044a: Unknown result type (might be due to invalid IL or missing references) //IL_038e: Unknown result type (might be due to invalid IL or missing references) //IL_039a: Unknown result type (might be due to invalid IL or missing references) //IL_04a6: Unknown result type (might be due to invalid IL or missing references) //IL_04ab: Unknown result type (might be due to invalid IL or missing references) //IL_04d3: Unknown result type (might be due to invalid IL or missing references) //IL_04d8: Unknown result type (might be due to invalid IL or missing references) //IL_04ec: Unknown result type (might be due to invalid IL or missing references) //IL_04f8: Unknown result type (might be due to invalid IL or missing references) //IL_0551: Unknown result type (might be due to invalid IL or missing references) //IL_055d: Unknown result type (might be due to invalid IL or missing references) //IL_05cc: Unknown result type (might be due to invalid IL or missing references) //IL_05db: Unknown result type (might be due to invalid IL or missing references) //IL_05dd: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Scene activeScene = SceneManager.GetActiveScene(); Plugin.Log.LogMessage((object)$"[DIAG] ==== t={Time.time:F1}s scene='{((Scene)(ref activeScene)).name}' ===="); try { NetworkLevelLoader instance = NetworkLevelLoader.Instance; QuestEventManager instance2 = QuestEventManager.Instance; CharacterManager instance3 = CharacterManager.Instance; if ((Object)(object)instance != (Object)null) { Plugin.Log.LogMessage((object)($"[DIAG] loading: gameplayLoading={instance.IsGameplayLoading} sceneLoading={instance.IsSceneLoading} " + string.Format("joiningWorld={0} syncedEventOnce={1} ", instance.IsJoiningWorld, ((Object)(object)instance2 != (Object)null) ? instance2.SyncedEventOnce.ToString() : "?") + string.Format("defaultVisuals={0} allDone={1} ", ((Object)(object)instance3 != (Object)null) ? instance3.IsDefaultVisualsLoaded.ToString() : "?", instance.AllPlayerDoneLoading) + $"allReady={instance.AllPlayerReadyToContinue} continueAfter={instance.ContinueAfterLoading}")); } } catch (Exception ex) { Plugin.Log.LogMessage((object)("[DIAG] loading: state read failed (" + ex.Message + ")")); } if ((Object)(object)localPlayerCharacter == (Object)null) { Plugin.Log.LogMessage((object)"[DIAG] no local player."); return; } CharacterStats stats = localPlayerCharacter.Stats; List engagedCharacters = localPlayerCharacter.EngagedCharacters; string arg = (((Object)(object)stats != (Object)null) ? ($"hp={stats.CurrentHealth:F0}/{stats.MaxHealth:F0} ({((stats.MaxHealth > 0f) ? (stats.CurrentHealth / stats.MaxHealth * 100f) : (-1f)):F0}%) " + $"stamina={stats.CurrentStamina:F0}/{stats.MaxStamina:F0} mana={stats.CurrentMana:F0}/{stats.MaxMana:F0} ") : ""); ManualLogSource log = Plugin.Log; Vector3 position = ((Component)localPlayerCharacter).transform.position; log.LogMessage((object)(string.Format("[DIAG] player: pos={0} {1}alive={2} ", ((Vector3)(ref position)).ToString("F1"), arg, localPlayerCharacter.Alive) + $"inCombat={localPlayerCharacter.InCombat} engaged={engagedCharacters?.Count ?? 0}")); if (engagedCharacters != null) { for (int i = 0; i < engagedCharacters.Count; i++) { Character val = engagedCharacters[i]; if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)$"[DIAG] engaged[{i}] "); continue; } Plugin.Log.LogMessage((object)$"[DIAG] engaged[{i}] '{val.Name}' faction={val.Faction} dist={Vector3.Distance(((Component)localPlayerCharacter).transform.position, ((Component)val).transform.position):F1} alive={val.Alive}"); } } Pet activePet = p.ActivePet; List list = new List(); CharacterManager instance4 = CharacterManager.Instance; DictionaryExt val2 = ((instance4 != null) ? instance4.Characters : null); if (val2 != null) { for (int j = 0; j < val2.Count; j++) { Character val3 = val2.Values[j]; if (Plugin.IsCommandable(val3, localPlayerCharacter) && (engagedCharacters == null || !engagedCharacters.Contains(val3)) && Vector3.Distance(((Component)localPlayerCharacter).transform.position, ((Component)val3).transform.position) <= Plugin.DiagRadius.Value) { list.Add(val3); } } } Plugin.Log.LogMessage((object)$"[DIAG] nearby AI (<={Plugin.DiagRadius.Value:F0}m, excl. engaged/anchor/allies): {list.Count}"); foreach (Character item in list) { Plugin.Log.LogMessage((object)$"[DIAG] '{item.Name}' faction={item.Faction} dist={Vector3.Distance(((Component)localPlayerCharacter).transform.position, ((Component)item).transform.position):F1} alive={item.Alive}"); } if (activePet?.Sim != null) { PetStatus val4 = activePet.Sim.Status(); CompanionBody body = ((Companion)activePet).Body; object obj; if (!((Object)(object)body != (Object)null)) { obj = ""; } else { position = ((Component)body).transform.position; obj = string.Format(" pos={0} dist={1:F1}", ((Vector3)(ref position)).ToString("F1"), Vector3.Distance(((Component)localPlayerCharacter).transform.position, ((Component)body).transform.position)); } string text = (string)obj; CompanionCombat combat = ((Companion)activePet).Combat; Character val5 = (((Object)(object)combat != (Object)null) ? combat.CurrentTarget : null); string text2 = (((Object)(object)val5 != (Object)null) ? $" target='{val5.Name}' targetDist={Vector3.Distance(((Component)body).transform.position, ((Component)val5).transform.position):F0}" : " target=none"); string text3 = (((Object)(object)((Companion)activePet).Stance.CommandedTarget != (Object)null) ? (" commanded='" + ((Companion)activePet).Stance.CommandedTarget.Name + "'") : ""); Plugin.Log.LogMessage((object)($"[DIAG] pet: species='{activePet.SpeciesId}' loyalty={val4.LoyaltyValue} ({val4.Loyalty}) hunger={activePet.Sim.HungerFraction:P0} " + string.Format("stance={0}{1}{2} bodyAlive={3}{4} ghost={5}", ((Companion)activePet).Stance.Passive ? "disengage" : "engage", text2, text3, (Object)(object)body != (Object)null, text, p.ActivePetIsGhost))); Plugin.Log.LogMessage((object)("[DIAG] comfort: " + PetComfortTable.ComfortSummary(p))); if (((Companion)activePet).Anchor.HasLiveAnchor) { Plugin.Log.LogMessage((object)("[DIAG] anchor: " + ((Companion)activePet).Anchor.HealthSummary() + " " + ((Companion)activePet).Anchor.PhysicsFragment())); } } else { Plugin.Log.LogMessage((object)"[DIAG] no active pet."); } } internal static void CombatCheck(Plugin p, Character player) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) List engagedCharacters = player.EngagedCharacters; Plugin.Log.LogMessage((object)$"[COMBAT] InCombat={player.InCombat}; engaged={engagedCharacters?.Count ?? 0}"); CompanionBody val = ((Companion)(p.ActivePet?)).Body; CompanionAnchor val2 = ((Companion)(p.ActivePet?)).Anchor; if ((Object)(object)val != (Object)null && val2 != null && val2.HasLiveAnchor) { Plugin.Log.LogMessage((object)($"[COMBAT] glue: mode={Plugin.GlueMode.Value} sep={Vector3.Distance(((Component)val).transform.position, ((Component)val2.Current).transform.position):F2}m " + "unifiedLock=" + (((Object)(object)val2.LastAssertedTarget != (Object)null) ? val2.LastAssertedTarget.Name : "none") + " followOverride=" + (((Object)(object)val.FollowOverride != (Object)null) ? "anchor" : "player"))); } if (engagedCharacters == null) { return; } for (int i = 0; i < engagedCharacters.Count; i++) { Character val3 = engagedCharacters[i]; if ((Object)(object)val3 == (Object)null) { Plugin.Log.LogMessage((object)$"[COMBAT] [{i}] "); continue; } Plugin.Log.LogMessage((object)$"[COMBAT] [{i}] '{((Object)val3).name}' faction={val3.Faction} enabled={((Behaviour)val3).enabled}"); } } internal static void AnimDump(Plugin p) { //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Invalid comparison between Unknown and I4 //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Invalid comparison between Unknown and I4 //IL_01dd: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Invalid comparison between Unknown and I4 Pet activePet = p.ActivePet; if ((Object)(object)((Companion)(activePet?)).Body == (Object)null) { Plugin.Log.LogMessage((object)"[ANIM] no active pet."); return; } Animator[] componentsInChildren = ((Component)((Companion)activePet).Body).GetComponentsInChildren(true); foreach (Animator val in componentsInChildren) { string text = (((Object)(object)val.runtimeAnimatorController != (Object)null) ? ((Object)val.runtimeAnimatorController).name : "null"); Plugin.Log.LogMessage((object)$"[ANIM] '{((Object)((Component)val).gameObject).name}': enabled={((Behaviour)val).enabled} ctrl={text} speed={val.speed} rootMotion={val.applyRootMotion} culling={val.cullingMode} update={val.updateMode} layers={val.layerCount} init={val.isInitialized}"); if (val.isInitialized && ((Component)val).gameObject.activeInHierarchy && val.layerCount > 0) { AnimatorStateInfo currentAnimatorStateInfo = val.GetCurrentAnimatorStateInfo(0); Plugin.Log.LogMessage((object)$"[ANIM] layer0: stateHash={((AnimatorStateInfo)(ref currentAnimatorStateInfo)).fullPathHash} normTime={((AnimatorStateInfo)(ref currentAnimatorStateInfo)).normalizedTime:F2}"); } AnimatorControllerParameter[] parameters = val.parameters; foreach (AnimatorControllerParameter val2 in parameters) { string arg = (((int)val2.type == 1) ? val.GetFloat(val2.name).ToString("F2") : (((int)val2.type == 3) ? val.GetInteger(val2.name).ToString() : (((int)val2.type == 4) ? val.GetBool(val2.name).ToString() : "(trigger)"))); Plugin.Log.LogMessage((object)$"[ANIM] {val2.type} '{val2.name}' = {arg}"); } } } internal static void CompDump(Plugin p) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //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) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if ((Object)(object)((Companion)(activePet?)).Body == (Object)null) { Plugin.Log.LogMessage((object)"[COMP] no active pet."); return; } GameObject gameObject = ((Component)((Companion)activePet).Body).gameObject; ManualLogSource log = Plugin.Log; string name = ((Object)gameObject).name; object arg = gameObject.activeInHierarchy; Vector3 val = gameObject.transform.position; log.LogMessage((object)string.Format("[COMP] body '{0}' activeInHierarchy={1} pos={2}", name, arg, ((Vector3)(ref val)).ToString("F1"))); Renderer[] componentsInChildren = gameObject.GetComponentsInChildren(true); foreach (Renderer val2 in componentsInChildren) { ManualLogSource log2 = Plugin.Log; object[] obj = new object[8] { ((object)val2).GetType().Name, ((Object)((Component)val2).gameObject).name, val2.enabled, val2.isVisible, ((Component)val2).gameObject.activeInHierarchy, null, null, null }; Renderer obj2 = ((val2 is SkinnedMeshRenderer) ? val2 : null); obj[5] = ((obj2 != null) ? new bool?(((SkinnedMeshRenderer)obj2).updateWhenOffscreen) : ((bool?)null)); Bounds bounds = val2.bounds; val = ((Bounds)(ref bounds)).center; obj[6] = ((Vector3)(ref val)).ToString("F1"); bounds = val2.bounds; val = ((Bounds)(ref bounds)).size; obj[7] = ((Vector3)(ref val)).ToString("F1"); log2.LogMessage((object)string.Format("[REND] {0} on '{1}' enabled={2} isVisible={3} active={4} updOff={5} bounds.c={6} size={7}", obj)); } Component[] componentsInChildren2 = gameObject.GetComponentsInChildren(true); foreach (Component val3 in componentsInChildren2) { if (!((Object)(object)val3 == (Object)null)) { Behaviour val4 = (Behaviour)(object)((val3 is Behaviour) ? val3 : null); string text = ((val4 != null) ? $" enabled={val4.enabled}" : ""); Plugin.Log.LogMessage((object)("[COMP] " + ((object)val3).GetType().Name + " on '" + ((Object)val3.gameObject).name + "'" + text)); } } } internal static void GroundProbe(Plugin p, Character player) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) Vector3 position = ((Component)player).transform.position; Plugin.Log.LogMessage((object)("[PROBE] player pos=" + ((Vector3)(ref position)).ToString("F2"))); float[] array = new float[6] { 0.5f, 1f, 1.5f, 2.5f, 5f, 10f }; NavMeshHit val = default(NavMeshHit); foreach (float num in array) { bool flag = NavMesh.SamplePosition(position, ref val, num, -1); ManualLogSource log = Plugin.Log; string text; if (!flag) { text = $"[PROBE] r={num:F1}: nothing"; } else { object[] obj = new object[4] { num, ((NavMeshHit)(ref val)).position.y - position.y, Vector3.Distance(position, ((NavMeshHit)(ref val)).position), null }; Vector3 position2 = ((NavMeshHit)(ref val)).position; obj[3] = ((Vector3)(ref position2)).ToString("F2"); text = string.Format("[PROBE] r={0:F1}: hit dy={1:+0.00;-0.00} dist={2:F2} at {3}", obj); } log.LogMessage((object)text); } CompanionBody val2 = ((Companion)(p.ActivePet?)).Body; if ((Object)(object)val2 != (Object)null) { NavMeshAgent component = ((Component)val2).GetComponent(); Plugin.Log.LogMessage((object)$"[PROBE] puppet y={((Component)val2).transform.position.y:F2} dy={((Component)val2).transform.position.y - position.y:+0.00;-0.00} onNav={(Object)(object)component != (Object)null && component.isOnNavMesh}"); } CompanionAnchor val3 = ((Companion)(p.ActivePet?)).Anchor; if (val3 != null && val3.HasLiveAnchor) { Plugin.Log.LogMessage((object)$"[PROBE] anchor y={((Component)val3.Current).transform.position.y:F2} dy={((Component)val3.Current).transform.position.y - position.y:+0.00;-0.00}"); } } internal static void PetSoundProbe(Plugin p) { //IL_0152: Unknown result type (might be due to invalid IL or missing references) CompanionBody val = ((Companion)(p.ActivePet?)).Body; if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[SOUND] no active pet body."); return; } CharacterSoundManager component = ((Component)val).GetComponent(); CharacterSoundsPresets val2 = (((Object)(object)component != (Object)null) ? component.m_characterSoundsPresets : null); string text = "no-anchor"; CompanionAnchor val3 = ((Companion)(p.ActivePet?)).Anchor; if (val3 != null && val3.HasLiveAnchor) { AnchorVoice component2 = ((Component)val3.Current).GetComponent(); int num = ((Component)val3.Current).GetComponentsInChildren(true).Length; text = string.Format("hurtReceiver={0} csmAnywhere={1}", (!((Object)(object)component2 != (Object)null)) ? "MISSING" : (((Object)(object)component2.BodySound != (Object)null) ? "wired" : "attached-but-unwired"), num); } Plugin.Log.LogMessage((object)string.Format("[SOUND] body '{0}': soundMgr={1} preset={2} | anchor: {3}", val.SpeciesId, (Object)(object)component != (Object)null, ((Object)(object)val2 != (Object)null) ? ((Object)val2).name : "NONE", text)); if ((Object)(object)component == (Object)null || (Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)"[SOUND] body has no species sound preset — nothing to play."); return; } try { Plugin.Log.LogMessage((object)"[SOUND] playing species ATTACK sound (hurt at +1.2s, death at +2.6s)..."); Global.AudioManager.PlaySoundAtPosition(component.GetAttackSound(0), ((Component)val).transform, 0f, 1f, 1f, 1f, 1f); ((MonoBehaviour)p).StartCoroutine(PetSoundSequence(component, ((Component)val).transform)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SOUND] playback failed: " + ex.Message)); } } internal static IEnumerator PetSoundSequence(CharacterSoundManager csm, Transform at) { yield return (object)new WaitForSeconds(1.2f); if ((Object)(object)csm == (Object)null || (Object)(object)at == (Object)null) { yield break; } try { Global.AudioManager.PlaySoundAtPosition(csm.GetHurtSound(), at, 0f, 1f, 1f, 1f, 1f); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SOUND] hurt playback failed: " + ex.Message)); } yield return (object)new WaitForSeconds(1.4f); if ((Object)(object)csm == (Object)null || (Object)(object)at == (Object)null) { yield break; } try { Global.AudioManager.PlaySoundAtPosition(csm.GetDeathSound(), at, 0f, 1f, 1f, 1f, 1f); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("[SOUND] death playback failed: " + ex2.Message)); } } internal static void HuntAsOneSyncDump(Character player) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Invalid comparison between Unknown and I4 //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) CharacterInventory inventory = player.Inventory; object obj; if (inventory == null) { obj = null; } else { CharacterSkillKnowledge skillKnowledge = inventory.SkillKnowledge; obj = ((skillKnowledge != null) ? ((CharacterKnowledge)skillKnowledge).GetLearnedItems() : null); } IList list = (IList)obj; if (list == null) { Plugin.Log.LogWarning((object)"[HUNTASONE] no SkillKnowledge."); return; } bool flag = false; for (int i = 0; i < list.Count; i++) { Item val = list[i]; if (!((Object)(object)val == (Object)null) && val.ItemID == 87001) { flag = true; bool flag2 = (Object)(object)player.CurrentWeapon != (Object)null && (int)player.CurrentWeapon.Type == 200; Plugin.Log.LogMessage((object)("[HUNTASONE] sync dump: equipped=" + (flag2 ? "bow" : "melee/none") + " " + $"ActivateEffectAnimType={val.ActivateEffectAnimType} CastModifier={val.CastModifier} " + $"AlternateAnimHasSkillID={val.AlternateAnimHasSkillID}")); } } if (!flag) { Plugin.Log.LogWarning((object)"[HUNTASONE] Hunt-as-One not learned yet."); } } internal static void StatusDump() { List list = new List(ResourcesPrefabManager.STATUSEFFECT_PREFABS.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); Plugin.Log.LogMessage((object)$"[STATUSDUMP] {list.Count} loadable StatusEffect prefab name(s):"); foreach (string item in list) { Plugin.Log.LogMessage((object)("[STATUSDUMP] " + item)); } } internal static void PetStatusLog(Plugin p) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: 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_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0072: 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_0080: 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_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) //IL_026c: Unknown result type (might be due to invalid IL or missing references) //IL_027a: Unknown result type (might be due to invalid IL or missing references) //IL_02cb: Unknown result type (might be due to invalid IL or missing references) //IL_02cc: Unknown result type (might be due to invalid IL or missing references) //IL_02db: Unknown result type (might be due to invalid IL or missing references) //IL_02dc: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; KillFavorBuff.Dump(Plugin.LocalPlayerCharacter); if (activePet?.Sim == null) { Plugin.Log.LogMessage((object)"[PET] no active pet."); return; } PetStatus val = activePet.Sim.Status(); Plugin.Log.LogMessage((object)$"[PET] {activePet.SpeciesId}: loyalty {val.LoyaltyValue} ({val.Loyalty}), comfort {val.Comfort}, responsiveness {val.Responsiveness}."); Plugin.Log.LogMessage((object)($"[PET] gain {Plugin.LoyaltyGainPercent.Value:0.##}% of face value" + $" (a +10 preferred meal banks {10f * Plugin.LoyaltyGainPercent.Value / 100f:0.##});" + $" banked {activePet.Sim.LoyaltyGainCarry:0.###} of the next point. Losses are unscaled.")); Plugin.Log.LogMessage((object)string.Format("[PET] hunger {0:P0} of a day{1}; diet via 'dietdump'.", activePet.Sim.HungerFraction, activePet.Sim.IsSatiated ? " — SATIATED (refuses food)" : "")); PetHealthReading val2 = PetHealthView.Resolve(p); Plugin.Log.LogMessage((object)("[HP] provider=" + ((PetHealthReading)(ref val2)).SourceLabel + " " + (((PetHealthReading)(ref val2)).Known ? $"{val2.Current:F0}/{val2.Max:F0}" : "no reading (bodiless / not announced)"))); Plugin.Log.LogMessage((object)("[FOODHEX] " + FoodHexTable.FoodHexSummary(p))); Plugin.Log.LogMessage((object)("[PETCMD] stance=" + (((Companion)activePet).Stance.Passive ? "PASSIVE (disengaged)" : "engaged (auto-assist)") + " commanded=" + (((Object)(object)((Companion)activePet).Stance.CommandedTarget != (Object)null) ? ((Companion)activePet).Stance.CommandedTarget.Name : "none"))); Plugin.Log.LogMessage((object)("[TEMP] " + PetComfortTable.ComfortSummary(p))); ScentTracker scent = activePet.Scent; ScentAlert val3 = ((scent != null) ? scent.Current : null); Plugin.Log.LogMessage((object)("[SCENT] " + ((PetSenseTable.Resolve(activePet.SpeciesId) == null) ? "species has no nose (no PetSenses.json entry)." : ((val3 != null) ? $"holding '{val3.SenseKey}' ({val3.Strength}, {PetSenses.DirectionLabel(val3.Direction)}, {val3.Distance:F0}m); details via 'scentdump'." : "no scent held; details via 'scentdump'.")))); PetScavengeEntry val4 = ScavengeTable.Resolve(activePet.SpeciesId); Plugin.Log.LogMessage((object)("[SCAVENGE] " + ((val4 == null) ? "species has no scavenge perk (no PetScavenge.json entry)." : $"+{PetScavenge.ExtraSlots(val4, val.Loyalty)} slot(s) per matching container at tier {val.Loyalty}; details via 'scavengedump'."))); Plugin.Log.LogMessage((object)("[HAO] " + HuntCooldownSummary(p, activePet))); Plugin.Log.LogMessage((object)("[POWER] " + PetSystems.PowerSummary(p))); Plugin.Log.LogMessage((object)("[RELIC] " + RelicSummary(activePet))); Plugin.Log.LogMessage((object)("[BUFFFOOD] " + BuffFoodTable.StatusSummary(activePet))); Plugin.Log.LogMessage((object)("[REGEN] " + ((activePet.State.HealthRecoveryLevel > 0) ? ("health recovery " + HealthRecovery.Describe(activePet.State.HealthRecoveryLevel, activePet.State.HealthRecoverySecondsLeft) + (Plugin.EnableFoodHealthRecovery.Value ? "" : " — INERT (EnableFoodHealthRecovery off; the clock still burns)")) : ("no health recovery running" + (Plugin.EnableFoodHealthRecovery.Value ? "" : " (EnableFoodHealthRecovery off)"))) + ".")); PetPassiveBuff.Dump(Plugin.LocalPlayerCharacter); PetBagPerk.Dump(Plugin.LocalPlayerCharacter); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; PetSimulation sim = activePet.Sim; ScentTracker scent2 = activePet.Scent; PetStatusEffects.Dump(localPlayerCharacter, sim, ((scent2 != null) ? scent2.Current : null) != null); PetGearSense.Dump(p, Plugin.LocalPlayerCharacter); } private static string RelicSummary(Pet pet) { if (pet?.State == null) { return "no pet state."; } RelicDef val = SpeciesRelics.For(pet.SpeciesId); if (val == null && pet.State.RelicStacks <= 0) { return "not a relic species — relic feeds refused."; } if (pet.State.RelicStacks <= 0) { return $"0/{5} stacks — feed {val.ItemLabel} (ItemID {val.ItemId})."; } return SpeciesRelics.Describe(pet.State.RelicStacks) + ((val != null) ? (" (" + val.ItemLabel + ")") : " — species has no relic (dev-set stacks?)"); } private static string HuntCooldownSummary(Plugin p, Pet pet) { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown if (!Plugin.SyncSkillCooldownToPet.Value) { return "cooldown sync OFF ([HuntAsOne] SyncSkillCooldownToPet) — the skill's 1s clock and the pet's table cooldown run independently; a press between them is eaten (Bug 30)."; } PetSpecialAttack petSpecialAttack = (((Object)(object)((Companion)pet).Body != (Object)null) ? ((Component)((Companion)pet).Body).GetComponent() : null); if ((Object)(object)petSpecialAttack == (Object)null) { return "no PetSpecialAttack on the body (is [Combat] EnableSpecialAttack on?)."; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item obj = ((instance != null) ? instance.GetItemPrefab(87001) : null); Skill val = (Skill)(object)((obj is Skill) ? obj : null); float cooldownRemaining = petSpecialAttack.CooldownRemaining; float skillLeft = 0f; float advertised = 0f; float mod = 1f; SkillInstanceSync.Sync(87001, Plugin.LocalPlayerCharacter, (Apply)delegate(Item item) { Skill val2 = (Skill)(object)((item is Skill) ? item : null); if (val2 != null && ((Item)val2).IsChildToCharacter) { skillLeft = Mathf.Max(skillLeft, val2.RemainingCooldownSeconds()); advertised = val2.Cooldown; mod = val2.m_cooldownModifierOnCast; } return false; }); if (advertised <= 0f && (Object)(object)val != (Object)null) { advertised = val.Cooldown; } bool flag = !HuntCooldown.Diverged(skillLeft, cooldownRemaining, 0.25f); string reason; return string.Format("cooldowns: skill {0:0.0}s left / pet {1:0.0}s left — {2}. ", skillLeft, cooldownRemaining, flag ? "AGREE" : "*** DIVERGED (Bug 30 would bite here) ***") + $"Skill advertises {advertised:0.#}s (table {petSpecialAttack.TableCooldownSeconds:0.#}s, cooldown-reduction mod {mod:0.##}). " + "Can act now: " + (petSpecialAttack.CanFireIgnoringCooldown(Plugin.LocalPlayerCharacter, out reason) ? "yes" : ("no — " + reason)); } } internal static class CombatStageVerbs { private static string GuestMissNote() { if (!PhotonNetwork.isNonMasterClientInRoom) { return ""; } return " (guest client: guest mirrors carry no AI — run on the host)"; } internal static void AggroVerb(Plugin p, Character player, string[] parts) { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) string text = ((parts.Length > 1) ? parts[1].ToLowerInvariant() : null); Pet activePet = p.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; Character val2; if (text == "me") { val2 = player; } else { if (!(text == "pet")) { Plugin.Log.LogWarning((object)"[DEV] usage: aggro [name] (nearest wild AI within 30m, or the nearest name-match)"); return; } val2 = val; if ((Object)(object)val2 == (Object)null || !val2.Alive) { Plugin.Log.LogWarning((object)"[DEV] aggro pet: no live anchor (a bodiless/ghost pet can't be attacked)."); return; } } string text2 = ((parts.Length > 2) ? string.Join(" ", parts, 2, parts.Length - 2) : null); CharacterAI val3 = AggroStage.Find(text2, ((Component)player).transform.position, 30f, val); if ((Object)(object)val3 == (Object)null) { Plugin.Log.LogWarning((object)("[DEV] aggro: no wild AI" + ((text2 != null) ? (" matching '" + text2 + "'") : "") + " within 30m." + GuestMissNote())); return; } bool flag = AggroStage.ForceTarget(val3, val2); Plugin.Log.LogMessage((object)(string.Format("[DEV] aggro: '{0}' [{1}] -> {2}: ", ((CharacterControl)val3).Character.Name, ((CharacterControl)val3).Character.Faction, (text == "me") ? "player" : "pet anchor") + (flag ? ("locked (state " + ((object)val3.CurrentAiState)?.GetType().Name + ").") : "FAILED (no combat state, or the lock didn't hold — check factions)."))); } internal static void PacifyVerb(Plugin p, Character player, string[] parts) { //IL_0069: Unknown result type (might be due to invalid IL or missing references) float result = 30f; if (parts.Length > 1 && (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out result) || result <= 0f || result > 100f)) { Plugin.Log.LogWarning((object)"[DEV] usage: pacify [radius 1-100, default 30]"); return; } Pet activePet = p.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character exclude = (Character)obj; int num = 0; foreach (CharacterAI item in AggroStage.AisInRange(((Component)player).transform.position, result, exclude)) { if (AggroStage.Calm(item)) { num++; } } Plugin.Log.LogMessage((object)($"[DEV] pacified {num} AI(s) within {result:F0}m." + ((num == 0) ? GuestMissNote() : ""))); } internal static void PetDownVerb(Plugin p) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; if ((Object)(object)val == (Object)null || !val.Alive) { Plugin.Log.LogWarning((object)"[DEV] petdown: no live anchor to down."); return; } Plugin.Log.LogMessage((object)($"[DEV] petdown: overkilling the anchor (hp {(((Object)(object)val.Stats != (Object)null) ? val.Stats.CurrentHealth : (-1f)):F0}) " + $"via ReceiveHit — expect the [PET] DOWNED line; re-form in ~{Plugin.AnchorRespawnSeconds.Value:F0}s.")); val.ReceiveHit((Weapon)null, 999999f, Vector3.forward, val.CenterPosition, 45f, 1f, (Character)null, 0f); if (val.Alive) { Plugin.Log.LogWarning((object)"[DEV] petdown: the anchor is STILL alive after the overkill hit — resistant/invulnerable?"); } } internal static void ReformTest(Plugin p, Character player) { //IL_002b: 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_003c: 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_0059: Expected O, but got Unknown Character val = BodyFactory.FindNearest(player, Plugin.TameRange.Value, (string)null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)"[PET] no creature nearby."); return; } PetSave val2 = new PetSave { SpeciesId = val.Name, Name = val.Name, LoyaltyValue = Plugin.InitialLoyalty.Value }; CompanionBody val3 = BodyFactory.BuildPuppet(val, player, false, SpecialAttacks.RangedCaptureFilter(val.Name), SpecialAttacks.RangedCaptureSkillId(val.Name), (CreatureAttributes)null, Plugin.CkHost); if ((Object)(object)val3 != (Object)null) { val2.Attributes = val3.CapturedStats; } p.SetPet(val3, val2); } } internal static class DonorSpotVerb { internal static void DonorSpot(Plugin p) { //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_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Invalid comparison between Unknown and I4 //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; List list = new List(); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); int num = 0; Plugin.Log.LogMessage((object)("[DONORSPOT] scene '" + ((Scene)(ref activeScene)).name + "' — AI roster:")); GameObject[] rootGameObjects = ((Scene)(ref activeScene)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { Character[] componentsInChildren = val.GetComponentsInChildren(true); foreach (Character val2 in componentsInChildren) { if (!((Object)(object)val2 == (Object)null) && val2.IsAI) { num++; bool? flag = null; try { flag = val2.Alive; } catch { } bool flag2 = false; try { flag2 = (int)val2.Faction == 1; } catch { } float num2 = (((Object)(object)localPlayerCharacter != (Object)null) ? Vector3.Distance(((Component)localPlayerCharacter).transform.position, ((Component)val2).transform.position) : (-1f)); string text = DonorTable.StripInstanceSuffix(((Object)((Component)val2).gameObject).name); Plugin.Log.LogMessage((object)(string.Format("[DONORSPOT] '{0}' go='{1}' alive={2} active={3} dist={4:F0}", val2.Name, text, flag.HasValue ? flag.Value.ToString() : "?", ((Component)val2).gameObject.activeInHierarchy, num2) + (flag2 ? " (Player faction — likely the pet anchor, excluded)" : ""))); if (!(flag != true || flag2) && !list.Contains(val2.Name)) { list.Add(val2.Name); dictionary[val2.Name] = text; } } } } if (list.Count == 0) { Plugin.Log.LogMessage((object)$"[DONORSPOT] no live wild AI here ({num} AI total)."); return; } foreach (string item in list) { string text2 = dictionary[item]; bool flag3 = !string.Equals(text2, item, StringComparison.OrdinalIgnoreCase); Plugin.Log.LogMessage((object)("[DONORSPOT] paste into DonorScenes.txt (or BepInEx/config/DonorScenes.txt): " + (flag3 ? (item + "|" + text2 + "=" + ((Scene)(ref activeScene)).name) : (item + "=" + ((Scene)(ref activeScene)).name)) + (flag3 ? (" # PINNED: its GameObject is '" + text2 + "' but it calls itself '" + item + "'. If that name is assigned at runtime (bosses, named NPCs) an unpinned row can NEVER harvest — the frozen donor doesn't know it yet (bug 28).") : ""))); } } } internal static class GiveVerbs { internal static void GiveTamingItem(Character player, string[] parts, bool scroll) { string text = Plugin.Tail(parts); TamingEntry val = null; if (!string.IsNullOrEmpty(text)) { val = TamingFoodTable.Resolve(text); } else { using Dictionary.ValueCollection.Enumerator enumerator = TamingFoodTable.Table.Values.GetEnumerator(); if (enumerator.MoveNext()) { TamingEntry current = enumerator.Current; val = current; } } if (val == null || !TamingFoodSetup.Registered.TryGetValue(val.Species, out (int, int) value)) { Plugin.Log.LogWarning((object)("[TAMEFOOD] no registered taming-food entry for '" + (text ?? "(first)") + "' — see tamingdump.")); return; } int num; if (!scroll) { (num, _) = value; } else { num = value.Item2; } int num2 = num; Item val2 = ItemManager.Instance.GenerateItemNetwork(num2); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)$"[TAMEFOOD] GenerateItemNetwork({num2}) returned null."); return; } val2.ChangeParent(((Component)player.Inventory.Pouch).transform); Plugin.Log.LogMessage((object)$"[TAMEFOOD] gave '{val2.Name}' (ItemID={num2}) to the player pouch."); } internal static void GiveBlanket(Character player, string[] parts) { //IL_00af: 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) string text = Plugin.Tail(parts)?.Trim() ?? "heat"; ComfortSide? val = (text.StartsWith("heat", StringComparison.OrdinalIgnoreCase) ? new ComfortSide?((ComfortSide)1) : (text.StartsWith("cool", StringComparison.OrdinalIgnoreCase) ? new ComfortSide?((ComfortSide)2) : ((ComfortSide?)null))); int num = 0; string text2 = null; foreach (KeyValuePair item in BlanketSetup.Registered) { BlanketDef value; BlanketDef val2 = (PetComfortTable.Blankets.TryGetValue(item.Key, out value) ? value : null); bool num2; if (!val.HasValue) { num2 = item.Key.IndexOf(text, StringComparison.OrdinalIgnoreCase) >= 0; } else { if (val2 == null) { continue; } num2 = (ComfortSide?)val2.Side == val; } if (num2) { num = item.Value; text2 = item.Key; break; } } if (text2 == null) { Plugin.Log.LogWarning((object)("[BLANKET] no registered blanket matches '" + text + "' — see the [BLANKET] boot lines.")); return; } Item val3 = ItemManager.Instance.GenerateItemNetwork(num); if ((Object)(object)val3 == (Object)null) { Plugin.Log.LogWarning((object)$"[BLANKET] GenerateItemNetwork({num}) returned null."); return; } val3.ChangeParent(((Component)player.Inventory.Pouch).transform); Plugin.Log.LogMessage((object)$"[BLANKET] gave '{text2}' (ItemID={num}) to the player pouch."); } internal static void GiveFeather(Plugin plugin, Character player, string[] parts) { if (!FeatherFletchSetup.FeatherRegistered) { Plugin.Log.LogWarning((object)"[FLETCH] the feather qualities never registered this boot (kill-switch off, or see the [FLETCH] boot lines) — nothing given."); return; } FeatherQuality val = null; int num = 1; int num2 = 1; if (parts.Length > num2 && !int.TryParse(parts[num2], out var _)) { FeatherQuality[] qualities = FeatherFletching.Qualities; foreach (FeatherQuality val2 in qualities) { if (val2.Quality.Equals(parts[num2].Trim(), StringComparison.OrdinalIgnoreCase)) { val = val2; } } if (val == null) { Plugin.Log.LogWarning((object)("[FLETCH] givefeather: '" + parts[num2] + "' is neither a loyalty number nor a quality (Tattered/Ruffled/Sleek/Resplendent). Usage: 'givefeather [loyalty|quality] [qty]'.")); return; } num2++; } else if (parts.Length > num2) { int.TryParse(parts[num2], out var result2); val = FeatherFletching.QualityFor(result2); num2++; } if (val == null) { Pet activePet = plugin.ActivePet; int? obj; if (activePet == null) { obj = null; } else { PetSimulation sim = activePet.Sim; obj = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue); } val = FeatherFletching.QualityFor(obj ?? 50); } if (parts.Length > num2 && int.TryParse(parts[num2], out var result3)) { num = ((result3 < 1) ? 1 : ((result3 > 999) ? 999 : result3)); } Item val3 = ItemManager.Instance.GenerateItemNetwork(val.ItemId); if ((Object)(object)val3 == (Object)null) { Plugin.Log.LogWarning((object)$"[FLETCH] GenerateItemNetwork({val.ItemId}) returned null."); return; } if (num > 1) { val3.RemainingAmount = num; } val3.ChangeParent(((Component)player.Inventory.Pouch).transform); Plugin.Log.LogMessage((object)$"[FLETCH] gave {num}x '{val3.Name}' (ItemID={val.ItemId}, +{val.Percent}%) to the player pouch."); } internal static void FletchDump(Character player) { Dictionary table = FletchTable.Table; Plugin.Log.LogMessage((object)($"[FLETCH] fletchdump: EnableFeatherFletching={Plugin.EnableFeatherFletching.Value}, " + $"FletchNameSuffix={Plugin.FletchNameSuffix.Value}, " + $"feathers registered={FeatherFletchSetup.FeatherRegistered}, enchantments registered={FeatherFletchSetup.EnchantmentsRegistered}, " + $"{table.Count} table row(s), {FeatherFletchSetup.Registered.Count} registered, {FeatherFletchSetup.RecipeUids.Count} recipe uid(s).")); FeatherQuality[] qualities = FeatherFletching.Qualities; foreach (FeatherQuality val in qualities) { bool flag = (Object)(object)ResourcesPrefabManager.Instance.GetEnchantmentPrefab(val.EnchantmentId) != (Object)null; bool flag2 = RecipeManager.Instance != null && RecipeManager.Instance.m_enchantmentRecipes != null && RecipeManager.Instance.m_enchantmentRecipes.ContainsKey(val.EnchantmentId); Plugin.Log.LogMessage((object)($"[FLETCH] quality {val.Quality}: item {val.ItemId}, enchantment {val.EnchantmentId} (+{val.Percent}%) — " + "prefab=" + (flag ? "OK" : "MISSING") + ", recipe=" + (flag2 ? "OK" : "MISSING"))); } foreach (FletchEntry value2 in table.Values) { string text = FeatherFletchSetup.RecipeUid(value2.Key); int value; string text2 = (FeatherFletchSetup.Registered.TryGetValue(value2.Key, out value) ? $"REGISTERED (arrow {value}, enchant-in-place)" : "NOT registered (boot-time only — see setup warnings / relaunch)"); object obj; if (player == null) { obj = null; } else { CharacterInventory inventory = player.Inventory; obj = ((inventory != null) ? inventory.RecipeKnowledge : null); } string text3 = (((Object)obj != (Object)null) ? (player.Inventory.RecipeKnowledge.IsRecipeLearned(text) ? "LEARNED" : "not learned") : "no player"); Plugin.Log.LogMessage((object)$"[FLETCH] '{value2.Key}': arrow {((object)value2.ArrowId) ?? value2.ArrowKey}, uid '{text}' — {text2}, recipe {text3}"); } if ((Object)(object)player == (Object)null) { return; } int num = 0; foreach (Item item in PetFeeder.InventoryItems(player)) { FeatherQuality val2 = FeatherFletching.QualityForItemId(item.ItemID); if (val2 != null) { num++; Plugin.Log.LogMessage((object)($"[FLETCH] held {item.RemainingAmount}x '{item.Name}' (+{val2.Percent}%, " + $"stack cap {item.MaxStackAmount})")); continue; } Ammunition val3 = (Ammunition)(object)((item is Ammunition) ? item : null); if (val3 != null) { num++; DumpAmmoStack("held", val3); } } CharacterInventory inventory2 = player.Inventory; Ammunition val4 = ((inventory2 != null) ? inventory2.GetEquippedAmmunition() : null); if ((Object)(object)val4 != (Object)null) { num++; DumpAmmoStack("EQUIPPED", val4); } Plugin.Log.LogMessage((object)$"[FLETCH] {num} feather/ammo stack(s) inspected (pouch/bag/quiver)."); } private static void DumpAmmoStack(string where, Ammunition ammo) { FeatherQuality val = FletchState.QualityOf((Item)(object)ammo); string text = FletchState.SetKeyOf((Item)(object)ammo); string text2 = ((val != null) ? $"fletched +{val.Percent}% ({val.Quality})" : ((text.Length > 0) ? ("foreign enchantment(s) [" + text + "]") : "plain")); Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(((Item)ammo).ItemID); string text3 = ((val != null && SumDamage(ammo) <= SumDamage((Ammunition)(object)((itemPrefab is Ammunition) ? itemPrefab : null))) ? " **PHANTOM? (id present, no damage folded — pre-fix split artifact; self-heals on reload)**" : string.Empty); Plugin.Log.LogMessage((object)($"[FLETCH] {where} {((Item)ammo).RemainingAmount}x '{((Item)ammo).Name}' ({((Item)ammo).ItemID}) — {text2}; " + "live [" + DamageOf(ammo) + "] vs prefab [" + DamageOf((Ammunition)(object)((itemPrefab is Ammunition) ? itemPrefab : null)) + "]" + text3)); } private static float SumDamage(Ammunition ammo) { if ((Object)(object)ammo == (Object)null || ((Weapon)ammo).Damage == null) { return 0f; } float num = 0f; for (int i = 0; i < ((Weapon)ammo).Damage.Count; i++) { num += ((Weapon)ammo).Damage[i].Damage; } return num; } private static string DamageOf(Ammunition ammo) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ammo == (Object)null || ((Weapon)ammo).Damage == null) { return "?"; } List list = new List(); for (int i = 0; i < ((Weapon)ammo).Damage.Count; i++) { list.Add($"{((Weapon)ammo).Damage[i].Type}={((Weapon)ammo).Damage[i].Damage:0.##}"); } return string.Join(" ", list.ToArray()); } private static Item FindInventoryItem(Character player, string key) { ItemQuery val = ItemQuery.Parse(key); if (val == null) { return null; } foreach (Item item in PetFeeder.InventoryItems(player)) { if (val.Matches(item.Name, item.ItemID)) { return item; } } return null; } internal static void FeedItemVerb(Character player, string[] parts) { bool flag = parts.Length > 1 && parts[1].Equals("force", StringComparison.OrdinalIgnoreCase); int num = ((!flag) ? 1 : 2); if (parts.Length <= num) { Plugin.Log.LogMessage((object)("[FEEDITEM] no item named — falling through to 'feed' (first diet-matching item)" + (flag ? " (FORCED past satiation)" : "") + ".")); PetFeeder.FeedFirstMatching(player, flag); return; } string text = string.Join(" ", parts, num, parts.Length - num).Trim(); Item val = FindInventoryItem(player, text); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)("[FEEDITEM] no inventory item matching '" + text + "'.")); return; } Plugin.Log.LogMessage((object)($"[FEEDITEM] feeding '{val.Name}' (ItemID={val.ItemID}, qty {val.RemainingAmount}) to the pet" + (flag ? " (FORCED past satiation)" : "") + "...")); PetFeeder.TryFeed(val, player, flag); } } internal static class HuntAsOneCastDiag { private static bool IsOurs(int itemId) { if (itemId != 87000 && itemId != 87001) { return itemId == 87002; } return true; } private static string Tag(int itemId) { return itemId switch { 87000 => "HealPet", 87001 => "HuntAsOne", 87002 => "PetCommand", _ => "?", }; } internal static void LogQuickSlotUse(Item item) { if (!((Object)(object)item == (Object)null) && IsOurs(item.ItemID)) { Plugin.Log.LogMessage((object)("[CASTDIAG] TryQuickSlotUse: " + Tag(item.ItemID) + " (UID=" + item.UID + ").")); } } internal static void LogHasAllRequirements(Skill skill, bool result) { if (!((Object)(object)skill == (Object)null) && IsOurs(((Item)skill).ItemID)) { Character ownerCharacter = ((EffectSynchronizer)skill).OwnerCharacter; string arg = (((Object)(object)ownerCharacter != (Object)null) ? $"InCooldown={skill.InCooldown()} IsHandlingWeapon={ownerCharacter.IsHandlingWeapon} IsHandlingBag={ownerCharacter.IsHandlingBag} InLocomotion={ownerCharacter.InLocomotion} NextIsLocomotion={ownerCharacter.NextIsLocomotion}" : "no owner"); Plugin.Log.LogMessage((object)$"[CASTDIAG] HasAllRequirements: {Tag(((Item)skill).ItemID)} -> {result} ({arg})."); } } internal static void LogUse(Item item, bool result) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)item == (Object)null) && IsOurs(item.ItemID)) { Plugin.Log.LogMessage((object)("[CASTDIAG] Item.Use: " + Tag(item.ItemID) + " (UID=" + item.UID + ") " + $"ActivateEffectAnimType={item.ActivateEffectAnimType} CastModifier={item.CastModifier} " + $"AlternateAnimHasSkillID={item.AlternateAnimHasSkillID} -> result={result}.")); } } internal static void LogCastSpellItem(Character character, SpellCastType type, Item itemReceiver, SpellCastModifier modifier, bool result) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)itemReceiver == (Object)null) && IsOurs(itemReceiver.ItemID)) { Plugin.Log.LogMessage((object)($"[CASTDIAG] CastSpell(Item): {Tag(itemReceiver.ItemID)} type={type} modifier={modifier} " + $"IsLocalPlayer={((character != null) ? new bool?(character.IsLocalPlayer) : ((bool?)null))} -> result={result}.")); } } internal static void LogSendPerformSpellCastItem(Character character, int spellCastTypeID, string itemUID, int modifier) { //IL_00de: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)character == (Object)null) && character.IsLocalPlayer) { ItemManager instance = ItemManager.Instance; Item val = ((instance != null) ? instance.GetItem(itemUID) : null); bool flag = (Object)(object)val != (Object)null && IsOurs(val.ItemID); Plugin.Log.LogMessage((object)("[CASTDIAG] SendPerformSpellCastItem: itemUID=" + itemUID + " item=" + (((Object)(object)val != (Object)null) ? string.Format("{0} (ID={1}{2})", val.Name, val.ItemID, flag ? (", " + Tag(val.ItemID)) : "") : "") + " " + $"spellCastTypeID={spellCastTypeID} modifier={(object)(SpellCastModifier)modifier} " + $"-- POST-STATE: IsCasting={character.IsCasting} CurrentSpellCast={character.CurrentSpellCast} " + "CastReceiver=" + (((Object)(object)character.CastReceiver != (Object)null) ? ((Object)character.CastReceiver).name : "") + ".")); } } internal static void LogSkillStarted(Skill skill) { if (!((Object)(object)skill == (Object)null) && IsOurs(((Item)skill).ItemID)) { Plugin.Log.LogMessage((object)("[CASTDIAG] Skill.SkillStarted: " + Tag(((Item)skill).ItemID) + " (UID=" + ((Item)skill).UID + ") -- this is what fires ActivateLocally.")); } } internal static void LogCastDone(Character character) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)character == (Object)null) && character.IsLocalPlayer && character.IsCasting) { GameObject castReceiver = character.CastReceiver; Plugin.Log.LogMessage((object)($"[CASTDIAG] CastDone (about to clear): CurrentSpellCast={character.CurrentSpellCast} " + "CastReceiver=" + (((Object)(object)castReceiver != (Object)null) ? ((Object)castReceiver).name : "") + ".")); } } internal static void LogRegisterEffect(Effect effect) { if (!(effect is DelegateEffect)) { return; } string name = ((Object)((Component)effect).gameObject).name; int num = -1; if (!name.Contains("Normal")) { switch (name) { case "Effects": case "Effect": case "ExtraEffects": case "HiddenEffects": break; default: goto IL_005c; } } num = 1; goto IL_00c9; IL_005c: if (name.Contains("Activation") || name.Contains("Passive")) { num = 2; } else if (name.Contains("Restauration") || name.Contains("Restoration")) { num = 3; } else if (name.Contains("Hit")) { num = 4; } else if (name.Contains("Referenced")) { num = 5; } else if (name.Contains("Block")) { num = 6; } goto IL_00c9; IL_00c9: Plugin.Log.LogMessage((object)("[CASTDIAG] RegisterEffect: " + ((object)effect).GetType().Name + " on GameObject '" + name + "' " + string.Format("-> category {0} ({1}) ", num, Enum.IsDefined(typeof(EffectCategories), num) ? ((object)(EffectCategories)num) : "?") + $"-- SkillStarted() reads category {2} (Activation).")); } } [HarmonyPatch(typeof(Item), "TryQuickSlotUse")] internal static class TryQuickSlotUsePatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } [HarmonyPrefix] private static void Prefix(Item __instance) { HuntAsOneCastDiag.LogQuickSlotUse(__instance); } } [HarmonyPatch(typeof(Skill), "HasAllRequirements")] internal static class HasAllRequirementsPatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } [HarmonyPostfix] private static void Postfix(Skill __instance, bool __result) { HuntAsOneCastDiag.LogHasAllRequirements(__instance, __result); } } [HarmonyPatch] internal static class ItemUsePatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } private static MethodBase TargetMethod() { return AccessTools.Method(typeof(Item), "Use", new Type[1] { typeof(Character) }, (Type[])null); } [HarmonyPostfix] private static void Postfix(Item __instance, bool __result) { HuntAsOneCastDiag.LogUse(__instance, __result); } } [HarmonyPatch] internal static class CastSpellItemPatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } private static MethodBase TargetMethod() { return AccessTools.Method(typeof(Character), "CastSpell", new Type[5] { typeof(SpellCastType), typeof(Item), typeof(SpellCastModifier), typeof(int), typeof(float) }, (Type[])null); } [HarmonyPostfix] private static void Postfix(Character __instance, SpellCastType _type, Item _itemReceiver, SpellCastModifier _modifier, bool __result) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) HuntAsOneCastDiag.LogCastSpellItem(__instance, _type, _itemReceiver, _modifier, __result); } } [HarmonyPatch] internal static class SendPerformSpellCastItemPatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } private static MethodBase TargetMethod() { return AccessTools.Method(typeof(Character), "SendPerformSpellCastItem", (Type[])null, (Type[])null); } [HarmonyPostfix] private static void Postfix(Character __instance, int _spellCastTypeID, string _itemUID, int _modifier) { HuntAsOneCastDiag.LogSendPerformSpellCastItem(__instance, _spellCastTypeID, _itemUID, _modifier); } } [HarmonyPatch(typeof(Skill), "SkillStarted")] internal static class SkillStartedPatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } [HarmonyPostfix] private static void Postfix(Skill __instance) { HuntAsOneCastDiag.LogSkillStarted(__instance); } } [HarmonyPatch(typeof(Character), "CastDone")] internal static class CastDonePatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } [HarmonyPrefix] private static void Prefix(Character __instance) { HuntAsOneCastDiag.LogCastDone(__instance); } } [HarmonyPatch(typeof(EffectSynchronizer), "RegisterEffect", new Type[] { typeof(Effect), typeof(EffectActivationStack) })] internal static class RegisterEffectPatch { private static bool Prepare() { return Plugin.CastDiagPatches.Value; } [HarmonyPostfix] private static void Postfix(Effect _effect) { HuntAsOneCastDiag.LogRegisterEffect(_effect); } } internal static class MusicRecon { private unsafe static string TrackName(Sounds s) { return ((object)(*(Sounds*)(&s))/*cast due to .constrained prefix*/).ToString(); } internal static string Ident(GlobalAudioManager g) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) if (g == null) { return ""; } int instanceID = ((Object)g).GetInstanceID(); string arg; try { object obj; if (!((Object)(object)g == (Object)null)) { Scene scene = ((Component)g).gameObject.scene; obj = ((Scene)(ref scene)).name; } else { obj = "DESTROYED"; } arg = (string)obj; } catch { arg = "DESTROYED"; } bool flag = false; try { flag = Global.AudioManager == g; } catch { } return string.Format("#{0}({1}{2})", instanceID, arg, flag ? ",SINGLETON" : ""); } internal static void Dump() { //IL_03d4: Unknown result type (might be due to invalid IL or missing references) //IL_03e3: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0439: 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_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_031e: Unknown result type (might be due to invalid IL or missing references) //IL_0323: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_0353: Unknown result type (might be due to invalid IL or missing references) //IL_0283: Unknown result type (might be due to invalid IL or missing references) //IL_0288: Unknown result type (might be due to invalid IL or missing references) ManualLogSource log = Plugin.Log; try { if (!Plugin.MusicReconPatches.Value) { log.LogMessage((object)"[MUSIC] NOTE: passive [MUSIC-TAP] taps DISABLED ([Diag] MusicReconPatches=false) -- this snapshot is live-read and fully valid, but no transition timeline is being recorded. Set MusicReconPatches=true + relaunch to re-arm."); } EnvironmentConditions instance = EnvironmentConditions.Instance; Scene scene; object obj; if (instance != null) { if (!((Object)(object)instance == (Object)null)) { object arg = ((Object)instance).GetInstanceID(); scene = ((Component)instance).gameObject.scene; obj = $"alive #{arg} scene='{((Scene)(ref scene)).name}'"; } else { obj = "DESTROYED (dangling)"; } } else { obj = "NULL"; } string text = (string)obj; log.LogMessage((object)("[MUSIC] EnvironmentConditions.Instance: " + text + (((Object)(object)instance == (Object)null) ? " — GAM.Update() EARLY-RETURNS before ALL music logic. SMOKING GUN: no music can start/switch until a scene load." : ""))); log.LogMessage((object)("[MUSIC] statics: current='" + TrackName(GlobalAudioManager.s_currentMusic) + "' wanted='" + TrackName(GlobalAudioManager.s_wantedMusic) + "'")); DictionaryExt s_musicSources = GlobalAudioManager.s_musicSources; for (int i = 0; i < s_musicSources.Values.Count; i++) { MusicSource val = s_musicSources.Values[i]; string text2 = (((Object)(object)val.Source == (Object)null) ? "DESTROYED" : $"playing={val.Source.isPlaying} vol={val.Source.volume:0.00}/{val.OrigVolume:0.00} t={val.Source.timeSamples} active={((Component)val.Source).gameObject.activeSelf}"); log.LogMessage((object)("[MUSIC] source '" + TrackName(s_musicSources.Keys[i]) + "' (created in '" + val.SceneName + "'): " + text2)); } GlobalCombatManager combatManager = Global.CombatManager; if ((Object)(object)combatManager == (Object)null) { log.LogMessage((object)"[MUSIC] Global.CombatManager is null/DESTROYED — nothing can start combat music at all."); } else { StringBuilder stringBuilder = new StringBuilder(); foreach (Character item in combatManager.PlayersInCombat) { stringBuilder.Append(((Object)(object)item == (Object)null) ? "" : item.Name).Append(';'); } object[] obj2 = new object[5] { ((Object)combatManager).GetInstanceID(), null, null, null, null }; scene = ((Component)combatManager).gameObject.scene; obj2[1] = ((Scene)(ref scene)).name; obj2[2] = combatManager.PlayersInCombat.Count; obj2[3] = stringBuilder; obj2[4] = combatManager.LocalPlayersInCombat.Count; log.LogMessage((object)string.Format("[MUSIC] GCM #{0} scene='{1}' players={2}[{3}] local={4}", obj2)); DescribeSubscribers(log, "OnLocalCombatStarted", combatManager.OnLocalCombatStarted); DescribeSubscribers(log, "OnLocalCombatEnded", combatManager.OnLocalCombatEnded); } GlobalAudioManager[] array = Resources.FindObjectsOfTypeAll(); int num = 0; GlobalAudioManager[] array2 = array; foreach (GlobalAudioManager val2 in array2) { if ((Object)(object)val2 == (Object)null) { continue; } scene = ((Component)val2).gameObject.scene; if (!((Scene)(ref scene)).IsValid()) { continue; } num++; string text3 = "n/a"; try { if (val2.MusicCombat != null && val2.MusicCombat.Length != 0) { text3 = TrackName(val2.ChooseCombatMusic()); } } catch (Exception ex) { text3 = "THREW:" + ex.GetType().Name; } string text4 = $"[MUSIC] GAM {Ident(val2)} active={((Behaviour)val2).isActiveAndEnabled} inCombat={val2.m_inCombat} firstMusicStarted={val2.m_firstMusicStarted} "; object[] obj3 = new object[4] { TrackName(val2.m_eventMusic), TrackName(val2.m_ambienceMusic), null, null }; CombatMusic[] musicCombat = val2.MusicCombat; obj3[2] = ((musicCombat != null) ? musicCombat.Length : 0); obj3[3] = text3; log.LogMessage((object)(text4 + string.Format("eventMusic='{0}' ambience='{1}' combatTracks={2} wouldPick='{3}'", obj3))); if (!val2.m_firstMusicStarted && ((Behaviour)val2).isActiveAndEnabled) { log.LogMessage((object)"[MUSIC] ^ GATE: firstMusicStarted=False — this instance's Update() SKIPS ALL music logic (StartAmbient yield-break path)."); } if ((int)val2.m_eventMusic != 0) { log.LogMessage((object)"[MUSIC] ^ GATE: eventMusic set — combat/ambience switching is fully blocked until it clears."); } } if (num > 1) { log.LogMessage((object)$"[MUSIC] {num} GAM instances ALIVE AT ONCE — their Update()s share the STATIC s_currentMusic/s_wantedMusic; if their picks differ they will silently fight (each stops the other's track). This is the duel candidate for Bug 12."); } if (num == 0) { log.LogMessage((object)"[MUSIC] no alive GlobalAudioManager in any scene!"); } } catch (Exception ex2) { log.LogWarning((object)("[MUSIC] dump failed: " + ex2)); } } private static void DescribeSubscribers(ManualLogSource log, string name, Action evt) { if (evt == null) { log.LogMessage((object)("[MUSIC] GCM." + name + ": NO SUBSCRIBERS — combat music can never start/stop. SMOKING GUN if this shows during a fight.")); return; } StringBuilder stringBuilder = new StringBuilder(); Delegate[] invocationList = evt.GetInvocationList(); foreach (Delegate obj in invocationList) { object? target = obj.Target; Object val = (Object)((target is Object) ? target : null); object obj2; if (obj.Target != null) { GlobalAudioManager val2 = (GlobalAudioManager)(object)((val is GlobalAudioManager) ? val : null); obj2 = ((val2 != null) ? ("GAM" + Ident(val2) + "." + obj.Method.Name) : string.Format("{0}#{1}.{2}{3}", obj.Target.GetType().Name, (val != (Object)null) ? val.GetInstanceID() : 0, obj.Method.Name, (val != null && val == (Object)null) ? "(DESTROYED)" : "")); } else { obj2 = "static " + obj.Method.DeclaringType?.Name + "." + obj.Method.Name; } string value = (string)obj2; stringBuilder.Append(value).Append("; "); } log.LogMessage((object)$"[MUSIC] GCM.{name}: {stringBuilder}"); } } [HarmonyPatch(typeof(GlobalAudioManager), "CombatStarted")] internal static class MusicTapCombatStarted { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Postfix(GlobalAudioManager __instance) { int num = (((Object)(object)Global.CombatManager != (Object)null) ? Global.CombatManager.LocalPlayersInCombat.Count : (-1)); Plugin.Log.LogMessage((object)($"[MUSIC-TAP] CombatStarted -> GAM {MusicRecon.Ident(__instance)} localCount={num} m_inCombat={__instance.m_inCombat}" + (__instance.m_inCombat ? "" : " — COUNT!=1, m_inCombat NOT set (stale-list candidate)"))); } } [HarmonyPatch(typeof(GlobalAudioManager), "CombatEnded")] internal static class MusicTapCombatEnded { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Postfix(GlobalAudioManager __instance) { int num = (((Object)(object)Global.CombatManager != (Object)null) ? Global.CombatManager.LocalPlayersInCombat.Count : (-1)); Plugin.Log.LogMessage((object)($"[MUSIC-TAP] CombatEnded -> GAM {MusicRecon.Ident(__instance)} localCount={num} m_inCombat={__instance.m_inCombat}" + ((!__instance.m_inCombat) ? "" : " — COUNT!=0, m_inCombat STILL TRUE (stuck-ON candidate)"))); } } [HarmonyPatch(typeof(GlobalAudioManager), "StartMusic")] internal static class MusicTapStart { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Postfix(GlobalAudioManager __instance, Sounds _music, float _fade) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)$"[MUSIC-TAP] StartMusic('{_music}', fade={_fade:0.#}) by GAM {MusicRecon.Ident(__instance)}"); } } [HarmonyPatch(typeof(GlobalAudioManager), "StopMusic")] internal static class MusicTapStop { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Postfix(GlobalAudioManager __instance, Sounds _music, float _fade) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)$"[MUSIC-TAP] StopMusic('{_music}', fade={_fade:0.#}) by GAM {MusicRecon.Ident(__instance)}"); } } [HarmonyPatch(typeof(GlobalAudioManager), "QueueMusic")] internal static class MusicTapQueue { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Prefix(GlobalAudioManager __instance, Sounds _music, bool _override) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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) Plugin.Log.LogMessage((object)($"[MUSIC-TAP] QueueMusic('{_music}', override={_override}) by GAM {MusicRecon.Ident(__instance)} " + $"(current='{GlobalAudioManager.s_currentMusic}' wanted='{GlobalAudioManager.s_wantedMusic}')")); } } [HarmonyPatch(typeof(GlobalAudioManager), "ChooseCombatMusic")] internal static class MusicTapChoose { private static readonly Dictionary _last = new Dictionary(); private static bool Prepare() { return Plugin.MusicReconPatches.Value; } internal static void ResetForScene() { _last.Clear(); } private static void Postfix(GlobalAudioManager __instance, Sounds __result) { //IL_0021: 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_0016: 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) int instanceID = ((Object)__instance).GetInstanceID(); if (!_last.TryGetValue(instanceID, out var value) || value != __result) { _last[instanceID] = __result; Plugin.Log.LogMessage((object)$"[MUSIC-TAP] ChooseCombatMusic -> '{__result}' by GAM {MusicRecon.Ident(__instance)}"); } } } [HarmonyPatch(typeof(GlobalAudioManager), "Start")] internal static class MusicTapGamStart { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Postfix(GlobalAudioManager __instance) { bool flag = (Object)(object)Global.CombatManager != (Object)null; bool flag2 = false; if (flag && Global.CombatManager.OnLocalCombatStarted != null) { Delegate[] invocationList = Global.CombatManager.OnLocalCombatStarted.GetInvocationList(); foreach (Delegate obj in invocationList) { if (obj.Target == __instance) { flag2 = true; break; } } } Plugin.Log.LogMessage((object)($"[MUSIC-TAP] GAM.Start {MusicRecon.Ident(__instance)} combatMgrAlive={flag} subscribed={flag2}" + (flag2 ? "" : " — THIS INSTANCE WILL NEVER RECEIVE COMBAT EVENTS"))); } } [HarmonyPatch(typeof(GlobalAudioManager), "Update")] internal static class MusicTapUpdateGate { private static bool _gated; private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Prefix() { bool flag = (Object)(object)EnvironmentConditions.Instance == (Object)null; if (flag != _gated) { _gated = flag; if (flag) { Plugin.Log.LogWarning((object)"[MUSIC-TAP] GlobalAudioManager.Update is now GATED (EnvironmentConditions.Instance null/destroyed) — ALL music logic suspended until a scene load (Bug-12 root-cause gate)."); } else { Plugin.Log.LogMessage((object)"[MUSIC-TAP] GlobalAudioManager.Update UNGATED (EnvironmentConditions.Instance alive again)."); } } } } [HarmonyPatch(typeof(GlobalCombatManager), "OnLevelWasLoaded")] internal static class MusicTapLevelClear { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Prefix(GlobalCombatManager __instance) { if (__instance.PlayersInCombat != null && __instance.PlayersInCombat.Count != 0) { Plugin.Log.LogMessage((object)$"[MUSIC-TAP] GCM.OnLevelWasLoaded clearing players={__instance.PlayersInCombat.Count} local={__instance.LocalPlayersInCombat.Count} WITHOUT CombatEnded events — subscribed GAMs' m_inCombat is now orphaned (stuck-ON mechanism)."); } } } [HarmonyPatch(typeof(GlobalCombatManager), "AddCombatCharacter")] internal static class MusicTapAddIgnored { private static bool Prepare() { return Plugin.MusicReconPatches.Value; } private static void Prefix(GlobalCombatManager __instance, Character _char) { if ((Object)(object)_char != (Object)null && _char.IsLocalPlayer && __instance.PlayersInCombat.Contains(_char)) { Plugin.Log.LogMessage((object)"[MUSIC-TAP] AddCombatCharacter IGNORED for local player (already in PlayersInCombat) — OnLocalCombatStarted will NOT fire (stuck-OFF candidate)."); } } } internal static class RecipeVerbs { internal static void RecipeDump(Character player) { //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0387: Unknown result type (might be due to invalid IL or missing references) //IL_02ea: Unknown result type (might be due to invalid IL or missing references) //IL_02ef: Unknown result type (might be due to invalid IL or missing references) //IL_034e: Unknown result type (might be due to invalid IL or missing references) //IL_0353: Unknown result type (might be due to invalid IL or missing references) RecipeManager instance = RecipeManager.Instance; if (instance == null) { Plugin.Log.LogMessage((object)"[RECIPE] RecipeManager.Instance is null — no recipes to inspect."); return; } List value; List list = ((instance.m_recipeUIDsPerUstensils != null && instance.m_recipeUIDsPerUstensils.TryGetValue((CraftingType)2, out value)) ? value : null); Plugin.Log.LogMessage((object)($"[RECIPE] recipedump: m_recipes={instance.m_recipes?.Count ?? (-1)}, " + $"Survival station list={list?.Count ?? (-1)} uid(s). " + $"{FeatherFletchSetup.RecipeUids.Count} bw.fletch uid(s), {FeatherFletchSetup.FletchableArrows.Count} fletchable arrow id(s).")); Plugin.Log.LogMessage((object)"[RECIPE] -- bw.fletch.* recipes --"); if (FeatherFletchSetup.RecipeUids.Count == 0) { Plugin.Log.LogMessage((object)"[RECIPE] (none registered this boot — kill-switch off, or see the [FLETCH] setup warnings)"); } foreach (string recipeUid in FeatherFletchSetup.RecipeUids) { DumpRecipe(recipeUid, instance, list, player); } Plugin.Log.LogMessage((object)"[RECIPE] -- vanilla Survival recipes resulting in a fletchable arrow --"); int num = 0; if (list != null) { foreach (UID item in list) { UID current2 = item; string value2 = ((UID)(ref current2)).Value; if (value2 != null && !FeatherFletchSetup.RecipeUids.Contains(value2) && instance.m_recipes != null && instance.m_recipes.TryGetValue(value2, out var value3) && !((Object)(object)value3 == (Object)null) && value3.Results != null && value3.Results.Length != 0 && FeatherFletchSetup.FletchableArrows.Contains(value3.Results[0].ItemID)) { num++; DumpRecipe(value2, instance, list, player); } } } if (num == 0) { Plugin.Log.LogMessage((object)"[RECIPE] (none found in the Survival station list — the vanilla arrow recipes are baked/learned per-save; craft one to LEARN it, then re-run)"); } Plugin.Log.LogMessage((object)"[RECIPE] -- ALL BW-registered recipes (bw.* / beastwhispering.*) --"); int num2 = 0; if (instance.m_recipes != null) { foreach (KeyValuePair recipe in instance.m_recipes) { string key = recipe.Key; if (key != null && IsBwUid(key)) { num2++; DumpBwRecipe(key, recipe.Value, instance, player); } } } if (num2 == 0) { Plugin.Log.LogMessage((object)"[RECIPE] (no bw.*/beastwhispering.* recipe in m_recipes — kill-switches off, or registration failed; see [BLANKET]/[TAMEFOOD]/[FLETCH] setup warnings)"); } DumpLearnedRecipes(instance, player); CraftingMenu[] array = Object.FindObjectsOfType(); Plugin.Log.LogMessage((object)$"[RECIPE] -- live CraftingMenu(s): {array.Length} --"); CraftingMenu[] array2 = array; foreach (CraftingMenu val in array2) { HashSet hashSet = new HashSet(); UID uID; if (val.m_complexeRecipes != null) { foreach (KeyValuePair complexeRecipe in val.m_complexeRecipes) { if ((Object)(object)complexeRecipe.Value != (Object)null) { uID = complexeRecipe.Value.UID; hashSet.Add(((UID)(ref uID)).Value); } } } HashSet hashSet2 = new HashSet(); if (val.m_allRecipes != null) { foreach (Recipe allRecipe in val.m_allRecipes) { if ((Object)(object)allRecipe != (Object)null) { uID = allRecipe.UID; hashSet2.Add(((UID)(ref uID)).Value); } } } Plugin.Log.LogMessage((object)($"[RECIPE] menu: displayedType={val.m_currentlyDisplayedCraftingType}, " + $"m_allRecipes={val.m_allRecipes?.Count ?? (-1)}, m_complexeRecipes={val.m_complexeRecipes?.Count ?? (-1)}, " + $"m_refreshComplexeRecipeRequired={val.m_refreshComplexeRecipeRequired}")); foreach (string recipeUid2 in FeatherFletchSetup.RecipeUids) { Plugin.Log.LogMessage((object)$"[RECIPE] '{recipeUid2}': in m_allRecipes={hashSet2.Contains(recipeUid2)}, in m_complexeRecipes(shown)={hashSet.Contains(recipeUid2)}"); } } if (array.Length == 0) { Plugin.Log.LogMessage((object)"[RECIPE] (no CraftingMenu instantiated — open a Survival crafting station and re-run to inspect the visible-list cache)"); } } private static void DumpRecipe(string uid, RecipeManager rm, List survival, Character player) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) Recipe value = null; if (rm.m_recipes != null) { rm.m_recipes.TryGetValue(uid, out value); } bool flag = false; if (survival != null) { foreach (UID item in survival) { UID current = item; if (((UID)(ref current)).Value == uid) { flag = true; break; } } } object obj; if (player == null) { obj = null; } else { CharacterInventory inventory = player.Inventory; obj = ((inventory != null) ? inventory.RecipeKnowledge : null); } string arg = ((!((Object)obj != (Object)null)) ? "no player" : (player.Inventory.RecipeKnowledge.IsRecipeLearned(uid) ? "LEARNED" : "not learned")); if ((Object)(object)value == (Object)null) { Plugin.Log.LogMessage((object)($"[RECIPE] '{uid}': in m_recipes=false, in Survival list={flag}, recipe={arg} " + "(NOT registered — a missing row here is a registration bug)")); return; } string arg2 = "?"; int num = ((value.Results != null && value.Results.Length != 0) ? value.Results[0].ItemID : (-1)); if (num != -1) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; object obj2; if (instance == null) { obj2 = null; } else { Item itemPrefab = instance.GetItemPrefab(num); obj2 = ((itemPrefab != null) ? itemPrefab.Name : null); } if (obj2 == null) { obj2 = "?"; } arg2 = (string)obj2; } Plugin.Log.LogMessage((object)($"[RECIPE] '{uid}': in m_recipes=true, in Survival list={flag}, " + $"RecipeID={value.RecipeID}, IngredientCount={value.IngredientCount}, " + $"station={value.CraftingStationType}, RequiredPType={value.RequiredPType}, " + $"result={arg2} ({num}), recipe={arg}")); } private static bool IsBwUid(string uid) { if (!uid.StartsWith("bw.", StringComparison.Ordinal)) { return uid.StartsWith("beastwhispering.", StringComparison.Ordinal); } return true; } private static void DumpBwRecipe(string uid, Recipe recipe, RecipeManager rm, Character player) { //IL_00cd: Unknown result type (might be due to invalid IL or missing references) object obj; if (player == null) { obj = null; } else { CharacterInventory inventory = player.Inventory; obj = ((inventory != null) ? inventory.RecipeKnowledge : null); } string text = ((!((Object)obj != (Object)null)) ? "no player" : (player.Inventory.RecipeKnowledge.IsRecipeLearned(uid) ? "LEARNED" : "not learned")); if ((Object)(object)recipe == (Object)null) { Plugin.Log.LogMessage((object)("[RECIPE] '" + uid + "': (null recipe object), recipe=" + text)); return; } int num = ((recipe.Results != null && recipe.Results.Length != 0) ? recipe.Results[0].ItemID : (-1)); object obj2; if (num == -1) { obj2 = "?"; } else { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; if (instance == null) { obj2 = null; } else { Item itemPrefab = instance.GetItemPrefab(num); obj2 = ((itemPrefab != null) ? itemPrefab.Name : null); } if (obj2 == null) { obj2 = "?"; } } string text2 = (string)obj2; Plugin.Log.LogMessage((object)($"[RECIPE] '{uid}': station={recipe.CraftingStationType}, in station list(s)=[{StationsOf(uid, rm)}], " + $"IngredientCount={recipe.IngredientCount}, result={text2} ({num}), recipe={text}")); } private static string StationsOf(string uid, RecipeManager rm) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //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) if (rm?.m_recipeUIDsPerUstensils == null) { return ""; } List list = new List(); foreach (KeyValuePair> recipeUIDsPerUstensil in rm.m_recipeUIDsPerUstensils) { if (recipeUIDsPerUstensil.Value == null) { continue; } foreach (UID item in recipeUIDsPerUstensil.Value) { UID current2 = item; if (((UID)(ref current2)).Value == uid) { list.Add(((object)recipeUIDsPerUstensil.Key/*cast due to .constrained prefix*/).ToString()); break; } } } return string.Join(", ", list.ToArray()); } private static void DumpLearnedRecipes(RecipeManager rm, Character player) { //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) object obj; if (player == null) { obj = null; } else { CharacterInventory inventory = player.Inventory; obj = ((inventory == null) ? null : ((CharacterKnowledge)(inventory.RecipeKnowledge?)).m_learnedItemUIDs); } List list = (List)obj; if (list == null) { Plugin.Log.LogMessage((object)"[RECIPE] -- learned recipes: no player / no RecipeKnowledge --"); return; } int startingRecipeCount = player.Inventory.RecipeKnowledge.StartingRecipeCount; Plugin.Log.LogMessage((object)($"[RECIPE] -- learned recipes (this save): {list.Count} total " + $"(StartingRecipeCount={startingRecipeCount}) — SUSPECT = bandage/linen/cloth/blanket/rag name-or-uid --")); int num = 0; foreach (string item in list) { if (item == null) { continue; } Recipe value = null; if (rm?.m_recipes != null) { rm.m_recipes.TryGetValue(item, out value); } int num2 = ((((value != null) ? value.Results : null) != null && value.Results.Length != 0) ? value.Results[0].ItemID : (-1)); object obj2; if (num2 == -1) { obj2 = (((Object)(object)value != (Object)null) ? "(no results)" : "(unresolved — not in m_recipes)"); } else { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; if (instance == null) { obj2 = null; } else { Item itemPrefab = instance.GetItemPrefab(num2); obj2 = ((itemPrefab != null) ? itemPrefab.Name : null); } if (obj2 == null) { obj2 = "?"; } } string text = (string)obj2; string text2 = (IsBwUid(item) ? "BW" : "vanilla"); bool flag = IsBug47Suspect(item) || IsBug47Suspect(text); if (flag) { num++; } if (text2 == "BW" || flag) { Plugin.Log.LogMessage((object)($"[RECIPE] [{text2}] '{item}': result={text} ({num2}), " + "station=" + (((Object)(object)value != (Object)null) ? ((object)value.CraftingStationType/*cast due to .constrained prefix*/).ToString() : "?") + (flag ? " <-- SUSPECT (Bug 47)" : ""))); } } Plugin.Log.LogMessage((object)($"[RECIPE] ({num} suspect row(s); non-suspect vanilla rows omitted — " + "compare the TOTAL count against a pre-repro dump to catch a newly-learned row).")); } private static bool IsBug47Suspect(string s) { if (string.IsNullOrEmpty(s)) { return false; } string text = s.ToLowerInvariant(); if (!text.Contains("bandage") && !text.Contains("linen") && !text.Contains("cloth") && !text.Contains("blanket")) { return text.Contains("rag"); } return true; } } internal static class RegistryDump { internal static void Run() { string text = Path.Combine(Paths.ConfigPath, "bw_registries"); Directory.CreateDirectory(text); DumpItems(text); DumpStatuses(text); DumpScenes(text); Plugin.Log.LogMessage((object)("[REGDUMP] wrote items/statuses/scenes.json to " + text + " — copy into data/registries/ for `bwspecies check`.")); } private static void DumpItems(string dir) { StringBuilder stringBuilder = new StringBuilder("{\n \"items\": [\n"); bool flag = true; int num = 0; foreach (KeyValuePair iTEM_PREFAB in ResourcesPrefabManager.ITEM_PREFABS) { Item value = iTEM_PREFAB.Value; if (!((Object)(object)value == (Object)null)) { if (!flag) { stringBuilder.Append(",\n"); } flag = false; num++; stringBuilder.Append($" {{ \"id\": {value.ItemID}, \"name\": {Q(value.Name)} }}"); } } stringBuilder.Append("\n ]\n}\n"); File.WriteAllText(Path.Combine(dir, "items.json"), stringBuilder.ToString()); Plugin.Log.LogMessage((object)$"[REGDUMP] items: {num}"); } private static void DumpStatuses(string dir) { List list = new List(ResourcesPrefabManager.STATUSEFFECT_PREFABS.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); StringBuilder stringBuilder = new StringBuilder("{\n \"statuses\": [\n"); for (int i = 0; i < list.Count; i++) { stringBuilder.Append(" ").Append(Q(list[i])).Append((i < list.Count - 1) ? ",\n" : "\n"); } stringBuilder.Append(" ]\n}\n"); File.WriteAllText(Path.Combine(dir, "statuses.json"), stringBuilder.ToString()); Plugin.Log.LogMessage((object)$"[REGDUMP] statuses: {list.Count}"); } private static void DumpScenes(string dir) { //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) string path = Path.Combine(dir, "scenes.json"); Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); if (File.Exists(path) && Json.Parse(File.ReadAllText(path), (Action)null) is Dictionary dictionary2 && dictionary2.TryGetValue("rosters", out var value) && value is Dictionary dictionary3) { foreach (KeyValuePair item2 in dictionary3) { if (!(item2.Value is List list)) { continue; } List list2 = new List(); foreach (object item3 in list) { if (item3 is string item) { list2.Add(item); } } dictionary[item2.Key] = list2; } } Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; List list3 = new List(); Character[] array = Object.FindObjectsOfType(); foreach (Character val in array) { if ((Object)(object)val != (Object)null && (Object)(object)((Component)val).GetComponent() != (Object)null && !string.IsNullOrEmpty(val.Name) && !list3.Contains(val.Name)) { list3.Add(val.Name); } } if (list3.Count > 0) { dictionary[name] = list3; } List list4 = new List(); for (int j = 0; j < SceneManager.sceneCountInBuildSettings; j++) { list4.Add(Path.GetFileNameWithoutExtension(SceneUtility.GetScenePathByBuildIndex(j))); } StringBuilder stringBuilder = new StringBuilder("{\n \"scenes\": [\n"); for (int k = 0; k < list4.Count; k++) { stringBuilder.Append(" ").Append(Q(list4[k])).Append((k < list4.Count - 1) ? ",\n" : "\n"); } stringBuilder.Append(" ],\n \"rosters\": {\n"); int num = 0; foreach (KeyValuePair> item4 in dictionary) { stringBuilder.Append(" ").Append(Q(item4.Key)).Append(": ["); for (int l = 0; l < item4.Value.Count; l++) { stringBuilder.Append(Q(item4.Value[l])).Append((l < item4.Value.Count - 1) ? ", " : ""); } stringBuilder.Append((++num < dictionary.Count) ? "],\n" : "]\n"); } stringBuilder.Append(" }\n}\n"); File.WriteAllText(path, stringBuilder.ToString()); Plugin.Log.LogMessage((object)$"[REGDUMP] scenes: {list4.Count} build scenes; roster for '{name}': {list3.Count} creature(s) (rosters accumulate across calls)."); } private static string Q(string s) { StringBuilder stringBuilder = new StringBuilder("\""); string text = s ?? ""; foreach (char c in text) { switch (c) { case '"': stringBuilder.Append("\\\""); break; case '\\': stringBuilder.Append("\\\\"); break; case '\n': stringBuilder.Append("\\n"); break; case '\r': stringBuilder.Append("\\r"); break; case '\t': stringBuilder.Append("\\t"); break; default: stringBuilder.Append(c); break; } } return stringBuilder.Append('"').ToString(); } } internal static class TestStateVerbs { internal static void SetLoyaltyVerb(Plugin p, Character player, string[] parts) { //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_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: 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_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.Sim == null) { Plugin.Log.LogWarning((object)"[DEV] no active pet."); return; } if (parts.Length < 2 || !int.TryParse(parts[1], out var result) || result < 0 || result > 100) { Plugin.Log.LogWarning((object)"[DEV] usage: setloyalty <0-100> (0 needs 'setloyalty 0 force')"); return; } if (result == 0 && (parts.Length < 3 || !string.Equals(parts[2], "force", StringComparison.OrdinalIgnoreCase))) { Plugin.Log.LogWarning((object)"[DEV] setloyalty 0 = ABANDONMENT — the pet despawns and its save clears on the next sim tick. If that is really what you want: 'setloyalty 0 force'. (For the bottom of a loyalty curve, use 'setloyalty 1'.)"); return; } int loyaltyValue = activePet.Sim.State.LoyaltyValue; LoyaltyTier val = Loyalty.Tier(loyaltyValue); PetStatus val2 = activePet.Sim.SetLoyalty(result); PetSystems.ApplyEffects(activePet, val2); p.Persistence.SaveNow(player); Plugin.Log.LogMessage((object)($"[DEV] loyalty {loyaltyValue} ({val}) -> {val2.LoyaltyValue} ({val2.Loyalty})." + ((result == 0) ? " 0 = abandonment — the pet despawns on the next sim tick." : ""))); } internal static void SetHungerVerb(Plugin p, Character player, string[] parts) { //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) Pet activePet = p.ActivePet; if (activePet?.Sim == null) { Plugin.Log.LogWarning((object)"[DEV] no active pet."); return; } if (parts.Length < 2 || !double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || result < 0.0 || result > 1.5) { Plugin.Log.LogWarning((object)"[DEV] usage: sethunger <0-1.5> (fraction of the hunger-day; 1.0 = a full unfed day)"); return; } double hungerFraction = activePet.Sim.HungerFraction; PetStatus status = activePet.Sim.SetHungerFraction(result); PetSystems.ApplyEffects(activePet, status); p.Persistence.SaveNow(player); Plugin.Log.LogMessage((object)($"[DEV] hunger {hungerFraction:P0} -> {activePet.Sim.HungerFraction:P0} " + $"(satiated={activePet.Sim.IsSatiated}, hunger-day={activePet.Sim.HungerSecondsPerDay:F0}s).")); } internal static void SetCourageVerb(Plugin p, Character player, string[] parts) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.Sim == null) { Plugin.Log.LogWarning((object)"[DEV] no active pet."); return; } if (parts.Length < 2 || !int.TryParse(parts[1], out var result) || result < 0 || result > 5) { Plugin.Log.LogWarning((object)$"[DEV] usage: setcourage <0-{5}>"); return; } int relicStacks = activePet.State.RelicStacks; activePet.State.RelicStacks = result; PetSystems.ApplyEffects(activePet, activePet.Sim.Status()); p.Persistence.SaveNow(player); RelicDef val = SpeciesRelics.For(activePet.SpeciesId); Plugin.Log.LogMessage((object)($"[DEV] relic stacks {relicStacks} -> {result} ({SpeciesRelics.Describe(result)})." + ((val != null) ? "" : (" NB '" + activePet.SpeciesId + "' has no species relic — a feed would refuse; dev-set stacks apply anyway.")))); } internal static void SetHealthRecoveryVerb(Plugin p, Character player, string[] parts) { Pet activePet = p.ActivePet; if (activePet?.Sim == null) { Plugin.Log.LogWarning((object)"[DEV] no active pet."); return; } if (parts.Length < 2 || !int.TryParse(parts[1], out var result) || result < 0 || result > 5) { Plugin.Log.LogWarning((object)$"[DEV] usage: hr <0-{5}> [seconds] (0 = clear; default {600.0:F0}s)"); return; } double result2 = 600.0; if (parts.Length > 2 && (!double.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out result2) || result2 <= 0.0)) { Plugin.Log.LogWarning((object)"[DEV] usage: hr <0-5> [seconds] (seconds must be > 0)"); return; } activePet.State.HealthRecoveryLevel = result; activePet.State.HealthRecoverySecondsLeft = ((result > 0) ? result2 : 0.0); p.Persistence.SaveNow(player); Plugin.Log.LogMessage((object)("[DEV] health recovery " + ((result > 0) ? (HealthRecovery.Describe(result, activePet.State.HealthRecoverySecondsLeft) + (Plugin.EnableFoodHealthRecovery.Value ? "" : " — NB EnableFoodHealthRecovery is OFF, the clock burns inert")) : "cleared") + ".")); } internal static void SimSkipVerb(Plugin p, string[] parts) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.Sim == null) { Plugin.Log.LogWarning((object)"[DEV] no active pet."); return; } if (parts.Length < 2 || !double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || result <= 0.0 || result > 86400.0) { Plugin.Log.LogWarning((object)"[DEV] usage: simskip (1-86400)"); return; } Plugin.Log.LogMessage((object)($"[DEV] simskip {result:F0}s: before loyalty={activePet.Sim.State.LoyaltyValue} " + $"hunger={activePet.Sim.HungerFraction:P0} comfort={activePet.Sim.State.TemperatureStage} " + $"blanket={activePet.Sim.State.BlanketSecondsLeft:F0}s drink={activePet.Sim.State.DrinkSecondsLeft:F0}s")); p.TickSystems((float)result); Pet activePet2 = p.ActivePet; if (activePet2?.Sim == null) { Plugin.Log.LogMessage((object)"[DEV] simskip done: the pet did not survive the skip (abandoned or died) — see the lines above."); } else { Plugin.Log.LogMessage((object)($"[DEV] simskip done: loyalty={activePet2.Sim.State.LoyaltyValue} " + $"hunger={activePet2.Sim.HungerFraction:P0} comfort={activePet2.Sim.State.TemperatureStage} " + $"blanket={activePet2.Sim.State.BlanketSecondsLeft:F0}s drink={activePet2.Sim.State.DrinkSecondsLeft:F0}s")); } } internal static void SetHpVerb(Plugin p, Character player, string[] parts) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Invalid comparison between Unknown and I4 HpArgs val = DevState.ParseSetHp(parts); if (val.Error != null) { Plugin.Log.LogWarning((object)("[DEV] sethp: " + val.Error)); return; } if ((int)val.Target == 1) { CharacterStats stats = player.Stats; if ((Object)(object)stats == (Object)null) { Plugin.Log.LogWarning((object)"[DEV] sethp: no player stats."); return; } float activeMaxHealth = stats.ActiveMaxHealth; float currentHealth = stats.CurrentHealth; stats.SetHealth(Mathf.Clamp((float)(val.IsPercent ? ((double)activeMaxHealth * val.Value / 100.0) : val.Value), 1f, activeMaxHealth)); Plugin.Log.LogMessage((object)$"[DEV] player hp {currentHealth:F0} -> {stats.CurrentHealth:F0}/{activeMaxHealth:F0}."); return; } Pet activePet = p.ActivePet; if (((Companion)(activePet?)).Anchor == null) { Plugin.Log.LogWarning((object)"[DEV] sethp: no active pet."); return; } if (PhotonNetwork.isNonMasterClientInRoom) { if (PetProxyClient.RequestSetHealth((float)val.Value, val.IsPercent)) { Plugin.Log.LogMessage((object)(string.Format("[DEV] [G→M] sethp pet {0:F0}{1} sent to the master ", val.Value, val.IsPercent ? "%" : "") + "(the proxied anchor owns the HP; watch the pet HUD / ck.proxy.hp mirror for the ack).")); } else { Plugin.Log.LogWarning((object)"[DEV] sethp: no announced proxy pet on the master yet — re-check the bond (dump) and retry."); } return; } float num = default(float); float num2 = default(float); if (!((Companion)activePet).Anchor.TryGetHealth(ref num, ref num2)) { Plugin.Log.LogWarning((object)"[DEV] sethp: no live anchor (a bodiless/ghost pet has no HP to set)."); return; } float health = (float)(val.IsPercent ? ((double)num2 * val.Value / 100.0) : val.Value); if (!((Companion)activePet).Anchor.SetHealth(health)) { Plugin.Log.LogWarning((object)"[DEV] sethp: set failed (anchor died mid-call?)."); } } } internal static class EmbeddedRes { internal static string Text(string suffix, string logTag) { return EmbeddedRes.Text(typeof(Plugin).Assembly, suffix, logTag, Plugin.Log); } internal static Sprite Sprite(string suffix, SpriteBorderTypes border, string logTag) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) Texture2D val = Texture(suffix, logTag); if ((Object)(object)val == (Object)null) { return null; } Sprite val2 = CustomTextures.CreateSprite(val, border); return IconPin.Pin(val2); } internal static Texture2D Texture(string suffix, string logTag) { return EmbeddedRes.Texture(typeof(Plugin).Assembly, suffix, logTag, Plugin.Log); } } [HarmonyPatch(typeof(CraftingMenu), "CraftingDone")] internal static class FeatherFletchCrafting { internal static bool Prefix(CraftingMenu __instance) { //IL_0086: Unknown result type (might be due to invalid IL or missing references) Recipe val; try { if (!Plugin.EnableFeatherFletching.Value) { return true; } if (FeatherFletchSetup.RecipeUids.Count == 0) { return true; } int num = ((__instance.m_lastRecipeIndex != -1) ? __instance.m_complexeRecipes[__instance.m_lastRecipeIndex].Key : __instance.m_lastFreeRecipeIndex); if (num < 0 || num >= __instance.m_allRecipes.Count) { return true; } val = __instance.m_allRecipes[num]; if ((Object)(object)val == (Object)null || !FeatherFletchSetup.RecipeUids.Contains(UID.op_Implicit(val.UID))) { return true; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] craft-intercept identification failed (nothing consumed) — vanilla craft runs: " + ex)); return true; } try { Craft(__instance, val); } catch (Exception ex2) { Plugin.Log.LogError((object)("[FLETCH] craft intercept failed mid-craft: " + ex2)); } return false; } private static void Craft(CraftingMenu menu, Recipe recipe) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_02c7: Unknown result type (might be due to invalid IL or missing references) //IL_0353: Unknown result type (might be due to invalid IL or missing references) //IL_035a: Expected O, but got Unknown //IL_03f0: Unknown result type (might be due to invalid IL or missing references) //IL_0400: Expected O, but got Unknown //IL_0269: Unknown result type (might be due to invalid IL or missing references) //IL_0273: Expected I4, but got Unknown //IL_049b: Unknown result type (might be due to invalid IL or missing references) //IL_056f: Unknown result type (might be due to invalid IL or missing references) menu.m_craftingTimer = -999f; CompatibleIngredient val = null; CompatibleIngredient val2 = null; IngredientSelector[] ingredientSelectors = menu.m_ingredientSelectors; foreach (IngredientSelector val3 in ingredientSelectors) { CompatibleIngredient val4 = (((Object)(object)val3 != (Object)null) ? val3.AssignedIngredient : null); if (val4 != null) { if (FeatherFletching.PercentForItemId(val4.ItemID).HasValue) { val = val4; } else if (FeatherFletchSetup.FletchableArrows.Contains(val4.ItemID)) { val2 = val4; } } } if (val == null || val2 == null) { Plugin.Log.LogWarning((object)$"[FLETCH] '{recipe.UID}' matched but selectors held feather={val != null}, arrow={val2 != null} — craft aborted, nothing consumed."); ResetProgressPanel(menu); return; } FeatherQuality val5 = FeatherFletching.QualityForItemId(val.ItemID); Item val6 = null; for (int j = 0; j < val2.m_ownedItems.Count; j++) { Item val7 = val2.m_ownedItems[j]; if (val7 is Ammunition && !val7.IsPendingDestroy) { if ((Object)(object)val6 == (Object)null) { val6 = val7; } if (FletchState.QualityOf(val7) == null) { val6 = val7; break; } } } if ((Object)(object)val6 == (Object)null || val6.RemainingAmount <= 0) { Plugin.Log.LogWarning((object)"[FLETCH] the assigned arrow ingredient has no owned stack — craft aborted, nothing consumed."); ResetProgressPanel(menu); return; } FeatherQuality val8 = FletchState.QualityOf(val6); if (!FeatherFletching.CanUpgradeFletch(val8?.Percent, val5.Percent)) { Notify.Player(((UIElement)menu).LocalCharacter, $"These arrows already carry an equal or finer fletching (+{val8.Percent}%)."); Plugin.Log.LogMessage((object)($"[FLETCH] refused: '{val6.Name}' carries +{val8.Percent}% and the " + $"{val5.Quality} feather offers +{val5.Percent}% — upgrade-only, nothing consumed.")); ResetProgressPanel(menu); return; } List list = new List(); Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(val6.ItemID); WeaponStats val9 = (((Object)(object)itemPrefab != (Object)null) ? ((Component)itemPrefab).GetComponent() : null); if ((Object)(object)val9 != (Object)null && val9.BaseDamage != null && val9.BaseDamage.List != null) { foreach (DamageType item in val9.BaseDamage.List) { if (item != null && item.Damage > 0f) { list.Add((int)item.Type); } } } int num = FeatherFletching.PickFlatType((IList)list, (double)Random.value); int num2 = FeatherFletching.EnchantmentIdFor(val5, num); int? num3 = FeatherFletching.FlatTypeForEnchantmentId(num2); string text = (num3.HasValue ? ((object)(Types)num3.Value/*cast due to .constrained prefix*/).ToString() : num.ToString()); Plugin.Log.LogMessage((object)("[FLETCH] '" + val6.Name + "' present damage types [" + string.Join(",", list) + "] -> " + $"flat {text}, {val5.Quality} variant {num2}" + ((list.Count > 1) ? " (multi-type arrow: type chosen at random)." : "."))); Equipment val10 = (Equipment)val6; if (val8 != null) { FeatherFletchSetup.RemoveFletchEnchantments(val10); Plugin.Log.LogMessage((object)$"[FLETCH] upgrading '{val6.Name}': +{val8.Percent}% removed for +{val5.Percent}%."); } val10.AddEnchantment(num2, false); if (!val10.ActiveEnchantmentIDs.Contains(num2)) { Plugin.Log.LogError((object)($"[FLETCH] AddEnchantment({num2}) did not take on '{val6.Name}' " + "(registration bug, report) — craft aborted, feather kept.")); ResetProgressPanel(menu); return; } if (Plugin.FletchNameSuffix.Value) { FletchState.StampName((Ammunition)val6, val5.Percent); } val.StackCountInUse = 1; int num4 = default(int); List> list2 = new List>(val.GetConsumedItems(false, ref num4)); for (int k = 0; k < menu.m_ingredientSelectors.Length; k++) { IngredientSelector val11 = menu.m_ingredientSelectors[k]; CompatibleIngredient val12 = (((Object)(object)val11 != (Object)null) ? val11.AssignedIngredient : null); if (val12 != null) { val11.Free(false); menu.m_lastFreeRecipeIngredientIDs[k] = ((val12.AvailableQty > 0) ? val12.ItemID : (-1)); } } ItemManager.Instance.ConsumeCraftingItems((string[])null, list2.ToArray()); if (!((UIElement)menu).LocalCharacter.Inventory.RecipeKnowledge.IsRecipeLearned(UID.op_Implicit(recipe.UID))) { menu.m_refreshComplexeRecipeRequired = true; ((UIElement)menu).LocalCharacter.Inventory.RecipeKnowledge.LearnRecipe(recipe); } Global.AudioManager.PlaySound((Sounds)90200, 0f, 1f, 1f, 1f, 1f); ResetProgressPanel(menu); Notify.Player(((UIElement)menu).LocalCharacter, $"Fletched {val6.RemainingAmount}x {val6.Name}."); Plugin.Log.LogMessage((object)($"[FLETCH] crafted: 1 {val5.ItemName} -> {val6.RemainingAmount}x '{val6.Name}' " + $"enchanted in place with {num2} (flat {text}, +{val5.Percent}%; {recipe.UID}).")); } private static void ResetProgressPanel(CraftingMenu menu) { if ((Object)(object)menu.m_craftProgressPanel != (Object)null && menu.m_craftProgressPanel.activeSelf) { menu.m_craftProgressPanel.SetActive(false); } if ((Object)(object)menu.m_sldCraftProgress != (Object)null) { menu.m_sldCraftProgress.normalizedValue = 0f; } } } internal static class FeatherFletchSetup { internal const string FeatherTagName = "BW_PearlbirdFeather"; private const int FeatherStackMax = 999; private const int FeatherDonorId = 6500090; private const string NeverQuestEventUid = "bw.fletch.never"; internal static readonly Dictionary Registered = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static readonly HashSet FletchableArrows = new HashSet(); internal static readonly HashSet RecipeUids = new HashSet(StringComparer.Ordinal); private static Tag _featherTag = Tag.None; private static bool _done; internal static bool FeatherRegistered { get; private set; } internal static bool EnchantmentsRegistered { get; private set; } internal static int EnchantmentVariantsRegistered { get; private set; } internal static void Init() { SL.OnPacksLoaded += Setup; } private static void Setup() { if (_done) { return; } _done = true; if (!Plugin.EnableFeatherFletching.Value) { Plugin.Log.LogMessage((object)"[FLETCH] EnableFeatherFletching=false — no feather items, enchantments or recipes registered."); } else if (RegisterFeathers() && RegisterEnchantments()) { SlFeatureSetup.Run("[FLETCH]", Plugin.EnableFeatherFletching, "EnableFeatherFletching=false — no fletch recipes registered.", FletchTable.Table.Values, (FletchEntry e) => e.Key, RegisterArrow, () => Registered.Count, "fletchable arrow recipes"); } } private static bool RegisterFeathers() { //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_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ResourcesPrefabManager.Instance.GetItemPrefab(6500090) == (Object)null) { Plugin.Log.LogWarning((object)$"[FLETCH] feather donor ItemID {6500090} (Linen Cloth) is not a known item — feature disabled this boot."); return false; } _featherTag = CustomTags.GetTag("BW_PearlbirdFeather", false); if (_featherTag.TagName != "BW_PearlbirdFeather") { _featherTag = CustomTags.CreateTag("BW_PearlbirdFeather"); } if (_featherTag.TagName != "BW_PearlbirdFeather") { Plugin.Log.LogWarning((object)"[FLETCH] custom tag 'BW_PearlbirdFeather' failed to register — feature disabled this boot."); return false; } int num = 0; FeatherQuality[] qualities = FeatherFletching.Qualities; foreach (FeatherQuality q in qualities) { if (RegisterQuality(q)) { num++; } } FeatherRegistered = num == FeatherFletching.Qualities.Length; if (!FeatherRegistered) { Plugin.Log.LogWarning((object)$"[FLETCH] only {num}/{FeatherFletching.Qualities.Length} feather qualities registered — enchantments/recipes skipped this boot (see warnings above)."); } return FeatherRegistered; } private static bool RegisterQuality(FeatherQuality q) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Unknown result type (might be due to invalid IL or missing references) if (!IdPool.Claim(q.ItemId, "[FLETCH] " + q.ItemName, (Action)delegate(string m) { Plugin.Log.LogError((object)m); })) { return false; } SL_Item val = new SL_Item(); val.Target_ItemID = 6500090; val.New_ItemID = q.ItemId; val.Name = q.ItemName; val.Description = $"A {q.Quality.ToLowerInvariant()} feather, given in trust at a {q.Tier} bond. " + "Said to true an arrow's flight and lend it a fiercer bite."; val.Tags = new string[1] { "BW_PearlbirdFeather" }; SL_Item val2 = val; ((ContentTemplate)val2).ApplyTemplate(); Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(q.ItemId); if ((Object)(object)itemPrefab == (Object)null) { Plugin.Log.LogWarning((object)$"[FLETCH] '{q.ItemName}' prefab {q.ItemId} missing after ApplyTemplate — quality skipped."); return false; } MultipleUsage val3 = ((Component)itemPrefab).GetComponent(); if ((Object)(object)val3 == (Object)null) { val3 = ((Component)itemPrefab).gameObject.AddComponent(); } val3.AutoStack = true; val3.m_maxStackAmount = 999; Sprite val4 = EmbeddedRes.Sprite("PearlbirdFeather" + q.Quality + ".png", (SpriteBorderTypes)1, "[FLETCH]"); if ((Object)(object)val4 != (Object)null) { CustomItemVisuals.SetSpriteLink(itemPrefab, val4, false); } ItemNameIndex.RegisterCustom(q.ItemName, q.ItemId); Plugin.Log.LogMessage((object)($"[FLETCH] feather quality {q.ItemId} '{q.ItemName}' registered " + string.Format("(+{0}%, tier {1}, stacks to {2}, tag '{3}').", q.Percent, q.Tier, 999, "BW_PearlbirdFeather"))); return true; } private static bool RegisterEnchantments() { int num = 0; int num2 = 0; FeatherQuality[] qualities = FeatherFletching.Qualities; foreach (FeatherQuality q in qualities) { foreach (int item in VariantTypes(q)) { num2++; if (RegisterEnchantment(q, item)) { num++; } } } EnchantmentVariantsRegistered = num; EnchantmentsRegistered = num == num2; if (!EnchantmentsRegistered) { Plugin.Log.LogWarning((object)$"[FLETCH] only {num}/{num2} fletch enchantment variants registered — recipes skipped this boot (see warnings above)."); } return EnchantmentsRegistered; } private static IEnumerable VariantTypes(FeatherQuality q) { FeatherQuality val = FeatherFletching.Qualities[FeatherFletching.Qualities.Length - 1]; if (q == val) { yield return 8; yield break; } for (int t = 0; t <= 5; t++) { yield return t; } } private static bool RegisterEnchantment(FeatherQuality q, int damageType) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: 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) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01b6: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_0260: Unknown result type (might be due to invalid IL or missing references) //IL_02c5: Unknown result type (might be due to invalid IL or missing references) //IL_02d7: Unknown result type (might be due to invalid IL or missing references) int num = FeatherFletching.EnchantmentIdFor(q, damageType); Types val = (Types)damageType; if (!IdPool.Claim(num, $"[FLETCH] {q.Quality} {val} enchantment", (Action)delegate(string m) { Plugin.Log.LogError((object)m); })) { return false; } if ((Object)(object)ResourcesPrefabManager.Instance.GetEnchantmentPrefab(num) != (Object)null) { Plugin.Log.LogWarning((object)($"[FLETCH] enchantment id {num} is already registered (another mod?) — " + $"'{q.Quality}'/{val} fletching skipped; recipes disabled this boot.")); return false; } float num2 = (float)q.FlatX * (1f + (float)q.Percent / 100f); SL_Damage[] flatDamageAdded = (SL_Damage[])(object)new SL_Damage[1] { new SL_Damage { Type = val, Damage = num2 } }; AdditionalDamage[] array = (AdditionalDamage[])(object)new AdditionalDamage[6]; for (int num3 = 0; num3 < array.Length; num3++) { array[num3] = new AdditionalDamage { AddedDamageType = (Types)num3, SourceDamageType = (Types)num3, ConversionRatio = (float)q.Percent / 100f }; } SL_EnchantmentRecipe val2 = new SL_EnchantmentRecipe(); val2.EnchantmentID = num; val2.Name = $"Pearl Fletching (+{q.Percent}%)"; val2.Description = "Fletched with a " + q.Quality.ToLowerInvariant() + " Pearlbird feather, given in trust — " + $"these arrows fly truer and strike {q.Percent}% harder."; val2.IncenseItemID = FeatherFletching.Qualities[0].ItemId; val2.PillarDatas = (PillarData[])(object)new PillarData[1] { new PillarData { Direction = (Directions)0, IsFar = false } }; val2.CompatibleEquipment = new EquipmentData { RequiredTag = "BW_PearlbirdFeather" }; val2.QuestEventUID = "bw.fletch.never"; val2.AddedDamages = array; val2.FlatDamageAdded = flatDamageAdded; SL_EnchantmentRecipe val3 = val2; ((ContentTemplate)val3).ApplyTemplate(); bool flag = (Object)(object)ResourcesPrefabManager.Instance.GetEnchantmentPrefab(num) != (Object)null; bool flag2 = RecipeManager.Instance != null && RecipeManager.Instance.m_enchantmentRecipes != null && RecipeManager.Instance.m_enchantmentRecipes.ContainsKey(num); if (!flag || !flag2) { Plugin.Log.LogWarning((object)($"[FLETCH] enchantment {num} '{q.Quality}'/{val} incomplete after ApplyTemplate " + $"(prefab={flag}, recipe={flag2}) — recipes disabled this boot.")); return false; } Plugin.Log.LogMessage((object)($"[FLETCH] enchantment {num} 'Pearl Fletching (+{q.Percent}%)' registered " + $"({q.Quality}/{val}; flat +{num2:0.##} {val} × identity ×{(float)q.Percent / 100f:0.00}, " + "table-locked via quest event 'bw.fletch.never').")); return true; } internal static bool TryTierSwap(int resolvedId, int loyalty, out int tierId, out FeatherQuality quality) { if (FeatherFletching.QualityForItemId(resolvedId) == null) { tierId = resolvedId; quality = null; return false; } quality = FeatherFletching.QualityFor(loyalty); tierId = quality.ItemId; return true; } private static void RegisterArrow(FletchEntry entry) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) int? num = ResolveArrow(entry); if (!num.HasValue) { return; } if (!(ResourcesPrefabManager.Instance.GetItemPrefab(num.Value) is Ammunition)) { Plugin.Log.LogWarning((object)$"[FLETCH] '{entry.Key}': ItemID {num.Value} is not an Ammunition item — row skipped."); return; } string text = RecipeUid(entry.Key); Recipe val = SlConsumables.RegisterRecipe("[FLETCH]", entry.Key, text, (CraftingType)2, new SlConsumables.IngredientSpec[2] { SlConsumables.IngredientSpec.Generic(_featherTag), SlConsumables.IngredientSpec.Specific(num.Value) }, num.Value, 1); if (!((Object)(object)val == (Object)null)) { Registered[entry.Key] = num.Value; FletchableArrows.Add(num.Value); RecipeUids.Add(text); Plugin.Log.LogMessage((object)($"[FLETCH] '{entry.Key}': arrow {num.Value} fletchable, recipe '{text}' " + "(Survival: any-quality feather + the arrow — enchants the stack in place).")); } } internal static string RecipeUid(string arrowKey) { return "bw.fletch." + (arrowKey ?? string.Empty).Trim().ToLowerInvariant().Replace(' ', '_'); } private static int? ResolveArrow(FletchEntry entry) { if (entry.ArrowId.HasValue) { if ((Object)(object)ResourcesPrefabManager.Instance.GetItemPrefab(entry.ArrowId.Value) == (Object)null) { Plugin.Log.LogWarning((object)$"[FLETCH] '{entry.Key}': arrow ItemID {entry.ArrowId.Value} is not a known item — row skipped."); return null; } return entry.ArrowId.Value; } if (ItemNameIndex.TryResolve(entry.ArrowKey, out var itemId)) { Plugin.Log.LogMessage((object)$"[FLETCH] '{entry.Key}': arrow '{entry.ArrowKey}' resolved to ItemID {itemId} (put the number in FeatherFletch.json to make it locale-proof)."); return itemId; } Plugin.Log.LogWarning((object)("[FLETCH] '" + entry.Key + "': arrow '" + entry.ArrowKey + "' matches no item display name on this locale — row skipped.")); return null; } internal static bool RemoveFletchEnchantments(Equipment eq) { if ((Object)(object)eq == (Object)null) { return false; } bool flag = false; for (int num = eq.m_activeEnchantments.Count - 1; num >= 0; num--) { Enchantment val = eq.m_activeEnchantments[num]; if (!((Object)(object)val == (Object)null) && FeatherFletching.IsFletchEnchantment(((EffectPreset)val).PresetID)) { val.UnapplyEnchantment(); eq.m_activeEnchantments.RemoveAt(num); flag = true; } } flag |= eq.m_enchantmentIDs.RemoveAll((Predicate)FeatherFletching.IsFletchEnchantment) > 0; if (flag) { eq.m_enchantmentsHaveChanged = true; eq.RefreshEnchantmentModifiers(); } return flag; } } internal static class FletchState { internal static FeatherQuality QualityOf(Item item) { Equipment val = (Equipment)(object)((item is Equipment) ? item : null); if (val == null || val.ActiveEnchantmentIDs == null) { return null; } for (int i = 0; i < val.ActiveEnchantmentIDs.Count; i++) { FeatherQuality val2 = FeatherFletching.QualityForEnchantmentId(val.ActiveEnchantmentIDs[i]); if (val2 != null) { return val2; } } return null; } internal static Types? FlatTypeOf(Item item) { Equipment val = (Equipment)(object)((item is Equipment) ? item : null); if (val == null || val.ActiveEnchantmentIDs == null) { return null; } for (int i = 0; i < val.ActiveEnchantmentIDs.Count; i++) { int? num = FeatherFletching.FlatTypeForEnchantmentId(val.ActiveEnchantmentIDs[i]); if (num.HasValue) { return (Types)num.Value; } } return null; } internal static string SetKeyOf(Item item) { Equipment val = (Equipment)(object)((item is Equipment) ? item : null); if (val == null) { return string.Empty; } return FeatherFletching.EnchantmentSetKey((IEnumerable)val.ActiveEnchantmentIDs); } internal static void StampName(Ammunition instance, int percent) { if ((Object)(object)instance == (Object)null) { return; } Item val = ((ResourcesPrefabManager.Instance != null) ? ResourcesPrefabManager.Instance.GetItemPrefab(((Item)instance).ItemID) : null); string text = (((Object)(object)val != (Object)null) ? val.Name : ((Item)instance).m_name); if (!string.IsNullOrEmpty(text)) { ((Item)instance).m_localizedName = FeatherFletching.DisplayNameFor(text, percent); if ((Object)(object)LocalizationManager.Instance != (Object)null) { ((Item)instance).m_lastNameLang = LocalizationManager.Instance.CurrentLanguage; } } } } [HarmonyPatch(typeof(ItemContainer), "AddItem", new Type[] { typeof(Item), typeof(bool) })] internal static class FeatherStackMergeGuard { internal static bool Prefix(ItemContainer __instance, Item _item, ref bool _stackIfPossible, ref bool __result) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0264: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Unknown result type (might be due to invalid IL or missing references) try { if (!Plugin.EnableFeatherFletching.Value) { return true; } if (!_stackIfPossible || !(_item is Ammunition) || !_item.IsStackable) { return true; } if ((Object)(object)__instance == (Object)null || __instance.m_containedItems == null) { return true; } if (PhotonNetwork.isNonMasterClientInRoom) { return true; } if (_item.IsFull) { _stackIfPossible = false; return true; } List list = new List(); bool flag = ((Item)(Equipment)_item).IsEnchanted; for (int i = 0; i < __instance.m_containedItems.Count; i++) { Item val = __instance.m_containedItems.Values[i]; if (!((Object)(object)val == (Object)null) && val.ItemID == _item.ItemID && !val.IsPendingDestroy && !val.m_beingThrown) { list.Add(val); Equipment val2 = (Equipment)(object)((val is Equipment) ? val : null); if (val2 != null && ((Item)val2).IsEnchanted) { flag = true; } } } if (!flag) { return true; } string text = FletchState.SetKeyOf(_item); List list2 = new List(list.Count); for (int j = 0; j < list.Count; j++) { list2.Add(new StackSlot(FletchState.SetKeyOf(list[j]), list[j].MaxStackAmount - list[j].RemainingAmount)); } bool flag2 = default(bool); int num = default(int); if (!FeatherFletching.ShouldMergeEnchantedStack(text, (IEnumerable)list2, ref flag2, ref num)) { _stackIfPossible = false; Plugin.Log.LogMessage((object)($"[FLETCH] kept '{_item.Name}' (x{_item.RemainingAmount}) as its own stack — " + "no same-fletching stack to merge into (differently-fletched stacks never combine).")); return true; } if (!flag2) { return true; } if (_item.RemainingAmount > num) { _stackIfPossible = false; return true; } __instance.m_itemsBaseSellValue += _item.Value; for (int k = 0; k < list.Count && _item.RemainingAmount > 0; k++) { Item val3 = list[k]; if (!string.Equals(FletchState.SetKeyOf(val3), text, StringComparison.Ordinal)) { continue; } int num2 = val3.MaxStackAmount - val3.RemainingAmount; if (num2 <= 0) { continue; } int num3 = Math.Min(num2, _item.RemainingAmount); val3.RemainingAmount += num3; val3.SetAquireTime(_item.AquireTime); if ((Object)(object)((EffectSynchronizer)__instance).OwnerCharacter != (Object)null) { if (_item.PreviousOwnerUID != null) { string previousOwnerUID = _item.PreviousOwnerUID; UID uID = ((EffectSynchronizer)__instance).OwnerCharacter.UID; if (!(previousOwnerUID != ((UID)(ref uID)).Value)) { goto IL_0281; } } val3.SetIsNew(true); } goto IL_0281; IL_0281: _item.RemainingAmount -= num3; } ItemManager.Instance.DestroyItem(_item.UID); __instance.UpdateVersion(); __result = false; Plugin.Log.LogMessage((object)("[FLETCH] merged '" + _item.Name + "' into its same-fletching stack past a differently-fletched stack of the same arrow (P7).")); return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] AddItem merge guard error: " + ex)); return true; } } } [HarmonyPatch(typeof(ItemManager), "CloneItem")] internal static class FeatherSplitGuard { private static readonly FieldInfo QueueField = AccessTools.Field(typeof(Equipment), "m_firstUpdateApplyEnchantments"); internal static void Postfix(Item _item, Item __result) { try { if (!Plugin.EnableFeatherFletching.Value) { return; } Ammunition val = (Ammunition)(object)((_item is Ammunition) ? _item : null); if (val == null) { return; } Ammunition val2 = (Ammunition)(object)((__result is Ammunition) ? __result : null); if (val2 == null || !((Item)val).IsEnchanted) { return; } if (!(QueueField?.GetValue(val2) is List list)) { Plugin.Log.LogWarning((object)"[FLETCH] split guard: Equipment.m_firstUpdateApplyEnchantments not found on this game version — the split-off half stays PLAIN."); return; } for (int i = 0; i < ((Equipment)val).ActiveEnchantmentIDs.Count; i++) { list.Add(((Equipment)val).ActiveEnchantmentIDs[i]); } ((Item)val2).RefreshAfterLoading(); FeatherQuality val3 = FletchState.QualityOf((Item)(object)val); if (val3 != null && Plugin.FletchNameSuffix.Value) { FletchState.StampName(val2, val3.Percent); } Plugin.Log.LogMessage((object)("[FLETCH] split defers enchantment(s) to first update on the new '" + ((Item)val2).Name + "' stack.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] CloneItem split guard error: " + ex)); } } } [HarmonyPatch(typeof(CharacterInventory), "GenerateItem", new Type[] { typeof(Item), typeof(int), typeof(bool) })] internal static class FeatherGenerateItemGuard { internal static bool Prefix(CharacterInventory __instance, Item _itemToGenerate, int _quantity, bool _tryToEquip) { try { if (!Plugin.EnableFeatherFletching.Value) { return true; } if ((Object)(object)_itemToGenerate == (Object)null || !(_itemToGenerate is Ammunition) || !_itemToGenerate.IsStackable) { return true; } ItemContainer mostRelevantContainer = __instance.GetMostRelevantContainer(_itemToGenerate); if ((Object)(object)mostRelevantContainer == (Object)null) { return true; } IList itemsFromID = mostRelevantContainer.GetItemsFromID(_itemToGenerate.ItemID); bool flag = false; for (int i = 0; i < itemsFromID.Count; i++) { Item obj = itemsFromID[i]; Equipment val = (Equipment)(object)((obj is Equipment) ? obj : null); if (val != null && ((Item)val).IsEnchanted) { flag = true; break; } } if (!flag) { return true; } bool flag2 = false; while (_quantity > 0) { if (!flag2 && _itemToGenerate.IsStackable) { IList itemsFromID2 = mostRelevantContainer.GetItemsFromID(_itemToGenerate.ItemID); for (int j = 0; j < itemsFromID2.Count; j++) { if (_quantity <= 0) { break; } Item val2 = itemsFromID2[j]; if (val2.IsPendingDestroy) { continue; } Equipment val3 = (Equipment)(object)((val2 is Equipment) ? val2 : null); if (val3 != null && ((Item)val3).IsEnchanted) { continue; } int num = val2.MaxStackAmount - val2.RemainingAmount; if (num > 0) { int num2 = Mathf.Min(num, _quantity); val2.RemainingAmount += num2; val2.SetAquireTime(EnvironmentConditions.GameTimeF); if (PhotonNetwork.isNonMasterClientInRoom) { Global.RPCManager.SendItemSyncToMaster(val2.ToNetworkData()); } _quantity -= num2; } } flag2 = true; } if (_quantity > 0) { Item val4 = ItemManager.Instance.GenerateItemNetwork(_itemToGenerate.ItemID); if (!val4.IsStackable) { _quantity--; } else { val4.RemainingAmount = Mathf.Clamp(_quantity, 0, val4.MaxStackAmount); _quantity -= val4.RemainingAmount; } __instance.TakeItem(val4, _tryToEquip); } } Plugin.Log.LogMessage((object)("[FLETCH] GenerateItem guarded: fresh '" + _itemToGenerate.Name + "' kept out of the fletched stack(s).")); return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] GenerateItem guard error (vanilla runs): " + ex)); return true; } } } [HarmonyPatch(typeof(CharacterInventory), "UnloadAmmunition", new Type[] { typeof(int), typeof(int) })] internal static class FeatherUnloadAmmunitionGuard { internal static bool Prefix(CharacterInventory __instance, int _itemID, int _shotCount) { try { if (!Plugin.EnableFeatherFletching.Value) { return true; } if (_itemID == -1 || PhotonNetwork.isNonMasterClientInRoom) { return true; } if (!(ResourcesPrefabManager.Instance.GetItemPrefab(_itemID) is Ammunition)) { return true; } Ammunition equippedAmmunition = __instance.GetEquippedAmmunition(); bool flag = (Object)(object)equippedAmmunition != (Object)null && ((Item)equippedAmmunition).ItemID == _itemID && ((Item)equippedAmmunition).IsEnchanted; List ownedItems = __instance.GetOwnedItems(_itemID); bool flag2 = false; if (ownedItems != null) { for (int i = 0; i < ownedItems.Count; i++) { Item obj = ownedItems[i]; Equipment val = (Equipment)(object)((obj is Equipment) ? obj : null); if (val != null && ((Item)val).IsEnchanted) { flag2 = true; break; } } } if (!flag && !flag2) { return true; } if ((Object)(object)equippedAmmunition != (Object)null && ((Item)equippedAmmunition).ItemID == _itemID && !((Item)equippedAmmunition).IsEnchanted) { int num = ((Item)equippedAmmunition).MaxStackAmount - ((Item)equippedAmmunition).RemainingAmount; _shotCount -= num; ((Item)equippedAmmunition).RemainingAmount = ((Item)equippedAmmunition).MaxStackAmount; } if (_shotCount > 0 && ownedItems != null) { for (int j = 0; j < ownedItems.Count; j++) { if (_shotCount <= 0) { break; } Item obj2 = ownedItems[j]; Equipment val2 = (Equipment)(object)((obj2 is Equipment) ? obj2 : null); if (val2 == null || !((Item)val2).IsEnchanted) { int num2 = ownedItems[j].MaxStackAmount - ownedItems[j].RemainingAmount; int num3 = Mathf.Min(_shotCount, num2); Item obj3 = ownedItems[j]; obj3.RemainingAmount += num3; _shotCount -= num3; } } } while (_shotCount > 0) { Item val3 = ItemManager.Instance.GenerateItem(_itemID); if ((Object)(object)val3 == (Object)null) { break; } __instance.TakeItem(val3, false); val3.ForceStartInit(); MultipleUsage component = ((Component)val3).GetComponent(); if ((Object)(object)component != (Object)null) { component.RemainingAmount = Mathf.Min(_shotCount, component.MaxStackAmount); } _shotCount -= ((!((Object)(object)component != (Object)null)) ? 1 : component.RemainingAmount); } Plugin.Log.LogMessage((object)"[FLETCH] UnloadAmmunition guarded: unloaded shots landed as plain arrows, fletched stacks untouched."); return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] UnloadAmmunition guard error (vanilla runs): " + ex)); return true; } } } [HarmonyPatch(typeof(Equipment), "ProcessExtraData")] internal static class FeatherNameRestamp { internal static void Postfix(Equipment __instance) { try { if (!Plugin.EnableFeatherFletching.Value || !Plugin.FletchNameSuffix.Value) { return; } Ammunition val = (Ammunition)(object)((__instance is Ammunition) ? __instance : null); if (val != null) { FeatherQuality val2 = FletchState.QualityOf((Item)(object)val); if (val2 != null) { FletchState.StampName(val, val2.Percent); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] name restamp error: " + ex)); } } } [HarmonyPatch(typeof(CharacterInventory), "TakeItem", new Type[] { typeof(Item), typeof(bool) })] internal static class FeatherQuiverMergeGuard { internal static bool Prefix(CharacterInventory __instance, Item takenItem, ref bool _tryToEquip) { try { if (!Plugin.EnableFeatherFletching.Value || !_tryToEquip) { return true; } if (!(takenItem is Ammunition)) { return true; } Ammunition val = (((Object)(object)__instance != (Object)null) ? __instance.GetEquippedAmmunition() : null); if ((Object)(object)val == (Object)null || ((Item)val).ItemID != takenItem.ItemID) { return true; } if (string.Equals(FletchState.SetKeyOf(takenItem), FletchState.SetKeyOf((Item)(object)val), StringComparison.Ordinal)) { return true; } _tryToEquip = false; Plugin.Log.LogMessage((object)($"[FLETCH] kept '{takenItem.Name}' (x{takenItem.RemainingAmount}) out of the " + "equipped quiver stack — differently-fletched stacks never combine.")); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] TakeItem quiver guard error: " + ex)); return true; } } } [HarmonyPatch(typeof(ItemDisplay), "TryEquipUnequip")] internal static class FeatherQuiverSwapGuard { internal static bool Prefix(ItemDisplay __instance, ref bool __result) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Expected O, but got Unknown try { if (!Plugin.EnableFeatherFletching.Value) { return true; } Item val = (((Object)(object)__instance != (Object)null) ? __instance.RefItem : null); if (!(val is Ammunition) || val.IsEquipped) { return true; } if (__instance.IsCharacterInAction) { return true; } if (!val.GetIsDLCOwned()) { return true; } if (!((Equipment)val).IsPlayerTypeValid()) { return true; } Character localCharacter = ((UIElement)__instance).LocalCharacter; Ammunition val2 = (((Object)(object)((localCharacter != null) ? localCharacter.Inventory : null) != (Object)null) ? localCharacter.Inventory.GetEquippedAmmunition() : null); if ((Object)(object)val2 == (Object)null || ((Item)val2).ItemID != val.ItemID) { return true; } if (string.Equals(FletchState.SetKeyOf(val), FletchState.SetKeyOf((Item)(object)val2), StringComparison.Ordinal)) { return true; } if ((Object)(object)((UIElement)__instance).m_characterUI != (Object)null && ((UIElement)__instance).m_characterUI.IsComparePanelDisplayed) { ((UIElement)__instance).m_characterUI.ToggleComparePanel((ItemDetailsDisplay)null); } localCharacter.Inventory.EquipItem((Equipment)val, true); Plugin.Log.LogMessage((object)($"[FLETCH] equip swap: '{val.Name}' (x{val.RemainingAmount}) replaces the " + $"differently-fletched quiver stack '{((Item)val2).Name}' (x{((Item)val2).RemainingAmount}).")); __result = true; return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FLETCH] TryEquipUnequip swap guard error: " + ex)); return true; } } } internal static class FeedAction { internal const int FeedActionId = 910070; } [HarmonyPatch(typeof(ItemDisplayOptionPanel), "GetActiveActions")] internal static class FeedAction_GetActiveActions { internal static void Postfix(ItemDisplayOptionPanel __instance, List __result) { try { if (__result != null && (Plugin.Instance?.ActivePet)?.Sim != null) { Item pendingItem = __instance.m_pendingItem; if (!((Object)(object)pendingItem == (Object)null) && !((Object)(object)((EffectSynchronizer)pendingItem).OwnerCharacter == (Object)null) && !((Object)(object)((EffectSynchronizer)pendingItem).OwnerCharacter != (Object)(object)((UIElement)__instance).LocalCharacter)) { __result.Add(910070); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FEED] GetActiveActions patch error: " + ex)); } } } [HarmonyPatch(typeof(ItemDisplayOptionPanel), "GetActionText")] internal static class FeedAction_GetActionText { internal static bool Prefix(ItemDisplayOptionPanel __instance, int _actionID, ref string __result) { if (_actionID != 910070) { return true; } if (PetBandage.IsBandage(__instance?.m_pendingItem)) { __result = PetBandage.ActionLabel(); return false; } string text = Plugin.Instance?.ActivePet?.SpeciesId; __result = (string.IsNullOrEmpty(text) ? "Feed pet" : ("Feed " + text)); return false; } } [HarmonyPatch(typeof(ItemDisplayOptionPanel), "ActionHasBeenPressed")] internal static class FeedAction_ActionHasBeenPressed { internal static bool Prefix(ItemDisplayOptionPanel __instance, int _actionID) { if (_actionID != 910070) { return true; } ItemDisplay activatedItemDisplay = __instance.m_activatedItemDisplay; Item pendingItem = __instance.m_pendingItem; int beforeAmount = (((Object)(object)pendingItem != (Object)null) ? pendingItem.RemainingAmount : 0); WaterContainer val = (WaterContainer)(object)((pendingItem is WaterContainer) ? pendingItem : null); int beforeUses = (((Object)(object)val != (Object)null) ? val.RemainingUse : (-1)); try { if (PetBandage.IsBandage(pendingItem)) { PetBandage.Apply(pendingItem, ((UIElement)__instance).LocalCharacter); } else { PetFeeder.TryFeed(__instance.m_pendingItem, ((UIElement)__instance).LocalCharacter); } } catch (Exception ex) { Plugin.Log.LogError((object)("[FEED] feed action failed: " + ex)); } RestoreFocusOnRefusal(__instance, activatedItemDisplay, pendingItem, val, beforeAmount, beforeUses); return true; } private static void RestoreFocusOnRefusal(ItemDisplayOptionPanel panel, ItemDisplay src, Item item, WaterContainer skin, int beforeAmount, int beforeUses) { try { if (!((Object)(object)item == (Object)null) && item.RemainingAmount >= beforeAmount && (!((Object)(object)skin != (Object)null) || skin.RemainingUse >= beforeUses) && !((Object)(object)src == (Object)null) && !((Object)(object)((UIElement)panel).m_characterUI == (Object)null)) { ((UIElement)panel).m_characterUI.SetSelectedGameObject(((Component)src).gameObject); Plugin.Log.LogMessage((object)"[FEED] feed refused — restored gamepad focus to the item display."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[FEED] focus restore after refused feed failed: " + ex)); } } } internal static class FletchTable { private static readonly TableLoader _loader = new TableLoader("FeatherFletch.json", "[FLETCH]", "arrow row", "arrow rows", (Func, Dictionary>)FeatherFletching.Parse, (Func, Dictionary, Dictionary>)FeatherFletching.Merge, "replaces per-arrow", (Func, Action, Dictionary>)null, (Func, string>)null, " Items/recipes register once at boot — a NEW row needs a relaunch; existing rows retune."); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[FLETCH]", Validate); internal static Dictionary Table => _axis.Table; internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { TableValidator.CheckItemTable("[FLETCH]", "FeatherFletch.json", "row", ItemRefs(), out var checkedRefs, out var misses); Plugin.Log.LogMessage((object)$"[FLETCH] boot check: {Table.Count} arrow row(s), {checkedRefs} item ref(s), {misses} unresolved."); } private static IEnumerable ItemRefs() { foreach (FletchEntry value in Table.Values) { yield return new ItemRef(value.ArrowId, value.ArrowKey, $"'{value.Key}': arrow ItemID {value.ArrowId}", "'" + value.Key + "': arrow '" + value.ArrowKey + "'"); } } } internal static class FoodCategoryTags { internal static void Init() { SL.OnPacksLoaded += delegate { Dump("[FOODCAT] boot check:"); }; } internal static bool TryGetTag(string canonical, out Tag tag) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) tag = Tag.None; TagSourceSelector val = SelectorFor(canonical); if (val == null || !((UidSelector)(object)val).IsSet) { return false; } tag = val.Tag; return true; } private static TagSourceSelector SelectorFor(string canonical) { TagSourceManager instance = TagSourceManager.Instance; if ((Object)(object)instance == (Object)null || canonical == null) { return null; } return (TagSourceSelector)(canonical switch { "Meat" => instance.m_anyMeatTag, "Fish" => instance.m_anyFishTag, "Vegetable" => instance.m_anyVegetableTag, "Egg" => instance.m_anyEggTag, "Bread" => instance.m_anyBreadTag, "Mushroom" => instance.m_anyMushroomTag, "RationIngredient" => instance.m_anyRationIngredientTag, "Water" => instance.m_waterTag, _ => null, }); } internal static List CategoriesOf(Item item) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) List list = new List(2); if ((Object)(object)item == (Object)null) { return list; } TagSource val = null; bool flag = false; string[] all = FoodCategories.All; foreach (string text in all) { if (!TryGetTag(text, out var tag)) { continue; } if (item.HasTag(tag)) { list.Add(text); continue; } if (!flag) { flag = true; if ((Object)(object)item.m_tagSource == (Object)null) { val = ((Component)item).GetComponent(); if (val != null) { _ = ((TagListSelectorComponent)val).Tags; } } } if ((Object)(object)val != (Object)null && ((TagListSelectorComponent)val).HasMatch(tag)) { list.Add(text); } } return list; } internal static void Dump(string prefix = "[FOODCAT]") { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)TagSourceManager.Instance == (Object)null) { Plugin.Log.LogWarning((object)(prefix + " TagSourceManager not up yet — no categories resolvable.")); return; } int num = 0; List list = new List(); string[] all = FoodCategories.All; foreach (string text in all) { if (TryGetTag(text, out var tag)) { num++; list.Add($"[FOODCAT] {text} -> tag '{tag.TagName}' (uid {((Tag)(ref tag)).UID})"); } else { list.Add("[FOODCAT] " + text + " -> NO TAG (selector unset on this game version) — rows using it can never match."); } } Plugin.Log.LogMessage((object)$"{prefix} {num}/{FoodCategories.All.Length} food-category tags resolved."); foreach (string item in list) { Plugin.Log.LogMessage((object)item); } } } internal static class FoodHexTable { private static readonly TableLoader _loader = new TableLoader("FoodHexes.json", "[FOODHEX]", "food-hex species entry", "food-hex species entries", (Func, Dictionary>)FoodHexes.Parse, (Func, Dictionary, Dictionary>)FoodHexes.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[FOODHEX]", Validate); internal static Dictionary Table => _axis.Table; internal static FoodHexEntry Resolve(string speciesId) { return FoodHexes.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = 0; int misses = 0; int misses2 = 0; HashSet checkedNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (FoodHexEntry value in Table.Values) { foreach (FoodHexMeal meal in value.Meals) { num++; TableValidator.CheckItemKey("[FOODHEX]", "FoodHexes.json", "mapping", meal.ItemId, meal.Key, $"'{value.Species}': meal ItemID {meal.ItemId}", "'" + value.Species + "': meal '" + meal.Key + "'", ref misses); TableValidator.CheckHexName("[FOODHEX]", "FoodHexes.json", "reloadfoodhexes", checkedNames, meal.HexId, "'" + value.Species + "'", ref misses2); } } Plugin.Log.LogMessage((object)($"[FOODHEX] boot check: {Table.Count} species, {num} meal mappings, " + $"{misses} unresolved meal key(s), {misses2} unknown hex name(s).")); } internal static void MealDump(Plugin p) { //IL_030e: Unknown result type (might be due to invalid IL or missing references) //IL_0313: Unknown result type (might be due to invalid IL or missing references) //IL_031f: Unknown result type (might be due to invalid IL or missing references) //IL_032b: Unknown result type (might be due to invalid IL or missing references) //IL_0332: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableFoodHexes.Value) { Plugin.Log.LogMessage((object)"[FOODHEX] EnableFoodHexes=false — system off (recording AND application)."); return; } Pet activePet = p.ActivePet; if (activePet?.State == null) { Plugin.Log.LogMessage((object)"[FOODHEX] no active pet."); return; } FoodHexEntry val = Resolve(activePet.SpeciesId); Plugin.Log.LogMessage((object)string.Format("[FOODHEX] species='{0}' tableSpecies={1} entry={2}", activePet.SpeciesId, Table.Count, (val != null) ? "yes" : "NO — no food-hex signature")); if (val == null) { return; } int num = FoodHexes.ClampWindow(val.Window ?? Plugin.HexMealWindow.Value); float num2 = (float)(val.BuildUpPercent ?? ((double)Plugin.HexBuildupPercent.Value)); Plugin.Log.LogMessage((object)string.Format("[FOODHEX] window={0} ({1}), perMeal={2:F0}% ({3}), historyCap={4}", num, val.Window.HasValue ? "table" : "config", num2, val.BuildUpPercent.HasValue ? "table" : "config", 20)); foreach (FoodHexMeal meal in val.Meals) { int num3; if (!meal.ItemId.HasValue) { num3 = 1; } else { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; num3 = (((Object)(object)((instance != null) ? instance.GetItemPrefab(meal.ItemId.Value) : null) != (Object)null) ? 1 : 0); } bool flag = (byte)num3 != 0; ResourcesPrefabManager instance2 = ResourcesPrefabManager.Instance; bool flag2 = (Object)(object)((instance2 != null) ? instance2.GetStatusEffectPrefab(meal.HexId) : null) != (Object)null; Plugin.Log.LogMessage((object)("[FOODHEX] maps: '" + meal.Key + "'" + (meal.ItemId.HasValue ? string.Format(" (ItemID {0}{1})", meal.ItemId.Value, flag ? "" : " — UNKNOWN ITEM") : " (by display name)") + " → '" + meal.HexId + "'" + (flag2 ? "" : " — NOT a loadable status (statusdump)"))); } Plugin.Log.LogMessage((object)("[FOODHEX] history (oldest→newest): [" + ((activePet.State.RecentMeals.Count > 0) ? string.Join(", ", activePet.State.RecentMeals.ToArray()) : "empty") + "]")); List list = FoodHexes.ComputeBuildups(val, (IReadOnlyList)activePet.State.RecentMeals, Plugin.HexMealWindow.Value, Plugin.HexBuildupPercent.Value); if (list.Count == 0) { Plugin.Log.LogMessage((object)"[FOODHEX] next Hunt-as-One hit: no build-up (no mapped meals in the window)."); return; } foreach (HexBuildUp item in list) { Plugin.Log.LogMessage((object)$"[FOODHEX] next Hunt-as-One hit: +{item.Percent:F0}% '{item.HexId}' ({item.MealCount} meal(s))"); } } internal static string FoodHexSummary(Plugin p) { //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.State == null) { return "no active pet."; } if (!Plugin.EnableFoodHexes.Value) { return "food hexes OFF."; } FoodHexEntry val = Resolve(activePet.SpeciesId); if (val == null) { return "'" + activePet.SpeciesId + "' has no FoodHexes.json entry — no food-hex signature."; } List list = FoodHexes.ComputeBuildups(val, (IReadOnlyList)activePet.State.RecentMeals, Plugin.HexMealWindow.Value, Plugin.HexBuildupPercent.Value); string text = ((activePet.State.RecentMeals.Count > 0) ? string.Join(", ", activePet.State.RecentMeals.ToArray()) : "none"); if (list.Count == 0) { return "meals [" + text + "] → no mapped meals in the window; the Hunt-as-One hit builds up nothing."; } List list2 = new List(); foreach (HexBuildUp item in list) { list2.Add($"{item.Percent:F0}% {item.HexId}"); } return string.Format("meals [{0}] → Hunt-as-One builds up {1} (window {2}).", text, string.Join(" + ", list2.ToArray()), FoodHexes.ClampWindow(val.Window ?? Plugin.HexMealWindow.Value)); } } internal static class ForTheKillSkill { internal static string LastCast = "none yet this session"; private static bool _favorGrantedThisCast; internal static void Cast(Character player) { if ((Object)(object)player == (Object)null) { return; } if (!Plugin.EnableForTheKill.Value) { Plugin.Log.LogMessage((object)"[FTK] For the Kill cast — [ForTheKill] EnableForTheKill is off, nothing happens."); return; } Pet pet = Plugin.Instance?.ActivePet; if (pet == null) { Notify.Player(player, "You have no companion to hunt with — the moment is wasted."); Plugin.Log.LogMessage((object)"[FTK] cast with no companion — nothing happens; cooldown burns (Cobalt's V13 ruling)."); LastCast = "burned: no companion"; return; } PetSpecialAttack petSpecialAttack = (((Object)(object)((Companion)pet).Body != (Object)null) ? ((Component)((Companion)pet).Body).GetComponent() : null); if ((Object)(object)petSpecialAttack == (Object)null) { Notify.Player(player, "Your companion cannot strike right now — the moment is wasted."); Plugin.Log.LogMessage((object)"[FTK] cast with no strike-capable body (re-forming, or [Combat] EnableSpecialAttack off) — nothing happens; cooldown burns."); LastCast = "burned: no strike-capable body"; return; } if (((Companion)pet).Stance.Passive) { PetCommands.EngagePet(Plugin.Instance); } _favorGrantedThisCast = false; int num = Synergy.ClampStacks(HuntSynergy.EffectiveStacks(player, pet), Plugin.SynergyMaxStacks.Value); float mult = (float)Synergy.FtkMult((double)Plugin.FtkBaseDamageMult.Value, (double)Plugin.FtkDamagePerStackPercent.Value, num); PetSpecialAttack.FireOverrides ov = new PetSpecialAttack.FireOverrides { BypassCooldown = true, ArmCooldown = false, DamageMultExtra = mult, OnHitExtra = delegate(SpecialHitContext ctx) { OnPetExecuteHit(ctx); }, SuppressSynergy = true, BraceAsMelee = true, Tag = "[FTK]" }; PetSpecialAttack.SpecialFireReport specialFireReport = petSpecialAttack.Fire(delegate(Character target) { HuntAsOnePlayer.SyncedStrike(player, target, mult, report: false); MaybeGrantKillFavor(pet.SpeciesId, target); }, ov); Plugin.Log.LogMessage((object)$"[FTK] cast -> {specialFireReport.Summary} (stacks={num}, mult=x{mult:F2})."); if (!specialFireReport.Started) { Notify.Player(player, "The strike never came together — the moment is wasted."); Plugin.Log.LogMessage((object)"[FTK] strike never started — stacks kept, cooldown burns."); LastCast = "burned (no strike, stacks kept): " + specialFireReport.Summary; return; } if (!specialFireReport.HadTarget) { Notify.Player(player, "Nothing to execute — the strike whiffs, your fervor holds."); Plugin.Log.LogMessage((object)"[FTK] whiffed at nothing — stacks kept, cooldown burns."); LastCast = "burned (whiff at nothing, stacks kept): " + specialFireReport.Summary; return; } if (num > 0) { SynergyStatus.Remove(player); Plugin.Instance?.ResyncBuffs(); } Notify.Player(player, (num > 0) ? $"For the kill! ({num} synergy consumed — x{mult:0.0#})" : "For the kill!"); LastCast = $"burned: {num} stack(s) consumed, x{mult:F2}, {specialFireReport.Summary}"; HuntAsOnePlayer.OnSpecialAttackFired(player, reportSynergy: false, mult); } private static void OnPetExecuteHit(SpecialHitContext ctx) { ExecuteContext ctx2 = new ExecuteContext { Pet = ctx.Pet, Target = ctx.Target, Dealer = ctx.Dealer }; if (SpeciesRegistry.For(ctx.Pet?.SpeciesId).OnExecuteHit(in ctx2)) { MaybeGrantKillFavor(ctx.Pet?.SpeciesId, ctx.Target); } } internal static void MaybeGrantKillFavor(string species, Character target) { //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)target == (Object)null || target.Alive || _favorGrantedThisCast) { return; } if (!Plugin.EnableKillFavor.Value) { Plugin.Log.LogMessage((object)"[KILLFAVOR] kill landed but [ForTheKill] EnableKillFavor=false — no favor."); return; } ForTheKillEntry val = ForTheKillTable.Resolve(species); if (val?.KillBuff == null) { Plugin.Log.LogMessage((object)("[KILLFAVOR] kill landed but '" + species + "' has no killBuff in ForTheKill.json — no favor.")); return; } if (!KillFavorStatus.Registered) { Plugin.Log.LogWarning((object)"[KILLFAVOR] kill favor earned but 'BW_KillFavor' never registered — no buff (see boot warnings)."); return; } Plugin instance = Plugin.Instance; int? obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; if (activePet == null) { obj = null; } else { PetSimulation sim = activePet.Sim; obj = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue); } } int num = obj ?? (-1); LoyaltyTier val2 = (LoyaltyTier)((num >= 0) ? ((int)Loyalty.Tier(num)) : 0); float num2 = KillFavor.Amount(val.KillBuff, val2); if (num2 <= 0f) { Plugin.Log.LogMessage((object)$"[KILLFAVOR] kill landed but loyalty tier {val2} yields no favor (amount=0)."); return; } _favorGrantedThisCast = true; Character localPlayerCharacter = Plugin.LocalPlayerCharacter; KillFavorBuff.Grant(localPlayerCharacter, val.KillBuff.Stat, num2); KillFavorStatus.AddOrRefresh(localPlayerCharacter); Notify.Player(localPlayerCharacter, "A killing favor: " + KillFavorBuff.Describe(val.KillBuff.Stat, num2) + " for " + $"{Plugin.KillFavorDurationSeconds.Value / 60f:0.#}m."); } internal static void SyncCooldown(Character player) { int num = SkillCooldowns.Sync(87005, player, Plugin.FtkCooldownSeconds.Value); if (num > 0) { Plugin.Log.LogMessage((object)($"[FTK] cooldown stamped to {Mathf.Max(0f, Plugin.FtkCooldownSeconds.Value):0}s " + $"on {num} object(s) ([ForTheKill] CooldownSeconds).")); } } internal static void Dump(Plugin plugin) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item obj = ((instance != null) ? instance.GetItemPrefab(87005) : null); Skill val = (Skill)(object)((obj is Skill) ? obj : null); Plugin.Log.LogMessage((object)"[FTKDUMP] cooldown policy: EVERY cast burns (V13 ruling); stacks consumed only on a started strike."); Plugin.Log.LogMessage((object)($"[FTKDUMP] EnableForTheKill={Plugin.EnableForTheKill.Value}, " + $"CooldownSeconds={Plugin.FtkCooldownSeconds.Value:0} " + "(live prefab Cooldown=" + (((Object)(object)val != (Object)null) ? val.Cooldown.ToString("0") : "n/a") + "), " + $"BaseDamageMult={Plugin.FtkBaseDamageMult.Value:0.##}, " + $"DamagePerStackPercent={Plugin.FtkDamagePerStackPercent.Value:0.##}.")); Plugin.Log.LogMessage((object)$"[FTKDUMP] {ForTheKillTable.Table.Count} species entr(ies):"); foreach (ForTheKillEntry value in ForTheKillTable.Table.Values) { ResourcesPrefabManager instance2 = ResourcesPrefabManager.Instance; bool flag = (Object)(object)((instance2 != null) ? instance2.GetStatusEffectPrefab(value.StatusName) : null) != (Object)null; Plugin.Log.LogMessage((object)("[FTKDUMP] '" + value.Species + "': '" + value.StatusName + "' (" + (value.BuildupPercent.HasValue ? $"build-up {value.BuildupPercent.Value:F0}%" : "direct apply") + ", " + (flag ? "resolves" : "NOT a loadable status") + ").")); } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Pet activePet = plugin.ActivePet; int num = Synergy.ClampStacks(HuntSynergy.EffectiveStacks(localPlayerCharacter, activePet), Plugin.SynergyMaxStacks.Value); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i <= Plugin.SynergyMaxStacks.Value; i++) { stringBuilder.Append($"{i}→x{Synergy.FtkMult((double)Plugin.FtkBaseDamageMult.Value, (double)Plugin.FtkDamagePerStackPercent.Value, i):0.0#} "); } Plugin.Log.LogMessage((object)($"[FTKDUMP] current stacks={num} → a cast now uses " + $"x{Synergy.FtkMult((double)Plugin.FtkBaseDamageMult.Value, (double)Plugin.FtkDamagePerStackPercent.Value, num):F2}; " + $"full table: {stringBuilder}")); Plugin.Log.LogMessage((object)("[FTKDUMP] active species: " + ((activePet != null) ? ("'" + activePet.SpeciesId + "' → " + ((ForTheKillTable.Resolve(activePet.SpeciesId) != null) ? "has a debuff entry" : "no entry (strike only)")) : "no pet") + "; last cast: " + LastCast + ".")); } } internal static class ForTheKillTable { private const string FileName = "ForTheKill.json"; private const string Tag = "[FTK]"; private static readonly TableLoader _loader = new TableLoader("ForTheKill.json", "[FTK]", "species execute debuff", "species execute debuffs", (Func, Dictionary>)ForTheKill.Parse, (Func, Dictionary, Dictionary>)ForTheKill.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[FTK]", Validate); internal static Dictionary Table => _axis.Table; internal static ForTheKillEntry Resolve(string speciesId) { return ForTheKill.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { SpecialAttackDef val = default(SpecialAttackDef); foreach (ForTheKillEntry value in Table.Values) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; bool flag = (Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(value.StatusName) : null) != (Object)null; if (!flag) { Plugin.Log.LogWarning((object)("[FTK] '" + value.Species + "': debuff '" + value.StatusName + "' is not a loadable StatusEffect prefab name — the execute will land damage but NO debuff. Run `statusdump` for the real names, fix the table (config override) + `reloadftk`.")); } if (SpecialAttackTable.TryGet(SpecialAttacks.Table, value.Species, ref val) && !string.IsNullOrEmpty(val.StatusEffectId) && string.Equals(val.StatusEffectId, value.StatusName, StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogWarning((object)("[FTK] '" + value.Species + "': the execute debuff equals the species' everyday signature-attack status ('" + value.StatusName + "') — a 3-minute payoff that Hunt-as-One already applies every few seconds is probably an authoring mistake.")); } Plugin.Log.LogMessage((object)("[FTK] '" + value.Species + "': '" + value.StatusName + "' (" + (value.BuildupPercent.HasValue ? $"build-up {value.BuildupPercent.Value:F0}%" : "direct apply") + ", " + (flag ? "armed" : "INERT") + ").")); } Plugin.Log.LogMessage((object)(string.Format("{0} boot check: {1} species execute debuff(s), ", "[FTK]", Table.Count) + $"EnableForTheKill={Plugin.EnableForTheKill.Value}.")); } } internal static class GiftTable { private static readonly TableLoader _loader = new TableLoader("PetGifts.json", "[GIFT]", "species gift entry", "species gift entries", (Func, Dictionary>)PetGifts.Parse, (Func, Dictionary, Dictionary>)PetGifts.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[GIFT]", Validate); internal static Dictionary Table => _axis.Table; internal static PetGiftEntry Resolve(string speciesId) { return PetGifts.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = 0; int misses = 0; foreach (PetGiftEntry value in Table.Values) { num++; TableValidator.CheckItemKey("[GIFT]", "PetGifts.json", "entry", value.Default.ItemId, value.Default.Key, $"'{value.Species}': default ItemID {value.Default.ItemId}", "'" + value.Species + "': default '" + value.Default.Key + "'", ref misses); foreach (GiftDrop drop in value.Drops) { num++; TableValidator.CheckItemKey("[GIFT]", "PetGifts.json", "entry", drop.ItemId, drop.Key, $"'{value.Species}': drop ItemID {drop.ItemId}", "'" + value.Species + "': drop '" + drop.Key + "'", ref misses); } foreach (GiftAuditFinding item in PetGifts.Audit(value)) { Plugin.Log.LogWarning((object)("[GIFT] '" + value.Species + "': " + item.Message)); } GiftMix val = PetGifts.EffectiveMix(value, 0); GiftMix val2 = PetGifts.EffectiveMix(value, 100); Plugin.Log.LogMessage((object)($"[GIFT] '{value.Species}': {value.Drops.Count} explicit drop(s); " + $"default '{value.Default.Key}' {val.DefaultChance:0.###}->{val2.DefaultChance:0.###}, " + $"nothing {val.NothingChance:0.###}->{val2.NothingChance:0.###} (loyalty 0->100).")); } Plugin.Log.LogMessage((object)$"[GIFT] boot check: {Table.Count} species, {num} item ref(s), {misses} unresolved."); } } internal static class GrowthTable { private static readonly TableLoader> _loader = new TableLoader>("SpeciesGrowth.txt", "[GROWTH]", "growth species entry", "growth species entries", (Func, Dictionary>>)PetGrowth.Parse, (Func>, Dictionary>, Dictionary>>)PetGrowth.Merge, "replaces per-species", (Func>, Action, Dictionary>>)null, (Func>, string>)null, ""); private static readonly DataAxis>> _axis = BwAxis.Table(_loader, "[GROWTH]", Validate); internal static Dictionary> Table => _axis.Table; internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = TableValidator.CheckSpeciesKeys("[GROWTH]", "SpeciesGrowth.txt", Table.Keys); Plugin.Log.LogMessage((object)string.Format("[GROWTH] boot check: {0} growth species entr{1}, {2} unknown species key(s).", Table.Count, (Table.Count == 1) ? "y" : "ies", num)); } } internal static class GuestTame { internal const string DespawnVerb = "bw.tame.despawn"; internal static void Init() { NetBus.Register("bw.tame.despawn", (Action)OnDespawnRequest); } internal static bool TameAsGuest(PetLifecycle lifecycle, Character src, Character actor = null) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) string text = (((Object)(object)src != (Object)null) ? UID.op_Implicit(src.UID) : null); string text2 = (((Object)(object)src != (Object)null) ? src.Name : "?"); Character val = actor ?? Plugin.LocalPlayer; if (!lifecycle.TameSpecificNonConsume(src, val)) { return false; } if (!NetBus.SendToMaster("bw.tame.despawn", ((Object)(object)val != (Object)null) ? UID.op_Implicit(val.UID) : "", text)) { Plugin.Log.LogWarning((object)("[TAME] guest tame landed but the despawn request for '" + text2 + "' (" + text + ") could not be sent — the wild original will remain until the master's world removes it.")); } else { Plugin.Log.LogMessage((object)("[TAME] guest tame landed — despawn of the wild '" + text2 + "' (" + text + ") requested from the master.")); } return true; } private static void OnDespawnRequest(NetMessage msg) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Invalid comparison between Unknown and I4 //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Invalid comparison between Unknown and I4 //IL_01d2: Unknown result type (might be due to invalid IL or missing references) if (PhotonNetwork.isNonMasterClientInRoom) { return; } string payload = msg.Payload; if (string.IsNullOrEmpty(payload)) { return; } Character val = (((Object)(object)CharacterManager.Instance != (Object)null) ? CharacterManager.Instance.GetCharacter(payload) : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)("[TAME] guest despawn request (owner '" + msg.OwnerUid + "'): creature '" + payload + "' not found — already gone, nothing to do.")); return; } if (!val.IsAI || (int)val.Faction == 1 || (int)val.Faction == 0 || (int)val.Faction == 7 || (Object)(object)val.OwnerPlayerSys != (Object)null || CompanionAnchor.IsAnchor(val) || AnchorSentinel.IsAnchorUid(payload)) { NetBus.CountDrop("bw.tame.despawn", "refused-validation"); Plugin.Log.LogWarning((object)("[TAME] guest despawn request for '" + val.Name + "' (" + payload + ") REFUSED — not a hostile wild AI creature " + $"(IsAI={val.IsAI}, faction={val.Faction}, playerOwned={(Object)(object)val.OwnerPlayerSys != (Object)null}, anchor={CompanionAnchor.IsAnchor(val)}).")); return; } if (!val.Alive) { Plugin.Log.LogMessage((object)("[TAME] guest despawn request for '" + val.Name + "' (" + payload + "): already dead — corpse left for loot.")); return; } try { val.StartDestroy(); Plugin.Log.LogMessage((object)("[TAME] wild '" + val.Name + "' (" + payload + ") despawned on a guest's tame (owner '" + msg.OwnerUid + "').")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TAME] guest-tame despawn of '" + val.Name + "' threw: " + ex.Message)); } } } internal static class HuntAsOneCooldown { private static bool _reconcilePending; internal static void Init() { SL.OnPacksLoaded += BootCheck; } private static void BootCheck() { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item obj = ((instance != null) ? instance.GetItemPrefab(87001) : null); Skill val = (Skill)(object)((obj is Skill) ? obj : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[HAO] boot check: Hunt as One's prefab didn't resolve — cooldown sync is inert."); return; } Plugin.Log.LogMessage((object)($"[HAO] boot check: skill Cooldown={val.Cooldown:0.##}s Lifespan={val.Lifespan:0.##}s " + $"StaminaCost={val.StaminaCost:0.#}" + ((val.Lifespan > 0f) ? " — WARNING: Lifespan>0 DEFERS StartCooldown and holds InCooldown() true via m_inProgress; the Bug-30 adopt assumes an immediate cooldown." : " (Lifespan 0 = the cooldown starts at cast, as the Bug-30 fix assumes)."))); foreach (KeyValuePair item in SpecialAttacks.Table) { Plugin.Log.LogMessage((object)($"[HAO] boot check: live table row '{item.Key}' cooldown={item.Value.CooldownSeconds:0.#}s " + "(this is what the skill advertises for that species — compare against the shipped table if a config override is in play).")); } } internal static float WantSeconds() { float num = 0f; string text = Plugin.Instance?.ActivePet?.SpeciesId; SpecialAttackDef val = default(SpecialAttackDef); bool flag = !string.IsNullOrEmpty(text) && SpecialAttackTable.TryGet(SpecialAttacks.Table, text, ref val) && (num = val.CooldownSeconds) >= 0f; return HuntCooldown.WantSkillCooldown(Plugin.SyncSkillCooldownToPet.Value, flag, num, Plugin.BaseSkillCooldownSeconds.Value); } internal static void SyncCooldown(Character player) { float num = WantSeconds(); int num2 = SkillCooldowns.Sync(87001, player, num); if (num2 > 0) { Plugin.Log.LogMessage((object)($"[HAO] skill cooldown stamped to {num:0.#}s on {num2} object(s) " + "(species '" + (Plugin.Instance?.ActivePet?.SpeciesId ?? "none") + "', " + $"sync={Plugin.SyncSkillCooldownToPet.Value}).")); } GuardAgainstAReadyButtonOverAPetOnCooldown(player); } private static void GuardAgainstAReadyButtonOverAPetOnCooldown(Character player) { //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown if (!Plugin.SyncSkillCooldownToPet.Value || _reconcilePending) { return; } float petLeft = (float)(Plugin.Instance?.ActivePet?.State?.SpecialCooldownSecondsLeft).GetValueOrDefault(); if (petLeft <= 0f) { return; } SkillInstanceSync.Sync(87001, player, (Apply)delegate(Item item) { Skill val = (Skill)(object)((item is Skill) ? item : null); if (val == null || !((Item)val).IsChildToCharacter) { return false; } float num = val.RemainingCooldownSeconds(); if (num >= petLeft || !HuntCooldown.Diverged(num, petLeft, 0.25f)) { return false; } val.SetRemainingCooldown(petLeft); Plugin.Log.LogMessage((object)($"[HAO] the hotbar read {num:0.#}s while the pet still had {petLeft:0.#}s " + "left — raised the skill to match (the button must never be live before the pet is).")); return true; }); } internal static void ReconcileAfterFire(Character player, PetSpecialAttack special, bool fromSkill) { if (!((Object)(object)Plugin.Instance == (Object)null) && !((Object)(object)special == (Object)null) && Plugin.SyncSkillCooldownToPet.Value) { _reconcilePending = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(ReconcileNextFrame(player, special, fromSkill)); } } private static IEnumerator ReconcileNextFrame(Character player, PetSpecialAttack special, bool fromSkill) { yield return null; _reconcilePending = false; if (!((Object)(object)special == (Object)null)) { if (fromSkill) { CommittedCooldown(player, out var full, out var remaining); float num = HuntCooldown.AdoptedCooldownSeconds(remaining, special.TableCooldownSeconds); float num2 = HuntCooldown.AdoptedCooldownSeconds(full, special.TableCooldownSeconds); special.OverrideCooldown(num, num2); Plugin.Log.LogMessage((object)($"[HAO] pet-special cooldown adopted the skill's committed {num2:0.#}s " + $"({num:0.#}s left; species table says {special.TableCooldownSeconds:0.#}s" + ((full > 0f && !Mathf.Approximately(full, special.TableCooldownSeconds)) ? " — the player's cooldown reduction shortened BOTH clocks" : "") + ").")); } else { StampSkillRemaining(player, special.CooldownRemaining); Plugin.Log.LogMessage((object)($"[HAO] non-skill fire — stamped {special.CooldownRemaining:0.#}s onto the " + "Hunt-as-One skill so the hotbar shows the real cooldown.")); } } } private static void CommittedCooldown(Character player, out float full, out float remaining) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown float f = 0f; float r = 0f; SkillInstanceSync.Sync(87001, player, (Apply)delegate(Item item) { Skill val = (Skill)(object)((item is Skill) ? item : null); if (val != null && ((Item)val).IsChildToCharacter && val.InCooldown() && val.RealCooldown > f) { f = val.RealCooldown; r = val.RemainingCooldownSeconds(); } return false; }); full = f; remaining = r; } internal static void StampSkillRemaining(Character player, float seconds) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown seconds = Mathf.Max(0f, seconds); SkillInstanceSync.Sync(87001, player, (Apply)delegate(Item item) { Skill val = (Skill)(object)((item is Skill) ? item : null); if (val == null || !((Item)val).IsChildToCharacter) { return false; } if (!HuntCooldown.Diverged(val.RemainingCooldownSeconds(), seconds, 0.25f)) { return false; } val.SetRemainingCooldown(seconds); return true; }); } } internal static class SkillCooldownFields { internal static float RemainingCooldownSeconds(this Skill skill) { return SkillClock.Remaining(skill); } internal static void SetRemainingCooldown(this Skill skill, float seconds) { SkillClock.SetRemaining(skill, seconds); } } internal static class HuntAsOnePlayer { internal static void OnSpecialAttackFired(Character player, bool reportSynergy = true, float bonusMult = 1f) { if ((Object)(object)player == (Object)null) { return; } Weapon currentWeapon = player.CurrentWeapon; ProjectileWeapon val = (ProjectileWeapon)(object)((currentWeapon is ProjectileWeapon) ? currentWeapon : null); if (val != null) { if ((Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(DelayedForceShoot(val, Plugin.HuntAsOneBowShotDelay.Value, reportSynergy, bonusMult)); } else { FireBow(val, reportSynergy, bonusMult); } } } private static IEnumerator DelayedForceShoot(ProjectileWeapon bow, float delay, bool reportSynergy, float bonusMult) { yield return (object)new WaitForSeconds(delay); FireBow(bow, reportSynergy, bonusMult); } private static void FireBow(ProjectileWeapon bow, bool reportSynergy, float bonusMult) { if ((Object)(object)bow == (Object)null) { return; } if (((Weapon)bow).IsEmpty) { ((Weapon)bow).InstantLoadWeapon(); } if (((Weapon)bow).IsEmpty) { Plugin.Log.LogMessage((object)"[HUNTASONE] bow still empty after InstantLoadWeapon (no ammo equipped?) -- ForceShoot skipped, no arrow fired."); if (Plugin.HonestHits.Value && reportSynergy) { HuntSynergy.ReportPlayer((StrikeOutcome)6); } return; } if (Plugin.HonestHits.Value) { HuntSynergy.ArmArrowCapture(((EffectSynchronizer)bow).OwnerCharacter ?? Plugin.LocalPlayerCharacter, reportSynergy, bonusMult); } bow.ForceShoot(); Plugin.Log.LogMessage((object)"[HUNTASONE] bow ForceShoot fired (real arrow; the armed capture tags it when it spawns, and the flat bonus lands AT the arrow's impact)."); } internal static void SyncedStrike(Character player, Character target, float damageMult = 1f, bool report = true) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: 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_0052: 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_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Invalid comparison between Unknown and I4 //IL_00fb: 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_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Invalid comparison between Unknown and I4 //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01f8: Unknown result type (might be due to invalid IL or missing references) //IL_020e: Unknown result type (might be due to invalid IL or missing references) //IL_0219: Unknown result type (might be due to invalid IL or missing references) //IL_0238: Unknown result type (might be due to invalid IL or missing references) //IL_0243: Unknown result type (might be due to invalid IL or missing references) //IL_024e: Unknown result type (might be due to invalid IL or missing references) //IL_0253: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || (Object)(object)target == (Object)null) { return; } float num = Plugin.HuntAsOneBonusDamage.Value * ((damageMult > 0f) ? damageMult : 1f); Vector3 val = ((Component)target).transform.position - ((Component)player).transform.position; Vector3 normalized = ((Vector3)(ref val)).normalized; if (!Plugin.HonestHits.Value) { if (target.Alive && !(num <= 0f)) { target.ReceiveHit((Weapon)null, num, normalized, target.CenterPosition, 45f, 1f, player, 1.5f); Plugin.Log.LogMessage((object)$"[HUNTASONE] player bonus strike hit '{target.Name}' for {num:F0} (HonestHits off — guaranteed)."); if (report) { HuntSynergy.ReportPlayer((StrikeOutcome)1); } } } else { if (player.CurrentWeapon is ProjectileWeapon) { return; } StrikeOutcome val2 = HuntSynergy.JudgeStrike(((Component)player).transform.position, ((Component)player).transform.forward, target, Plugin.PlayerMeleeRangeMeters.Value, Plugin.PlayerMeleeConeDegrees.Value); if (report) { HuntSynergy.ReportPlayer(val2); } if ((int)val2 != 1) { if ((int)val2 == 2) { float angleFromTargetForward = Vector3.Angle(((Component)target).transform.forward, ((Component)player).transform.position - ((Component)target).transform.position); HuntSynergy.PlayBlock(target, (MonoBehaviour)(((object)player.CurrentWeapon) ?? ((object)player)), num, normalized, angleFromTargetForward, player); Plugin.Log.LogMessage((object)("[HUNTASONE] player strike BLOCKED by '" + target.Name + "' — no damage.")); } else { Plugin.Log.LogMessage((object)($"[HUNTASONE] player strike on '{target.Name}' failed to connect ({val2}) " + $"— dist {Vector3.Distance(((Component)player).transform.position, ((Component)target).transform.position):0.0}m, " + $"facing-angle {Vector3.Angle(((Component)player).transform.forward, ((Component)target).transform.position - ((Component)player).transform.position):0}°.")); } } else { if (num > 0f) { target.ReceiveHit((Weapon)null, num, normalized, target.CenterPosition, 45f, 1f, player, 1.5f); } Plugin.Log.LogMessage((object)($"[HUNTASONE] player strike LANDED on '{target.Name}' for {num:F0} " + $"(dist {Vector3.Distance(((Component)player).transform.position, ((Component)target).transform.position):0.0}m).")); } } } } internal static class HuntSynergy { private struct TaggedArrow { public Projectile P; public bool Report; public float BonusMult; public float Deadline; } private static SynergyAttempt _attempt; private static Character _target; private static string _lastClose = "none yet"; private static readonly List _arrows = new List(); private static Character _armPlayer; private static bool _armReport; private static float _armBonus; private static float _armDeadline; private static bool _seamProven; private static bool _activateSeamProven; private static bool _hitSeamProven; internal static bool AttemptOpen { get { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if (_attempt != null) { return (int)_attempt.Status == 0; } return false; } } internal static void Open(Character target) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown if (Plugin.EnableSynergy.Value && SynergyStatus.Registered) { if (AttemptOpen) { _lastClose = "superseded: " + _attempt.Verdict; Plugin.Log.LogMessage((object)("[SYNERGY] previous attempt superseded by a new cast (" + _attempt.Verdict + ").")); ClearAttemptArrows(); } _attempt = new SynergyAttempt((double)Time.time, (double)Plugin.SynergyWindowSeconds.Value); _target = target; Plugin.Log.LogMessage((object)("[SYNERGY] attempt open on '" + (((Object)(object)target != (Object)null) ? target.Name : "?") + "' " + $"(window {Plugin.SynergyWindowSeconds.Value:0.#}s).")); } } internal static void ReportPet(StrikeOutcome outcome) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Report((StrikeHalf)0, outcome); } internal static void ReportPlayer(StrikeOutcome outcome) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Report((StrikeHalf)1, outcome); } private static void Report(StrikeHalf half, StrikeOutcome outcome) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) if (AttemptOpen) { AttemptStatus val = _attempt.Report(half, outcome); if ((int)val != 0) { Close(val); } } } internal static void Tick(float now) { //IL_0079: 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) if (_arrows.Count > 0) { PruneArrows(now); } if ((Object)(object)_armPlayer != (Object)null && now > _armDeadline) { string arg = (_seamProven ? "arrows DO reach the postfix — suspect the owner/window scope" : "the postfix has NEVER run — suspect the patch target itself"); Plugin.Log.LogWarning((object)("[SYNERGY] arrow capture EXPIRED with nothing tagged — the bow half reads " + $"TimedOut and no stack can be granted (V7). seam-ever-ran={_seamProven} ({arg}).")); DisarmArrowCapture(); } if (AttemptOpen && (int)_attempt.Tick((double)now) != 0) { Close(_attempt.Status); } } private static void Close(AttemptStatus st) { //IL_0005: 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_0021: Invalid comparison between Unknown and I4 _lastClose = $"{st}: {_attempt.Verdict}"; if ((int)st == 1) { Grant(); } else { Plugin.Log.LogMessage((object)("[SYNERGY] no stack — " + _attempt.Verdict + ".")); } ClearAttemptArrows(); _target = null; _attempt = null; } private static void Grant() { Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if (!((Object)(object)localPlayerCharacter == (Object)null)) { int num = SynergyStatus.StackCount(localPlayerCharacter); int value = Plugin.SynergyMaxStacks.Value; if (num > value) { SynergyStatus.SetStacks(localPlayerCharacter, Math.Max(0, value)); int num2 = SynergyStatus.StackCount(localPlayerCharacter); Plugin.Log.LogMessage((object)$"[SYNERGY] trimmed {num} stack(s) to the lowered cap ({value}) -> {num2}."); num = num2; } if (num < value) { SynergyStatus.AddStack(localPlayerCharacter); } SynergyStatus.RefreshAll(localPlayerCharacter, Plugin.SynergyDurationSeconds.Value); int num3 = SynergyStatus.StackCount(localPlayerCharacter); Notify.Player(localPlayerCharacter, (num3 > num) ? $"Synergy x{num3}!" : $"Synergy refreshed (x{num3})."); Plugin.Log.LogMessage((object)($"[SYNERGY] stack granted ({num3}/{value})" + ((num3 == num) ? " — at cap, timers refreshed." : "."))); Plugin.Instance?.ResyncBuffs(); } } internal static StrikeOutcome JudgeStrike(Vector3 attackerPos, Vector3 attackerForward, Character target, float maxRange, float coneDegrees) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) return StrikeJudge.Judge(attackerPos, attackerForward, target, maxRange, coneDegrees); } internal static void PlayBlock(Character target, MonoBehaviour source, float damage, Vector3 dir, float angleFromTargetForward, Character dealer) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) StrikeJudge.PlayBlock(target, source, damage, dir, angleFromTargetForward, dealer, (Action)delegate(string line) { Plugin.Log.LogMessage((object)("[SYNERGY] " + line)); }); } internal static void ArmArrowCapture(Character player, bool report, float bonusMult) { _armPlayer = player; _armReport = report; _armBonus = bonusMult; _armDeadline = Time.time + Mathf.Max(2f, Plugin.SynergyWindowSeconds.Value); Plugin.Log.LogMessage((object)("[SYNERGY] arrow capture armed for '" + (((Object)(object)player != (Object)null) ? player.Name : "?") + "' " + string.Format("({0}, ≤{1:0.#}s).", report ? "HAO" : "FTK", _armDeadline - Time.time))); } private static void DisarmArrowCapture() { _armPlayer = null; } private static bool IsArmedOwner(Character c) { //IL_0023: 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) if ((Object)(object)c == (Object)null || (Object)(object)_armPlayer == (Object)null) { return false; } if (c == _armPlayer) { return true; } string text = UID.op_Implicit(c.UID); string text2 = UID.op_Implicit(_armPlayer.UID); if (!string.IsNullOrEmpty(text)) { return text == text2; } return false; } internal static void LogArrowCapturePatchState(Harmony harmony) { MethodInfo methodInfo = AccessTools.Method(typeof(ShootProjectile), "PerformShootProjectile", (Type[])null, (Type[])null); if (methodInfo == null) { Plugin.Log.LogWarning((object)"[SYNERGY] arrow-capture patch: ShootProjectile.PerformShootProjectile NOT FOUND — the seam does not exist under that name, so bow synergy cannot work (V7)."); return; } int valueOrDefault = (Harmony.GetPatchInfo((MethodBase)methodInfo)?.Postfixes?.Count).GetValueOrDefault(); if (valueOrDefault > 0) { Plugin.Log.LogMessage((object)("[SYNERGY] arrow-capture patch ATTACHED to ShootProjectile.PerformShootProjectile " + $"({valueOrDefault} postfix(es)) — if no arrow is ever tagged, the method is not running for our shot.")); } else { Plugin.Log.LogWarning((object)"[SYNERGY] arrow-capture patch did NOT attach to ShootProjectile.PerformShootProjectile — bow synergy cannot work (V7)."); } } internal static void OnProjectileFired(Character affected, Projectile fired) { //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) if (!_seamProven) { _seamProven = true; Plugin.Log.LogMessage((object)("[SYNERGY] arrow-capture seam is LIVE — ShootProjectile.PerformShootProjectile ran (shooter '" + (((Object)(object)affected != (Object)null) ? affected.Name : "null") + "', projectile " + (((Object)(object)fired != (Object)null) ? "present" : "NULL") + "). If a bow Hunt-as-One still never tags an arrow, the fault is the owner/window scope, NOT the patch.")); } if (!((Object)(object)_armPlayer == (Object)null)) { if (Time.time > _armDeadline) { DisarmArrowCapture(); } else if ((Object)(object)fired == (Object)null) { Plugin.Log.LogWarning((object)"[SYNERGY] armed, but PerformShootProjectile recorded no projectile (m_lastShotProjectile null) — nothing to tag."); } else if (!IsArmedOwner(affected)) { string text = (((Object)(object)affected != (Object)null) ? $"'{affected.Name}' (uid {affected.UID})" : "null"); Plugin.Log.LogWarning((object)($"[SYNERGY] armed for '{_armPlayer.Name}' (uid {_armPlayer.UID}) but this " + "projectile was fired by " + text + " — not ours, ignored. If those UIDs match, the owner gate itself is broken; if they differ, we armed the wrong character.")); } else { TagArrow(fired, _armReport, _armBonus); DisarmArrowCapture(); } } } private static void TryAdoptAtImpact(Projectile proj) { if ((Object)(object)_armPlayer == (Object)null || (Object)(object)proj == (Object)null || Time.time > _armDeadline) { return; } for (int i = 0; i < _arrows.Count; i++) { if (_arrows[i].P == proj) { return; } } Character ownerCharacter = ((EffectSynchronizer)proj).OwnerCharacter; if (IsArmedOwner(ownerCharacter)) { Plugin.Log.LogMessage((object)"[SYNERGY] arrow adopted AT IMPACT (the spawn seam never fired — this is the seam-agnostic path; the shot is still judged honestly from here)."); TagArrow(proj, _armReport, _armBonus); DisarmArrowCapture(); } } internal static void OnShooterActivated(ShootProjectile shooter, Character affected) { if (!_activateSeamProven) { _activateSeamProven = true; Plugin.Log.LogMessage((object)("[SYNERGY] shooter-ACTIVATE seam is LIVE — ShootProjectile.ActivateLocally runs (shooter '" + (((Object)(object)affected != (Object)null) ? affected.Name : "null") + "'). If PerformShootProjectile's postfix still never fires, the bug is in THAT patch, not in the game's call path.")); } if (!((Object)(object)_armPlayer == (Object)null) && !((Object)(object)shooter == (Object)null)) { OnProjectileFired(affected, shooter.m_lastShotProjectile); } } internal static void TagArrow(Projectile p, bool report, float bonusMult) { if ((Object)(object)p == (Object)null) { return; } for (int i = 0; i < _arrows.Count; i++) { if (_arrows[i].P == p) { return; } } _arrows.Add(new TaggedArrow { P = p, Report = report, BonusMult = bonusMult, Deadline = Time.time + Mathf.Max(2f, Plugin.SynergyWindowSeconds.Value) }); Plugin.Log.LogMessage((object)string.Format("[SYNERGY] arrow tagged ({0}, bonus x{1:0.##}).", report ? "HAO" : "FTK", bonusMult)); } internal static void OnProjectileEvent(Projectile proj, Character victim, Vector3 hitDir, bool blocked) { //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Unknown result type (might be due to invalid IL or missing references) if (!_hitSeamProven) { _hitSeamProven = true; Plugin.Log.LogMessage((object)"[SYNERGY] projectile-HIT seam is LIVE — Projectile.OnProjectileHit runs."); } TryAdoptAtImpact(proj); if (_arrows.Count == 0) { return; } int num = -1; for (int i = 0; i < _arrows.Count; i++) { if (_arrows[i].P == proj) { num = i; break; } } if (num < 0) { return; } TaggedArrow taggedArrow = _arrows[num]; _arrows.RemoveAt(num); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)victim == (Object)null) { if (taggedArrow.Report) { ReportPlayer((StrikeOutcome)6); } Plugin.Log.LogMessage((object)"[SYNERGY] arrow hit terrain/expired — MISSED, no bonus damage."); return; } if (blocked) { if (taggedArrow.Report) { ReportPlayer((StrikeOutcome)2); } Plugin.Log.LogMessage((object)("[SYNERGY] arrow BLOCKED by '" + victim.Name + "' — no bonus damage.")); return; } if (!Plugin.IsCommandable(victim, localPlayerCharacter)) { if (taggedArrow.Report) { ReportPlayer((StrikeOutcome)6); } Plugin.Log.LogMessage((object)("[SYNERGY] arrow struck non-enemy '" + victim.Name + "' — MISSED for synergy, no bonus damage.")); return; } if (taggedArrow.Report) { bool flag = Plugin.SynergyRequireSameTarget.Value && (Object)(object)_target != (Object)null && (Object)(object)victim != (Object)(object)_target; ReportPlayer((StrikeOutcome)((!flag) ? 1 : 6)); if (flag) { Plugin.Log.LogMessage((object)("[SYNERGY] arrow landed on '" + victim.Name + "' but RequireSameTarget wanted '" + _target.Name + "' — no stack from this one.")); } } float num2 = Plugin.HuntAsOneBonusDamage.Value * ((taggedArrow.BonusMult > 0f) ? taggedArrow.BonusMult : 1f); if (num2 > 0f && (Object)(object)localPlayerCharacter != (Object)null && victim.Alive) { victim.ReceiveHit((Weapon)null, num2, hitDir, victim.CenterPosition, 45f, 1f, localPlayerCharacter, 1.5f); Plugin.Log.LogMessage((object)$"[SYNERGY] arrow landed on '{victim.Name}' — bonus strike {num2:F0} dealt at impact."); } if (!taggedArrow.Report) { ForTheKillSkill.MaybeGrantKillFavor(Plugin.Instance?.ActivePet?.SpeciesId, victim); } } private static void ClearAttemptArrows() { _arrows.RemoveAll((TaggedArrow a) => a.Report); } private static void PruneArrows(float now) { _arrows.RemoveAll((TaggedArrow a) => (Object)(object)a.P == (Object)null || now > a.Deadline); } internal static int EffectiveStacks(Character player, Pet pet) { if (!Plugin.EnableSynergy.Value || pet == null) { return 0; } return Synergy.ClampStacks(SynergyStatus.StackCount(player), Plugin.SynergyMaxStacks.Value); } internal static void SyncBuffs(Character player, Pet pet) { if (!((Object)(object)player == (Object)null)) { int num = EffectiveStacks(player, pet); if (num == 0 && (!Plugin.EnableSynergy.Value || pet == null) && SynergyStatus.StackCount(player) > 0) { SynergyStatus.Remove(player); Plugin.Log.LogMessage((object)"[SYNERGY] leftover BW_Synergy cleared (kill-switch off or no companion)."); } SynergyBuff.Sync(player, num, Plugin.SynergyPercentPerStack.Value); } } internal static void DevSetStacks(Character player, string[] parts) { if ((Object)(object)player == (Object)null) { Plugin.Log.LogWarning((object)"[SYNERGY] no player."); return; } if (parts == null || parts.Length < 2 || !int.TryParse(parts[1], out var result)) { Plugin.Log.LogWarning((object)("[SYNERGY] usage: synergy <0-" + Plugin.SynergyMaxStacks.Value + ">")); return; } result = Synergy.ClampStacks(result, Plugin.SynergyMaxStacks.Value); SynergyStatus.SetStacks(player, result); Plugin.Instance?.ResyncBuffs(); Plugin.Log.LogMessage((object)$"[SYNERGY] dev-set stacks -> {SynergyStatus.StackCount(player)} (wanted {result})."); } internal static void Dump(Plugin plugin) { //IL_0243: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Plugin.Log.LogMessage((object)($"[SYNERGYDUMP] EnableSynergy={Plugin.EnableSynergy.Value}, HonestHits={Plugin.HonestHits.Value}, " + $"registered={SynergyStatus.Registered}, PercentPerStack={Plugin.SynergyPercentPerStack.Value:0.##}, " + $"MaxStacks={Plugin.SynergyMaxStacks.Value}, Duration={Plugin.SynergyDurationSeconds.Value:0}s, " + $"Window={Plugin.SynergyWindowSeconds.Value:0.#}s, RequireSameTarget={Plugin.SynergyRequireSameTarget.Value}.")); Plugin.Log.LogMessage((object)($"[SYNERGYDUMP] ranges: petMelee={Plugin.PetMeleeRangeMeters.Value:0.#}m, " + $"playerMelee={Plugin.PlayerMeleeRangeMeters.Value:0.#}m cone={Plugin.PlayerMeleeConeDegrees.Value:0}°.")); Plugin.Log.LogMessage((object)(AttemptOpen ? ("[SYNERGYDUMP] attempt OPEN on '" + (((Object)(object)_target != (Object)null) ? _target.Name : "?") + "': " + _attempt.Verdict + ", " + $"{Mathf.Max(0f, (float)(_attempt.OpenedAt + _attempt.WindowSeconds - (double)Time.time)):0.#}s left, " + $"{_arrows.Count} tagged arrow(s).") : $"[SYNERGYDUMP] no open attempt; last close: {_lastClose}; {_arrows.Count} tagged arrow(s).")); StatusEffect val = SynergyStatus.Live(localPlayerCharacter); Plugin.Log.LogMessage((object)(((Object)(object)val != (Object)null) ? ($"[SYNERGYDUMP] status: {val.StackCount} stack(s), {val.RemainingLifespan:0.#}s remaining, " + $"family '{val.EffectFamily?.Name}' stack={val.EffectFamily?.StackBehavior}.") : "[SYNERGYDUMP] status: none on the player.")); object obj; if (localPlayerCharacter == null) { obj = null; } else { CharacterStats stats = localPlayerCharacter.Stats; obj = ((stats != null) ? stats.m_damageTypesModifier[0] : null); } Stat val2 = (Stat)obj; Plugin.Log.LogMessage((object)("[SYNERGYDUMP] player PhysicalDamage readback: " + ((val2 != null) ? val2.CurrentValue.ToString("F4") : "n/a") + " (1.0000 = unbuffed; synergy composes with bond buffs).")); Pet activePet = plugin.ActivePet; CompanionCombat val3 = ((activePet != null) ? ((Companion)activePet).Combat : null); Plugin.Log.LogMessage((object)("[SYNERGYDUMP] pet DamageMultiplier in force: " + (((Object)(object)val3 != (Object)null) ? val3.DamageMultiplier.ToString("F4") : "n/a (no body)") + ".")); } } internal static class SynergyBuff { private const string SourceId = "Beastwhispering.Synergy"; private static readonly PlayerStatBuff _applier = new PlayerStatBuff(false); internal static void Sync(Character player, int stacks, float percentPerStack) { //IL_006e: 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_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0075: 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_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return; } float num = (float)(Synergy.DamageMult(stacks, (double)percentPerStack) - 1.0); List list = new List(); if (num > 0f && (Object)(object)player.Stats != (Object)null) { for (int i = 0; i <= 5; i++) { list.Add(new StatStackSpec(player.Stats.m_damageTypesModifier[i], "Beastwhispering.Synergy", num, true, (string)null)); } } StatBuffResult val = _applier.Sync(player, list, (Action)null); if ((int)val.Change != 0) { if (val.AppliedCount > 0) { Plugin.Log.LogMessage((object)($"[SYNERGY] player ALL-damage +{num * 100f:0.##}% " + string.Format("({0} stack(s){1}).", stacks, val.SamePlayer ? "" : ", fresh player"))); } else if (val.SamePlayer) { Plugin.Log.LogMessage((object)"[SYNERGY] player damage bonus cleared."); } } } internal static void Clear() { _applier.Clear(); } } [HarmonyPatch(typeof(Projectile), "OnProjectileHit")] internal static class SynergyArrowRelay { private static void Postfix(Projectile __instance, Character _affectedCharacter, Vector3 _hitDir, bool _blocked) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) HuntSynergy.OnProjectileEvent(__instance, _affectedCharacter, _hitDir, _blocked); } } [HarmonyPatch(typeof(ShootProjectile), "PerformShootProjectile")] internal static class SynergyArrowCapture { private static void Postfix(ShootProjectile __instance, Character _affectedCharacter) { HuntSynergy.OnProjectileFired(_affectedCharacter, ((Object)(object)__instance != (Object)null) ? __instance.m_lastShotProjectile : null); } } [HarmonyPatch(typeof(ShootProjectile), "ActivateLocally")] internal static class SynergyShooterActivate { private static void Postfix(ShootProjectile __instance, Character _affectedCharacter) { HuntSynergy.OnShooterActivated(__instance, _affectedCharacter); } } [HarmonyPatch(typeof(Character), "HasHit")] internal static class SynergyHasHitObserver { private static bool Prepare() { return BwConfig.Synergy.ObserverPatch?.Value ?? false; } private static void Postfix(Character __instance, float _damage, bool _blocked, Character _target) { if (HuntSynergy.AttemptOpen && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Plugin.LocalPlayerCharacter)) { Plugin.Log.LogMessage((object)("[SYNERGY] observer: real player swing connected '" + (((Object)(object)_target != (Object)null) ? _target.Name : "?") + "' " + $"blocked={_blocked} dmg={_damage:F0} (compare vs the judged verdict — plan R1).")); } } } internal static class ItemNameIndex { internal static int Count => ItemNameIndex.Count; internal static void RegisterCustom(string displayName, int itemId) { ItemNameIndex.RegisterCustom(displayName, itemId); } internal static bool TryResolve(string displayName, out int itemId) { return ItemNameIndex.TryResolve(displayName, ref itemId); } internal static bool TryResolveCatalog(string candidate, out int itemId, out string via) { return ItemNameIndex.TryResolveCatalog(candidate, ref itemId, ref via); } internal static List> Suggest(string fragment, int max = 8) { return ItemNameIndex.Suggest(fragment, max, (Func)null); } } internal static class KillFavorBuff { private const string SourceId = "Beastwhispering.KillFavor"; private static readonly PlayerStatBuff _applier = new PlayerStatBuff(true); private static KillFavorStat? _appliedStat; private static float _appliedAmount; private static StatStackSpec? SpecFor(Character player, KillFavorStat stat, float amount) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected I4, but got Unknown //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) CharacterStats val = ((player != null) ? player.Stats : null); return (int)stat switch { 0 => new StatStackSpec(val?.m_staminaUseModifiers, "Beastwhispering.KillFavor", (0f - amount) * 0.01f, true, (string)null), 1 => new StatStackSpec(val?.m_manaUseModifiers, "Beastwhispering.KillFavor", (0f - amount) * 0.01f, true, (string)null), 2 => new StatStackSpec(val?.m_allDamageProtection, "Beastwhispering.KillFavor", amount, false, (string)null), _ => null, }; } internal static void Grant(Character player, KillFavorStat stat, float amount, string logSuffix = "") { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_007d: 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) if (!((Object)(object)player == (Object)null) && !(amount <= 0f)) { Clear(); StatStackSpec? val = SpecFor(player, stat, amount); if (!val.HasValue) { Plugin.Log.LogWarning((object)$"[KILLFAVOR] no player stat mapping for {stat} — nothing applied."); return; } _applier.Apply(player, new List { val.Value }, (Action)null); _appliedStat = stat; _appliedAmount = amount; Plugin.Log.LogMessage((object)$"[KILLFAVOR] granted {Describe(stat, amount)} for {Plugin.KillFavorDurationSeconds.Value:0}s{logSuffix}."); } } internal static string Describe(KillFavorStat stat, float amount) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected I4, but got Unknown //IL_0052: Unknown result type (might be due to invalid IL or missing references) return (int)stat switch { 0 => $"stamina costs −{amount:0.##}%", 1 => $"mana costs −{amount:0.##}%", 2 => $"+{amount:0.##} protection", _ => $"+{amount:0.##} {stat}", }; } internal static void Sync(Character player) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return; } StatusEffect val = KillFavorStatus.Live(player); if (!_appliedStat.HasValue) { if ((Object)(object)val != (Object)null) { KillFavorStatus.Remove(player); Plugin.Log.LogMessage((object)"[KILLFAVOR] restored status with no in-memory grant (relaunch mid-buff) — removed."); } } else if ((Object)(object)val == (Object)null) { Clear(); } else if (!_applier.SamePlayer(player)) { Grant(player, _appliedStat.Value, _appliedAmount, ", fresh player"); } } internal static void Clear() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) bool flag = (Object)(object)_applier.AppliedTo != (Object)null && _appliedStat.HasValue; string text = (flag ? Describe(_appliedStat.Value, _appliedAmount) : null); _applier.Clear(); if (flag) { Plugin.Log.LogMessage((object)("[KILLFAVOR] cleared (" + text + ").")); } _appliedStat = null; _appliedAmount = 0f; } internal static void Dump(Character player) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableKillFavor.Value) { Plugin.Log.LogMessage((object)"[KILLFAVOR] killing favor DISABLED ([ForTheKill] EnableKillFavor)."); return; } bool flag = (Object)(object)player != (Object)null && (Object)(object)KillFavorStatus.Live(player) != (Object)null; if (!_appliedStat.HasValue) { Plugin.Log.LogMessage((object)$"[KILLFAVOR] no buff applied (status live: {flag})."); } else { Plugin.Log.LogMessage((object)("[KILLFAVOR] " + Describe(_appliedStat.Value, _appliedAmount) + " applied " + string.Format("(status live: {0}{1}).", flag, _applier.SamePlayer(player) ? "" : ", STALE — applied-to is not the current player"))); } if ((Object)(object)((player != null) ? player.Stats : null) != (Object)null) { ManualLogSource log = Plugin.Log; Stat staminaUseModifiers = player.Stats.m_staminaUseModifiers; string text = string.Format("staminaUse x{0:0.###}{1}, ", (staminaUseModifiers != null) ? new float?(staminaUseModifiers.CurrentValue) : ((float?)null), PlayerStatBuff.OurStack(player.Stats.m_staminaUseModifiers, "Beastwhispering.KillFavor", true)); Stat manaUseModifiers = player.Stats.m_manaUseModifiers; string text2 = string.Format("manaUse x{0:0.###}{1}, ", (manaUseModifiers != null) ? new float?(manaUseModifiers.CurrentValue) : ((float?)null), PlayerStatBuff.OurStack(player.Stats.m_manaUseModifiers, "Beastwhispering.KillFavor", true)); Stat allDamageProtection = player.Stats.m_allDamageProtection; log.LogMessage((object)("[KILLFAVOR] live readback: " + text + text2 + string.Format("allProtection +{0:0.###}{1}.", (allDamageProtection != null) ? new float?(allDamageProtection.CurrentValue) : ((float?)null), PlayerStatBuff.OurStack(player.Stats.m_allDamageProtection, "Beastwhispering.KillFavor", false)))); } } } internal static class KillFavorStatus { internal const string Id = "BW_KillFavor"; private static bool _registered; internal static bool Registered => _registered; internal static void Init() { SL.OnPacksLoaded += Register; } private static void Register() { if (_registered) { return; } SlStatusSpec spec = new SlStatusSpec { Id = "BW_KillFavor", NumId = 87062, Name = "Killing Favor", Description = "Your companion's killing blow favors you — a temporary edge, its shape set by your bond.", Lifespan = Mathf.Max(1f, Plugin.KillFavorDurationSeconds.Value), IconPng = "PetKillFavor.png", IsMalus = false, Tag = "[KILLFAVOR]", DisabledSuffix = "the killing-favor buff is disabled." }; if (!SlStatus.ResolveDonor(spec, out var donor)) { return; } try { if (SlStatus.Register(spec, donor, out var prefab)) { _registered = true; Plugin.Log.LogMessage((object)$"[KILLFAVOR] 'BW_KillFavor' registered (donor '{donor}', lifespan {prefab.StatusData?.LifeSpan:0}s)."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[KILLFAVOR] 'BW_KillFavor' registration failed: " + ex)); } } internal static StatusEffect Live(Character player) { return SlStatus.Live(player, "BW_KillFavor"); } internal static void AddOrRefresh(Character player) { if (_registered) { SlStatus.AddOrRefresh(player, "BW_KillFavor", Plugin.KillFavorDurationSeconds.Value); } } internal static void Remove(Character player) { SlStatus.Remove(player, "BW_KillFavor"); } internal static void SyncDuration() { if (_registered) { SlStatus.SyncDuration("BW_KillFavor", Mathf.Max(1f, Plugin.KillFavorDurationSeconds.Value)); } } } internal static class LanternShare { internal sealed class LanternPin : MonoBehaviour { public Transform Body; public float TopLocalY; private void LateUpdate() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: 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_007e: 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_0093: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)Body == (Object)null)) { Vector3 val = Vector3.ProjectOnPlane(Body.forward, Vector3.up); val = ((((Vector3)(ref val)).sqrMagnitude > 0.0001f) ? ((Vector3)(ref val)).normalized : Vector3.forward); ((Component)this).transform.position = Body.position + Vector3.up * (TopLocalY + Plugin.LanternUpOffset.Value) + val * Plugin.LanternForwardOffset.Value; ((Component)this).transform.rotation = Quaternion.identity; } } } private static float _lastPoll = -999f; private static readonly NameCandidates _statusNames = new NameCandidates("Runic Lantern", (Func)(() => Plugin.LanternStatusNames.Value), (Func)NameCandidates.StatusPrefabExists, (Action)null, (Action)null); private static GameObject _clone; private static StatusEffect _sourceStatus; private static string _lastAction = "none yet"; private static bool _guestModeLogged; private static bool _buildFailWarned; internal static void Tick(Plugin plugin) { //IL_00cf: 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_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Invalid comparison between Unknown and I4 //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Invalid comparison between Unknown and I4 if (!Plugin.EnableLanternShare.Value) { if ((Object)(object)_clone != (Object)null) { DestroyClone("kill-switch off"); } } else { if (Time.time - _lastPoll < Plugin.LanternPollSeconds.Value) { return; } _lastPoll = Time.time; Character localPlayer = Plugin.LocalPlayer; if ((Object)(object)((localPlayer != null) ? localPlayer.StatusEffectMngr : null) == (Object)null) { return; } if (PhotonNetwork.isNonMasterClientInRoom) { if (!_guestModeLogged) { _guestModeLogged = true; Plugin.Log.LogMessage((object)"[LANTERN] lantern share: guest mode — the pet body is proxied on the master; mirroring is owner-side v1, skipping."); } if ((Object)(object)_clone != (Object)null) { DestroyClone("guest mode"); } return; } CompanionBody val = ((Companion)(plugin.ActivePet?)).Body; StatusEffect val2 = FindPlayerLanternStatus(localPlayer); LanternAction val3 = LanternRules.Mirror((Object)(object)val2 != (Object)null, (Object)(object)val != (Object)null, (Object)(object)_clone != (Object)null); if ((int)val3 != 1) { if ((int)val3 == 2) { DestroyClone(((Object)(object)val2 != (Object)null) ? "pet body gone" : "player's lantern gone"); } } else { GameObject val4 = BuildStrippedClone(val2); if (!((Object)(object)val4 == (Object)null)) { AttachToBody(val4, val); _clone = val4; _buildFailWarned = false; _lastAction = "created above the pet"; Plugin.Log.LogMessage((object)("[LANTERN] player's Runic Lantern mirrored — clone above '" + val.SpeciesId + "'.")); } } if (val2 != _sourceStatus) { if ((Object)(object)val2 != (Object)null && _sourceStatus != null && (Object)(object)_clone != (Object)null) { Plugin.Log.LogMessage((object)"[LANTERN] status instance swapped (recast) — clone persists, refreshed duration inherited."); } _sourceStatus = val2; } } } private static StatusEffect FindPlayerLanternStatus(Character player) { ValidateCandidates(); return _statusNames.FindFirst((Func)((string cand) => player.StatusEffectMngr.GetStatusEffectOfName(cand))); } private static void ValidateCandidates() { Validation val = default(Validation); if (_statusNames.TryValidate(ref val)) { int count = ((Validation)(ref val)).Known.Count; if (((Validation)(ref val)).NoneKnown) { Plugin.Log.LogWarning((object)("[LANTERN] NO candidate in '" + ((Validation)(ref val)).Raw + "' exists in the status registry — detection can never fire; fix the [Lantern] name list (discovery: cast, then `lanterndump`'s status census, or `statusdump`).")); } else if (((Validation)(ref val)).Unknown.Count > 0) { Plugin.Log.LogWarning((object)string.Format("[LANTERN] candidate(s) not in the status registry (typo?): '{0}' — the other {1} candidate(s) still work.", string.Join("', '", ((Validation)(ref val)).Unknown), count)); } else { Plugin.Log.LogMessage((object)$"[LANTERN] all {count} status candidate(s) exist in the registry; detection checks each against the player per poll."); } } } private static GameObject BuildStrippedClone(StatusEffect status) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Expected O, but got Unknown GameObject val = (((Object)(object)status.FxTransform != (Object)null) ? ((Component)status.FxTransform).gameObject : (((Object)(object)status.FXPrefab != (Object)null) ? ((Component)status.FXPrefab).gameObject : null)); if ((Object)(object)val == (Object)null) { if (!_buildFailWarned) { _buildFailWarned = true; Plugin.Log.LogWarning((object)("[LANTERN] status '" + status.IdentifierName + "' has no live FX and no FXPrefab — nothing to mirror (will keep retrying while it lives).")); } return null; } GameObject val2 = null; GameObject val3 = null; try { val2 = new GameObject("BW_LanternHolder"); val2.SetActive(false); val3 = Object.Instantiate(val, val2.transform); ((Object)val3).name = "BW_PetLantern"; MonoBehaviour[] componentsInChildren = val3.GetComponentsInChildren(true); foreach (MonoBehaviour val4 in componentsInChildren) { if ((Object)(object)val4 != (Object)null) { try { Object.DestroyImmediate((Object)(object)val4); } catch { } } } Collider[] componentsInChildren2 = val3.GetComponentsInChildren(true); foreach (Collider val5 in componentsInChildren2) { if ((Object)(object)val5 != (Object)null) { try { Object.DestroyImmediate((Object)(object)val5); } catch { } } } Rigidbody[] componentsInChildren3 = val3.GetComponentsInChildren(true); foreach (Rigidbody val6 in componentsInChildren3) { if ((Object)(object)val6 != (Object)null) { try { Object.DestroyImmediate((Object)(object)val6); } catch { } } } if (val3.GetComponentsInChildren(true).Length == 0) { Plugin.Log.LogWarning((object)"[LANTERN] the cloned FX has no Light component — mirroring its renderers/particles only."); } return val3; } catch (Exception ex) { if ((Object)(object)val3 != (Object)null) { Object.Destroy((Object)(object)val3); } if ((Object)(object)val2 != (Object)null) { Object.Destroy((Object)(object)val2); } if (!_buildFailWarned) { _buildFailWarned = true; Plugin.Log.LogWarning((object)("[LANTERN] lantern clone failed: " + ex.Message + " — will keep retrying while the player's lantern lives.")); } return null; } } private static void AttachToBody(GameObject clone, CompanionBody body) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) LanternPin lanternPin = clone.AddComponent(); lanternPin.Body = ((Component)body).transform; lanternPin.TopLocalY = BodyTopY(body); Transform parent = clone.transform.parent; clone.transform.SetParent(((Component)body).transform, false); clone.transform.localPosition = Vector3.up * (lanternPin.TopLocalY + Plugin.LanternUpOffset.Value); clone.SetActive(true); if ((Object)(object)parent != (Object)null) { Object.Destroy((Object)(object)((Component)parent).gameObject); } } private static float BodyTopY(CompanionBody body) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) SkinnedMeshRenderer componentInChildren = ((Component)body).GetComponentInChildren(); if ((Object)(object)componentInChildren == (Object)null) { return 1.5f; } Bounds bounds = ((Renderer)componentInChildren).bounds; return Mathf.Max(0.2f, ((Bounds)(ref bounds)).max.y - ((Component)body).transform.position.y); } private static void DestroyClone(string why) { if ((Object)(object)_clone != (Object)null) { Object.Destroy((Object)(object)_clone); } _clone = null; _lastAction = "destroyed (" + why + ")"; Plugin.Log.LogMessage((object)("[LANTERN] pet lantern removed — " + why + ".")); } internal static void Dump(Plugin plugin) { //IL_0442: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableLanternShare.Value) { Plugin.Log.LogMessage((object)"[LANTERN] lantern sharing DISABLED ([Lantern] EnableLanternShare)."); return; } Plugin.Log.LogMessage((object)($"[LANTERN] poll every {Plugin.LanternPollSeconds.Value:0.##}s, " + $"offsets up={Plugin.LanternUpOffset.Value:0.##}m forward={Plugin.LanternForwardOffset.Value:0.##}m.")); if (PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogMessage((object)"[LANTERN] role: GUEST — mirroring is owner-side v1, inactive here."); } Character localPlayer = Plugin.LocalPlayer; if ((Object)(object)((localPlayer != null) ? localPlayer.StatusEffectMngr : null) == (Object)null) { Plugin.Log.LogMessage((object)"[LANTERN] no player / StatusEffectMngr — cast first, then dump."); return; } foreach (string item in _statusNames.Parse()) { bool flag = NameCandidates.StatusPrefabExists(item); StatusEffect statusEffectOfName = localPlayer.StatusEffectMngr.GetStatusEffectOfName(item); Plugin.Log.LogMessage((object)("[LANTERN] candidate '" + item + "': registry=" + (flag ? "yes" : "NO") + ", player carries=" + (((Object)(object)statusEffectOfName != (Object)null) ? $"YES ({statusEffectOfName.RemainingLifespan:0.#}s left)" : "no"))); } List statuses = localPlayer.StatusEffectMngr.Statuses; int num = 0; if (statuses != null) { foreach (StatusEffect item2 in statuses) { if (!((Object)(object)item2 == (Object)null)) { num++; Plugin.Log.LogMessage((object)string.Format("[LANTERN] status '{0}' remaining={1:0.#}s fx={2}", item2.IdentifierName, item2.RemainingLifespan, item2.HasExternalFXAttached ? PathOf(item2.FxTransform, ((Component)localPlayer).transform) : "none")); } } } Plugin.Log.LogMessage((object)$"[LANTERN] status census: {num} status(es) on the player (above)."); Light[] componentsInChildren = ((Component)localPlayer).GetComponentsInChildren(true); Plugin.Log.LogMessage((object)$"[LANTERN] light census: {componentsInChildren.Length} Light(s) under the player hierarchy."); Light[] array = componentsInChildren; foreach (Light val in array) { if ((Object)(object)val != (Object)null) { Plugin.Log.LogMessage((object)$"[LANTERN] light '{((Object)((Component)val).gameObject).name}' path={PathOf(((Component)val).transform, ((Component)localPlayer).transform)} enabled={((Behaviour)val).enabled} activeInHierarchy={((Component)val).gameObject.activeInHierarchy}"); } } StatusEffect val2 = FindPlayerLanternStatus(localPlayer); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogMessage((object)"[LANTERN] detection: the player carries none of the candidates (if a lantern is up, the census above names the real identifier — add it to [Lantern] LanternStatusNames)."); } else { Plugin.Log.LogMessage((object)($"[LANTERN] detection: status '{val2.IdentifierName}' remaining={val2.RemainingLifespan:0.#}s, " + string.Format("hasLiveFx={0}, fx={1}, ", val2.HasExternalFXAttached, ((Object)(object)val2.FxTransform != (Object)null) ? PathOf(val2.FxTransform, ((Component)localPlayer).transform) : "null") + "fxPrefab=" + (((Object)(object)val2.FXPrefab != (Object)null) ? ((Object)val2.FXPrefab).name : "null") + ".")); } CompanionBody val3 = ((Companion)(plugin.ActivePet?)).Body; Plugin.Log.LogMessage((object)("[LANTERN] body: " + (((Object)(object)val3 == (Object)null) ? "none" : $"'{val3.SpeciesId}' alive, bounds-top {BodyTopY(val3):0.##}m."))); if ((Object)(object)_clone == (Object)null) { Plugin.Log.LogMessage((object)("[LANTERN] clone: none; last action: " + _lastAction + ".")); return; } Plugin.Log.LogMessage((object)($"[LANTERN] clone: '{((Object)_clone).name}' at {_clone.transform.position} — " + $"{_clone.GetComponentsInChildren(true).Length} light(s), " + $"{_clone.GetComponentsInChildren(true).Length} renderer(s), " + $"{_clone.GetComponentsInChildren(true).Length} particle system(s); " + "last action: " + _lastAction + ".")); } private static string PathOf(Transform t, Transform root) { if ((Object)(object)t == (Object)null) { return "null"; } string text = ((Object)t).name; Transform parent = t.parent; while ((Object)(object)parent != (Object)null && (Object)(object)parent != (Object)(object)root) { text = ((Object)parent).name + "/" + text; parent = parent.parent; } return text; } } internal static class MarenSetup { internal const string NpcId = "bw.maren"; internal const string TreeUid = "bw.skilltree"; private const string HomeScene = "CierzoNewTerrain"; private const float HomeX = 1414.735f; private const float HomeY = 5.65796f; private const float HomeZ = 1674.953f; private const float HomeYaw = 168.1f; private static ConfigEntry _enabled; private static ConfigEntry _scene; private static ConfigEntry _posX; private static ConfigEntry _posY; private static ConfigEntry _posZ; private static ConfigEntry _rotY; internal static NpcSpec Spec; internal static bool Enabled { get { if (_enabled != null) { return _enabled.Value; } return false; } } internal static void Init(ConfigFile config) { _enabled = config.Bind("Maren", "EnableMaren", true, "Maren the Beastwhisperer — the trainer NPC selling the Beastwhispering skill tree (docs/maren-trainer-spike-plan.md). Off = she is never registered with StoryKit (relaunch to change)."); _scene = config.Bind("Maren", "MarenScene", "CierzoNewTerrain", "Scene build name Maren stands in. Default = Cierzo town (Cobalt's chosen spot)."); _posX = config.Bind("Maren", "MarenPosX", 1414.735f, "Maren's spawn position. Ships pre-stamped in Cierzo town; re-stamp by standing at the new spot and running `marenhere`. (0,0,0) = stale/unset — she falls back to the built-in default spot."); _posY = config.Bind("Maren", "MarenPosY", 5.65796f, "See MarenPosX."); _posZ = config.Bind("Maren", "MarenPosZ", 1674.953f, "See MarenPosX."); _rotY = config.Bind("Maren", "MarenRotY", 168.1f, "Maren's facing (yaw degrees). Stamped by `marenhere` — stand AS her: she takes your exact position AND facing (Cobalt's ruling, session 1)."); if (!Enabled) { Plugin.Log.LogMessage((object)"[MAREN] disabled by config."); return; } Spec = BuildSpec(); NpcRegistry.Register(Spec); } private static Placement HomePlacement() { //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown float value = _posX.Value; float value2 = _posY.Value; float value3 = _posZ.Value; float value4 = _rotY.Value; if (value == 0f && value2 == 0f && value3 == 0f) { Plugin.Log.LogWarning((object)string.Format("[MAREN] config position is (0,0,0) — a stale cfg from a pre-2026-07-16 build (BepInEx never migrates changed defaults; the installer never touches config). Self-healing to the built-in spot ({0:F1},{1:F1},{2:F1}) in {3}; run `marenhere` to stamp your own, or delete the [Maren] lines to re-adopt defaults.", 1414.735f, 5.65796f, 1674.953f, "CierzoNewTerrain")); return new Placement("CierzoNewTerrain", 1414.735f, 5.65796f, 1674.953f, 168.1f); } return new Placement(_scene.Value, value, value2, value3, value4); } private static NpcSpec BuildSpec() { //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_001b: 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_0032: 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_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Expected O, but got Unknown //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Expected O, but got Unknown //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Expected O, but got Unknown //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Expected O, but got Unknown //IL_014a: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Expected O, but got Unknown //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Expected O, but got Unknown //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Expected O, but got Unknown //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Expected O, but got Unknown //IL_01b2: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Expected O, but got Unknown //IL_01c6: Expected O, but got Unknown //IL_01cb: Expected O, but got Unknown //IL_01cc: Expected O, but got Unknown return new NpcSpec { Id = "bw.maren", Name = "Maren", Placements = { HomePlacement() }, LookFollowEnabled = false, ChestId = 3000000, BootsId = 3000002, Dialogue = new DialogueSpec { Greetings = { "You've got the look of someone who's been staring at the wild a little too long. Good. That's where it starts." }, Choices = { Choice.Train("train", "Train with me.") }, Choices = { Choice.Reply("who", "Who are you?", "Maren. I've spent longer listening to Aurai's beasts than to its people — the beasts lie less. If you want to learn what they've taught me, say so.") } }, Trainer = new TrainerSpec { SkillTreeUID = "bw.skilltree", Tree = new SkillTreeDef { Name = "Beastwhispering", Slots = new List { new SlotDef(1, 2, 87006, 50, 0, 0, false), new SlotDef(2, 1, 87002, 100, 1, 2, false), new SlotDef(2, 2, 87007, 100, 1, 2, false), new SlotDef(2, 3, 87003, 50, 1, 2, false), new SlotDef(3, 2, 87008, 500, 0, 0, true), new SlotDef(4, 1, 87001, 500, 3, 2, false), new SlotDef(4, 2, 87009, 600, 3, 2, false), new SlotDef(4, 3, 87000, 500, 3, 2, false), new SlotDef(5, 1, 87005, 600, 4, 1, false), new SlotDef(5, 3, 87004, 600, 4, 3, false) } } } }; } internal static void StampHere(Character player) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0099: 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_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) if (Spec == null) { Plugin.Log.LogWarning((object)"[MAREN] disabled — nothing to stamp."); return; } Vector3 position = ((Component)player).transform.position; string activeSceneName = SceneManagerHelper.ActiveSceneName; float y = ((Component)player).transform.eulerAngles.y; _scene.Value = activeSceneName; _posX.Value = position.x; _posY.Value = position.y; _posZ.Value = position.z; _rotY.Value = y; Placement val = Spec.Placements[0]; val.Scene = activeSceneName; val.X = position.x; val.Y = position.y; val.Z = position.z; val.RotationY = y; Plugin.Log.LogMessage((object)$"[MAREN] stamped: scene={activeSceneName} — copy-paste config:\n[Maren]\nMarenScene = {activeSceneName}\nMarenPosX = {position.x:F2}\nMarenPosY = {position.y:F2}\nMarenPosZ = {position.z:F2}\nMarenRotY = {y:F1}"); NpcRegistry.Respawn("bw.maren"); } } internal sealed class Pet : Companion { public PetSave State; public PetSimulation Sim; public ScentTracker Scent = new ScentTracker(); public string OwnerUid; public string SpeciesId => State?.SpeciesId; public Pet() : base(Plugin.CkHost, (ICompanionSettings)null) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown base.NetMirrorFactory = ProxyPets.NewGuestMirror; base.Anchor.EnableHealthPersistence((Func)(() => Plugin.PersistPetHealth.Value)); ((Companion)this).BindCapturedStats((Func)(() => State?.Attributes), (Action)delegate(CreatureAttributes v) { if (State != null) { State.Attributes = v; } }); } } internal static class PetBagPerk { private static float _wanted; private static readonly Stamp _stamp = new Stamp((Func)((Bag b) => (Object)(object)b != (Object)null && (Object)(object)b.Container != (Object)null), (Func)((Bag b) => ((ItemContainer)b.Container).BonusCapacity), (Action)delegate(Bag b, float v) { ((ItemContainer)b.Container).BonusCapacity = v; }, 0f, (Func)((float v) => v <= 0f), (Func)null) { TargetLostReason = "no longer your equipped pack", NothingWantedReason = "the bond grants nothing right now", OnForeignWriter = delegate(Bag bag, float current, float stamped) { Plugin.Log.LogWarning((object)$"[BAGPERK] '{((Item)bag).Name}' bonus reads {current:0.#} but we stamped {stamped:0.#} — another writer? Converging onto ours."); }, OnStamp = delegate(Bag bag, float v) { Plugin.Log.LogMessage((object)$"[BAGPERK] carry gift +{v:0.#} on '{((Item)bag).Name}' -> {((ItemContainer)bag.Container).ContainerCapacity:0.#} capacity."); }, OnWithdraw = delegate(Bag bag, float v, string why) { Plugin.Log.LogMessage((object)$"[BAGPERK] carry gift +{v:0.#} withdrawn from '{((Item)bag).Name}' ({why})."); } }; internal static void Sync(Character player, string speciesId, LoyaltyTier tier, bool communionEnabled, bool beastOfBurdenLearned) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) float num = ((!string.IsNullOrEmpty(speciesId)) ? Resolve(speciesId, tier) : 0f); _wanted = PetBuffs.BagCapacityTotal(num, Plugin.EnableBagPerk.Value && communionEnabled, BwConfig.Skills.BeastOfBurdenCapacity.Value, beastOfBurdenLearned); _stamp.Sync(EquippedBagOf(player), _wanted); } internal static void Clear() { _stamp.Clear("the bond ended"); _wanted = 0f; _stamp.ResetForeignWarning(); } internal static void Dump(Character player) { Plugin.Log.LogMessage((object)("[BAGPERK] species gift " + (Plugin.EnableBagPerk.Value ? "enabled" : "DISABLED ([Systems] EnableBagPerk)") + $"; Beast of Burden adds {BwConfig.Skills.BeastOfBurdenCapacity.Value:0.#} while learned + bonded " + $"(learned={PetSystems.SkillLearned(player, 87007)}).")); Bag val = EquippedBagOf(player); if ((Object)(object)val == (Object)null || (Object)(object)val.Container == (Object)null) { Plugin.Log.LogMessage((object)$"[BAGPERK] wanted +{_wanted:0.#} capacity — no backpack equipped (gift dormant, pouch untouched by design)."); return; } float bonusCapacity = ((ItemContainer)val.Container).BonusCapacity; Plugin.Log.LogMessage((object)$"[BAGPERK] wanted +{_wanted:0.#} — '{((Item)val).Name}' readback: base {((ItemContainer)val.Container).ContainerCapacity - bonusCapacity:0.#} + bonus {bonusCapacity:0.#} = {((ItemContainer)val.Container).ContainerCapacity:0.#} capacity."); } private static float Resolve(string speciesId, LoyaltyTier tier) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Invalid comparison between Unknown and I4 float num = 0f; foreach (ResolvedBuff item in PetBuffs.Resolve(PetBuffTable.Table, speciesId, tier)) { ResolvedBuff current = item; if ((int)((ResolvedBuff)(ref current)).Stat == 7) { num += ((ResolvedBuff)(ref current)).Amount; } } return num; } private static Bag EquippedBagOf(Character player) { if (!((Object)(object)player != (Object)null) || !((Object)(object)player.Inventory != (Object)null)) { return null; } return player.Inventory.EquippedBag; } } internal static class PetBandage { private const float AckTimeoutSeconds = 2.5f; private static string _idsKey; private static HashSet _ids; private static readonly NameCandidates _status = new NameCandidates("bandage heal", (Func)(() => Plugin.BandageStatusNames.Value), (Func)NameCandidates.StatusPrefabExists, (Action)delegate(string cand) { Plugin.Log.LogMessage((object)("[BANDAGE] resolved bandage heal status: '" + cand + "'.")); }, (Action)delegate(string raw) { Plugin.Log.LogWarning((object)("[BANDAGE] no bandage heal status resolved from candidates '" + raw + "' — fix [Bandage] BandageStatusNames (discovery: `statusdump`).")); }); private static string _lastAction = "none yet"; internal static bool IsBandage(Item item) { if ((Object)(object)item == (Object)null || !Plugin.EnableBandageHealing.Value) { return false; } return ItemIds().Contains(item.ItemID); } internal static void Apply(Item item, Character player) { if ((Object)(object)item == (Object)null || (Object)(object)player == (Object)null) { Plugin.Log.LogWarning((object)"[BANDAGE] no item/player on the bandage attempt."); return; } Pet pet = Plugin.Instance?.ActivePet; if (pet?.Sim == null) { Notify.Player(player, "You have no companion to bandage."); return; } string text = (string.IsNullOrEmpty(pet.SpeciesId) ? "Your pet" : pet.SpeciesId); if (PhotonNetwork.isNonMasterClientInRoom) { RequestFromMaster(item, player, text); return; } CompanionAnchor anchor = ((Companion)pet).Anchor; Character val = ((anchor != null) ? anchor.Current : null); if ((Object)(object)((val != null) ? val.StatusEffectMngr : null) == (Object)null) { Plugin.Log.LogMessage((object)$"[BANDAGE] '{item.Name}' (id={item.ItemID}) not applied — {text} has no live body to bandage; nothing consumed."); Notify.Player(player, text + " has no body to bandage right now."); return; } string text2 = ResolveStatus(); if (text2 == null) { Notify.Player(player, "You can't seem to bandage " + text + "."); return; } bool flag = (Object)(object)val.StatusEffectMngr.GetStatusEffectOfName(text2) != (Object)null; val.StatusEffectMngr.AddStatusEffect(text2); int remainingAmount = item.RemainingAmount; item.RemoveQuantity(1); StatusEffect statusEffectOfName = val.StatusEffectMngr.GetStatusEffectOfName(text2); _lastAction = "applied '" + text2 + "' to " + text + "'s anchor"; Plugin.Log.LogMessage((object)("[BANDAGE] applied '" + text2 + "' to " + text + "'s anchor " + (((Object)(object)statusEffectOfName != (Object)null) ? string.Format("({0:0.#}s left{1}, {2}) ", statusEffectOfName.RemainingLifespan, flag ? ", refreshed" : "", AnchorHp(val)) : "(status absent-until-init) ") + $"— consumed 1x '{item.Name}' (stack {remainingAmount} -> {remainingAmount - 1}); player NOT buffed.")); Notify.Player(player, "You bandage " + text + "."); } private static void RequestFromMaster(Item item, Character player, string species) { if (!PetProxyClient.TryGetMirroredHealth(out var _, out var max)) { Plugin.Log.LogMessage((object)$"[BANDAGE] '{item.Name}' (id={item.ItemID}) not applied — {species} has no live body to bandage; nothing consumed."); Notify.Player(player, species + " has no body to bandage right now."); } else if (PetProxyClient.SendBandageRequest(2.5f, delegate(RequestResult r) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) OnBandageResult(r, item, player, species); })) { string text = default(string); NetBus.TryGetPendingRequest("bw.bandage", ref text, ref max); _lastAction = "awaiting the master's ack (token " + text + ")"; Plugin.Log.LogMessage((object)("[BANDAGE] [G→M] requested a bandage for " + species + " (token " + text + ") — consumed only on the master's ack.")); } } private static void OnBandageResult(RequestResult r, Item item, Character player, string species) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Invalid comparison between Unknown and I4 //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: 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) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_028c: Unknown result type (might be due to invalid IL or missing references) //IL_021f: Unknown result type (might be due to invalid IL or missing references) if ((int)r.Outcome == 2) { if (r.Refusal == "in-flight") { Notify.Player(player, "You're already bandaging " + species + "."); return; } _lastAction = "request not sent (" + r.Refusal + ")"; Plugin.Log.LogMessage((object)("[BANDAGE] [G→M] '" + item.Name + "' request could not be sent (" + r.Refusal + ": no announced proxy / bus down) — nothing consumed.")); Notify.Player(player, species + " has no body to bandage right now."); return; } if ((int)r.Outcome == 1) { _lastAction = "timed out waiting for the master's ack"; Plugin.Log.LogMessage((object)($"[BANDAGE] [G→M] no answer from the master (token {r.Token}, {2.5f:0.#}s) " + "— bandage kept.")); Notify.Player(player, species + " has no body to bandage right now."); return; } if (r.Result != "ok") { string text = ((r.Result == "disabled") ? "the host has bandage healing turned off" : ((r.Result == "unresolved") ? "the host could not resolve the bandage heal status ([Bandage] BandageStatusNames there)" : "the host has no live body for your pet")); _lastAction = "master refused: " + r.Result; Plugin.Log.LogMessage((object)("[BANDAGE] [G→M] the master refused (token " + r.Token + ", " + r.Result + ") — " + text + "; nothing consumed.")); Notify.Player(player, species + " has no body to bandage right now."); return; } if ((Object)(object)item == (Object)null || (Object)(object)((EffectSynchronizer)item).OwnerCharacter != (Object)(object)player) { item = null; foreach (Item item2 in PetFeeder.InventoryItems(player)) { if (IsBandage(item2)) { item = item2; break; } } } if ((Object)(object)item == (Object)null) { _lastAction = "master applied it but no bandage remained to consume"; Plugin.Log.LogWarning((object)("[BANDAGE] [G→M] the master applied the bandage (token " + r.Token + ") but no bandage remains in the inventory to consume — the heal was free (report if this recurs).")); Notify.Player(player, "You bandage " + species + "."); return; } int remainingAmount = item.RemainingAmount; item.RemoveQuantity(1); _lastAction = "master applied it to " + species + "'s proxied anchor"; Plugin.Log.LogMessage((object)("[BANDAGE] [G→M] the master applied the bandage to " + species + "'s proxied anchor " + $"(token {r.Token}) — consumed 1x '{item.Name}' (stack {remainingAmount} -> {remainingAmount - 1}); player NOT buffed.")); Notify.Player(player, "You bandage " + species + "."); } internal static void ApplyToProxyAnchor(Character anchor, string statusId, string ownerUid) { if (!((Object)(object)((anchor != null) ? anchor.StatusEffectMngr : null) == (Object)null) && !string.IsNullOrEmpty(statusId)) { anchor.StatusEffectMngr.AddStatusEffect(statusId); Plugin.Log.LogMessage((object)("[BANDAGE] [G→M] guest '" + ownerUid + "' — '" + statusId + "' applied to their proxied anchor (" + AnchorHp(anchor) + ").")); } } internal static string ActionLabel() { string text = Plugin.Instance?.ActivePet?.SpeciesId; if (!string.IsNullOrEmpty(text)) { return "Bandage " + text; } return "Bandage pet"; } private static string AnchorHp(Character anchor) { if (!((Object)(object)anchor.Stats != (Object)null)) { return "hp=?"; } return $"hp={anchor.Stats.CurrentHealth:F0}/{anchor.Stats.MaxHealth:F0}"; } private static HashSet ItemIds() { string text = Plugin.BandageItemIds.Value ?? ""; if (text == _idsKey && _ids != null) { return _ids; } _idsKey = text; _ids = new HashSet(); string[] array = text.Split(new char[1] { ',' }); foreach (string text2 in array) { if (int.TryParse(text2.Trim(), out var result)) { _ids.Add(result); } } return _ids; } internal static string ResolveStatus() { return _status.Resolve(); } internal static void ApplyFirstFromInventory(Character player) { if ((Object)(object)player == (Object)null) { Plugin.Log.LogWarning((object)"[BANDAGE] no local player."); return; } if (!Plugin.EnableBandageHealing.Value) { Plugin.Log.LogMessage((object)"[BANDAGE] bandage healing DISABLED ([Bandage] EnableBandageHealing)."); return; } foreach (Item item in PetFeeder.InventoryItems(player)) { if (IsBandage(item)) { Apply(item, player); return; } } Plugin.Log.LogMessage((object)("[BANDAGE] no bandage ([Bandage] BandageItemIds=" + Plugin.BandageItemIds.Value + ") in the inventory — nothing applied.")); Notify.Player(player, "You have no bandages."); } internal static void Dump(Plugin plugin) { if (!Plugin.EnableBandageHealing.Value) { Plugin.Log.LogMessage((object)"[BANDAGE] bandage healing DISABLED ([Bandage] EnableBandageHealing)."); return; } HashSet values = ItemIds(); string text = ResolveStatus() ?? "UNRESOLVED"; Plugin.Log.LogMessage((object)("[BANDAGE] bandage item ids=[" + string.Join(", ", values) + "]; heal status='" + text + "'.")); if (PhotonNetwork.isNonMasterClientInRoom) { string arg = default(string); float num = default(float); Plugin.Log.LogMessage((object)("[BANDAGE] role: GUEST — bandages route to the master's proxy anchor (bw.bandage), consumed only on its ack; pending: " + (NetBus.TryGetPendingRequest("bw.bandage", ref arg, ref num) ? $"token {arg}, {num:0.##}s left" : "none"))); } else { Plugin.Log.LogMessage((object)"[BANDAGE] role: MASTER/SP — bandages apply to the local anchor; guests' requests arrive as bw.bandage and are answered with bw.bandage.ack."); } Pet activePet = plugin.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; if (activePet?.Sim == null) { Plugin.Log.LogMessage((object)"[BANDAGE] no active pet."); return; } if ((Object)(object)((val != null) ? val.StatusEffectMngr : null) == (Object)null && PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogMessage((object)("[BANDAGE] pet '" + activePet.SpeciesId + "': no LOCAL anchor (normal on a guest — it is proxied on the master); mirrored health " + (PetProxyClient.TryGetMirroredHealth(out var current, out var max) ? $"hp={current:F0}/{max:F0} — a bandage would be requested" : "unavailable — a bandage would be refused without consuming") + ".")); return; } if ((Object)(object)((val != null) ? val.StatusEffectMngr : null) == (Object)null) { Plugin.Log.LogMessage((object)("[BANDAGE] pet '" + activePet.SpeciesId + "': no live anchor — a bandage would be refused without consuming.")); return; } bool flag = text != "UNRESOLVED" && val.StatusEffectMngr.GetStatusEffectOfName(text) != null; StatusEffect val2 = ((text != "UNRESOLVED") ? val.StatusEffectMngr.GetStatusEffectOfName(text) : null); Plugin.Log.LogMessage((object)("[BANDAGE] pet '" + activePet.SpeciesId + "': anchor " + AnchorHp(val) + "; bandage status currently " + (flag ? $"ACTIVE ({((val2 != null) ? new float?(val2.RemainingLifespan) : ((float?)null)):0.#}s left)" : "not present") + "; last action: " + _lastAction + ".")); } } internal static class PetBuffTable { private static readonly TableLoader> _loader = new TableLoader>("SpeciesBuffs.txt", "[BUFF]", "buff species entry", "buff species entries", (Func, Dictionary>>)PetBuffs.Parse, (Func>, Dictionary>, Dictionary>>)PetBuffs.Merge, "replaces per-species", (Func>, Action, Dictionary>>)null, (Func>, string>)null, ""); private static readonly DataAxis>> _axis = BwAxis.Table(_loader, "[BUFF]", Validate); internal static Dictionary> Table => _axis.Table; internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = TableValidator.CheckSpeciesKeys("[BUFF]", "SpeciesBuffs.txt", Table.Keys); Plugin.Log.LogMessage((object)string.Format("[BUFF] boot check: {0} buff species entr{1}, {2} unknown species key(s).", Table.Count, (Table.Count == 1) ? "y" : "ies", num)); } } internal sealed class PetCanActCondition : EffectCondition { private const float CacheSeconds = 0.2f; private float _cachedAt = -999f; private bool _cached = true; private string _reason; internal static void Init() { SL.OnPacksLoaded += Attach; } private static void Attach() { //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_0090: Expected O, but got Unknown ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item obj = ((instance != null) ? instance.GetItemPrefab(87001) : null); Skill val = (Skill)(object)((obj is Skill) ? obj : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[HAO] can't attach the can-act condition — Hunt as One's prefab didn't resolve. A press with no target will still swing for nothing (Bug 30 Leg B inactive)."); return; } ActivationCondition[] array = (ActivationCondition[])(((object)val.m_additionalConditions) ?? ((object)new ActivationCondition[0])); for (int i = 0; i < array.Length; i++) { if (array[i]?.Condition is PetCanActCondition) { return; } } PetCanActCondition condition = ((Component)val).gameObject.AddComponent(); ActivationCondition val2 = new ActivationCondition { Condition = (EffectCondition)(object)condition }; ActivationCondition[] array2 = (ActivationCondition[])(((object)val.m_additionalConditions) ?? ((object)new ActivationCondition[0])); ActivationCondition[] array3 = (ActivationCondition[])(object)new ActivationCondition[array2.Length + 1]; array2.CopyTo(array3, 0); array3[array2.Length] = val2; val.m_additionalConditions = array3; Plugin.Log.LogMessage((object)("[HAO] can-act condition attached to Hunt as One " + $"({array2.Length} pre-existing condition(s) preserved) — a press with no target is now refused " + "before it costs stamina or a swing (Bug 30 Leg B).")); } public override bool CheckIsValid(Character _affectedCharacter) { if (Time.time - _cachedAt < 0.2f) { return _cached; } _cachedAt = Time.time; _cached = Evaluate(_affectedCharacter, out _reason); return _cached; } public override string ProcessMessage(string _message) { if (!string.IsNullOrEmpty(_reason)) { return _reason; } return _message ?? "Your companion cannot do that right now."; } private static bool Evaluate(Character player, out string reason) { if (!Plugin.SyncSkillCooldownToPet.Value) { reason = null; return true; } if (!Plugin.EnableSpecialAttack.Value) { reason = null; return true; } Pet pet = Plugin.Instance?.ActivePet; if (pet == null) { reason = "You have no companion."; return false; } CompanionBody body = ((Companion)pet).Body; if ((Object)(object)body == (Object)null) { reason = "Your companion has no body to strike with."; return false; } PetSpecialAttack component = ((Component)body).GetComponent(); if ((Object)(object)component == (Object)null) { reason = "Your companion cannot use a signature attack."; return false; } return component.CanFireIgnoringCooldown(player, out reason); } } internal static class PetComfortTable { private static readonly TableLoader _bandLoader = new TableLoader("SpeciesComfort.txt", "[TEMP]", "comfort band", "comfort bands", (Func, Dictionary>)SpeciesComfort.Parse, (Func, Dictionary, Dictionary>)SpeciesComfort.Merge, "replaces per-key", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _bandAxis = BwAxis.Table(_bandLoader, "[TEMP]", Validate); private static readonly TableLoader _blanketLoader = new TableLoader("Blankets.txt", "[TEMP]", "blanket", "blankets", (Func, Dictionary>)Blankets.Parse, (Func, Dictionary, Dictionary>)Blankets.Merge, "replaces per-key", (Func, Action, Dictionary>)null, (Func, string>)null, ""); internal static readonly ComfortBand FallbackBand = new ComfortBand((TempStep)2, (TempStep)5); internal static Dictionary Bands => _bandAxis.Table; internal static Dictionary Blankets => ((TableLoader)_blanketLoader).Table; internal static ComfortBand BandFor(string speciesId) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) return SpeciesComfort.Resolve(Bands, speciesId, FallbackBand); } internal static ComfortRelief ReliefFor(PetSave state) { if (state == null || string.IsNullOrEmpty(state.BlanketKey) || state.BlanketSecondsLeft <= 0.0) { return null; } if (!Blankets.TryGetValue(state.BlanketKey, out var value)) { return null; } return value.ToRelief(); } internal static void Init() { _bandAxis.Init(); } internal static void Reload() { _bandAxis.Reload(); ((TableLoader)_blanketLoader).Reload(); } private static void Validate() { int num = TableValidator.CheckSpeciesKeys("[TEMP]", "SpeciesComfort.txt", Bands.Keys, "Default"); Plugin.Log.LogMessage((object)$"[TEMP] boot check: {Bands.Count} comfort band(s), {num} unknown species key(s)."); } internal static void TempDump(Plugin p) { //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableTemperatureSystem.Value) { Plugin.Log.LogMessage((object)"[TEMP] EnableTemperatureSystem=false — system off."); return; } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Plugin.Log.LogMessage((object)("[TEMP] gate (as of last tick): " + PetSystems.GateReport())); EnvironmentConditions val = PetSystems.CurrentEnvironment(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)"[TEMP] no EnvironmentConditions in this scene (menu/void?)."); return; } if ((Object)(object)localPlayerCharacter != (Object)null) { Plugin.Log.LogMessage((object)$"[TEMP] ambient at PLAYER: {(object)(TempStep)Mathf.Clamp(val.GetTemperature(((Component)localPlayerCharacter).transform, -1f), 0, 8)}"); } Pet activePet = p.ActivePet; if (activePet?.State == null) { Plugin.Log.LogMessage((object)"[TEMP] no active pet."); return; } PetSystems.ComfortSample comfortSample = PetSystems.SampleComfort(activePet, localPlayerCharacter); Plugin.Log.LogMessage((object)("[TEMP] gate (this dump): " + PetSystems.GateReport())); Plugin.Log.LogMessage((object)string.Format("[TEMP] ambient at PET ({0}): {1}", ((Object)(object)((Companion)activePet).Body != (Object)null) ? "body" : "player fallback", comfortSample.Ambient)); Plugin.Log.LogMessage((object)("[TEMP] " + ComfortSummary(p))); Plugin.Log.LogMessage((object)$"[TEMP] ramp: escalate {Plugin.TempEscalateSeconds.Value:F0}s/stage (×{((ComfortReading)(ref comfortSample.Reading)).EscalateMult:0.#} in force), recover {Plugin.TempRecoverSeconds.Value:F0}s/stage."); Plugin.Log.LogMessage((object)$"[TEMP] drain policy: Suffering {Plugin.SufferingDrainPerMinute.Value:0.#}%/min, Critical {Plugin.CriticalDrainPerMinute.Value:0.#}%/min of max HP; at zero: {Plugin.PetDeathModeConfig.Value}; decay mults {Plugin.UneasyDecayMult.Value:0.#}/{Plugin.SufferingDecayMult.Value:0.#}."); Plugin.Log.LogMessage((object)$"[TEMP] tables: {Bands.Count} band entries, {Blankets.Count} blankets ('reloadcomfort' re-reads overrides)."); } internal static string ComfortSummary(Plugin p) { //IL_0108: 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) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_01e8: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.State == null) { return "no active pet."; } if (!Plugin.EnableTemperatureSystem.Value) { return "temperature system OFF."; } PetSystems.ComfortSample lastSample = PetSystems.LastSample; string text = (string.IsNullOrEmpty(activePet.State.BlanketKey) ? "none" : $"{activePet.State.BlanketKey} ({activePet.State.BlanketSecondsLeft:F0}s left)"); string text2 = (string.IsNullOrEmpty(activePet.State.DrinkKey) ? "none" : $"{WeatherFoodTable.DisplayName(activePet.State.DrinkKey)} ({activePet.State.DrinkSecondsLeft:F0}s left)"); if (!lastSample.Valid) { return "no sample yet (menu/void scene?); blanket=" + text + ", drink=" + text2 + ". " + PetSystems.GateReport(); } return $"ambient={lastSample.Ambient}, band={((ComfortBand)(ref lastSample.Band)).Min}-{((ComfortBand)(ref lastSample.Band)).Max}, out={((ComfortReading)(ref lastSample.Reading)).StepsOutside}" + ((((ComfortReading)(ref lastSample.Reading)).RawStepsOutside == ((ComfortReading)(ref lastSample.Reading)).StepsOutside) ? "" : (((ComfortReading)(ref lastSample.Reading)).Immune ? $" (raw {((ComfortReading)(ref lastSample.Reading)).RawStepsOutside}, IMMUNE)" : $" (raw {((ComfortReading)(ref lastSample.Reading)).RawStepsOutside}, relief in force)")) + $", side={((ComfortReading)(ref lastSample.Reading)).Side}, stage={activePet.State.TemperatureStage}, escMult={((ComfortReading)(ref lastSample.Reading)).EscalateMult:0.#}, blanket={text}, drink={text2}" + "; " + PetSystems.GateReport(); } } internal static class PetCommands { internal static void FireSpecialAttack(Plugin p, bool fromSkill = false) { CompanionBody val = ((Companion)(p.ActivePet?)).Body; if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)"[HAO] press -> SKIPPED: no active pet body."); return; } PetSpecialAttack component = ((Component)val).GetComponent(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogMessage((object)"[HAO] press -> SKIPPED: pet has no PetSpecialAttack (is [Combat] EnableSpecialAttack on?)."); return; } if (p.ActivePet != null && ((Companion)p.ActivePet).Stance.Passive) { EngagePet(p); } Character player = Plugin.LocalPlayer; PetSpecialAttack.SpecialFireReport specialFireReport = component.Fire((Plugin.EnableHuntAsOne.Value && (Object)(object)player != (Object)null) ? ((Action)delegate(Character target) { HuntAsOnePlayer.SyncedStrike(player, target); }) : null); Plugin.Log.LogMessage((object)("[HAO] press -> " + specialFireReport.Summary)); if (specialFireReport.Started && !specialFireReport.SynergyDeferred && Plugin.EnableHuntAsOne.Value && (Object)(object)player != (Object)null) { CompanionCombat combat = ((Companion)p.ActivePet).Combat; HuntSynergy.Open((combat != null) ? combat.SpecialAttackTarget : null); HuntAsOnePlayer.OnSpecialAttackFired(player); } if (specialFireReport.Started) { HuntAsOneCooldown.ReconcileAfterFire(player, component, fromSkill); } } internal static void TogglePetCommand(Plugin p) { if (p.ActivePet != null && ((Companion)p.ActivePet).Stance.Passive) { EngagePet(p); } else { DisengagePet(p); } } internal static void EngagePet(Plugin p) { Character localPlayer = Plugin.LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { Plugin.Log.LogMessage((object)"[PETCMD] no player."); return; } if (p.ActivePet == null) { Plugin.Log.LogMessage((object)"[PETCMD] no active pet to command."); return; } string how; Character val = FindEngageTarget(localPlayer, out how); ((Companion)p.ActivePet).Stance.CommandEngage(val); SkillRegistry.SyncIcons(localPlayer); Plugin.Log.LogMessage((object)(((Object)(object)val != (Object)null) ? ("[PETCMD] ENGAGE '" + val.Name + "' (" + how + ").") : ("[PETCMD] ENGAGE — " + how + "; auto-assist resumed (the pet fights what you fight)."))); } internal static void DisengagePet(Plugin p) { if (p.ActivePet == null) { Plugin.Log.LogMessage((object)"[PETCMD] no active pet to command."); return; } ((Companion)p.ActivePet).Stance.CommandDisengage(); SkillRegistry.SyncIcons(Plugin.LocalPlayer); Plugin.Log.LogMessage((object)"[PETCMD] DISENGAGE — pet returns to you and waits for the next order."); } internal static Character FindEngageTarget(Character player, out string how) { //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0103: 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_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) Character val = (((Object)(object)player.TargetingSystem != (Object)null) ? player.TargetingSystem.LockedCharacter : null); if (Plugin.IsCommandable(val, player)) { how = "your locked target"; return val; } CharacterManager instance = CharacterManager.Instance; DictionaryExt val2 = ((instance != null) ? instance.Characters : null); if (val2 == null) { how = "no characters loaded"; return null; } List list = new List(); List list2 = new List(); for (int i = 0; i < val2.Count; i++) { Character val3 = val2.Values[i]; if (Plugin.IsCommandable(val3, player)) { list.Add(val3); list2.Add(new Candidate { X = ((Component)val3).transform.position.x, Z = ((Component)val3).transform.position.z }); } } Vector3 forward = ((Component)player).transform.forward; int num = TargetPick.PickFrontTarget(((Component)player).transform.position.x, ((Component)player).transform.position.z, forward.x, forward.z, Plugin.EngageRange.Value, Plugin.EngageConeDegrees.Value, (IList)list2); how = ((num >= 0) ? "closest enemy in front of you" : "no locked target and nothing in the front cone"); if (num < 0) { return null; } return list[num]; } } internal static class PetDietTable { private static readonly TableLoader _loader = new TableLoader("PetDiets.json", "[FEED]", "diet species entry", "diet species entries", (Func, Dictionary>)PetDiet.Parse, (Func, Dictionary, Dictionary>)PetDiet.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)((Dictionary t) => (!t.ContainsKey("Default")) ? " — NO Default fallback: unlisted species reject everything" : " (incl. the Default fallback diet)"), ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[FEED]", Validate); internal static Dictionary Table => _axis.Table; internal static IReadOnlyList Resolve(string speciesId) { return PetDiet.Resolve(Table, speciesId); } internal static double? HungerFor(string speciesId) { return PetDiet.HungerSecondsPerDay(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { TableValidator.CheckItemTable("[FEED]", "PetDiets.json", "food row", ItemRefs(), out var checkedRefs, out var misses); Plugin.Log.LogMessage((object)(string.Format("[FEED] boot check: {0} diet species entr{1}, ", Table.Count, (Table.Count == 1) ? "y" : "ies") + $"{checkedRefs} item ref(s) (category rows skip the registry), {misses} unresolved.")); } private static IEnumerable ItemRefs() { foreach (KeyValuePair kv in Table) { foreach (FoodEntry food in kv.Value.Foods) { if (food.Category == null) { yield return new ItemRef(food.ItemId, food.Key, $"'{kv.Key}': food ItemID {food.ItemId}", "'" + kv.Key + "': food '" + food.Key + "'"); } } } } } internal static class PetFeeder { private static readonly List Interceptors = new List { new RelicFeedInterceptor(), new BuffFoodFeedInterceptor(), new MealFeedInterceptor() }; internal static void LogLadder() { List list = new List(Interceptors.Count); foreach (IFeedInterceptor interceptor in Interceptors) { list.Add(interceptor.Rung); } string text = FeedLadder.Mismatch((IReadOnlyList)list); if (text != null) { Plugin.Log.LogError((object)("[FEED] intercept ladder DRIFTED — " + text + ". Fix PetFeeder.Interceptors or Core.FeedLadder.Order; until then feeding precedence is not what the rules say.")); } else { Plugin.Log.LogMessage((object)("[FEED] intercept ladder: " + FeedLadder.Describe((IReadOnlyList)list) + " (diet and buff table are ORTHOGONAL — Core.FeedPrecedence, Cobalt 2026-07-29: the bufffood rung owns buff-ONLY items; a dual-listed item is a meal with the buff layered on, one item consumed).")); } } internal static void TryFeed(Item item, Character player, bool force = false) { if ((Object)(object)item == (Object)null || (Object)(object)player == (Object)null) { Plugin.Log.LogWarning((object)"[FEED] no item/player on the feed attempt."); return; } Pet pet = Plugin.Instance?.ActivePet; if (pet?.Sim == null) { Notify.Player(player, "You have no companion to feed."); return; } FeedContext ctx = new FeedContext(pet, player, item, force); foreach (IFeedInterceptor interceptor in Interceptors) { if (interceptor.Owns(ctx) && interceptor.Apply(ctx)) { break; } } } internal static void FeedAsMeal(FeedContext ctx) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006d: 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_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_0188: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Invalid comparison between Unknown and I4 //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_0204: Unknown result type (might be due to invalid IL or missing references) //IL_021b: Unknown result type (might be due to invalid IL or missing references) //IL_0232: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Invalid comparison between Unknown and I4 //IL_0270: Unknown result type (might be due to invalid IL or missing references) //IL_0275: Unknown result type (might be due to invalid IL or missing references) //IL_0277: Unknown result type (might be due to invalid IL or missing references) //IL_027b: Unknown result type (might be due to invalid IL or missing references) //IL_027e: Invalid comparison between Unknown and I4 //IL_02e5: Unknown result type (might be due to invalid IL or missing references) //IL_02ea: Unknown result type (might be due to invalid IL or missing references) //IL_02ef: Unknown result type (might be due to invalid IL or missing references) //IL_0306: Unknown result type (might be due to invalid IL or missing references) //IL_0309: Invalid comparison between Unknown and I4 //IL_0316: Unknown result type (might be due to invalid IL or missing references) //IL_0341: Unknown result type (might be due to invalid IL or missing references) //IL_0384: Unknown result type (might be due to invalid IL or missing references) //IL_0390: Unknown result type (might be due to invalid IL or missing references) //IL_0392: Unknown result type (might be due to invalid IL or missing references) //IL_03a9: Unknown result type (might be due to invalid IL or missing references) Pet pet = ctx.Pet; Character player = ctx.Player; Item item = ctx.Item; WaterContainer skin = ctx.Skin; int feedId = ctx.FeedId; string feedName = ctx.FeedName; string species = ctx.Species; bool force = ctx.Force; bool satiated = ctx.Satiated; IReadOnlyList diet = ctx.Diet; List categories = ctx.Categories; FeedDecision d = PetDiet.Decide(satiated, diet, feedId, feedName, (IReadOnlyCollection)categories, Plugin.FeedHealAmount.Value); WeatherFoodDef val = WeatherFoodTable.Match(feedId, feedName); DrinkRuling val2 = WeatherFoods.RuleFeed(((FeedDecision)(ref d)).Outcome, val); Plugin.Log.LogMessage((object)($"[FEED] offer '{item.Name}' (id={item.ItemID}, stack={item.RemainingAmount}) to '{species}': " + (((Object)(object)skin != (Object)null) ? $"contains '{feedName}' (id={feedId}, {skin.RemainingUse} use(s)) — matching the water, " : "") + string.Format("satiated={0}{1} hunger={2:P0} ", satiated, (force && pet.Sim.IsSatiated) ? " (FORCED past satiation)" : "", pet.Sim.HungerFraction) + ((categories.Count > 0) ? ("categories=[" + string.Join(", ", categories) + "] ") : "") + $"dietEntries={diet.Count} -> {((FeedDecision)(ref d)).Outcome}" + (((int)((FeedDecision)(ref d)).Outcome == 2) ? $" (matched '{((FeedDecision)(ref d)).MatchedKey}' as {((FeedDecision)(ref d)).Kind}: +{((FeedDecision)(ref d)).LoyaltyGain} loyalty, heal {((FeedDecision)(ref d)).Heal:F0}, hr lvl {((FeedDecision)(ref d)).HealthRecoveryLevel})" : "") + (((int)val2 != 0) ? $" | weather food '{val.Key}' -> {val2}" : ""))); if ((int)val2 == 2) { ConsumeOne(item, skin, feedName); ApplyDrink(pet, val, feedName); Plugin.Instance.PersistPet(); Notify.Player(player, species + " laps up the " + feedName + "."); return; } FeedOutcome outcome = ((FeedDecision)(ref d)).Outcome; if ((int)outcome != 0) { if ((int)outcome == 1) { Notify.Player(player, species + " turns its nose up at the " + item.Name + "."); return; } string text = feedName; int num = feedId; ConsumeOne(item, skin, feedName); PetGearSense.Apply(pet, player); int loyaltyValue = pet.Sim.State.LoyaltyValue; PetStatus val3 = pet.Sim.Feed(((FeedDecision)(ref d)).ToFoodMatch()); SpeciesRegistry.For(pet.SpeciesId).OnMealConsumed(pet, num, text); if ((int)val2 == 1) { ApplyDrink(pet, val, text); } ApplyHealthRecovery(pet, d, num, text); bool isMealPlusBuff = ((FeedPlan)(ref ctx.Plan)).IsMealPlusBuff; if (isMealPlusBuff) { ApplyBuffFood(pet, ctx.BuffRow, text); } PetSystems.ApplyEffects(pet, val3); bool flag = ((Companion)pet).Anchor != null && ((Companion)pet).Anchor.HealAmount(((FeedDecision)(ref d)).Heal, false); Plugin.Instance.PersistPet(); Plugin.Log.LogMessage((object)($"[FEED] fed -> loyalty {val3.LoyaltyValue} ({val3.Loyalty}) " + $"[+{val3.LoyaltyValue - loyaltyValue} landed of +{((FeedDecision)(ref d)).LoyaltyGain} face at " + $"{Plugin.LoyaltyGainPercent.Value:0.##}%, {pet.Sim.LoyaltyGainCarry:0.###} banked], " + $"satiatedNow={pet.Sim.IsSatiated}, " + "heal=" + (flag ? ((FeedDecision)(ref d)).Heal.ToString("F0") : "no-live-anchor") + " | anchor " + ((((Companion)pet).Anchor != null) ? ((Companion)pet).Anchor.HealthSummary() : "none"))); Notify.Player(player, isMealPlusBuff ? BuffFoodToast(ctx.BuffRow, ctx) : (species + " eats the " + text + ".")); } else { Notify.Player(player, species + " is not hungry."); } } internal static string BuffFoodToast(BuffFoodDef bf, FeedContext ctx) { if (string.IsNullOrEmpty(bf?.Toast)) { return ctx.Species + " devours the " + ctx.FeedName + " and something wild stirs in it."; } return bf.Toast; } internal static void FeedIdentity(Item item, out WaterContainer skin, out int id, out string name, out Item identity) { skin = (WaterContainer)(object)((item is WaterContainer) ? item : null); Item val = (Item)(object)(((Object)(object)skin != (Object)null && !skin.IsEmpty) ? skin.GetWaterItem() : null); if ((Object)(object)val == (Object)null) { skin = null; id = item.ItemID; name = item.Name; identity = item; } else { id = val.ItemID; name = val.Name; identity = val; } } internal static void ConsumeOne(Item item, WaterContainer skin, string feedName) { int remainingAmount = item.RemainingAmount; if ((Object)(object)skin != (Object)null) { ((Item)skin).RemoveQuantity(1, false); skin.m_hasChanged = true; Plugin.Log.LogMessage((object)$"[FEED] drank 1x '{feedName}' from the {item.Name} ({remainingAmount} -> {remainingAmount - 1} use(s))."); } else { item.RemoveQuantity(1); Plugin.Log.LogMessage((object)$"[FEED] consumed 1x '{feedName}' (stack {remainingAmount} -> {remainingAmount - 1})."); } } private static void ApplyDrink(Pet pet, WeatherFoodDef wf, string itemName) { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Invalid comparison between Unknown and I4 //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Invalid comparison between Unknown and I4 if (wf.ColdSteps == 0 && wf.HotSteps == 0 && !wf.Immune) { Plugin.Log.LogMessage((object)("[WEATHER] '" + itemName + "': zero-relief row — consumed, drink slot untouched.")); return; } string drinkKey = pet.State.DrinkKey; double drinkSecondsLeft = pet.State.DrinkSecondsLeft; bool flag = string.Equals(drinkKey, wf.Key, StringComparison.OrdinalIgnoreCase); pet.State.DrinkKey = wf.Key; pet.State.DrinkSecondsLeft = wf.DurationSeconds; string text = ""; PetSystems.ComfortSample lastSample = PetSystems.LastSample; if (wf.Immune) { text = ""; } else if (lastSample.Valid && (int)((ComfortReading)(ref lastSample.Reading)).Side == 1 && wf.ColdSteps == 0) { text = " — no help against the cold right now"; } else if (lastSample.Valid && (int)((ComfortReading)(ref lastSample.Reading)).Side == 2 && wf.HotSteps == 0) { text = " — no help against the heat right now"; } Plugin.Log.LogMessage((object)("[WEATHER] drink buff '" + wf.Key + "' (" + itemName + "): " + (wf.Immune ? "IMMUNE (total weather immunity, both sides) " : "") + $"cold={wf.ColdSteps} hot={wf.HotSteps} " + $"escalate={wf.EscalateMult} for {wf.DurationSeconds:F0}s" + ((flag && drinkSecondsLeft > 0.0) ? $" (refreshed, was {drinkSecondsLeft:F0}s)" : ((drinkSecondsLeft > 0.0) ? $" (replaced '{drinkKey}', {drinkSecondsLeft:F0}s lost)" : "")) + text)); } internal static void ApplyBuffFood(Pet pet, BuffFoodDef bf, string itemName) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) double num = BuffFoods.ResolveDuration(bf, pet.Sim.HungerSecondsPerDay); string buffFoodKey = pet.State.BuffFoodKey; double buffFoodSecondsLeft = pet.State.BuffFoodSecondsLeft; bool flag = string.Equals(buffFoodKey, bf.Key, StringComparison.OrdinalIgnoreCase); pet.State.BuffFoodKey = bf.Key; pet.State.BuffFoodSecondsLeft = num; LoyaltyTier loyalty = pet.Sim.Status().Loyalty; Plugin.Log.LogMessage((object)("[BUFFFOOD] buff '" + bf.Key + "' (" + itemName + "): " + BuffFoods.Describe(bf, loyalty, num) + ((flag && buffFoodSecondsLeft > 0.0) ? $" (refreshed, was {buffFoodSecondsLeft:F0}s)" : ((buffFoodSecondsLeft > 0.0) ? $" (replaced '{buffFoodKey}', {buffFoodSecondsLeft:F0}s lost)" : "")))); } private static void ApplyHealthRecovery(Pet pet, FeedDecision d, int feedId, string itemName) { if (Plugin.EnableFoodHealthRecovery.Value) { bool flag = Plugin.EnableTamingFoods.Value && TamingFoodTable.ForChowId(feedId) != null; int num = HealthRecovery.EffectiveLevel((int?)((FeedDecision)(ref d)).HealthRecoveryLevel, flag); int healthRecoveryLevel = pet.State.HealthRecoveryLevel; double healthRecoverySecondsLeft = pet.State.HealthRecoverySecondsLeft; pet.State.HealthRecoveryLevel = num; pet.State.HealthRecoverySecondsLeft = 600.0; Plugin.Log.LogMessage((object)("[REGEN] health recovery " + HealthRecovery.Describe(num, 600.0) + " (" + itemName + ")" + (flag ? " — taming food, forced level 5" : "") + ((healthRecoverySecondsLeft > 0.0) ? $" (replaced level {healthRecoveryLevel}, {healthRecoverySecondsLeft:F0}s lost)" : ""))); } } internal static void FeedFirstMatching(Character player, bool force = false) { //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Invalid comparison between Unknown and I4 if ((Object)(object)player == (Object)null) { Plugin.Log.LogWarning((object)"[FEED] no local player."); return; } Pet pet = Plugin.Instance?.ActivePet; if (pet?.Sim == null) { Plugin.Log.LogMessage((object)"[FEED] no pet to feed."); return; } if (!force && pet.Sim.IsSatiated) { Plugin.Log.LogMessage((object)$"[FEED] pet is satiated (hunger={pet.Sim.HungerFraction:P0}) — refusing. Use 'feed force' to bypass."); Notify.Player(player, pet.SpeciesId + " is not hungry."); return; } IReadOnlyList readOnlyList = PetDietTable.Resolve(pet.SpeciesId); foreach (Item item in InventoryItems(player)) { FeedIdentity(item, out var _, out var id, out var name, out var identity); FeedDecision val = PetDiet.Decide(false, readOnlyList, id, name, (IReadOnlyCollection)FoodCategoryTags.CategoriesOf(identity), Plugin.FeedHealAmount.Value); if ((int)((FeedDecision)(ref val)).Outcome == 2) { TryFeed(item, player, force: true); return; } } Plugin.Log.LogMessage((object)$"[FEED] no item in the inventory matches '{pet.SpeciesId}'s diet ({readOnlyList.Count} entries) — nothing fed. See 'dietdump'."); Notify.Player(player, "You have nothing " + pet.SpeciesId + " wants to eat."); } internal static void DietDump(Character player) { //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_031d: Unknown result type (might be due to invalid IL or missing references) //IL_0322: Unknown result type (might be due to invalid IL or missing references) //IL_0331: Unknown result type (might be due to invalid IL or missing references) //IL_0338: Unknown result type (might be due to invalid IL or missing references) //IL_033d: Unknown result type (might be due to invalid IL or missing references) //IL_0345: Unknown result type (might be due to invalid IL or missing references) //IL_034b: Invalid comparison between Unknown and I4 //IL_0407: Unknown result type (might be due to invalid IL or missing references) //IL_040a: Invalid comparison between Unknown and I4 //IL_0386: Unknown result type (might be due to invalid IL or missing references) //IL_03cd: Unknown result type (might be due to invalid IL or missing references) //IL_03d0: Invalid comparison between Unknown and I4 Pet pet = Plugin.Instance?.ActivePet; if (pet?.Sim == null) { Plugin.Log.LogMessage((object)"[DIET] no active pet."); return; } IReadOnlyList readOnlyList = PetDietTable.Resolve(pet.SpeciesId); Plugin.Log.LogMessage((object)($"[DIET] species='{pet.SpeciesId}' satiated={pet.Sim.IsSatiated} hunger={pet.Sim.HungerFraction:P0} " + $"(SatiationFraction={Plugin.SatiationFraction.Value}, tableSpecies={PetDietTable.Table.Count}, dietEntries={readOnlyList.Count})")); foreach (FoodEntry item in readOnlyList) { Tag tag; string text = ((item.Category != null) ? (FoodCategoryTags.TryGetTag(item.Category, out tag) ? ("category tag '" + tag.TagName + "'") : ("category '" + item.Category + "' — NO TAG, can never match (see 'foodcats')")) : (item.ItemId.HasValue ? $"ItemID {item.ItemId.Value}" : "by display name — English locale only")); Plugin.Log.LogMessage((object)("[DIET] accepts: '" + item.Key + "' (" + text + ") " + string.Format("{0} +{1} loyalty, heal {2}, ", item.Kind, item.LoyaltyGain, item.Heal.HasValue ? item.Heal.Value.ToString("F0") : $"default {Plugin.FeedHealAmount.Value:F0}") + "hr " + (item.HealthRecovery.HasValue ? $"lvl {item.HealthRecovery.Value}" : $"default lvl {1}"))); } if ((Object)(object)player == (Object)null) { return; } int num = 0; int num2 = 0; foreach (Item item2 in InventoryItems(player)) { FeedIdentity(item2, out var skin, out var id, out var name, out var identity); List list = FoodCategoryTags.CategoriesOf(identity); string text2 = (((Object)(object)skin != (Object)null) ? ("'" + item2.Name + "' -> '" + name + "'") : ("'" + item2.Name + "'")) + ((list.Count > 0) ? (" [" + string.Join(", ", list) + "]") : ""); FeedDecision val = PetDiet.Decide(false, readOnlyList, id, name, (IReadOnlyCollection)list, Plugin.FeedHealAmount.Value); WeatherFoodDef val2 = WeatherFoodTable.Match(id, name); DrinkRuling val3 = WeatherFoods.RuleFeed(((FeedDecision)(ref val)).Outcome, val2); num++; if ((int)((FeedDecision)(ref val)).Outcome == 2) { num2++; Plugin.Log.LogMessage((object)($"[DIET] inventory: {text2} (id={id}) x{item2.RemainingAmount} -> WOULD FEED ({((FeedDecision)(ref val)).Kind} '{((FeedDecision)(ref val)).MatchedKey}', +{((FeedDecision)(ref val)).LoyaltyGain}, heal {((FeedDecision)(ref val)).Heal:F0}, hr lvl {((FeedDecision)(ref val)).HealthRecoveryLevel})" + (((int)val3 == 1) ? $" + weather relief (cold={val2.ColdSteps} hot={val2.HotSteps})" : ""))); } else if ((int)val3 == 2) { Plugin.Log.LogMessage((object)$"[DIET] inventory: {text2} (id={id}) x{item2.RemainingAmount} -> WOULD DRINK (universal; cold={val2.ColdSteps} hot={val2.HotSteps}, relief only)"); } } Plugin.Log.LogMessage((object)($"[DIET] inventory dry-run: {num2} of {num} item(s) would be accepted" + (pet.Sim.IsSatiated ? " (but the pet is currently SATIATED — a real feed would refuse)." : "."))); } internal static IEnumerable InventoryItems(Character player) { return Inventories.All(player); } } internal sealed class FeedContext { internal readonly Pet Pet; internal readonly Character Player; internal readonly Item Item; internal readonly bool Force; internal readonly string Species; internal readonly IReadOnlyList Diet; internal readonly bool Satiated; internal readonly WaterContainer Skin; internal readonly int FeedId; internal readonly string FeedName; internal readonly Item Identity; internal readonly List Categories; internal readonly FoodEntry DietRow; internal readonly BuffFoodDef BuffRow; internal readonly FeedPlan Plan; internal FeedContext(Pet pet, Character player, Item item, bool force) { //IL_00eb: 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) Pet = pet; Player = player; Item = item; Force = force; Species = (string.IsNullOrEmpty(pet.SpeciesId) ? "Your pet" : pet.SpeciesId); Diet = PetDietTable.Resolve(pet.SpeciesId); Satiated = !force && pet.Sim.IsSatiated; PetFeeder.FeedIdentity(item, out Skin, out FeedId, out FeedName, out Identity); Categories = FoodCategoryTags.CategoriesOf(Identity); DietRow = FeedPrecedence.MatchingDietRow(Diet, FeedId, FeedName, (IReadOnlyCollection)Categories); BuffRow = BuffFoodTable.Match(pet.SpeciesId, FeedId, FeedName); Plan = FeedPrecedence.Plan(BuffRow != null, DietRow); } internal void ConsumeOne() { PetFeeder.ConsumeOne(Item, Skin, FeedName); } } internal interface IFeedInterceptor { string Rung { get; } bool Owns(FeedContext ctx); bool Apply(FeedContext ctx); } internal sealed class RelicFeedInterceptor : IFeedInterceptor { public string Rung => "relic"; public bool Owns(FeedContext ctx) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Invalid comparison between Unknown and I4 return (int)SpeciesRelics.RuleFeed(ctx.Pet.SpeciesId, ctx.FeedId, ctx.Pet.State.RelicStacks) > 0; } public bool Apply(FeedContext ctx) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Invalid comparison between Unknown and I4 //IL_00d8: 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) Pet pet = ctx.Pet; Ruling val = SpeciesRelics.RuleFeed(pet.SpeciesId, ctx.FeedId, pet.State.RelicStacks); RelicDef val2 = SpeciesRelics.For(pet.SpeciesId); if ((int)val == 2) { Plugin.Log.LogMessage((object)($"[RELIC] offer '{ctx.Item.Name}' (id={ctx.Item.ItemID}) refused at " + $"{5}/{5} stacks — nothing consumed.")); Notify.Player(ctx.Player, ctx.Species + " has taken all it can from " + (val2?.ItemLabel ?? "the relic") + "."); return true; } ctx.ConsumeOne(); PetSave state = pet.State; state.RelicStacks++; PetSystems.ApplyEffects(pet, pet.Sim.Status()); Plugin.Instance.PersistPet(); Plugin.Log.LogMessage((object)("[RELIC] fed '" + ctx.FeedName + "' -> " + SpeciesRelics.Describe(pet.State.RelicStacks) + ((PetSystems.EffectiveStats(pet, pet.Sim.Status()) == null) ? " (stats gated — no capture or [Systems] UseSpeciesStats off; the buff lands when stats go live)" : ""))); Notify.Player(ctx.Player, ctx.Species + " swallows the relic. " + (val2?.GrantPhrase ?? "Something stirs") + " " + $"({pet.State.RelicStacks}/{5})."); return true; } } internal sealed class BuffFoodFeedInterceptor : IFeedInterceptor { public string Rung => "bufffood"; public bool Owns(FeedContext ctx) { if (!((FeedPlan)(ref ctx.Plan)).AppliesBuff) { return false; } if (!((FeedPlan)(ref ctx.Plan)).IsMealPlusBuff) { return true; } Plugin.Log.LogMessage((object)("[BUFFFOOD] '" + ctx.FeedName + "' is BOTH a '" + ctx.Pet.SpeciesId + "' buff food ('" + ctx.BuffRow.Key + "') and a diet food ('" + ctx.DietRow.Key + "'" + ((ctx.DietRow.Category != null) ? " — a category row" : "") + ") — it is fed as a MEAL with the buff layered on top (one item consumed); the meal rung owns it.")); return false; } public bool Apply(FeedContext ctx) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) BuffFoodDef buffRow = ctx.BuffRow; if (buffRow == null) { return false; } Pet pet = ctx.Pet; ctx.ConsumeOne(); PetFeeder.ApplyBuffFood(pet, buffRow, ctx.FeedName); PetSystems.ApplyEffects(pet, pet.Sim.Status()); Plugin.Instance.PersistPet(); Notify.Player(ctx.Player, PetFeeder.BuffFoodToast(buffRow, ctx)); return true; } } internal sealed class MealFeedInterceptor : IFeedInterceptor { public string Rung => "meal"; public bool Owns(FeedContext ctx) { return true; } public bool Apply(FeedContext ctx) { PetFeeder.FeedAsMeal(ctx); return true; } } internal static class PetGearSense { private static string _lastSummary = "\0"; internal static GearEffects Resolve(Character player, string speciesId) { if (!Plugin.EnableGearEffects.Value || (Object)(object)player == (Object)null) { return GearEffects.None; } GearEffects val = PetGear.Resolve(PetGearTable.Table, (IReadOnlyList)ReadWorn(player), speciesId, (Action)null); string text = Summarize(val); if (text != _lastSummary) { _lastSummary = text; Plugin.Log.LogMessage((object)("[GEAR] equipped effects -> " + text + " (pet '" + speciesId + "').")); } return val; } internal static void Apply(Pet pet, Character player) { if (pet?.Sim != null) { pet.Sim.Gear = Resolve(player, pet.SpeciesId); } } internal static void Reset() { _lastSummary = "\0"; } internal static List ReadWorn(Character player) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) List list = new List(); object obj; if (player == null) { obj = null; } else { CharacterInventory inventory = player.Inventory; if (inventory == null) { obj = null; } else { CharacterEquipment equipment = inventory.Equipment; obj = ((equipment != null) ? equipment.EquipmentSlots : null); } } EquipmentSlot[] array = (EquipmentSlot[])obj; if (array == null) { return list; } for (int i = 0; i < array.Length; i++) { EquipmentSlot val = array[i]; if (!((Object)(object)val == (Object)null) && val.HasItemEquipped) { Equipment equippedItem = val.EquippedItem; if (!((Object)(object)equippedItem == (Object)null)) { GearSlot val2 = (GearSlot)((i >= 0 && i <= 8) ? i : (-1)); list.Add(new EquippedItem(((Item)equippedItem).ItemID, ((Item)equippedItem).Name, val2)); } } } return list; } private static string Summarize(GearEffects e) { if (e.IsIdentity) { return "none"; } List list = new List(); if (e.LoyaltyGainMult != 1f) { list.Add($"loyaltyGain x{e.LoyaltyGainMult:0.###}"); } if (e.LoyaltyDecayMult != 1f) { list.Add($"loyaltyDecay x{e.LoyaltyDecayMult:0.###}"); } if (e.PetStats.Health != 1f) { list.Add($"petHealth x{e.PetStats.Health:0.###}"); } if (e.PetStats.Damage != 1f) { list.Add($"petDamage x{e.PetStats.Damage:0.###}"); } if (e.PetStats.Defense != 1f) { list.Add($"petDefense x{e.PetStats.Defense:0.###}"); } if (e.PetStats.Speed != 1f) { list.Add($"petSpeed x{e.PetStats.Speed:0.###}"); } return string.Join(", ", list); } internal static void Dump(Plugin plugin, Character player) { //IL_00fc: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableGearEffects.Value) { Plugin.Log.LogMessage((object)"[GEAR] gear effects DISABLED ([Gear] EnableGearEffects)."); return; } string text = (plugin?.ActivePet)?.SpeciesId ?? "(no pet)"; Plugin.Log.LogMessage((object)string.Format("[GEAR] {0} table entr{1}; pet species '{2}'.", PetGearTable.Table.Count, (PetGearTable.Table.Count == 1) ? "y" : "ies", text)); foreach (GearEntry value in PetGearTable.Table.Values) { Plugin.Log.LogMessage((object)("[GEAR] table: '" + value.ItemKey + "'" + (value.ItemId.HasValue ? $" (ItemID {value.ItemId.Value})" : " (by name)") + string.Format(" slot={0} species={1} effects=[{2}]", value.Slot, value.Species ?? "any", Describe(value)))); } if ((Object)(object)player == (Object)null) { Plugin.Log.LogMessage((object)"[GEAR] no local player to read equipment from."); return; } List list = ReadWorn(player); Plugin.Log.LogMessage((object)("[GEAR] worn: " + ((list.Count == 0) ? "(nothing)" : string.Join(", ", list.Select((EquippedItem w) => $"{w.Name}#{w.ItemId}@{w.Slot}"))))); GearEffects val = PetGear.Resolve(PetGearTable.Table, (IReadOnlyList)list, text, (Action)delegate(string m) { Plugin.Log.LogMessage((object)("[GEAR] MATCH " + m)); }); Plugin.Log.LogMessage((object)("[GEAR] resolved -> " + Summarize(val))); if (!Plugin.UseSpeciesStats.Value && (val.PetStats.Health != 1f || val.PetStats.Damage != 1f || val.PetStats.Defense != 1f || val.PetStats.Speed != 1f)) { Plugin.Log.LogWarning((object)"[GEAR] pet-stat channels are set but [Systems] UseSpeciesStats is OFF — they won't apply until it's on and the pet has captured stats."); } } private static string Describe(GearEntry entry) { return string.Join(", ", entry.Effects.Select((GearEffectDef d) => $"{d.Kind} x{d.Value:0.###}")); } } internal static class PetGearTable { private static readonly TableLoader _loader = new TableLoader("PetGear.json", "[GEAR]", "gear entry", "gear entries", (Func, Dictionary>)PetGear.Parse, (Func, Dictionary, Dictionary>)PetGear.Merge, "replaces per-item", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[GEAR]", Validate); internal static Dictionary Table => _axis.Table; internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { TableValidator.CheckItemTable("[GEAR]", "PetGear.json", "entry", ItemRefs(), out var _, out var misses); Plugin.Log.LogMessage((object)string.Format("[GEAR] boot check: {0} gear entr{1}, {2} unresolved item key(s).", Table.Count, (Table.Count == 1) ? "y" : "ies", misses)); } private static IEnumerable ItemRefs() { foreach (GearEntry value in Table.Values) { yield return new ItemRef(value.ItemId, value.ItemKey, $"'{value.ItemKey}': ItemID {value.ItemId}", "'" + value.ItemKey + "':"); } } } internal static class PetGifting { internal static double LastRoll = -1.0; internal static int LastRollLoyalty = -1; internal static void Cast(Character player) { if ((Object)(object)player == (Object)null) { return; } if (!Plugin.EnableGiftSkill.Value) { Plugin.Log.LogMessage((object)"[GIFT] Pet Gift cast — [Gifts] EnableGiftSkill is off, nothing happens."); return; } Pet pet = Plugin.Instance?.ActivePet; if (pet == null) { Notify.Player(player, "You have no companion to receive a gift from."); RefundCooldown(player); return; } string text = pet.SpeciesId ?? "Your companion"; PetGiftEntry val = GiftTable.Resolve(pet.SpeciesId); if (val == null) { Notify.Player(player, text + " has nothing to give."); Plugin.Log.LogMessage((object)("[GIFT] '" + text + "' has no PetGifts.json entry — nothing to give (cooldown stands).")); return; } PetSimulation sim = pet.Sim; int num = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue) ?? Plugin.InitialLoyalty.Value; double num2 = (LastRoll = Random.value); LastRollLoyalty = num; GiftDrop drop = PetGifts.Roll(val, num, num2); GiftRollContext ctx = new GiftRollContext { Pet = pet, Player = player, Species = text, Entry = val, Drop = drop, Loyalty = num, Roll = num2 }; SpeciesRegistry.For(pet.SpeciesId).OnGiftRolled(in ctx); } internal static void Give(Character player, string species, GiftDrop drop, int loyalty, double roll) { int itemId = drop.ItemId.GetValueOrDefault(); if (itemId == 0 && !ItemNameIndex.TryResolveCatalog(drop.Key, out itemId, out var _)) { Notify.Player(player, species + " fumbles the gift — no harm done."); Plugin.Log.LogWarning((object)("[GIFT] gift item '" + drop.Key + "' did not resolve to an ItemID — fix PetGifts.json (the [GIFT] boot check flags this too).")); RefundCooldown(player, "gift item unresolved (table bug, not a wasted roll)"); return; } string text = ""; if (FeatherFletchSetup.TryTierSwap(itemId, loyalty, out var tierId, out var quality)) { itemId = tierId; text = $" Feather quality '{quality.Quality}' (+{quality.Percent}%) for loyalty {loyalty}."; } Bag equippedBag = player.Inventory.EquippedBag; ItemContainer pouch = player.Inventory.Pouch; ItemContainer val = (ItemContainer)(object)(((Object)(object)equippedBag != (Object)null) ? equippedBag.Container : null); int num = StackScore(pouch, itemId); int num2 = (((Object)(object)val != (Object)null) ? StackScore(val, itemId) : 0); ItemContainer val2; string text2; if (num == 0 && num2 == 0) { if ((Object)(object)val != (Object)null) { val2 = val; text2 = "backpack (default — no existing stack)"; } else { val2 = pouch; text2 = "pouch (default — no backpack)"; } } else if (num2 > num) { val2 = val; text2 = "backpack (existing stack)"; } else { val2 = pouch; text2 = "pouch (existing stack)"; } Transform transform = ((Component)val2).transform; Item val3 = null; int num3 = 0; for (int i = 0; i < drop.Qty; i++) { Item val4 = ItemManager.Instance.GenerateItemNetwork(itemId); if ((Object)(object)val4 == (Object)null) { break; } val4.ChangeParent(transform); if ((Object)(object)val3 == (Object)null) { val3 = val4; } num3++; } if (num3 == 0) { Notify.Player(player, species + " fumbles the gift — no harm done."); Plugin.Log.LogWarning((object)$"[GIFT] GenerateItemNetwork({itemId}) returned null — nothing given."); RefundCooldown(player, "engine returned no item (not a wasted roll)"); return; } string text3 = ((num3 > 1) ? (num3 + "x ") : ""); Notify.Player(player, species + " brings you " + text3 + val3.Name + "!"); Plugin.Log.LogMessage((object)($"[GIFT] '{species}' gave {num3}x '{val3.Name}' (ItemID={itemId}, " + $"roll {roll:0.000} at loyalty {loyalty}, dest={text2}).{text}")); } private static int StackScore(ItemContainer container, int itemId) { if ((Object)(object)container == (Object)null) { return 0; } List itemsFromID = container.GetItemsFromID(itemId); if (itemsFromID == null || itemsFromID.Count == 0) { return 0; } foreach (Item item in itemsFromID) { if ((Object)(object)item != (Object)null && item.IsStackable && item.RemainingAmount < item.MaxStackAmount) { return 2; } } return 1; } internal static void SyncCooldown(Character player) { int num = SkillCooldowns.Sync(87004, player, Plugin.GiftCooldownSeconds.Value); if (num > 0) { Plugin.Log.LogMessage((object)($"[GIFT] cooldown stamped to {Mathf.Max(0f, Plugin.GiftCooldownSeconds.Value):0}s on {num} object(s) " + "([Gifts] GiftCooldownSeconds).")); } } private static void RefundCooldown(Character player, string reason = "no-companion cast") { SkillCooldowns.RefundNextFrame(87004, player, "[GIFT]", reason); } internal static void Dump(Plugin plugin) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item obj = ((instance != null) ? instance.GetItemPrefab(87004) : null); Skill val = (Skill)(object)((obj is Skill) ? obj : null); Plugin.Log.LogMessage((object)($"[GIFTDUMP] {GiftTable.Table.Count} species entr(ies); " + $"EnableGiftSkill={Plugin.EnableGiftSkill.Value}, " + $"GiftCooldownSeconds={Plugin.GiftCooldownSeconds.Value:0} " + "(live prefab Cooldown=" + (((Object)(object)val != (Object)null) ? val.Cooldown.ToString("0") : "n/a") + ").")); Pet activePet = plugin.ActivePet; string text = activePet?.SpeciesId; int? obj2; if (activePet == null) { obj2 = null; } else { PetSimulation sim = activePet.Sim; obj2 = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue); } int num = obj2 ?? (-1); foreach (PetGiftEntry value in GiftTable.Table.Values) { Plugin.Log.LogMessage((object)("[GIFTDUMP] '" + value.Species + "': default " + Describe(value.Default) + "; " + $"nothing {value.NothingChance:0.###}->{value.NothingChanceAt100:0.###}.")); foreach (GiftDrop drop in value.Drops) { Plugin.Log.LogMessage((object)$"[GIFTDUMP] drop {Describe(drop)} chance {drop.Chance:0.###}->{drop.ChanceAt100:0.###}."); } GiftMix mix = PetGifts.EffectiveMix(value, 0); GiftMix mix2 = PetGifts.EffectiveMix(value, 100); Plugin.Log.LogMessage((object)("[GIFTDUMP] mix at loyalty 0: " + MixLine(value, mix))); Plugin.Log.LogMessage((object)("[GIFTDUMP] mix at loyalty 100: " + MixLine(value, mix2))); if (text != null && GiftTable.Resolve(text) == value && num >= 0) { Plugin.Log.LogMessage((object)($"[GIFTDUMP] mix at loyalty {num,3} (ACTIVE PET): " + MixLine(value, PetGifts.EffectiveMix(value, num)))); } int? num2 = PetGifts.DefaultZeroLoyalty(value); if (num2.HasValue) { Plugin.Log.LogMessage((object)$"[GIFTDUMP] default '{value.Default.Key}' hits 0% at loyalty >= {num2.Value}."); } } Plugin.Log.LogMessage((object)((LastRoll >= 0.0) ? $"[GIFTDUMP] last roll: {LastRoll:0.000} at loyalty {LastRollLoyalty}." : "[GIFTDUMP] no roll yet this session.")); } private static string Describe(GiftDrop d) { int itemId = d.ItemId.GetValueOrDefault(); string via = "table"; if (itemId == 0 && !ItemNameIndex.TryResolveCatalog(d.Key, out itemId, out via)) { itemId = 0; via = "UNRESOLVED"; } string text = ((d.Qty > 1) ? $" x{d.Qty}" : ""); return "'" + d.Key + "' (id " + ((itemId != 0) ? itemId.ToString() : "?") + ", " + via + ")" + text; } private static string MixLine(PetGiftEntry entry, GiftMix mix) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < entry.Drops.Count; i++) { stringBuilder.Append($"'{entry.Drops[i].Key}' {mix.DropChances[i]:0.###}, "); } stringBuilder.Append($"default {mix.DefaultChance:0.###}, nothing {mix.NothingChance:0.###}"); return stringBuilder.ToString(); } } internal static class PetHealthView { internal static PetHealthReading Resolve(Plugin plugin) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) CompanionAnchor val = ((Companion)(plugin?.ActivePet?)).Anchor; float num = 0f; float num2 = 0f; bool flag = val != null && val.TryGetHealth(ref num, ref num2); float current; float max; bool flag2 = PetProxyClient.TryGetMirroredHealth(out current, out max); return PetHealthProvider.Resolve(flag, num, num2, flag2, current, max); } } internal class PetHud : MonoBehaviour { private static Texture2D _fill; private static Texture2D _back; private string _label = string.Empty; private int _lastCurrentRounded = int.MinValue; private int _lastMaxRounded = int.MinValue; private void OnGUI() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Invalid comparison between Unknown and I4 //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_0037: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) if ((int)Event.current.type != 7 || !Plugin.EnableHealthHud.Value) { return; } PetHealthReading val = PetHealthView.Resolve(Plugin.Instance); if (((PetHealthReading)(ref val)).Known) { float current = val.Current; float max = val.Max; if ((Object)(object)_fill == (Object)null) { _fill = SolidTexture(new Color(0.75f, 0.15f, 0.15f, 0.9f)); } if ((Object)(object)_back == (Object)null) { _back = SolidTexture(new Color(0f, 0f, 0f, 0.55f)); } float num = 16f; float num2 = 34f; float num3 = Mathf.Clamp01(current / max); int num4 = Mathf.RoundToInt(current); int num5 = Mathf.RoundToInt(max); if (num4 != _lastCurrentRounded || num5 != _lastMaxRounded) { _lastCurrentRounded = num4; _lastMaxRounded = num5; _label = $"{current:F0} / {max:F0}"; } GUI.DrawTexture(new Rect(num, num2, 220f, 22f), (Texture)(object)_back); GUI.DrawTexture(new Rect(num, num2, 220f * num3, 22f), (Texture)(object)_fill); GUI.Label(new Rect(num, num2 - 18f, 220f, 18f), "Pet"); GUI.Label(new Rect(num, num2 + 2f, 220f, 22f), _label); } } private static Texture2D SolidTexture(Color c) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown //IL_000b: Unknown result type (might be due to invalid IL or missing references) Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, c); val.Apply(); return val; } } internal sealed class PetLifecycle { private readonly Plugin _plugin; internal Pet _activePet; internal bool _petIsGhost; internal readonly BodyAcquisition Acquisition; internal readonly Dictionary _sessionYawOverrides = new Dictionary(StringComparer.OrdinalIgnoreCase); internal string LastBodyLossReason = ""; private double _releaseArmedAt = double.NegativeInfinity; private Pet _releaseArmedPet; private static Character LocalPlayer => Plugin.LocalPlayerCharacter; private static ManualLogSource Log => Plugin.Log; internal PetLifecycle(Plugin plugin) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Expected O, but got Unknown _plugin = plugin; BodyAcquisitionSpec val = new BodyAcquisitionSpec(); val.Host = Plugin.CkHost; val.Runner = (MonoBehaviour)(object)plugin; val.Noun = "pet"; val.Live = () => (Companion)(object)_activePet; val.Player = () => LocalPlayer; val.SpeciesId = () => _activePet?.State?.SpeciesId; val.IsGhost = () => _petIsGhost; val.OnBodyBuilt = delegate(CompanionBody body, bool isGhost) { SwapBody(body, isGhost); }; val.TryBuildNearby = TryBuildFromNearbyWild; val.UseTemplateCache = true; val.UseDonorHarvest = true; val.UseGhostStandIn = true; val.RangedCaptureFilter = SpecialAttacks.RangedCaptureFilter; val.RangedCaptureSkillId = SpecialAttacks.RangedCaptureSkillId; Acquisition = new BodyAcquisition(val); } internal void Tame() { Character localPlayer = LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { Log.LogWarning((object)"[PET] no local player."); return; } Character val = BodyFactory.FindNearestCharacter(localPlayer, Plugin.TameRange.Value, (Func)BodyFactory.IsWildTamable); if ((Object)(object)val == (Object)null) { Log.LogMessage((object)"[PET] no wild tamable creature nearby to tame."); } else { TameSpecific(val); } } internal bool TameSpecific(Character src, Character actor = null) { //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Expected O, but got Unknown //IL_01ea: Unknown result type (might be due to invalid IL or missing references) if (!BodyFactory.IsWildTamable(src)) { Character val = actor ?? LocalPlayer; if ((Object)(object)val != (Object)null) { Notify.Player(val, "That creature cannot be tamed."); } Log.LogWarning((object)("[TAME] tame REFUSED: '" + (((Object)(object)src != (Object)null) ? src.Name : "?") + "' is not a wild tamable (" + BodyFactory.WhyNotWildTamable(src) + ") — shared ladder, all machines (session-3 CKA1 finding).")); return false; } if (UntameableSpecies.IsBlocked(((Object)(object)src != (Object)null) ? src.Name : null)) { Character val2 = actor ?? LocalPlayer; if ((Object)(object)val2 != (Object)null) { Notify.Player(val2, "That creature will never bond with you."); } Log.LogWarning((object)("[TAME] tame REFUSED: '" + (((Object)(object)src != (Object)null) ? src.Name : "?") + "' is untameable (it is on the untameable-species list) — exact-name blocklist, all tame routes (Cobalt's ruling).")); return false; } if (PhotonNetwork.isNonMasterClientInRoom) { return GuestTame.TameAsGuest(this, src, actor); } Character val3 = actor ?? LocalPlayer; if ((Object)(object)val3 == (Object)null || (Object)(object)src == (Object)null) { return false; } if (PhotonNetwork.inRoom && PhotonNetwork.room != null && PhotonNetwork.room.PlayerCount > 1 && !BwConfig.MP.AllowConsumeTameInRoom.Value) { Notify.Player(val3, "Taming is disabled while others are in your world — the wild one would linger as a ghost for them."); Log.LogWarning((object)"[TAME] consume-tame REFUSED — in a room with other players, and the consumed creature's mirror would freeze as an immortal statue on every guest (health-review 2.5b). Set [MP] AllowConsumeTameInRoom=true to override."); return false; } CompanionBody val4 = BodyFactory.BuildPuppet(src, val3, true, SpecialAttacks.RangedCaptureFilter(src.Name), SpecialAttacks.RangedCaptureSkillId(src.Name), (CreatureAttributes)null, Plugin.CkHost); if ((Object)(object)val4 == (Object)null) { return false; } PetSave val5 = new PetSave { LoyaltyValue = Plugin.InitialLoyalty.Value, Attributes = val4.CapturedStats }; SeedTameHealthRecovery(val5); SetPet(val4, val5, val3, src.Name); _plugin.Persistence.SaveNow(val3); Log.LogMessage((object)$"[PET] tamed '{val5.SpeciesId}' (loyalty {val5.LoyaltyValue}, owner '{UID.op_Implicit(val3.UID)}')."); return true; } internal bool TameSpecificNonConsume(Character src, Character actor = null) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown //IL_00b3: Unknown result type (might be due to invalid IL or missing references) Character val = actor ?? LocalPlayer; if ((Object)(object)val == (Object)null || (Object)(object)src == (Object)null) { return false; } CompanionBody val2 = BodyFactory.BuildPuppet(src, val, false, SpecialAttacks.RangedCaptureFilter(src.Name), SpecialAttacks.RangedCaptureSkillId(src.Name), (CreatureAttributes)null, Plugin.CkHost); if ((Object)(object)val2 == (Object)null) { return false; } PetSave val3 = new PetSave { LoyaltyValue = Plugin.InitialLoyalty.Value, Attributes = val2.CapturedStats }; SeedTameHealthRecovery(val3); SetPet(val2, val3, val, src.Name); _plugin.Persistence.SaveNow(val); Log.LogMessage((object)$"[PET] tamed '{val3.SpeciesId}' (loyalty {val3.LoyaltyValue}, non-consume, owner '{UID.op_Implicit(val.UID)}')."); return true; } internal void TameCached(string[] parts) { //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Expected O, but got Unknown Character localPlayer = LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { Log.LogWarning((object)"[PET] no local player."); return; } if (parts.Length < 2) { Log.LogMessage((object)"[PET] usage: tamecached — the cache census:"); Log.LogMessage((object)BodyTemplateCache.Dump()); return; } string text = string.Join(" ", parts, 1, parts.Length - 1); if (UntameableSpecies.IsBlocked(text)) { Notify.Player(localPlayer, "That creature will never bond with you."); Log.LogWarning((object)("[TAME] tamecached REFUSED: '" + text + "' is untameable (it is on the untameable-species list) — exact-name blocklist (Cobalt's ruling).")); return; } BodyTemplate val = default(BodyTemplate); if (!BodyTemplateCache.TryResolve(text, ref val)) { Log.LogWarning((object)("[PET] no cached body template matches '" + text + "' — run an expedition first ('expedition' shows the census).")); return; } if (UntameableSpecies.IsBlocked(val.SpeciesId)) { Notify.Player(localPlayer, "That creature will never bond with you."); Log.LogWarning((object)("[TAME] tamecached REFUSED: the cache resolved '" + text + "' to '" + val.SpeciesId + "', which is untameable (it is on the untameable-species list).")); return; } CompanionBody val2 = BodyTemplateCache.PuppetFrom(val, localPlayer, SpecialAttacks.RangedCaptureFilter(val.SpeciesId), SpecialAttacks.RangedCaptureSkillId(val.SpeciesId)); if ((Object)(object)val2 == (Object)null) { Log.LogWarning((object)("[PET] cache build for '" + val.SpeciesId + "' failed — see the [TEMPLATE] lines above.")); return; } PetSave val3 = new PetSave { LoyaltyValue = Plugin.InitialLoyalty.Value, Attributes = val2.CapturedStats }; SeedTameHealthRecovery(val3); SetPet(val2, val3, null, val.SpeciesId); _plugin.Persistence.SaveNow(localPlayer); Log.LogMessage((object)$"[PET] tamed '{val3.SpeciesId}' from the session template cache (loyalty {val3.LoyaltyValue})."); } private static void SeedTameHealthRecovery(PetSave state) { if (Plugin.EnableFoodHealthRecovery.Value) { state.HealthRecoveryLevel = 5; state.HealthRecoverySecondsLeft = 600.0; } } internal Pet NewPet(PetSave state, string ownerUid) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown Pet pet = new Pet { State = state, OwnerUid = ownerUid, Sim = new PetSimulation(state, Plugin.Tuning, PetDietTable.HungerFor(state?.SpeciesId)) }; ((Companion)pet).Anchor.OnAnchorDeath = OnAnchorDied; ((Companion)pet).Anchor.OnAnchorCriticallyHurt = OnAnchorCriticallyHurt; return pet; } internal static string StandInSavedIdentityError(PetSave saved, string ownerUid) { if (saved == null || !SpeciesResolve.IsStandInBodyName(saved.SpeciesId)) { return null; } return "REFUSED to load the saved pet: its species cell is the stand-in body name '" + saved.SpeciesId + "' (bug 1b escaped into a save written by an older build). The file is KEPT untouched. Fix: edit BepInEx/config/bw_pets_" + ownerUid + ".txt's species cell to the real creature name (e.g. 'Hyena'), or release and re-tame."; } internal void SetPet(CompanionBody body, PetSave state, Character owner = null, string tameSpeciesId = null) { //IL_00c7: Unknown result type (might be due to invalid IL or missing references) if (state != null) { string text = ((!string.IsNullOrEmpty(tameSpeciesId)) ? tameSpeciesId : state.SpeciesId); if (SpeciesResolve.IsStandInBodyName(text)) { Log.LogWarning((object)("[LIFECYCLE] REFUSED to persist a stand-in body name '" + text + "' as the pet's identity (bug 1b tripwire) — keeping '" + state.SpeciesId + "'. State.SpeciesId is never a stand-in name.")); text = (SpeciesResolve.IsStandInBodyName(state.SpeciesId) ? null : state.SpeciesId); } if (!string.IsNullOrEmpty(text)) { state.SpeciesId = text; state.Name = text; } } if (_activePet != null) { PetPassiveBuff.Clear(); PetBagPerk.Clear(); ClearReleaseArm(); ((Companion)_activePet).Despawn(); } Character val = owner ?? LocalPlayer; _activePet = NewPet(state, ((Object)(object)val != (Object)null) ? UID.op_Implicit(val.UID) : null); _petIsGhost = false; SkillRegistry.SyncIcons(LocalPlayer); AttachBodyExtras(body); _plugin.SimTicker.Prime(); } internal void SwapBody(CompanionBody body, bool isGhost) { if (_activePet == null || (Object)(object)body == (Object)null) { if ((Object)(object)body != (Object)null) { Object.Destroy((Object)(object)((Component)body).gameObject); Log.LogMessage((object)"[LIFECYCLE] SwapBody with no active pet: destroying the offered body."); } return; } if ((Object)(object)((Companion)_activePet).Body != (Object)null) { Object.Destroy((Object)(object)((Component)((Companion)_activePet).Body).gameObject); } _petIsGhost = isGhost; if (isGhost && _activePet.State != null && !string.IsNullOrEmpty(_activePet.State.SpeciesId) && body.SpeciesId != _activePet.State.SpeciesId) { Log.LogMessage((object)("[LIFECYCLE] ghost stand-in: preserving the bonded species '" + _activePet.State.SpeciesId + "' on the stand-in body (was '" + body.SpeciesId + "') — the ghost must not overwrite the pet's identity.")); body.SpeciesId = _activePet.State.SpeciesId; } if (!isGhost && _activePet.State != null) { string speciesId = _activePet.State.SpeciesId; bool flag = body.CapturedStats != null; bool flag2 = _activePet.State.Attributes != null; if (SpeciesResolve.IsStandInBodyName(speciesId)) { Log.LogWarning((object)("[LIFECYCLE] SwapBody: the pet's saved identity is a stand-in name '" + speciesId + "' (bug 1b tripwire) — NOT adopting species stats against a corrupt identity.")); } else if (StatAdoption.ShouldAdopt(speciesId, body.SpeciesId, flag, flag2)) { ((Companion)_activePet).CapturedStats = body.CapturedStats; _plugin.Persistence.SaveNow(LocalPlayer); Log.LogMessage((object)("[STATS] adopted species stats from the re-form source for '" + body.SpeciesId + "' (v1-save migration).")); } else if (flag && !flag2 && !StatAdoption.IsSameSpecies(speciesId, body.SpeciesId)) { Log.LogWarning((object)("[STATS] NOT adopting stats: this pet is a '" + speciesId + "', but its body was built from a '" + body.SpeciesId + "' (a near-miss donor). Writing those stats would permanently record the WRONG creature in your save (Bug 31). Falling back to the config stat path until a true '" + speciesId + "' body is found.")); } } AttachBodyExtras(body); } private void AttachBodyExtras(CompanionBody body) { //IL_00fb: Unknown result type (might be due to invalid IL or missing references) if (float.IsNaN(body.YawOffset)) { float num = SpeciesYaw.Resolve(YawTable.Pairs, Plugin.CfgYawPairs, (IReadOnlyDictionary)_sessionYawOverrides, body.SpeciesId); if (!float.IsNaN(num)) { body.YawOffset = num; string arg = (_sessionYawOverrides.ContainsKey(body.SpeciesId) ? "session yaw tune" : ((!float.IsNaN(SpeciesYaw.For(Plugin.CfgYawPairs, body.SpeciesId))) ? "SpeciesYawOffsets cfg" : "embedded yaw table")); Log.LogMessage((object)$"[PET] species yaw override for '{body.SpeciesId}': {num:F0}° ({arg})."); } } CompanionCombat val = ((Companion)_activePet).AdoptBody(body, Plugin.EnableCombat.Value, false); if ((Object)(object)val != (Object)null) { val.OnEnemyDefeated = OnPetDefeatedEnemy; if (Plugin.EnableSpecialAttack.Value) { ((Component)body).gameObject.AddComponent().InjectCombat(val); } } PetSystems.ApplyEffects(_activePet, _activePet.Sim.Status()); _plugin.AnchorUpkeep(); } private PetStatus HandleLoyaltyEvent(LoyaltyEvent evt) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: 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_0043: Unknown result type (might be due to invalid IL or missing references) PetGearSense.Apply(_activePet, LocalPlayer); PetStatus val = _activePet.Sim.OnLoyaltyEvent(evt); PetSystems.ApplyEffects(_activePet, val); _plugin.Persistence.SaveNow(LocalPlayer); return val; } internal void OnPetDefeatedEnemy() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0032: 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) if (_activePet?.Sim != null) { PetStatus val = HandleLoyaltyEvent((LoyaltyEvent)5); Log.LogMessage((object)($"[PET] defeated a nearby enemy -> loyalty {val.LoyaltyValue} ({val.Loyalty}), " + $"{_activePet.Sim.LoyaltyGainCarry:0.###} banked toward the next point.")); } } internal void OnAnchorDied() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) if (_activePet == null) { return; } PetStatus val = _activePet.Sim.OnLoyaltyEvent((LoyaltyEvent)4); if (val.Abandoned) { Log.LogMessage((object)"[PET] the pet was DOWNED and the bond broke (loyalty 0) — it abandons you for good."); DespawnPet(LocalPlayer); LastBodyLossReason = "anchor-died bond-broken"; return; } LastBodyLossReason = "anchor-died body-retreat"; _activePet.State.HealthFraction = 1.0; _plugin.Persistence.SaveNow(LocalPlayer); Log.LogMessage((object)$"[PET] the pet was DOWNED -> loyalty {val.LoyaltyValue} ({val.Loyalty}); it retreats and re-forms in ~{Plugin.AnchorRespawnSeconds.Value:F0}s."); if ((Object)(object)((Companion)_activePet).Body != (Object)null) { Object.Destroy((Object)(object)((Component)((Companion)_activePet).Body).gameObject); ((Companion)_activePet).Body = null; } ((MonoBehaviour)_plugin).StartCoroutine(DownedRecovery(_activePet)); } private IEnumerator DownedRecovery(Pet pet) { yield return (object)new WaitForSeconds(Plugin.AnchorRespawnSeconds.Value); if (_activePet == pet && (Object)(object)((Companion)pet).Body == (Object)null) { Acquisition.Kick(); } } internal void OnAnchorCriticallyHurt() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0032: 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) if (_activePet?.Sim != null) { PetStatus val = HandleLoyaltyEvent((LoyaltyEvent)3); Log.LogMessage((object)$"[PET] pet critically hurt -> loyalty {val.LoyaltyValue} ({val.Loyalty})."); } } internal void OnRegionTravelArrival(Character player, string fromScene, string toScene) { //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_01ad: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) if (!BwConfig.Skills.EnableWildUnknownGate.Value) { Log.LogMessage((object)"[TRAVEL] arrival: EnableWildUnknownGate is off — no effect."); return; } if (_activePet?.State == null) { Log.LogMessage((object)("[TRAVEL] arrival '" + fromScene + "' -> '" + toScene + "': no pet bond — nothing to gate.")); return; } string text = Regions.RegionOf(fromScene) ?? fromScene; string text2 = Regions.RegionOf(toScene) ?? toScene; string text3 = (string.IsNullOrEmpty(_activePet.SpeciesId) ? "Your companion" : _activePet.SpeciesId); if (string.Equals(text, text2, StringComparison.OrdinalIgnoreCase)) { Log.LogMessage((object)("[TRAVEL] arrival '" + fromScene + "' -> '" + toScene + "': same region ('" + text2 + "') — no crossing.")); return; } if (!WildUnknown.Learned(player)) { Log.LogMessage((object)("[TRAVEL] arrival '" + text + "' -> '" + text2 + "': Wild Unknown not learned — bond with '" + text3 + "' BROKEN.")); Notify.Player(player, text3 + " could not follow you — the bond is broken."); DespawnPet(player); return; } bool flag = RegionCrossings.TryRecord(_activePet.State.CrossedRegionPairs, text, text2); RegionCrossOutcome val = PetRules.OnRegionCross(true, flag); if (((RegionCrossOutcome)(ref val)).LoyaltyDelta != 0) { PetStatus val2 = HandleLoyaltyEvent((LoyaltyEvent)6); Log.LogMessage((object)$"[TRAVEL] arrival '{text}' -> '{text2}': first crossing of this pair -> loyalty {val2.LoyaltyValue} ({val2.Loyalty})."); Notify.Player(player, text3 + " follows you into the unknown. (+5 loyalty)"); } else { _plugin.Persistence.SaveNow(player); Log.LogMessage((object)("[TRAVEL] arrival '" + text + "' -> '" + text2 + "': pair already crossed — no bonus.")); } } private void ClearReleaseArm() { _releaseArmedAt = double.NegativeInfinity; _releaseArmedPet = null; } internal void ReleasePet(bool confirmed = false) { //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Invalid comparison between Unknown and I4 Character localPlayer = LocalPlayer; if (_activePet == null) { Notify.Player(localPlayer, "You have no companion to release."); return; } string text = (string.IsNullOrEmpty(_activePet.SpeciesId) ? "Your companion" : _activePet.SpeciesId); int num = _activePet.State?.LoyaltyValue ?? (-1); double num2 = Time.unscaledTime; double num3 = ((_releaseArmedPet == _activePet) ? _releaseArmedAt : double.NegativeInfinity); bool flag = false; ReleaseRuling val = (ReleaseRuling)((!confirmed) ? ((int)ReleaseGate.Evaluate(num, Plugin.ReleaseConfirmLoyaltyThreshold.Value, num3, num2, ref flag, 30.0, 1.0)) : 0); if (ReleaseGate.Holds(val)) { if (flag) { _releaseArmedAt = num2; _releaseArmedPet = _activePet; } int num4 = 30; string arg = (((int)val == 2) ? "the earlier confirmation expired" : (flag ? "confirmation required" : $"too soon — a confirmation must be a SECOND decision (wait {1.0:0.#}s)")); Log.LogWarning((object)($"[PET] release of '{text}' (loyalty {num}) HELD — {arg}" + (flag ? $"; re-arming {num4}s." : "; the open window still stands.") + " Cast Release Pet again, or run 'release confirm'.")); Notify.Player(localPlayer, $"{text} looks at you, uncertain — release again within {num4}s to let it go for good."); } else { ClearReleaseArm(); DespawnPet(localPlayer); Log.LogMessage((object)$"[PET] released '{text}' (loyalty was {num}) — bond ended, save cleared."); Notify.Player(localPlayer, text + " returns to the wild."); } } internal void DespawnPet(Character player) { TeardownLivePet(player); LastBodyLossReason = "deliberate despawn/release"; if ((Object)(object)player != (Object)null) { PetSaveStore.Clear(player); } } internal void TeardownLivePet(Character player) { LastBodyLossReason = "session teardown"; ClearReleaseArm(); BraceDriver.ForceExit("pet teardown"); TauntController.Release("pet teardown"); PetPassiveBuff.Clear(); PetBagPerk.Clear(); PetStatusEffects.Clear(player); PetGearSense.Reset(); PetTemperatureDriver.Reset(); Pet activePet = _activePet; if (activePet != null) { ((Companion)activePet).Despawn(); } _activePet = null; _petIsGhost = false; _plugin.Persistence._lastSavedLoyalty = int.MinValue; } internal void RecallPet() { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) Character localPlayer = LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { Log.LogMessage((object)"[RECALL] no player."); } else if (_activePet == null) { Log.LogMessage((object)"[RECALL] no active pet."); } else if ((Object)(object)((Companion)_activePet).Body != (Object)null) { ((Companion)_activePet).Body.PlaceAt(ClearSpotForward(localPlayer, 1.5f)); if (((Companion)_activePet).Anchor.HasLiveAnchor) { ((Companion)_activePet).Anchor.Current.Teleport(ClearSpotForward(localPlayer, 0.5f), Quaternion.identity); } Log.LogMessage((object)"[RECALL] pet re-placed at the player."); } else if (!Acquisition.Active) { Log.LogMessage((object)"[RECALL] pet has no body — triggering re-form now."); Acquisition.Kick(); } else { Log.LogMessage((object)"[RECALL] re-form already in progress."); } } private static Vector3 ClearSpotForward(Character player, float forward) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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_002d: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //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) Vector3 val = ((Component)player).transform.position + Vector3.up * 1.2f; Vector3 forward2 = ((Component)player).transform.forward; RaycastHit val2 = default(RaycastHit); if (Physics.Raycast(val, forward2, ref val2, forward, Global.LargeEnvironmentMask)) { forward = Mathf.Max(0f, ((RaycastHit)(ref val2)).distance - 0.5f); } return ((Component)player).transform.position + forward2 * forward; } internal void Harvest(string sceneName, string creatureName) { Character localPlayer = LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { Log.LogWarning((object)"[HARVEST] no local player."); return; } ((MonoBehaviour)_plugin).StartCoroutine(DonorHarvest.Harvest(sceneName, creatureName, localPlayer, (Action)delegate(CompanionBody body) { if (!((Object)(object)body == (Object)null)) { if (_activePet != null) { SwapBody(body, isGhost: false); } else { Log.LogMessage((object)"[HARVEST] no active pet to attach the harvested body to (clone proven; destroying it — diagnostic only)."); Object.Destroy((Object)(object)((Component)body).gameObject); } } }, (string)null, 0)); } private CompanionBody TryBuildFromNearbyWild(Character player) { Pet activePet = _activePet; if (activePet?.State == null) { return null; } Character val = BodyFactory.FindNearest(player, Plugin.TameRange.Value, activePet.State.SpeciesId); if ((Object)(object)val == (Object)null) { return null; } Log.LogMessage((object)("[PERSIST] re-forming the real '" + activePet.State.SpeciesId + "' from a nearby wild one.")); CompanionBody val2 = BodyFactory.BuildPuppet(val, player, false, SpecialAttacks.RangedCaptureFilter(val.Name), SpecialAttacks.RangedCaptureSkillId(val.Name), (CreatureAttributes)null, Plugin.CkHost); if ((Object)(object)val2 != (Object)null) { return val2; } Log.LogMessage((object)("[PERSIST] the wild '" + activePet.State.SpeciesId + "' build FAILED (source died mid-clone?) — falling through to the cache/harvest/ghost ladder.")); return null; } } internal class PetMenuPanel : MenuPanelBase { private static readonly Color Parchment = new Color(0.906f, 0.874f, 0.78f); private static readonly Color MutedCol = new Color(0.615f, 0.576f, 0.51f); private static readonly Color Gold = new Color(0.78f, 0.643f, 0.29f); private static readonly Color GoodCol = new Color(0.565f, 0.729f, 0.408f); private static readonly Color WarnCol = new Color(0.839f, 0.655f, 0.235f); private static readonly Color BadCol = new Color(0.784f, 0.376f, 0.322f); private static readonly Color BoxBg = new Color(0.078f, 0.067f, 0.055f, 0.94f); private static readonly Color DimBg = new Color(0f, 0f, 0f, 0.45f); private bool _built; private Font _font; private Text _headerText; private Text _subText; private Text _emptyText; private RectTransform _leftCol; private RectTransform _rightCol; private ScrollRect _leftScroll; private ScrollRect _rightScroll; private Image _leftScrollBg; private Image _rightScrollBg; private int _activeColumn; private string _sig; private float _lastRefresh; public override void Show() { EnsureBuilt(); ((MenuPanel)this).Show(); if ((Object)(object)((MenuPanel)this).m_parentPanelHolder == (Object)null) { Plugin.Log.LogWarning((object)"[PETTAB] Show() with no parent holder — window will auto-close."); } _activeColumn = -1; SetActiveColumn(0); _sig = null; Refresh(); } private void Update() { ((Panel)this).Update(); if (_built && ((UIElement)this).IsDisplayed) { PollControllerNav(); if (!(Time.unscaledTime - _lastRefresh < 0.5f)) { _lastRefresh = Time.unscaledTime; Refresh(); } } } private void PollControllerNav() { if (!BwConfig.PetPanel.EnableControllerNav.Value) { return; } Player val = (((Object)(object)((UIElement)this).m_characterUI != (Object)null) ? ReInput.players.GetPlayer(((UIElement)this).m_characterUI.RewiredID) : null); if (val == null) { return; } foreach (Joystick joystick in val.controllers.Joysticks) { IGamepadTemplate template = ((Controller)joystick).GetTemplate(); if (template == null) { continue; } if (template.leftTrigger.AsButton.justPressed) { SetActiveColumn(0); } if (template.rightTrigger.AsButton.justPressed) { SetActiveColumn(1); } float value = template.rightStick.vertical.value; float value2 = BwConfig.PetPanel.StickDeadzone.Value; if (!(Mathf.Abs(value) <= value2)) { ScrollRect val2 = ((_activeColumn == 0) ? _leftScroll : _rightScroll); if (!((Object)(object)val2 == (Object)null)) { float num = value * BwConfig.PetPanel.StickScrollSpeed.Value * Time.unscaledDeltaTime; val2.verticalNormalizedPosition = Mathf.Clamp01(val2.verticalNormalizedPosition + num); break; } } } } private void SetActiveColumn(int column) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) if (_activeColumn != column) { _activeColumn = column; if ((Object)(object)_leftScrollBg != (Object)null) { ((Graphic)_leftScrollBg).color = (Color)((_activeColumn == 0) ? new Color(Gold.r, Gold.g, Gold.b, 0.05f) : Color.clear); } if ((Object)(object)_rightScrollBg != (Object)null) { ((Graphic)_rightScrollBg).color = (Color)((_activeColumn == 1) ? new Color(Gold.r, Gold.g, Gold.b, 0.05f) : Color.clear); } } } private void Refresh() { PetPanelModel val; try { val = PetPanel.Build(PetPanelData.Gather()); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[PETTAB] refresh failed: " + ex.Message)); return; } string text = Signature(val); if (text == _sig) { return; } _sig = text; _headerText.text = (val.HasPet ? val.Header : "Companion"); _subText.text = val.SubHeader; ((Component)_emptyText).gameObject.SetActive(!val.HasPet); _emptyText.text = val.EmptyText; ClearColumn(_leftCol); ClearColumn(_rightCol); if (!val.HasPet) { return; } foreach (PetPanelSection section in val.Sections) { RectTransform col = ((section.Title == "Offense" || section.Title == "Defenses" || section.Title == "Bond gifts") ? _rightCol : _leftCol); AddSection(col, section); } } private static string Signature(PetPanelModel m) { //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected I4, but got Unknown StringBuilder stringBuilder = new StringBuilder(512); stringBuilder.Append(m.HasPet).Append('|').Append(m.Header) .Append('|') .Append(m.SubHeader); foreach (PetPanelSection section in m.Sections) { stringBuilder.Append('#').Append(section.Title); foreach (PetPanelRow row in section.Rows) { stringBuilder.Append('|').Append(row.Label).Append('=') .Append(row.Value) .Append(';') .Append((int)row.Tone) .Append(';') .Append(float.IsNaN(row.Bar) ? "-" : row.Bar.ToString("F2")); } } return stringBuilder.ToString(); } private static void ClearColumn(RectTransform col) { for (int num = ((Transform)col).childCount - 1; num >= 0; num--) { Object.DestroyImmediate((Object)(object)((Component)((Transform)col).GetChild(num)).gameObject); } } private void AddSection(RectTransform col, PetPanelSection s) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Expected O, but got Unknown //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) GameObject val = NewLayoutChild(col, "Title:" + s.Title, 24f); Text val2 = AddText(val.transform, "Text", 15, (TextAnchor)6, Gold, (FontStyle)1); val2.text = s.Title.ToUpperInvariant(); StretchInto(((Graphic)val2).rectTransform, 2f, 0f, -2f, 0f); GameObject val3 = new GameObject("Line", new Type[2] { typeof(RectTransform), typeof(Image) }); val3.transform.SetParent(val.transform, false); RectTransform val4 = (RectTransform)val3.transform; val4.anchorMin = new Vector2(0f, 0f); val4.anchorMax = new Vector2(1f, 0f); val4.offsetMin = new Vector2(0f, 0f); val4.offsetMax = new Vector2(0f, 1f); ((Graphic)val3.GetComponent()).color = new Color(Gold.r, Gold.g, Gold.b, 0.35f); foreach (PetPanelRow row in s.Rows) { AddRow(col, row); } NewLayoutChild(col, "Spacer", 5f); } private void AddRow(RectTransform col, PetPanelRow r) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Expected O, but got Unknown //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Expected O, but got Unknown //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_010e: 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) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Invalid comparison between Unknown and I4 //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Invalid comparison between Unknown and I4 //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_0142: 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: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_01f9: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Unknown result type (might be due to invalid IL or missing references) GameObject val = NewLayoutChild(col, "Row", 20f); if (!float.IsNaN(r.Bar)) { GameObject val2 = new GameObject("BarBack", new Type[2] { typeof(RectTransform), typeof(Image) }); val2.transform.SetParent(val.transform, false); StretchInto((RectTransform)val2.transform, 0f, 1f, 0f, -1f); ((Graphic)val2.GetComponent()).color = new Color(1f, 1f, 1f, 0.05f); GameObject val3 = new GameObject("BarFill", new Type[2] { typeof(RectTransform), typeof(Image) }); val3.transform.SetParent(val2.transform, false); RectTransform val4 = (RectTransform)val3.transform; val4.anchorMin = Vector2.zero; val4.anchorMax = new Vector2(Mathf.Clamp01(r.Bar), 1f); Vector2 offsetMin = (val4.offsetMax = Vector2.zero); val4.offsetMin = offsetMin; Color val5 = ToneColor((PetPanelTone)(((int)r.Tone != 4) ? ((int)r.Tone) : 0)); ((Graphic)val3.GetComponent()).color = new Color(val5.r, val5.g, val5.b, 0.28f); } if (!string.IsNullOrEmpty(r.Label)) { Text val6 = AddText(val.transform, "Label", 14, (TextAnchor)3, ((int)r.Tone == 4) ? MutedCol : Parchment, (FontStyle)0); val6.text = r.Label; StretchInto(((Graphic)val6).rectTransform, 8f, 0f, -8f, 0f); } Text val7 = AddText(val.transform, "Value", 14, (TextAnchor)(string.IsNullOrEmpty(r.Label) ? 3 : 5), ToneColor(r.Tone), (FontStyle)0); val7.text = r.Value; StretchInto(((Graphic)val7).rectTransform, 8f, 0f, -8f, 0f); } private static Color ToneColor(PetPanelTone tone) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected I4, but got Unknown //IL_001a: 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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) return (Color)((tone - 1) switch { 0 => GoodCol, 1 => WarnCol, 2 => BadCol, 3 => MutedCol, _ => Parchment, }); } private void EnsureBuilt() { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected O, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Expected O, but got Unknown //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Expected O, but got Unknown //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Unknown result type (might be due to invalid IL or missing references) //IL_0233: Unknown result type (might be due to invalid IL or missing references) //IL_02bb: Unknown result type (might be due to invalid IL or missing references) //IL_02e4: Unknown result type (might be due to invalid IL or missing references) //IL_02fa: Unknown result type (might be due to invalid IL or missing references) //IL_0308: Unknown result type (might be due to invalid IL or missing references) //IL_030d: Unknown result type (might be due to invalid IL or missing references) //IL_030e: Unknown result type (might be due to invalid IL or missing references) //IL_0315: Unknown result type (might be due to invalid IL or missing references) if (!_built) { _built = true; Text val = (((Object)(object)((UIElement)this).m_characterUI != (Object)null) ? ((UIElement)this).m_characterUI.m_lblSectionName : null); _font = (((Object)(object)val != (Object)null) ? val.font : null); if ((Object)(object)_font == (Object)null) { _font = Resources.GetBuiltinResource("Arial.ttf"); } GameObject val2 = new GameObject("Dim", new Type[2] { typeof(RectTransform), typeof(Image) }); val2.transform.SetParent(((Component)this).transform, false); StretchInto((RectTransform)val2.transform, 0f, 0f, 0f, 0f); ((Graphic)val2.GetComponent()).color = DimBg; GameObject val3 = new GameObject("Box", new Type[2] { typeof(RectTransform), typeof(Image) }); val3.transform.SetParent(((Component)this).transform, false); RectTransform rt = (RectTransform)val3.transform; StretchInto(rt, 0f, 0f, 0f, 0f); ((Graphic)val3.GetComponent()).color = BoxBg; _headerText = AddText(val3.transform, "Header", 26, (TextAnchor)6, Parchment, (FontStyle)0); RectTransform rectTransform = ((Graphic)_headerText).rectTransform; rectTransform.anchorMin = new Vector2(0f, 1f); rectTransform.anchorMax = Vector2.one; rectTransform.offsetMin = new Vector2(30f, -62f); rectTransform.offsetMax = new Vector2(-30f, -18f); _subText = AddText(val3.transform, "SubHeader", 14, (TextAnchor)0, MutedCol, (FontStyle)2); RectTransform rectTransform2 = ((Graphic)_subText).rectTransform; rectTransform2.anchorMin = new Vector2(0f, 1f); rectTransform2.anchorMax = Vector2.one; rectTransform2.offsetMin = new Vector2(31f, -88f); rectTransform2.offsetMax = new Vector2(-30f, -64f); _leftCol = MakeColumn(val3.transform, "LeftColumn", 0f, 0.5f, 30f, -12f, out _leftScroll, out _leftScrollBg); _rightCol = MakeColumn(val3.transform, "RightColumn", 0.5f, 1f, 12f, -30f, out _rightScroll, out _rightScrollBg); _emptyText = AddText(val3.transform, "Empty", 17, (TextAnchor)4, MutedCol, (FontStyle)2); RectTransform rectTransform3 = ((Graphic)_emptyText).rectTransform; rectTransform3.anchorMin = new Vector2(0.1f, 0.2f); rectTransform3.anchorMax = new Vector2(0.9f, 0.8f); Vector2 offsetMin = (rectTransform3.offsetMax = Vector2.zero); rectTransform3.offsetMin = offsetMin; ((Component)_emptyText).gameObject.SetActive(false); Plugin.Log.LogMessage((object)("[PETTAB] panel chrome built (font='" + ((Object)_font).name + "').")); } } private RectTransform MakeColumn(Transform parent, string name, float anchorLeft, float anchorRight, float padLeft, float padRight, out ScrollRect scrollOut, out Image bgOut) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_0061: 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_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Expected O, but got Unknown //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Expected O, but got Unknown //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[4] { typeof(RectTransform), typeof(Image), typeof(RectMask2D), typeof(ScrollRect) }); val.transform.SetParent(parent, false); RectTransform val2 = (RectTransform)val.transform; val2.anchorMin = new Vector2(anchorLeft, 0f); val2.anchorMax = new Vector2(anchorRight, 1f); val2.offsetMin = new Vector2(padLeft, 24f); val2.offsetMax = new Vector2(padRight, -96f); Image component = val.GetComponent(); ((Graphic)component).color = Color.clear; ((Graphic)component).raycastTarget = true; bgOut = component; GameObject val3 = new GameObject("Content", new Type[3] { typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter) }); val3.transform.SetParent(val.transform, false); RectTransform val4 = (RectTransform)val3.transform; val4.anchorMin = new Vector2(0f, 1f); val4.anchorMax = Vector2.one; val4.pivot = new Vector2(0.5f, 1f); Vector2 offsetMin = (val4.offsetMax = Vector2.zero); val4.offsetMin = offsetMin; VerticalLayoutGroup component2 = val3.GetComponent(); ((HorizontalOrVerticalLayoutGroup)component2).spacing = 2f; ((LayoutGroup)component2).childAlignment = (TextAnchor)0; ((HorizontalOrVerticalLayoutGroup)component2).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)component2).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)component2).childForceExpandWidth = true; ((HorizontalOrVerticalLayoutGroup)component2).childForceExpandHeight = false; val3.GetComponent().verticalFit = (FitMode)2; ScrollRect component3 = val.GetComponent(); component3.content = val4; component3.horizontal = false; component3.vertical = true; component3.movementType = (MovementType)2; component3.scrollSensitivity = 25f; component3.verticalScrollbar = MakeScrollbar(val.transform); component3.verticalScrollbarVisibility = (ScrollbarVisibility)1; scrollOut = component3; return val4; } private static Scrollbar MakeScrollbar(Transform scrollHost) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown //IL_005c: 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_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: 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_00f5: Expected O, but got Unknown //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Expected O, but got Unknown //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Scrollbar", new Type[3] { typeof(RectTransform), typeof(Image), typeof(Scrollbar) }); val.transform.SetParent(scrollHost, false); RectTransform val2 = (RectTransform)val.transform; val2.anchorMin = new Vector2(1f, 0f); val2.anchorMax = Vector2.one; val2.pivot = new Vector2(1f, 0.5f); val2.offsetMin = new Vector2(-6f, 0f); val2.offsetMax = Vector2.zero; ((Graphic)val.GetComponent()).color = new Color(1f, 1f, 1f, 0.04f); GameObject val3 = new GameObject("Handle", new Type[2] { typeof(RectTransform), typeof(Image) }); val3.transform.SetParent(val.transform, false); RectTransform val4 = (RectTransform)val3.transform; Vector2 offsetMin = (val4.offsetMax = Vector2.zero); val4.offsetMin = offsetMin; ((Graphic)val3.GetComponent()).color = new Color(Gold.r, Gold.g, Gold.b, 0.35f); Scrollbar component = val.GetComponent(); component.handleRect = val4; component.direction = (Direction)2; ((Selectable)component).targetGraphic = (Graphic)(object)val3.GetComponent(); return component; } private static GameObject NewLayoutChild(RectTransform col, string name, float height) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown GameObject val = new GameObject(name, new Type[2] { typeof(RectTransform), typeof(LayoutElement) }); val.transform.SetParent((Transform)(object)col, false); LayoutElement component = val.GetComponent(); component.minHeight = height; component.preferredHeight = height; return val; } private Text AddText(Transform parent, string name, int size, TextAnchor align, Color color, FontStyle style = (FontStyle)0) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[1] { typeof(RectTransform) }); val.transform.SetParent(parent, false); Text val2 = val.AddComponent(); val2.font = _font; val2.fontSize = size; val2.fontStyle = style; val2.alignment = align; ((Graphic)val2).color = color; val2.horizontalOverflow = (HorizontalWrapMode)1; val2.verticalOverflow = (VerticalWrapMode)1; ((Graphic)val2).raycastTarget = false; return val2; } private static void StretchInto(RectTransform rt, float left, float bottom, float right, float top) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0019: 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) rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = new Vector2(left, bottom); rt.offsetMax = new Vector2(right, top); } } internal static class PetPanelData { internal static PetPanelInput Gather() { //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_0029: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: 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_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0146: 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_014c: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_02d4: Unknown result type (might be due to invalid IL or missing references) //IL_02d9: Unknown result type (might be due to invalid IL or missing references) //IL_02f3: Unknown result type (might be due to invalid IL or missing references) //IL_0305: Unknown result type (might be due to invalid IL or missing references) //IL_031e: Unknown result type (might be due to invalid IL or missing references) //IL_03cf: Unknown result type (might be due to invalid IL or missing references) //IL_03db: Unknown result type (might be due to invalid IL or missing references) //IL_0495: Unknown result type (might be due to invalid IL or missing references) //IL_0496: Unknown result type (might be due to invalid IL or missing references) //IL_04aa: Unknown result type (might be due to invalid IL or missing references) //IL_04b0: Invalid comparison between Unknown and I4 //IL_0582: Unknown result type (might be due to invalid IL or missing references) //IL_0588: Invalid comparison between Unknown and I4 //IL_05a4: Unknown result type (might be due to invalid IL or missing references) //IL_05aa: Invalid comparison between Unknown and I4 //IL_06f3: Unknown result type (might be due to invalid IL or missing references) //IL_06f4: Unknown result type (might be due to invalid IL or missing references) //IL_0703: Unknown result type (might be due to invalid IL or missing references) //IL_0725: Unknown result type (might be due to invalid IL or missing references) //IL_07aa: Unknown result type (might be due to invalid IL or missing references) //IL_07ba: Unknown result type (might be due to invalid IL or missing references) Plugin instance = Plugin.Instance; Pet pet = (((Object)(object)instance != (Object)null) ? instance.ActivePet : null); PetPanelInput val = new PetPanelInput { HasPet = (pet != null) }; if (pet == null) { return val; } PetStatus val2 = pet.Sim.Status(); val.SpeciesId = pet.SpeciesId; val.PetName = ((pet.State != null) ? pet.State.Name : null); val.Bond.LoyaltyValue = val2.LoyaltyValue; val.Bond.Responsiveness = val2.Responsiveness; val.Bond.SpeedMultiplier = PetSystems.SpeedMultiplier(val2.Responsiveness); val.Bond.FollowSpeed = Plugin.FollowSpeed.Value; val.Bond.GainPercent = Plugin.LoyaltyGainPercent.Value; val.Bond.GainCarry = pet.Sim.LoyaltyGainCarry; val.Hunger.HungerSecondsSinceFed = ((pet.State != null) ? pet.State.HungerSecondsSinceFed : 0.0); val.Hunger.HungerSecondsPerDay = Plugin.HungerDayFor(pet.State); val.Hunger.DailyLoyaltyDecay = Plugin.SpeciesDailyDecay.Value; val.Climate.Comfort = val2.Comfort; val.Climate.ComfortSide = val2.ComfortSide; val.Climate.BlanketName = ((pet.State != null) ? pet.State.BlanketKey : ""); val.Climate.BlanketSecondsLeft = ((pet.State != null) ? pet.State.BlanketSecondsLeft : 0.0); val.Climate.BlanketSide = (ComfortSide)((!string.IsNullOrEmpty(val.Climate.BlanketName) && PetComfortTable.Blankets.TryGetValue(val.Climate.BlanketName, out var value)) ? ((int)value.Side) : 0); string text = ((pet.State != null) ? pet.State.DrinkKey : ""); WeatherFoodDef val3 = WeatherFoodTable.DefFor(text); val.Climate.DrinkName = (string.IsNullOrEmpty(text) ? "" : WeatherFoodTable.DisplayName(text)); val.Climate.DrinkSecondsLeft = ((pet.State != null) ? pet.State.DrinkSecondsLeft : 0.0); val.Climate.DrinkColdSteps = val3?.ColdSteps ?? 0; val.Climate.DrinkHotSteps = val3?.HotSteps ?? 0; val.Climate.DrinkImmune = val3?.Immune ?? false; val.Vitals.HasBody = (Object)(object)((Companion)pet).Body != (Object)null; val.Vitals.BodyIsGhost = instance.ActivePetIsGhost; val.Vitals.Reforming = instance.ReformInProgress; PetHealthReading val4 = PetHealthView.Resolve(instance); val.Vitals.HasAnchor = ((PetHealthReading)(ref val4)).Known; val.Vitals.HealthCurrent = val4.Current; val.Vitals.HealthMax = val4.Max; StatTuningPolicy val5 = Plugin.TuningPolicy(); val.Vitals.HealthFactor = PetStatTuning.Factor(val2.LoyaltyValue, val5.HealthAt0, val5.HealthAt100); val.Vitals.RelicStacks = ((pet.State != null) ? pet.State.RelicStacks : 0); val.Vitals.RelicLabel = SpeciesRelics.For(pet.SpeciesId)?.ItemLabel ?? ""; if (Plugin.EnableFoodHealthRecovery.Value && pet.State != null) { val.Vitals.HealthRecoveryLevel = pet.State.HealthRecoveryLevel; val.Vitals.HealthRecoverySecondsLeft = pet.State.HealthRecoverySecondsLeft; } val.Offense.DamageFactor = PetSystems.CombatDamageMultiplier(pet, val2); CreatureAttributes val6 = PetSystems.EffectiveStats(pet, val2); bool flag = val6 != null && val6.HasDamage; val.Offense.BaseAttackDamage = (flag ? val6.DamageTotal : Plugin.AttackDamage.Value); val.Offense.AttackDamageProfile = (flag ? val6.Damage : null); val.Offense.AttackInterval = Plugin.AttackInterval.Value; val.Offense.AttackRange = Plugin.AttackRange.Value; val.Offense.AggroRange = Plugin.AggroRange.Value; BuffFoodDef val7 = BuffFoodTable.ActiveDef(pet); if (val7 != null && pet.State != null) { val.Offense.BuffFoodName = BuffFoodTable.DisplayName(val7); double num = BuffFoods.Percent(val7, val2.Loyalty); val.Offense.BuffFoodEffect = (((int)val7.Kind == 1) ? $"+{num:F0}% of damage as Decay" : $"+{num:F0}% damage"); val.Offense.BuffFoodSecondsLeft = pet.State.BuffFoodSecondsLeft; } SpecialAttackDef val8 = default(SpecialAttackDef); if (Plugin.EnableCombat.Value && Plugin.EnableSpecialAttack.Value && SpecialAttackTable.TryGet(SpecialAttacks.Table, pet.SpeciesId, ref val8)) { val.Offense.HasSpecialAttack = true; val.Offense.SpecialStatusEffect = val8.StatusEffectId; val.Offense.SpecialBuildupPercent = val8.BuildupPercent; val.Offense.SpecialDamageMultiplier = val8.DamageMultiplier; val.Offense.SpecialCooldownSeconds = val8.CooldownSeconds; val.Offense.SpecialIsRanged = (int)val8.Kind == 1 && Plugin.EnableRangedSpecial.Value; val.Offense.SpecialIsBrace = (int)val8.Kind == 2 && Plugin.EnableBrace.Value; PetSpecialAttack petSpecialAttack = (((Object)(object)((Companion)pet).Body != (Object)null) ? ((Component)((Companion)pet).Body).GetComponent() : null); val.Offense.SpecialReadySeconds = (((Object)(object)petSpecialAttack != (Object)null) ? petSpecialAttack.CooldownRemaining : 0f); if (Plugin.EnableFoodHexes.Value && pet.State != null) { FoodHexEntry val9 = FoodHexTable.Resolve(pet.SpeciesId); if (val9 != null) { val.Offense.FoodHexAvailable = true; val.Offense.FoodHexMix = FoodHexes.ComputeBuildups(val9, (IReadOnlyList)pet.State.RecentMeals, Plugin.HexMealWindow.Value, Plugin.HexBuildupPercent.Value); } } } float[] resistancesPct = default(float[]); if (((Companion)pet).Anchor != null && ((Companion)pet).Anchor.TryGetResistances(PetPanel.DamageTypeNames.Length, ref resistancesPct)) { val.Defense.ResistancesPct = resistancesPct; } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; bool flag2 = !BwConfig.Skills.EnableCommunionGate.Value || PetSystems.SkillLearned(localPlayerCharacter, 87009); val.Gifts.BuffsEnabled = Plugin.EnablePassiveBuffs.Value && flag2; if (val.Gifts.BuffsEnabled && !string.IsNullOrEmpty(pet.SpeciesId)) { val.Gifts.Buffs = PetBuffs.Resolve(PetBuffTable.Table, pet.SpeciesId, val2.Loyalty); LoyaltyTier val10 = default(LoyaltyTier); int num2 = default(int); if (Loyalty.NextTier(val2.LoyaltyValue, ref val10, ref num2)) { val.Gifts.NextTierBuffs = PetBuffs.Resolve(PetBuffTable.Table, pet.SpeciesId, val10); } } CompanionCombat combat = ((Companion)pet).Combat; Character val11 = (((Object)(object)combat != (Object)null) ? combat.CurrentTarget : null); val.Now.CombatTargetName = (((Object)(object)val11 != (Object)null && val11.Alive) ? val11.Name : null); Character localPlayerCharacter2 = Plugin.LocalPlayerCharacter; val.Now.DistanceToPlayer = (((Object)(object)localPlayerCharacter2 != (Object)null && (Object)(object)((Companion)pet).Body != (Object)null) ? Vector3.Distance(((Component)localPlayerCharacter2).transform.position, ((Component)((Companion)pet).Body).transform.position) : float.NaN); return val; } internal static void Dump() { //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) PetPanelModel val = PetPanel.Build(Gather()); if (!val.HasPet) { Plugin.Log.LogMessage((object)("[PETPANEL] no pet — empty state: " + val.EmptyText.Replace("\n", " ").Trim())); return; } Plugin.Log.LogMessage((object)("[PETPANEL] '" + val.Header + "' — " + val.SubHeader)); foreach (PetPanelSection section in val.Sections) { Plugin.Log.LogMessage((object)("[PETPANEL] == " + section.Title + " ==")); foreach (PetPanelRow row in section.Rows) { Plugin.Log.LogMessage((object)($"[PETPANEL] {row.Label,-20} {row.Value}" + (float.IsNaN(row.Bar) ? "" : $" bar={row.Bar:F2}") + (((int)row.Tone != 0) ? $" [{row.Tone}]" : ""))); } } } } internal static class PetPassiveBuff { private const string SourcePrefix = "Beastwhispering.PetBuff."; private static readonly PlayerStatBuff _applier = new PlayerStatBuff(true); internal static void Sync(Character player, string speciesId, LoyaltyTier tier, bool communionEnabled) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return; } List list = ((Plugin.EnablePassiveBuffs.Value && communionEnabled && !string.IsNullOrEmpty(speciesId)) ? PetBuffs.Resolve(PetBuffTable.Table, speciesId, tier) : new List()); list.RemoveAll((ResolvedBuff b) => PetBuffs.IsFlat(((ResolvedBuff)(ref b)).Stat)); List list2 = ((IEnumerable)list).Select((Func)((ResolvedBuff b) => new StatStackSpec(StatFor(player, ((ResolvedBuff)(ref b)).Stat), "Beastwhispering.PetBuff." + ((object)((ResolvedBuff)(ref b)).Stat/*cast due to .constrained prefix*/).ToString(), ((ResolvedBuff)(ref b)).Amount * 0.01f, true, ((object)((ResolvedBuff)(ref b)).Stat/*cast due to .constrained prefix*/).ToString()))).ToList(); StatBuffResult val = _applier.Sync(player, list2, (Action)delegate(StatStackSpec s) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogWarning((object)("[BUFF] no player stat for " + s.Label + " — skipped.")); }); if ((int)val.Change != 0) { List list3 = list.Where((ResolvedBuff b) => StatFor(player, ((ResolvedBuff)(ref b)).Stat) != null).ToList(); if (list3.Count > 0) { Plugin.Log.LogMessage((object)string.Format("[BUFF] bond buff -> {0} (loyalty {1}{2}).", Describe(list3), tier, val.SamePlayer ? "" : ", fresh player")); } else if (val.SamePlayer) { Plugin.Log.LogMessage((object)$"[BUFF] bond buff cleared (loyalty {tier})."); } } } internal static void Clear() { _applier.Clear(); } internal static void Dump(Character player) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: 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_00cb: 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_00e3: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnablePassiveBuffs.Value) { Plugin.Log.LogMessage((object)"[BUFF] passive buffs DISABLED ([Systems] EnablePassiveBuffs)."); return; } if (_applier.AppliedCount == 0) { Stat val = StatFor(player, (PetBuffStat)0); Plugin.Log.LogMessage((object)("[BUFF] no bond buff applied (PhysicalDamage readback: " + ((val != null) ? val.CurrentValue.ToString("F3") : "n/a") + ").")); return; } if (!_applier.SamePlayer(player)) { Plugin.Log.LogWarning((object)"[BUFF] applied-to Character is not the current player (stale until the next sim tick re-syncs)."); } foreach (StatStackSpec item in _applier.Applied) { Plugin.Log.LogMessage((object)string.Format("[BUFF] {0} +{1:0.##}% (stack '{2}') — player readback: {3} (1.000 = unbuffed).", item.Label, item.Value * 100f, item.SourceId, (item.Target != null) ? item.Target.CurrentValue.ToString("F3") : "n/a")); } } private static Stat StatFor(Character c, PetBuffStat stat) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected I4, but got Unknown CharacterStats val = (((Object)(object)c != (Object)null) ? c.Stats : null); if ((Object)(object)val == (Object)null) { return null; } return (Stat)((int)stat switch { 0 => val.m_damageTypesModifier[0], 1 => val.m_damageTypesModifier[1], 2 => val.m_damageTypesModifier[2], 3 => val.m_damageTypesModifier[3], 4 => val.m_damageTypesModifier[4], 5 => val.m_damageTypesModifier[5], 6 => val.m_movementSpeed, _ => null, }); } private static string Describe(List buffs) { return string.Join(", ", buffs.Select((ResolvedBuff b) => $"+{((ResolvedBuff)(ref b)).Amount:0.##}% {((ResolvedBuff)(ref b)).Stat}")); } } internal sealed class PetPersistence { private readonly Plugin _plugin; private readonly PetLifecycle _lifecycle; private readonly CompanionPersistence _spine; internal const float SaveHeartbeatSeconds = 30f; internal const float InSessionRestoreWindowSeconds = 6f; internal const float InSessionRestorePollSeconds = 0.4f; internal int _lastSavedLoyalty = int.MinValue; internal string _lastSavedBlanket = ""; internal string _lastSavedDrink = ""; internal string _lastSavedBuffFood = ""; internal string _lastSavedHealthRecovery = ""; internal float _lastSaveAt; internal double _lastSavedSpecialCooldown; private static Character LocalPlayer => Plugin.LocalPlayerCharacter; internal PetPersistence(Plugin plugin, PetLifecycle lifecycle) { _plugin = plugin; _lifecycle = lifecycle; _spine = new CompanionPersistence(new CompanionPersistenceSpec { Host = Plugin.CkHost, Runner = (MonoBehaviour)(object)plugin, Noun = "pet", WaitKey = this, Store = PetSaveStore.Store, LocalPlayer = () => LocalPlayer, HasLive = () => _lifecycle._activePet != null, LiveBody = () => ((Companion)(_lifecycle._activePet?)).Body, LiveOwnerUid = () => _lifecycle._activePet?.OwnerUid, LiveSpeciesId = () => _lifecycle._activePet?.SpeciesId, TeardownLive = delegate(Character player) { _lifecycle.TeardownLivePet(player); }, OnSceneReset = delegate { RecipeScrollDrops.ResetForScene(); MusicTapChoose.ResetForScene(); }, OnSessionEnd = delegate { WildUnknown.ClearPending("main menu"); }, OnPlayerReadyFirst = delegate(string scene, Character player) { SkySnapshot.Log("player-ready '" + scene + "'"); SkillRegistry.SyncIcons(player); }, OnBeforeLoad = delegate(string scene, Character player) { WildUnknown.PendingTravel pendingTravel = WildUnknown.Consume(); if (pendingTravel != null) { _lifecycle.OnRegionTravelArrival(player, pendingTravel.FromScene, pendingTravel.ToScene); } ExpeditionOrchestrator.OpportunisticCapture(scene); }, SavedIdentityError = PetLifecycle.StandInSavedIdentityError, AdoptSaved = AdoptSavedPet, OnInSessionArrival = InSessionArrival, OnAfterLoad = delegate { ExpeditionOrchestrator.AutoWarm(); }, Acquisition = lifecycle.Acquisition }); } internal void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_000f: 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) if ((int)mode != 1) { PetSystems.ResetDiagnostics(); } _spine.OnSceneLoaded(scene, mode); } private void AdoptSavedPet(PetSave saved, Character player) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) string ownerUid = UID.op_Implicit(player.UID); saved.SpecialCooldownSecondsLeft = HuntCooldown.EffectiveRemainder(Plugin.SyncSkillCooldownToPet.Value, saved.SpecialCooldownSecondsLeft); _lifecycle._activePet = _lifecycle.NewPet(saved, ownerUid); ((Companion)_lifecycle._activePet).Anchor.SeedHealthFraction((float)saved.HealthFraction); _lastSavedLoyalty = saved.LoyaltyValue; _lastSavedBlanket = saved.BlanketKey; _lastSavedDrink = saved.DrinkKey; _lastSavedBuffFood = saved.BuffFoodKey; _lastSavedHealthRecovery = HealthRecoveryMark(saved); _plugin.SimTicker.Prime(); Plugin.Log.LogMessage((object)($"[PERSIST] loaded pet '{saved.SpeciesId}' (loyalty {saved.LoyaltyValue}, stage {saved.TemperatureStage}, " + "blanket " + (string.IsNullOrEmpty(saved.BlanketKey) ? "none" : $"'{saved.BlanketKey}' {saved.BlanketSecondsLeft:F0}s") + ", " + $"hp {saved.HealthFraction * 100.0:F0}%, " + "regen " + ((saved.HealthRecoveryLevel > 0) ? HealthRecovery.Describe(saved.HealthRecoveryLevel, saved.HealthRecoverySecondsLeft) : "none") + ", " + $"gain bank {saved.LoyaltyGainCarry:0.###}); " + "systems ticking, finding a body.")); } private void InSessionArrival(string scene, Character player) { if (_lifecycle._activePet?.State != null) { float num = (float)_lifecycle._activePet.State.HealthFraction; ((Companion)_lifecycle._activePet).Anchor.SeedHealthFraction(num); if (!PhotonNetwork.isNonMasterClientInRoom && Plugin.PersistPetHealth.Value && num < 0.999f) { Plugin.Log.LogMessage((object)($"[ANCHOR] carried HP {(double)num * 100.0:F0}% across the in-session swap into " + "'" + scene + "' — restoring the fresh/persisting anchor to it, not full (Bug 48).")); ((MonoBehaviour)_plugin).StartCoroutine(RestoreInSessionHealth(_lifecycle._activePet, num, scene)); } } } private IEnumerator RestoreInSessionHealth(Pet pet, float frac, string scene) { if (frac >= 0.999f) { yield break; } float deadline = Time.time + 6f; int corrected = 0; while (Time.time < deadline) { if (_lifecycle._activePet != pet || !Plugin.PersistPetHealth.Value) { yield break; } if (((Companion)pet).Anchor.RestoreLiveHealth(frac)) { corrected++; } else if (corrected > 0) { break; } yield return (object)new WaitForSeconds(0.4f); } Plugin.Log.LogMessage((object)("[ANCHOR] in-session HP watch for '" + scene + "' done: corrected the persisting anchor's " + $"over-heal {corrected} time(s) toward {(double)frac * 100.0:F0}% (Bug 48 reopened).")); } internal void SaveNow(Character player) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) Pet activePet = _lifecycle._activePet; if (!((Object)(object)player == (Object)null) && activePet?.State != null) { string text = UID.op_Implicit(player.UID); if (!string.IsNullOrEmpty(activePet.OwnerUid) && !string.IsNullOrEmpty(text) && activePet.OwnerUid != text) { Plugin.Log.LogWarning((object)("[PERSIST] save skipped: pet owner " + ReformFlow.Uid4(activePet.OwnerUid) + " != player " + ReformFlow.Uid4(text) + ".")); return; } double specialCooldownSecondsLeft = activePet.State.SpecialCooldownSecondsLeft; activePet.State.SpecialCooldownSecondsLeft = HuntCooldown.EffectiveRemainder(Plugin.SyncSkillCooldownToPet.Value, specialCooldownSecondsLeft); activePet.State.HealthFraction = (Plugin.PersistPetHealth.Value ? CurrentPersistFraction(activePet) : 1.0); PetSaveStore.Save(player, activePet.State); activePet.State.SpecialCooldownSecondsLeft = specialCooldownSecondsLeft; _lastSavedLoyalty = activePet.State.LoyaltyValue; _lastSavedBlanket = activePet.State.BlanketKey; _lastSavedDrink = activePet.State.DrinkKey; _lastSavedBuffFood = activePet.State.BuffFoodKey; _lastSavedHealthRecovery = HealthRecoveryMark(activePet.State); _lastSavedSpecialCooldown = activePet.State.SpecialCooldownSecondsLeft; _lastSaveAt = Time.time; } } internal static double CurrentPersistFraction(Pet pet) { bool isNonMasterClientInRoom = PhotonNetwork.isNonMasterClientInRoom; float current; float max; bool flag = PetProxyClient.TryGetMirroredHealth(out current, out max); return PetHealthProvider.PersistFraction(!isNonMasterClientInRoom, (double)((Companion)pet).Anchor.HealthFraction, flag, current, max, pet.State.HealthFraction); } internal static string HealthRecoveryMark(PetSave state) { if (state.HealthRecoveryLevel <= 0) { return ""; } return state.HealthRecoveryLevel.ToString(CultureInfo.InvariantCulture); } } internal static class PetProxyClient { internal const string StatusVerb = "bw.status"; internal const string TauntVerb = "bw.taunt"; internal const string WardVerb = "bw.ward"; internal const string BandageVerb = "bw.bandage"; internal const string BandageAckVerb = "bw.bandage.ack"; private static bool _announced; private static int _lastAnnouncedTier = -1; private static float _nextReannounceAt; private static StateMirror _statsMirror; private static CreatureAttributes _statsEff; private static float _statsMax; private static float _lastMaxHealth; private static bool _pinnedAtFloor; private static bool _hpValid; private static float _mirrorCur; private static float _mirrorMax; private static bool _warnedNoPlayerRoute; private static bool _loggedDrainSkip; private const float ReannounceSeconds = 30f; private static string _lastAttackedLogUid; internal static void Init() { //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected O, but got Unknown NetBus.Register("ck.proxy.died", (Action)OnDied, (HandlerRole)6); NetBus.Register("ck.proxy.crit", (Action)OnCrit, (HandlerRole)6); NetBus.Register("ck.proxy.calmed", (Action)OnCalmed, (HandlerRole)6); NetBus.Register("ck.proxy.attacked", (Action)OnAttacked, (HandlerRole)6); NetBus.Register("ck.proxy.pinned", (Action)OnPinned, (HandlerRole)6); NetBus.Register("ck.proxy.hp", (Action)OnHealthMirror, (HandlerRole)6); NetBus.Register("ck.proxy.hpclear", (Action)OnHealthClear, (HandlerRole)6); NetBus.Register("bw.status", (Action)OnStatusRequest, (HandlerRole)1); NetBus.Register("bw.taunt", (Action)OnTauntRequest, (HandlerRole)1); NetBus.Register("bw.ward", (Action)OnWardRequest, (HandlerRole)1); NetBus.RegisterRequestHandler("bw.bandage", (HandlerRole)1, (Action>)OnBandageRequest); _statsMirror = NetBus.Mirror("ck.proxy.stats", (MirrorTarget)0, (Func)BuildStatsPayload, new MirrorOptions { Extra = delegate { //IL_0010: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; return (localPlayerCharacter == null) ? "" : UID.op_Implicit(localPlayerCharacter.UID); } }); } private static string BuildStatsPayload() { if (!_announced || (Object)(object)Plugin.LocalPlayerCharacter == (Object)null) { return null; } return NetProtocol.BuildStats(_statsMax, (_statsEff != null) ? _statsEff.Flatten() : ""); } private static int CurrentTier(Pet pet) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) return PetBuffs.Level(Loyalty.Tier((pet?.State?.LoyaltyValue).GetValueOrDefault())); } internal static void Maintain(Plugin p) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Pet activePet = p.ActivePet; bool flag = activePet != null && (Object)(object)((Companion)activePet).Body != (Object)null && (Object)(object)localPlayerCharacter != (Object)null; CompanionEffigy.SyncOwnedPet(((Object)(object)localPlayerCharacter != (Object)null) ? UID.op_Implicit(localPlayerCharacter.UID) : "", flag ? activePet.SpeciesId : "", flag ? CurrentTier(activePet) : 0, flag, flag && ((Companion)activePet).Stance.Passive); if (!PhotonNetwork.inRoom || !PhotonNetwork.isNonMasterClientInRoom) { _announced = false; _lastAnnouncedTier = -1; _pinnedAtFloor = false; _lastMaxHealth = 0f; StateMirror statsMirror = _statsMirror; if (statsMirror != null) { statsMirror.Reset(); } ClearHealthMirror(); return; } Character localPlayerCharacter2 = Plugin.LocalPlayerCharacter; Pet activePet2 = p.ActivePet; bool flag2 = activePet2 != null && (Object)(object)((Companion)activePet2).Body != (Object)null && (Object)(object)localPlayerCharacter2 != (Object)null; if (flag2) { int num = CurrentTier(activePet2); if (!_announced) { SendAnnounce(activePet2, localPlayerCharacter2, num, "fresh"); } else if (num != _lastAnnouncedTier) { SendAnnounce(activePet2, localPlayerCharacter2, num, "tier changed"); } else if (Time.time >= _nextReannounceAt) { SendAnnounce(activePet2, localPlayerCharacter2, num, "periodic"); } } else if (!flag2 && _announced) { string text = (((Object)(object)localPlayerCharacter2 != (Object)null) ? UID.op_Implicit(localPlayerCharacter2.UID) : ""); string text2 = Plugin.Instance?.Lifecycle?.LastBodyLossReason ?? ""; if (NetBus.SendToMaster("ck.proxy.release", text, text2)) { _announced = false; _lastAnnouncedTier = -1; _pinnedAtFloor = false; _lastMaxHealth = 0f; StateMirror statsMirror2 = _statsMirror; if (statsMirror2 != null) { statsMirror2.Reset(); } ClearHealthMirror(); if (Plugin.Instance?.Lifecycle != null) { Plugin.Instance.Lifecycle.LastBodyLossReason = ""; } Plugin.Log.LogMessage((object)("[PROXY] [G→M] released the proxied anchor (pet bodiless or gone" + ((text2.Length > 0) ? ("; reason: " + text2) : "") + ").")); } } SelfHealStaleCombat(localPlayerCharacter2); } private static void SelfHealStaleCombat(Character player) { //IL_0080: 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_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) if ((Object)(object)player == (Object)null || !player.InCombat) { return; } List list = null; try { list = player.EngagedCharacters; } catch { } if (list == null || list.Count == 0) { return; } float value = Plugin.CombatLeashDistance.Value; Vector3 position = ((Component)player).transform.position; for (int i = 0; i < list.Count; i++) { Character val = list[i]; bool flag; try { flag = (Object)(object)val != (Object)null && val.Alive; } catch { flag = false; } float num = (flag ? Vector3.Distance(((Component)val).transform.position, position) : 0f); if (!CombatSweep.ShouldDrop(flag, num, value)) { return; } } Plugin.Log.LogMessage((object)"[PROXY] combat self-heal: every engagement is droppable — sweeping (a lost ck.proxy.calmed, or a fight that ended while no message could land)."); DisengageStalePetCombat(); } private static void SendAnnounce(Pet pet, Character player, int tier, string why) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) double num = ((pet.State != null) ? pet.State.HealthFraction : 1.0); if (NetBus.SendToMaster("ck.proxy.announce", UID.op_Implicit(player.UID), NetProtocol.BuildAnnounce(pet.SpeciesId, tier, num))) { bool flag = !_announced; _announced = true; _lastAnnouncedTier = tier; _nextReannounceAt = Time.time + 30f; StateMirror statsMirror = _statsMirror; if (statsMirror != null) { statsMirror.Invalidate(); } if (flag) { _pinnedAtFloor = false; _lastMaxHealth = 0f; ClearHealthMirror(); } CompanionCombat combat = ((Companion)pet).Combat; if (combat != null) { combat.ResetProxyTargetMirror(); } CompanionCombat combat2 = ((Companion)pet).Combat; if (combat2 != null) { combat2.ResetProxyStanceMirror(); } if (flag) { Plugin.Log.LogMessage((object)($"[PROXY] [G→M] announced '{pet.SpeciesId}' (tier {tier}) to the master — " + "the combat anchor is proxied there.")); } else { Plugin.Log.LogMessage((object)($"[PROXY] [G→M] re-announced '{pet.SpeciesId}' (tier {tier}, {why}) — " + "stats and target re-flow on the next syncs.")); } } } internal static void SyncStats(Pet pet, CreatureAttributes eff, float maxHealth) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Invalid comparison between Unknown and I4 if (PhotonNetwork.isNonMasterClientInRoom) { _lastMaxHealth = maxHealth; _statsEff = eff; _statsMax = maxHealth; StateMirror statsMirror = _statsMirror; if (statsMirror != null && (int)statsMirror.Tick(0) == 1) { Plugin.Log.LogMessage((object)string.Format("[PROXY] [G→M] proxied anchor stats updated (maxHealth={0:F0}, eff={1}).", maxHealth, (eff != null) ? "set" : "none")); } } } private static void OnDied(NetMessage msg) { if (Plugin.Instance?.ActivePet != null) { Plugin.Log.LogMessage((object)"[PROXY] [M→G] the master reports the proxied anchor DOWN — running the local downed flow."); Plugin.Instance.Lifecycle.OnAnchorDied(); DisengageStalePetCombat(); } } private static void DisengageStalePetCombat() { //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: 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) if (!PhotonNetwork.isNonMasterClientInRoom) { return; } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null) { return; } List list = null; try { list = localPlayerCharacter.EngagedCharacters; } catch { } if (list == null || list.Count == 0) { return; } float value = Plugin.CombatLeashDistance.Value; Vector3 position = ((Component)localPlayerCharacter).transform.position; int num = 0; int num2 = 0; for (int num3 = list.Count - 1; num3 >= 0; num3--) { Character val = list[num3]; bool flag; try { flag = (Object)(object)val != (Object)null && val.Alive; } catch { flag = false; } if ((Object)(object)val != (Object)null && flag) { if (CombatSweep.ShouldDrop(true, Vector3.Distance(((Component)val).transform.position, position), value)) { try { localPlayerCharacter.RemoveCombatEngagement(val); } catch { } num2++; } } else { list.RemoveAt(num3); num++; } } if (num + num2 == 0) { return; } if (list.Count == 0 && (Object)(object)Global.CombatManager != (Object)null) { try { Global.CombatManager.RemoveCombatCharacter(localPlayerCharacter); } catch { } } Plugin.Log.LogMessage((object)($"[PROXY] [M→G] guest disengage: swept {num + num2} engagement(s) " + $"({num} dead, {num2} live-beyond-leash) from the player after the pet's fight " + $"ended (InCombat={localPlayerCharacter.InCombat}, engaged={list.Count}).")); } private static void OnCalmed(NetMessage msg) { Plugin.Log.LogMessage((object)"[PROXY] [M→G] the master reports the proxied anchor's fight ENDED — sweeping stale engagements."); DisengageStalePetCombat(); } private static void OnAttacked(NetMessage msg) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) Pet pet = Plugin.Instance?.ActivePet; if (pet != null) { CharacterManager instance = CharacterManager.Instance; Character val = ((instance != null) ? instance.GetCharacter(msg.Payload ?? "") : null); if ((Object)(object)val == (Object)null) { NetBus.CountDrop("ck.proxy.attacked", "no-attacker"); } else if ((Object)(object)((Companion)pet).Combat == (Object)null) { NetBus.CountDrop("ck.proxy.attacked", "no-body"); } else if (!((Companion)pet).Combat.ReportAnchorAttacked(val)) { NetBus.CountDrop("ck.proxy.attacked", "invalid-attacker"); } else if (!string.Equals(_lastAttackedLogUid, msg.Payload, StringComparison.Ordinal)) { _lastAttackedLogUid = msg.Payload; Plugin.Log.LogMessage((object)("[PROXY] [M→G] proxied anchor attacked by '" + val.Name + "' — feeding the pet's anchor-defend ladder (stance rules apply locally).")); } } } private static void OnCrit(NetMessage msg) { if (Plugin.Instance?.ActivePet != null) { Plugin.Instance.Lifecycle.OnAnchorCriticallyHurt(); } } private static void OnPinned(NetMessage msg) { if (Plugin.Instance?.ActivePet != null) { _pinnedAtFloor = true; } } private static void OnHealthMirror(NetMessage msg) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Instance?.ActivePet != null) { float mirrorCur = default(float); float mirrorMax = default(float); if (!NetProtocol.TryParseHealth(msg.Payload, ref mirrorCur, ref mirrorMax)) { NetBus.CountDrop("ck.proxy.hp", "unparseable"); return; } _mirrorCur = mirrorCur; _mirrorMax = mirrorMax; _hpValid = true; } } private static void OnHealthClear(NetMessage msg) { ClearHealthMirror(); } private static void ClearHealthMirror() { _hpValid = false; _mirrorCur = 0f; _mirrorMax = 0f; } internal static bool TryGetMirroredHealth(out float current, out float max) { current = _mirrorCur; max = _mirrorMax; return PetHealthProvider.ProxyMirrorUsable(_announced, _hpValid); } internal static bool RouteEnemyStatus(Character target, string status, float buildupPercent, string tag) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) if (!PhotonNetwork.isNonMasterClientInRoom) { return false; } if ((Object)(object)target == (Object)null || string.IsNullOrEmpty(status)) { return true; } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null) { NetBus.CountDrop("bw.status", "no-player"); if (!_warnedNoPlayerRoute) { _warnedNoPlayerRoute = true; Plugin.Log.LogWarning((object)(tag + " enemy-status route suppressed — no local player Character (mid-reload window). Counted as bw.status/no-player; further hits count silently.")); } return true; } string text = NetProtocol.BuildStatusRequest(UID.op_Implicit(target.UID), status, buildupPercent); if (NetBus.SendToMaster("bw.status", UID.op_Implicit(localPlayerCharacter.UID), text)) { Plugin.Log.LogMessage((object)(tag + " '" + status + "'" + ((buildupPercent > 0f) ? $" +{buildupPercent:F0}% build-up" : "") + " routed to the master for '" + target.Name + "' (guest).")); } else { Plugin.Log.LogWarning((object)(tag + " '" + status + "' for '" + target.Name + "' could not be routed to the master — lost.")); } return true; } internal static bool RequestTaunt(Character enemy, float seconds) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null || (Object)(object)enemy == (Object)null || !_announced) { return false; } return NetBus.SendToMaster("bw.taunt", UID.op_Implicit(localPlayerCharacter.UID), NetProtocol.BuildTaunt(UID.op_Implicit(enemy.UID), seconds)); } internal static bool SendWardToMaster(string status) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null || !_announced || string.IsNullOrEmpty(status)) { return false; } return NetBus.SendToMaster("bw.ward", UID.op_Implicit(localPlayerCharacter.UID), NetProtocol.BuildWardRequest(status)); } internal static bool SendBandageRequest(float timeoutSeconds, Action onResult) { //IL_0021: 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) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null || !_announced) { onResult(RequestResult.Refused("no-proxy", "")); return false; } return NetBus.SendRequest("bw.bandage", UID.op_Implicit(localPlayerCharacter.UID), "", timeoutSeconds, onResult); } internal static bool RequestHealFull() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter != (Object)null && _announced) { return ProxyPets.RequestHealFull(UID.op_Implicit(localPlayerCharacter.UID)); } return false; } internal static bool RequestSetHealth(float value, bool isPercent) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter != (Object)null && _announced) { return ProxyPets.RequestSetHealth(UID.op_Implicit(localPlayerCharacter.UID), value, isPercent); } return false; } internal static bool RequestDrainAndCheckPinned(float pctPerMin, float dt) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter != (Object)null && _announced && _lastMaxHealth > 0f) { ProxyPets.RequestDrain(UID.op_Implicit(localPlayerCharacter.UID), _lastMaxHealth * pctPerMin / 100f * (dt / 60f)); _loggedDrainSkip = false; } else if ((Object)(object)localPlayerCharacter != (Object)null && _announced) { NetBus.CountDrop("ck.proxy.sethealth", "no-max"); if (!_loggedDrainSkip) { _loggedDrainSkip = true; Plugin.Log.LogMessage((object)"[PROXY] [G→M] temperature drain skipped — no synced maxHealth yet (fresh-bond window; SyncStats refills on the next stats sync). Counted as ck.proxy.sethealth/no-max."); } } bool result = _announced && _pinnedAtFloor; _pinnedAtFloor = false; return result; } private static void OnStatusRequest(NetMessage msg) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0075: 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) if (!ProxyPets.AuthorizeSender("bw.status", msg)) { return; } string text = default(string); string text2 = default(string); float num = default(float); if (!NetProtocol.TryParseStatusRequest(msg.Payload, ref text, ref text2, ref num)) { NetBus.CountDrop("bw.status", "unparseable"); return; } CharacterManager instance = CharacterManager.Instance; Character val = ((instance != null) ? instance.GetCharacter(text) : null); if ((Object)(object)val == (Object)null || !val.Alive) { NetBus.CountDrop("bw.status", "no-target"); } else { if (!ValidCombatTarget(val, text, "bw.status", msg)) { return; } Character val2 = default(Character); ProxyPets.TryGetAnchorCharacter(msg.OwnerUid, ref val2); if (num > 0f) { StatusEffectManager statusEffectMngr = val.StatusEffectMngr; if (statusEffectMngr != null) { statusEffectMngr.AddStatusEffectBuildUp(text2, num, val2); } Plugin.Log.LogMessage((object)$"[PROXY] [G→M] +{num:F0}% '{text2}' build-up on '{val.Name}' (guest '{msg.OwnerUid}')."); } else { PetSpecialAttack.ApplyStatus(val, text2, "[PROXY]"); } } } private static bool ValidCombatTarget(Character target, string uid, string verb, NetMessage msg) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 if (target.IsAI && (int)target.Faction != 1 && !CompanionAnchor.IsAnchor(target) && !AnchorSentinel.IsAnchorUid(uid)) { return true; } NetBus.CountDrop(verb, "protected-target"); Plugin.Log.LogWarning((object)($"[PROXY] [G→M] '{verb}' from '{msg.OwnerUid}' (actor {msg.SenderActorId}) REFUSED — " + $"target '{target.Name}' ({uid}) is not a hostile AI (IsAI={target.IsAI}, faction={target.Faction}, " + $"anchor={CompanionAnchor.IsAnchor(target)}).")); return false; } private static void OnWardRequest(NetMessage msg) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002a: 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_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableWardShare.Value) { NetBus.CountDrop("bw.ward", "disabled"); } else if (ProxyPets.AuthorizeSender("bw.ward", msg)) { string text = default(string); Character anchor = default(Character); if (!NetProtocol.TryParseWardRequest(msg.Payload, ref text)) { NetBus.CountDrop("bw.ward", "unparseable"); } else if (!WardShare.IsAllowedAnchorWard(text)) { NetBus.CountDrop("bw.ward", "not-allowlisted"); Plugin.Log.LogWarning((object)($"[WARD] [G→M] ward request from '{msg.OwnerUid}' (actor {msg.SenderActorId}) REFUSED — " + "'" + text + "' is not on the master's allowlist (" + WardShare.DescribeAllowlist() + "). A legit refusal here means the two machines' [Wards] name candidates resolved differently.")); } else if (!ProxyPets.TryGetAnchorCharacter(msg.OwnerUid, ref anchor)) { NetBus.CountDrop("bw.ward", "no-anchor"); } else { WardShare.ApplyGuestWard(anchor, text, msg.OwnerUid); } } } private static void OnBandageRequest(NetMessage msg, Action reply) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) if (!ProxyPets.AuthorizeSender("bw.bandage", msg)) { return; } if (!Plugin.EnableBandageHealing.Value) { NetBus.CountDrop("bw.bandage", "disabled"); reply("disabled"); return; } string text = PetBandage.ResolveStatus(); Character anchor = default(Character); if (text == null) { reply("unresolved"); } else if (!ProxyPets.TryGetAnchorCharacter(msg.OwnerUid, ref anchor)) { NetBus.CountDrop("bw.bandage", "no-anchor"); reply("no-anchor"); } else { PetBandage.ApplyToProxyAnchor(anchor, text, msg.OwnerUid); reply("ok"); } } private static void OnTauntRequest(NetMessage msg) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_006a: 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_009b: Unknown result type (might be due to invalid IL or missing references) if (!ProxyPets.AuthorizeSender("bw.taunt", msg)) { return; } string text = default(string); float seconds = default(float); if (!NetProtocol.TryParseTaunt(msg.Payload, ref text, ref seconds)) { NetBus.CountDrop("bw.taunt", "unparseable"); return; } CharacterManager instance = CharacterManager.Instance; Character val = ((instance != null) ? instance.GetCharacter(text) : null); if ((Object)(object)val == (Object)null || !val.Alive) { NetBus.CountDrop("bw.taunt", "no-target"); } else if (ValidCombatTarget(val, text, "bw.taunt", msg)) { Character anchor = default(Character); if (!ProxyPets.TryGetAnchorCharacter(msg.OwnerUid, ref anchor)) { NetBus.CountDrop("bw.taunt", "no-anchor"); Plugin.Log.LogMessage((object)("[TAUNT] [G→M] guest taunt request from '" + msg.OwnerUid + "' skipped — no live proxied anchor.")); } else { TauntController.Begin(val, anchor, seconds); } } } } internal static class PetSaveStore { private static CompanionSaveStore _store; internal static CompanionSaveStore Store => _store ?? (_store = new CompanionSaveStore(Plugin.CkHost, "bw_pets_", (Func)Encode, (Func)Decode)); private static string Encode(PetSave pet) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown SaveFile val = new SaveFile(); if (pet != null) { val.Pets.Add(pet); } return SaveCodec.Serialize(val, (Action)delegate(string m) { Plugin.Log.LogWarning((object)("[SAVE] " + m)); }); } private static PetSave Decode(string content, string path) { SaveFile val = SaveCodec.Deserialize(content, (Action)delegate(string m) { Plugin.Log.LogWarning((object)("[SAVE] " + m)); }); if (val.Pets.Count == 0) { Plugin.Log.LogWarning((object)("[SAVE] '" + path + "' exists (" + new FileInfo(path).Length + " bytes) but held no readable pet — previous write may have been interrupted.")); return null; } if (val.Pets.Count > 1) { Plugin.Log.LogWarning((object)("[SAVE] '" + path + "' holds " + val.Pets.Count + " pet rows; loading only the first (Beastwhispering keeps a single pet).")); } return val.Pets[0]; } internal static void Save(Character player, PetSave pet) { Store.Save(player, pet); } internal static void Clear(Character player) { Store.Clear(player); } internal static PetSave Load(Character player) { return Store.Load(player); } } internal static class PetSenseTable { private const string FileName = "PetSenses.json"; private const string Tag = "[SCENT]"; private static readonly TableLoader _loader = new TableLoader("PetSenses.json", "[SCENT]", "species nose", "species noses", (Func, Dictionary>)PetSenses.Parse, (Func, Dictionary, Dictionary>)PetSenses.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[SCENT]", Validate); internal static Dictionary Table => _axis.Table; internal static PetSenseEntry Resolve(string speciesId) { return PetSenses.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; foreach (PetSenseEntry value2 in Table.Values) { SenseMap senseMap = new SenseMap(); foreach (SenseDef sense in value2.Senses) { num++; List list = new List(); if (string.Equals(sense.Kind, "character", StringComparison.OrdinalIgnoreCase)) { senseMap.Characters.Add(sense); num4++; if (sense.CharacterCandidates.Count > 0) { list.Add(string.Format("{0} character-name matcher(s) [{1}]", sense.CharacterCandidates.Count, string.Join(", ", sense.CharacterCandidates.ToArray()))); } if (sense.FactionCandidates.Count > 0) { list.Add("faction matcher(s) [" + string.Join(", ", sense.FactionCandidates.ToArray()) + "]"); } if (sense.ComponentCandidates.Count > 0) { list.Add("component matcher(s) [" + string.Join(", ", sense.ComponentCandidates.ToArray()) + "]"); } string arg = (string.Equals(sense.Scope, "any", StringComparison.OrdinalIgnoreCase) ? "" : (", scope " + sense.Scope)); Plugin.Log.LogMessage((object)("[SCENT] '" + value2.Species + "/" + sense.Key + "' armed: " + string.Join(" + ", list.ToArray()) + " " + $"(character kind, radius {sense.Radius:F0}m{arg})")); continue; } if (sense.ItemId.HasValue) { if ((Object)(object)ResourcesPrefabManager.Instance.GetItemPrefab(sense.ItemId.Value) == (Object)null) { Plugin.Log.LogWarning((object)(string.Format("{0} '{1}/{2}': authored ItemID {3} ", "[SCENT]", value2.Species, sense.Key, sense.ItemId.Value) + "is not a known catalog item — that id can never match (name candidates still arm).")); } else if (ClaimId(senseMap, sense.ItemId.Value, sense, value2.Species)) { num2++; list.Add($"ItemID {sense.ItemId.Value}"); } } foreach (string itemCandidate in sense.ItemCandidates) { if (ItemNameIndex.TryResolveCatalog(itemCandidate, out var itemId, out var via) && ClaimId(senseMap, itemId, sense, value2.Species)) { num2++; list.Add(string.Format("ItemID {0} (via {1} — put the number in {2} to make it locale-proof)", itemId, via, "PetSenses.json")); } if (senseMap.ByName.TryGetValue(itemCandidate, out var value)) { if (value != sense) { Plugin.Log.LogWarning((object)("[SCENT] '" + value2.Species + "/" + sense.Key + "': name '" + itemCandidate + "' already claimed by sense '" + value.Key + "' — later entry ignored (fix PetSenses.json).")); } } else { senseMap.ByName[itemCandidate] = sense; num3++; } } if (sense.ItemCandidates.Count > 0) { list.Add(string.Format("{0} live-name matcher(s) [{1}]", sense.ItemCandidates.Count, string.Join(", ", sense.ItemCandidates.ToArray()))); } Plugin.Log.LogMessage((object)("[SCENT] '" + value2.Species + "/" + sense.Key + "' armed: " + ((list.Count > 0) ? string.Join(" + ", list.ToArray()) : "NOTHING — sense is inert (fix the table)"))); } if (senseMap.Count > 0) { dictionary[value2.Species] = senseMap; } } ScentSense.Rebuild(dictionary); Plugin.Log.LogMessage((object)(string.Format("{0} boot check: {1} species nose(s), {2} sense(s) — ", "[SCENT]", Table.Count, num) + $"{num2} catalog id(s), {num3} live-name matcher(s), {num4} character sense(s). " + "Gatherable-class nodes match by name (not in the catalog, live ItemID=0 — expected).")); } private static bool ClaimId(SenseMap map, int id, SenseDef def, string species) { if (map.ById.TryGetValue(id, out var value)) { if (value != def) { Plugin.Log.LogWarning((object)(string.Format("{0} '{1}/{2}': ItemID {3} already claimed by ", "[SCENT]", species, def.Key, id) + "sense '" + value.Key + "' — later claim ignored (fix PetSenses.json).")); } return false; } map.ById[id] = def; return true; } } [HarmonyPatch(typeof(ProximityCondition), "CheckIsValid")] internal static class PetSigilComboAlias { internal static bool Attached; private static bool _reentrant; private static float _lastFireLog = -999f; private static void Postfix(ProximityCondition __instance, Character _affectedCharacter, ref bool __result) { try { if (__result || _reentrant) { return; } ItemRequired[] proximityItemReq = __instance.ProximityItemReq; if (proximityItemReq == null || proximityItemReq.Length == 0 || Plugin.EnablePetSigils == null || !Plugin.EnablePetSigils.Value) { return; } Item[] array = null; for (int i = 0; i < proximityItemReq.Length; i++) { Item val = ((ItemQuantity)(proximityItemReq[i]?)).Item; if (!((Object)(object)val == (Object)null) && PetSigilSetup.TryCloneForVanilla(val.ItemID, out var _)) { if (array == null) { array = (Item[])(object)new Item[proximityItemReq.Length]; } array[i] = val; } } if (array == null) { return; } try { _reentrant = true; for (int j = 0; j < proximityItemReq.Length; j++) { if ((Object)(object)array[j] != (Object)null && PetSigilSetup.TryCloneForVanilla(array[j].ItemID, out var clonePrefab2)) { ((ItemQuantity)proximityItemReq[j]).Item = clonePrefab2; } } __result = ((EffectCondition)__instance).CheckIsValid(_affectedCharacter); } finally { _reentrant = false; for (int k = 0; k < proximityItemReq.Length; k++) { if ((Object)(object)array[k] != (Object)null) { ((ItemQuantity)proximityItemReq[k]).Item = array[k]; } } } if (__result && Time.unscaledTime - _lastFireLog > 1f) { _lastFireLog = Time.unscaledTime; Plugin.Log.LogMessage((object)"[PETSIGIL] combo alias: proximity satisfied by a pet circle (vanilla ids re-run as clones)."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[PETSIGIL] combo alias postfix failed (vanilla result kept): " + ex.Message)); } } } internal enum SigilCastSource { DevVerb, Skill, Auto, Echo } internal struct SigilOverrides { public bool BypassCooldown; public bool ArmCooldown; } internal static class PetSigilCommand { private static Item _lastSpawned; internal static bool Cast(Plugin p, string key, SigilCastSource source, SigilOverrides ov) { string text = $"[PETSIGIL] {source} '{key}' ->"; if (!Plugin.EnablePetSigils.Value) { Plugin.Log.LogMessage((object)(text + " SKIPPED: [Sigils] EnablePetSigils is off.")); return false; } if (PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogMessage((object)(text + " SKIPPED: guest client (pet sigils are master-only in the spike).")); return false; } Pet activePet = p.ActivePet; CompanionBody val = ((Companion)(activePet?)).Body; if ((Object)(object)val == (Object)null) { Plugin.Log.LogMessage((object)(text + " SKIPPED: no active pet body.")); return false; } if (!PetSigilSetup.CloneIdByKey.TryGetValue(key ?? "", out var value)) { Plugin.Log.LogMessage((object)(text + " SKIPPED: unknown sigil key (use one of: " + string.Join(", ", new List(PetSigilSetup.CloneIdByKey.Keys).ToArray()) + ").")); return false; } double num = activePet.State?.SigilCooldownSecondsLeft ?? 0.0; if (num > 0.0 && !ov.BypassCooldown) { Plugin.Log.LogMessage((object)$"{text} SKIPPED: on cooldown ({num:F0}s left)."); return false; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val2 = ((instance != null) ? instance.GetItemPrefab(value) : null); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)$"{text} SKIPPED: clone ItemID {value} not registered (SL pack failed — see the [PETSIGIL] boot lines)."); return false; } return Spawn(p, activePet, val, key, value, val2, text, ov); } private static bool Spawn(Plugin p, Pet pet, CompanionBody body, string key, int cloneId, Item prefab, string tag, SigilOverrides ov) { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_lastSpawned != (Object)null && _lastSpawned.IsInWorld) { Plugin.Log.LogMessage((object)("[PETSIGIL] replacing the previous pet circle ('" + _lastSpawned.Name + "').")); ItemManager instance = ItemManager.Instance; if (instance != null) { instance.DestroyItem(_lastSpawned.UID); } } _lastSpawned = null; Character val = ((((Companion)pet).Anchor != null && ((Companion)pet).Anchor.HasLiveAnchor) ? ((Companion)pet).Anchor.Current : Plugin.LocalPlayerCharacter); Item val2 = Object.Instantiate(prefab); ((Component)val2).gameObject.SetActive(true); ((Component)val2).transform.position = SigilSense.GroundSnap(((Component)body).transform.position); val2.ForceUpdateParentChange(); Ephemeral componentInChildren = ((Component)val2).GetComponentInChildren(); if ((Object)(object)componentInChildren != (Object)null && (Object)(object)val != (Object)null) { componentInChildren.SetSourceCharacter(val); } _lastSpawned = val2; if (ov.ArmCooldown && pet.State != null) { pet.State.SigilCooldownSecondsLeft = Plugin.PetSigilCooldownSeconds.Value; } Plugin.Log.LogMessage((object)($"{tag} FIRED: '{val2.Name}' (id={cloneId}) at the pet's feet " + $"({((Component)val2).transform.position.x:F1}, {((Component)val2).transform.position.y:F1}, {((Component)val2).transform.position.z:F1}), " + (((Object)(object)componentInChildren != (Object)null) ? $"lifespan {componentInChildren.LifeSpan:F0}s" : "NO Ephemeral") + ", caster '" + (((Object)(object)val != (Object)null) ? val.Name : "none") + "'" + (ov.ArmCooldown ? $", cooldown armed {Plugin.PetSigilCooldownSeconds.Value:F0}s" : "") + " — `sigildump` shows detection.")); return true; } internal static void PetSigilVerb(Plugin p, Character player, string[] parts) { //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) List list = new List(); for (int i = 1; i < ((parts != null) ? parts.Length : 0); i++) { if (!string.IsNullOrEmpty(parts[i])) { list.Add(parts[i].Trim()); } } bool flag = list.Remove("here"); string text = ((list.Count > 0) ? list[0] : null); if (string.IsNullOrEmpty(text)) { Plugin.Log.LogWarning((object)("[PETSIGIL] usage: petsigil [here] — keys: " + string.Join(", ", new List(PetSigilSetup.CloneIdByKey.Keys).ToArray()) + ".")); return; } if (!flag) { Cast(p, text, SigilCastSource.DevVerb, new SigilOverrides { BypassCooldown = true, ArmCooldown = false }); return; } if (!PetSigilSetup.CloneIdByKey.TryGetValue(text, out var value) || (Object)(object)player == (Object)null) { Plugin.Log.LogWarning((object)("[PETSIGIL] 'here': unknown key '" + text + "' or no player.")); return; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(value) : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)$"[PETSIGIL] clone {value} not registered."); return; } Item val2 = Object.Instantiate(val); ((Component)val2).gameObject.SetActive(true); ((Component)val2).transform.position = SigilSense.GroundSnap(((Component)player).transform.position + ((Component)player).transform.forward * 1.5f); val2.ForceUpdateParentChange(); Ephemeral componentInChildren = ((Component)val2).GetComponentInChildren(); if (componentInChildren != null) { componentInChildren.SetSourceCharacter(player); } Plugin.Log.LogMessage((object)$"[PETSIGIL] DevVerb '{text}' -> FIRED (here): '{val2.Name}' (id={value}) at your feet — compare against `sigilspawn {text}`."); } } internal static class PetSigilSetup { internal static readonly (string Key, int VanillaId, int CloneId)[] Pairs; internal static readonly Dictionary CloneIdByKey; internal static readonly Dictionary VanillaToClone; private static readonly Dictionary _clonePrefabByVanillaId; private static readonly Dictionary _originalScale; static PetSigilSetup() { Pairs = new(string, int, int)[4] { ("Fire", 8000010, 87150), ("Frost", 8000040, 87151), ("Air", 8000050, 87152), ("Blood", 8000011, 87153) }; _clonePrefabByVanillaId = new Dictionary(); _originalScale = new Dictionary(); CloneIdByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); VanillaToClone = new Dictionary(); (string, int, int)[] pairs = Pairs; for (int i = 0; i < pairs.Length; i++) { var (key, key2, value) = pairs[i]; CloneIdByKey[key] = value; VanillaToClone[key2] = value; } } internal static bool TryCloneForVanilla(int vanillaId, out Item clonePrefab) { if (_clonePrefabByVanillaId.TryGetValue(vanillaId, out clonePrefab)) { return (Object)(object)clonePrefab != (Object)null; } if (!VanillaToClone.TryGetValue(vanillaId, out var value)) { clonePrefab = null; return false; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; clonePrefab = ((instance != null) ? instance.GetItemPrefab(value) : null); if ((Object)(object)clonePrefab != (Object)null) { _clonePrefabByVanillaId[vanillaId] = clonePrefab; } return (Object)(object)clonePrefab != (Object)null; } internal static void Init() { SL.OnPacksLoaded += ApplyScale; } internal static void ApplyScale() { //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) float value = Plugin.PetSigilScale.Value; foreach (KeyValuePair item in CloneIdByKey) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(item.Value) : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)($"[PETSIGIL] '{item.Key}' clone ItemID {item.Value} not in the registry — its SL_Item XML " + "did not load (check the SideLoader pack); this pet sigil can never spawn.")); continue; } if (!_originalScale.TryGetValue(item.Value, out var value2)) { value2 = ((Component)val).transform.localScale; _originalScale[item.Value] = value2; } ((Component)val).transform.localScale = value2 * value; Ephemeral componentInChildren = ((Component)val).GetComponentInChildren(true); Plugin.Log.LogMessage((object)($"[PETSIGIL] '{item.Key}' clone {item.Value} ('{val.Name}') scaled x{value:F2} " + $"(prefab scale {((Component)val).transform.localScale.x:F2}), " + (((Object)(object)componentInChildren != (Object)null) ? $"lifespan {componentInChildren.LifeSpan:F0}s" : "NO Ephemeral (never expires — clone lost it?)") + ".")); } } internal static string DescribeClone(string key) { //IL_009b: Unknown result type (might be due to invalid IL or missing references) if (!CloneIdByKey.TryGetValue(key, out var value)) { return "'" + key + "': not a pet sigil key"; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(value) : null); if ((Object)(object)val == (Object)null) { return $"'{key}' clone {value}: NOT REGISTERED (SL_Item XML missing/failed)"; } bool flag = _originalScale.ContainsKey(value); Ephemeral componentInChildren = ((Component)val).GetComponentInChildren(true); Collider val2 = FindWorldItemsCollider(val); return $"'{key}' clone {value} ('{val.Name}'): scale {((Component)val).transform.localScale.x:F2}" + (flag ? "" : " (bake hook NEVER RAN)") + ", Ephemeral " + (((Object)(object)componentInChildren != (Object)null) ? $"OK ({componentInChildren.LifeSpan:F0}s)" : "MISSING") + ", WorldItems collider " + (((Object)(object)val2 != (Object)null) ? ("OK ('" + ((Object)val2).name + "')") : "MISSING"); } internal static bool CloneHealthy(string key) { if (!CloneIdByKey.TryGetValue(key, out var value)) { return false; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(value) : null); if ((Object)(object)val != (Object)null && _originalScale.ContainsKey(value) && (Object)(object)((Component)val).GetComponentInChildren(true) != (Object)null) { return (Object)(object)FindWorldItemsCollider(val) != (Object)null; } return false; } private static Collider FindWorldItemsCollider(Item prefab) { Collider[] componentsInChildren = ((Component)prefab).GetComponentsInChildren(true); foreach (Collider val in componentsInChildren) { if (((1 << ((Component)val).gameObject.layer) & Global.WorldItemsMask) != 0) { return val; } } return null; } } internal static class PetSkillSetup { internal const int HealPetItemId = 87000; internal const int HuntAsOneItemId = 87001; internal const int PetCommandItemId = 87002; internal const int ReleasePetItemId = 87003; internal const int PetGiftItemId = 87004; internal const int ForTheKillItemId = 87005; internal const int ScatologyItemId = 87006; internal const int BeastOfBurdenItemId = 87007; internal const int WildUnknownItemId = 87008; internal const int CommunionItemId = 87009; internal static readonly int[] AllSkillIds = new int[10] { 87000, 87001, 87002, 87003, 87004, 87005, 87006, 87007, 87008, 87009 }; private const int ConjureDonorItemId = 8100150; internal static void Init() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0021: 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_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Expected O, but got Unknown //IL_00cf: 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_00df: 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_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Expected O, but got Unknown //IL_016e: Expected O, but got Unknown //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01c3: Expected O, but got Unknown //IL_01c3: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Expected O, but got Unknown //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_021e: Expected O, but got Unknown //IL_0281: Unknown result type (might be due to invalid IL or missing references) //IL_0286: Unknown result type (might be due to invalid IL or missing references) //IL_0291: Unknown result type (might be due to invalid IL or missing references) //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02a3: Unknown result type (might be due to invalid IL or missing references) //IL_02b8: Expected O, but got Unknown //IL_02b8: Unknown result type (might be due to invalid IL or missing references) //IL_02bd: Unknown result type (might be due to invalid IL or missing references) //IL_02c8: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Unknown result type (might be due to invalid IL or missing references) //IL_02da: Unknown result type (might be due to invalid IL or missing references) //IL_02ef: Expected O, but got Unknown //IL_02ef: Unknown result type (might be due to invalid IL or missing references) //IL_02f4: Unknown result type (might be due to invalid IL or missing references) //IL_02ff: Unknown result type (might be due to invalid IL or missing references) //IL_030a: Unknown result type (might be due to invalid IL or missing references) //IL_0311: Unknown result type (might be due to invalid IL or missing references) //IL_0326: Expected O, but got Unknown //IL_0326: Unknown result type (might be due to invalid IL or missing references) //IL_032b: Unknown result type (might be due to invalid IL or missing references) //IL_0336: Unknown result type (might be due to invalid IL or missing references) //IL_0341: Unknown result type (might be due to invalid IL or missing references) //IL_0348: Unknown result type (might be due to invalid IL or missing references) //IL_035d: Expected O, but got Unknown SL.OnPacksLoaded += DonorBootCheck; SkillRegistry.Register(new SkillSpec { ItemId = 87000, Label = "HealPet", OnCast = delegate { CompanionAnchor val2 = ((Companion)(Plugin.Instance?.ActivePet?)).Anchor; bool flag = (PhotonNetwork.isNonMasterClientInRoom ? PetProxyClient.RequestHealFull() : (val2 != null && val2.Heal())); Plugin.Log.LogMessage((object)(flag ? "[SKILL] Heal Pet cast — healed to full." : "[SKILL] Heal Pet cast — no live pet anchor to heal.")); }, Icon = DynamicIcon.Fixed("HealPet.png") }); SkillSpec val = new SkillSpec(); val.ItemId = 87001; val.Label = "HuntAsOne"; val.OnCast = delegate { PetCommands.FireSpecialAttack(Plugin.Instance, fromSkill: true); }; val.CastAnim = HuntCastPick; val.Icon = DynamicIcon.Fixed("HuntAsOne.png"); SkillRegistry.Register(val); SkillRegistry.Register(new SkillSpec { ItemId = 87002, Label = "PetCommand", OnCast = delegate { PetCommands.TogglePetCommand(Plugin.Instance); }, Icon = new DynamicIcon { Select = delegate { Plugin instance = Plugin.Instance; bool? obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; obj = ((activePet != null) ? new bool?(((Companion)activePet).Stance.Passive) : ((bool?)null)); } bool? flag = obj; return (flag != true) ? "disengage" : "engage"; }, Sprites = { ["engage"] = "PetCommandEngage.png" }, Sprites = { ["disengage"] = "PetCommandDisengage.png" } } }); SkillRegistry.Register(new SkillSpec { ItemId = 87003, Label = "ReleasePet", OnCast = delegate { Plugin.Instance.ReleasePet(); }, Icon = DynamicIcon.Fixed("ReleasePet.png") }); SkillRegistry.Register(new SkillSpec { ItemId = 87004, Label = "PetGift", OnCast = delegate(Character player) { PetGifting.Cast(player); }, Icon = DynamicIcon.Fixed("PetGift.png") }); val = new SkillSpec(); val.ItemId = 87005; val.Label = "ForTheKill"; val.OnCast = delegate(Character player) { ForTheKillSkill.Cast(player); }; val.CastAnim = HuntCastPick; val.Icon = DynamicIcon.Fixed("ForTheKill.png"); SkillRegistry.Register(val); SkillRegistry.Register(new SkillSpec { ItemId = 87006, Label = "Scatology", Passive = true, Icon = DynamicIcon.Fixed("Scatology.png") }); SkillRegistry.Register(new SkillSpec { ItemId = 87007, Label = "BeastOfBurden", Passive = true, Icon = DynamicIcon.Fixed("BeastOfBurden.png") }); SkillRegistry.Register(new SkillSpec { ItemId = 87008, Label = "WildUnknown", Passive = true, Icon = DynamicIcon.Fixed("WildUnknown.png") }); SkillRegistry.Register(new SkillSpec { ItemId = 87009, Label = "Communion", Passive = true, Icon = DynamicIcon.Fixed("Communion.png") }); } private static void DonorBootCheck() { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item obj = ((instance != null) ? instance.GetItemPrefab(8100150) : null); Skill val = (Skill)(object)((obj is Skill) ? obj : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)($"[GIFT] boot check: the Conjure donor (ItemID {8100150}) didn't resolve — " + "Pet Gift / Heal Pet cloned from an unknown base?")); } else { Plugin.Log.LogMessage((object)($"[GIFT] boot check: Conjure donor {8100150} ('{((Item)val).Name}') " + $"Lifespan={val.Lifespan:0.##}s PlayEndAnim={val.PlayEndAnim}" + ((val.Lifespan > 0f) ? " — Lifespan>0: casts DEFER StartCooldown and hold InCooldown() via m_inProgress; the refund path relies on SetRemaining(0) clearing that flag." : " (Lifespan 0 = the cooldown starts at cast; the refund path degenerates to a plain clear)."))); } } private static CastPick HuntCastPick(Character player) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 //IL_0049: 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) if (!((Object)(object)((player != null) ? player.CurrentWeapon : null) != (Object)null) || (int)player.CurrentWeapon.Type != 200) { return new CastPick(Plugin.HuntAsOneMeleeAnimation.Value, (SpellCastModifier)2); } return new CastPick(Plugin.HuntAsOneBowAnimation.Value, (SpellCastModifier)0); } } internal class PetSpecialAttack : MonoBehaviour { internal struct SpecialFireReport { public bool Started; public string Summary; public bool HadTarget; public bool SynergyDeferred; public static SpecialFireReport Skip(string why) { return new SpecialFireReport { Started = false, Summary = "SKIPPED: " + why }; } public static SpecialFireReport Go(string how) { return new SpecialFireReport { Started = true, Summary = "FIRED " + how }; } } internal struct FireOverrides { public bool BypassCooldown; public bool ArmCooldown; public float DamageMultExtra; public Action OnHitExtra; public bool SuppressSynergy; public bool BraceAsMelee; public string Tag; public static FireOverrides Default => new FireOverrides { ArmCooldown = true, DamageMultExtra = 1f, Tag = "[SPECIALATK]" }; } private CompanionCombat _combat; private CompanionBody _body; private Animator _anim; private string _speciesId; private float _lastCooldownSeconds; private bool _windingUp; private float _windupStartedAt; private float _windupExpectedSeconds; private bool _noRigWarned; internal float CooldownRemaining => (float)(Plugin.Instance?.ActivePet?.State?.SpecialCooldownSecondsLeft).GetValueOrDefault(); internal float TableCooldownSeconds { get { SpecialAttackDef val = default(SpecialAttackDef); if (!SpecialAttackTable.TryGet(SpecialAttacks.Table, _speciesId, ref val)) { return 0f; } return Mathf.Max(0f, val.CooldownSeconds); } } internal void InjectCombat(CompanionCombat combat) { _combat = combat; } private void SetCooldown(float secondsLeft) { Pet pet = Plugin.Instance?.ActivePet; if (pet?.State != null && !((Object)(object)((Companion)pet).Body != (Object)(object)_body)) { pet.State.SpecialCooldownSecondsLeft = Mathf.Max(0f, secondsLeft); } } internal void OverrideCooldown(float secondsLeft, float fullSeconds = -1f) { secondsLeft = Mathf.Max(0f, secondsLeft); SetCooldown(secondsLeft); _lastCooldownSeconds = ((fullSeconds > 0f) ? fullSeconds : secondsLeft); } private void Start() { _body = ((Component)this).GetComponent(); _anim = ((Component)this).GetComponent(); _speciesId = _body?.SpeciesId ?? ""; float cooldownRemaining = CooldownRemaining; if (cooldownRemaining > 0f) { _lastCooldownSeconds = cooldownRemaining; Plugin.Log.LogMessage((object)($"[HAO] body ready; the pet-special cooldown is still running at {cooldownRemaining:0.#}s " + "(it survived on the save aggregate, as designed).")); } } private void OnDisable() { if (_windingUp) { _windingUp = false; Plugin.Log.LogMessage((object)"[SPECIALATK] windup cleared on component disable — the resolve coroutine died with the body; the next press is free (BUG-WINDUPSTUCK guard)."); } } private bool WindingUp() { if (!_windingUp) { return false; } if (!RangedSpecial.WindupStale(_windupStartedAt, _windupExpectedSeconds, Time.time, 2f)) { return true; } _windingUp = false; Plugin.Log.LogWarning((object)("[SPECIALATK] stale windup cleared — flagged mid-windup " + $"{Time.time - _windupStartedAt:F1}s ago for a {_windupExpectedSeconds:F2}s windup, so the resolve " + "coroutine never finished (body swap / teardown mid-strike). Letting this press through.")); return false; } internal bool CanFireIgnoringCooldown(Character player, out string reason) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Invalid comparison between Unknown and I4 if ((Object)(object)_combat == (Object)null) { reason = "Your companion cannot fight right now."; return false; } if (WindingUp()) { reason = "Your companion is already mid-strike."; return false; } SpecialAttackDef val = default(SpecialAttackDef); if (!SpecialAttackTable.TryGet(SpecialAttacks.Table, _speciesId, ref val)) { reason = "A " + _speciesId + " has no signature attack."; return false; } AttackKind kind = val.Kind; bool num = Plugin.EnableBrace.Value && !PhotonNetwork.isNonMasterClientInRoom; Plugin instance = Plugin.Instance; bool? obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? new bool?(anchor.HasLiveAnchor) : ((bool?)null)); } } bool? flag = obj; BraceRoute val2 = SpecialAttackTable.RouteBrace(kind, num, flag == true); if ((int)val2 == 1) { if (BraceDriver.Active) { reason = "Your companion is already braced."; return false; } reason = null; return true; } if ((Object)(object)_anim == (Object)null) { reason = "Your companion cannot fight right now."; return false; } if (!HasTrigger(val.TriggerParam)) { reason = "A " + _speciesId + " has no signature attack."; return false; } reason = null; return true; } internal SpecialFireReport Fire(Action onSyncedHit = null) { return Fire(onSyncedHit, FireOverrides.Default); } internal SpecialFireReport Fire(Action onSyncedHit, FireOverrides ov) { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Invalid comparison between Unknown and I4 //IL_029b: Unknown result type (might be due to invalid IL or missing references) //IL_029d: Invalid comparison between Unknown and I4 //IL_02c0: Unknown result type (might be due to invalid IL or missing references) //IL_02c2: Invalid comparison between Unknown and I4 //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_032e: Unknown result type (might be due to invalid IL or missing references) //IL_0364: Unknown result type (might be due to invalid IL or missing references) //IL_0369: Unknown result type (might be due to invalid IL or missing references) //IL_036a: Unknown result type (might be due to invalid IL or missing references) //IL_036c: Invalid comparison between Unknown and I4 //IL_039e: Unknown result type (might be due to invalid IL or missing references) //IL_03a0: Invalid comparison between Unknown and I4 //IL_0484: Unknown result type (might be due to invalid IL or missing references) //IL_053a: Unknown result type (might be due to invalid IL or missing references) //IL_053c: Invalid comparison between Unknown and I4 //IL_055f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_combat == (Object)null) { return SpecialFireReport.Skip("no CompanionCombat on the body (is [Combat] EnableCombat on?)"); } if (WindingUp()) { return SpecialFireReport.Skip("still mid-windup from the previous press"); } if (!ov.BypassCooldown && CooldownRemaining > 0f) { return SpecialFireReport.Skip($"pet-special on cooldown, {CooldownRemaining:F1}s of {_lastCooldownSeconds:F0}s left" + (Plugin.SyncSkillCooldownToPet.Value ? " (the skill's own cooldown should have refused this press — see [HAO] skill cooldown stamped)" : " (the Hunt-as-One SKILL re-arms in ~1s, but the pet special is gated separately by SpeciesSpecialAttacks CooldownSeconds — Bug 30; set [HuntAsOne] SyncSkillCooldownToPet=true)")); } SpecialAttackDef val = default(SpecialAttackDef); if (!SpecialAttackTable.TryGet(SpecialAttacks.Table, _speciesId, ref val)) { return SpecialFireReport.Skip("'" + _speciesId + "' has no SpeciesSpecialAttacks row"); } int num2; if (!ov.BraceAsMelee) { AttackKind kind = val.Kind; bool num = Plugin.EnableBrace.Value && !PhotonNetwork.isNonMasterClientInRoom; Plugin instance = Plugin.Instance; bool? obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? new bool?(anchor.HasLiveAnchor) : ((bool?)null)); } } bool? flag = obj; num2 = (int)SpecialAttackTable.RouteBrace(kind, num, flag == true); } else { num2 = 0; } BraceRoute val2 = (BraceRoute)num2; if ((int)val2 == 1) { if (BraceDriver.Active) { return SpecialFireReport.Skip("already braced — the window is still open"); } if (ov.ArmCooldown) { SetCooldown(val.CooldownSeconds); _lastCooldownSeconds = val.CooldownSeconds; } _combat.PlayVocal(1); BraceDriver.Enter(_body, _combat, val); Plugin.Log.LogMessage((object)((ov.Tag ?? "[SPECIALATK]") + " '" + _speciesId + "' BRACES " + string.Format("({0:F1}s window, statuses={1}, riposte x{2:F2}", Plugin.BraceWindowSeconds.Value, val.StatusEffectId ?? "none", val.DamageMultiplier) + (ov.ArmCooldown ? $", cooldown={val.CooldownSeconds:F0}s" : "") + ").")); return new SpecialFireReport { Started = true, SynergyDeferred = true, HadTarget = true, Summary = $"FIRED (Brace, {Plugin.BraceWindowSeconds.Value:F1}s window" + (ov.ArmCooldown ? $", cooldown {val.CooldownSeconds:F0}s set)" : ")") }; } if ((int)val2 == 2) { Plugin.Log.LogWarning((object)("[SPECIALATK] '" + _speciesId + "' is table-Brace but there is no live anchor (the ghost body enemies hit is down) — using the melee strike.")); } else if ((int)val2 == 3) { Plugin.Log.LogMessage((object)"[SPECIALATK] brace disabled ([Brace] EnableBrace=false) — melee strike."); } Character specialAttackTarget = _combat.SpecialAttackTarget; if ((Object)(object)_anim == (Object)null) { return SpecialFireReport.Skip("no Animator on the body"); } if (!HasTrigger(val.TriggerParam)) { LogMissingTrigger(val.TriggerParam); return SpecialFireReport.Skip("animator trigger '" + val.TriggerParam + "' missing (see the [SPECIALATK] param dump)"); } RangedRoute val3 = SpecialAttackTable.Route(val.Kind, Plugin.EnableRangedSpecial.Value, _body?.RangedRig != null && _body.RangedRig.Ready); if ((int)val3 == 2 && !_noRigWarned) { _noRigWarned = true; Plugin.Log.LogWarning((object)("[SPECIALATK] '" + _speciesId + "' is table-Ranged but this body has no captured projectile (ghost stand-in, or the capture failed — see the [BOLT] lines at tame/re-form) — using the melee strike. (Warned once.)")); } else if ((int)val3 == 3) { Plugin.Log.LogMessage((object)"[SPECIALATK] ranged special disabled ([HuntAsOne] EnableRangedSpecial=false) — melee strike."); } if (ov.ArmCooldown) { SetCooldown(val.CooldownSeconds); _lastCooldownSeconds = val.CooldownSeconds; } _windingUp = true; _windupStartedAt = Time.time; _windupExpectedSeconds = val.WindupSeconds; _anim.SetTrigger(val.TriggerParam); _combat.PlayVocal(1); Plugin.Log.LogMessage((object)((ov.Tag ?? "[SPECIALATK]") + " '" + _speciesId + "' firing '" + val.TriggerParam + "' at '" + (((Object)(object)specialAttackTarget != (Object)null) ? specialAttackTarget.Name : "nothing (no target — honest whiff)") + "' " + string.Format("(kind={0} windup={1:F2}s status={2} dmgMult={3:F2}", val3, val.WindupSeconds, val.StatusEffectId ?? "none", val.DamageMultiplier) + ((ov.DamageMultExtra > 0f && !Mathf.Approximately(ov.DamageMultExtra, 1f)) ? $" x{ov.DamageMultExtra:F2} extra" : "") + (ov.ArmCooldown ? $" cooldown={val.CooldownSeconds:F0}s" : " (pet-special cooldown untouched)") + ").")); ((MonoBehaviour)this).StartCoroutine(((int)val3 == 1) ? ResolveRangedAfterWindup(val, specialAttackTarget, onSyncedHit, ov) : ResolveAfterWindup(val, specialAttackTarget, onSyncedHit, ov)); SpecialFireReport result = SpecialFireReport.Go($"({val3}, windup {val.WindupSeconds:F2}s" + (ov.ArmCooldown ? $", cooldown {val.CooldownSeconds:F0}s set)" : ")")); result.HadTarget = (Object)(object)specialAttackTarget != (Object)null; return result; } private IEnumerator ResolveAfterWindup(SpecialAttackDef def, Character target, Action onSyncedHit, FireOverrides ov) { yield return (object)new WaitForSeconds(def.WindupSeconds); _windingUp = false; if (!StillResolvable(target)) { if (!ov.SuppressSynergy) { HuntSynergy.ReportPet((StrikeOutcome)6); } } else { Vector3 val = ((Component)target).transform.position - ((Component)this).transform.position; Vector3 normalized = ((Vector3)(ref val)).normalized; ResolveHit(def, target, normalized, onSyncedHit, null, ov); } } private IEnumerator ResolveRangedAfterWindup(SpecialAttackDef def, Character target, Action onSyncedHit, FireOverrides ov) { yield return (object)new WaitForSeconds(def.WindupSeconds); _windingUp = false; if (!StillResolvable(target)) { if (!ov.SuppressSynergy) { HuntSynergy.ReportPet((StrikeOutcome)6); } yield break; } RangedAttackRig rig = _body?.RangedRig; Pet pet = Plugin.Instance?.ActivePet; Character val = ((((Companion)(pet?)).Anchor != null && ((Companion)pet).Anchor.HasLiveAnchor) ? ((Companion)pet).Anchor.Current : Plugin.LocalPlayerCharacter); if (rig != null && (((Companion)(pet?)).Anchor == null || !((Companion)pet).Anchor.HasLiveAnchor)) { Plugin.Log.LogMessage((object)"[SPECIALATK] no live anchor — the PLAYER owns this bolt's faction snapshot (same allied result)."); } Vector3 muzzle = MuzzlePosition(rig); Vector3 val2 = target.CenterPosition - muzzle; Vector3 aim = ((Vector3)(ref val2)).normalized; List list = FriendlyColliders(val); muzzle = ClearMuzzle(muzzle, target, list, ref aim); if (rig == null || !rig.EnsureSetup(val) || !rig.Fire(target, muzzle, aim, (Func)IsValidBoltTarget, (IList)list, Plugin.LocalPlayerCharacter)) { Plugin.Log.LogWarning((object)"[SPECIALATK] bolt launch failed — resolving as a melee strike instead (see the [BOLT] lines)."); if (StillResolvable(target)) { ResolveHit(def, target, aim, onSyncedHit, null, ov); } else if (!ov.SuppressSynergy) { HuntSynergy.ReportPet((StrikeOutcome)6); } yield break; } float timeout = RangedSpecial.TimeoutSeconds(Plugin.BoltMaxFlightSeconds.Value, rig.ProjectileLifespan); float t0 = Time.time; while (Time.time - t0 < timeout && (int)rig.Volley == 0) { yield return null; } if ((int)rig.Volley == 1 && (Object)(object)rig.ResolvedHit != (Object)null && (Object)(object)_combat != (Object)null) { Character resolvedHit = rig.ResolvedHit; val2 = ((Component)resolvedHit).transform.position - muzzle; Vector3 normalized = ((Vector3)(ref val2)).normalized; ResolveHit(def, resolvedHit, normalized, onSyncedHit, rig.NativeDamage, ov, atImpact: true); yield break; } bool flag = (int)rig.Volley == 0; if (!ov.SuppressSynergy) { HuntSynergy.ReportPet((StrikeOutcome)(flag ? 7 : 6)); } Plugin.Log.LogMessage((object)("[SPECIALATK] bolt missed (" + (flag ? "timeout" : "all bolts exploded clean") + ") — no damage, no status, no synced strike; cooldown stands.")); } private bool StillResolvable(Character target) { if ((Object)(object)target == (Object)null) { Plugin.Log.LogMessage((object)"[SPECIALATK] no target to strike -- the swing whiffs, nothing lands."); return false; } if (!target.Alive) { Plugin.Log.LogMessage((object)"[SPECIALATK] target died or vanished during the windup -- no hit landed."); return false; } if ((Object)(object)_combat == (Object)null) { Plugin.Log.LogMessage((object)"[SPECIALATK] pet lost its CompanionCombat mid-windup (body swap?) -- no hit landed."); return false; } return true; } private void ResolveHit(SpecialAttackDef def, Character target, Vector3 dir, Action onSyncedHit, float[] typedProfile, FireOverrides ov, bool atImpact = false) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Invalid comparison between Unknown and I4 //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Invalid comparison between Unknown and I4 //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_0265: Unknown result type (might be due to invalid IL or missing references) string text = ov.Tag ?? "[SPECIALATK]"; float num = Mathf.Max(0f, def.DamageMultiplier) * ((ov.DamageMultExtra > 0f) ? ov.DamageMultExtra : 1f); float num2 = _combat.BaseDamage * num; StrikeOutcome val = (StrikeOutcome)((!Plugin.HonestHits.Value) ? 1 : ((int)HuntSynergy.JudgeStrike(((Component)this).transform.position, ((Component)this).transform.forward, target, atImpact ? 0f : Plugin.PetMeleeRangeMeters.Value, 0f))); if (!ov.SuppressSynergy) { HuntSynergy.ReportPet(val); } if ((int)val != 1) { if ((int)val == 2) { float angleFromTargetForward = Vector3.Angle(((Component)target).transform.forward, ((Component)this).transform.position - ((Component)target).transform.position); CompanionCombat combat = _combat; Plugin instance = Plugin.Instance; object dealer; if (instance == null) { dealer = null; } else { Pet activePet = instance.ActivePet; if (activePet == null) { dealer = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; dealer = ((anchor != null) ? anchor.Current : null); } } HuntSynergy.PlayBlock(target, (MonoBehaviour)(object)combat, num2, dir, angleFromTargetForward, (Character)dealer); Plugin.Log.LogMessage((object)(text + " '" + target.Name + "' BLOCKED the strike — no damage, no status, no hexes.")); } else { Plugin.Log.LogMessage((object)$"{text} strike on '{target.Name}' failed to connect ({val}) — nothing lands."); } onSyncedHit?.Invoke(target); return; } if (typedProfile != null) { _combat.DealDamageTyped(target, num2, dir, typedProfile); } else { _combat.DealDamage(target, num2, dir, -1f); } Plugin.Log.LogMessage((object)($"{text} hit '{target.Name}' for {num2:F0} (base {_combat.BaseDamage:F0} x{num:F2}" + ((typedProfile != null) ? ", bolt-typed" : "") + ").")); Pet pet = Plugin.Instance?.ActivePet; SpecialHitContext specialHitContext = new SpecialHitContext { Pet = pet, Def = def, Target = target }; object dealer2; if (pet == null) { dealer2 = null; } else { CompanionAnchor anchor2 = ((Companion)pet).Anchor; dealer2 = ((anchor2 != null) ? anchor2.Current : null); } specialHitContext.Dealer = (Character)dealer2; specialHitContext.ActiveSigils = (Plugin.EnableSigilSynergies.Value ? SigilSense.ActiveKeysAt(((Component)this).transform.position) : SigilSense.None); SpecialHitContext ctx = specialHitContext; SpeciesRegistry.For(_speciesId).OnSpecialAttackHit(in ctx); ov.OnHitExtra?.Invoke(ctx); onSyncedHit?.Invoke(target); } private Vector3 MuzzlePosition(RangedAttackRig rig) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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_006c: 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) if (rig != null && !string.IsNullOrEmpty(rig.TransformName)) { ShooterTransform[] componentsInChildren = ((Component)this).GetComponentsInChildren(true); foreach (ShooterTransform val in componentsInChildren) { if ((Object)(object)val != (Object)null && ((Object)val).name == rig.TransformName) { return ((Component)val).transform.position; } } } return ((Component)this).transform.position + Vector3.up * 1.2f; } private List FriendlyColliders(Character owner) { List list = new List(); ProjectileCapture.AddColliders((IList)list, (Component)(object)this); ProjectileCapture.AddColliders((IList)list, (Component)(object)owner); Plugin instance = Plugin.Instance; object obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } } ProjectileCapture.AddColliders((IList)list, (Component)obj); ProjectileCapture.AddColliders((IList)list, (Component)(object)Plugin.LocalPlayerCharacter); return list; } private Vector3 ClearMuzzle(Vector3 muzzle, Character target, List friendlies, ref Vector3 aim) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0154: 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_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: 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_0086: 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_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) if (((Vector3)(ref aim)).sqrMagnitude < 1E-06f || friendlies == null || friendlies.Count == 0) { return muzzle; } if (!BlockedByFriendly(muzzle, aim, friendlies, out var who)) { return muzzle; } float num2 = default(float); float num3 = default(float); float num4 = default(float); for (int i = 0; i < 2; i++) { float num = ((i == 0) ? 1f : (-1f)) * 0.8f; RangedSpecial.MuzzleClearanceOffset(aim.x, aim.y, aim.z, num, 0.4f, ref num2, ref num3, ref num4); Vector3 val = muzzle + new Vector3(num2, num3, num4); Vector3 val2 = target.CenterPosition - val; Vector3 normalized = ((Vector3)(ref val2)).normalized; if (!(((Vector3)(ref normalized)).sqrMagnitude < 1E-06f) && !BlockedByFriendly(val, normalized, friendlies, out var _)) { Plugin.Log.LogMessage((object)("[BOLT] muzzle clearance: '" + who + "' was in the way — muzzle stepped " + string.Format("{0} {1:F1}m ", (i == 0) ? "right" : "left", 0.8f) + $"+{0.4f:F1}m up, line is clear.")); aim = normalized; return val; } } Plugin.Log.LogMessage((object)("[BOLT] muzzle clearance: '" + who + "' is in the way and neither sidestep clears it — firing anyway; the ignored-character slot / HitEnemiesOnly / collider pairing carry the shot.")); return muzzle; } private bool BlockedByFriendly(Vector3 muzzle, Vector3 aim, List friendlies, out string who) { //IL_0004: 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_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) who = null; RaycastHit[] array; try { array = Physics.RaycastAll(muzzle, aim, 3.5f, -1, (QueryTriggerInteraction)1); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BOLT] muzzle clearance raycast threw: " + ex.Message + " — skipping the check.")); return false; } float num = float.MaxValue; RaycastHit[] array2 = array; for (int i = 0; i < array2.Length; i++) { RaycastHit val = array2[i]; if (!((Object)(object)((RaycastHit)(ref val)).collider == (Object)null) && friendlies.Contains(((RaycastHit)(ref val)).collider) && RangedSpecial.MuzzleBlocked(true, ((RaycastHit)(ref val)).distance, 3.5f) && !(((RaycastHit)(ref val)).distance >= num)) { num = ((RaycastHit)(ref val)).distance; who = (((Object)(object)((Component)((RaycastHit)(ref val)).collider).transform.root != (Object)null) ? ((Object)((Component)((RaycastHit)(ref val)).collider).transform.root).name : ((Object)((RaycastHit)(ref val)).collider).name); } } return who != null; } internal static bool IsValidBoltTarget(Character c) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Invalid comparison between Unknown and I4 if ((Object)(object)c != (Object)null && c.Alive && c.IsAI) { return (int)c.Faction != 1; } return false; } internal static void ApplyStatuses(Character target, SpecialAttackDef def, Character dealer) { string[] array = SpecialAttackTable.SplitStatuses(def?.StatusEffectId); if (array.Length == 0 || (Object)(object)target == (Object)null) { return; } string text = SpecialAttackTable.PickStatus(array, (double)Random.value); if (array.Length > 1) { Plugin.Log.LogMessage((object)("[SPECIALATK] random status pick: '" + text + "' (from " + def.StatusEffectId + ").")); } if (def.BuildupPercent > 0f) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; if ((Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(text) : null) == (Object)null) { Plugin.Log.LogWarning((object)("[SPECIALATK] status '" + text + "' is not a loadable StatusEffect prefab name — skipped. Run 'statusdump' for the real names, fix SpeciesSpecialAttacks.txt (config override) and 'reloadattacks'.")); } else if (!PetProxyClient.RouteEnemyStatus(target, text, def.BuildupPercent, "[SPECIALATK]")) { StatusEffectManager statusEffectMngr = target.StatusEffectMngr; if (statusEffectMngr != null) { statusEffectMngr.AddStatusEffectBuildUp(text, def.BuildupPercent, dealer); } Plugin.Log.LogMessage((object)$"[SPECIALATK] +{def.BuildupPercent:F0}% '{text}' build-up on '{target.Name}'."); } } else { ApplyStatus(target, text); } } internal static void ApplyStatus(Character target, string statusId, string tag = "[SPECIALATK]") { if (tag != "[PROXY]" && PetProxyClient.RouteEnemyStatus(target, statusId, 0f, tag)) { return; } try { StatusEffectManager statusEffectMngr = target.StatusEffectMngr; StatusEffect val = ((statusEffectMngr != null) ? statusEffectMngr.AddStatusEffect(statusId) : null); if ((Object)(object)val != (Object)null) { Plugin.Log.LogMessage((object)(tag + " applied status '" + statusId + "' to '" + target.Name + "'.")); } else { Plugin.Log.LogWarning((object)(tag + " AddStatusEffect('" + statusId + "') returned null on '" + target.Name + "' -- likely not a real StatusEffect prefab name. Run 'statusdump' to see what's loadable, then fix the table (config override) and its reload verb -- no relaunch needed.")); } } catch (Exception ex) { Plugin.Log.LogWarning((object)(tag + " AddStatusEffect('" + statusId + "') threw: " + ex.Message)); } } private bool HasTrigger(string name) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 AnimatorControllerParameter[] parameters = _anim.parameters; foreach (AnimatorControllerParameter val in parameters) { if ((int)val.type == 9 && val.name == name) { return true; } } return false; } private void LogMissingTrigger(string wanted) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) List list = new List(); AnimatorControllerParameter[] parameters = _anim.parameters; foreach (AnimatorControllerParameter val in parameters) { list.Add($"{val.type}:{val.name}"); } Plugin.Log.LogWarning((object)("[SPECIALATK] '" + _speciesId + "': trigger '" + wanted + "' not found on this Animator. Available params: " + string.Join(", ", list) + ". Fix the trigger name in SpeciesSpecialAttacks.txt (config override), then 'reloadattacks'.")); } internal static void BoltDump(Plugin p, Character player) { //IL_0037: 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_0363: Unknown result type (might be due to invalid IL or missing references) Character val = BodyFactory.FindNearest(player, Plugin.TameRange.Value * 2f, (string)null); if ((Object)(object)val != (Object)null) { Plugin.Log.LogMessage((object)$"[BOLT] (a) census on nearest wild '{val.Name}' ({Vector3.Distance(((Component)player).transform.position, ((Component)val).transform.position):F0}m):"); ShootProjectile[] array = default(ShootProjectile[]); ProjectileCapture.Census(val, ref array); } else { Plugin.Log.LogMessage((object)$"[BOLT] (a) no wild creature within {Plugin.TameRange.Value * 2f:0.#}m to census."); } Pet activePet = p.ActivePet; CompanionBody val2 = ((Companion)(activePet?)).Body; RangedAttackRig val3 = val2?.RangedRig; if (val3 == null) { Plugin.Log.LogMessage((object)("[BOLT] (b) " + (((Object)(object)val2 == (Object)null) ? "no active pet body" : ("'" + val2.SpeciesId + "' has no captured rig (melee species, ghost stand-in, or capture failed at tame — see the tame-time [BOLT] lines)")) + ".")); } else { bool flag = false; if (!string.IsNullOrEmpty(val3.TransformName)) { ShooterTransform[] componentsInChildren = ((Component)val2).GetComponentsInChildren(true); foreach (ShooterTransform val4 in componentsInChildren) { if ((Object)(object)val4 != (Object)null && ((Object)val4).name == val3.TransformName) { flag = true; break; } } } Plugin.Log.LogMessage((object)($"[BOLT] (b) rig on '{val2.SpeciesId}': ready={val3.Ready} projectile='{val3.ProjectileName}' " + $"source='{val3.SourcePath}' force={val3.Force:F0} mode={val3.TargetingModeName} cast={val3.CastPositionName} " + $"shots={val3.ShotCount} lifespan={val3.ProjectileLifespan:F1}s pool={val3.PoolSize} " + "setupOwner=" + (((Object)(object)val3.SetupOwner != (Object)null) ? val3.SetupOwner.Name : "none (pool builds on first fire)") + " " + $"muzzleBone('{val3.TransformName}') on puppet={flag}.")); Plugin.Log.LogMessage((object)("[BOLT] (b) bolt class='" + (string.IsNullOrEmpty(val3.ProjectileTypeName) ? "unknown (no Setup yet)" : val3.ProjectileTypeName) + "' " + $"hitEnemiesOnly={val3.HitEnemiesOnlyArmed} — a RaycastProjectile is filtered by " + "HitEnemiesOnly + the ignored-character slot; a PhysicProjectile by the collider pairing.")); Plugin.Log.LogMessage((object)("[BOLT] (b) native damage=[" + RangedAttackRig.ProfileString(val3.NativeDamage) + "] stripped=[" + ((val3.RemovedPayload.Count > 0) ? string.Join(", ", val3.RemovedPayload) : "nothing yet — strip runs at first Setup") + "].")); } string text = val2?.SpeciesId ?? activePet?.State?.SpeciesId; SpecialAttackDef val5 = default(SpecialAttackDef); if (text != null && SpecialAttackTable.TryGet(SpecialAttacks.Table, text, ref val5)) { Plugin.Log.LogMessage((object)($"[BOLT] (c) table row for '{text}': trigger={val5.TriggerParam} kind={val5.Kind} " + "filter='" + (val5.ProjectileFilter ?? "none") + "' skillPrefab=" + ((val5.ProjectileSkillId != 0) ? val5.ProjectileSkillId.ToString() : "none") + " " + $"windup={val5.WindupSeconds:F2}s dmgMult={val5.DamageMultiplier:F2} " + string.Format("cooldown={0:F0}s; killSwitch={1} ", val5.CooldownSeconds, Plugin.EnableRangedSpecial.Value ? "ON" : "OFF") + $"awaitTimeout={RangedSpecial.TimeoutSeconds(Plugin.BoltMaxFlightSeconds.Value, val3?.ProjectileLifespan ?? 0f):F1}s.")); } else { Plugin.Log.LogMessage((object)("[BOLT] (c) " + ((text == null) ? "no active pet" : ("'" + text + "' has no SpeciesSpecialAttacks row")) + ".")); } } internal static void FireBolt(Plugin p, Character player) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //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) Pet activePet = p.ActivePet; CompanionBody val = ((Companion)(activePet?)).Body; RangedAttackRig val2 = val?.RangedRig; if (val2 == null) { Plugin.Log.LogWarning((object)"[BOLT] firebolt: no captured rig on the active pet (see 'boltdump')."); return; } CompanionCombat combat = ((Companion)activePet).Combat; Character val3 = ((combat != null) ? combat.CurrentTarget : null) ?? BodyFactory.FindNearest(player, 20f, (string)null); if ((Object)(object)val3 == (Object)null) { Plugin.Log.LogWarning((object)"[BOLT] firebolt: no combat target and no wild creature ≤20m to aim at."); return; } Character val4 = ((((Companion)activePet).Anchor != null && ((Companion)activePet).Anchor.HasLiveAnchor) ? ((Companion)activePet).Anchor.Current : player); if (!val2.EnsureSetup(val4)) { Plugin.Log.LogWarning((object)"[BOLT] firebolt: rig setup failed (see the [BOLT] lines)."); return; } Vector3 val5 = ((Component)val).transform.position + Vector3.up * 1.2f; if (!string.IsNullOrEmpty(val2.TransformName)) { ShooterTransform[] componentsInChildren = ((Component)val).GetComponentsInChildren(true); foreach (ShooterTransform val6 in componentsInChildren) { if ((Object)(object)val6 != (Object)null && ((Object)val6).name == val2.TransformName) { val5 = ((Component)val6).transform.position; break; } } } Vector3 val7 = val3.CenterPosition - val5; Vector3 normalized = ((Vector3)(ref val7)).normalized; List list = new List(); ProjectileCapture.AddColliders((IList)list, (Component)(object)val); ProjectileCapture.AddColliders((IList)list, (Component)(object)val4); ProjectileCapture.AddColliders((IList)list, (Component)(object)player); if (!val2.Fire(val3, val5, normalized, (Func)IsValidBoltTarget, (IList)list, player)) { Plugin.Log.LogWarning((object)"[BOLT] firebolt: launch failed."); } else { ((MonoBehaviour)p).StartCoroutine(AwaitBoltVerdict(val2)); } } private static IEnumerator AwaitBoltVerdict(RangedAttackRig rig) { float timeout = RangedSpecial.TimeoutSeconds(Plugin.BoltMaxFlightSeconds.Value, rig.ProjectileLifespan); float t0 = Time.time; while (Time.time - t0 < timeout && (int)rig.Volley == 0) { yield return null; } ManualLogSource log = Plugin.Log; string text = $"[BOLT] firebolt verdict after {Time.time - t0:F1}s: "; object obj; if ((int)rig.Volley != 1) { obj = (((int)rig.Volley == 0) ? "TIMEOUT (no relay reported)" : "MISS (all bolts exploded clean)"); } else { Character resolvedHit = rig.ResolvedHit; obj = "HIT '" + ((resolvedHit != null) ? resolvedHit.Name : null) + "'"; } log.LogMessage((object)(text + (string?)obj + ".")); } } internal static class PetStatusEffects { private struct Def { public PetStatusIconDef Row; public string Png; public string Desc; public PetIcon Icon => Row.Icon; public string Id => Row.Id; public int NumId => Row.NumId; public string Name => Row.Name; public bool Malus => Row.Malus; } private static readonly Dictionary Display = new Dictionary { { (PetIcon)1, ("PetHungry.png", "Your companion is hungry — feed it soon, or the bond will begin to fray.") }, { (PetIcon)2, ("PetStarving.png", "Your companion has gone a full day unfed. Its loyalty is decaying.") }, { (PetIcon)3, ("PetBondFraying.png", "Your companion's trust is fraying. Feed it and fight beside it.") }, { (PetIcon)4, ("PetBondBroken.png", "Your companion's loyalty is nearly spent — at nothing left, it will abandon you.") }, { (PetIcon)5, ("PetBondSteady.png", "Your companion trusts you. The bond steels you both.") }, { (PetIcon)6, ("PetBondDevoted.png", "Your companion would follow you anywhere. The bond runs deep.") }, { (PetIcon)7, ("PetCold.png", "Your companion is uneasy in the cold. Get it somewhere warmer, near a fire, or wrap it in a heating blanket.") }, { (PetIcon)8, ("PetFreezing.png", "Your companion is suffering from the cold — it is losing health and trust. Warm it up before the climate claims it.") }, { (PetIcon)9, ("PetHot.png", "Your companion is uneasy in the heat. Get it somewhere cooler, or wrap it in a cooling blanket.") }, { (PetIcon)10, ("PetOverheating.png", "Your companion is suffering from the heat — it is losing health and trust. Cool it down before the climate claims it.") }, { (PetIcon)11, ("PetScent.png", "Your companion has caught a scent — watch where it points; something interesting is nearby.") }, { (PetIcon)12, ("PetCommunionBroken.png", "The communion is all but severed — the bond grants almost nothing.") }, { (PetIcon)13, ("PetCommunionFraying.png", "The communion is fraying — the bond's gifts are weakening.") }, { (PetIcon)14, ("PetCommunionSteady.png", "The communion holds steady — the bond's gifts flow to you.") }, { (PetIcon)15, ("PetCommunionDevoted.png", "The communion runs deep — the bond's gifts are at their fullest.") }, { (PetIcon)16, ("PetBuffCrystalPowder.png", "Crystal Powder glitters in your companion's blood — its strikes bite deeper.") }, { (PetIcon)17, ("PetBuffAmbraine.png", "The Ambraine's resin sings in your companion's veins — its blows land harder.") }, { (PetIcon)18, ("PetBuffGaberryWine.png", "Warmed by Gaberry Wine, your companion fights with a fiercer heart.") }, { (PetIcon)19, ("PetBuffDarkStone.png", "Shadow bleeds from the Dark Stone into your companion's bite — its wounds fester.") } }; private static readonly Def[] Defs = BuildDefs(); private static bool _registered; private static string _lastLoggedDesc; private static Def[] BuildDefs() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) List list = new List(PetStatusIconDefs.All.Length); PetStatusIconDef[] all = PetStatusIconDefs.All; foreach (PetStatusIconDef val in all) { Display.TryGetValue(val.Icon, out (string, string) value); Def item = new Def { Row = val }; (item.Png, item.Desc) = value; list.Add(item); } return list.ToArray(); } private static void WarnOnDisplayGaps() { //IL_002d: Unknown result type (might be due to invalid IL or missing references) Def[] defs = Defs; for (int i = 0; i < defs.Length; i++) { Def def = defs[i]; if (def.Png == null) { Plugin.Log.LogError((object)($"[ICONS] '{def.Id}' ({def.Icon}) has NO Display entry — no art, no description. " + "A Core.PetStatusIconDefs row was added without its game-side Display row; add one in PetStatusEffects.")); } } } internal static void Init() { SL.OnPacksLoaded += Register; } private static void Register() { if (_registered) { return; } SlStatusSpec spec = new SlStatusSpec { Tag = "[ICONS]", DisabledSuffix = "pet status icons disabled." }; if (!SlStatus.ResolveDonor(spec, out var donor)) { return; } WarnOnDisplayGaps(); int num = 0; Def[] defs = Defs; for (int i = 0; i < defs.Length; i++) { Def def = defs[i]; try { if (RegisterOne(def, donor)) { num++; } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[ICONS] '{def.Id}' registration failed: {arg}"); } } _registered = num == Defs.Length; int num2 = 0; Def[] defs2 = Defs; foreach (Def def2 in defs2) { if (SlStatus.IconHeld(def2.Id)) { num2++; } } Plugin.Log.LogMessage((object)($"[ICONS] registered {num}/{Defs.Length} pet status icons (donor '{donor}'), " + $"icons pinned {num2}/{Defs.Length}" + (_registered ? "." : " — INCOMPLETE, icon sync disabled."))); } private static bool RegisterOne(Def def, string donor) { SlStatusSpec spec = new SlStatusSpec { Id = def.Id, NumId = def.NumId, Name = def.Name, Description = def.Desc, Lifespan = -1f, IconPng = def.Png, IsMalus = def.Malus, Tag = "[ICONS]" }; StatusEffect prefab; return SlStatus.Register(spec, donor, out prefab); } internal static bool AuditFx(out string detail) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) List list = new List(); int num = 0; foreach (string item in AllRegisteredIds()) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; StatusEffect val = ((instance != null) ? instance.GetStatusEffectPrefab(item) : null); if (!((Object)(object)val == (Object)null)) { num++; if ((Object)(object)val.FXPrefab != (Object)null) { list.Add(item + " (FXPrefab='" + ((Object)val.FXPrefab).name + "')"); } else if ((int)val.FxInstantiation != 0) { list.Add($"{item} (FxInstantiation={val.FxInstantiation})"); } else if ((Object)(object)val.SpecialFXPrefab != (Object)null) { list.Add(item + " (SpecialFXPrefab='" + ((Object)val.SpecialFXPrefab).name + "')"); } } } detail = ((list.Count == 0) ? $"{num} registered status(es) carry no inherited donor FX" : ($"{list.Count} of {num} status(es) still carry donor FX — SlStatus.Register's " + "unconditional StripInheritedFx regressed (bug 27): " + string.Join(", ", list.ToArray()))); return list.Count == 0; } private static IEnumerable AllRegisteredIds() { Def[] defs = Defs; for (int i = 0; i < defs.Length; i++) { Def def = defs[i]; yield return def.Row.Id; } yield return "BW_Synergy"; yield return "BW_KillFavor"; } internal static void Sync(Character player, double hungerFraction, LoyaltyTier tier, ComfortStage comfort, ComfortSide side, bool scentActive = false, bool communionBadge = false, string communionDesc = null) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002c: 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_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_0076: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) bool value = Plugin.ShowPetStatusIcons.Value; Apply(player, (PetIcon)(value ? ((int)PetStatusIcons.HungerIcon(hungerFraction, (double)Plugin.HungryIconFraction.Value)) : 0), (PetIcon)(value ? ((int)PetStatusIcons.BondIcon(tier, communionBadge)) : 0), (PetIcon)((value && Plugin.EnableTemperatureSystem.Value) ? ((int)PetStatusIcons.TemperatureIcon(comfort, side)) : 0), (PetIcon)((value && Plugin.EnableScentSense.Value && Plugin.IndicateStatusIcon.Value) ? ((int)PetStatusIcons.ScentIcon(scentActive)) : 0), (PetIcon)(value ? ((int)WantBuffFood()) : 0), communionDesc); } private static PetIcon WantBuffFood() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) Pet pet = Plugin.Instance?.ActivePet; if (pet == null) { return (PetIcon)0; } BuffFoodDef val = BuffFoodTable.ActiveDef(pet); if (val != null) { return PetStatusIcons.BuffFoodIcon(BuffFoodItemId(val)); } return (PetIcon)0; } private static int BuffFoodItemId(BuffFoodDef def) { if (def == null) { return 0; } if (def.ItemId.HasValue) { return def.ItemId.Value; } if (!ItemNameIndex.TryResolve(def.Key, out var itemId)) { return 0; } return itemId; } internal static void Clear(Character player) { Apply(player, (PetIcon)0, (PetIcon)0, (PetIcon)0, (PetIcon)0, (PetIcon)0); } private static bool IsCommunion(PetIcon icon) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if ((int)icon != 12 && (int)icon != 13 && (int)icon != 14) { return (int)icon == 15; } return true; } private static void Apply(Character player, PetIcon wantHunger, PetIcon wantBond, PetIcon wantTemp, PetIcon wantScent, PetIcon wantBuff, string communionDesc = null) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) if (!_registered || (Object)(object)player == (Object)null) { return; } StatusEffectManager statusEffectMngr = player.StatusEffectMngr; if ((Object)(object)statusEffectMngr == (Object)null) { return; } Def[] defs = Defs; for (int i = 0; i < defs.Length; i++) { Def def = defs[i]; bool flag = def.Icon == wantHunger || def.Icon == wantBond || def.Icon == wantTemp || def.Icon == wantScent || def.Icon == wantBuff; StatusEffect inst = statusEffectMngr.GetStatusEffectOfName(def.Id); bool flag2 = (Object)(object)inst != (Object)null; if (flag != flag2) { if (flag) { statusEffectMngr.AddStatusEffect(def.Id); inst = statusEffectMngr.GetStatusEffectOfName(def.Id); Plugin.Log.LogMessage((object)("[ICONS] +" + def.Id)); } else { statusEffectMngr.RemoveStatusWithIdentifierName(def.Id); inst = null; Plugin.Log.LogMessage((object)("[ICONS] -" + def.Id)); } } if (flag && SlStatus.EnsureIcon(def.Id, inst, "[ICONS]")) { Plugin.Log.LogMessage((object)("[ICONS] re-stamped " + def.Id + " — its icon had gone missing (the HUD would have been showing the donor badge instead).")); } if (flag && communionDesc != null && (Object)(object)inst != (Object)null && IsCommunion(def.Icon) && Converge.To((Func)(() => inst.m_description), communionDesc, (Action)delegate(string v) { inst.m_description = v; }, (Func)null) && communionDesc != _lastLoggedDesc) { _lastLoggedDesc = communionDesc; Plugin.Log.LogMessage((object)("[ICONS] " + def.Id + " description -> \"" + communionDesc + "\"")); } } } internal static void Dump(Character player, PetSimulation sim, bool scentActive = false) { //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0112: 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) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Unknown result type (might be due to invalid IL or missing references) //IL_01e8: Unknown result type (might be due to invalid IL or missing references) //IL_020f: 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) Plugin.Log.LogMessage((object)($"[ICONS] ShowPetStatusIcons={Plugin.ShowPetStatusIcons.Value}, registered={_registered}, " + $"HungryIconFraction={Plugin.HungryIconFraction.Value:0.##}")); bool value = BwConfig.Skills.EnableCommunionGate.Value; bool flag = PetSystems.SkillLearned(player, 87009); bool flag2 = value && flag; Plugin.Log.LogMessage((object)($"[ICONS] communion: gate={value}, Communion learned={flag}, " + $"BeastOfBurden learned={PetSystems.SkillLearned(player, 87007)} " + $"-> buffsEnabled={!value || flag}, badgeSwap={flag2}")); if (sim != null) { PetStatus val = sim.Status(); Plugin.Log.LogMessage((object)($"[ICONS] inputs: hunger {sim.HungerFraction:P0} of a day, loyalty {val.LoyaltyValue} ({val.Loyalty}), " + $"comfort {val.Comfort}/{val.ComfortSide}, scent held={scentActive} " + $"-> wanted: hunger={PetStatusIcons.HungerIcon(sim.HungerFraction, (double)Plugin.HungryIconFraction.Value)}, " + $"bond={PetStatusIcons.BondIcon(val.Loyalty, flag2)}, " + $"temp={PetStatusIcons.TemperatureIcon(val.Comfort, val.ComfortSide)}, " + $"scent={PetStatusIcons.ScentIcon(scentActive)}, " + $"buffFood={WantBuffFood()}")); if (flag2) { Plugin.Log.LogMessage((object)("[ICONS] communion badge text: \"" + CommunionBadge.Description(val.Loyalty, CommunionBadge.Describe((IReadOnlyList)PetBuffs.Resolve(PetBuffTable.Table, Plugin.Instance?.ActivePet?.SpeciesId, val.Loyalty))) + "\"")); } } else { Plugin.Log.LogMessage((object)"[ICONS] no active pet -> wanted: none."); } StatusEffectManager val2 = (((Object)(object)player != (Object)null) ? player.StatusEffectMngr : null); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogMessage((object)"[ICONS] no player StatusEffectMngr to read back."); return; } List list = new List(); Def[] defs = Defs; for (int i = 0; i < defs.Length; i++) { Def def = defs[i]; if ((Object)(object)val2.GetStatusEffectOfName(def.Id) != (Object)null) { list.Add(def.Id); } } Plugin.Log.LogMessage((object)("[ICONS] player has: " + ((list.Count > 0) ? string.Join(", ", list) : "none") + ".")); int num = 0; Def[] defs2 = Defs; for (int j = 0; j < defs2.Length; j++) { Def def2 = defs2[j]; if (SlStatus.IconHeld(def2.Id)) { num++; } StatusEffect statusEffectOfName = val2.GetStatusEffectOfName(def2.Id); if ((Object)(object)statusEffectOfName != (Object)null || !SlStatus.IconHeld(def2.Id)) { Plugin.Log.LogMessage((object)("[ICONS] art " + SlStatus.IconState(def2.Id, statusEffectOfName))); } } Plugin.Log.LogMessage((object)($"[ICONS] icons pinned {num}/{Defs.Length}" + ((num == Defs.Length) ? "." : " — the unpinned ones will render as the donor badge."))); Plugin.Log.LogMessage((object)("[ICONS] " + SummonIconGuard.Describe(player))); } } internal static class PetSystems { internal struct ComfortSample { public bool Valid; public TempStep Ambient; public ComfortBand Band; public ComfortReading Reading; public ComfortGate Gate; public AmbientSource Source; } internal static ComfortSample LastSample; internal static ComfortGate LastOuterGate; internal static int SimTickCount; internal static int PausedTickCount; internal static int Rediscoveries; private static readonly HashSet _warnedGates = new HashSet(); private static bool _healLogged; private static bool _lastStampWasOuter; internal static ComfortGate LastGate; private static bool _communionGateLogged; internal static ComfortGate EffectiveGate { get { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) if (!_lastStampWasOuter) { return LastGate; } return LastOuterGate; } } internal static float SpeedMultiplier(Responsiveness r) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return ResponsivenessEffects.SpeedMultiplier(r); } internal static void NoteGate(ComfortGate gate) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 //IL_001c: Unknown result type (might be due to invalid IL or missing references) LastOuterGate = gate; _lastStampWasOuter = true; if ((int)gate == 6) { PausedTickCount++; } WarnOnce(gate); } private static void WarnOnce(ComfortGate gate) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000e: 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 (ComfortGates.IsFault(gate) && _warnedGates.Add(gate)) { Plugin.Log.LogWarning((object)("[TEMP] comfort sampling blocked: reason=" + ComfortGates.Code(gate) + " " + $"(ticks={SimTickCount}, pausedTicks={PausedTickCount}) — first occurrence this session; " + "no further warning for this reason.")); } } private static EnvironmentConditions ResolveEnvironment(out AmbientSource source) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected I4, but got Unknown //IL_0080: 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) EnvironmentConditions instance = EnvironmentConditions.Instance; if ((Object)(object)instance != (Object)null) { source = (AmbientSource)1; return instance; } EnvironmentConditions val = FindLiveEnvironment(); source = (AmbientSource)(int)ComfortGates.PreferredAmbientSource(false, (Object)(object)val != (Object)null); if ((Object)(object)val == (Object)null) { return null; } EnvironmentConditions.Instance = val; Rediscoveries++; if (!_healLogged) { _healLogged = true; ManualLogSource log = Plugin.Log; string[] obj = new string[5] { "[TEMP] fallback source: ", ComfortGates.SourceLabel(source), " — EnvironmentConditions.Instance was dangling (donor-load hijack, bug-12) and the live '", null, null }; Scene scene = ((Component)val).gameObject.scene; obj[3] = ((Scene)(ref scene)).name; obj[4] = "' instance was re-found; singleton repaired. Further heals are counted as rediscoveries=N, not logged."; log.LogWarning((object)string.Concat(obj)); } return val; } private static EnvironmentConditions FindLiveEnvironment() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) EnvironmentConditions[] array = Object.FindObjectsOfType(); if (array == null || array.Length == 0) { return null; } Scene val = SceneManager.GetActiveScene(); string name = ((Scene)(ref val)).name; for (int i = 0; i < array.Length; i++) { if ((Object)(object)array[i] != (Object)null) { val = ((Component)array[i]).gameObject.scene; if (((Scene)(ref val)).name == name) { return array[i]; } } } return array[0]; } internal static void ResetDiagnostics() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) LastSample = default(ComfortSample); LastGate = (ComfortGate)0; LastOuterGate = (ComfortGate)0; _lastStampWasOuter = false; SimTickCount = 0; PausedTickCount = 0; Rediscoveries = 0; _warnedGates.Clear(); _healLogged = false; } internal static EnvironmentConditions CurrentEnvironment() { AmbientSource source; return ResolveEnvironment(out source); } internal static ComfortSample SampleComfort(Pet pet, Character player) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_014b: 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_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_008e: 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_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0125: 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_0131: Unknown result type (might be due to invalid IL or missing references) ComfortSample comfortSample = new ComfortSample { Reading = ComfortReading.InBand }; bool value = Plugin.EnableTemperatureSystem.Value; bool flag = pet?.State != null; if (value && flag) { AmbientSource source; EnvironmentConditions val = ResolveEnvironment(out source); comfortSample.Source = source; Transform val2 = (((Object)(object)((Companion)pet).Body != (Object)null) ? ((Component)((Companion)pet).Body).transform : (((Object)(object)player != (Object)null) ? ((Component)player).transform : null)); comfortSample.Gate = ComfortGates.Evaluate(true, true, (Object)(object)val != (Object)null, (Object)(object)val2 != (Object)null); if ((Object)(object)val != (Object)null && (Object)(object)val2 != (Object)null) { comfortSample.Ambient = (TempStep)Mathf.Clamp(val.GetTemperature(val2, -1f), 0, 8); comfortSample.Band = PetComfortTable.BandFor(pet.SpeciesId); List list = new List(3); ComfortRelief val3 = PetComfortTable.ReliefFor(pet.State); if (val3 != null) { list.Add(val3); } List list2 = WeatherFoodTable.ReliefsFor(pet.State); if (list2 != null) { list.AddRange(list2); } comfortSample.Reading = PetComfort.Evaluate(comfortSample.Ambient, comfortSample.Band, (IReadOnlyList)list); comfortSample.Valid = true; } } else { comfortSample.Gate = ComfortGates.Evaluate(value, flag, true, true); } LastGate = comfortSample.Gate; _lastStampWasOuter = false; WarnOnce(comfortSample.Gate); LastSample = comfortSample; return comfortSample; } internal static string GateReport() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) return "reason=" + ComfortGates.Code(EffectiveGate) + ", source=" + ComfortGates.SourceLabel(LastSample.Source) + $", ticks={SimTickCount}, pausedTicks={PausedTickCount}, rediscoveries={Rediscoveries}"; } internal static float DamageFactor(Responsiveness r) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return ResponsivenessEffects.DamageFactor(r); } internal static float CombatDamageMultiplier(Pet pet, PetStatus status) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) float num = DamageFactor(status.Responsiveness); float num2 = (float)Synergy.DamageMult(HuntSynergy.EffectiveStacks(Plugin.LocalPlayerCharacter, pet), (double)Plugin.SynergyPercentPerStack.Value); float num3 = PetPower.Govern(SkillLearned(Plugin.LocalPlayerCharacter, 87008), Plugin.UngovernedPetDamagePercent.Value); float num4 = BuffFoodTable.DamageFactor(pet, status.Loyalty); return num * num2 * Plugin.PetDamageScale(pet?.SpeciesId) * num3 * num4; } internal static CreatureAttributes EffectiveStats(Pet pet, PetStatus status) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.UseSpeciesStats.Value || ((pet != null) ? ((Companion)pet).CapturedStats : null) == null) { return null; } StatModifiers obj = PetGrowth.Resolve(GrowthTable.Table, pet.SpeciesId, status.LoyaltyValue); PetSimulation sim = pet.Sim; object obj2; if (sim == null) { obj2 = null; } else { GearEffects gear = sim.Gear; obj2 = ((gear != null) ? gear.PetStats : null); } StatModifiers val = obj.CombinedWith((StatModifiers)obj2); CreatureAttributes val2 = PetStatTuning.Effective(((Companion)pet).CapturedStats, status.LoyaltyValue, Plugin.TuningPolicy(), val); SpeciesRelics.Apply(val2, pet.State.RelicStacks); return val2; } internal static bool SkillLearned(Character player, int itemId) { if ((Object)(object)player != (Object)null && (Object)(object)player.Inventory != (Object)null && (Object)(object)player.Inventory.SkillKnowledge != (Object)null) { return ((CharacterKnowledge)player.Inventory.SkillKnowledge).IsItemLearned(itemId); } return false; } internal static void ApplyEffects(Pet pet, PetStatus status) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0080: 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_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_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: 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_0136: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) if (pet == null) { return; } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; bool value = BwConfig.Skills.EnableCommunionGate.Value; bool flag = SkillLearned(localPlayerCharacter, 87009); bool communionEnabled = !value || flag; bool flag2 = value && flag; bool beastOfBurdenLearned = SkillLearned(localPlayerCharacter, 87007); if (value && (Object)(object)localPlayerCharacter != (Object)null && !flag && !_communionGateLogged) { _communionGateLogged = true; Plugin.Log.LogMessage((object)"[COMMUNION] passive bond buffs now require the Communion skill ([Skills] EnableCommunionGate) — the pet's stat buffs and bag perk are withheld until it is learned (a deliberate balance change; Maren sells it, or set the gate to false)."); } PetPassiveBuff.Sync(localPlayerCharacter, pet.SpeciesId, status.Loyalty, communionEnabled); PetBagPerk.Sync(localPlayerCharacter, pet.SpeciesId, status.Loyalty, communionEnabled, beastOfBurdenLearned); string communionDesc = (flag2 ? CommunionBadge.Description(status.Loyalty, CommunionBadge.Describe((IReadOnlyList)PetBuffs.Resolve(PetBuffTable.Table, pet.SpeciesId, status.Loyalty))) : null); PetSimulation sim = pet.Sim; double hungerFraction = ((sim != null) ? sim.HungerFraction : 0.0); LoyaltyTier loyalty = status.Loyalty; ComfortStage comfort = status.Comfort; ComfortSide comfortSide = status.ComfortSide; ScentTracker scent = pet.Scent; PetStatusEffects.Sync(localPlayerCharacter, hungerFraction, loyalty, comfort, comfortSide, ((scent != null) ? scent.Current : null) != null, flag2, communionDesc); HuntSynergy.SyncBuffs(Plugin.LocalPlayerCharacter, pet); if (!((Object)(object)((Companion)pet).Body == (Object)null)) { if ((Object)(object)((Companion)pet).Combat != (Object)null) { ((Companion)pet).Combat.DecayRiderFraction = BuffFoodTable.DecayFraction(pet, status.Loyalty); } CreatureAttributes val = EffectiveStats(pet, status); bool flag3 = val != null && val.MoveSpeed > 0f; float num = (flag3 ? val.MoveSpeed : Plugin.FollowSpeed.Value); StatTuningPolicy val2 = Plugin.TuningPolicy(); float num2 = ((val != null && val.MaxHealth > 0f) ? val.MaxHealth : (Plugin.AnchorBaseHealth.Value * PetStatTuning.Factor(status.LoyaltyValue, val2.HealthAt0, val2.HealthAt100))); ((Companion)pet).ApplyAttributes(val, (float?)num2, (float?)(num * SpeedMultiplier(status.Responsiveness)), (float?)(flag3 ? Plugin.MinFollowSpeed.Value : 0f), (float?)CombatDamageMultiplier(pet, status)); PetProxyClient.SyncStats(pet, val, num2); } } internal static void StatDumpVerb(Plugin p) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0078: 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_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_0193: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.Sim == null) { Plugin.Log.LogMessage((object)"[STATS] no active pet."); return; } CreatureAttributes val = activePet.State?.Attributes; Plugin.Log.LogMessage((object)string.Format("[STATS] UseSpeciesStats={0}; captured: {1}", Plugin.UseSpeciesStats.Value, (val != null) ? AttributeCapture.Describe(val) : "NONE (v1 save or capture failure — config fallback; re-form adopts a capture)")); PetStatus val2 = activePet.Sim.Status(); CreatureAttributes val3 = EffectiveStats(activePet, val2); if (val3 != null) { StatTuningPolicy val4 = Plugin.TuningPolicy(); Plugin.Log.LogMessage((object)($"[STATS] effective @ loyalty {val2.LoyaltyValue}: {AttributeCapture.Describe(val3)} " + $"(factors: hp x{PetStatTuning.Factor(val2.LoyaltyValue, val4.HealthAt0, val4.HealthAt100):F2}" + $" dmg x{PetStatTuning.Factor(val2.LoyaltyValue, val4.DamageAt0, val4.DamageAt100):F2}" + $" def x{PetStatTuning.Factor(val2.LoyaltyValue, val4.DefenseAt0, val4.DefenseAt100):F2}" + $" spd x{PetStatTuning.Factor(val2.LoyaltyValue, val4.SpeedAt0, val4.SpeedAt100):F2})")); StatModifiers val5 = PetGrowth.Resolve(GrowthTable.Table, activePet.SpeciesId, val2.LoyaltyValue); Plugin.Log.LogMessage((object)($"[GROWTH] '{activePet.SpeciesId}' @ loyalty {val2.LoyaltyValue}: " + $"hp x{val5.Health:F3} dmg x{val5.Damage:F3} def x{val5.Defense:F3} spd x{val5.Speed:F3}" + ((val5.Health == 1f && val5.Damage == 1f && val5.Defense == 1f && val5.Speed == 1f) ? " (no growth entry — neutral)" : " (SpeciesGrowth.txt; retune via override + reloadgrowth)"))); } ((Companion)activePet).Anchor.DumpCreatureStats(); Plugin.Log.LogMessage((object)("[STATS] anchor hp: " + ((Companion)activePet).Anchor.HealthSummary())); CompanionBody body = ((Companion)activePet).Body; CompanionCombat combat = ((Companion)activePet).Combat; if ((Object)(object)combat != (Object)null) { Plugin.Log.LogMessage((object)$"[STATS] combat: BaseDamage={combat.BaseDamage:F1} (DamageMultiplier x{combat.DamageMultiplier:F2} — see [POWER] for the breakdown)"); } if ((Object)(object)body != (Object)null) { Plugin.Log.LogMessage((object)$"[STATS] speed: {body.Speed:F2} m/s (follow floor {body.FollowSpeedFloor:F2}, fighting={(Object)(object)body.CombatTarget != (Object)null})"); } Plugin.Log.LogMessage((object)("[POWER] " + PowerSummary(p))); } internal static string PowerSummary(Plugin p) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: 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_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; if (activePet?.Sim == null) { return "no active pet."; } string speciesId = activePet.SpeciesId; float percent; string matchedKey; float num = Plugin.PetDamageScale(speciesId, out percent, out matchedKey); string arg = ((matchedKey != null) ? $"SpeciesDamageScales '{matchedKey}={percent:F0}%'" : $"global PetDamageScalePercent={percent:F0}%"); PetStatus val = activePet.Sim.Status(); float num2 = DamageFactor(val.Responsiveness); int num3 = HuntSynergy.EffectiveStacks(Plugin.LocalPlayerCharacter, activePet); float num4 = (float)Synergy.DamageMult(num3, (double)Plugin.SynergyPercentPerStack.Value); bool flag = SkillLearned(Plugin.LocalPlayerCharacter, 87008); float num5 = PetPower.Govern(flag, Plugin.UngovernedPetDamagePercent.Value); float num6 = BuffFoodTable.DamageFactor(activePet, val.Loyalty); float num7 = BuffFoodTable.DecayFraction(activePet, val.Loyalty); CompanionCombat combat = ((Companion)activePet).Combat; string arg2 = (((Object)(object)combat != (Object)null) ? $" | BaseDamage={combat.BaseDamage:F1}" : " | no body — nothing to deal damage yet"); return $"species '{speciesId}': scale x{num:F2} (from {arg})" + $" | responsiveness x{num2:F2} ({val.Responsiveness})" + $" | synergy x{num4:F2} ({num3} stacks)" + string.Format(" | governor x{0:F2} (Wild Unknown {1})", num5, flag ? "learned" : "NOT learned") + $" | buffFood x{num6:F2}" + ((num7 > 0f) ? $" (+ Decay rider {num7 * 100f:F0}% of total)" : "") + $" => DamageMultiplier x{CombatDamageMultiplier(activePet, val):F2}{arg2}"; } } [HarmonyPatch(typeof(CharacterUI), "Awake")] internal static class CharacterUIAwakePatch { [HarmonyPostfix] private static void Postfix(CharacterUI __instance) { PetTabInjector.Inject(__instance); } } [HarmonyPatch(typeof(CharacterUI), "ShowMenu", new Type[] { typeof(MenuScreens), typeof(Item) })] internal static class CharacterUIShowMenuPatch { [HarmonyPostfix] private static void Postfix(CharacterUI __instance, MenuScreens _menu) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) PetTabInjector.OnShowMenu(__instance, _menu); } } internal static class PetTabInjector { internal const string TabLabel = "Companion"; private static readonly MenuTabSpec Spec; internal static void Inject(CharacterUI ui) { MenuTabInjector.Inject(ui, Spec); } internal static void OnShowMenu(CharacterUI ui, MenuScreens menu) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) MenuTabInjector.OnShowMenu(ui, menu, Spec); } internal static bool ShowFor(Character player) { return MenuTabInjector.ShowFor(player, Spec); } static PetTabInjector() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown MenuTabSpec val = new MenuTabSpec(); val.Label = "Companion"; val.PanelType = typeof(PetMenuPanel); val.Log = Plugin.Log; val.Tag = "[PETTAB]"; val.PanelObjectName = "PetMenuPanel"; val.TabObjectName = "PetTab"; Spec = val; } } internal static class PetTemperatureDriver { private static ComfortStage _lastComfort; internal static void Reset() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) _lastComfort = (ComfortStage)0; } internal static void LogComfortTransition(Plugin p, PetStatus st, PetSystems.ComfortSample cs, Character player) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0084: 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_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Invalid comparison between Unknown and I4 //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Invalid comparison between Unknown and I4 //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Invalid comparison between Unknown and I4 //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Invalid comparison between Unknown and I4 //IL_022e: Unknown result type (might be due to invalid IL or missing references) //IL_022f: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Invalid comparison between Unknown and I4 if (st.Comfort != _lastComfort) { bool flag = st.Comfort > _lastComfort; string arg = ((flag && ((ComfortReading)(ref cs.Reading)).StepsOutside == 0) ? ", saved stage resuming — not this sample" : ""); Plugin.Log.LogMessage((object)(string.Format("[TEMP] comfort {0}: {1} -> {2}", flag ? "worsened" : "recovered", _lastComfort, st.Comfort) + (cs.Valid ? (string.Format(" (side={0}, ambient={1} at {2}, ", st.ComfortSide, cs.Ambient, ((Object)(object)((Companion)(p.ActivePet?)).Body != (Object)null) ? "body" : "player") + $"band={((ComfortBand)(ref cs.Band)).Min}-{((ComfortBand)(ref cs.Band)).Max}, out={((ComfortReading)(ref cs.Reading)).StepsOutside}(raw {((ComfortReading)(ref cs.Reading)).RawStepsOutside}), " + $"escMult={((ComfortReading)(ref cs.Reading)).EscalateMult:0.#}{arg})") : " (no sample)"))); _lastComfort = st.Comfort; string text = (((int)st.ComfortSide == 2) ? "heat" : "cold"); string text2 = p.ActivePet?.SpeciesId ?? "Your companion"; if (flag && (int)st.Comfort == 1) { Notify.Player(player, text2 + " is uneasy in the " + text + "."); } else if (flag && (int)st.Comfort == 2) { Notify.Player(player, text2 + " is suffering from the " + text + "!"); } else if (flag && (int)st.Comfort == 3) { Notify.Player(player, text2 + " is " + (((int)st.ComfortSide == 2) ? "overheating" : "freezing") + " — get it out of the " + text + "!"); } else if (!flag && (int)st.Comfort == 0) { Notify.Player(player, text2 + " is comfortable again."); } } } internal static bool TickTemperatureDrain(Plugin p, PetStatus st, float dt, Character player) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Invalid comparison between Unknown and I4 //IL_00bc: 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) //IL_00c3: Invalid comparison between Unknown and I4 //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Invalid comparison between Unknown and I4 if (!Plugin.EnableTemperatureSystem.Value) { return false; } ComfortStage comfort = st.Comfort; float num = (((int)comfort == 2) ? Plugin.SufferingDrainPerMinute.Value : (((int)comfort != 3) ? 0f : Plugin.CriticalDrainPerMinute.Value)); float num2 = num; if (num2 <= 0f || p.ActivePet == null) { return false; } bool flag; if (PhotonNetwork.isNonMasterClientInRoom) { flag = PetProxyClient.RequestDrainAndCheckPinned(num2, dt); if (p.ActivePet == null) { return false; } } else { float num3 = default(float); if (!((Companion)p.ActivePet).Anchor.TryGetHealth(ref num, ref num3)) { return false; } flag = ((Companion)p.ActivePet).Anchor.ApplyTemperatureDrain(num3 * num2 / 100f * (dt / 60f)); } if (!flag || (int)st.Comfort != 3) { return false; } string text = (((int)st.ComfortSide == 2) ? "heat" : "cold"); string text2 = p.ActivePet?.SpeciesId ?? "Your companion"; DeathOutcome val = PetRules.OnZeroHealth(Plugin.PetDeathModeConfig.Value); if ((int)val != 0) { if ((int)val == 1) { Plugin.Log.LogMessage((object)("[TEMP] the pet collapsed from the " + text + " (PetDeathMode=KnockedOut).")); Notify.Player(player, text2 + " collapses from the " + text + "!"); Pet activePet = p.ActivePet; if (activePet != null) { ((Companion)activePet).Anchor.DestroyCurrent(); } p.Lifecycle.OnAnchorDied(); return p.ActivePet == null; } return false; } Plugin.Log.LogMessage((object)("[TEMP] the pet succumbed to the " + text + " (PetDeathMode=Permanent) — bond ended, save cleared.")); Notify.Player(player, text2 + " succumbs to the " + text + "."); p.Lifecycle.DespawnPet(player); return true; } } [BepInPlugin("cobalt.beastwhispering", "Beastwhispering", "0.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string GUID = "cobalt.beastwhispering"; public const string NAME = "Beastwhispering"; public const string VERSION = "0.2.0"; internal static ManualLogSource Log; internal readonly CompanionTicker SimTicker = new CompanionTicker((Func)(() => SimTickSeconds.Value)); private float _sniffLast; private float _diagLast; private CommandRegistry _commands; private CommandChannel _channel; private float _castOpenSince; private bool _castWarned; private static string _damageScalesRaw; private static List> _damageScales; private static PetTuning _tuning; public static ConfigEntry TameKey => BwConfig.Keys.TameKey; public static ConfigEntry FeedKey => BwConfig.Keys.FeedKey; public static ConfigEntry SelfTestKey => BwConfig.Keys.SelfTestKey; public static ConfigEntry RecallKey => BwConfig.Keys.RecallKey; public static ConfigEntry DiagKey => BwConfig.Keys.DiagKey; public static ConfigEntry TameRange => BwConfig.Pet.TameRange; public static ConfigEntry ModelYawOffset => BwConfig.Pet.ModelYawOffset; public static ConfigEntry SpeciesYawOffsets => BwConfig.Pet.SpeciesYawOffsets; public static ConfigEntry FollowSpeed => BwConfig.Pet.FollowSpeed; public static ConfigEntry MinFollowSpeed => BwConfig.Pet.MinFollowSpeed; public static ConfigEntry RestHealsPet => BwConfig.Pet.RestHealsPet; public static ConfigEntry LoafDistanceMin => BwConfig.Follow.LoafDistanceMin; public static ConfigEntry LoafDistanceMax => BwConfig.Follow.LoafDistanceMax; public static ConfigEntry LoafDistanceRepick => BwConfig.Follow.LoafDistanceRepick; public static ConfigEntry DiagIntervalSeconds => BwConfig.Diag.DiagIntervalSeconds; public static ConfigEntry DiagRadius => BwConfig.Diag.DiagRadius; public static ConfigEntry CastDiagPatches => BwConfig.Diag.CastDiagPatches; public static ConfigEntry MusicReconPatches => BwConfig.Diag.MusicReconPatches; public static ConfigEntry EnableTamingFoods => BwConfig.Taming.EnableTamingFoods; public static ConfigEntry TameRadius => BwConfig.Taming.TameRadius; public static ConfigEntry TameRecheckGraceMult => BwConfig.Taming.TameRecheckGraceMult; public static ConfigEntry RecipeDropChance => BwConfig.Taming.RecipeDropChance; public static ConfigEntry RunSelfTestOnLoad => BwConfig.SelfTest.RunSelfTestOnLoad; public static ConfigEntry InitialLoyalty => BwConfig.Systems.InitialLoyalty; public static ConfigEntry HungerSecondsPerDay => BwConfig.Systems.HungerSecondsPerDay; public static ConfigEntry TempEscalateSeconds => BwConfig.Systems.TempEscalateSeconds; public static ConfigEntry TempRecoverSeconds => BwConfig.Systems.TempRecoverSeconds; public static ConfigEntry SpeciesDailyDecay => BwConfig.Systems.SpeciesDailyDecay; public static ConfigEntry LoyaltyGainPercent => BwConfig.Systems.LoyaltyGainPercent; public static ConfigEntry SimTickSeconds => BwConfig.Systems.SimTickSeconds; public static ConfigEntry CastWatchdogWarnSeconds => BwConfig.Systems.CastWatchdogWarnSeconds; public static ConfigEntry CastWatchdogClearSeconds => BwConfig.Systems.CastWatchdogClearSeconds; public static ConfigEntry FeedHealAmount => BwConfig.Systems.FeedHealAmount; public static ConfigEntry EnableFoodHealthRecovery => BwConfig.Systems.EnableFoodHealthRecovery; public static ConfigEntry SatiationFraction => BwConfig.Systems.SatiationFraction; public static ConfigEntry EnablePassiveBuffs => BwConfig.Systems.EnablePassiveBuffs; public static ConfigEntry EnableBagPerk => BwConfig.Systems.EnableBagPerk; public static ConfigEntry ShowPetStatusIcons => BwConfig.Systems.ShowPetStatusIcons; public static ConfigEntry HungryIconFraction => BwConfig.Systems.HungryIconFraction; public static ConfigEntry UseSpeciesStats => BwConfig.Systems.UseSpeciesStats; public static ConfigEntry PersistPetHealth => BwConfig.Systems.PersistPetHealth; public static ConfigEntry HealthLoyaltyFactor0 => BwConfig.Systems.HealthLoyaltyFactor0; public static ConfigEntry HealthLoyaltyFactor100 => BwConfig.Systems.HealthLoyaltyFactor100; public static ConfigEntry DamageLoyaltyFactor0 => BwConfig.Systems.DamageLoyaltyFactor0; public static ConfigEntry DamageLoyaltyFactor100 => BwConfig.Systems.DamageLoyaltyFactor100; public static ConfigEntry DefenseLoyaltyFactor0 => BwConfig.Systems.DefenseLoyaltyFactor0; public static ConfigEntry DefenseLoyaltyFactor100 => BwConfig.Systems.DefenseLoyaltyFactor100; public static ConfigEntry SpeedLoyaltyFactor0 => BwConfig.Systems.SpeedLoyaltyFactor0; public static ConfigEntry SpeedLoyaltyFactor100 => BwConfig.Systems.SpeedLoyaltyFactor100; public static ConfigEntry ReleaseConfirmLoyaltyThreshold => BwConfig.Systems.ReleaseConfirmLoyaltyThreshold; public static ConfigEntry EnableTemperatureSystem => BwConfig.Temperature.EnableTemperatureSystem; public static ConfigEntry EnableWeatherFoods => BwConfig.Temperature.EnableWeatherFoods; public static ConfigEntry EnableBuffFoods => BwConfig.BuffFoods.EnableBuffFoods; public static ConfigEntry PetDeathModeConfig => BwConfig.Temperature.PetDeathModeConfig; public static ConfigEntry SufferingDrainPerMinute => BwConfig.Temperature.SufferingDrainPerMinute; public static ConfigEntry CriticalDrainPerMinute => BwConfig.Temperature.CriticalDrainPerMinute; public static ConfigEntry UneasyDecayMult => BwConfig.Temperature.UneasyDecayMult; public static ConfigEntry SufferingDecayMult => BwConfig.Temperature.SufferingDecayMult; public static ConfigEntry EnableCombat => BwConfig.Combat.EnableCombat; public static ConfigEntry AttackDamage => BwConfig.Combat.AttackDamage; public static ConfigEntry AttackInterval => BwConfig.Combat.AttackInterval; public static ConfigEntry AggroRange => BwConfig.Combat.AggroRange; public static ConfigEntry AttackRange => BwConfig.Combat.AttackRange; public static ConfigEntry CombatLeashDistance => BwConfig.Combat.CombatLeashDistance; public static ConfigEntry EnableSpecialAttack => BwConfig.Combat.EnableSpecialAttack; public static ConfigEntry EngageRange => BwConfig.Combat.EngageRange; public static ConfigEntry EngageConeDegrees => BwConfig.Combat.EngageConeDegrees; public static ConfigEntry DisengageRunHomeSeconds => BwConfig.Combat.DisengageRunHomeSeconds; public static ConfigEntry PetAttackVocals => BwConfig.Combat.PetAttackVocals; public static ConfigEntry PetDamageScalePercent => BwConfig.Combat.PetDamageScalePercent; public static ConfigEntry SpeciesDamageScales => BwConfig.Combat.SpeciesDamageScales; public static ConfigEntry UngovernedPetDamagePercent => BwConfig.Combat.UngovernedPetDamagePercent; public static ConfigEntry EnableHuntAsOne => BwConfig.HuntAsOne.EnableHuntAsOne; public static ConfigEntry HuntAsOneMeleeAnimation => BwConfig.HuntAsOne.HuntAsOneMeleeAnimation; public static ConfigEntry HuntAsOneBowAnimation => BwConfig.HuntAsOne.HuntAsOneBowAnimation; public static ConfigEntry HuntAsOneBonusDamage => BwConfig.HuntAsOne.HuntAsOneBonusDamage; public static ConfigEntry HuntAsOneBowShotDelay => BwConfig.HuntAsOne.HuntAsOneBowShotDelay; public static ConfigEntry EnableFoodHexes => BwConfig.HuntAsOne.EnableFoodHexes; public static ConfigEntry HexMealWindow => BwConfig.HuntAsOne.HexMealWindow; public static ConfigEntry HexBuildupPercent => BwConfig.HuntAsOne.HexBuildupPercent; public static ConfigEntry EnableRangedSpecial => BwConfig.HuntAsOne.EnableRangedSpecial; public static ConfigEntry BoltMaxFlightSeconds => BwConfig.HuntAsOne.BoltMaxFlightSeconds; public static ConfigEntry EnableSigilSynergies => BwConfig.Sigils.EnableSigilSynergies; public static ConfigEntry EnablePetSigils => BwConfig.Sigils.EnablePetSigils; public static ConfigEntry PetSigilScale => BwConfig.Sigils.PetSigilScale; public static ConfigEntry PetSigilCooldownSeconds => BwConfig.Sigils.PetSigilCooldownSeconds; public static ConfigEntry EnableGearEffects => BwConfig.Gear.EnableGearEffects; public static ConfigEntry EnableGiftSkill => BwConfig.Gifts.EnableGiftSkill; public static ConfigEntry GiftCooldownSeconds => BwConfig.Gifts.GiftCooldownSeconds; public static ConfigEntry EnableFeatherFletching => BwConfig.Gifts.EnableFeatherFletching; public static ConfigEntry FletchNameSuffix => BwConfig.Gifts.FletchNameSuffix; public static ConfigEntry EnableScavengeBonus => BwConfig.Scavenge.EnableScavengeBonus; public static ConfigEntry EnableScentSense => BwConfig.Scent.EnableScentSense; public static ConfigEntry SniffIntervalSeconds => BwConfig.Scent.SniffIntervalSeconds; public static ConfigEntry SkipEmptyGatherables => BwConfig.Scent.SkipEmptyGatherables; public static ConfigEntry NorthYawOffsetDegrees => BwConfig.Scent.NorthYawOffsetDegrees; public static ConfigEntry IndicatePoint => BwConfig.Scent.IndicatePoint; public static ConfigEntry PointSeconds => BwConfig.Scent.PointSeconds; public static ConfigEntry IndicateStatusIcon => BwConfig.Scent.IndicateStatusIcon; public static ConfigEntry IndicateToast => BwConfig.Scent.IndicateToast; public static ConfigEntry EnableAnchor => BwConfig.Anchor.EnableAnchor; public static ConfigEntry AnchorInvisible => BwConfig.Anchor.AnchorInvisible; public static ConfigEntry AnchorShowHealthBar => BwConfig.Anchor.AnchorShowHealthBar; public static ConfigEntry AnchorLinkSummonSlot => BwConfig.Anchor.AnchorLinkSummonSlot; public static ConfigEntry HideSummonIcon => BwConfig.Anchor.HideSummonIcon; public static ConfigEntry AnchorLeashDistance => BwConfig.Anchor.AnchorLeashDistance; public static ConfigEntry AnchorRespawnSeconds => BwConfig.Anchor.AnchorRespawnSeconds; public static ConfigEntry AnchorBaseHealth => BwConfig.Anchor.AnchorBaseHealth; public static ConfigEntry AnchorDealsDamage => BwConfig.Anchor.AnchorDealsDamage; public static ConfigEntry AnchorSpeciesVoice => BwConfig.Anchor.AnchorSpeciesVoice; public static ConfigEntry GlueMode => BwConfig.Anchor.GlueMode; public static ConfigEntry GlueOffsetBehind => BwConfig.Anchor.GlueOffsetBehind; public static ConfigEntry UnifyTargets => BwConfig.Anchor.UnifyTargets; public static ConfigEntry AnchorPlayerCollision => BwConfig.Anchor.AnchorPlayerCollision; public static ConfigEntry EnableHealthHud => BwConfig.Hud.EnableHealthHud; public static ConfigEntry HonestHits => BwConfig.HuntAsOne.HonestHits; public static ConfigEntry SyncSkillCooldownToPet => BwConfig.HuntAsOne.SyncSkillCooldownToPet; public static ConfigEntry BaseSkillCooldownSeconds => BwConfig.HuntAsOne.BaseSkillCooldownSeconds; public static ConfigEntry EnableSynergy => BwConfig.Synergy.EnableSynergy; public static ConfigEntry SynergyPercentPerStack => BwConfig.Synergy.PercentPerStack; public static ConfigEntry SynergyMaxStacks => BwConfig.Synergy.MaxStacks; public static ConfigEntry SynergyDurationSeconds => BwConfig.Synergy.DurationSeconds; public static ConfigEntry SynergyWindowSeconds => BwConfig.Synergy.WindowSeconds; public static ConfigEntry SynergyRequireSameTarget => BwConfig.Synergy.RequireSameTarget; public static ConfigEntry PlayerMeleeRangeMeters => BwConfig.Synergy.PlayerMeleeRangeMeters; public static ConfigEntry PlayerMeleeConeDegrees => BwConfig.Synergy.PlayerMeleeConeDegrees; public static ConfigEntry PetMeleeRangeMeters => BwConfig.Synergy.PetMeleeRangeMeters; public static ConfigEntry EnableBrace => BwConfig.Brace.EnableBrace; public static ConfigEntry BraceWindowSeconds => BwConfig.Brace.WindowSeconds; public static ConfigEntry BracePerAttackerOnce => BwConfig.Brace.PerAttackerOnce; public static ConfigEntry BraceNegateCounteredHit => BwConfig.Brace.NegateCounteredHit; public static ConfigEntry BraceRiposteImpact => BwConfig.Brace.RiposteImpact; public static ConfigEntry BraceCueVolume => BwConfig.Brace.CueVolume; public static ConfigEntry BraceEnableTaunt => BwConfig.Brace.EnableTaunt; public static ConfigEntry EnableSkillEcho => BwConfig.SkillEcho.EnableSkillEcho; public static ConfigEntry EchoDamageMult => BwConfig.SkillEcho.EchoDamageMult; public static ConfigEntry EchoImpactMult => BwConfig.SkillEcho.EchoImpactMult; public static ConfigEntry EchoWindupSeconds => BwConfig.SkillEcho.EchoWindupSeconds; public static ConfigEntry EchoCooldownSeconds => BwConfig.SkillEcho.EchoCooldownSeconds; public static ConfigEntry EchoRangeMeters => BwConfig.SkillEcho.EchoRangeMeters; public static ConfigEntry EchoWhilePassive => BwConfig.SkillEcho.EchoWhilePassive; public static ConfigEntry EchoCueOnPet => BwConfig.SkillEcho.CueOnPet; public static ConfigEntry EchoCueStatusName => BwConfig.SkillEcho.CueStatusName; public static ConfigEntry EchoCueSeconds => BwConfig.SkillEcho.CueSeconds; public static ConfigEntry EnableForTheKill => BwConfig.ForTheKill.EnableForTheKill; public static ConfigEntry FtkCooldownSeconds => BwConfig.ForTheKill.CooldownSeconds; public static ConfigEntry FtkBaseDamageMult => BwConfig.ForTheKill.BaseDamageMult; public static ConfigEntry FtkDamagePerStackPercent => BwConfig.ForTheKill.DamagePerStackPercent; public static ConfigEntry EnableKillFavor => BwConfig.ForTheKill.EnableKillFavor; public static ConfigEntry KillFavorDurationSeconds => BwConfig.ForTheKill.KillFavorDurationSeconds; public static ConfigEntry EnableWardShare => BwConfig.Wards.EnableWardShare; public static ConfigEntry WardPollSeconds => BwConfig.Wards.WardPollSeconds; public static ConfigEntry ManaWardStatusNames => BwConfig.Wards.ManaWardStatusNames; public static ConfigEntry GiftOfBloodStatusNames => BwConfig.Wards.GiftOfBloodStatusNames; public static ConfigEntry GiftOfBloodAllyStatusNames => BwConfig.Wards.GiftOfBloodAllyStatusNames; public static ConfigEntry EnableBandageHealing => BwConfig.Bandage.EnableBandageHealing; public static ConfigEntry BandageItemIds => BwConfig.Bandage.BandageItemIds; public static ConfigEntry BandageStatusNames => BwConfig.Bandage.BandageStatusNames; public static ConfigEntry EnableLanternShare => BwConfig.Lantern.EnableLanternShare; public static ConfigEntry LanternPollSeconds => BwConfig.Lantern.LanternPollSeconds; public static ConfigEntry LanternStatusNames => BwConfig.Lantern.LanternStatusNames; public static ConfigEntry LanternUpOffset => BwConfig.Lantern.LanternUpOffset; public static ConfigEntry LanternForwardOffset => BwConfig.Lantern.LanternForwardOffset; internal static List> CfgYawPairs => SpeciesYaw.Parse(SpeciesYawOffsets.Value); internal static Plugin Instance { get; private set; } internal static CompanionHost CkHost { get; private set; } internal PetLifecycle Lifecycle { get; private set; } internal PetPersistence Persistence { get; private set; } internal Pet ActivePet => Lifecycle?._activePet; internal bool ActivePetIsGhost => Lifecycle?._petIsGhost ?? false; internal float NextSniffEta => Mathf.Max(0f, SniffIntervalSeconds.Value - (Time.time - _sniffLast)); internal bool ReformInProgress { get { PetLifecycle lifecycle = Lifecycle; if (lifecycle == null) { return false; } return lifecycle.Acquisition.Active; } } internal static Character LocalPlayer { get { CharacterManager instance = CharacterManager.Instance; if (instance == null) { return null; } return instance.GetFirstLocalCharacter(); } } internal static Character LocalPlayerCharacter => LocalPlayer; internal static PetTuning Tuning => _tuning; private void WatchForStuckCast(Character player) { //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || !player.IsCasting) { _castOpenSince = 0f; _castWarned = false; return; } if (_castOpenSince <= 0f) { _castOpenSince = Time.time; _castWarned = false; return; } float num = Time.time - _castOpenSince; if (!_castWarned && num > CastWatchdogWarnSeconds.Value) { _castWarned = true; Log.LogMessage((object)$"[CAST] watchdog: a cast has been open {num:F1}s (CurrentSpellCast={player.CurrentSpellCast}) -- normal for a long vanilla cast; if wedged, force-clear comes at {CastWatchdogClearSeconds.Value:F0}s."); } if (CastWatchdogClearSeconds.Value > 0f && num > CastWatchdogClearSeconds.Value) { player.SetCastData((SpellCastType)(-1), (GameObject)null); Log.LogWarning((object)$"[CAST] watchdog: IsCasting wedged for {num:F1}s (CurrentSpellCast was {player.CurrentSpellCast}) -- force-cleared (bug-16 class)."); _castOpenSince = 0f; _castWarned = false; } } internal void Awake() { //IL_0354: Unknown result type (might be due to invalid IL or missing references) //IL_035a: Expected O, but got Unknown //IL_0561: Unknown result type (might be due to invalid IL or missing references) //IL_056b: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Instance = this; CkHost = CompanionHost.Create("cobalt.beastwhispering", "Beastwhispering", Log, (ICompanionSettings)(object)new BwCompanionSettings(), "", "PETCOMBAT"); ProxyPets.Configure(CkHost); Notify.Log = Log; BwConfig.Keys.Bind(((BaseUnityPlugin)this).Config); BwConfig.Pet.Bind(((BaseUnityPlugin)this).Config); BwConfig.Follow.Bind(((BaseUnityPlugin)this).Config); BwConfig.Diag.Bind(((BaseUnityPlugin)this).Config); BwConfig.Harvest.Bind(((BaseUnityPlugin)this).Config); BwConfig.Expedition.Bind(((BaseUnityPlugin)this).Config); BwConfig.Taming.Bind(((BaseUnityPlugin)this).Config); BwConfig.SelfTest.Bind(((BaseUnityPlugin)this).Config); BwConfig.Systems.Bind(((BaseUnityPlugin)this).Config); BwConfig.Temperature.Bind(((BaseUnityPlugin)this).Config); BwConfig.Combat.Bind(((BaseUnityPlugin)this).Config); BwConfig.HuntAsOne.Bind(((BaseUnityPlugin)this).Config); BwConfig.Sigils.Bind(((BaseUnityPlugin)this).Config); BwConfig.Gear.Bind(((BaseUnityPlugin)this).Config); BwConfig.Gifts.Bind(((BaseUnityPlugin)this).Config); BwConfig.BuffFoods.Bind(((BaseUnityPlugin)this).Config); BwConfig.Scent.Bind(((BaseUnityPlugin)this).Config); BwConfig.Scavenge.Bind(((BaseUnityPlugin)this).Config); BwConfig.HuntAsOne.BindRanged(((BaseUnityPlugin)this).Config); BwConfig.Anchor.Bind(((BaseUnityPlugin)this).Config); BwConfig.Hud.Bind(((BaseUnityPlugin)this).Config); BwConfig.PetPanel.Bind(((BaseUnityPlugin)this).Config); BwConfig.Synergy.Bind(((BaseUnityPlugin)this).Config); BwConfig.Brace.Bind(((BaseUnityPlugin)this).Config); BwConfig.SkillEcho.Bind(((BaseUnityPlugin)this).Config); BwConfig.ForTheKill.Bind(((BaseUnityPlugin)this).Config); BwConfig.Skills.Bind(((BaseUnityPlugin)this).Config); BwConfig.Wards.Bind(((BaseUnityPlugin)this).Config); BwConfig.Bandage.Bind(((BaseUnityPlugin)this).Config); BwConfig.Lantern.Bind(((BaseUnityPlugin)this).Config); BwConfig.MP.Bind(((BaseUnityPlugin)this).Config); TerrainGuard.UnloadModeProvider = () => BwConfig.Harvest.UnloadUnusedAssetsMode.Value; TerrainGuard.UnloadEveryNProvider = () => BwConfig.Harvest.UnloadEveryNHarvests.Value; TerrainGuard.FlushAfterPurgeProvider = () => BwConfig.Harvest.FlushTerrainAfterPurge.Value; Lifecycle = new PetLifecycle(this); Persistence = new PetPersistence(this, Lifecycle); ExpeditionOrchestrator.ActiveSpeciesProvider = () => Instance?.Lifecycle?._activePet?.State?.SpeciesId; ExpeditionOrchestrator.AutoWarmDeferred = true; ExpeditionOrchestrator.RegisterAlwaysWarmSpecies("Pearlbird"); BwConfig.Expedition.MigrateToCompanionKit(); ((Component)this).gameObject.AddComponent(); PetSkillSetup.Init(); PetCanActCondition.Init(); HuntAsOneCooldown.Init(); FoodCategoryTags.Init(); TamingFoodSetup.Init(); TamingFoodTable.Init(); PetDietTable.Init(); PetBuffTable.Init(); GrowthTable.Init(); YawTable.Init(); PetComfortTable.Init(); FoodHexTable.Init(); SpecialAttacks.Init(); SigilTable.Init(); PetGearTable.Init(); PetSenseTable.Init(); ScavengeTable.Init(); FeatherFletchSetup.Init(); FletchTable.Init(); GiftTable.Init(); ForTheKillTable.Init(); SkillEchoTable.Init(); WeatherFoodTable.Init(); BuffFoodTable.Init(); PetFeeder.LogLadder(); BlanketSetup.Init(); PetSigilSetup.Init(); PetStatusEffects.Init(); SynergyStatus.Init(); KillFavorStatus.Init(); GuestTame.Init(); PetProxyClient.Init(); MarenSetup.Init(((BaseUnityPlugin)this).Config); _ = TamingFoodTable.Table; _ = PetComfortTable.Bands; _tuning = BuildTuning(); _ = PetDietTable.Table; RegisterVerbs(); SceneManager.sceneLoaded += OnSceneLoaded; Harmony val = new Harmony("cobalt.beastwhispering"); val.PatchAll(); Patches patchInfo = Harmony.GetPatchInfo((MethodBase)AccessTools.Method(typeof(Character), "ReceiveHit", new Type[9] { typeof(Object), typeof(DamageList), typeof(Vector3), typeof(Vector3), typeof(float), typeof(float), typeof(Character), typeof(float), typeof(bool) }, (Type[])null)); BraceReceiveHit.Attached = patchInfo != null && patchInfo.Prefixes?.Count > 0; Log.LogMessage((object)$"[BRACE] ReceiveHit prefix attached: {BraceReceiveHit.Attached}."); Patches patchInfo2 = Harmony.GetPatchInfo((MethodBase)AccessTools.Method(typeof(Skill), "SkillStarted", (Type[])null, (Type[])null)); SkillEchoCast.Attached = patchInfo2 != null && patchInfo2.Postfixes?.Count > 0; Log.LogMessage((object)$"[ECHO] SkillStarted postfix attached: {SkillEchoCast.Attached}."); Patches patchInfo3 = Harmony.GetPatchInfo((MethodBase)AccessTools.Method(typeof(ProximityCondition), "CheckIsValid", (Type[])null, (Type[])null)); PetSigilComboAlias.Attached = patchInfo3 != null && patchInfo3.Postfixes?.Count > 0; Log.LogMessage((object)$"[PETSIGIL] ProximityCondition.CheckIsValid combo-alias postfix attached: {PetSigilComboAlias.Attached}."); HuntSynergy.LogArrowCapturePatchState(val); _channel = new CommandChannel("bw_cmd.txt", Log, _commands, 0.5f, true, true); RunSpeciesAudit(); NetBus.RegisterHandshakeFragment("bw.version", (Func)(() => "0.2.0")); NetBus.RegisterHandshakeFragment("bw.taming", (Func)delegate { List list = new List(); foreach (KeyValuePair item in TamingFoodSetup.Registered) { list.Add(TamingFoods.RegisteredIdLine(item.Key, item.Value.Item1, item.Value.Item2)); } return NetProtocol.HashTable(TamingFoods.HandshakeLines((IEnumerable)list, (IEnumerable)(TamingFoodTable.Table?.Values))); }); Log.LogMessage((object)"Beastwhispering 0.2.0 loaded."); Log.LogMessage((object)("[BW] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); if (RunSelfTestOnLoad.Value) { SelfTest.Run(Log); } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) Persistence.OnSceneLoaded(scene, mode); } internal static double HungerDayFor(PetSave s) { return ((s != null) ? PetDietTable.HungerFor(s.SpeciesId) : ((double?)null)) ?? ((double)HungerSecondsPerDay.Value); } internal void Tame() { Lifecycle.Tame(); } internal bool TameSpecific(Character src, Character actor = null) { return Lifecycle.TameSpecific(src, actor); } internal void SetPet(CompanionBody body, PetSave state) { Lifecycle.SetPet(body, state); } internal void ReleasePet(bool confirmed = false) { Lifecycle.ReleasePet(confirmed); } internal void RecallPet() { Lifecycle.RecallPet(); } internal void Expedition(string[] parts) { ExpeditionOrchestrator.RunVerb(parts); } internal void PersistPet() { Persistence.SaveNow(LocalPlayer); } internal void Update() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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_0043: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = TameKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { Tame(); } else { value = FeedKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { Feed(); } else { value = SelfTestKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { SelfTest.Run(Log); } else { value = RecallKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { RecallPet(); } else { value = DiagKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { Diag(); } } } } } Pet activePet = ActivePet; if (activePet != null) { ((Companion)activePet).RetryPendingAttributes(); } _channel.Tick(); if (DiagIntervalSeconds.Value > 0f && Time.unscaledTime - _diagLast > DiagIntervalSeconds.Value) { _diagLast = Time.unscaledTime; Diag(); } WatchForStuckCast(LocalPlayer); HuntSynergy.Tick(Time.time); BraceDriver.Tick(); TauntController.Tick(); WardShare.Tick(this); LanternShare.Tick(this); if (SimTicker.Due) { float dt = SimTicker.Advance(); bool flag = (Object)(object)NetworkLevelLoader.Instance != (Object)null && NetworkLevelLoader.Instance.IsGameplayPaused; if (ActivePet != null) { if (!flag) { TickSystems(dt); } else { PetSystems.NoteGate((ComfortGate)6); } AnchorUpkeep(); } else { PetStatusEffects.Clear(LocalPlayer); HuntSynergy.SyncBuffs(LocalPlayer, null); } PetProxyClient.Maintain(this); PetGifting.SyncCooldown(LocalPlayer); ForTheKillSkill.SyncCooldown(LocalPlayer); HuntAsOneCooldown.SyncCooldown(LocalPlayer); KillFavorBuff.Sync(LocalPlayer); } if (ActivePet != null && Time.time - _sniffLast >= SniffIntervalSeconds.Value) { _sniffLast = Time.time; ScentSniffer.Tick(ActivePet, LocalPlayer); } } internal void AnchorUpkeep() { if (ActivePet == null) { return; } if (!EnableAnchor.Value) { if ((Object)(object)((Companion)ActivePet).Anchor.Current != (Object)null) { ((Companion)ActivePet).Anchor.DestroyCurrent(); } } else { ((Companion)ActivePet).Tick(LocalPlayer, (MonoBehaviour)(object)this); } } internal static string Tail(string[] parts) { if (parts == null || parts.Length <= 1) { return null; } return string.Join(" ", parts, 1, parts.Length - 1); } internal static void RunSpeciesAudit() { //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_0015: 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_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0075: 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_0095: 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_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Expected O, but got Unknown Tables val = new Tables { Tamable = TamingFoodTable.Table.Keys, Diet = PetDietTable.Table.Keys, Donor = DonorHarvest.DonorScenes.Keys, Comfort = PetComfortTable.Bands.Keys, Buffs = PetBuffTable.Table.Keys, SpecialAttacks = SpecialAttacks.Table.Keys, FoodHex = FoodHexTable.Table.Keys, Gifts = GiftTable.Table.Keys, Senses = PetSenseTable.Table.Keys, Scavenge = ScavengeTable.Table.Keys, LoyaltyGrowth = GrowthTable.Table.Keys, BuffFoods = BuffFoodTable.Table.Keys, ForTheKill = ForTheKillTable.Table.Keys, SkillEchoes = SkillEchoTable.Table.Keys }; List list = SpeciesAudit.Run(val); int num = 0; foreach (Finding item in list) { if (item.IsWarning) { num++; } } Log.LogMessage((object)$"[AUDIT] {TamingFoodTable.Table.Count} tamable species checked against diet/donor/comfort/buff/special/food-hex/gifts/senses/scavenge/growth/buff-food/FTK/echo tables — {num} warning(s), {list.Count - num} note(s)."); foreach (Finding item2 in list) { if (item2.IsWarning) { Log.LogWarning((object)("[AUDIT] " + item2.Message)); } else { Log.LogMessage((object)("[AUDIT] " + item2.Message)); } } } private void RegisterVerbs() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown _commands = new CommandRegistry(Log); CommandRegistry commands = _commands; VerbHost h = new VerbHost(commands, Log, (Func)(() => LocalPlayer)); LifecycleVerbs.Register(commands, h, this); DiagVerbs.Register(commands, h, this); CombatVerbs.Register(commands, h, this); SystemVerbs.Register(commands, this); ReconVerbs.Register(commands, h, this); MarenVerbs.Register(commands, h); CaravanVerbs.Register(commands, h); TravelVerbs.Register(commands, h, this); TableVerbs.RegisterAll(commands, this); } internal void Feed(bool force = false) { PetFeeder.FeedFirstMatching(LocalPlayer, force); } internal void TickSystems(float dt) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0226: Unknown result type (might be due to invalid IL or missing references) Character localPlayer = LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { PetSystems.NoteGate((ComfortGate)4); return; } if (ActivePet?.Sim == null) { PetSystems.NoteGate((ComfortGate)5); return; } PetSystems.SimTickCount++; PetGearSense.Apply(ActivePet, localPlayer); PetSystems.ComfortSample cs = PetSystems.SampleComfort(ActivePet, localPlayer); int healthRecoveryLevel = ActivePet.State.HealthRecoveryLevel; PetStatus val = ActivePet.Sim.Tick(new PetTick { Dt = dt, TempStepsOutsideComfort = ((ComfortReading)(ref cs.Reading)).StepsOutside, TempSide = ((ComfortReading)(ref cs.Reading)).Side, TempEscalateMult = ((ComfortReading)(ref cs.Reading)).EscalateMult }); PetTemperatureDriver.LogComfortTransition(this, val, cs, localPlayer); if (val.HungerDecayDays > 0) { Log.LogMessage((object)$"[PET] hunger decay: {val.HungerDecayDays} day(s) unfed — loyalty {val.LoyaltyBeforeHungerDecay} -> {val.LoyaltyValue}."); } if (val.SelfHealHp > 0.0 && EnableFoodHealthRecovery.Value && ((Companion)ActivePet).Anchor != null) { ((Companion)ActivePet).Anchor.HealAmount((float)val.SelfHealHp, true); } if (healthRecoveryLevel > 0 && ActivePet.State.HealthRecoveryLevel == 0) { Log.LogMessage((object)"[REGEN] health recovery expired."); } PetSystems.ApplyEffects(ActivePet, val); if (val.Abandoned) { Log.LogMessage((object)"[PET] loyalty hit 0 — the pet abandons you."); Lifecycle.DespawnPet(localPlayer); } else if (!PetTemperatureDriver.TickTemperatureDrain(this, val, dt, localPlayer)) { if (PersistPetHealth.Value) { ActivePet.State.HealthFraction = PetPersistence.CurrentPersistFraction(ActivePet); } ActivePet.State.SpecialCooldownSecondsLeft = HuntCooldown.Drain(ActivePet.State.SpecialCooldownSecondsLeft, (double)dt); ActivePet.State.SigilCooldownSecondsLeft = HuntCooldown.Drain(ActivePet.State.SigilCooldownSecondsLeft, (double)dt); PetSaveMark val2 = default(PetSaveMark); ((PetSaveMark)(ref val2))..ctor(val.LoyaltyValue, ActivePet.State.BlanketKey, ActivePet.State.DrinkKey, ActivePet.State.BuffFoodKey, PetPersistence.HealthRecoveryMark(ActivePet.State), ActivePet.State.SpecialCooldownSecondsLeft); PetSaveMark val3 = default(PetSaveMark); ((PetSaveMark)(ref val3))..ctor(Persistence._lastSavedLoyalty, Persistence._lastSavedBlanket, Persistence._lastSavedDrink, Persistence._lastSavedBuffFood, Persistence._lastSavedHealthRecovery, Persistence._lastSavedSpecialCooldown); if (SavePolicy.ShouldSave(ref val2, ref val3, (double)(Time.time - Persistence._lastSaveAt), 30.0)) { Persistence.SaveNow(localPlayer); } } } internal void ResyncBuffs() { //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (ActivePet?.Sim != null) { PetSystems.ApplyEffects(ActivePet, ActivePet.Sim.Status()); } else { PetPassiveBuff.Clear(); PetBagPerk.Clear(); PetStatusEffects.Clear(LocalPlayer); HuntSynergy.SyncBuffs(LocalPlayer, null); } KillFavorBuff.Sync(LocalPlayer); } internal void ReloadCfg() { try { ((BaseUnityPlugin)this).Config.Reload(); FillTuning(_tuning); SlStatus.ResetIconRetries(); ResyncBuffs(); PetGifting.SyncCooldown(LocalPlayer); ForTheKillSkill.SyncCooldown(LocalPlayer); HuntAsOneCooldown.SyncCooldown(LocalPlayer); SynergyStatus.SyncDuration(); KillFavorStatus.SyncDuration(); PetSigilSetup.ApplyScale(); Log.LogMessage((object)($"[CMD] config reloaded from disk (UseSpeciesStats={UseSpeciesStats.Value}); effects re-applied. " + $"Tuning refreshed (SatiationFraction={SatiationFraction.Value}, SpeciesDailyDecay={SpeciesDailyDecay.Value}); " + "NB HungerSecondsPerDay/TempEscalateSeconds/TempRecoverSeconds are baked into the sim's timers at pet creation and only apply to the NEXT tame/reload.")); } catch (Exception ex) { Log.LogWarning((object)("[CMD] config reload failed: " + ex.Message)); } } internal void Diag() { BwDiagnostics.Diag(this); } internal void LearnSkills() { Character localPlayer = LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { Log.LogWarning((object)"[LEARNSKILLS] no local player."); return; } int[] allSkillIds = PetSkillSetup.AllSkillIds; foreach (int num in allSkillIds) { SkillRegistry.Learn(localPlayer, num); } } internal static bool IsCommandable(Character c, Character player) { //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Invalid comparison between Unknown and I4 //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Invalid comparison between Unknown and I4 //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Invalid comparison between Unknown and I4 if ((Object)(object)c != (Object)null && c.Alive && c.IsAI && (Object)(object)c != (Object)(object)player) { Plugin instance = Instance; object obj; if (instance == null) { obj = null; } else { Pet activePet = instance.ActivePet; obj = ((activePet != null) ? ((Companion)activePet).Anchor.Current : null); } if (((Object)obj == (Object)null || (Object)(object)c != (Object)(object)((Companion)Instance.ActivePet).Anchor.Current) && (int)c.Faction != 1 && (int)c.Faction != 7) { return (int)c.Faction > 0; } } return false; } internal static List> DamageScaleOverrides() { string text = SpeciesDamageScales.Value ?? ""; if (_damageScales != null && text == _damageScalesRaw) { return _damageScales; } _damageScalesRaw = text; _damageScales = PetPower.ParseOverrides(text); WarnOnDamageScales(text, _damageScales); return _damageScales; } private static void WarnOnDamageScales(string raw, List> parsed) { int num = 0; string[] array = raw.Split(new char[1] { ',' }); foreach (string text in array) { if (text.Trim().Length > 0) { num++; } } if (num > parsed.Count) { Log.LogWarning((object)($"[POWER] [Combat] SpeciesDamageScales: {num - parsed.Count} of {num} " + "entries were malformed and SKIPPED (expected 'Species=percent') — raw: '" + raw + "'.")); } foreach (KeyValuePair item in parsed) { if (PetPower.IsOutOfRange(item.Value)) { Log.LogWarning((object)($"[POWER] [Combat] SpeciesDamageScales: '{item.Key}={item.Value:F0}' is outside " + $"{0f:F0}-{1000f:F0}% — CLAMPED.")); } } } internal static float PetDamageScale(string speciesId, out float percent, out string matchedKey) { percent = PetPower.PercentFor(DamageScaleOverrides(), speciesId, PetDamageScalePercent.Value, ref matchedKey); return PetPower.Scale(percent); } internal static float PetDamageScale(string speciesId) { float percent; string matchedKey; return PetDamageScale(speciesId, out percent, out matchedKey); } internal static StatTuningPolicy TuningPolicy() { //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_0015: 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_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown return new StatTuningPolicy { HealthAt0 = HealthLoyaltyFactor0.Value, HealthAt100 = HealthLoyaltyFactor100.Value, DamageAt0 = DamageLoyaltyFactor0.Value, DamageAt100 = DamageLoyaltyFactor100.Value, DefenseAt0 = DefenseLoyaltyFactor0.Value, DefenseAt100 = DefenseLoyaltyFactor100.Value, SpeedAt0 = SpeedLoyaltyFactor0.Value, SpeedAt100 = SpeedLoyaltyFactor100.Value }; } private static PetTuning BuildTuning() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown PetTuning val = new PetTuning(); FillTuning(val); return val; } private static void FillTuning(PetTuning t) { t.HungerSecondsPerDay = HungerSecondsPerDay.Value; t.TempEscalateSeconds = TempEscalateSeconds.Value; t.TempRecoverSeconds = TempRecoverSeconds.Value; t.SpeciesDailyDecay = SpeciesDailyDecay.Value; t.LoyaltyGainScale = (double)Math.Max(0f, LoyaltyGainPercent.Value) / 100.0; t.SatiationFraction = SatiationFraction.Value; t.UneasyDecayMult = UneasyDecayMult.Value; t.SufferingDecayMult = SufferingDecayMult.Value; } } [HarmonyPatch(typeof(LootableOnDeath), "OnDeath")] internal static class RecipeScrollDrops { private static readonly HashSet _rolled = new HashSet(); internal static void ResetForScene() { _rolled.Clear(); } internal static void Postfix(LootableOnDeath __instance, bool _loadedDead) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Invalid comparison between Unknown and I4 //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) try { if (!Plugin.EnableTamingFoods.Value || (Object)(object)__instance == (Object)null || !((Behaviour)__instance).enabled || _loadedDead || PhotonNetwork.isNonMasterClientInRoom) { return; } Character character = __instance.Character; if ((Object)(object)character == (Object)null || character.Alive || !character.IsAI || (int)character.Faction == 1) { return; } ItemContainer val = (((Object)(object)character.Inventory != (Object)null) ? character.Inventory.Pouch : null); if ((Object)(object)val == (Object)null || !((Item)val).HasInteractionTrigger) { return; } TamingEntry val2 = TamingFoodTable.Resolve(character.Name); if (val2 == null || !TamingFoodSetup.Registered.TryGetValue(val2.Species, out (int, int) value) || !_rolled.Add(((object)character.UID/*cast due to .constrained prefix*/).ToString())) { return; } if (BwConfig.Skills.ScatologyGatesScrolls.Value && !AnyPlayerKnowsScatology()) { Plugin.Log.LogMessage((object)("[TAMING] recipe scroll for '" + character.Name + "' withheld — nobody knows Scatology " + $"({87006}) [{ScatologyKnowledgeBreakdown()}].")); return; } double num = Random.value; double num2 = val2.DropChance * (double)Plugin.RecipeDropChance.Value; if (!TamingFoods.ShouldDrop(val2.DropChance, (double)Plugin.RecipeDropChance.Value, num)) { Plugin.Log.LogMessage((object)$"[TAMEDROP] '{character.Name}' rolled {num:0.000} vs {num2:0.000} — no scroll."); return; } Item val3 = ItemManager.Instance.GenerateItemNetwork(value.Item2); if ((Object)(object)val3 == (Object)null) { Plugin.Log.LogWarning((object)$"[TAMEDROP] GenerateItemNetwork({value.Item2}) returned null — scroll not dropped."); return; } val3.ChangeParent(((Component)val).transform); Plugin.Log.LogMessage((object)$"[TAMEDROP] '{character.Name}' dropped 'Recipe: {val2.ChowName}' (roll {num:0.000} vs {num2:0.000})."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TAMEDROP] OnDeath postfix error: " + ex)); } } private static bool AnyPlayerKnowsScatology() { CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return false; } for (int i = 0; i < instance.PlayerCharacters.Count; i++) { Character character = instance.GetCharacter(instance.PlayerCharacters.Values[i]); object obj; if (character == null) { obj = null; } else { CharacterInventory inventory = character.Inventory; obj = ((inventory != null) ? inventory.SkillKnowledge : null); } if (!((Object)obj == (Object)null) && ((CharacterKnowledge)character.Inventory.SkillKnowledge).IsItemLearned(87006)) { return true; } } return false; } private static string ScatologyKnowledgeBreakdown() { CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return "no CharacterManager"; } List list = new List(); for (int i = 0; i < instance.PlayerCharacters.Count; i++) { Character character = instance.GetCharacter(instance.PlayerCharacters.Values[i]); if ((Object)(object)character == (Object)null) { list.Add("?=n/a"); continue; } CharacterInventory inventory = character.Inventory; if ((Object)(object)((inventory != null) ? inventory.SkillKnowledge : null) == (Object)null) { list.Add(character.Name + "=n/a"); } else { list.Add($"{character.Name}={((CharacterKnowledge)character.Inventory.SkillKnowledge).IsItemLearned(87006)}"); } } if (list.Count != 0) { return string.Join(", ", list); } return "no players"; } } [HarmonyPatch(typeof(PlayerCharacterStats), "UpdateStatsAfterRest")] internal static class RestHealPatch { internal static void Postfix(PlayerCharacterStats __instance) { try { if (!Plugin.RestHealsPet.Value) { return; } Character val = (((Object)(object)__instance != (Object)null) ? ((CharacterStats)__instance).m_character : null); if ((Object)(object)val == (Object)null || (Object)(object)val != (Object)(object)Plugin.LocalPlayer) { return; } Pet pet = Plugin.Instance?.ActivePet; if (pet == null) { return; } CompanionAnchor anchor = ((Companion)pet).Anchor; float num = default(float); float num2 = default(float); if (anchor == null || !anchor.TryGetHealth(ref num, ref num2)) { return; } float num3 = (((Object)(object)val.CharacterResting != (Object)null) ? val.CharacterResting.GetPostRestSleepLength() : 0f); if (num3 <= 0f) { Plugin.Log.LogMessage((object)$"[REST] sleep 0.0h — no pet heal (hp {num:F0}/{num2:F0})."); return; } if (RestHeal.IsFullHeal((double)num3)) { anchor.Heal(); } else { anchor.HealAmount((float)RestHeal.HealAmount((double)num3, (double)num2), false); } float num4 = default(float); float num5 = default(float); anchor.TryGetHealth(ref num4, ref num5); Plugin.Log.LogMessage((object)$"[REST] sleep {num3:F1}h → pet heal {num:F0}->{num4:F0}/{num2:F0} (frac {RestHeal.HealFraction((double)num3):0.00})."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[REST] UpdateStatsAfterRest postfix error: " + ex)); } } } [HarmonyPatch(typeof(TreasureChest), "ProcessGenerateContent")] internal static class ScavengeBonus { internal static string LastFill; internal static void Prefix(TreasureChest __instance, out bool __state) { __state = (Object)(object)__instance != (Object)null && (__instance.m_ignoreHasGeneratedContent || !__instance.m_hasGeneratedContent); } internal static void Postfix(TreasureChest __instance, bool __state) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) try { if (!__state || !Plugin.EnableScavengeBonus.Value || (Object)(object)__instance == (Object)null || PhotonNetwork.isNonMasterClientInRoom || (int)((ItemContainer)__instance).SpecialType != 0) { return; } List list = Candidates(); if (list.Count == 0) { return; } ScavengePick val = PetScavenge.PickBonus((IList)list, ((Item)__instance).ItemID, ((Item)__instance).Name); if (val.Winner == null) { foreach (ScavengeCandidate item in val.MatchedButZero) { Plugin.Log.LogMessage((object)("[SCAVENGE] '" + ((Item)__instance).Name.Trim() + "': " + item.Species + " (" + item.Label + ") " + $"nosed it over, but tier {item.Tier} grants no extra slots.")); } return; } int extraSlots = val.ExtraSlots; int num = RollExtra(__instance, extraSlots); LastFill = $"'{((Item)__instance).Name.Trim()}' (ItemID {((Item)__instance).ItemID}): +{extraSlots} slot(s) at tier " + $"{val.Winner.Tier} ({val.Winner.Label}) across {num} drop table(s)"; Plugin.Log.LogMessage((object)("[SCAVENGE] " + val.Winner.Species + " rummages: " + LastFill + ".")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SCAVENGE] ProcessGenerateContent postfix error: " + ex)); } } internal static List Candidates() { //IL_0030: 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_0040: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown List list = new List(); Pet pet = (((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.ActivePet : null); if (pet?.Sim != null) { list.Add(new ScavengeCandidate { Label = "host pet", Species = pet.SpeciesId, Entry = ScavengeTable.Resolve(pet.SpeciesId), Tier = pet.Sim.Status().Loyalty }); } foreach (ProxyInfo item in ProxyPets.ProxyInfos().OrderBy((ProxyInfo r) => r.OwnerUid, StringComparer.Ordinal)) { list.Add(new ScavengeCandidate { Label = "guest '" + item.OwnerUid + "'", Species = item.SpeciesKey, Entry = ScavengeTable.Resolve(item.SpeciesKey), Tier = PetScavenge.TierFromLevel(item.LoyaltyTier) }); } return list; } private static int RollExtra(TreasureChest chest, int extra) { int num = 0; List drops = ((SelfFilledItemContainer)chest).m_drops; if (drops == null) { return 0; } foreach (Dropable item in drops) { if ((Object)(object)item == (Object)null) { continue; } List mainDropTables = item.m_mainDropTables; if (mainDropTables != null) { foreach (DropTable item2 in mainDropTables) { if ((Object)(object)item2 != (Object)null && RollOne(item2, chest, extra)) { num++; } } } List conditionalDropTables = item.m_conditionalDropTables; if (conditionalDropTables == null) { continue; } foreach (ConditionalDropTable item3 in conditionalDropTables) { if ((Object)(object)((ConditionalDropper)(object)item3)?.Dropper != (Object)null && ((ConditionalDropper)(object)item3).Validate(item, item.ResponsibleCharacter) && RollOne(((ConditionalDropper)(object)item3).Dropper, chest, extra)) { num++; } } } return num; } private static bool RollOne(DropTable table, TreasureChest chest, int extra) { PinDice(table, extra, delegate { ((ItemDropper)table).GenerateDrop((ItemContainer)(object)chest); }); return true; } internal static void PinDice(DropTable table, int extra, Action body) { int minNumberOfDrops = table.MinNumberOfDrops; int maxNumberOfDrops = table.MaxNumberOfDrops; table.MinNumberOfDrops = extra; table.MaxNumberOfDrops = extra; try { body(); } finally { table.MinNumberOfDrops = minNumberOfDrops; table.MaxNumberOfDrops = maxNumberOfDrops; } } internal static void Dump(Plugin p) { //IL_01ee: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)($"[SCAVENGE] ── table ({ScavengeTable.Table.Count} species, " + $"EnableScavengeBonus={Plugin.EnableScavengeBonus.Value}) ──")); foreach (PetScavengeEntry value in ScavengeTable.Table.Values) { List list = new List(); foreach (int containerId in value.ContainerIds) { list.Add(containerId.ToString()); } Plugin.Log.LogMessage((object)("[SCAVENGE] '" + value.Species + "': containers [" + string.Join(", ", value.ContainerCandidates.ToArray()) + ((list.Count > 0) ? (" + ItemIDs " + string.Join(", ", list.ToArray())) : "") + "], slots/tier [" + string.Join(", ", Slots(value)) + "].")); } List list2 = Candidates(); if (list2.Count == 0) { Plugin.Log.LogMessage((object)"[SCAVENGE] no active pet and no guest-pet proxies — no perk in force."); return; } foreach (ScavengeCandidate item in list2) { if (item.Entry == null) { Plugin.Log.LogMessage((object)("[SCAVENGE] '" + item.Species + "' (" + item.Label + ") has no scavenge entry — no perk.")); } else { Plugin.Log.LogMessage((object)($"[SCAVENGE] '{item.Species}' ({item.Label}) at tier {item.Tier}: a matching container " + $"would roll +{PetScavenge.ExtraSlots(item.Entry, item.Tier)} slot(s) per drop table.")); } } Plugin.Log.LogMessage((object)"[SCAVENGE] pick order: host pet first, then guest proxies in OwnerUid order — one pet's bonus per fill (a tier-0 match never blocks a later candidate)."); Plugin.Log.LogMessage((object)("[SCAVENGE] last armed fill: " + (LastFill ?? "none this session."))); } private static string[] Slots(PetScavengeEntry entry) { int[] array = entry.SlotsPerTier ?? PetScavenge.DefaultSlotsPerTier; string[] array2 = new string[array.Length]; for (int i = 0; i < array.Length; i++) { array2[i] = array[i].ToString(); } return array2; } } internal static class ScavengeSim { private const string Tag = "[SCAVENGE]"; internal const int IterationsDefault = 1000; internal const int IterationsMin = 10; internal const int IterationsMax = 200000; internal const float RadiusDefault = 15f; internal static void Run(Plugin plugin, Character player, string[] parts, ManualLogSource log) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: 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_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Unknown result type (might be due to invalid IL or missing references) //IL_02da: Unknown result type (might be due to invalid IL or missing references) //IL_02df: Unknown result type (might be due to invalid IL or missing references) //IL_0365: Unknown result type (might be due to invalid IL or missing references) ParseArgs(parts, out var iterations, out var filter, out var radius, out var clamped, out var radiusClamped); if (clamped) { log.LogMessage((object)(string.Format("{0} sim: iterations clamped to {1} ", "[SCAVENGE]", iterations) + $"(range {10}-{200000}).")); } if (radiusClamped) { log.LogMessage((object)(string.Format("{0} sim: radius clamped to {1:F0}m ", "[SCAVENGE]", radius) + $"(range {1.0:F0}-{60.0:F0}).")); } Vector3 position = ((Component)player).transform.position; List list = Containers.Scan(position, radius, log); ItemContainer val = Containers.Nearest(list, position, filter); if ((Object)(object)val == (Object)null) { log.LogWarning((object)(string.Format("{0} sim: no container matching '{1}' within {2:F0}m ", "[SCAVENGE]", filter ?? "", radius) + $"({list.Count} container(s) were in range). Try `containerdump {radius:F0}` for the census.")); return; } SelfFilledItemContainer val2 = (SelfFilledItemContainer)(object)((val is SelfFilledItemContainer) ? val : null); if ((Object)(object)val2 == (Object)null) { log.LogWarning((object)("[SCAVENGE] sim: '" + (((Item)val).Name ?? "").Trim() + "' is a " + ((object)val).GetType().Name + " with no drop tables — nothing to simulate.")); return; } List list2 = ScavengeBonus.Candidates(); ScavengePick val3 = PetScavenge.PickBonus((IList)list2, ((Item)val).ItemID, ((Item)val).Name); int num = ((val3.Winner != null) ? val3.ExtraSlots : 0); string text = null; if (!(val is TreasureChest)) { text = "the live perk patches TreasureChest.ProcessGenerateContent, and this is a " + ((object)val).GetType().Name + " — a SIBLING type that calls base, never TreasureChest's. The patch CANNOT fire here"; } else if ((int)val.SpecialType != 0) { text = "the live postfix bails on SpecialType != None and this container is " + $"{val.SpecialType} (stash/legacy/enchant containers are never scavenged). " + "The patch will not fire here"; } if (text != null) { num = 0; } string arg = ((text != null) ? "+0 slot(s) — NOT REACHABLE" : ((val3.Winner != null) ? $"{val3.Winner.Species} ({val3.Winner.Label}) at tier {val3.Winner.Tier} -> +{num} slot(s)" : ((list2.Count == 0) ? "no pet known to this client -> +0 slot(s)" : "no candidate grants slots on this container (wrong species, wrong container, or tier Gone) -> +0 slot(s)"))); log.LogMessage((object)(string.Format("{0} sim: '{1}' (id {2}, {3}, ", "[SCAVENGE]", (((Item)val).Name ?? "").Trim(), ((Item)val).ItemID, ((object)val).GetType().Name) + $"{Vector3.Distance(((Component)val).transform.position, position):F1}m), n={iterations} iteration(s), {arg}")); if (text != null) { log.LogWarning((object)("[SCAVENGE] sim: NOT REACHABLE — " + text + ". Reporting +0 slot(s) regardless of the pet: the vanilla column below is the whole story for this container. " + ((val3.Winner != null) ? ("(For the record, the pick rule WOULD have granted " + val3.Winner.Species + " " + $"+{val3.ExtraSlots} slot(s) at tier {val3.Winner.Tier} on a reachable container — " + "that gap is the finding, see container-verb-testplan.md CV1.)") : ""))); } else if (num == 0) { log.LogMessage((object)"[SCAVENGE] sim: the bonus is ZERO here — the vanilla column below is the whole story."); } float totalVanilla = 0f; float totalBonus = 0f; int tables = 0; foreach (Dropable item in val2.m_drops ?? new List()) { if ((Object)(object)item == (Object)null) { continue; } if (item.m_mainDropTables != null) { foreach (DropTable mainDropTable in item.m_mainDropTables) { if ((Object)(object)mainDropTable != (Object)null) { SimTable(mainDropTable, "main", num, iterations, log, ref totalVanilla, ref totalBonus, ref tables); } } } if (item.m_conditionalDropTables == null) { continue; } foreach (ConditionalDropTable conditionalDropTable in item.m_conditionalDropTables) { if (!((Object)(object)((ConditionalDropper)(object)conditionalDropTable)?.Dropper == (Object)null)) { if (!((ConditionalDropper)(object)conditionalDropTable).Validate(item, item.ResponsibleCharacter)) { log.LogMessage((object)("[SCAVENGE] sim: cond '" + ((Object)((ConditionalDropper)(object)conditionalDropTable).Dropper).name + "': Validate() false right now — skipped, same as a real fill.")); } else { SimTable(((ConditionalDropper)(object)conditionalDropTable).Dropper, "cond", num, iterations, log, ref totalVanilla, ref totalBonus, ref tables); } } } } if (tables == 0) { log.LogMessage((object)"[SCAVENGE] sim: this container has no chance-based drop tables (guaranteed drops only) — nothing the perk can add to."); return; } log.LogMessage((object)(string.Format("{0} sim: TOTAL across {1} table(s): vanilla avg value ", "[SCAVENGE]", tables) + $"{F(totalVanilla)} -> +{num} bonus dice avg {F(totalVanilla + totalBonus)} " + "(delta +" + F(totalBonus) + ", " + Pct(totalVanilla, totalBonus) + ").")); } private static void SimTable(DropTable table, string kind, int extra, int iterations, ManualLogSource log, ref float totalVanilla, ref float totalBonus, ref int tables) { tables++; float num = table.SimulateAverageValue(iterations); float bonus = 0f; if (extra > 0) { ScavengeBonus.PinDice(table, extra, delegate { bonus = table.SimulateAverageValue(iterations); }); } totalVanilla += num; totalBonus += bonus; log.LogMessage((object)(string.Format("{0} sim: {1} '{2}' (dice {3}-{4}): ", "[SCAVENGE]", kind, ((Object)table).name, table.MinNumberOfDrops, table.MaxNumberOfDrops) + $"vanilla avg value {F(num)} -> +{extra} bonus dice avg {F(num + bonus)} " + "(delta +" + F(bonus) + ").")); } private static string F(float v) { return v.ToString("F2", CultureInfo.InvariantCulture); } private static string Pct(float baseline, float delta) { if (!(baseline <= 0.0001f)) { return "+" + (delta / baseline * 100f).ToString("F0", CultureInfo.InvariantCulture) + "%"; } return "baseline ~0, no meaningful ratio"; } internal static void ParseArgs(string[] parts, out int iterations, out string filter, out float radius, out bool clamped, out bool radiusClamped) { iterations = 1000; filter = null; radius = 15f; clamped = false; radiusClamped = false; int num = 1; int num2 = ((parts != null) ? parts.Length : 0); if (num2 > num && int.TryParse((parts[num] ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { int num3 = ((result < 10) ? 10 : ((result > 200000) ? 200000 : result)); clamped = num3 != result; iterations = num3; num++; } if (num2 > num && double.TryParse((parts[num2 - 1] ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { double num4 = Math.Min(60.0, Math.Max(1.0, result2)); radiusClamped = num4 != result2; radius = (float)num4; num2--; } if (num2 > num) { string text = string.Join(" ", parts, num, num2 - num).Trim(); if (text.Length > 0) { filter = text; } } } } internal static class ScavengeTable { private const string FileName = "PetScavenge.json"; private const string Tag = "[SCAVENGE]"; private static readonly TableLoader _loader = new TableLoader("PetScavenge.json", "[SCAVENGE]", "species scavenge perk", "species scavenge perks", (Func, Dictionary>)PetScavenge.Parse, (Func, Dictionary, Dictionary>)PetScavenge.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[SCAVENGE]", Validate); internal static Dictionary Table => _axis.Table; internal static PetScavengeEntry Resolve(string speciesId) { return PetScavenge.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { foreach (PetScavengeEntry value in Table.Values) { List list = new List(); foreach (int containerId in value.ContainerIds) { if ((Object)(object)ResourcesPrefabManager.Instance.GetItemPrefab(containerId) == (Object)null) { Plugin.Log.LogWarning((object)(string.Format("{0} '{1}': authored ItemID {2} is not a known ", "[SCAVENGE]", value.Species, containerId) + "catalog item — that id can never match a container (name candidates still arm).")); } else { list.Add($"ItemID {containerId}"); } } foreach (string containerCandidate in value.ContainerCandidates) { if (ItemNameIndex.TryResolveCatalog(containerCandidate, out var itemId, out var via)) { list.Add(string.Format("'{0}' (ItemID {1} via {2} — put the number in {3} to make it locale-proof)", containerCandidate, itemId, via, "PetScavenge.json")); } else { list.Add("'" + containerCandidate + "' (live-name only — not in the catalog under that spelling)"); } } Plugin.Log.LogMessage((object)("[SCAVENGE] '" + value.Species + "' armed: " + ((list.Count > 0) ? string.Join(" + ", list.ToArray()) : "NOTHING — perk is inert (fix the table)") + "; slots/tier [" + string.Join(", ", SlotStrings(value)) + "].")); } Plugin.Log.LogMessage((object)(string.Format("{0} boot check: {1} species scavenge perk(s), ", "[SCAVENGE]", Table.Count) + $"EnableScavengeBonus={Plugin.EnableScavengeBonus.Value}.")); } private static string[] SlotStrings(PetScavengeEntry entry) { int[] array = entry.SlotsPerTier ?? PetScavenge.DefaultSlotsPerTier; string[] array2 = new string[array.Length]; for (int i = 0; i < array.Length; i++) { array2[i] = array[i].ToString(); } return array2; } } internal static class ScentIndication { internal static void Indicate(in ScentContext ctx) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Invalid comparison between Unknown and I4 //IL_00cb: Unknown result type (might be due to invalid IL or missing references) if (Plugin.IndicateToast.Value && string.Equals(ctx.Sense?.Kind, "character", StringComparison.OrdinalIgnoreCase)) { string text = PetSenses.DirectionLabel(ctx.Alert.Direction); string text2 = (((int)ctx.Alert.Strength == 1) ? "close by" : ("to the " + text)); Notify.Player(Plugin.LocalPlayerCharacter, "Your companion scents " + ctx.Alert.SenseKey + " " + text2 + "."); } if (!Plugin.IndicatePoint.Value) { return; } CompanionBody val = ((Companion)(ctx.Pet?)).Body; if (!((Object)(object)val == (Object)null)) { val.FacePoint(ctx.TargetPosition, Plugin.PointSeconds.Value); CompanionCombat combat = ((Companion)ctx.Pet).Combat; if (combat != null) { combat.PlayVocal(0); } Plugin.Log.LogMessage((object)$"[SCENT] point: facing the '{ctx.Alert.SenseKey}' spawn for {Plugin.PointSeconds.Value:F0}s."); } } } internal sealed class SenseMap { internal readonly Dictionary ById = new Dictionary(); internal readonly Dictionary ByName = new Dictionary(StringComparer.OrdinalIgnoreCase); internal readonly List Characters = new List(); internal int Count => ById.Count + ByName.Count + Characters.Count; } internal static class ScentSense { private static readonly Vector3 ProbeOffset = new Vector3(0f, -0.5f, 0f); private static readonly Collider[] _hits = (Collider[])(object)new Collider[32]; private static bool _truncationWarned; private static readonly Dictionary _maps = new Dictionary(StringComparer.OrdinalIgnoreCase); private static float _maxRadius; internal static readonly List None = new List(); private const float DupeMeters = 5f; private static readonly Dictionary _componentTypes = new Dictionary(StringComparer.Ordinal); private static void WarnIfTruncated(int n, float radius, string where) { if (n >= _hits.Length && !_truncationWarned) { _truncationWarned = true; Plugin.Log.LogWarning((object)($"[SCENT] {where}: the overlap buffer FILLED ({n}/{_hits.Length} colliders within " + $"{radius:F0}m) — hits beyond it were dropped by the physics query, so a nearer node may be missed " + "here and in `scentdump`. Warned once per session.")); } } internal static void Rebuild(Dictionary maps) { _maps.Clear(); _maxRadius = 0f; if (maps == null) { return; } foreach (KeyValuePair map in maps) { _maps[map.Key] = map.Value; foreach (SenseDef value in map.Value.ById.Values) { if (value.Radius > _maxRadius) { _maxRadius = value.Radius; } } foreach (SenseDef value2 in map.Value.ByName.Values) { if (value2.Radius > _maxRadius) { _maxRadius = value2.Radius; } } } } internal static bool HasNose(string speciesTableKey) { if (speciesTableKey != null) { return _maps.ContainsKey(speciesTableKey); } return false; } internal static SenseMap MapFor(string speciesTableKey) { if (speciesTableKey == null || !_maps.TryGetValue(speciesTableKey, out var value)) { return null; } return value; } private static SenseDef Match(SenseMap map, Item item) { if (item.ItemID != 0 && map.ById.TryGetValue(item.ItemID, out var value)) { return value; } string name = item.Name; if (!string.IsNullOrEmpty(name) && map.ByName.TryGetValue(name.Trim(), out var value2)) { return value2; } return null; } private static string TargetIdOf(Item item) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0028: 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) string uID = item.UID; if (!string.IsNullOrEmpty(uID)) { return uID; } Vector3 position = ((Component)item).transform.position; return $"{item.Name}@{position.x:F0},{position.z:F0}"; } internal static bool SceneIsTownOrCity() { if ((Object)(object)AreaManager.Instance != (Object)null) { return AreaManager.Instance.GetIsCurrentAreaTownOrCity(); } return false; } private static bool Suppressed(SenseDef def, bool townOrCity) { return !PetSenses.ScopeAllows(def.Scope, townOrCity); } internal static List HitsAt(Vector3 pos, string speciesTableKey) { //IL_001e: 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_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) if (speciesTableKey == null || !_maps.TryGetValue(speciesTableKey, out var value)) { return None; } bool flag = SceneIsTownOrCity(); Vector3 val = pos + ProbeOffset; int num = Physics.OverlapSphereNonAlloc(val, _maxRadius, _hits, Global.WorldItemsMask); WarnIfTruncated(num, _maxRadius, "sniff"); List hits = null; HashSet hashSet = null; for (int i = 0; i < num; i++) { Collider val2 = _hits[i]; _hits[i] = null; if ((Object)(object)val2 == (Object)null) { continue; } Item componentInParent = ((Component)val2).GetComponentInParent(); if ((Object)(object)componentInParent == (Object)null || !componentInParent.IsInWorld) { continue; } SenseDef val3 = Match(value, componentInParent); if (val3 == null || Suppressed(val3, flag)) { continue; } string text = TargetIdOf(componentInParent); if (hashSet != null && hashSet.Contains(text)) { continue; } Vector3 position = ((Component)componentInParent).transform.position; float num2 = Vector3.Distance(position, pos); if (!(num2 > val3.Radius) && !RejectEmpty(componentInParent)) { if (hits == null) { hits = new List(2); hashSet = new HashSet(); } hashSet.Add(text); hits.Add(new ScentHit { SenseKey = val3.Key, TargetId = text, Dx = position.x - pos.x, Dz = position.z - pos.z, Distance = num2 }); } } if (value.Characters.Count > 0) { CharacterHitsInto(pos, value, flag, ref hits); ComponentHitsInto(pos, value, flag, ref hits); } return hits ?? None; } private static void CharacterHitsInto(Vector3 pos, SenseMap map, bool town, ref List hits) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Invalid comparison between Unknown and I4 //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00da: 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) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return; } foreach (Character value in instance.Characters.Values) { if ((Object)(object)value == (Object)null || !value.Alive || (int)value.Faction == 1) { continue; } SenseDef val = MatchCharacter(map, value); if (val == null || Suppressed(val, town)) { continue; } Vector3 position = ((Component)value).transform.position; float num = Vector3.Distance(position, pos); if (!(num > val.Radius)) { if (hits == null) { hits = new List(2); } hits.Add(new ScentHit { SenseKey = val.Key, TargetId = CharacterIdOf(value), Dx = position.x - pos.x, Dz = position.z - pos.z, Distance = num }); } } } private static SenseDef MatchCharacter(SenseMap map, Character c) { string name = c.Name; foreach (SenseDef character in map.Characters) { if (PetSenses.MatchesCharacter(character, name)) { return character; } } string text = ((object)Unsafe.As(ref c.Faction)/*cast due to .constrained prefix*/).ToString(); foreach (SenseDef character2 in map.Characters) { if (PetSenses.MatchesFaction(character2, text)) { return character2; } } return null; } private static string CharacterIdOf(Character c) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) string text = UID.op_Implicit(c.UID); if (string.IsNullOrEmpty(text)) { return c.Name + "@char"; } return text; } private static void ComponentHitsInto(Vector3 pos, SenseMap map, bool town, ref List hits) { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008d: 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_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00af: 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_00fb: 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_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0117: 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) foreach (SenseDef character in map.Characters) { if (Suppressed(character, town)) { continue; } foreach (string componentCandidate in character.ComponentCandidates) { Type type = ResolveComponentType(componentCandidate); if (type == null) { continue; } Object[] array = Object.FindObjectsOfType(type); foreach (Object val in array) { Component val2 = (Component)(object)((val is Component) ? val : null); if ((Object)(object)val2 == (Object)null) { continue; } Vector3 position = val2.transform.position; float num = Vector3.Distance(position, pos); if (!(num > character.Radius) && !IsDuplicateHit(hits, character.Key, pos, position)) { if (hits == null) { hits = new List(2); } hits.Add(new ScentHit { SenseKey = character.Key, TargetId = $"comp:{componentCandidate}#{((Object)val2).GetInstanceID()}", Dx = position.x - pos.x, Dz = position.z - pos.z, Distance = num }); } } } } } private static bool IsDuplicateHit(List hits, string senseKey, Vector3 pos, Vector3 target) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: 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_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: 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) if (hits == null) { return false; } foreach (ScentHit hit in hits) { if (!(hit.SenseKey != senseKey)) { float num = pos.x + hit.Dx - target.x; float num2 = pos.z + hit.Dz - target.z; if (num * num + num2 * num2 <= 25f) { return true; } } } return false; } internal static Type ResolveComponentType(string name) { if (string.IsNullOrEmpty(name)) { return null; } if (_componentTypes.TryGetValue(name, out var value)) { return value; } Type type = AccessTools.TypeByName(name); if (type == null) { Plugin.Log.LogWarning((object)("[SCENT] component candidate '" + name + "' is not a known type — that matcher can never fire.")); } else if (!typeof(Component).IsAssignableFrom(type)) { Plugin.Log.LogWarning((object)("[SCENT] component candidate '" + name + "' is not a Unity Component — that matcher can never fire.")); type = null; } _componentTypes[name] = type; return type; } private static bool RejectEmpty(Item item) { if (!Plugin.SkipEmptyGatherables.Value) { return false; } Gatherable val = (Gatherable)(object)((item is Gatherable) ? item : null); if ((Object)(object)val == (Object)null) { return false; } if (((ItemContainer)val).IsEmpty) { return !val.CanGather; } return false; } internal static string Describe(Vector3 pos, string speciesTableKey) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: 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_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_0412: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) //IL_027e: Unknown result type (might be due to invalid IL or missing references) SenseMap value = null; if (speciesTableKey != null) { _maps.TryGetValue(speciesTableKey, out value); } bool flag = SceneIsTownOrCity(); float num = Mathf.Max(_maxRadius, 60f); Vector3 val = pos + ProbeOffset; int num2 = Physics.OverlapSphereNonAlloc(val, num, _hits, Global.WorldItemsMask); WarnIfTruncated(num2, num, "scentdump"); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(" scene class: " + (flag ? "TOWN/CITY (overworld-scoped senses suppressed)" : "overworld/other")); if (num2 >= _hits.Length) { stringBuilder.AppendLine($" ⚠ overlap buffer FULL ({num2}/{_hits.Length}) — this listing is TRUNCATED; " + "a nearer node may be missing from it."); } if (num2 == 0) { stringBuilder.AppendLine($" no WorldItems-layer colliders within {num:F0}m of the probe."); stringBuilder.Append(DescribeCharacters(pos, value, flag)); return stringBuilder.ToString().TrimEnd(Array.Empty()); } HashSet hashSet = new HashSet(); int num3 = 0; for (int i = 0; i < num2; i++) { Collider val2 = _hits[i]; _hits[i] = null; if ((Object)(object)val2 == (Object)null) { continue; } Item componentInParent = ((Component)val2).GetComponentInParent(); if ((Object)(object)componentInParent == (Object)null || !hashSet.Add(TargetIdOf(componentInParent))) { continue; } num3++; string text = $" '{componentInParent.Name}' (id={componentInParent.ItemID}, {Vector3.Distance(((Component)componentInParent).transform.position, pos):F1}m, prefab '{((Object)val2).name}')"; if (!componentInParent.IsInWorld) { stringBuilder.AppendLine(text + ": not IsInWorld — REJECT."); continue; } SenseDef val3 = ((value != null) ? Match(value, componentInParent) : null); if (val3 == null) { stringBuilder.AppendLine(text + ": no id/name match for this species — ignored (candidate for PetSenses.json?)."); continue; } if (Suppressed(val3, flag)) { stringBuilder.AppendLine(text + ": sense '" + val3.Key + "' but scope '" + val3.Scope + "' in a town/city scene — SUPPRESSED."); continue; } string text2 = ((componentInParent.ItemID != 0 && value.ById.ContainsKey(componentInParent.ItemID)) ? "id" : "live name"); float num4 = Vector3.Distance(((Component)componentInParent).transform.position, pos); if (num4 > val3.Radius) { stringBuilder.AppendLine($"{text}: sense '{val3.Key}' (by {text2}) but {num4:F1}m > radius {val3.Radius:F0} — REJECT."); } else if (RejectEmpty(componentInParent)) { stringBuilder.AppendLine(text + ": sense '" + val3.Key + "' (by " + text2 + ") but already harvested (empty) — REJECT."); } else { stringBuilder.AppendLine(text + ": sense '" + val3.Key + "' (by " + text2 + ") — ACCEPT (" + ((num4 <= val3.NearRadius) ? "strong" : "faint") + ")."); } } if (num3 == 0) { stringBuilder.AppendLine($" no world Items within {num:F0}m of the probe."); } else { stringBuilder.AppendLine($" ({num3} world item(s) within {num:F0}m; species map: " + ((value != null) ? $"{value.ById.Count} by id + {value.ByName.Count} by name" : "none — no nose") + ")"); } stringBuilder.Append(DescribeCharacters(pos, value, flag)); return stringBuilder.ToString(); } private static string DescribeCharacters(Vector3 pos, SenseMap map, bool town) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Invalid comparison between Unknown and I4 //IL_0380: Unknown result type (might be due to invalid IL or missing references) //IL_0385: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return " (no CharacterManager — character census skipped)"; } float num = 60f; if (map != null) { foreach (SenseDef character in map.Characters) { if (character.Radius > num) { num = character.Radius; } } } StringBuilder stringBuilder = new StringBuilder(); int num2 = 0; foreach (Character value in instance.Characters.Values) { if ((Object)(object)value == (Object)null) { continue; } float num3 = Vector3.Distance(((Component)value).transform.position, pos); if (num3 > num) { continue; } num2++; string text = $" char '{value.Name}' (faction {value.Faction}, {num3:F1}m, uid {value.UID})"; if (!value.Alive) { stringBuilder.AppendLine(text + ": dead — REJECT."); continue; } if ((int)value.Faction == 1) { stringBuilder.AppendLine(text + ": player-faction — REJECT."); continue; } SenseDef val = ((map != null) ? MatchCharacter(map, value) : null); if (val == null) { stringBuilder.AppendLine(text + ": no character/faction match for this species — ignored."); } else if (Suppressed(val, town)) { stringBuilder.AppendLine(text + ": sense '" + val.Key + "' but scope '" + val.Scope + "' in a town/city scene — SUPPRESSED."); } else if (num3 > val.Radius) { stringBuilder.AppendLine($"{text}: sense '{val.Key}' but {num3:F1}m > radius {val.Radius:F0} — REJECT."); } else { stringBuilder.AppendLine(text + ": sense '" + val.Key + "' — ACCEPT (" + ((num3 <= val.NearRadius) ? "strong" : "faint") + ")."); } } stringBuilder.AppendLine($" ({num2} roster character(s) within {num:F0}m; species char senses: " + $"{map?.Characters.Count ?? 0})"); if (map != null) { foreach (SenseDef character2 in map.Characters) { foreach (string componentCandidate in character2.ComponentCandidates) { Type type = ResolveComponentType(componentCandidate); if (type == null) { stringBuilder.AppendLine(" component '" + componentCandidate + "': UNRESOLVED type — matcher inert."); continue; } Object[] array = Object.FindObjectsOfType(type); if (array.Length == 0) { stringBuilder.AppendLine(" component '" + componentCandidate + "': no live instances in scene."); continue; } Object[] array2 = array; foreach (Object val2 in array2) { Component val3 = (Component)(object)((val2 is Component) ? val2 : null); if (!((Object)(object)val3 == (Object)null)) { float num4 = Vector3.Distance(val3.transform.position, pos); string text2 = (Suppressed(character2, town) ? ("scope '" + character2.Scope + "' in a town/city scene — SUPPRESSED.") : ((num4 <= character2.Radius) ? ("ACCEPT (" + ((num4 <= character2.NearRadius) ? "strong" : "faint") + ").") : $"{num4:F1}m > radius {character2.Radius:F0} — REJECT.")); stringBuilder.AppendLine($" component '{componentCandidate}' on '{((Object)val3.gameObject).name}' ({num4:F1}m): sense '{character2.Key}' — {text2}"); } } } } } return stringBuilder.ToString().TrimEnd(Array.Empty()); } } internal static class ScentSniffer { private static bool _clearedWhileOff; internal static void Tick(Pet pet, Character player) { //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) //IL_01b4: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EnableScentSense.Value) { if (_clearedWhileOff) { return; } if (pet != null) { ScentTracker scent = pet.Scent; if (scent != null) { scent.Clear(); } } _clearedWhileOff = true; return; } _clearedWhileOff = false; if (pet?.State == null) { return; } PetSenseEntry entry = PetSenseTable.Resolve(pet.SpeciesId); if (entry == null || !ScentSense.HasNose(entry.Species)) { return; } Transform val = (((Object)(object)((Companion)pet).Body != (Object)null) ? ((Component)((Companion)pet).Body).transform : (((Object)(object)player != (Object)null) ? ((Component)player).transform : null)); if (!((Object)(object)val == (Object)null)) { Vector3 position = val.position; List list = ScentSense.HitsAt(position, entry.Species); ScentAlert val2 = pet.Scent.Decide((IReadOnlyList)list, (Func)((string key) => DefFor(entry, key)), (double)Time.time, Plugin.NorthYawOffsetDegrees.Value); if (val2 != null) { Plugin.Log.LogMessage((object)("[SCENT] alert: '" + val2.SenseKey + "' " + ((object)Unsafe.As(ref val2.Strength)/*cast due to .constrained prefix*/).ToString().ToLowerInvariant() + ", " + $"{PetSenses.DirectionLabel(val2.Direction)} ({val2.Distance:F0}m, target {val2.TargetId}).")); ScentContext ctx = new ScentContext { Pet = pet, Sense = DefFor(entry, val2.SenseKey), Alert = val2, TargetPosition = TargetPos(position, list, val2.TargetId) }; SpeciesRegistry.For(pet.SpeciesId).OnScentDetected(in ctx); } } } internal static SenseDef DefFor(PetSenseEntry entry, string senseKey) { if (entry == null || senseKey == null) { return null; } foreach (SenseDef sense in entry.Senses) { if (string.Equals(sense.Key, senseKey, StringComparison.OrdinalIgnoreCase)) { return sense; } } return null; } private static Vector3 TargetPos(Vector3 petPos, List hits, string targetId) { //IL_000b: 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_0011: 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_0020: 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_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: 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_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) foreach (ScentHit hit in hits) { if (hit.TargetId == targetId) { return petPos + new Vector3(hit.Dx, 0f, hit.Dz); } } return petPos; } internal static void ScentDump(Plugin p) { //IL_047d: Unknown result type (might be due to invalid IL or missing references) //IL_0518: Unknown result type (might be due to invalid IL or missing references) //IL_052e: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)$"[SCENT] ── table ({PetSenseTable.Table.Count} species nose(s), EnableScentSense={Plugin.EnableScentSense.Value}) ──"); foreach (PetSenseEntry value in PetSenseTable.Table.Values) { SenseMap senseMap = ScentSense.MapFor(value.Species); foreach (SenseDef sense in value.Senses) { List list = new List(); if (senseMap != null) { foreach (KeyValuePair item in senseMap.ById) { if (item.Value == sense) { list.Add($"ItemID {item.Key}"); } } List list2 = new List(); foreach (KeyValuePair item2 in senseMap.ByName) { if (item2.Value == sense) { list2.Add(item2.Key); } } if (list2.Count > 0) { list.Add("live-name [" + string.Join(", ", list2.ToArray()) + "]"); } if (senseMap.Characters.Contains(sense)) { list.Add("character roster scan"); } } List list3 = new List(); if (sense.ItemId.HasValue) { list3.Add($"{sense.ItemId.Value}"); } list3.AddRange(sense.ItemCandidates); list3.AddRange(sense.CharacterCandidates); foreach (string factionCandidate in sense.FactionCandidates) { list3.Add("faction:" + factionCandidate); } foreach (string componentCandidate in sense.ComponentCandidates) { list3.Add("component:" + componentCandidate); } string text = string.Join(", ", list3.ToArray()); string text2 = (string.Equals(sense.Scope, "any", StringComparison.OrdinalIgnoreCase) ? "" : (", scope " + sense.Scope)); Plugin.Log.LogMessage((object)("[SCENT] '" + value.Species + "/" + sense.Key + "' (" + sense.Kind + "): [" + text + "] → " + ((list.Count > 0) ? string.Join(" + ", list.ToArray()) : "UNARMED (inert)") + ", " + $"radius {sense.Radius:F0}m, cooldown {sense.CooldownSeconds:F0}s, near {sense.NearRadius:F0}m{text2}")); } } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Pet activePet = p.ActivePet; CompanionBody val = ((Companion)(activePet?)).Body; Transform val2 = (((Object)(object)val != (Object)null) ? ((Component)val).transform : (((Object)(object)localPlayerCharacter != (Object)null) ? ((Component)localPlayerCharacter).transform : null)); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogMessage((object)"[SCENT] no pet body and no player — skipping the live sections."); return; } string text3 = ((activePet == null) ? null : PetSenseTable.Resolve(activePet.SpeciesId)?.Species); Plugin.Log.LogMessage((object)("[SCENT] ── raw world items at the " + (((Object)(object)val != (Object)null) ? "PET body" : "PLAYER (no body)") + " (species map: '" + (text3 ?? "none") + "') ──")); Plugin.Log.LogMessage((object)ScentSense.Describe(val2.position, text3)); if (activePet == null) { Plugin.Log.LogMessage((object)"[SCENT] no active pet — no tracker/channel state."); return; } ScentTracker scent = activePet.Scent; ScentAlert val3 = ((scent != null) ? scent.Current : null); if (val3 != null) { SenseDef val4 = DefFor(PetSenseTable.Resolve(activePet.SpeciesId), val3.SenseKey); double num = ((val4 != null) ? activePet.Scent.CooldownRemaining(val3.TargetId, val4.CooldownSeconds, (double)Time.time) : 0.0); Plugin.Log.LogMessage((object)($"[SCENT] tracker: holding '{val3.SenseKey}' ({val3.Strength}, " + $"{PetSenses.DirectionLabel(val3.Direction)}, {val3.Distance:F0}m, target {val3.TargetId}); " + $"re-alert in {num:F0}s")); } else { Plugin.Log.LogMessage((object)"[SCENT] tracker: no scent held."); } Plugin.Log.LogMessage((object)($"[SCENT] channels: point={Plugin.IndicatePoint.Value} ({Plugin.PointSeconds.Value:F0}s), " + $"statusIcon={Plugin.IndicateStatusIcon.Value} (ShowPetStatusIcons={Plugin.ShowPetStatusIcons.Value}), " + $"toast={Plugin.IndicateToast.Value}; " + $"SkipEmptyGatherables={Plugin.SkipEmptyGatherables.Value}, interval {Plugin.SniffIntervalSeconds.Value:F0}s, " + $"next sniff in {p.NextSniffEta:F1}s.")); } } internal static class SelfTest { public static void Run(ManualLogSource log) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Invalid comparison between Unknown and I4 //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Invalid comparison between Unknown and I4 //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Invalid comparison between Unknown and I4 //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Invalid comparison between Unknown and I4 //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Expected O, but got Unknown //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Expected O, but got Unknown //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Invalid comparison between Unknown and I4 //IL_0238: Unknown result type (might be due to invalid IL or missing references) //IL_0251: Unknown result type (might be due to invalid IL or missing references) //IL_0265: Unknown result type (might be due to invalid IL or missing references) //IL_02e7: Unknown result type (might be due to invalid IL or missing references) //IL_02ee: Expected O, but got Unknown //IL_0309: Unknown result type (might be due to invalid IL or missing references) //IL_030f: Invalid comparison between Unknown and I4 //IL_0331: Unknown result type (might be due to invalid IL or missing references) //IL_0337: Invalid comparison between Unknown and I4 //IL_0356: Unknown result type (might be due to invalid IL or missing references) //IL_0378: Unknown result type (might be due to invalid IL or missing references) //IL_037e: Invalid comparison between Unknown and I4 //IL_038d: Unknown result type (might be due to invalid IL or missing references) //IL_0392: Unknown result type (might be due to invalid IL or missing references) //IL_03b2: Unknown result type (might be due to invalid IL or missing references) //IL_03b8: Invalid comparison between Unknown and I4 //IL_03d1: Unknown result type (might be due to invalid IL or missing references) //IL_03d7: Invalid comparison between Unknown and I4 //IL_03f0: Unknown result type (might be due to invalid IL or missing references) //IL_03f6: Invalid comparison between Unknown and I4 //IL_040f: Unknown result type (might be due to invalid IL or missing references) //IL_0415: Invalid comparison between Unknown and I4 SelfTestHarness t = new SelfTestHarness(log); t.Begin("Beastwhispering.Core in-game"); try { Check("loyalty 80 = Devoted", (int)Loyalty.Tier(80) == 4); Check("loyalty 0 = Gone", (int)Loyalty.Tier(0) == 0); Check("feed-bond +20 clamps at 100", Loyalty.Apply(95, (LoyaltyEvent)1, 15) == 100); ComfortBand val = default(ComfortBand); ((ComfortBand)(ref val))..ctor((TempStep)2, (TempStep)5); Check("VeryHot = 2 steps out", Temperature.StepsOutside((TempStep)7, val) == 2); Check("2 steps = Suffering", (int)Temperature.Stage(2) == 2); Check("Devoted+Critical = Unwilling", (int)ResponsivenessRules.Derive((LoyaltyTier)4, (ComfortStage)3) == 3); Dictionary dictionary = PetDiet.Parse("{ \"Pearlbird\": { \"hungerSecondsPerDay\": 600, \"foods\": [ { \"item\": \"cake\", \"kind\": \"Bond\" } ] } }", (Action)null); Check("diet object-form parsed", dictionary.Count == 1 && dictionary["Pearlbird"].Foods.Count == 1); Check("diet per-species hunger + Bond food", dictionary["Pearlbird"].HungerSecondsPerDay == 600.0 && (int)PetDiet.Resolve(dictionary, "Pearlbird")[0].Kind == 2); SaveFile val2 = new SaveFile(); val2.Pets.Add(new PetSave { SpeciesId = "Pearlbird", Name = "Pip", LoyaltyValue = 80, TemperatureStage = (ComfortStage)1 }); SaveFile val3 = SaveCodec.Deserialize(SaveCodec.Serialize(val2, (Action)null), (Action)null); Check("save round-trips", val3.Pets.Count == 1 && val3.Pets[0].LoyaltyValue == 80); Check("save tier derived = Devoted", (int)val3.Pets[0].Tier == 4); Dictionary dictionary2 = FoodHexes.Parse("{ \"Veaber\": { \"meals\": { \"Oil\": \"Scorch\", \"Crab Eye\": \"Curse\" } } }", (Action)null); List list = FoodHexes.ComputeBuildups(FoodHexes.Resolve(dictionary2, "Veaber"), (IReadOnlyList)new List { "Oil", "Crab Eye", "Oil" }, 3, 20f); Check("food-hex Oil,CrabEye,Oil = 40% Scorch + 20% Curse", list.Count == 2 && list[0].HexId == "Scorch" && list[0].Percent == 40f && list[1].Percent == 20f); List list2 = new List(); for (int i = 0; i < 22; i++) { FoodHexes.RecordMeal(list2, "m" + i); } Check("food-hex history caps at " + 20, list2.Count == 20); TemperatureExposure val4 = new TemperatureExposure(10.0, 5.0, (ComfortStage)0); Check("brief dip harmless", (int)val4.Tick(2, 9.0, 1.0) == 0); Check("sustained escalates", (int)val4.Tick(2, 5.0, 1.0) == 1); Check("hunger fires per day", new HungerTimer(100.0, 0.0).Tick(110.0) == 1); Check("permanent death dies", (int)PetRules.OnZeroHealth((PetDeathMode)0) == 0); RegionCrossOutcome val5 = PetRules.OnRegionCross(false, true); Check("region cross w/o breakthrough breaks bond", ((RegionCrossOutcome)(ref val5)).BondBroken); Check("glue routine frame = Write", (int)AnchorGlue.Decide((AnchorGlueMode)2, true, false, true, true, true, false, 0.5f) == 1); Check("glue gap = SafeTeleport", (int)AnchorGlue.Decide((AnchorGlueMode)2, true, false, true, true, true, false, 10f) == 2); Check("glue far-mode agent = AgentWarp", (int)AnchorGlue.Decide((AnchorGlueMode)2, true, false, true, true, true, true, 10f) == 3); Check("glue Off = None", (int)AnchorGlue.Decide((AnchorGlueMode)0, true, true, true, true, true, false, 10f) == 0); float value = default(float); float num = default(float); float num2 = default(float); AnchorGlue.WeldPosition(0f, 0f, 0f, 0f, 1f, 0.3f, ref value, ref num, ref num2); Check("glue weld offsets behind facing", Math.Abs(value) < 0.0001f && Math.Abs(num2 + 0.3f) < 0.0001f); string detail; bool ok = PetStatusEffects.AuditFx(out detail); Check("no status carries inherited donor FX (bug 27) — " + detail, ok); Check("no two mods claim the same key (bug 26)", !Keybinds.HasConflicts()); foreach (string key in PetSigilSetup.CloneIdByKey.Keys) { Check("pet sigil clone '" + key + "' registered+scaled+Ephemeral+collider", PetSigilSetup.CloneHealthy(key)); } Check("pet sigil combo-alias postfix attached (V9)", PetSigilComboAlias.Attached); (string, int, int)[] pairs = PetSigilSetup.Pairs; for (int j = 0; j < pairs.Length; j++) { (string, int, int) tuple = pairs[j]; string item = tuple.Item1; int item2 = tuple.Item2; int item3 = tuple.Item3; List list3 = SigilSense.KeyItemIds(item); Check("pet sigil pair '" + item + "' alias resolves + registry carries both ids", PetSigilSetup.TryCloneForVanilla(item2, out var _) && list3.Contains(item2) && list3.Contains(item3)); } CheckCompanionSettingsPin(Check); } catch (Exception ex) { t.Exception(ex); } t.Done(); void Check(string name, bool flag) { t.Check(name, flag); } } private static void CheckCompanionSettingsPin(Action Check) { //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Invalid comparison between Unknown and I4 //IL_0410: Unknown result type (might be due to invalid IL or missing references) //IL_0416: Unknown result type (might be due to invalid IL or missing references) //IL_0456: Unknown result type (might be due to invalid IL or missing references) //IL_045c: Unknown result type (might be due to invalid IL or missing references) //IL_0480: Unknown result type (might be due to invalid IL or missing references) //IL_0486: Unknown result type (might be due to invalid IL or missing references) BwCompanionSettings bwCompanionSettings = new BwCompanionSettings(); CompanionHost ckHost = Plugin.CkHost; ICompanionSettings val = ((ckHost != null) ? ckHost.Settings : null); Check.Invoke("pin: CritHealthFraction 0.2", bwCompanionSettings.CritHealthFraction == 0.2f); Check.Invoke("pin: CritRearmFraction 0.5", bwCompanionSettings.CritRearmFraction == 0.5f); Check.Invoke("pin: GhostPrefabName NewGhostOneHandedAlly", bwCompanionSettings.GhostPrefabName == "NewGhostOneHandedAlly"); Check.Invoke("pin: AnchorEnabled true", bwCompanionSettings.AnchorEnabled); Check.Invoke("pin: SuppressLeashWarp false", !bwCompanionSettings.SuppressLeashWarp); Check.Invoke("pin: BodilessAnchor DestroyAnchor (the old hand-rolled branch)", (int)bwCompanionSettings.BodilessAnchor == 0); Check.Invoke("pin: LogTagSuffix plain", string.IsNullOrEmpty(bwCompanionSettings.LogTagSuffix) || bwCompanionSettings.LogTagSuffix == null); Check.Invoke("pin: [Combat] defaults 25/1.4/12/2.6/40/12/vocals-on", D(Plugin.AttackDamage, 25f) && D(Plugin.AttackInterval, 1.4f) && D(Plugin.AggroRange, 12f) && D(Plugin.AttackRange, 2.6f) && D(Plugin.CombatLeashDistance, 40f) && D(Plugin.DisengageRunHomeSeconds, 12f) && D(Plugin.PetAttackVocals, expected: true)); Check.Invoke("pin: [Anchor] defaults inv/nobar/link/hideicon/20/60/nodmg/voice/Always/0.3/unify/PassPlayer", D(Plugin.AnchorInvisible, expected: true) && D(Plugin.AnchorShowHealthBar, expected: false) && D(Plugin.AnchorLinkSummonSlot, expected: true) && D(Plugin.HideSummonIcon, expected: true) && D(Plugin.AnchorLeashDistance, 20f) && D(Plugin.AnchorRespawnSeconds, 60f) && D(Plugin.AnchorDealsDamage, expected: false) && D(Plugin.AnchorSpeciesVoice, expected: true) && D(Plugin.GlueMode, (AnchorGlueMode)2) && D(Plugin.GlueOffsetBehind, 0.3f) && D(Plugin.UnifyTargets, expected: true) && D(Plugin.AnchorPlayerCollision, (AnchorCollisionMode)1)); Check.Invoke("pin: [Pet]/[Follow] defaults yaw180/loaf 2-4-3", D(Plugin.ModelYawOffset, 180f) && D(Plugin.LoafDistanceMin, 2f) && D(Plugin.LoafDistanceMax, 4f) && D(Plugin.LoafDistanceRepick, 3f)); Check.Invoke("pin: pass-throughs read the LIVE entries", bwCompanionSettings.AttackDamage == Plugin.AttackDamage.Value && bwCompanionSettings.CombatLeashDistance == Plugin.CombatLeashDistance.Value && bwCompanionSettings.AttackVocals == Plugin.PetAttackVocals.Value && bwCompanionSettings.ModelYawOffset == Plugin.ModelYawOffset.Value && bwCompanionSettings.AnchorLinkSummonSlot == Plugin.AnchorLinkSummonSlot.Value); Check.Invoke("pin: CkHost exists with plain tags + PETCOMBAT noun", ckHost != null && ckHost.TagSuffix == "" && ckHost.CombatTag == "PETCOMBAT"); if (val != null) { bool flag = val.AttackDamage == bwCompanionSettings.AttackDamage && val.AttackInterval == bwCompanionSettings.AttackInterval && val.AggroRange == bwCompanionSettings.AggroRange && val.AttackRange == bwCompanionSettings.AttackRange && val.CombatLeashDistance == bwCompanionSettings.CombatLeashDistance && val.DisengageRunHomeSeconds == bwCompanionSettings.DisengageRunHomeSeconds && val.AttackVocals == bwCompanionSettings.AttackVocals && val.AnchorInvisible == bwCompanionSettings.AnchorInvisible && val.AnchorShowHealthBar == bwCompanionSettings.AnchorShowHealthBar && val.AnchorLinkSummonSlot == bwCompanionSettings.AnchorLinkSummonSlot && val.AnchorHideSummonIcon == bwCompanionSettings.AnchorHideSummonIcon && val.AnchorLeashDistance == bwCompanionSettings.AnchorLeashDistance && val.AnchorRespawnSeconds == bwCompanionSettings.AnchorRespawnSeconds && val.AnchorDealsDamage == bwCompanionSettings.AnchorDealsDamage && val.CritHealthFraction == bwCompanionSettings.CritHealthFraction && val.CritRearmFraction == bwCompanionSettings.CritRearmFraction && val.SpeciesVoice == bwCompanionSettings.SpeciesVoice && val.GlueMode == bwCompanionSettings.GlueMode && val.GlueOffsetBehind == bwCompanionSettings.GlueOffsetBehind && val.UnifyTargets == bwCompanionSettings.UnifyTargets && val.GhostPrefabName == bwCompanionSettings.GhostPrefabName && val.AnchorPlayerCollision == bwCompanionSettings.AnchorPlayerCollision && val.AnchorEnabled == bwCompanionSettings.AnchorEnabled && val.SuppressLeashWarp == bwCompanionSettings.SuppressLeashWarp && val.BodilessAnchor == bwCompanionSettings.BodilessAnchor && val.ModelYawOffset == bwCompanionSettings.ModelYawOffset && val.LoafDistanceMin == bwCompanionSettings.LoafDistanceMin && val.LoafDistanceMax == bwCompanionSettings.LoafDistanceMax && val.LoafRepickDistance == bwCompanionSettings.LoafRepickDistance && string.IsNullOrEmpty(val.LogTagSuffix); Check.Invoke("pin: host wrapper forwards all 30 knobs unchanged", flag); } static bool D(ConfigEntry e, TVal expected) { if (e != null) { return object.Equals(((ConfigEntryBase)e).DefaultValue, expected); } return false; } } } internal static class SigilSense { private static readonly Vector3 ProbeOffset = new Vector3(0f, -0.5f, 0f); private static readonly Collider[] _hits = (Collider[])(object)new Collider[16]; private static readonly Dictionary _byItemId = new Dictionary(); private static readonly Dictionary _primaryByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); private static float _maxRadius = 1f; private static bool _closestPointWarned; private static bool _truncationWarned; internal static readonly List None = new List(); internal static IReadOnlyDictionary Registry => _byItemId; private static void WarnIfTruncated(int n, string where) { if (n >= _hits.Length && !_truncationWarned) { _truncationWarned = true; Plugin.Log.LogWarning((object)($"[SIGIL] {where}: the overlap buffer FILLED ({n}/{_hits.Length} colliders within " + $"{_maxRadius:F1}m) — hits beyond it were dropped, so an active sigil may be missed here and in " + "`sigildump`. Warned once per session.")); } } internal static bool TryKeyToItemId(string key, out int itemId) { if (_primaryByKey.TryGetValue(key ?? "", out itemId)) { return true; } itemId = 0; return false; } internal static List KeyItemIds(string key) { List list = new List(2); if (TryKeyToItemId(key, out var itemId)) { list.Add(itemId); } foreach (KeyValuePair item in _byItemId) { if (item.Key != itemId && string.Equals(item.Value.Key, key, StringComparison.OrdinalIgnoreCase)) { list.Add(item.Key); } } return list; } internal static void Rebuild(Dictionary byItemId, Dictionary primaryByKey) { _byItemId.Clear(); _primaryByKey.Clear(); _maxRadius = 1f; if (byItemId != null) { foreach (KeyValuePair item in byItemId) { _byItemId[item.Key] = item.Value; if (item.Value.Radius > _maxRadius) { _maxRadius = item.Value.Radius; } } } if (primaryByKey == null) { return; } foreach (KeyValuePair item2 in primaryByKey) { _primaryByKey[item2.Key] = item2.Value; } } internal static List ActiveKeysAt(Vector3 pos) { //IL_0012: 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_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) if (_byItemId.Count == 0) { return None; } Vector3 val = pos + ProbeOffset; int num = Physics.OverlapSphereNonAlloc(val, _maxRadius, _hits, Global.WorldItemsMask); WarnIfTruncated(num, "detect"); List list = null; for (int i = 0; i < num; i++) { Collider val2 = _hits[i]; _hits[i] = null; if ((Object)(object)val2 == (Object)null) { continue; } Item componentInParent = ((Component)val2).GetComponentInParent(); if (!((Object)(object)componentInParent == (Object)null) && componentInParent.IsInWorld && _byItemId.TryGetValue(componentInParent.ItemID, out var value) && WithinRadius(val2, val, value.Radius, out var _)) { if (list == null) { list = new List(2); } if (!list.Contains(value.Key)) { list.Add(value.Key); } } } if (list != null) { Plugin.Log.LogMessage((object)("[SIGIL] detect: standing in [" + string.Join(", ", list.ToArray()) + "].")); } return list ?? None; } internal static string Describe(Vector3 pos) { //IL_0012: 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_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) if (_byItemId.Count == 0) { return " (no sigil ItemIDs resolved — detection is inert; see the registry section)"; } Vector3 val = pos + ProbeOffset; int num = Physics.OverlapSphereNonAlloc(val, _maxRadius, _hits, Global.WorldItemsMask); if (num == 0) { return $" no WorldItems-layer colliders within {_maxRadius:F1}m of the probe — standing in nothing."; } WarnIfTruncated(num, "sigildump"); StringBuilder stringBuilder = new StringBuilder(); if (num >= _hits.Length) { stringBuilder.AppendLine($" ⚠ overlap buffer FULL ({num}/{_hits.Length}) — this listing is TRUNCATED; " + "an active sigil may be missing from it."); } List list = new List(); for (int i = 0; i < num; i++) { Collider val2 = _hits[i]; _hits[i] = null; if ((Object)(object)val2 == (Object)null) { continue; } Item componentInParent = ((Component)val2).GetComponentInParent(); if ((Object)(object)componentInParent == (Object)null) { stringBuilder.AppendLine(" '" + ((Object)val2).name + "': no Item on the collider's parents — REJECT."); continue; } string text = $" '{componentInParent.Name}' (id={componentInParent.ItemID}, prefab '{((Object)val2).name}')"; if (!componentInParent.IsInWorld) { stringBuilder.AppendLine(text + ": not IsInWorld — REJECT."); continue; } if (!_byItemId.TryGetValue(componentInParent.ItemID, out var value)) { stringBuilder.AppendLine(text + ": ItemID not in the sigil registry — REJECT."); continue; } string how; bool flag = WithinRadius(val2, val, value.Radius, out how); stringBuilder.AppendLine(string.Format("{0}: registered '{1}', {2} vs radius {3:F1} — {4}.", text, value.Key, how, value.Radius, flag ? "ACCEPT" : "REJECT")); if (flag && !list.Contains(value.Key)) { list.Add(value.Key); } } stringBuilder.Append(" → active: [" + ((list.Count > 0) ? string.Join(", ", list.ToArray()) : "none") + "]"); return stringBuilder.ToString(); } private static bool WithinRadius(Collider col, Vector3 probe, float radius, out string how) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (radius >= _maxRadius) { how = "overlap"; return true; } try { float num = Vector3.Distance(col.ClosestPoint(probe), probe); how = $"closest-point {num:F2}m"; return num <= radius; } catch (Exception) { if (!_closestPointWarned) { _closestPointWarned = true; Plugin.Log.LogWarning((object)("[SIGIL] ClosestPoint failed on '" + ((Object)col).name + "' (non-convex collider?) — accepting on overlap instead. (Warned once.)")); } how = "overlap (closest-point unavailable)"; return true; } } internal static void SigilDump(Plugin p) { //IL_03ac: Unknown result type (might be due to invalid IL or missing references) //IL_03f1: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_0249: Unknown result type (might be due to invalid IL or missing references) //IL_0518: Unknown result type (might be due to invalid IL or missing references) //IL_05b3: Unknown result type (might be due to invalid IL or missing references) //IL_05b8: Unknown result type (might be due to invalid IL or missing references) //IL_05c4: Unknown result type (might be due to invalid IL or missing references) //IL_05d0: Unknown result type (might be due to invalid IL or missing references) //IL_05d7: Unknown result type (might be due to invalid IL or missing references) //IL_05ed: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)$"[SIGIL] ── registry ({SigilTable.Table.Sigils.Count} sigil(s), EnableSigilSynergies={Plugin.EnableSigilSynergies.Value}) ──"); foreach (SigilDef value in SigilTable.Table.Sigils.Values) { List list = KeyItemIds(value.Key); string text = ((list.Count > 0) ? string.Format("ItemID(s) [{0}] (primary {1})", string.Join(", ", list.ConvertAll((int i) => i.ToString()).ToArray()), list[0]) : "UNRESOLVED (inert)"); Plugin.Log.LogMessage((object)string.Format("[SIGIL] '{0}': [{1}] → {2}, radius {3:F1}", value.Key, string.Join(", ", value.ItemCandidates.ToArray()), text, value.Radius)); } Plugin.Log.LogMessage((object)$"[SIGIL] ── pet sigil clones (EnablePetSigils={Plugin.EnablePetSigils.Value}, scale {Plugin.PetSigilScale.Value:F2}) ──"); foreach (string key in PetSigilSetup.CloneIdByKey.Keys) { Plugin.Log.LogMessage((object)("[SIGIL] " + PetSigilSetup.DescribeClone(key))); } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter == (Object)null) { Plugin.Log.LogMessage((object)"[SIGIL] no local player — skipping the world scan + detection sections."); return; } Plugin.Log.LogMessage((object)$"[SIGIL] ── world items within {25f:F0}m (a live sigil's ItemID reads here) ──"); int num = 0; if ((Object)(object)ItemManager.Instance != (Object)null) { foreach (Item value2 in ItemManager.Instance.WorldItems.Values) { if (!((Object)(object)value2 == (Object)null)) { float num2 = Vector3.Distance(((Component)value2).transform.position, ((Component)localPlayerCharacter).transform.position); if (!(num2 > 25f)) { num++; Ephemeral componentInChildren = ((Component)value2).GetComponentInChildren(); string text2 = (((Object)(object)componentInChildren == (Object)null) ? "" : (", ephemeral " + ((componentInChildren.m_remainingTime <= -999f) ? $"lifespan {componentInChildren.LifeSpan:F0}s (not ticking yet)" : $"{componentInChildren.m_remainingTime:F0}s left") + " caster='" + (((Object)(object)componentInChildren.SourceCharacter != (Object)null) ? componentInChildren.SourceCharacter.Name : "?") + "'")); Plugin.Log.LogMessage((object)$"[SIGIL] '{value2.Name}' id={value2.ItemID} prefab='{((Object)((Component)value2).gameObject).name}' {num2:F1}m{text2}"); } } } } Plugin.Log.LogMessage((object)$"[SIGIL] ({num} world item(s) in range)"); Plugin.Log.LogMessage((object)"[SIGIL] ── detection at the PLAYER ──"); Plugin.Log.LogMessage((object)Describe(((Component)localPlayerCharacter).transform.position)); CompanionBody val = ((Companion)(p.ActivePet?)).Body; if ((Object)(object)val != (Object)null) { Plugin.Log.LogMessage((object)"[SIGIL] ── detection at the PET body ──"); Plugin.Log.LogMessage((object)Describe(((Component)val).transform.position)); } else { Plugin.Log.LogMessage((object)"[SIGIL] no active pet body — no pet-position detection."); } Pet activePet = p.ActivePet; if (activePet?.State == null) { Plugin.Log.LogMessage((object)"[SIGIL] no active pet — no next-hit preview."); return; } SigilSpeciesEntry val2 = SigilTable.Resolve(activePet.SpeciesId); Plugin.Log.LogMessage((object)("[SIGIL] ── next special hit for '" + activePet.SpeciesId + "' (sigil entry: " + ((val2 != null) ? string.Join(", ", new List(val2.BySigil.Keys).ToArray()) : "NONE — no sigil signature") + ") ──")); FoodHexEntry val3 = (Plugin.EnableFoodHexes.Value ? FoodHexTable.Resolve(activePet.SpeciesId) : null); List list2 = ((val3 != null) ? FoodHexes.ComputeBuildups(val3, (IReadOnlyList)activePet.State.RecentMeals, Plugin.HexMealWindow.Value, Plugin.HexBuildupPercent.Value) : new List()); List list3 = (((Object)(object)val != (Object)null && Plugin.EnableSigilSynergies.Value) ? ActiveKeysAt(((Component)val).transform.position) : None); List list4 = SigilSynergies.ActiveSynergies(val2, (IReadOnlyList)list3, "specialHit"); Plugin.Log.LogMessage((object)string.Format("[SIGIL] food hexes: {0}, pet stands in: [{1}], firing synergies: {2}", list2.Count, (list3.Count > 0) ? string.Join(", ", list3.ToArray()) : "nothing", list4.Count)); List list5 = SigilSynergies.ComposeBuildups(list2, (IReadOnlyList)list4); if (list5.Count == 0) { Plugin.Log.LogMessage((object)"[SIGIL] next hit builds up NOTHING (no mapped meals, no active synergy)."); return; } foreach (HexBuildUp item in list5) { Plugin.Log.LogMessage((object)string.Format("[SIGIL] next hit: +{0:F0}% '{1}' ({2})", item.Percent, item.HexId, (item.MealCount > 0) ? $"{item.MealCount} meal(s)" : "sigil")); } } internal static void SigilSpawn(Character player, string[] parts) { //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) string text = Plugin.Tail(parts)?.Trim(); if (string.IsNullOrEmpty(text)) { List list = new List(); foreach (SigilDef value in SigilTable.Table.Sigils.Values) { list.Add(value.Key); } Plugin.Log.LogWarning((object)("[SIGIL] usage: sigilspawn — registered keys: " + string.Join(", ", list.ToArray()) + ".")); return; } if (!int.TryParse(text, out var result) && !TryKeyToItemId(text, out result)) { Plugin.Log.LogWarning((object)("[SIGIL] '" + text + "' is not a resolved sigil key — see `sigildump` (or pass a numeric ItemID directly).")); return; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(result) : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)$"[SIGIL] ItemID {result} is not a known item prefab."); return; } if (val is Skill) { string arg = ((result == 8200033) ? " — use the circle 'Activated Bloodstone' (8000011) instead" : ""); Plugin.Log.LogWarning((object)$"[SIGIL] '{val.Name}' resolved to skill ItemID {result}, not the circle world item; refusing to spawn (it would be invisible and false-positive the detector){arg}."); return; } Item val2 = Object.Instantiate(val); ((Component)val2).gameObject.SetActive(true); ((Component)val2).transform.position = GroundSnap(((Component)player).transform.position + ((Component)player).transform.forward * 1.5f); val2.ForceUpdateParentChange(); Ephemeral componentInChildren = ((Component)val2).GetComponentInChildren(); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.SetSourceCharacter(player); } Plugin.Log.LogMessage((object)($"[SIGIL] spawned '{val2.Name}' (id={result}) at your feet" + (((Object)(object)componentInChildren != (Object)null) ? $", lifespan {componentInChildren.LifeSpan:F0}s game-time" : ", NO Ephemeral (never expires — is this really a sigil?)") + " — step in and run `sigildump`.")); } internal static Vector3 GroundSnap(Vector3 at) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: 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_0015: Unknown result type (might be due to invalid IL or missing references) //IL_002f: 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) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) RaycastHit val = default(RaycastHit); if (!Physics.Raycast(at + Vector3.up * 2f, Vector3.down, ref val, 8f, Global.LargeEnvironmentMask)) { return at; } return new Vector3(at.x, ((RaycastHit)(ref val)).point.y, at.z); } } internal static class SigilTable { private const string FileName = "Sigils.json"; private const string Tag = "[SIGIL]"; private static readonly DataAxis _axis = BwAxis.Composite(Load, delegate(SigilSynergyTable t) { Plugin.Log.LogMessage((object)string.Format("{0} reloaded {1} ({2} sigil(s), {3} species entr{4}).", "[SIGIL]", "Sigils.json", t.Sigils.Count, t.Species.Count, (t.Species.Count == 1) ? "y" : "ies")); }, "[SIGIL]", Validate); internal static SigilSynergyTable Table => _axis.Table; internal static SigilSpeciesEntry Resolve(string speciesId) { return SigilSynergies.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static SigilSynergyTable Load() { Action action = delegate(string m) { Plugin.Log.LogWarning((object)("[SIGIL] Sigils.json: " + m)); }; SigilSynergyTable val = SigilSynergies.Parse(EmbeddedRes.Text("Sigils.json", "[SIGIL]"), action); Plugin.Log.LogMessage((object)string.Format("{0} loaded {1} built-in sigil(s), {2} built-in species entr{3}.", "[SIGIL]", val.Sigils.Count, val.Species.Count, (val.Species.Count == 1) ? "y" : "ies")); try { string path = Path.Combine(Paths.ConfigPath, "Sigils.json"); if (File.Exists(path)) { SigilSynergyTable val2 = SigilSynergies.Parse(File.ReadAllText(path), action); val = SigilSynergies.Merge(val, val2); Plugin.Log.LogMessage((object)string.Format("{0} merged config override ({1} sigil(s) replaced per-key, {2} species entr{3} replaced wholesale).", "[SIGIL]", val2.Sigils.Count, val2.Species.Count, (val2.Species.Count == 1) ? "y" : "ies")); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SIGIL] Sigils.json override load failed: " + ex.Message)); } SigilSynergies.Audit(val, action); return val; } private static void Validate() { Dictionary dictionary = new Dictionary(); Dictionary dictionary2 = new Dictionary(StringComparer.OrdinalIgnoreCase); int num = 0; foreach (SigilDef value2 in Table.Sigils.Values) { string via; List list = ResolveItems(value2, out via); if (list.Count > 0) { num++; bool flag = false; foreach (int item in list) { if (dictionary.TryGetValue(item, out var value)) { Plugin.Log.LogWarning((object)string.Format("{0} '{1}' resolved to ItemID {2}, already claimed by '{3}' — that id stays '{4}''s (fix {5}).", "[SIGIL]", value2.Key, item, value.Key, value.Key, "Sigils.json")); } else { dictionary[item] = value2; if (!flag) { dictionary2[value2.Key] = item; flag = true; } } } if (!flag) { Plugin.Log.LogWarning((object)("[SIGIL] '" + value2.Key + "': every resolved id was claimed by another key — '" + value2.Key + "' is INERT (fix Sigils.json).")); continue; } Plugin.Log.LogMessage((object)(string.Format("{0} '{1}' resolved via {2} → ItemID(s) [{3}], primary {4}", "[SIGIL]", value2.Key, via, string.Join(", ", list.ConvertAll((int i) => i.ToString()).ToArray()), dictionary2[value2.Key]) + (value2.ItemId.HasValue ? "." : " (put the number in Sigils.json to make it locale-proof)."))); } else { Plugin.Log.LogWarning((object)("[SIGIL] '" + value2.Key + "': no 'item' candidate resolved [" + string.Join(", ", value2.ItemCandidates.ToArray()) + "] — detection for this sigil is INERT. Cast/spawn one and run `sigildump` to discover the real ItemID, then fix the config override and `reloadsigils`.")); } } int num2 = 0; int misses = 0; HashSet checkedNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (SigilSpeciesEntry value3 in Table.Species.Values) { foreach (SigilSynergyDef value4 in value3.BySigil.Values) { num2++; foreach (SigilHex hex in value4.Hexes) { TableValidator.CheckHexName("[SIGIL]", "Sigils.json", "reloadsigils", checkedNames, hex.HexId, "'" + value3.Species + "." + value4.SigilKey + "'", ref misses); } } } SigilSense.Rebuild(dictionary, dictionary2); Plugin.Log.LogMessage((object)(string.Format("{0} boot check: {1} sigil(s) ({2} resolved), ", "[SIGIL]", Table.Sigils.Count, num) + string.Format("{0} species entr{1}, {2} synerg{3}, {4} unknown hex name(s).", Table.Species.Count, (Table.Species.Count == 1) ? "y" : "ies", num2, (num2 == 1) ? "y" : "ies", misses))); } private static List ResolveItems(SigilDef def, out string via) { List list = new List(2); via = null; foreach (string itemCandidate in def.ItemCandidates) { if (!int.TryParse(itemCandidate, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { continue; } if ((Object)(object)ResourcesPrefabManager.Instance.GetItemPrefab(result) == (Object)null) { Plugin.Log.LogWarning((object)string.Format("{0} '{1}': ItemID {2} is not a known item — candidate skipped.", "[SIGIL]", def.Key, result)); continue; } if (!list.Contains(result)) { list.Add(result); } if (via == null) { via = "ItemID '" + itemCandidate + "'"; } } if (list.Count > 0) { return list; } foreach (string itemCandidate2 in def.ItemCandidates) { if (!int.TryParse(itemCandidate2, NumberStyles.Integer, CultureInfo.InvariantCulture, out var _) && ItemNameIndex.TryResolveCatalog(itemCandidate2, out var itemId, out via)) { list.Add(itemId); break; } } return list; } } internal static class SkillCooldowns { internal static int Sync(int itemId, Character player, float want) { return SkillCooldowns.Sync(itemId, player, want); } internal static void RefundNextFrame(int itemId, Character player, string tag, string reason) { SkillCooldowns.RefundNextFrame(itemId, player, tag, reason); } } [HarmonyPatch(typeof(Skill), "SkillStarted")] internal static class SkillEchoCast { internal static bool Attached; [HarmonyPostfix] private static void Postfix(Skill __instance) { try { if (!((Object)(object)__instance == (Object)null) && SkillEchoes.IsRosterSkill(((Item)__instance).ItemID) && Plugin.EnableSkillEcho.Value) { Character ownerCharacter = ((EffectSynchronizer)__instance).OwnerCharacter; if (!((Object)(object)ownerCharacter == (Object)null) && !((Object)(object)ownerCharacter != (Object)(object)Plugin.LocalPlayerCharacter)) { SkillEchoDriver.OnPlayerCast(((Item)__instance).ItemID, ownerCharacter); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ECHO] cast hook threw (echo skipped, the cast itself is unaffected): " + ex.Message)); } } } internal static class SkillEchoCue { private static readonly string[] Fallbacks = new string[3] { "Rage", "Rage Amplified", "Enrage" }; private static string _cacheKey; private static StatusEffect _cached; private static string _cachedName; private static bool _resolveWarned; private static bool _petRendererWarned; private static bool _playWarned; internal static string Play(CompanionBody body, Character player) { //IL_0083: 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) float value = Plugin.EchoCueSeconds.Value; if (value <= 0f) { return "off (CueSeconds=0)"; } string name; StatusEffect val = Resolve(out name); if ((Object)(object)val == (Object)null) { return "no FX resolved (see the [ECHO] cue warning)"; } if ((Object)(object)player == (Object)null) { return "no player to bind to"; } bool flag = Plugin.EchoCueOnPet.Value && (Object)(object)body != (Object)null; Transform val2 = (flag ? ((Component)body).transform : ((Component)player).transform); try { Transform val3 = Object.Instantiate(val.FXPrefab); val3.SetParent(val2, false); val3.localPosition = val.FxOffset; val3.localRotation = Quaternion.identity; ((Component)val3).gameObject.SetActive(true); VFXSystem[] componentsInChildren = ((Component)val3).GetComponentsInChildren(true); foreach (VFXSystem val4 in componentsInChildren) { val4.Play(player, (Character)null); } if (flag) { SkinnedMeshRenderer componentInChildren = ((Component)body).GetComponentInChildren(); if ((Object)(object)componentInChildren != (Object)null) { Rebind(val3, componentInChildren); } else if (!_petRendererWarned) { _petRendererWarned = true; Plugin.Log.LogWarning((object)"[ECHO] cue: the pet body has no SkinnedMeshRenderer (ghost stand-in?) — the cue plays on the player instead. (Warned once.)"); } } Object.Destroy((Object)(object)((Component)val3).gameObject, value); return string.Format("'{0}' fx '{1}' on {2} for {3:F1}s", name, ((Object)val.FXPrefab).name, flag ? "pet" : "player", value); } catch (Exception ex) { if (!_playWarned) { _playWarned = true; Plugin.Log.LogWarning((object)$"[ECHO] cue threw (echo unaffected, won't retry-log): {ex}"); } return "threw: " + ex.Message; } } private static void Rebind(Transform fx, SkinnedMeshRenderer smr) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) VFXParticlesOnRenderer[] componentsInChildren = ((Component)fx).GetComponentsInChildren(true); foreach (VFXParticlesOnRenderer val in componentsInChildren) { ParticleSystem component = ((Component)val).GetComponent(); if (!((Object)(object)component == (Object)null)) { ShapeModule shape = component.shape; ((ShapeModule)(ref shape)).shapeType = (ParticleSystemShapeType)14; ((ShapeModule)(ref shape)).skinnedMeshRenderer = smr; val.m_targetRenderer = (Renderer)(object)smr; } } } private static StatusEffect Resolve(out string name) { //IL_00f1: Unknown result type (might be due to invalid IL or missing references) string text = Plugin.EchoCueStatusName.Value?.Trim() ?? ""; if (text == _cacheKey && (Object)(object)_cached != (Object)null) { name = _cachedName; return _cached; } name = null; if (text.Length == 0) { return null; } if (ResourcesPrefabManager.Instance == null) { return null; } foreach (string item in Candidates(text)) { StatusEffect statusEffectPrefab = ResourcesPrefabManager.Instance.GetStatusEffectPrefab(item); if (!((Object)(object)statusEffectPrefab == (Object)null) && !((Object)(object)statusEffectPrefab.FXPrefab == (Object)null)) { _cacheKey = text; _cached = statusEffectPrefab; _cachedName = (name = item); Plugin.Log.LogMessage((object)("[ECHO] cue resolved: status '" + item + "' fx '" + ((Object)statusEffectPrefab.FXPrefab).name + "' " + $"(FxInstantiation={statusEffectPrefab.FxInstantiation}). Record the winner in [SkillEcho] CueStatusName.")); return statusEffectPrefab; } } if (!_resolveWarned) { _resolveWarned = true; Plugin.Log.LogWarning((object)("[ECHO] cue: no candidate ('" + text + "', " + string.Join(", ", Fallbacks) + ") resolves to a status with an FX prefab — no cue (echo unaffected). Run `echodump` for the audit; record the real name in [SkillEcho] CueStatusName + reloadcfg. (Warned once.)")); } return null; } private static IEnumerable Candidates(string cfg) { yield return cfg; string[] fallbacks = Fallbacks; foreach (string text in fallbacks) { if (!string.Equals(text, cfg, StringComparison.OrdinalIgnoreCase)) { yield return text; } } } internal static string Audit() { //IL_0119: 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) StringBuilder stringBuilder = new StringBuilder("[ECHO] cue audit (candidates → live prefab):\n"); if (ResourcesPrefabManager.Instance == null) { return stringBuilder.Append(" (prefab manager not ready)").ToString(); } foreach (string item in Candidates(Plugin.EchoCueStatusName.Value?.Trim() ?? "")) { StatusEffect statusEffectPrefab = ResourcesPrefabManager.Instance.GetStatusEffectPrefab(item); if ((Object)(object)statusEffectPrefab == (Object)null) { stringBuilder.Append(" '" + item + "': NO status prefab\n"); continue; } if ((Object)(object)statusEffectPrefab.FXPrefab == (Object)null) { stringBuilder.Append($" '{item}': status ok, NO FXPrefab (FxInstantiation={statusEffectPrefab.FxInstantiation}, " + "specialFx=" + (((Object)(object)statusEffectPrefab.SpecialFXPrefab != (Object)null) ? ((Object)statusEffectPrefab.SpecialFXPrefab).name : "none") + ")\n"); continue; } int num = ((Component)statusEffectPrefab.FXPrefab).GetComponentsInChildren(true).Length; int num2 = ((Component)statusEffectPrefab.FXPrefab).GetComponentsInChildren(true).Length; stringBuilder.Append($" '{item}': fx '{((Object)statusEffectPrefab.FXPrefab).name}' (FxInstantiation={statusEffectPrefab.FxInstantiation}, " + $"{num} VFXSystem(s), {num2} on-renderer emitter(s))\n"); } stringBuilder.Append($" in force: CueStatusName='{Plugin.EchoCueStatusName.Value}', CueSeconds={Plugin.EchoCueSeconds.Value:F1}, " + $"CueOnPet={Plugin.EchoCueOnPet.Value}"); return stringBuilder.ToString(); } } internal static class SkillEchoDriver { internal sealed class FireRecord { public float Time; public string Skill; public string Outcome; public string Detail; } internal static FireRecord LastFire; private static float _nextReady; private static float _nextPassiveLog; private static bool _animWarned; internal static void OnPlayerCast(int skillItemId, Character player, bool devBypassCooldown = false) { //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) string text = SkillEchoes.NameOf(skillItemId) ?? skillItemId.ToString(); Pet pet = Plugin.Instance?.ActivePet; CompanionBody val = ((Companion)(pet?)).Body; CompanionCombat val2 = ((pet != null) ? ((Companion)pet).Combat : null); if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null) { return; } if (((Companion)pet).Stance.Passive && !Plugin.EchoWhilePassive.Value) { if (Time.time >= _nextPassiveLog) { _nextPassiveLog = Time.time + 10f; Skip(text, "pet is in Disengage stance (EchoWhilePassive=false; throttled 10s)"); } return; } if (!devBypassCooldown && Time.time < _nextReady) { Skip(text, $"echo cooldown, {_nextReady - Time.time:F1}s left"); return; } Character val3 = val2.CurrentTarget; string how = "the pet's combat target"; if ((Object)(object)val3 == (Object)null || !val3.Alive) { val3 = PetCommands.FindEngageTarget(player, out how); } if ((Object)(object)val3 == (Object)null || !val3.Alive) { Skip(text, "nothing to strike (no combat target, no locked target, nothing in the front cone)"); return; } float num = Vector3.Distance(((Component)val).transform.position, ((Component)val3).transform.position); if (num > Plugin.EchoRangeMeters.Value) { Skip(text, $"'{val3.Name}' is {num:F0}m from the pet (EchoRangeMeters={Plugin.EchoRangeMeters.Value:F0})"); return; } SkillEchoDef val4 = SkillEchoes.ForSkill(SkillEchoTable.Resolve(pet.SpeciesId), text); float num2 = val2.BaseDamage * (float)SkillEchoes.EffectiveDamageMult(val4, (double)Plugin.EchoDamageMult.Value); float num3 = val2.AttackImpact * (float)SkillEchoes.EffectiveImpactMult(val4, (double)Plugin.EchoImpactMult.Value); float num4 = Mathf.Max(0f, Plugin.EchoWindupSeconds.Value); _nextReady = Time.time + Mathf.Max(0f, Plugin.EchoCooldownSeconds.Value); TriggerAttackAnim(val); val2.PlayVocal(0); string arg = SkillEchoCue.Play(val, player); string text2 = $"at '{val3.Name}' ({how}) — dmg {num2:F0} (base {val2.BaseDamage:F0} " + $"x{SkillEchoes.EffectiveDamageMult(val4, (double)Plugin.EchoDamageMult.Value):F2}), impact {num3:F1} " + $"(x{SkillEchoes.EffectiveImpactMult(val4, (double)Plugin.EchoImpactMult.Value):F2}), " + string.Format("status={0}, windup {1:F2}s, cue: {2}", val4?.StatusName ?? "none", num4, arg); Plugin.Log.LogMessage((object)("[ECHO] '" + text + "' -> '" + pet.SpeciesId + "' echoes " + text2 + ".")); LastFire = new FireRecord { Time = Time.time, Skill = text, Outcome = "fired", Detail = text2 }; ((MonoBehaviour)Plugin.Instance).StartCoroutine(ResolveAfterWindup(pet, val2, val3, val4, num2, num3, num4)); } private static void Skip(string skillName, string reason) { Plugin.Log.LogMessage((object)("[ECHO] '" + skillName + "' -> SKIPPED: " + reason + ".")); LastFire = new FireRecord { Time = Time.time, Skill = skillName, Outcome = "skipped: " + reason }; } private static void TriggerAttackAnim(CompanionBody body) { Animator component = ((Component)body).GetComponent(); if ((Object)(object)component != (Object)null) { AnimatorControllerParameter[] parameters = component.parameters; foreach (AnimatorControllerParameter val in parameters) { if (val.name == "Attack1") { component.SetTrigger("Attack1"); return; } } } if (!_animWarned) { _animWarned = true; Plugin.Log.LogWarning((object)"[ECHO] this body's animator has no 'Attack1' trigger — echoes deal their damage without a visible swing. (Warned once.)"); } } private static IEnumerator ResolveAfterWindup(Pet pet, CompanionCombat combat, Character target, SkillEchoDef def, float dmg, float impact, float windup) { yield return (object)new WaitForSeconds(windup); if ((Object)(object)target == (Object)null || !target.Alive) { Plugin.Log.LogMessage((object)"[ECHO] target died or vanished during the windup — no hit landed."); yield break; } if ((Object)(object)combat == (Object)null) { Plugin.Log.LogMessage((object)"[ECHO] pet lost its CompanionCombat mid-windup (body swap?) — no hit landed."); yield break; } Vector3 val = ((Component)target).transform.position - ((Component)combat).transform.position; Vector3 normalized = ((Vector3)(ref val)).normalized; combat.DealDamage(target, dmg, normalized, impact); Plugin.Log.LogMessage((object)$"[ECHO] hit '{target.Name}' for {dmg:F0} (impact {impact:F1})."); if (def?.StatusName == null) { yield break; } object obj; if (pet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)pet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val2 = (Character)obj; if (def.BuildupPercent.HasValue) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; if ((Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(def.StatusName) : null) == (Object)null) { Plugin.Log.LogWarning((object)("[ECHO] status '" + def.StatusName + "' is not a loadable StatusEffect prefab name — skipped. Run `statusdump` for the real names, fix SkillEchoes.json (config override) + `reloadskillechoes`.")); } else if (!PetProxyClient.RouteEnemyStatus(target, def.StatusName, (float)def.BuildupPercent.Value, "[ECHO]")) { StatusEffectManager statusEffectMngr = target.StatusEffectMngr; if (statusEffectMngr != null) { statusEffectMngr.AddStatusEffectBuildUp(def.StatusName, (float)def.BuildupPercent.Value, val2); } Plugin.Log.LogMessage((object)$"[ECHO] +{def.BuildupPercent.Value:F0}% '{def.StatusName}' build-up on '{target.Name}'."); } } else { PetSpecialAttack.ApplyStatus(target, def.StatusName, "[ECHO]"); } } internal static void DevEcho(Character player, string[] parts) { if (!Plugin.EnableSkillEcho.Value) { Plugin.Log.LogMessage((object)"[ECHO] EnableSkillEcho=false — the echo system is off (the verb honors the kill-switch)."); return; } string text = ((parts != null && parts.Length > 1) ? string.Join(" ", parts, 1, parts.Length - 1).Trim() : "Puncture"); int skillItemId = default(int); string text2 = default(string); if (!SkillEchoes.TryResolveName(text, ref skillItemId, ref text2)) { Plugin.Log.LogWarning((object)("[ECHO] '" + text + "' is not a roster skill (valid: " + SkillEchoes.RosterNames() + ").")); } else { Plugin.Log.LogMessage((object)("[ECHO] dev fire '" + text2 + "' (echo cooldown bypassed).")); OnPlayerCast(skillItemId, player, devBypassCooldown: true); } } internal static void Dump(Plugin plugin) { StringBuilder stringBuilder = new StringBuilder("[ECHO] skill-echo pipeline:\n"); stringBuilder.Append($" EnableSkillEcho={Plugin.EnableSkillEcho.Value}, defaults dmg x{Plugin.EchoDamageMult.Value:F2} / " + $"impact x{Plugin.EchoImpactMult.Value:F2}, windup {Plugin.EchoWindupSeconds.Value:F2}s, " + $"cooldown {Plugin.EchoCooldownSeconds.Value:F1}s" + ((Time.time < _nextReady) ? $" ({_nextReady - Time.time:F1}s left)" : " (ready)") + ", " + $"range {Plugin.EchoRangeMeters.Value:F0}m, EchoWhilePassive={Plugin.EchoWhilePassive.Value}\n"); stringBuilder.Append($" SkillStarted postfix attached: {SkillEchoCast.Attached}\n"); Pet pet = plugin?.ActivePet; SkillEchoEntry val = ((pet != null) ? SkillEchoTable.Resolve(pet.SpeciesId) : null); stringBuilder.Append((pet == null) ? " roster (no active pet — code defaults would apply to any):\n" : (" roster for '" + pet.SpeciesId + "' (" + ((val != null) ? "table overrides below" : "no table entry — pure defaults") + "):\n")); (string, int)[] roster = SkillEchoes.Roster; for (int i = 0; i < roster.Length; i++) { (string, int) tuple = roster[i]; string item = tuple.Item1; int item2 = tuple.Item2; ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; bool flag = (Object)(object)((instance != null) ? instance.GetItemPrefab(item2) : null) != (Object)null; SkillEchoDef val2 = SkillEchoes.ForSkill(val, item); stringBuilder.Append(string.Format(" {0} ({1}{2}) -> ", item, item2, flag ? "" : ", NO live item prefab!") + $"dmg x{SkillEchoes.EffectiveDamageMult(val2, (double)Plugin.EchoDamageMult.Value):F2}, " + $"impact x{SkillEchoes.EffectiveImpactMult(val2, (double)Plugin.EchoImpactMult.Value):F2}, " + "status=" + (val2?.StatusName ?? "none") + ((val2 != null && val2.BuildupPercent.HasValue) ? $" (build-up {val2.BuildupPercent.Value:F0}%)" : "") + ((val2 != null) ? " [override]" : "") + "\n"); } stringBuilder.Append(SkillEchoCue.Audit()).Append('\n'); stringBuilder.Append((LastFire == null) ? " last fire: none this session" : ($" last fire: t={LastFire.Time:F0}s '{LastFire.Skill}' -> {LastFire.Outcome}" + ((LastFire.Detail != null) ? (" " + LastFire.Detail) : ""))); Plugin.Log.LogMessage((object)stringBuilder.ToString()); } } internal static class SkillEchoTable { private const string FileName = "SkillEchoes.json"; private const string Tag = "[ECHO]"; private static readonly TableLoader _loader = new TableLoader("SkillEchoes.json", "[ECHO]", "species skill echo", "species skill echoes", (Func, Dictionary>)SkillEchoes.Parse, (Func, Dictionary, Dictionary>)SkillEchoes.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[ECHO]", Validate); internal static Dictionary Table => _axis.Table; internal static SkillEchoEntry Resolve(string speciesId) { return SkillEchoes.Resolve(Table, speciesId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { foreach (SkillEchoEntry value2 in Table.Values) { foreach (KeyValuePair item in value2.BySkill) { SkillEchoDef value = item.Value; string statusName = value.StatusName; int num; if (statusName != null) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; num = (((Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(statusName) : null) != (Object)null) ? 1 : 0); } else { num = 1; } bool flag = (byte)num != 0; if (!flag) { Plugin.Log.LogWarning((object)("[ECHO] '" + value2.Species + "' " + item.Key + ": status '" + statusName + "' is not a loadable StatusEffect prefab name — the echo will land damage but NO status. Run `statusdump` for the real names, fix the table (config override) + `reloadskillechoes`.")); } string text = (value.DamageMult.HasValue ? $"dmg x{value.DamageMult.Value:F2}" : "dmg default") + ", " + (value.ImpactMult.HasValue ? $"impact x{value.ImpactMult.Value:F2}" : "impact default"); string text2 = ((statusName == null) ? "no status" : ("'" + statusName + "' (" + (value.BuildupPercent.HasValue ? $"build-up {value.BuildupPercent.Value:F0}%" : "direct apply") + ", " + (flag ? "armed" : "INERT") + ")")); Plugin.Log.LogMessage((object)("[ECHO] '" + value2.Species + "' " + item.Key + ": " + text2 + ", " + text + ".")); } } Plugin.Log.LogMessage((object)(string.Format("{0} boot check: {1} species override(s); defaults dmg ", "[ECHO]", Table.Count) + $"x{Plugin.EchoDamageMult.Value:F2} / impact x{Plugin.EchoImpactMult.Value:F2} on every roster " + $"skill, EnableSkillEcho={Plugin.EnableSkillEcho.Value}.")); } } internal static class SlConsumables { internal readonly struct IngredientSpec { internal readonly int ItemId; internal readonly Tag Tag; internal readonly bool IsGeneric; private IngredientSpec(int itemId, Tag tag, bool generic) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) ItemId = itemId; Tag = tag; IsGeneric = generic; } internal static IngredientSpec Specific(int itemId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return new IngredientSpec(itemId, Tag.None, generic: false); } internal static IngredientSpec Generic(Tag tag) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return new IngredientSpec(0, tag, generic: true); } } private static readonly Dictionary _iconCache = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static void SetVendorValue(string tag, string key, Item prefab, int silver) { if (!((Object)(object)prefab == (Object)null)) { ItemStats val = prefab.Stats ?? ((Component)prefab).GetComponent(); if ((Object)(object)val != (Object)null) { val.m_baseValue = silver; } prefab.m_buyValue_DEPRECATED = silver; prefab.m_overrideSellModifier = 1f; Plugin.Log.LogMessage((object)($"{tag} '{key}': vendor value pinned — sells for exactly {silver} silver " + string.Format("(base={0}, sell modifier overridden to 1.0{1}).", silver, ((Object)(object)val == (Object)null) ? "; NO ItemStats — used the deprecated buy-value field" : ""))); } } internal static Item RegisterUsable(string tag, string key, int donorId, int newId, string name, string description, Action addEffects, string iconPng, SpriteBorderTypes iconBorder) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: 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_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown //IL_00da: Unknown result type (might be due to invalid IL or missing references) if (!IdPool.Claim(newId, tag + " '" + key + "'", (Action)delegate(string m) { Plugin.Log.LogError((object)m); })) { return null; } SL_Item val = new SL_Item { Target_ItemID = donorId, New_ItemID = newId, Name = name, Description = description, IsUsable = true, QtyRemovedOnUse = 0 }; ((ContentTemplate)val).ApplyTemplate(); Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(newId); if ((Object)(object)itemPrefab == (Object)null) { Plugin.Log.LogWarning((object)$"{tag} '{key}': item prefab {newId} missing after ApplyTemplate — skipped."); return null; } GameObject val2 = new GameObject("Effects"); val2.transform.parent = ((Component)itemPrefab).transform; addEffects?.Invoke(val2); Sprite val3 = LoadIcon(tag, iconPng, iconBorder); if ((Object)(object)val3 != (Object)null) { CustomItemVisuals.SetSpriteLink(itemPrefab, val3, false); } ItemNameIndex.RegisterCustom(name, newId); return itemPrefab; } internal static Recipe RegisterRecipe(string tag, string key, string uid, CraftingType station, IEnumerable ingredientIds, int resultId, int resultQty) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) List list = new List(); foreach (int ingredientId in ingredientIds) { list.Add(IngredientSpec.Specific(ingredientId)); } return RegisterRecipe(tag, key, uid, station, list, resultId, resultQty); } internal static Recipe RegisterRecipe(string tag, string key, string uid, CraftingType station, IReadOnlyList ingredients, int resultId, int resultQty) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006d: 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_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_009b: 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_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Expected O, but got Unknown //IL_0081: Expected O, but got Unknown //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_01ad: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01d7: Unknown result type (might be due to invalid IL or missing references) //IL_01dc: Unknown result type (might be due to invalid IL or missing references) //IL_01e0: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Unknown result type (might be due to invalid IL or missing references) //IL_0210: Unknown result type (might be due to invalid IL or missing references) //IL_0215: Unknown result type (might be due to invalid IL or missing references) //IL_021f: Expected O, but got Unknown SL_Recipe val = new SL_Recipe { UID = uid, StationType = station }; foreach (IngredientSpec ingredient in ingredients) { List ingredients2 = val.Ingredients; ? val2; if (!ingredient.IsGeneric) { val2 = new Ingredient { Type = (ActionTypes)1 }; ? val3 = val2; int itemId = ingredient.ItemId; ((Ingredient)val3).SelectorValue = itemId.ToString(CultureInfo.InvariantCulture); } else { val2 = new Ingredient { Type = (ActionTypes)0, SelectorValue = ingredient.Tag.TagName }; } ingredients2.Add((Ingredient)val2); } val.Results.Add(new ItemQty { ItemID = resultId, Quantity = resultQty }); ((ContentTemplate)val).ApplyTemplate(); if (!References.ALL_RECIPES.TryGetValue(uid, out var value) || (Object)(object)value == (Object)null) { Plugin.Log.LogWarning((object)(tag + " '" + key + "': recipe '" + uid + "' did not register (see SideLoader warnings above) — skipped.")); return null; } RecipeIngredient[] ingredients3 = value.Ingredients; for (int i = 0; i < ingredients.Count && i < ingredients3.Length; i++) { if (ingredients[i].IsGeneric && ingredients3[i] != null) { Tag val4 = ((ingredients3[i].AddedIngredientType != null) ? ingredients3[i].AddedIngredientType.Tag : Tag.None); UID uID = ((Tag)(ref val4)).UID; Tag tag2 = ingredients[i].Tag; if (!(uID == ((Tag)(ref tag2)).UID)) { ManualLogSource log = Plugin.Log; string text = $"{tag} '{key}': recipe '{uid}' slot {i + 1} resolved tag '{val4.TagName}' (uid {((Tag)(ref val4)).UID}) "; tag2 = ingredients[i].Tag; log.LogWarning((object)(text + $"instead of uid {((Tag)(ref tag2)).UID} — re-stamping the exact tag.")); ingredients3[i].ActionType = (ActionTypes)0; ingredients3[i].AddedIngredientType = new TagSourceSelector(ingredients[i].Tag); } } } return value; } private unsafe static Sprite LoadIcon(string tag, string png, SpriteBorderTypes border) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(png)) { return null; } string key = png + "|" + ((object)(*(SpriteBorderTypes*)(&border))/*cast due to .constrained prefix*/).ToString(); if (_iconCache.TryGetValue(key, out var value)) { return value; } Sprite val = EmbeddedRes.Sprite(png, border, tag); if ((Object)(object)val != (Object)null) { _iconCache[key] = val; } return val; } } internal static class SlFeatureSetup { internal static void Run(string tag, ConfigEntry gate, string disabledMessage, ICollection entries, Func nameOf, Action register, Func registeredCount, string entryNoun) { if (!gate.Value) { Plugin.Log.LogMessage((object)(tag + " " + disabledMessage)); return; } foreach (T entry in entries) { try { register(entry); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"{tag} '{nameOf(entry)}' registration failed: {arg}"); } } Plugin.Log.LogMessage((object)$"{tag} registered {registeredCount()}/{entries.Count} {entryNoun}."); } } internal static class SlStatus { internal static string[] DefaultDonors => SlStatus.DefaultDonors; static SlStatus() { SlStatus.Log = Plugin.Log; SlStatus.DefaultIconLoader = (string png, string tag) => EmbeddedRes.Sprite(png, (SpriteBorderTypes)0, tag); } internal static bool ResolveDonor(SlStatusSpec spec, out string donor) { return SlStatus.ResolveDonor((SlStatusSpec)(object)spec, ref donor); } internal static bool Register(SlStatusSpec spec, string donor, out StatusEffect prefab) { return SlStatus.Register((SlStatusSpec)(object)spec, donor, ref prefab); } internal static void ResetIconRetries() { SlStatus.ResetIconRetries(); } internal static bool IconHeld(string id) { return SlStatus.IconHeld(id); } internal static bool EnsureIcon(string id, StatusEffect live, string tag) { return SlStatus.EnsureIcon(id, live, tag); } internal static string IconState(string id, StatusEffect live) { return SlStatus.IconState(id, live); } internal static void StripInheritedFx(StatusEffect prefab, string donor, string id, string tag) { SlStatus.StripInheritedFx(prefab, donor, id, tag); } internal static StatusEffect Live(Character player, string id) { return SlStatus.Live(player, id); } internal static int StackCount(Character player, string id) { return SlStatus.StackCount(player, id); } internal static void AddStack(Character player, string id) { SlStatus.AddStack(player, id); } internal static void Remove(Character player, string id) { SlStatus.Remove(player, id); } internal static void RefreshAll(Character player, string id, float durationSeconds) { SlStatus.RefreshAll(player, id, durationSeconds); } internal static void SetStacks(Character player, string id, int n, float durationSeconds, bool canAdd) { SlStatus.SetStacks(player, id, n, durationSeconds, canAdd); } internal static void AddOrRefresh(Character player, string id, float durationSeconds) { SlStatus.AddOrRefresh(player, id, durationSeconds); } internal static void SyncDuration(string id, float durationSeconds) { SlStatus.SyncDuration(id, durationSeconds); } } internal sealed class SlStatusSpec : SlStatusSpec { } internal static class SpecialAttacks { private static readonly TableLoader _loader = new TableLoader("SpeciesSpecialAttacks.txt", "[SPECIALATK]", "special-attack entry", "special-attack entries", (Func, Dictionary>)SpecialAttackTable.Parse, (Func, Dictionary, Dictionary>)SpecialAttackTable.Merge, "wins per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[SPECIALATK]", Validate); internal static Dictionary Table => _axis.Table; internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = 0; int misses = 0; HashSet checkedNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (SpecialAttackDef value in Table.Values) { num++; string[] array = SpecialAttackTable.SplitStatuses(value.StatusEffectId); foreach (string text in array) { TableValidator.CheckHexName("[SPECIALATK]", "SpeciesSpecialAttacks.txt", "reloadattacks", checkedNames, text, "status '" + text + "'", ref misses); } } int num2 = TableValidator.CheckSpeciesKeys("[SPECIALATK]", "SpeciesSpecialAttacks.txt", Table.Keys); Plugin.Log.LogMessage((object)$"[SPECIALATK] boot check: {num} rows, {misses} unknown status name(s), {num2} unknown species key(s)."); } internal static string RangedCaptureFilter(string speciesId) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 SpecialAttackDef val = default(SpecialAttackDef); if (!SpecialAttackTable.TryGet(Table, speciesId, ref val)) { return null; } if ((int)val.Kind != 1) { return null; } return val.ProjectileFilter ?? ""; } internal static int RangedCaptureSkillId(string speciesId) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 SpecialAttackDef val = default(SpecialAttackDef); if (!SpecialAttackTable.TryGet(Table, speciesId, ref val)) { return 0; } if ((int)val.Kind != 1) { return 0; } return val.ProjectileSkillId; } } internal struct SpecialHitContext { public Pet Pet; public SpecialAttackDef Def; public Character Target; public Character Dealer; public List ActiveSigils; } internal struct ScentContext { public Pet Pet; public SenseDef Sense; public ScentAlert Alert; public Vector3 TargetPosition; } internal struct ExecuteContext { public Pet Pet; public Character Target; public Character Dealer; } internal struct GiftRollContext { public Pet Pet; public Character Player; public string Species; public PetGiftEntry Entry; public GiftDrop Drop; public int Loyalty; public double Roll; } internal abstract class SpeciesBehavior { public abstract string SpeciesKey { get; } public virtual void OnMealConsumed(Pet pet, int itemId, string itemName) { if (!Plugin.EnableFoodHexes.Value || pet?.Sim == null) { return; } FoodHexEntry val = FoodHexTable.Resolve(pet.SpeciesId); if (val != null) { string text = FoodHexes.MatchMeal(val, itemId, itemName); if (text == null) { Plugin.Log.LogMessage((object)$"[FOODHEX] '{itemName}' (id={itemId}) has no hex mapping for '{pet.SpeciesId}' — not recorded; the meal window is unchanged."); return; } pet.Sim.RecordMeal(text); Plugin.Log.LogMessage((object)("[FOODHEX] recorded meal '" + text + "' — history now [" + string.Join(", ", pet.State.RecentMeals.ToArray()) + "].")); } } public virtual void OnSpecialAttackHit(in SpecialHitContext ctx) { PetSpecialAttack.ApplyStatuses(ctx.Target, ctx.Def, ctx.Dealer); ApplyHexBuildups(in ctx); } public virtual void OnScentDetected(in ScentContext ctx) { ScentIndication.Indicate(in ctx); } public virtual bool OnExecuteHit(in ExecuteContext ctx) { if ((Object)(object)ctx.Target == (Object)null) { return false; } string text = ctx.Pet?.SpeciesId; ForTheKillEntry val = ForTheKillTable.Resolve(text); if (val == null) { Plugin.Log.LogMessage((object)("[FTK] '" + text + "' has no ForTheKill.json entry — strike only, no debuff.")); return false; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; if ((Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(val.StatusName) : null) == (Object)null) { Plugin.Log.LogWarning((object)("[FTK] debuff '" + val.StatusName + "' is not a loadable StatusEffect prefab name — skipped (the [FTK] boot check flags this too; fix the table + `reloadftk`).")); return false; } if (val.BuildupPercent.HasValue) { if (PetProxyClient.RouteEnemyStatus(ctx.Target, val.StatusName, (float)val.BuildupPercent.Value, "[FTK]")) { return true; } StatusEffectManager statusEffectMngr = ctx.Target.StatusEffectMngr; if (statusEffectMngr != null) { statusEffectMngr.AddStatusEffectBuildUp(val.StatusName, (float)val.BuildupPercent.Value, ctx.Dealer); } Plugin.Log.LogMessage((object)$"[FTK] +{val.BuildupPercent.Value:F0}% '{val.StatusName}' build-up on '{ctx.Target.Name}'."); } else { PetSpecialAttack.ApplyStatus(ctx.Target, val.StatusName); Plugin.Log.LogMessage((object)("[FTK] '" + val.StatusName + "' applied to '" + ctx.Target.Name + "'.")); } return true; } public virtual void OnGiftRolled(in GiftRollContext ctx) { if (ctx.Drop == null) { Notify.Player(ctx.Player, ctx.Species + " has nothing to give this time."); Plugin.Log.LogMessage((object)$"[GIFT] '{ctx.Species}' rolled {ctx.Roll:0.000} at loyalty {ctx.Loyalty} — nothing (cooldown stands)."); } else { PetGifting.Give(ctx.Player, ctx.Species, ctx.Drop, ctx.Loyalty, ctx.Roll); } } protected void ApplyHexBuildups(in SpecialHitContext ctx) { //IL_019b: 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) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01ef: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Unknown result type (might be due to invalid IL or missing references) //IL_0221: Unknown result type (might be due to invalid IL or missing references) //IL_0265: Unknown result type (might be due to invalid IL or missing references) //IL_0237: Unknown result type (might be due to invalid IL or missing references) //IL_028f: Unknown result type (might be due to invalid IL or missing references) //IL_029e: Unknown result type (might be due to invalid IL or missing references) //IL_024d: Unknown result type (might be due to invalid IL or missing references) Pet pet = ctx.Pet; if (pet?.State == null || (Object)(object)ctx.Target == (Object)null) { return; } FoodHexEntry val = (Plugin.EnableFoodHexes.Value ? FoodHexTable.Resolve(pet.SpeciesId) : null); List list = ((val != null) ? FoodHexes.ComputeBuildups(val, (IReadOnlyList)pet.State.RecentMeals, Plugin.HexMealWindow.Value, Plugin.HexBuildupPercent.Value) : new List()); List list2 = (Plugin.EnableSigilSynergies.Value ? SigilSynergies.ActiveSynergies(SigilTable.Resolve(pet.SpeciesId), (IReadOnlyList)ctx.ActiveSigils, "specialHit") : null); bool flag = list2 != null && list2.Count > 0; List list3 = SigilSynergies.ComposeBuildups(list, (IReadOnlyList)list2); if (flag) { List list4 = new List(); foreach (SigilSynergyDef item in list2) { list4.Add(item.SigilKey + " (" + item.Mode + ")"); } Plugin.Log.LogMessage((object)string.Format("[SIGIL] '{0}' special hit inside [{1}] → {2} hex(es).", pet.SpeciesId, string.Join(", ", list4.ToArray()), list3.Count)); } if (list3.Count == 0) { if (val != null && !flag) { Plugin.Log.LogMessage((object)("[FOODHEX] '" + pet.SpeciesId + "' has a food-hex table but no mapped meals in the window — no build-up applied (feed it; see `mealdump`).")); } return; } string text = (flag ? "[SIGIL]" : "[FOODHEX]"); foreach (HexBuildUp item2 in list3) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; if ((Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(item2.HexId) : null) == (Object)null) { Plugin.Log.LogWarning((object)(text + " hex '" + item2.HexId + "' is not a loadable StatusEffect prefab name — skipped. Run `statusdump` for the real names, fix the table (config override) and reload it.")); } else if (!PetProxyClient.RouteEnemyStatus(ctx.Target, item2.HexId, item2.Percent, text)) { StatusEffectManager statusEffectMngr = ctx.Target.StatusEffectMngr; if (statusEffectMngr != null) { statusEffectMngr.AddStatusEffectBuildUp(item2.HexId, item2.Percent, ctx.Dealer); } string text2 = ((!flag) ? $"{item2.MealCount} meal(s) in the window" : ((item2.MealCount > 0) ? $"{item2.MealCount} meal(s) + sigil" : "sigil")); Plugin.Log.LogMessage((object)$"{text} +{item2.Percent:F0}% '{item2.HexId}' build-up on '{ctx.Target.Name}' ({text2})."); } } } } internal sealed class DefaultSpeciesBehavior : SpeciesBehavior { public override string SpeciesKey => ""; } internal static class SpeciesRegistry { private static readonly List _registered = new List(); private static readonly List _keys = new List(); private static readonly SpeciesBehavior Fallback = new DefaultSpeciesBehavior(); public static void Register(SpeciesBehavior behavior) { if (behavior != null) { string speciesKey = behavior.SpeciesKey; if (string.IsNullOrEmpty(speciesKey)) { Plugin.Log.LogWarning((object)("[SPECIES] " + behavior.GetType().Name + " has an empty SpeciesKey — it could never resolve; not registered.")); } else if (_keys.Contains(speciesKey)) { Plugin.Log.LogWarning((object)("[SPECIES] a behavior for '" + speciesKey + "' is already registered — " + behavior.GetType().Name + " ignored (duplicate key).")); } else { _registered.Add(behavior); _keys.Add(speciesKey); Plugin.Log.LogMessage((object)("[SPECIES] registered behavior '" + behavior.GetType().Name + "' for species key '" + speciesKey + "'.")); } } } public static SpeciesBehavior For(string speciesId) { int num = SpeciesResolve.MatchIndex(speciesId, (IReadOnlyList)_keys); if (num < 0) { return Fallback; } return _registered[num]; } } internal static class SynergyStatus { internal const string Id = "BW_Synergy"; internal const string FamilyUid = "BW_SynergyFam"; private static bool _registered; internal static bool Registered => _registered; internal static void Init() { SL.OnPacksLoaded += Register; } private static void Register() { //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_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Invalid comparison between Unknown and I4 if (_registered) { return; } SlStatusSpec spec = new SlStatusSpec { Id = "BW_Synergy", NumId = 87061, Name = "Synergy", Description = "You and your companion strike as one. Each stack sharpens both your damage; spend the stacks with For the Kill.", Lifespan = Mathf.Max(1f, Plugin.SynergyDurationSeconds.Value), IconPng = "PetSynergy.png", IsMalus = false, Tag = "[SYNERGY]", DisabledSuffix = "the Synergy buff is disabled.", BindFamily = new SL_StatusEffectFamily { UID = "BW_SynergyFam", Name = "BW_SynergyFam", StackBehaviour = (StackBehaviors)3, MaxStackCount = -1, LengthType = (LengthTypes)0 } }; if (!SlStatus.ResolveDonor(spec, out var donor)) { return; } try { if (SlStatus.Register(spec, donor, out var prefab)) { _registered = true; StatusEffectFamily effectFamily = prefab.EffectFamily; Plugin.Log.LogMessage((object)($"[SYNERGY] 'BW_Synergy' registered (donor '{donor}', lifespan {prefab.StatusData?.LifeSpan:0}s, " + $"family '{effectFamily?.Name}' stack={effectFamily?.StackBehavior} max={effectFamily?.MaxStackCount}).")); if (effectFamily == (StatusEffectFamily)null || (int)effectFamily.StackBehavior != 3) { Plugin.Log.LogWarning((object)"[SYNERGY] the bound family did NOT take StackAll — grants will override instead of stacking (plan risk R3; fallback = mod-tracked count)."); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SYNERGY] 'BW_Synergy' registration failed: " + ex)); } } internal static StatusEffect Live(Character player) { return SlStatus.Live(player, "BW_Synergy"); } internal static int StackCount(Character player) { return SlStatus.StackCount(player, "BW_Synergy"); } internal static void AddStack(Character player) { if (_registered) { SlStatus.AddStack(player, "BW_Synergy"); } } internal static void RefreshAll(Character player, float durationSeconds) { SlStatus.RefreshAll(player, "BW_Synergy", durationSeconds); } internal static void Remove(Character player) { SlStatus.Remove(player, "BW_Synergy"); } internal static void SetStacks(Character player, int n) { SlStatus.SetStacks(player, "BW_Synergy", n, Plugin.SynergyDurationSeconds.Value, _registered); } internal static void SyncDuration() { if (_registered) { SlStatus.SyncDuration("BW_Synergy", Mathf.Max(1f, Plugin.SynergyDurationSeconds.Value)); } } } internal sealed class TableLoader : TableLoader { internal TableLoader(string fileName, string logTag, string singularNoun, string pluralNoun, Func, Dictionary> parse, Func, Dictionary, Dictionary> merge, string mergeNote = "replaces per-species", Func, Action, Dictionary> postLoad = null, Func, string> builtInSuffix = null, string reloadSuffix = "") : base(typeof(Plugin).Assembly, Plugin.Log, fileName, logTag, singularNoun, pluralNoun, parse, merge, mergeNote, postLoad, builtInSuffix, reloadSuffix) { } } internal static class TableValidator { internal static bool RegistryReady(string tag) { return TableValidator.RegistryReady(tag, Plugin.Log); } internal static void CheckItemKey(string tag, string fileName, string noun, int? itemId, string nameKey, string idSubject, string nameSubject, ref int misses) { TableValidator.CheckItemKey(tag, fileName, noun, itemId, nameKey, idSubject, nameSubject, ref misses, Plugin.Log); } internal static void CheckItemTable(string tag, string fileName, string noun, IEnumerable refs, out int checkedRefs, out int misses) { TableValidator.CheckItemTable(tag, fileName, noun, refs, ref checkedRefs, ref misses, Plugin.Log); } internal static void CheckHexName(string tag, string fileName, string reloadVerb, HashSet checkedNames, string hexId, string subject, ref int misses) { TableValidator.CheckHexName(tag, fileName, reloadVerb, checkedNames, hexId, subject, ref misses, Plugin.Log); } internal static int CheckSpeciesKeys(string tag, string fileName, IEnumerable keys, string reservedKey = null) { List list = new List(TamingFoodTable.Table.Keys); list.AddRange(DonorHarvest.DonorScenes.Keys); List list2 = SpeciesAudit.UnknownKeys(fileName, keys, (IEnumerable)list, reservedKey); foreach (Finding item in list2) { Plugin.Log.LogWarning((object)(tag + " " + item.Message)); } return list2.Count; } } internal static class TableVerbs { private static readonly (string Verb, string Help, Action Reload)[] All = new(string, string, Action)[18] { ("reloaddiets", "Re-read the PetDiets.json config override.", delegate { PetDietTable.Reload(); }), ("reloadtaming", "Re-read the TamingFoods.json config override (retunes only).", delegate { TamingFoodTable.Reload(); }), ("reloadcomfort", "Re-read SpeciesComfort/Blankets config overrides live.", delegate { PetComfortTable.Reload(); }), ("reloadweather", "Re-read the WeatherFoods.json config override + re-run its boot check.", delegate { WeatherFoodTable.Reload(); }), ("reloadgear", "Re-read the PetGear.json config override + re-run the boot check (fully live).", delegate { PetGearTable.Reload(); }), ("reloadscents", "Re-read the PetSenses.json config override + re-run the boot check (fully live).", delegate { PetSenseTable.Reload(); }), ("reloadscavenge", "Re-read the PetScavenge.json config override + re-run the boot check (fully live).", delegate { ScavengeTable.Reload(); }), ("reloadattacks", "Re-read the SpeciesSpecialAttacks config override (also re-stamps Hunt as One's cooldown).", delegate { SpecialAttacks.Reload(); HuntAsOneCooldown.SyncCooldown(Plugin.LocalPlayerCharacter); }), ("reloadfoodhexes", "Re-read FoodHexes.json + re-run its boot check (live).", delegate { FoodHexTable.Reload(); }), ("reloadsigils", "Re-read Sigils.json + re-run its boot check + rebuild detection (live).", delegate { SigilTable.Reload(); }), ("reloadgifts", "Re-read PetGifts.json + re-run its boot check (live).", delegate { GiftTable.Reload(); }), ("reloadftk", "Re-read ForTheKill.json + re-run its boot check (fully live, registration-free).", delegate { ForTheKillTable.Reload(); }), ("reloadbufffoods", "Re-read the BuffFoods.json config override + re-run its boot check (fully live).", delegate { BuffFoodTable.Reload(); }), ("reloadskillechoes", "Re-read SkillEchoes.json + re-run its boot check (fully live, registration-free).", delegate { SkillEchoTable.Reload(); }), ("reloadfletch", "Re-read FeatherFletch.json + re-run its boot check (items/recipes register at boot — new rows need a relaunch).", delegate { FletchTable.Reload(); }), ("reloadbuffs", "Re-read SpeciesBuffs.txt + re-apply on the spot.", delegate(Plugin p) { PetBuffTable.Reload(); p.ResyncBuffs(); }), ("reloadgrowth", "Re-read SpeciesGrowth.txt (the loyalty-growth distribution) — the next sim tick re-applies.", delegate { GrowthTable.Reload(); }), ("reloadyaw", "Re-read the SpeciesYawOffsets.txt embedded/override yaw table — the next re-form picks it up (a live 'yaw' tune / the cfg string still win).", delegate { YawTable.Reload(); }) }; internal static void RegisterAll(CommandRegistry c, Plugin plugin) { (string, string, Action)[] all = All; for (int i = 0; i < all.Length; i++) { var (text, text2, reload) = all[i]; c.Register(text, text2, (Action)delegate { reload(plugin); }); } } } internal static class TameOnUse { internal static Character FindTarget(Character player, TamingEntry entry, float radius) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || entry == null) { return null; } List list = new List(); CharacterManager.Instance.FindCharactersInRange(((Component)player).transform.position, radius, ref list); Character result = null; float num = float.MaxValue; foreach (Character item in list) { if (!((Object)(object)item == (Object)null) && !((Object)(object)item == (Object)(object)player) && item.IsAI && item.Alive && BodyFactory.IsWildTamable(item) && !UntameableSpecies.IsBlocked(item.Name) && TamingFoodTable.Resolve(item.Name) == entry) { float num2 = Vector3.Distance(((Component)player).transform.position, ((Component)item).transform.position); if (num2 < num) { num = num2; result = item; } } } return result; } } internal sealed class TamingUseVeto : IUseVeto { public string Tag => "[TAMEFOOD]"; public bool Owns(int itemId) { if (Plugin.EnableTamingFoods.Value) { return TamingFoodTable.ForChowId(itemId) != null; } return false; } public UseVeto Check(Item item, Character user) { TamingEntry val = TamingFoodTable.ForChowId(item.ItemID); if (val == null) { return null; } if ((Object)(object)user != (Object)null && user.IsLocalPlayer && (Object)(object)user != (Object)(object)Plugin.LocalPlayer) { return new UseVeto("Only the first player can bond a companion for now.", "[TAME] use of '" + val.ChowName + "' vetoed: a non-first local player (split-screen P2) can't bond a companion yet."); } if (Plugin.Instance?.ActivePet != null) { return new UseVeto("You already have a companion.", "[TAMEFOOD] use of '" + val.ChowName + "' vetoed: a pet already exists."); } if ((Object)(object)TameOnUse.FindTarget(user, val, Plugin.TameRadius.Value) == (Object)null) { return new UseVeto("There is no wild " + val.Species + " close enough.", $"[TAMEFOOD] use of '{val.ChowName}' vetoed: no {val.Species} within {Plugin.TameRadius.Value:0.#}."); } return null; } } internal class TamingFoodEffect : Effect { public string Species; public override void ActivateLocally(Character _affectedCharacter, object[] _infos) { try { TamingEntry val = TamingFoodTable.Resolve(Species); if (val == null || (Object)(object)_affectedCharacter == (Object)null) { return; } if (!_affectedCharacter.IsLocalPlayer) { Plugin.Log.LogMessage((object)("[TAMEFOOD] replicated chow-use by remote '" + _affectedCharacter.Name + "' ignored on this machine (the actor's machine owns the tame).")); } else if (!Plugin.EnableTamingFoods.Value) { Notify.Player(_affectedCharacter, "Taming is disabled right now — the chow was not consumed."); Plugin.Log.LogMessage((object)"[TAMEFOOD] chow use ignored — [Taming] EnableTamingFoods is off (flip it back + `reloadcfg` to re-enable; the chow was not consumed)."); } else if (_affectedCharacter.IsLocalPlayer && (Object)(object)_affectedCharacter != (Object)(object)Plugin.LocalPlayer) { Notify.Player(_affectedCharacter, "Only the first player can bond a companion for now."); Plugin.Log.LogMessage((object)"[TAME] tame-on-use refused: a non-first local player (split-screen P2) can't bond a companion yet — chow kept."); } else { if (Plugin.Instance?.ActivePet != null) { return; } float num = Mathf.Max(1f, Plugin.TameRecheckGraceMult.Value); float num2 = Plugin.TameRadius.Value * num; Character val2 = TameOnUse.FindTarget(_affectedCharacter, val, num2); if ((Object)(object)val2 == (Object)null) { Notify.Player(_affectedCharacter, "The " + val.Species + " is gone. The " + val.ChowName + " is unspent."); Plugin.Log.LogMessage((object)($"[TAMEFOOD] target vanished mid-use (re-checked within {num2:0.#}m " + $"= TameRadius {Plugin.TameRadius.Value:0.#} x grace {num:0.##}) — chow kept.")); return; } string name = val2.Name; if (!Plugin.Instance.TameSpecific(val2, _affectedCharacter)) { Notify.Player(_affectedCharacter, "Something spooked it — nothing happens."); Plugin.Log.LogWarning((object)"[TAMEFOOD] TameSpecific failed — chow kept."); return; } Item parentItem = ((Effect)this).ParentItem; if ((Object)(object)parentItem != (Object)null) { parentItem.RemoveQuantity(1); } else { Plugin.Log.LogWarning((object)"[TAMEFOOD] no ParentItem to consume — tame landed, chow NOT consumed (bug, report)."); } Plugin.Log.LogMessage((object)("[TAMEFOOD] '" + val.ChowName + "' consumed → tamed '" + name + "'.")); Notify.Player(_affectedCharacter, "The " + name + " devours the " + val.ChowName + " — it follows you curiously."); } } catch (Exception ex) { Plugin.Log.LogError((object)("[TAMEFOOD] tame-on-use failed: " + ex)); } } } internal static class TamingFoodSetup { internal static readonly Dictionary Registered = new Dictionary(StringComparer.OrdinalIgnoreCase); private const int ChowVendorSilver = 1; private static bool _done; internal static void Init() { SL.OnPacksLoaded += Setup; } private static void Setup() { if (!_done) { _done = true; SlFeatureSetup.Run("[TAMEFOOD]", Plugin.EnableTamingFoods, "EnableTamingFoods=false — no taming items/recipes registered.", TamingFoodTable.Table.Values, (TamingEntry e) => e.Species, RegisterSpecies, () => Registered.Count, "taming-food species"); } } private static void RegisterSpecies(TamingEntry entry) { //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_0238: Unknown result type (might be due to invalid IL or missing references) //IL_029f: Unknown result type (might be due to invalid IL or missing references) //IL_02a6: Expected O, but got Unknown ValueTuple valueTuple = TamingIds.Resolve(entry); int item = valueTuple.Item1; int item2 = valueTuple.Item2; int? num = ResolveItemKey(entry.CloneFrom, entry.Species, "cloneFrom"); if (!num.HasValue) { return; } List list = new List(); foreach (TamingItemKey ingredient in entry.Ingredients) { if (ingredient.Category != null) { if (!FoodCategoryTags.TryGetTag(ingredient.Category, out var tag)) { Plugin.Log.LogWarning((object)("[TAMEFOOD] '" + entry.Species + "': ingredient category '" + ingredient.Category + "' has no game tag (valid: " + FoodCategories.ValidList() + "; see 'foodcats') — species skipped.")); return; } list.Add(SlConsumables.IngredientSpec.Generic(tag)); } else { int? num2 = ResolveItemKey(ingredient, entry.Species, "ingredient"); if (!num2.HasValue) { return; } list.Add(SlConsumables.IngredientSpec.Specific(num2.Value)); } } int? num3 = ((entry.ScrollCloneFrom != null) ? ResolveItemKey(entry.ScrollCloneFrom, entry.Species, "scrollCloneFrom") : FindVanillaRecipeScroll()); if (!num3.HasValue) { return; } Item val = SlConsumables.RegisterUsable("[TAMEFOOD]", entry.Species, num.Value, item, entry.ChowName, entry.ChowDesc, delegate(GameObject host) { host.AddComponent().Species = entry.Species; }, "TamingChow.png", (SpriteBorderTypes)1); if ((Object)(object)val == (Object)null) { return; } SlConsumables.SetVendorValue("[TAMEFOOD]", entry.Species, val, 1); string text = TamingFoods.RecipeUid(entry.Species); Recipe val2 = SlConsumables.RegisterRecipe("[TAMEFOOD]", entry.Species, text, (CraftingType)Enum.Parse(typeof(CraftingType), entry.Station), list, item, entry.ResultCount); if (!((Object)(object)val2 == (Object)null) && IdPool.Claim(item2, "[TAMEFOOD] recipe scroll for '" + entry.Species + "'", (Action)delegate(string m) { Plugin.Log.LogError((object)m); })) { SL_RecipeItem val3 = new SL_RecipeItem(); ((SL_Item)val3).Target_ItemID = num3.Value; ((SL_Item)val3).New_ItemID = item2; ((SL_Item)val3).Name = "Recipe: " + entry.ChowName; ((SL_Item)val3).Description = "Teaches the recipe for " + entry.ChowName + " — the way to a wild " + entry.Species + "'s heart."; val3.RecipeUID = text; SL_RecipeItem val4 = val3; ((ContentTemplate)val4).ApplyTemplate(); Item itemPrefab = ResourcesPrefabManager.Instance.GetItemPrefab(item2); RecipeItem val5 = (RecipeItem)(object)((itemPrefab is RecipeItem) ? itemPrefab : null); if (val5 != null) { val5.Recipe = val2; SlConsumables.SetVendorValue("[TAMEFOOD]", entry.Species, (Item)(object)val5, 1); Registered[entry.Species] = (item, item2); Plugin.Log.LogMessage((object)($"[TAMEFOOD] '{entry.Species}': chow {item} '{entry.ChowName}' (donor {num}), " + $"recipe '{text}' ({entry.Station}: {list.Count} ingredients → {entry.ResultCount}x), scroll {item2} (donor {num3}).")); } else { Plugin.Log.LogWarning((object)$"[TAMEFOOD] '{entry.Species}': scroll prefab {item2} missing/not a RecipeItem after ApplyTemplate — species skipped."); } } } private static int? ResolveItemKey(TamingItemKey key, string species, string field) { if (key.ItemId.HasValue) { if ((Object)(object)ResourcesPrefabManager.Instance.GetItemPrefab(key.ItemId.Value) == (Object)null) { Plugin.Log.LogWarning((object)$"[TAMEFOOD] '{species}': {field} ItemID {key.ItemId.Value} is not a known item — species skipped."); return null; } return key.ItemId.Value; } if (ItemNameIndex.TryResolve(key.Key, out var itemId)) { Plugin.Log.LogMessage((object)$"[TAMEFOOD] '{species}': {field} '{key.Key}' resolved to ItemID {itemId} (put the number in TamingFoods.json to make it locale-proof)."); return itemId; } Plugin.Log.LogWarning((object)("[TAMEFOOD] '" + species + "': " + field + " '" + key.Key + "' matches no item display name on this locale — species skipped.")); return null; } private static int? FindVanillaRecipeScroll() { int num = int.MaxValue; foreach (Item value in ResourcesPrefabManager.ITEM_PREFABS.Values) { if (value is RecipeItem && value.ItemID < num) { num = value.ItemID; } } if (num == int.MaxValue) { Plugin.Log.LogWarning((object)"[TAMEFOOD] no vanilla RecipeItem found to clone the scroll from (?) — set scrollCloneFrom in TamingFoods.json."); return null; } return num; } internal static void Dump() { Dictionary table = TamingFoodTable.Table; Plugin.Log.LogMessage((object)(string.Format("[TAMEFOOD] tamingdump: {0} table entr{1}, ", table.Count, (table.Count == 1) ? "y" : "ies") + $"{Registered.Count} registered, EnableTamingFoods={Plugin.EnableTamingFoods.Value}, " + $"TameRadius={Plugin.TameRadius.Value}, RecipeDropChance={Plugin.RecipeDropChance.Value}.")); Character localPlayerCharacter = Plugin.LocalPlayerCharacter; foreach (TamingEntry value in table.Values) { ValueTuple valueTuple = TamingIds.Resolve(value); int item = valueTuple.Item1; int item2 = valueTuple.Item2; string text = (Registered.ContainsKey(value.Species) ? "REGISTERED" : "NOT registered (boot-time only — see setup warnings / relaunch)"); string text2 = TamingFoods.RecipeUid(value.Species); object obj; if (localPlayerCharacter == null) { obj = null; } else { CharacterInventory inventory = localPlayerCharacter.Inventory; obj = ((inventory != null) ? inventory.RecipeKnowledge : null); } string text3 = (((Object)obj != (Object)null) ? (localPlayerCharacter.Inventory.RecipeKnowledge.IsRecipeLearned(text2) ? "LEARNED" : "not learned") : "no player"); Plugin.Log.LogMessage((object)(string.Format("[TAMEFOOD] '{0}': chow {1} '{2}' ← {3}[{4}] ×{5}, ", value.Species, item, value.ChowName, value.Station, string.Join(" + ", value.Ingredients), value.ResultCount) + $"scroll {item2}, dropChance {value.DropChance:0.##}, uid '{text2}' — {text}, recipe {text3}")); } } } internal static class TamingFoodTable { private static readonly TableLoader _loader = new TableLoader("TamingFoods.json", "[TAMEFOOD]", "taming-food species entry", "taming-food species entries", (Func, Dictionary>)TamingFoods.Parse, (Func, Dictionary, Dictionary>)TamingFoods.Merge, "replaces per-species", (Func, Action, Dictionary>)PruneIdCollisions, (Func, string>)null, " NOTE: item/recipe registration is boot-time — retunes (names, chances, radius targets) apply now, but a NEW species' chow/scroll/recipe needs a relaunch."); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[TAMEFOOD]", Validate); internal static Dictionary Table => _axis.Table; internal static TamingEntry Resolve(string speciesId) { return TamingFoods.Resolve(Table, speciesId); } internal static TamingEntry ForChowId(int itemId) { return TamingFoods.ForChowId(Table, itemId); } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { TableValidator.CheckItemTable("[TAMEFOOD]", "TamingFoods.json", "item ref", ItemRefs(), out var checkedRefs, out var misses); Plugin.Log.LogMessage((object)(string.Format("[TAMEFOOD] boot check: {0} taming-food species entr{1}, ", Table.Count, (Table.Count == 1) ? "y" : "ies") + $"{checkedRefs} item ref(s) (category ingredients skip the registry), {misses} unresolved.")); } private static IEnumerable ItemRefs() { foreach (TamingEntry entry in Table.Values) { if (entry.CloneFrom != null) { yield return Ref(entry.Species, "cloneFrom", entry.CloneFrom); } if (entry.ScrollCloneFrom != null) { yield return Ref(entry.Species, "scrollCloneFrom", entry.ScrollCloneFrom); } foreach (TamingItemKey ingredient in entry.Ingredients) { if (ingredient.Category == null) { yield return Ref(entry.Species, "ingredient", ingredient); } } } } private static ItemRef Ref(string species, string field, TamingItemKey key) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown return new ItemRef(key.ItemId, key.Key, $"'{species}': {field} ItemID {key.ItemId}", "'" + species + "': " + field + " '" + key.Key + "'"); } private static Dictionary PruneIdCollisions(Dictionary table, Action warn) { List list = TamingIds.Validate((IEnumerable)table.Values, warn); if (list.Count == table.Count) { return table; } Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (TamingEntry item in list) { dictionary[item.Species] = item; } return dictionary; } } internal static class TauntController { internal const string Owner = "bw"; internal static bool Active => TauntController.CountOwnedBy("bw") > 0; internal static string Forensics => TauntController.ForensicsFor("bw"); internal static void Begin(Character enemy, Character anchor, float seconds) { if ((Object)(object)enemy == (Object)null || seconds <= 0f) { return; } if (PhotonNetwork.isNonMasterClientInRoom) { if (PetProxyClient.RequestTaunt(enemy, seconds)) { Plugin.Log.LogMessage((object)$"[TAUNT] taunt of '{enemy.Name}' ({seconds:F1}s) routed to the master (guest pet)."); } else { Plugin.Log.LogMessage((object)"[TAUNT] skipped on a non-master client — no proxied anchor to route to (P3 gate)."); } } else { TauntController.Hold(anchor, enemy, seconds, BwConfig.Combat.CombatLeashDistance.Value, "bw", (Action)Plugin.Log.LogMessage); } } internal static void Tick() { if (Active) { if (!Plugin.BraceEnableTaunt.Value) { Release("kill-switch"); } else { TauntController.Tick(); } } } internal static void Release(string reason) { TauntController.ReleaseOwnedBy("bw", reason); } internal static void ForceTaunt(Plugin p, Character player, string[] parts) { //IL_0065: Unknown result type (might be due to invalid IL or missing references) Pet activePet = p.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; if ((Object)(object)val == (Object)null || !val.Alive) { Plugin.Log.LogWarning((object)"[TAUNT] taunt: no live anchor to pin onto."); return; } string text = ((parts != null && parts.Length > 1) ? string.Join(" ", parts, 1, parts.Length - 1) : null); CharacterAI val2 = AggroStage.Find(text, ((Component)player).transform.position, 30f, val); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)("[TAUNT] taunt: no enemy AI ≤30m" + ((text != null) ? (" matching '" + text + "'") : "") + ".")); } else { Begin(((CharacterControl)val2).Character, val, 10f); } } internal static void TauntDump(Plugin p) { //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_024a: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)"[TAUNT] ── tauntdump ──"); Plugin.Log.LogMessage((object)($"[TAUNT] config: [Brace] EnableTaunt={Plugin.BraceEnableTaunt.Value} " + $"leash={BwConfig.Combat.CombatLeashDistance.Value:F0}m reassert=0.3s")); string text = p.ActivePet?.SpeciesId; Pet activePet = p.ActivePet; int? obj; if (activePet == null) { obj = null; } else { PetSimulation sim = activePet.Sim; obj = ((sim == null) ? ((int?)null) : sim.State?.LoyaltyValue); } int num = obj ?? (-1); SpecialAttackDef val = default(SpecialAttackDef); if (SpecialAttackTable.TryGet(SpecialAttacks.Table, text, ref val) && (val.TauntMinSeconds > 0f || val.TauntMaxSeconds > 0f)) { Plugin.Log.LogMessage((object)($"[TAUNT] '{text}' 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 (fires on Hunt as One)")); } else { Plugin.Log.LogMessage((object)("[TAUNT] '" + (text ?? "no pet") + "' has no signature-attack taunt axis — Hunt as One never taunts for it.")); } Plugin.Log.LogMessage((object)("[TAUNT] hold: " + Forensics)); Pet activePet2 = p.ActivePet; object obj2; if (activePet2 == null) { obj2 = null; } else { CompanionAnchor anchor = ((Companion)activePet2).Anchor; obj2 = ((anchor != null) ? anchor.Current : null); } Character val2 = (Character)obj2; if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogMessage((object)"[TAUNT] no live anchor — no aggro census."); return; } int num2 = 0; int num3 = 0; foreach (CharacterAI item in AggroStage.AisInRange(((Component)val2).transform.position, 30f, val2)) { num2++; Character val3 = (((Object)(object)item.TargetingSystem != (Object)null) ? item.TargetingSystem.LockedCharacter : null); if ((Object)(object)val3 == (Object)(object)val2) { num3++; } ManualLogSource log = Plugin.Log; Character character = ((CharacterControl)item).Character; log.LogMessage((object)($"[TAUNT] '{((character != null) ? character.Name : null)}' ({Vector3.Distance(((Component)val2).transform.position, ((Component)item).transform.position):F0}m) " + "-> " + (((Object)(object)val3 == (Object)null) ? "no target" : (((Object)(object)val3 == (Object)(object)val2) ? "THE COMPANION" : ("'" + val3.Name + "'"))))); } Plugin.Log.LogMessage((object)$"[TAUNT] census: {num2} hostile AI(s) ≤30m, {num3} locked onto the companion."); } } internal sealed class UseVeto { public readonly string Toast; public readonly string Log; public UseVeto(string toast, string log) { Toast = toast; Log = log; } } internal interface IUseVeto { string Tag { get; } bool Owns(int itemId); UseVeto Check(Item item, Character user); } [HarmonyPatch(typeof(Item), "Use", new Type[] { typeof(Character) })] internal static class ItemUseVetoPatch { private static readonly List Vetoes = new List { new TamingUseVeto(), new BlanketUseVeto() }; internal static bool Prefix(Item __instance, Character _character, ref bool __result) { if ((Object)(object)__instance == (Object)null || (Object)(object)_character == (Object)null) { return true; } foreach (IUseVeto veto in Vetoes) { if (!veto.Owns(__instance.ItemID)) { continue; } try { UseVeto useVeto = veto.Check(__instance, _character); if (useVeto == null) { return true; } Notify.Player(_character, useVeto.Toast); Plugin.Log.LogMessage((object)useVeto.Log); __result = false; return false; } catch (Exception ex) { Plugin.Log.LogWarning((object)(veto.Tag + " Item.Use prefix error (letting vanilla run): " + ex)); return true; } } return true; } } internal static class CaravanVerbs { private const string Tag = "[CARAVAN]"; private const int TreeLineCap = 250; internal static void Register(CommandRegistry c, VerbHost h) { h.Register("caravandump", "Locate the Soroborean Caravanner: NPC identity/position, its spawn-roll candidates (RandomPositionOnStart), and the fast-travel destination table. `caravandump tree` also dumps the full GameObject hierarchy around the merchant rig (Bug 49 forensics; auto-fires when the Character grab fails).", (Action)delegate(VerbContext ctx) { Dump(ctx.Player, string.Equals(ctx.Arg(1), "tree", StringComparison.OrdinalIgnoreCase)); }, "[CARAVAN]", true, true, false, (string)null); h.Register("caravanhere", "Move the caravanner (NPC + cart as a unit) to 2.5m in front of you, ground-snapped. Host/SP only — a guest keeps the old spot.", (Action)delegate(VerbContext ctx) { MoveHere(ctx.Player); }, "[CARAVAN]", true, true, true, (string)null); h.Register("caravanspawn", "caravanspawn — place the caravanner on one of its own baked candidate spawn points (indices from caravandump). Host/SP only.", (Action)delegate(VerbContext ctx) { MoveToSpawnPoint(ctx.Player, ctx.Arg(1)); }, "[CARAVAN]", true, true, true, (string)null); h.Register("caravanreroll", "Re-run the caravanner's OWN vanilla spawn roll (RandomPositionOnStart.InitPosition) — the exact placement the game performs at scene load, without a scene load. Host/SP only: nothing is rebroadcast, so a guest keeps its load-time spot. A repeat of the same spawn point is a legitimate outcome, not a failure.", (Action)delegate(VerbContext ctx) { Reroll(ctx.Player); }, "[CARAVAN]", true, true, true, (string)null); } private static void Reroll(Character player) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: 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_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0104: 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) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) if (!Find(out var _, out var npc, out var rpos) || (Object)(object)rpos == (Object)null) { Plugin.Log.LogWarning((object)"[CARAVAN] no caravanner spawn roll in this scene (no RandomPositionOnStart — fixed placement or no rig)."); return; } Transform val = FirstPlaced(rpos) ?? (((Object)(object)npc != (Object)null) ? ((Component)npc).transform : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[CARAVAN] spawn-roll holder has no placeable transforms — nothing to reroll."); return; } Vector3 position = val.position; rpos.UsedSpawns?.Clear(); rpos.InitPosition(); Vector3 position2 = val.position; Plugin.Log.LogMessage((object)(string.Format("{0} reroll: ({1:F1}, {2:F1}, {3:F1}) -> ", "[CARAVAN]", position.x, position.y, position.z) + $"({position2.x:F1}, {position2.y:F1}, {position2.z:F1}), moved {Vector3.Distance(position, position2):F1}m, " + $"landed on spawn[{NearestSpawnIndex(rpos, position2)}]" + ((Vector3.Distance(position, position2) < 0.5f) ? " — SAME spot: the roll is uniform over the baked points, so a repeat is a legitimate outcome." : "."))); Plugin.Log.LogMessage((object)"[CARAVAN] reroll: host/SP only — no SyncRandomSpawn is rebroadcast, so a guest still sees the caravanner where it was at load."); Dump(player); } private static int NearestSpawnIndex(RandomPositionOnStart rpos, Vector3 pos) { //IL_0026: 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) Transform[] spawnPoints = rpos.SpawnPoints; if (spawnPoints == null) { return -1; } int result = -1; float num = float.MaxValue; for (int i = 0; i < spawnPoints.Length; i++) { if (!((Object)(object)spawnPoints[i] == (Object)null)) { float num2 = Vector3.Distance(spawnPoints[i].position, pos); if (num2 < num) { num = num2; result = i; } } } return result; } private static bool Find(out MerchantFastTravel mft, out Character npc, out RandomPositionOnStart rpos) { mft = Object.FindObjectOfType(); npc = null; rpos = (((Object)(object)mft != (Object)null) ? ((Component)mft).GetComponentInParent() : null); if ((Object)(object)mft != (Object)null) { Transform val = (((Object)(object)rpos != (Object)null) ? ((Component)rpos).transform : ((Component)mft).transform.root); npc = ((Component)val).GetComponentInChildren(true); if ((Object)(object)npc == (Object)null) { npc = ((Component)mft).GetComponentInParent(); } } return (Object)(object)mft != (Object)null; } private static void Dump(Character player, bool tree = false) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: 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_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_0280: Unknown result type (might be due to invalid IL or missing references) //IL_028f: Unknown result type (might be due to invalid IL or missing references) //IL_029e: Unknown result type (might be due to invalid IL or missing references) //IL_02b5: Unknown result type (might be due to invalid IL or missing references) //IL_02bd: Unknown result type (might be due to invalid IL or missing references) if (!Find(out var mft, out var npc, out var rpos)) { Plugin.Log.LogMessage((object)"[CARAVAN] no MerchantFastTravel in this scene — no caravanner here."); DumpMerchantRoster(player); return; } if (tree || (Object)(object)npc == (Object)null) { DumpTree(((Object)(object)rpos != (Object)null) ? ((Component)rpos).transform : ((Component)mft).transform.root); } if ((Object)(object)npc != (Object)null) { Vector3 position = ((Component)npc).transform.position; Plugin.Log.LogMessage((object)(string.Format("{0} NPC '{1}' uid={2} faction={3} alive={4} ", "[CARAVAN]", npc.Name, npc.UID, npc.Faction, npc.Alive) + $"pos=({position.x:F1}, {position.y:F1}, {position.z:F1}) dist={Vector3.Distance(position, ((Component)player).transform.position):F1}m")); } else { Plugin.Log.LogMessage((object)"[CARAVAN] MerchantFastTravel found but NO Character above it — dumping what's known."); } if ((Object)(object)rpos == (Object)null) { Plugin.Log.LogMessage((object)"[CARAVAN] no RandomPositionOnStart above the NPC — fixed placement (city), nothing to roll."); } else { Plugin.Log.LogMessage((object)(string.Format("{0} spawn roll: '{1}' uid={2}, ", "[CARAVAN]", ((Object)((Component)rpos).gameObject).name, rpos.UID) + $"{((rpos.TransformsToPlace != null) ? rpos.TransformsToPlace.Length : 0)} transform(s) to place, " + "UsedSpawns=[" + string.Join(", ", ToStrings(rpos.UsedSpawns)) + "]")); Transform[] spawnPoints = rpos.SpawnPoints; if (spawnPoints == null || spawnPoints.Length == 0) { Plugin.Log.LogMessage((object)"[CARAVAN] no baked SpawnPoints (?)"); } else { for (int i = 0; i < spawnPoints.Length; i++) { if ((Object)(object)spawnPoints[i] == (Object)null) { Plugin.Log.LogMessage((object)string.Format("{0} spawn[{1}]: ", "[CARAVAN]", i)); continue; } Vector3 position2 = spawnPoints[i].position; float num = (((Object)(object)npc != (Object)null) ? Vector3.Distance(position2, ((Component)npc).transform.position) : (-1f)); Plugin.Log.LogMessage((object)(string.Format("{0} spawn[{1}]: ({2:F1}, {3:F1}, {4:F1}) ", "[CARAVAN]", i, position2.x, position2.y, position2.z) + $"dist {Vector3.Distance(position2, ((Component)player).transform.position):F0}m from you" + ((num >= 0f && num < 8f) ? " <- CURRENT (nearest to the NPC)" : ""))); } } } foreach (KeyValuePair destination in MerchantFastTravel.Destinations) { Plugin.Log.LogMessage((object)(string.Format("{0} fast travel: scene '{1}' -> {2} ", "[CARAVAN]", destination.Key, (object)(AreaEnum)destination.Value.DestinationID) + $"(id {destination.Value.DestinationID}), lastRefresh t={destination.Value.LastRefreshTime:F1}h")); } } private static void DumpTree(Transform root) { Plugin.Log.LogMessage((object)("[CARAVAN] ── rig hierarchy under '" + ((Object)((Component)root).gameObject).name + "' ──")); int lines = 0; DumpNode(root, 0, ref lines); if (lines >= 250) { Plugin.Log.LogMessage((object)string.Format("{0} … tree truncated at {1} lines.", "[CARAVAN]", 250)); } } private static void DumpNode(Transform t, int depth, ref int lines) { if (++lines > 250) { return; } List list = new List(); Component[] components = ((Component)t).GetComponents(); foreach (Component val in components) { if ((Object)(object)val != (Object)null && !(val is Transform)) { list.Add(((object)val).GetType().Name); } } Plugin.Log.LogMessage((object)("[CARAVAN] " + new string(' ', depth * 2) + "'" + ((Object)((Component)t).gameObject).name + "' " + string.Format("active={0} [{1}]", ((Component)t).gameObject.activeSelf, string.Join(", ", list.ToArray())))); for (int j = 0; j < t.childCount; j++) { DumpNode(t.GetChild(j), depth + 1, ref lines); } } private static void DumpMerchantRoster(Character player) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Invalid comparison between Unknown and I4 //IL_0059: 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_007e: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return; } int num = 0; foreach (Character value in instance.Characters.Values) { if (!((Object)(object)value == (Object)null) && (int)value.Faction == 7) { num++; Plugin.Log.LogMessage((object)(string.Format("{0} merchant '{1}' uid={2} ", "[CARAVAN]", value.Name, value.UID) + $"dist={Vector3.Distance(((Component)value).transform.position, ((Component)player).transform.position):F1}m")); } } Plugin.Log.LogMessage((object)string.Format("{0} ({1} Merchants-faction character(s) in the roster)", "[CARAVAN]", num)); } private static void MoveHere(Character player) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: 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_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008d: 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_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) if (!Find(out var _, out var npc, out var rpos) || ((Object)(object)npc == (Object)null && (Object)(object)rpos == (Object)null)) { Plugin.Log.LogWarning((object)"[CARAVAN] no caravanner rig in this scene — nothing to move."); return; } Transform val = (((Object)(object)npc != (Object)null) ? ((Component)npc).transform : FirstPlaced(rpos)); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[CARAVAN] spawn-roll holder has no placeable transforms — nothing to move."); return; } Vector3 val2 = SnapToGround(((Component)player).transform.position + ((Component)player).transform.forward * 2.5f); MoveUnit(npc, rpos, val2 - val.position); Plugin.Log.LogMessage((object)(string.Format("{0} moved the caravanner to ({1:F1}, {2:F1}, {3:F1}) ", "[CARAVAN]", val2.x, val2.y, val2.z) + "(" + (((Object)(object)rpos != (Object)null) ? "unit move via RandomPositionOnStart" : "no spawn-roll holder — Character.Teleport") + (((Object)(object)npc == (Object)null) ? ", Character-less rig" : "") + ").")); } private static Transform FirstPlaced(RandomPositionOnStart rpos) { if ((Object)(object)rpos == (Object)null || rpos.TransformsToPlace == null) { return null; } Transform[] transformsToPlace = rpos.TransformsToPlace; foreach (Transform val in transformsToPlace) { if ((Object)(object)val != (Object)null) { return val; } } return null; } private static void MoveToSpawnPoint(Character player, string arg) { //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) if (!int.TryParse(arg ?? "", out var result)) { Plugin.Log.LogWarning((object)"[CARAVAN] usage: caravanspawn (list the indices with caravandump)."); return; } if (!Find(out var _, out var npc, out var rpos) || (Object)(object)rpos == (Object)null) { Plugin.Log.LogWarning((object)"[CARAVAN] no caravanner spawn roll in this scene (no RandomPositionOnStart — fixed placement or no rig)."); return; } Transform[] spawnPoints = rpos.SpawnPoints; if (spawnPoints == null || result < 0 || result >= spawnPoints.Length || (Object)(object)spawnPoints[result] == (Object)null) { Plugin.Log.LogWarning((object)(string.Format("{0} spawn index {1} out of range/null ", "[CARAVAN]", result) + $"(0..{((spawnPoints != null) ? (spawnPoints.Length - 1) : (-1))} — see caravandump).")); return; } Transform val = FirstPlaced(rpos) ?? (((Object)(object)npc != (Object)null) ? ((Component)npc).transform : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[CARAVAN] spawn-roll holder has no placeable transforms — nothing to move."); return; } MoveUnit(npc, rpos, spawnPoints[result].position - val.position); Plugin.Log.LogMessage((object)string.Format("{0} placed the caravanner on spawn[{1}] at {2}.", "[CARAVAN]", result, spawnPoints[result].position)); } private static void MoveUnit(Character npc, RandomPositionOnStart rpos, Vector3 delta) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0040: 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_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)rpos == (Object)null || rpos.TransformsToPlace == null || rpos.TransformsToPlace.Length == 0) { if ((Object)(object)npc == (Object)null) { Plugin.Log.LogWarning((object)"[CARAVAN] nothing movable (no placed transforms, no Character)."); } else { npc.Teleport(((Component)npc).transform.position + delta, ((Component)npc).transform.rotation); } return; } Transform[] transformsToPlace = rpos.TransformsToPlace; foreach (Transform val in transformsToPlace) { if ((Object)(object)val != (Object)null) { val.position += delta; } } Character[] componentsInChildren = ((Component)rpos).GetComponentsInChildren(true); foreach (Character val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null) { val2.CheckLoadPosition(true); } } } private static Vector3 SnapToGround(Vector3 p) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: 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_0043: 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_0022: 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_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) NavMeshHit val = default(NavMeshHit); if (NavMesh.SamplePosition(p, ref val, 6f, -1)) { return new Vector3(p.x, ((NavMeshHit)(ref val)).position.y, p.z); } RaycastHit val2 = default(RaycastHit); if (Physics.Raycast(p + Vector3.up * 1.2f, Vector3.down, ref val2, 30f, -1, (QueryTriggerInteraction)1)) { return new Vector3(p.x, ((RaycastHit)(ref val2)).point.y, p.z); } return p; } private static string[] ToStrings(List list) { if (list == null) { return new string[0]; } string[] array = new string[list.Count]; for (int i = 0; i < list.Count; i++) { array[i] = list[i].ToString(); } return array; } } internal static class CombatVerbs { internal static void Register(CommandRegistry c, VerbHost h, Plugin plugin) { h.Register("combatcheck", "Player combat state + the anchor weld health.", (Action)delegate(VerbContext ctx) { BwDiagnostics.CombatCheck(plugin, ctx.Player); }, "[COMBAT]", true, true, false, "[COMBAT] no player."); c.Register("special", "Fire the pet's Hunt-as-One signature attack (quickslot skill; no keybind since Bug 26).", (Action)delegate { PetCommands.FireSpecialAttack(plugin); }); h.Register("boltdump", "Ranged-special pipeline: live shooter census -> captured rig -> table row.", (Action)delegate(VerbContext ctx) { PetSpecialAttack.BoltDump(plugin, ctx.Player); }, "[BOLT]", true, true, false, (string)null); h.Register("firebolt", "Force-fire the captured bolt at the target (skips cooldown/anim; deals no damage).", (Action)delegate(VerbContext ctx) { PetSpecialAttack.FireBolt(plugin, ctx.Player); }, "[BOLT]", true, true, false, (string)null); c.Register("synergydump", "Synergy pipeline: config -> open attempt -> last close -> status stacks -> damage readbacks.", (Action)delegate { HuntSynergy.Dump(plugin); }); h.Register("synergy", "Dev-set the Synergy stack count ('synergy <0-4>') — FTK testable without landing hits.", (Action)delegate(VerbContext ctx) { HuntSynergy.DevSetStacks(ctx.Player, ctx.Parts); }, "[SYNERGY]", true, true, false, (string)null); h.Register("ftk", "Cast the For the Kill skill body (skill-free; honors the kill-switch).", (Action)delegate(VerbContext ctx) { ForTheKillSkill.Cast(ctx.Player); }, "[FTK]", true, true, false, (string)null); c.Register("ftkdump", "For-the-Kill pipeline: table -> stacks -> the mults a cast now would use -> cooldown -> last cast.", (Action)delegate { ForTheKillSkill.Dump(plugin); }); c.Register("killfavordump", "Killing-favor buff: applied (stat, amount) vs the game status' live state.", (Action)delegate { KillFavorBuff.Dump(Plugin.LocalPlayer); }); c.Register("brace", "Force-enter the braced stance (Brace-kind species only; bypasses the cooldown).", (Action)delegate { BraceDriver.ForceBrace(plugin); }); c.Register("bracedump", "Brace pipeline: config -> route for the active species -> live window state -> patch attach.", (Action)delegate { BraceDriver.BraceDump(plugin); }); h.Register("taunt", "Dev-force a 10s taunt onto the nearest (or named) enemy ('taunt [name]').", (Action)delegate(VerbContext ctx) { TauntController.ForceTaunt(plugin, ctx.Player, ctx.Parts); }, "[TAUNT]", true, true, false, (string)null); c.Register("tauntdump", "Taunt pipeline: config -> table axis -> live hold state (asserts, steal-corrections).", (Action)delegate { TauntController.TauntDump(plugin); }); c.Register("warddump", "Ward-share pipeline: config -> resolved status ids -> player/anchor live state -> anchor stat readback.", (Action)delegate { WardShare.Dump(plugin); }); c.Register("lanterndump", "Lantern-share pipeline: config/resolution -> player status census + Light census (the identifier/FX discovery log) -> detected status -> body -> clone state.", (Action)delegate { LanternShare.Dump(plugin); }); h.Register("echo", "Fire the pet's skill echo headless ('echo [skill name]', default Puncture; bypasses the echo cooldown).", (Action)delegate(VerbContext ctx) { SkillEchoDriver.DevEcho(ctx.Player, ctx.Parts); }, "[ECHO]", true, true, false, (string)null); c.Register("echodump", "Skill-echo pipeline: config -> patch attach -> roster + active species' numbers -> cue audit -> last fire.", (Action)delegate { SkillEchoDriver.Dump(plugin); }); c.Register("petcommand", "Toggle the Command Pet Engage/Disengage stance.", (Action)delegate { PetCommands.TogglePetCommand(plugin); }); c.Register("engage", "Command the pet to engage your locked/front target.", (Action)delegate { PetCommands.EngagePet(plugin); }); c.Register("disengage", "Command the pet to disengage and return to you.", (Action)delegate { PetCommands.DisengagePet(plugin); }); c.Register("petheal", "Heal the anchor to full.", (Action)delegate { Plugin.Log.LogMessage((object)((plugin.ActivePet != null && ((Companion)plugin.ActivePet).Anchor.Heal()) ? "[ANCHOR] healed to full." : "[ANCHOR] no live anchor to heal.")); }); h.Register("gift", "Cast the Pet Gift skill body (skill-free; honors the kill-switch).", (Action)delegate(VerbContext ctx) { PetGifting.Cast(ctx.Player); }, "[GIFT]", true, true, false, (string)null); c.Register("giftdump", "Gift pipeline: table -> resolved ids -> loyalty-lerped mix -> last roll.", (Action)delegate { PetGifting.Dump(plugin); }); h.Register("fletchdump", "Fletch pipeline: table -> registration -> held feathers/arrows with live damage readback.", (Action)delegate(VerbContext ctx) { GiveVerbs.FletchDump(ctx.Player); }, "[FLETCH]", true, true, false, (string)null); h.Register("givefeather", "Spawn a feather-quality stack into the pouch ('givefeather [loyalty|quality] [qty]', default quality = live loyalty else 50).", (Action)delegate(VerbContext ctx) { GiveVerbs.GiveFeather(plugin, ctx.Player, ctx.Parts); }, "[FLETCH]", true, true, false, (string)null); h.Register("recipedump", "Recipe forensics: fletch/vanilla-arrow rows, ALL bw.*/beastwhispering.* recipes with station+result+learned state, the full learned-recipe count/names (Bug-47 bandage/linen/cloth suspects flagged), and any live CraftingMenu cache.", (Action)delegate(VerbContext ctx) { RecipeVerbs.RecipeDump(ctx.Player); }, "[RECIPE]", true, true, false, (string)null); c.Register("petpanel", "Dump the Companion-tab display model to the log.", (Action)delegate { PetPanelData.Dump(); }); c.Register("pettab", "Open the injected Companion menu tab directly.", (Action)delegate { if (!PetTabInjector.ShowFor(Plugin.LocalPlayer)) { Plugin.Log.LogWarning((object)"[PETTAB] no injected Companion tab for the local player yet (no CharacterUI has run Awake, or injection failed — see [PETTAB] lines above)."); } }); c.Register("reloadcfg", "Re-read the whole .cfg from disk + re-apply effects now.", (Action)delegate { plugin.ReloadCfg(); }); SkillVerbs.RegisterAll(c, Plugin.Log, (Func)(() => Plugin.LocalPlayer)); h.Register("huntasonesync", "Dump the live Hunt-as-One CastType/CastModifier stamp.", (Action)delegate(VerbContext ctx) { BwDiagnostics.HuntAsOneSyncDump(ctx.Player); }, "[HUNTASONE]", true, true, false, "[HUNTASONE] no player."); c.Register("learnskills", "Grant the Beastwhispering skills directly (cheat).", (Action)delegate { plugin.LearnSkills(); }); } } internal static class DiagVerbs { internal static void Register(CommandRegistry c, VerbHost h, Plugin plugin) { c.Register("petstatus", "The pet's whole Core state + applied buffs/status-icons.", (Action)delegate { BwDiagnostics.PetStatusLog(plugin); }); c.Register("diag", "Consolidated snapshot (location/vitals/combat/nearby AI/pet).", (Action)delegate { plugin.Diag(); }); c.Register("statdump", "Species-stats pipeline: captured vs tuned vs anchor readback.", (Action)delegate { PetSystems.StatDumpVerb(plugin); }); c.Register("mealdump", "Food-hex pipeline: entry -> history -> next Hunt-as-One build-up.", (Action)delegate { FoodHexTable.MealDump(plugin); }); c.Register("sigildump", "Sigil pipeline: registry -> nearby world items (ItemID discovery) -> live detection -> next-hit mix.", (Action)delegate { SigilSense.SigilDump(plugin); }); c.Register("geardump", "Equipped-gear pipeline: table -> worn items -> matched entries -> resolved pet effects.", (Action)delegate { PetGearSense.Dump(plugin, Plugin.LocalPlayer); }); c.Register("scentdump", "Scent pipeline: table/resolution -> raw nearby world items (ItemID discovery) -> tracker state -> channels.", (Action)delegate { ScentSniffer.ScentDump(plugin); }); c.Register("scavengedump", "Scavenge pipeline: table -> active pet's tier/slots -> last armed container fill.", (Action)delegate { ScavengeBonus.Dump(plugin); }); h.Register("scavengesim", "scavengesim [n] [name-filter] [radius] — simulate the nearest container's drop tables n times (default 1000) with and without the active pet's scavenge dice; prints per-table and total average value. Opens nothing, changes nothing.", (Action)delegate(VerbContext ctx) { ScavengeSim.Run(plugin, ctx.Player, ctx.Parts, Plugin.Log); }, "[SCAVENGE]", true, true, false, (string)null); c.Register("sniff", "Force a scent scan right now (skips the interval; honors the kill-switch).", (Action)delegate { if (plugin.ActivePet != null) { ScentSniffer.Tick(plugin.ActivePet, Plugin.LocalPlayer); } else { Plugin.Log.LogMessage((object)"[SCENT] no active pet — nothing to sniff with."); } }); c.Register("buffdump", "Applied bond buffs + bag perk + live player-stat readback.", (Action)delegate { PetPassiveBuff.Dump(Plugin.LocalPlayer); PetBagPerk.Dump(Plugin.LocalPlayer); }); c.Register("statusicons", "Status-icon pipeline: inputs -> wanted -> the player's actual statuses.", (Action)delegate { Character localPlayer = Plugin.LocalPlayer; PetSimulation sim = plugin.ActivePet?.Sim; Pet activePet = plugin.ActivePet; object obj; if (activePet == null) { obj = null; } else { ScentTracker scent = activePet.Scent; obj = ((scent != null) ? scent.Current : null); } PetStatusEffects.Dump(localPlayer, sim, obj != null); }); c.Register("pethealth", "The anchor's combat-HP summary.", (Action)delegate { Plugin.Log.LogMessage((object)("[ANCHOR] " + ((plugin.ActivePet != null) ? ((Companion)plugin.ActivePet).Anchor.HealthSummary() : "no active pet."))); }); c.Register("anchorphys", "Anchor<->player collision: colliders, live IgnoreCollision readback, re-stamp counters.", (Action)delegate { Plugin.Log.LogMessage((object)((plugin.ActivePet != null) ? ((Companion)plugin.ActivePet).Anchor.PhysicsSummary() : "[ANCHORPHYS] no active pet.")); }); c.Register("animdump", "Dump the pet's Animator(s): controller, flags, params.", (Action)delegate { BwDiagnostics.AnimDump(plugin); }); c.Register("posdump", "Dump the pet body's positions.", (Action)delegate { CompanionBody obj = ((Companion)(plugin.ActivePet?)).Body; if (obj != null) { obj.DumpPositions(); } }); h.Register("groundprobe", "Measure the navmesh at the player (nearest-poly hits by radius).", (Action)delegate(VerbContext ctx) { BwDiagnostics.GroundProbe(plugin, ctx.Player); }, "[PROBE]", true, false, false, "[PROBE] no player."); c.Register("compdump", "List every component on the pet hierarchy (+ enabled state).", (Action)delegate { BwDiagnostics.CompDump(plugin); }); c.Register("drifttest", "Find the root-motion drift bone.", (Action)delegate { CompanionBody obj = ((Companion)(plugin.ActivePet?)).Body; if (obj != null) { obj.FindDrift(); } }); c.Register("visdump", "Per-renderer draw-readiness dump on the live puppet.", (Action)delegate { if ((Object)(object)((Companion)(plugin.ActivePet?)).Body != (Object)null) { BodyFactory.VisDump(((Component)((Companion)plugin.ActivePet).Body).gameObject, "visdump"); } else { Plugin.Log.LogWarning((object)"[VIS] no active pet body."); } }); c.Register("visfix", "Conservative renderer repair on the live puppet + re-dump.", (Action)delegate { if ((Object)(object)((Companion)(plugin.ActivePet?)).Body != (Object)null) { BodyFactory.VisRepair(((Component)((Companion)plugin.ActivePet).Body).gameObject); BodyFactory.VisDump(((Component)((Companion)plugin.ActivePet).Body).gameObject, "post-visfix"); } else { Plugin.Log.LogWarning((object)"[VIS] no active pet body."); } }); c.Register("donorspot", "Log the scene's live AI roster as paste-ready DonorScenes lines.", (Action)delegate { DonorSpotVerb.DonorSpot(plugin); }); c.Register("yaw", "Live rig-facing tune ('yaw '); persist in [Pet] SpeciesYawOffsets.", (Action)delegate(string[] parts) { float result; if ((Object)(object)((Companion)(plugin.ActivePet?)).Body == (Object)null) { Plugin.Log.LogWarning((object)"[CMD] yaw: no active pet body."); } else if (parts.Length < 2 || !float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out result)) { Plugin.Log.LogWarning((object)("[CMD] usage: yaw (current: " + (float.IsNaN(((Companion)plugin.ActivePet).Body.YawOffset) ? $"{Plugin.ModelYawOffset.Value:F0} (config default)" : $"{((Companion)plugin.ActivePet).Body.YawOffset:F0}") + ")")); } else { ((Companion)plugin.ActivePet).Body.YawOffset = result; plugin.Lifecycle._sessionYawOverrides[((Companion)plugin.ActivePet).Body.SpeciesId] = result; Plugin.Log.LogMessage((object)$"[PET] yaw override for this body ('{((Companion)plugin.ActivePet).Body.SpeciesId}') = {result:F0}° — applies to future re-forms of this species this session; persist it in [Pet] SpeciesYawOffsets to survive a relaunch too."); } }); } } internal static class LifecycleVerbs { internal static void Register(CommandRegistry c, VerbHost h, Plugin plugin) { //IL_02ce: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Unknown result type (might be due to invalid IL or missing references) //IL_02e4: Unknown result type (might be due to invalid IL or missing references) //IL_02f5: Unknown result type (might be due to invalid IL or missing references) //IL_030c: Expected O, but got Unknown c.Register(new string[2] { "tame", "clonevisual" }, "Tame/clone the nearest wild creature (dev).", (Action)delegate { plugin.Tame(); }); c.Register("tamecached", "Tame a pet from the session body-template cache ('tamecached '; fill it via expedition).", (Action)delegate(string[] parts) { plugin.Lifecycle.TameCached(parts); }); c.Register("release", "Release the pet: full teardown + save cleared — IRREVERSIBLE, so it needs confirming ('release confirm', or call 'release' twice within 30s).", (Action)delegate(string[] parts) { plugin.ReleasePet(parts.Length > 1 && parts[1].Equals("confirm", StringComparison.OrdinalIgnoreCase)); }); c.Register("feed", "Feed the first diet-matching item ('feed force' bypasses satiation; argless 'feeditem' is the same thing).", (Action)delegate(string[] parts) { plugin.Feed(parts.Length > 1 && parts[1].Equals("force", StringComparison.OrdinalIgnoreCase)); }); c.Register("recall", "Recall/re-place the pet at your feet (F9).", (Action)delegate { plugin.RecallPet(); }); h.Register("reformtest", "Clone the nearest creature WITHOUT consuming it (no-consume path).", (Action)delegate(VerbContext ctx) { CombatStageVerbs.ReformTest(plugin, ctx.Player); }, "[PET]", true, true, false, (string)null); c.Register("resummon", "Re-spawn the pet's visible body.", (Action)delegate { CompanionBody obj = ((Companion)(plugin.ActivePet?)).Body; if (obj != null) { obj.Resummon(); } }); c.Register("dietdump", "Resolved diet + satiation + a dry-run ruling over inventory.", (Action)delegate { PetFeeder.DietDump(Plugin.LocalPlayer); }); c.Register("foodcats", "Food-category tags: canonical name -> live tag name/UID resolution.", (Action)delegate { FoodCategoryTags.Dump(); }); h.Register("bandage", "Apply the first bandage in your inventory to the pet (headless — the right-click 'Bandage ' action's pipeline; heals the anchor player-identically, no player buff).", (Action)delegate(VerbContext ctx) { PetBandage.ApplyFirstFromInventory(ctx.Player); }, "[BANDAGE]", true, true, false, (string)null); c.Register("bandagedump", "Bandage-to-pet pipeline: flags -> resolved item ids + heal status -> live anchor state -> last action.", (Action)delegate { PetBandage.Dump(plugin); }); c.Register("tamingdump", "Taming-food table + resolved ids + registration status.", (Action)delegate { TamingFoodSetup.Dump(); }); h.Register("givechow", "Spawn a species' taming chow into the pouch ('givechow [species]').", (Action)delegate(VerbContext ctx) { GiveVerbs.GiveTamingItem(ctx.Player, ctx.Parts, scroll: false); }, "[TAMEFOOD]", true, true, false, (string)null); h.Register("givescroll", "Spawn a species' recipe scroll into the pouch ('givescroll [species]').", (Action)delegate(VerbContext ctx) { GiveVerbs.GiveTamingItem(ctx.Player, ctx.Parts, scroll: true); }, "[TAMEFOOD]", true, true, false, (string)null); h.Register("giveblanket", "Spawn a registered blanket into the pouch ('giveblanket [heat|cool]').", (Action)delegate(VerbContext ctx) { GiveVerbs.GiveBlanket(ctx.Player, ctx.Parts); }, "[BLANKET]", true, true, false, (string)null); c.Register("tempdump", "The whole temperature pipeline (ambient/band/blanket/stage/drain).", (Action)delegate { PetComfortTable.TempDump(plugin); }); c.Register("weatherdump", "Weather-food pipeline: table -> resolution -> drink buff -> next eval.", (Action)delegate { WeatherFoodTable.WeatherDump(plugin); }); c.Register("bufffooddump", "Buff-food pipeline: flags -> table -> active slot -> damage-multiplier decomposition.", (Action)delegate { BuffFoodTable.BuffFoodDump(plugin); }); CommonVerbs.RegisterAll(h, Plugin.Log, new CommonVerbsOptions { Exclude = { "sethp" }, Exclude = { "groundprobe" }, PetTarget = delegate { Pet activePet = plugin.ActivePet; if (activePet == null) { return (Character)null; } CompanionAnchor anchor = ((Companion)activePet).Anchor; return (anchor == null) ? null : anchor.Current; } }); h.Register("feeditem", "Feed the first matching pouch/bag item to the active pet ('feeditem [force] ', force skips satiation; with NO name it is an alias for 'feed'; a same-batch unequip isn't reflected until the next poll).", (Action)delegate(VerbContext ctx) { GiveVerbs.FeedItemVerb(ctx.Player, ctx.Parts); }, "[FEEDITEM]", true, true, false, (string)null); h.Register("sigilspawn", "Spawn a sigil world item at your feet, vanilla Summon-style ('sigilspawn ').", (Action)delegate(VerbContext ctx) { SigilSense.SigilSpawn(ctx.Player, ctx.Parts); }, "[SIGIL]", true, true, false, (string)null); h.Register("petsigil", "The pet drops its own 0.75x sigil circle at its feet ('petsigil [here]'; 'here' drops at YOU for geometry tests; bypasses the sigil cooldown).", (Action)delegate(VerbContext ctx) { PetSigilCommand.PetSigilVerb(plugin, ctx.Player, ctx.Parts); }, "[PETSIGIL]", true, true, false, (string)null); h.Register("setloyalty", "Set pet loyalty ('setloyalty <0-100>'; 0 = abandonment and needs 'setloyalty 0 force').", (Action)delegate(VerbContext ctx) { TestStateVerbs.SetLoyaltyVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("sethunger", "Set pet hunger ('sethunger <0-1.5>', fraction of the hunger-day; 1.0 = starving).", (Action)delegate(VerbContext ctx) { TestStateVerbs.SetHungerVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("setcourage", "Set the species-relic stacks (Pearlbird's Courage / Leyline Figment) ('setcourage <0-5>'; stats re-derive now).", (Action)delegate(VerbContext ctx) { TestStateVerbs.SetCourageVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("hr", "Set the food health-recovery regen ('hr <0-5> [seconds]', 0 = clear; default 600s) — the feed-free staging seam.", (Action)delegate(VerbContext ctx) { TestStateVerbs.SetHealthRecoveryVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("simskip", "Fast-forward the pet sim ('simskip ', max 86400) — decay/expiry/drain apply honestly.", (Action)delegate(VerbContext ctx) { TestStateVerbs.SimSkipVerb(plugin, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("sethp", "Set HP ('sethp '; floor 1 — death needs real damage).", (Action)delegate(VerbContext ctx) { TestStateVerbs.SetHpVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); c.Register("expedition", "Round-trip to an oversized donor scene and cache body templates ('expedition '; no args = status + cache + manifest + config; host/offline only).", (Action)delegate(string[] parts) { plugin.Expedition(parts); }); c.Register("templateclear", "Clear the session body-template cache (logs the count freed).", (Action)delegate { Plugin.Log.LogMessage((object)$"[TEMPLATE] cleared {BodyTemplateCache.ClearAllAndForgetMisses()} cached body template(s)."); }); c.Register("templateprobe", "Per cached template: species + first SkinnedMeshRenderer sharedMesh.isReadable (defensive read).", (Action)delegate { Plugin.Log.LogMessage((object)BodyTemplateCache.Probe()); }); h.Register("aggro", "Make the nearest wild AI attack ('aggro [name]', 30m).", (Action)delegate(VerbContext ctx) { CombatStageVerbs.AggroVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("pacify", "Calm every wild AI in radius ('pacify [radius]', default 30m).", (Action)delegate(VerbContext ctx) { CombatStageVerbs.PacifyVerb(plugin, ctx.Player, ctx.Parts); }, "[DEV]", true, true, false, (string)null); h.Register("petdown", "Overkill the pet's anchor via the vanilla pipeline — runs the REAL downed flow.", (Action)delegate { CombatStageVerbs.PetDownVerb(plugin); }, "[DEV]", true, true, false, (string)null); } } internal static class MarenVerbs { internal static void Register(CommandRegistry c, VerbHost h) { c.Register("marenstatus", "Maren's StoryKit state (template/placement/live).", (Action)delegate { NpcRegistry.StatusDump("bw.maren"); }); c.Register("marenspawn", "Spawn Maren at her stamped placement.", (Action)delegate { NpcRegistry.TrySpawn("bw.maren"); }); c.Register("marendespawn", "Despawn Maren.", (Action)delegate { NpcRegistry.Despawn("bw.maren"); }); h.Register("marenhere", "Stamp YOUR position/facing as Maren's home (persists to config) + respawn her there.", (Action)delegate(VerbContext ctx) { MarenSetup.StampHere(ctx.Player); }, "[MAREN]", true, true, true, (string)null); } } internal static class ReconVerbs { internal static void Register(CommandRegistry c, VerbHost h, Plugin plugin) { c.Register(new string[2] { "musiccheck", "musicdump" }, "Music/combat-manager gate + subscriber dump.", (Action)delegate { MusicRecon.Dump(); }); c.Register("harvest", "Additive-load a donor scene and clone a creature ('harvest ').", (Action)delegate(string[] parts) { if (parts.Length < 3) { Plugin.Log.LogWarning((object)"[CMD] usage: harvest "); } else { plugin.Lifecycle.Harvest(parts[1], string.Join(" ", parts, 2, parts.Length - 2)); } }); h.Register("anchortest", "Anchor Phase-0 spike (spawn + watch timeline).", (Action)delegate(VerbContext ctx) { if (AnchorSpike.Spawn(ctx.Player)) { ((MonoBehaviour)plugin).StartCoroutine(AnchorSpike.WatchTimeline()); } }, "[ANCHOR]", true, true, false, (string)null); c.Register("anchorstatus", "Anchor spike status.", (Action)delegate { AnchorSpike.Status(); }); c.Register("anchorforce", "Force the anchor's legacy gates.", (Action)delegate { AnchorSpike.ForceLegacyGates(); }); c.Register("anchorclear", "Clear the anchor spike.", (Action)delegate { AnchorSpike.Clear(); }); } } internal static class SystemVerbs { internal static void Register(CommandRegistry c, Plugin plugin) { c.Register("registrydump", "Dump items/statuses/scenes snapshots for the offline `bwspecies check` tool.", (Action)delegate { RegistryDump.Run(); }); c.Register("speciesaudit", "Cross-table species integrity check (tamable vs diet/donor/comfort/...).", (Action)delegate { Plugin.RunSpeciesAudit(); }); c.Register("selftest", "Run the Core self-test now (F10).", (Action)delegate { SelfTest.Run(Plugin.Log); }); DonorVerbs.RegisterAll(c); c.Register("petsound", "Species-voice chain dump + plays attack/hurt/death in sequence.", (Action)delegate { BwDiagnostics.PetSoundProbe(plugin); }); c.Register("vocals", "Toggle the body-synced species attack vocal live.", (Action)delegate { Plugin.PetAttackVocals.Value = !Plugin.PetAttackVocals.Value; Plugin.Log.LogMessage((object)$"[SOUND] PetAttackVocals -> {Plugin.PetAttackVocals.Value} (body-synced species attack vocal on each bite)."); }); } } internal static class TravelVerbs { internal static void Register(CommandRegistry c, VerbHost h, Plugin plugin) { c.Register("travelcheck", "Wild Unknown gate state: flag -> skill learned -> bond -> pending travel record -> crossed pairs.", (Action)delegate { TravelCheck(plugin); }); h.Register("travelcross", "Run a region-travel ARRIVAL ruling headlessly ('travelcross '): break/allow/+5/dup for real. NB 'goto' does NOT simulate travel.", (Action)delegate(VerbContext ctx) { TravelCross(plugin, ctx.Player, ctx.Parts); }, "[TRAVEL]", true, true, false, (string)null); } private static void TravelCheck(Plugin plugin) { Character localPlayer = Plugin.LocalPlayer; Plugin.Log.LogMessage((object)("[TRAVEL] gate=" + (BwConfig.Skills.EnableWildUnknownGate.Value ? "ON" : "OFF") + ", " + $"Wild Unknown ({87008}) learned=" + (((Object)(object)localPlayer != (Object)null) ? WildUnknown.Learned(localPlayer).ToString() : "(no local player)"))); Pet activePet = plugin.ActivePet; if (activePet?.State != null) { Plugin.Log.LogMessage((object)($"[TRAVEL] bond: '{activePet.SpeciesId}' (loyalty {activePet.State.LoyaltyValue}); crossed pairs: " + ((activePet.State.CrossedRegionPairs.Count == 0) ? "(none)" : string.Join(", ", activePet.State.CrossedRegionPairs)))); } else { Plugin.Log.LogMessage((object)"[TRAVEL] bond: none active."); } float age; WildUnknown.PendingTravel pendingTravel = WildUnknown.Peek(out age); Plugin.Log.LogMessage((object)((pendingTravel == null) ? "[TRAVEL] pending record: none." : (string.Format("[TRAVEL] pending record: '{0}' -> '{1}' ({2}h{3}, ", pendingTravel.FromScene, pendingTravel.ToScene, pendingTravel.Hours, pendingTravel.Merchant ? ", merchant" : "") + $"age {age:F0}s, stale at {120f:F0}s)."))); } private static void TravelCross(Plugin plugin, Character player, string[] parts) { if (parts.Length < 3) { Plugin.Log.LogMessage((object)"[TRAVEL] usage: travelcross — scene names via scenedump; regions resolve through Regions.RegionOf (raw scene names as fallback pair tokens)."); } else { plugin.Lifecycle.OnRegionTravelArrival(player, parts[1], parts[2]); } } } internal static class WardShare { private static float _lastPoll = -999f; private static readonly NameCandidates _ward = Candidates("Mana Ward", () => Plugin.ManaWardStatusNames.Value); private static readonly NameCandidates _gift = Candidates("Gift of Blood", () => Plugin.GiftOfBloodStatusNames.Value); private static readonly NameCandidates _giftAlly = Candidates("Gift of Blood Ally", () => Plugin.GiftOfBloodAllyStatusNames.Value); private static bool _weAppliedWard; private static bool _playerHadGift; private static bool _confirmPending; private static string _lastAction = "none yet"; private static bool _guestModeLogged; private static bool _guestHadWard; private static bool _guestHadGift; private static NameCandidates Candidates(string label, Func read) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown return new NameCandidates(label, read, (Func)NameCandidates.StatusPrefabExists, (Action)delegate(string cand) { Plugin.Log.LogMessage((object)("[WARD] resolved " + label + " status: '" + cand + "'.")); }, (Action)delegate(string raw) { Plugin.Log.LogWarning((object)("[WARD] no " + label + " status resolved from candidates '" + raw + "' — fix the [Wards] name list (discovery: `statusdump`).")); }); } internal static void Tick(Plugin plugin) { if (!Plugin.EnableWardShare.Value) { _weAppliedWard = false; _playerHadGift = false; _guestHadWard = false; _guestHadGift = false; } else { if (Time.time - _lastPoll < Plugin.WardPollSeconds.Value) { return; } _lastPoll = Time.time; Character localPlayer = Plugin.LocalPlayer; if ((Object)(object)((localPlayer != null) ? localPlayer.StatusEffectMngr : null) == (Object)null) { return; } if (PhotonNetwork.isNonMasterClientInRoom) { GuestTick(localPlayer); return; } Pet activePet = plugin.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; if ((Object)(object)val == (Object)null) { _weAppliedWard = false; } MirrorManaWard(localPlayer, val); EdgeGrantGiftOfBlood(localPlayer, val); } } private static void MirrorManaWard(Character player, Character anchor) { //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Invalid comparison between Unknown and I4 //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Invalid comparison between Unknown and I4 string text = _ward.Resolve(); if (text == null) { return; } bool flag = (Object)(object)player.StatusEffectMngr.GetStatusEffectOfName(text) != (Object)null; object obj; if (anchor == null) { obj = null; } else { StatusEffectManager statusEffectMngr = anchor.StatusEffectMngr; obj = ((statusEffectMngr != null) ? statusEffectMngr.GetStatusEffectOfName(text) : null); } bool flag2 = (Object)obj != (Object)null; if (_confirmPending) { _confirmPending = false; if ((Object)(object)anchor == (Object)null) { Plugin.Log.LogMessage((object)"[WARD] mirror confirm skipped — anchor gone one poll after apply."); } else if (flag2) { StatusEffect statusEffectOfName = anchor.StatusEffectMngr.GetStatusEffectOfName(text); Plugin.Log.LogMessage((object)$"[WARD] mirror HOLDING one poll later: '{text}' on the anchor ({((statusEffectOfName != null) ? new float?(statusEffectOfName.RemainingLifespan) : ((float?)null)):0.#}s left), {ImmunityReadback(anchor)}."); } else { Plugin.Log.LogWarning((object)("[WARD] mirror did NOT stick — '" + text + "' absent from the anchor one poll after apply (AddStatusEffect silently failed?).")); } } WardAction val = WardRules.Mirror(flag, flag2, _weAppliedWard); if ((int)val != 1) { if ((int)val == 2) { anchor.StatusEffectMngr.RemoveStatusWithIdentifierName(text); _weAppliedWard = false; _lastAction = "removed '" + text + "' from the anchor"; Plugin.Log.LogMessage((object)("[WARD] player's Mana Ward ended — '" + text + "' removed from the anchor.")); } } else { if ((Object)(object)((anchor != null) ? anchor.StatusEffectMngr : null) == (Object)null) { return; } anchor.StatusEffectMngr.AddStatusEffect(text); _weAppliedWard = true; _confirmPending = true; bool flag3 = (Object)(object)anchor.StatusEffectMngr.GetStatusEffectOfName(text) != (Object)null; _lastAction = "applied '" + text + "' to the anchor"; Plugin.Log.LogMessage((object)("[WARD] player's Mana Ward mirrored — '" + text + "' applied to the anchor (immediate readback: status " + (flag3 ? "PRESENT" : "absent-until-init") + ", " + ImmunityReadback(anchor) + ").")); } if (!flag2) { _weAppliedWard &= flag; } } private static void EdgeGrantGiftOfBlood(Character player, Character anchor) { string text = _gift.Resolve(); if (text == null) { return; } bool flag = (Object)(object)player.StatusEffectMngr.GetStatusEffectOfName(text) != (Object)null; bool flag2 = WardRules.GrantOnEdge(flag, _playerHadGift); _playerHadGift = flag; if (!flag2) { return; } string text2 = _giftAlly.Resolve(); if (text2 != null) { if ((Object)(object)((anchor != null) ? anchor.StatusEffectMngr : null) == (Object)null) { Plugin.Log.LogMessage((object)"[WARD] player cast Gift of Blood but the pet has no live anchor — nothing granted."); } else if ((Object)(object)anchor.StatusEffectMngr.GetStatusEffectOfName(text2) != (Object)null) { _lastAction = "'" + text2 + "' already on the anchor (native)"; Plugin.Log.LogMessage((object)("[WARD] Gift of Blood: '" + text2 + "' already on the anchor (vanilla ally targeting reached it) — nothing to do.")); } else { anchor.StatusEffectMngr.AddStatusEffect(text2); _lastAction = "granted '" + text2 + "' to the anchor"; Plugin.Log.LogMessage((object)("[WARD] player's Gift of Blood — '" + text2 + "' granted to the anchor.")); } } } private static void GuestTick(Character player) { if (!_guestModeLogged) { _guestModeLogged = true; Plugin.Log.LogMessage((object)"[WARD] ward share: guest mode — wards route to the master's proxy anchor (bw.ward)."); } string text = _ward.Resolve(); if (text != null) { bool flag = (Object)(object)player.StatusEffectMngr.GetStatusEffectOfName(text) != (Object)null; bool flag2 = WardRules.GrantOnEdge(flag, _guestHadWard); bool flag3 = flag2 && PetProxyClient.SendWardToMaster(text); if (flag3) { _lastAction = "routed '" + text + "' to the master"; Plugin.Log.LogMessage((object)("[WARD] [G→M] player's Mana Ward — '" + text + "' routed to the proxied anchor.")); } if (!flag2 || flag3) { _guestHadWard = flag; } } string text2 = _gift.Resolve(); if (text2 == null) { return; } bool flag4 = (Object)(object)player.StatusEffectMngr.GetStatusEffectOfName(text2) != (Object)null; bool flag5 = WardRules.GrantOnEdge(flag4, _guestHadGift); bool flag6 = false; if (flag5) { string text3 = _giftAlly.Resolve(); if (text3 == null) { flag6 = true; } else if (PetProxyClient.SendWardToMaster(text3)) { flag6 = true; _lastAction = "routed '" + text3 + "' to the master"; Plugin.Log.LogMessage((object)("[WARD] [G→M] player's Gift of Blood — '" + text3 + "' routed to the proxied anchor.")); } } if (!flag5 || flag6) { _guestHadGift = flag4; } } internal static bool IsAllowedAnchorWard(string status) { if (string.IsNullOrEmpty(status)) { return false; } string b = _ward.Resolve(); string b2 = _giftAlly.Resolve(); if (!string.Equals(status, b, StringComparison.Ordinal)) { return string.Equals(status, b2, StringComparison.Ordinal); } return true; } internal static string DescribeAllowlist() { string text = _ward.Resolve() ?? "UNRESOLVED"; string text2 = _giftAlly.Resolve() ?? "UNRESOLVED"; return "ward='" + text + "' ally='" + text2 + "'"; } internal static void ApplyGuestWard(Character anchor, string status, string ownerUid) { if (!((Object)(object)((anchor != null) ? anchor.StatusEffectMngr : null) == (Object)null)) { if (string.Equals(status, _giftAlly.Cached, StringComparison.Ordinal) && (Object)(object)anchor.StatusEffectMngr.GetStatusEffectOfName(status) != (Object)null) { Plugin.Log.LogMessage((object)("[WARD] [G→M] guest '" + ownerUid + "': '" + status + "' already on the proxied anchor (native) — nothing to do.")); } else { anchor.StatusEffectMngr.AddStatusEffect(status); Plugin.Log.LogMessage((object)("[WARD] [G→M] guest '" + ownerUid + "''s ward — '" + status + "' applied to their proxied anchor.")); } } } private static string ImmunityReadback(Character anchor) { if ((Object)(object)((anchor != null) ? anchor.Stats : null) == (Object)null) { return "no Stats"; } object? obj = AccessTools.Field(typeof(CharacterStats), "m_damageImmunity")?.GetValue(anchor.Stats); Stat val = (Stat)((obj is Stat) ? obj : null); string obj2 = ((val != null) ? val.CurrentValue.ToString("0.###") : "n/a (field not in this build)"); Stat impactModifier = anchor.Stats.m_impactModifier; return "damageImmunity=" + obj2 + $", impactMod x{((impactModifier != null) ? new float?(impactModifier.CurrentValue) : ((float?)null)):0.###}"; } internal static void Dump(Plugin plugin) { if (!Plugin.EnableWardShare.Value) { Plugin.Log.LogMessage((object)"[WARD] ward sharing DISABLED ([Wards] EnableWardShare)."); return; } Character localPlayer = Plugin.LocalPlayer; Pet activePet = plugin.ActivePet; object obj; if (activePet == null) { obj = null; } else { CompanionAnchor anchor = ((Companion)activePet).Anchor; obj = ((anchor != null) ? anchor.Current : null); } Character val = (Character)obj; string text = _ward.Resolve() ?? "UNRESOLVED"; string text2 = _gift.Resolve() ?? "UNRESOLVED"; string text3 = _giftAlly.Resolve() ?? "UNRESOLVED"; Plugin.Log.LogMessage((object)$"[WARD] poll every {Plugin.WardPollSeconds.Value:0.##}s; statuses: ward='{text}', gift='{text2}', giftAlly='{text3}'."); if (PhotonNetwork.isNonMasterClientInRoom) { Plugin.Log.LogMessage((object)("[WARD] role: GUEST — wards route to the master's proxy anchor (bw.ward); " + $"latches guestHadWard={_guestHadWard}, guestHadGift={_guestHadGift}.")); } Plugin.Log.LogMessage((object)("[WARD] player: " + LiveLine(localPlayer, text, text2, text3) + ".")); Plugin.Log.LogMessage((object)("[WARD] anchor: " + (((Object)(object)val == (Object)null) ? "none" : LiveLine(val, text, text2, text3)) + $"; weAppliedWard={_weAppliedWard}, playerHadGift={_playerHadGift}; last action: {_lastAction}.")); if ((Object)(object)((val != null) ? val.Stats : null) != (Object)null) { Plugin.Log.LogMessage((object)("[WARD] anchor readback: " + ImmunityReadback(val) + " (immunity>0 = every non-raw hit wiped).")); } } private static string LiveLine(Character c, string ward, string gift, string ally) { StatusEffectManager m = ((c != null) ? c.StatusEffectMngr : null); if ((Object)(object)m == (Object)null) { return "no StatusEffectMngr"; } return "ward " + Has(ward) + ", gift " + Has(gift) + ", giftAlly " + Has(ally); string Has(string id) { if (id != "UNRESOLVED") { StatusEffect statusEffectOfName = m.GetStatusEffectOfName(id); if (statusEffectOfName != null) { return $"YES ({statusEffectOfName.RemainingLifespan:0.#}s left)"; } } return "no"; } } } internal static class WeatherFoodTable { private static readonly TableLoader _loader = new TableLoader("WeatherFoods.json", "[WEATHER]", "weather food", "weather foods", (Func, Dictionary>)WeatherFoods.Parse, (Func, Dictionary, Dictionary>)WeatherFoods.Merge, "replaces per-item", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[WEATHER]", Validate); internal static Dictionary Table => _axis.Table; internal static WeatherFoodDef Match(int itemId, string itemName) { if (!Plugin.EnableWeatherFoods.Value) { return null; } return WeatherFoods.Match(Table, itemId, itemName); } internal static List ReliefsFor(PetSave state) { if (!Plugin.EnableWeatherFoods.Value) { return null; } if (state == null || string.IsNullOrEmpty(state.DrinkKey) || state.DrinkSecondsLeft <= 0.0) { return null; } if (!Table.TryGetValue(state.DrinkKey, out var value)) { return null; } List list = value.ToReliefs(); if (list.Count <= 0) { return null; } return list; } internal static WeatherFoodDef DefFor(string drinkKey) { if (string.IsNullOrEmpty(drinkKey) || !Table.TryGetValue(drinkKey, out var value)) { return null; } return value; } internal static string DisplayName(string drinkKey) { if (string.IsNullOrEmpty(drinkKey)) { return ""; } if (int.TryParse(drinkKey, out var result)) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(result) : null); if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(val.Name)) { return val.Name; } } return drinkKey; } internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { TableValidator.CheckItemTable("[WEATHER]", "WeatherFoods.json", "entry", ItemRefs(), out var _, out var misses); Plugin.Log.LogMessage((object)$"[WEATHER] boot check: {Table.Count} weather foods, {misses} unresolved item key(s)."); } private static IEnumerable ItemRefs() { foreach (KeyValuePair item in Table) { yield return new ItemRef(item.Value.ItemId, item.Value.Key, $"weather food ItemID {item.Value.ItemId}", "weather food '" + item.Value.Key + "'"); } } internal static void WeatherDump(Plugin p) { //IL_0431: Unknown result type (might be due to invalid IL or missing references) //IL_0437: Unknown result type (might be due to invalid IL or missing references) //IL_0442: Unknown result type (might be due to invalid IL or missing references) //IL_0447: Unknown result type (might be due to invalid IL or missing references) //IL_045c: Unknown result type (might be due to invalid IL or missing references) //IL_0470: Unknown result type (might be due to invalid IL or missing references) //IL_0484: Unknown result type (might be due to invalid IL or missing references) //IL_04f5: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogMessage((object)$"[WEATHER] flags: EnableWeatherFoods={Plugin.EnableWeatherFoods.Value}, EnableTemperatureSystem={Plugin.EnableTemperatureSystem.Value}"); Plugin.Log.LogMessage((object)$"[WEATHER] ── table ({Table.Count} weather foods; 'reloadweather' re-reads the override) ──"); foreach (KeyValuePair item in Table) { WeatherFoodDef value = item.Value; object obj; if (!value.ItemId.HasValue) { obj = "(by display name)"; } else { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(value.ItemId.Value) : null); obj = ((val != null) ? ("'" + val.Name + "'") : "UNKNOWN ITEM"); } string text = (string)obj; Plugin.Log.LogMessage((object)string.Format("[WEATHER] '{0}' {1}: cold={2} hot={3} escalate={4:0.#} duration={5:F0}s{6}{7}", value.Key, text, value.ColdSteps, value.HotSteps, value.EscalateMult, value.DurationSeconds, value.Universal ? " UNIVERSAL" : "", value.Immune ? " IMMUNE" : "")); } Character localPlayerCharacter = Plugin.LocalPlayerCharacter; if ((Object)(object)localPlayerCharacter != (Object)null) { int num = 0; foreach (Item item2 in PetFeeder.InventoryItems(localPlayerCharacter)) { WaterContainer val2 = (WaterContainer)(object)((item2 is WaterContainer) ? item2 : null); if (val2 != null) { num++; Item val3 = (Item)(object)(val2.IsEmpty ? null : val2.GetWaterItem()); WeatherFoodDef val4 = (((Object)(object)val3 != (Object)null) ? WeatherFoods.Match(Table, val3.ItemID, val3.Name) : null); Plugin.Log.LogMessage((object)($"[WEATHER] waterskin: '{item2.Name}' {val2.RemainingUse}/{val2.MaxUseCount} use(s)" + (((Object)(object)val3 == (Object)null) ? " — EMPTY (feeds as the skin itself: normal refusal)." : ($" of '{val3.Name}' (id={val3.ItemID}) -> " + ((val4 == null) ? "NO table row." : string.Format("row '{0}': cold={1} hot={2} {3:F0}s{4}{5}", val4.Key, val4.ColdSteps, val4.HotSteps, val4.DurationSeconds, val4.Universal ? " UNIVERSAL" : "", val4.Immune ? " IMMUNE" : "")))))); } } if (num == 0) { Plugin.Log.LogMessage((object)"[WEATHER] waterskins in inventory: none."); } } Pet activePet = p.ActivePet; if (activePet?.State == null) { Plugin.Log.LogMessage((object)"[WEATHER] no active pet."); return; } PetSave state = activePet.State; if (string.IsNullOrEmpty(state.DrinkKey)) { Plugin.Log.LogMessage((object)"[WEATHER] drink buff: none."); } else { WeatherFoodDef val5 = DefFor(state.DrinkKey); Plugin.Log.LogMessage((object)($"[WEATHER] drink buff: '{state.DrinkKey}' ({DisplayName(state.DrinkKey)}), {state.DrinkSecondsLeft:F0}s left" + ((val5 == null) ? " — KEY NOT IN TABLE (override removed it?): ticking but inert." : string.Format(", cold={0} hot={1}{2}", val5.ColdSteps, val5.HotSteps, val5.Immune ? ", IMMUNE (no climate applies)" : "")))); } if (!Plugin.EnableTemperatureSystem.Value) { Plugin.Log.LogMessage((object)"[WEATHER] temperature system OFF — nothing evaluates."); return; } PetSystems.ComfortSample comfortSample = PetSystems.SampleComfort(activePet, Plugin.LocalPlayerCharacter); if (!comfortSample.Valid) { Plugin.Log.LogMessage((object)"[WEATHER] no sample (menu/void scene?)."); return; } ComfortReading val6 = PetComfort.Evaluate(comfortSample.Ambient, comfortSample.Band, PetComfortTable.ReliefFor(state)); Plugin.Log.LogMessage((object)($"[WEATHER] eval at {comfortSample.Ambient} (band {((ComfortBand)(ref comfortSample.Band)).Min}-{((ComfortBand)(ref comfortSample.Band)).Max}): raw={((ComfortReading)(ref comfortSample.Reading)).RawStepsOutside}" + $" -> blanket-only out={((ComfortReading)(ref val6)).StepsOutside} -> blanket+drink out={((ComfortReading)(ref comfortSample.Reading)).StepsOutside}" + (((ComfortReading)(ref comfortSample.Reading)).Immune ? " [IMMUNE — the drink blanks the climate outright]" : "") + $" (side={((ComfortReading)(ref comfortSample.Reading)).Side}, escMult={((ComfortReading)(ref comfortSample.Reading)).EscalateMult:0.#})")); } } internal static class WildUnknown { internal sealed class PendingTravel { public string FromScene; public string ToScene; public int Hours; public bool Merchant; } internal const float StaleSeconds = 120f; private static PendingTravel _pending; private static float _pendingAt; private static bool GateOn => BwConfig.Skills.EnableWildUnknownGate.Value; internal static bool Learned(Character player) { if (player == null) { return false; } CharacterInventory inventory = player.Inventory; bool? obj; if (inventory == null) { obj = null; } else { CharacterSkillKnowledge skillKnowledge = inventory.SkillKnowledge; obj = ((skillKnowledge != null) ? new bool?(((CharacterKnowledge)skillKnowledge).IsItemLearned(87008)) : ((bool?)null)); } bool? flag = obj; return flag == true; } internal static PendingTravel Peek(out float age) { age = ((_pending == null) ? 0f : (Time.realtimeSinceStartup - _pendingAt)); return _pending; } internal static void RecordFromManager(CharacterManager cm, bool merchant, string hook) { if (GateOn && !((Object)(object)cm == (Object)null) && cm.m_areaSwitchTravelTime > 0 && !ExpeditionHarvest.InProgress) { _pending = new PendingTravel { FromScene = cm.m_departureArea?.SceneName, ToScene = cm.m_pendingAreaSwitch?.SceneName, Hours = cm.m_areaSwitchTravelTime, Merchant = merchant }; _pendingAt = Time.realtimeSinceStartup; Plugin.Log.LogMessage((object)string.Format("[TRAVEL] recorded {0}travel '{1}' -> '{2}' ({3}h) at {4}.", merchant ? "merchant " : "", _pending.FromScene, _pending.ToScene, _pending.Hours, hook)); } } internal static PendingTravel Consume() { PendingTravel pending = _pending; if (pending == null) { return null; } _pending = null; float num = Time.realtimeSinceStartup - _pendingAt; if (num > 120f) { Plugin.Log.LogWarning((object)$"[TRAVEL] consume: stale record '{pending.FromScene}' -> '{pending.ToScene}' ({num:F0}s old, limit {120f:F0}s) — ignored."); return null; } Plugin.Log.LogMessage((object)string.Format("[TRAVEL] consume: '{0}' -> '{1}' ({2}h{3}, age {4:F0}s).", pending.FromScene, pending.ToScene, pending.Hours, pending.Merchant ? ", merchant" : "", num)); return pending; } internal static void ClearPending(string why) { if (_pending != null) { Plugin.Log.LogMessage((object)("[TRAVEL] pending record '" + _pending.FromScene + "' -> '" + _pending.ToScene + "' cleared (" + why + ").")); _pending = null; } } internal static void WarnIfBondAtRisk(string where) { if (GateOn) { Character localPlayerCharacter = Plugin.LocalPlayerCharacter; Pet pet = Plugin.Instance?.ActivePet; if (!((Object)(object)localPlayerCharacter == (Object)null) && pet != null && !Learned(localPlayerCharacter)) { string text = (string.IsNullOrEmpty(pet.SpeciesId) ? "Your companion" : pet.SpeciesId); Plugin.Log.LogMessage((object)("[TRAVEL] warned at " + where + ": '" + text + "' bond at risk (Wild Unknown not learned).")); Notify.Player(localPlayerCharacter, text + " cannot follow you across such a distance — traveling now will break your bond."); } } } } [HarmonyPatch(typeof(CharacterManager), "OnReceiveFastTravel")] internal static class WildUnknownFastTravelHook { internal static void Postfix(CharacterManager __instance) { try { WildUnknown.RecordFromManager(__instance, merchant: false, "OnReceiveFastTravel"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TRAVEL] OnReceiveFastTravel postfix error: " + ex)); } } } [HarmonyPatch(typeof(CharacterManager), "SendMerchantFastTravelRPC")] internal static class WildUnknownMerchantHook { internal static void Postfix(CharacterManager __instance) { try { WildUnknown.RecordFromManager(__instance, merchant: true, "SendMerchantFastTravelRPC"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TRAVEL] SendMerchantFastTravelRPC postfix error: " + ex)); } } } [HarmonyPatch(typeof(FastTravelMenu), "Show")] internal static class WildUnknownMenuWarning { internal static void Postfix() { try { WildUnknown.WarnIfBondAtRisk("FastTravelMenu"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TRAVEL] FastTravelMenu.Show postfix error: " + ex)); } } } [HarmonyPatch(typeof(MerchantFastTravel), "ProceedFastTravel")] internal static class WildUnknownMerchantWarning { internal static bool Prefix() { try { WildUnknown.WarnIfBondAtRisk("MerchantFastTravel"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TRAVEL] ProceedFastTravel prefix error: " + ex)); } return true; } } internal static class YawTable { private static readonly TableLoader _loader = new TableLoader("SpeciesYawOffsets.txt", "[YAW]", "yaw entry", "yaw entries", (Func, Dictionary>)SpeciesYaw.ParseTable, (Func, Dictionary, Dictionary>)SpeciesTable.Merge, "replaces per-species", (Func, Action, Dictionary>)null, (Func, string>)null, ""); private static readonly DataAxis> _axis = BwAxis.Table(_loader, "[YAW]", Validate); internal static Dictionary Table => _axis.Table; internal static List> Pairs => Table.ToList(); internal static void Init() { _axis.Init(); } internal static void Reload() { _axis.Reload(); } private static void Validate() { int num = TableValidator.CheckSpeciesKeys("[YAW]", "SpeciesYawOffsets.txt", Table.Keys); Plugin.Log.LogMessage((object)string.Format("[YAW] boot check: {0} yaw entr{1}, {2} unknown species key(s).", Table.Count, (Table.Count == 1) ? "y" : "ies", num)); } } }