Decompiled source of Offline Companions v1.2.8

Companions.dll

Decompiled 3 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.Rendering;
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: AssemblyVersion("0.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Companions
{
	internal static class ModConfig
	{
		internal static readonly Dictionary<string, List<ConfigEntryBase>> Categories = new Dictionary<string, List<ConfigEntryBase>>();

		internal static readonly List<string> CategoryOrder = new List<string>();

		internal static ConfigEntry<int> CompanionPrice;

		internal static ConfigEntry<bool> SpawnStarterCompanion;

		internal static ConfigEntry<float> MaxCarryWeight;

		internal static ConfigEntry<float> BaseHealth;

		internal static ConfigEntry<float> BaseStamina;

		internal static ConfigEntry<int> MaxFoodSlots;

		internal static ConfigEntry<float> MaxLeashDistance;

		internal static ConfigEntry<float> FollowTeleportDistance;

		internal static ConfigEntry<bool> ShowMinimapHudPanel;

		internal static ConfigEntry<string> RespawnMode;

		internal static ConfigEntry<float> ResurrectionDuration;

		internal static ConfigEntry<float> HealthRetreatPct;

		internal static ConfigEntry<float> StaminaRetreatPct;

		internal static ConfigEntry<float> HealthRecoverPct;

		internal static ConfigEntry<float> StaminaRecoverPct;

		internal static ConfigEntry<float> AttackCooldown;

		internal static ConfigEntry<float> PowerAttackCooldown;

		internal static ConfigEntry<float> BowMaxRange;

		internal static ConfigEntry<float> BowMinRange;

		internal static ConfigEntry<float> BowSwitchDistance;

		internal static ConfigEntry<float> MeleeSwitchDistance;

		internal static ConfigEntry<float> RetreatDistance;

		internal static ConfigEntry<float> BowDrawTime;

		internal static ConfigEntry<float> BowFireInterval;

		internal static ConfigEntry<float> DodgeCooldown;

		internal static ConfigEntry<float> DodgeStaminaCost;

		internal static ConfigEntry<float> ThreatDetectRange;

		internal static ConfigEntry<float> ProjectileDetectRange;

		internal static ConfigEntry<float> BlockSafetyCap;

		internal static ConfigEntry<float> CounterWindowDuration;

		internal static ConfigEntry<float> ConsumeCooldown;

		internal static ConfigEntry<float> CircleTargetInterval;

		internal static ConfigEntry<float> CircleTargetDuration;

		internal static ConfigEntry<float> CircleTargetDistance;

		internal static ConfigEntry<float> UpdateWeaponInterval;

		internal static ConfigEntry<float> UnableToAttackDuration;

		internal static ConfigEntry<float> RangedStandoff;

		internal static ConfigEntry<float> RangedFleeRadius;

		internal static ConfigEntry<float> ParryDelay;

		internal static ConfigEntry<float> WeaponSwitchCooldown;

		internal static ConfigEntry<float> FormationOffset;

		internal static ConfigEntry<float> FormationCatchupDist;

		internal static ConfigEntry<float> GiveUpTime;

		internal static ConfigEntry<float> UpdateTargetIntervalNear;

		internal static ConfigEntry<float> UpdateTargetIntervalFar;

		internal static ConfigEntry<float> SelfDefenseRange;

		internal static ConfigEntry<float> AutoPickupRange;

		internal static ConfigEntry<float> TombstoneScanInterval;

		internal static ConfigEntry<float> TombstoneNavTimeout;

		internal static ConfigEntry<float> WanderMoveInterval;

		internal static ConfigEntry<float> WalkSpeed;

		internal static ConfigEntry<float> RunSpeed;

		internal static ConfigEntry<float> SwimSpeed;

		internal static ConfigEntry<float> TurnSpeed;

		internal static ConfigEntry<float> ViewRange;

		internal static ConfigEntry<float> ViewAngle;

		internal static ConfigEntry<float> FoodRegenInterval;

		internal static ConfigEntry<float> MeadCooldown;

		internal static ConfigEntry<float> HealthMeadThreshold;

		internal static ConfigEntry<float> StaminaMeadThreshold;

		internal static ConfigEntry<float> ConsumeCheckInterval;

		internal static ConfigEntry<float> StaminaRegenRate;

		internal static ConfigEntry<float> StaminaRunDrain;

		internal static ConfigEntry<float> StaminaSneakDrain;

		internal static ConfigEntry<float> StaminaSwimDrain;

		internal static ConfigEntry<float> StaminaRegenDelay;

		internal static ConfigEntry<float> HarvestScanRadius;

		internal static ConfigEntry<float> HarvestScanInterval;

		internal static ConfigEntry<float> HarvestAttackInterval;

		internal static ConfigEntry<float> HarvestOverweightThreshold;

		internal static ConfigEntry<float> HarvestDropScanRadius;

		internal static ConfigEntry<float> HarvestBlacklistDuration;

		internal static ConfigEntry<string> ForageItems;

		internal static ConfigEntry<float> HomeZoneRadius;

		internal static ConfigEntry<float> SmeltScanRadius;

		internal static ConfigEntry<float> SmeltScanInterval;

		internal static ConfigEntry<int> SmeltMaxCarryOre;

		internal static ConfigEntry<int> SmeltMaxCarryFuel;

		internal static ConfigEntry<float> SmeltUseDistance;

		internal static ConfigEntry<float> FarmScanRadius;

		internal static ConfigEntry<float> FarmScanInterval;

		internal static ConfigEntry<float> FarmPlantSpacing;

		internal static ConfigEntry<float> FarmUseDistance;

		internal static ConfigEntry<float> FishScanRadius;

		internal static ConfigEntry<float> FishWaitTimeMin;

		internal static ConfigEntry<float> FishWaitTimeMax;

		internal static ConfigEntry<float> FishReelTimeMin;

		internal static ConfigEntry<float> FishReelTimeMax;

		internal static ConfigEntry<float> FishHookChance;

		internal static ConfigEntry<float> FishMissChance;

		internal static ConfigEntry<float> FishReelStaminaDrain;

		internal static ConfigEntry<float> FishMinDepth;

		internal static ConfigEntry<int> FishMaxConsecutiveMiss;

		internal static ConfigEntry<float> RepairDurabilityThreshold;

		internal static ConfigEntry<float> RepairScanRadius;

		internal static ConfigEntry<float> RepairScanInterval;

		internal static ConfigEntry<float> RepairTickInterval;

		internal static ConfigEntry<float> RepairUseDistance;

		internal static ConfigEntry<float> HomesteadScanRadius;

		internal static ConfigEntry<float> HomesteadScanInterval;

		internal static ConfigEntry<float> HomesteadFuelThreshold;

		internal static ConfigEntry<float> HomesteadTaskSlotTime;

		internal static ConfigEntry<float> HomesteadSmeltSlotTime;

		internal static ConfigEntry<float> HomesteadScanBackoff;

		internal static ConfigEntry<float> HomesteadUseDistance;

		internal static ConfigEntry<float> HomesteadIdleDelayMin;

		internal static ConfigEntry<float> HomesteadIdleDelayMax;

		internal static ConfigEntry<float> RestHealRate;

		internal static ConfigEntry<float> RestPlayerRange;

		internal static ConfigEntry<float> RestFireRange;

		internal static ConfigEntry<float> RestWarmupTime;

		internal static ConfigEntry<float> RestDirectedTimeout;

		internal static ConfigEntry<float> SpeechMinInterval;

		internal static ConfigEntry<float> SpeechMaxInterval;

		internal static ConfigEntry<float> SpeechCullDistance;

		internal static ConfigEntry<float> SpeechDuration;

		internal static ConfigEntry<float> SpeechSayCooldown;

		internal static ConfigEntry<float> SkillDeathLossFactor;

		internal static ConfigEntry<KeyCode> DirectTargetKey;

		internal static ConfigEntry<KeyCode> RadialMenuKey;

		internal static ConfigEntry<KeyCode> ConfigPanelKey;

		internal static ConfigEntry<KeyCode> FarmZoneKey;

		internal static ConfigEntry<KeyCode> FarmZoneModifier;

		internal static ConfigEntry<bool> MaleShowSpeechText;

		internal static ConfigEntry<bool> MaleEnableVoiceAudio;

		internal static ConfigEntry<bool> FemaleShowSpeechText;

		internal static ConfigEntry<bool> FemaleEnableVoiceAudio;

		internal static ConfigEntry<float> DoorScanRadius;

		internal static ConfigEntry<float> DoorInteractDist;

		internal static ConfigEntry<float> DoorProactiveScanRadius;

		internal static ConfigEntry<float> DoorFollowDistMin;

		internal static ConfigEntry<float> DoorCloseDistance;

		internal static ConfigEntry<float> DoorOpenWaitTime;

		internal static ConfigEntry<float> DoorPassTimeout;

		internal static ConfigEntry<float> DoorApproachTimeout;

		internal static ConfigEntry<float> DoorPostCloseCooldown;

		internal static ConfigEntry<float> CookScanRadius;

		internal static ConfigEntry<float> CookScanInterval;

		internal static ConfigEntry<float> CookUseDistance;

		internal static ConfigEntry<float> CookCraftingTime;

		internal static ConfigEntry<float> CookTapWaitTime;

		internal static ConfigEntry<float> HuntScanInterval;

		internal static ConfigEntry<float> HuntScanRadius;

		internal static ConfigEntry<float> HuntDropScanRadius;

		internal static ConfigEntry<float> HuntDropPickupRange;

		internal static ConfigEntry<float> HuntDropScanDelay;

		internal static ConfigEntry<float> HuntDropTimeout;

		internal static bool IsResurrectionMode
		{
			get
			{
				if (RespawnMode != null)
				{
					return RespawnMode.Value.Equals("Resurrection", StringComparison.OrdinalIgnoreCase);
				}
				return false;
			}
		}

		internal static void Init(ConfigFile cfg)
		{
			CompanionPrice = Bind(cfg, "General", "CompanionPrice", 2000, "Gold cost to purchase a companion", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 50000));
			SpawnStarterCompanion = Bind(cfg, "General", "SpawnStarterCompanion", defaultValue: true, "Spawn a free companion when entering a new world for the first time");
			MaxCarryWeight = Bind(cfg, "General", "MaxCarryWeight", 300f, "Maximum carry weight for companions", (AcceptableValueBase)(object)new AcceptableValueRange<float>(50f, 2000f));
			BaseHealth = Bind(cfg, "General", "BaseHealth", 25f, "Base HP before food bonuses", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 500f));
			BaseStamina = Bind(cfg, "General", "BaseStamina", 50f, "Base stamina before food bonuses", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 500f));
			MaxFoodSlots = Bind(cfg, "General", "MaxFoodSlots", 3, "Number of food slots", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 5));
			MaxLeashDistance = Bind(cfg, "General", "MaxLeashDistance", 50f, "StayHome teleport-home leash distance", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 200f));
			FollowTeleportDistance = Bind(cfg, "General", "FollowTeleportDistance", 50f, "Distance from player before companion warps", (AcceptableValueBase)(object)new AcceptableValueRange<float>(20f, 200f));
			ShowMinimapHudPanel = Bind(cfg, "General", "ShowMinimapHudPanel", defaultValue: true, "Show companion status panel below the minimap");
			RespawnMode = Bind(cfg, "General", "RespawnMode", "Bed", "How companions respawn after death. \"Bed\" = auto-respawn at player's bed. \"Resurrection\" = hold E on tombstone to resurrect.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "Bed", "Resurrection" }));
			ResurrectionDuration = Bind(cfg, "General", "ResurrectionDuration", 10f, "Seconds to hold E on tombstone to resurrect (Resurrection mode only)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f));
			HealthRetreatPct = Bind(cfg, "Combat", "HealthRetreatPct", 0.3f, "Retreat when HP drops below this %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 0.9f));
			StaminaRetreatPct = Bind(cfg, "Combat", "StaminaRetreatPct", 0.15f, "Retreat when stamina drops below this %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 0.9f));
			HealthRecoverPct = Bind(cfg, "Combat", "HealthRecoverPct", 0.5f, "Re-engage when HP recovers to this %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 1f));
			StaminaRecoverPct = Bind(cfg, "Combat", "StaminaRecoverPct", 0.3f, "Re-engage when stamina recovers to this %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 1f));
			AttackCooldown = Bind(cfg, "Combat", "AttackCooldown", 0.225f, "Seconds between melee attacks", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 2f));
			PowerAttackCooldown = Bind(cfg, "Combat", "PowerAttackCooldown", 3f, "Seconds between power attacks", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			BowMaxRange = Bind(cfg, "Combat", "BowMaxRange", 30f, "Maximum bow engagement range", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 80f));
			BowMinRange = Bind(cfg, "Combat", "BowMinRange", 10f, "Always use melee below this distance", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			BowSwitchDistance = Bind(cfg, "Combat", "BowSwitchDistance", 20f, "Switch TO bow when target exceeds this distance", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 60f));
			MeleeSwitchDistance = Bind(cfg, "Combat", "MeleeSwitchDistance", 12f, "Switch FROM bow when target is closer than this", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 40f));
			RetreatDistance = Bind(cfg, "Combat", "RetreatDistance", 12f, "How far to retreat from enemy", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 30f));
			BowDrawTime = Bind(cfg, "Combat", "BowDrawTime", 1.2f, "Seconds to fully draw bow", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.3f, 3f));
			BowFireInterval = Bind(cfg, "Combat", "BowFireInterval", 2.5f, "Seconds between bow shots", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 5f));
			DodgeCooldown = Bind(cfg, "Combat", "DodgeCooldown", 2.5f, "Seconds between dodges", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			DodgeStaminaCost = Bind(cfg, "Combat", "DodgeStaminaCost", 15f, "Stamina consumed per dodge", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 50f));
			ThreatDetectRange = Bind(cfg, "Combat", "ThreatDetectRange", 8f, "Range to detect enemy attacks for blocking", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 25f));
			ProjectileDetectRange = Bind(cfg, "Combat", "ProjectileDetectRange", 20f, "Range to detect incoming projectiles", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 50f));
			BlockSafetyCap = Bind(cfg, "Combat", "BlockSafetyCap", 3f, "Max continuous block time before forced counter", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 10f));
			CounterWindowDuration = Bind(cfg, "Combat", "CounterWindowDuration", 0.8f, "Post-parry attack window duration", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.2f, 3f));
			ConsumeCooldown = Bind(cfg, "Combat", "ConsumeCooldown", 10f, "Seconds between food consumption in combat", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f));
			CircleTargetInterval = Bind(cfg, "Combat", "CircleTargetInterval", 3f, "Seconds between circling/strafing movements", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 15f));
			CircleTargetDuration = Bind(cfg, "Combat", "CircleTargetDuration", 1f, "Seconds spent circling per interval", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.2f, 5f));
			CircleTargetDistance = Bind(cfg, "Combat", "CircleTargetDistance", 4f, "Circle strafe radius around target", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 15f));
			UpdateWeaponInterval = Bind(cfg, "Combat", "UpdateWeaponInterval", 1f, "Seconds between weapon selection re-evaluations", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.2f, 5f));
			UnableToAttackDuration = Bind(cfg, "Combat", "UnableToAttackDuration", 15f, "Seconds before giving up on unreachable target", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 60f));
			RangedStandoff = Bind(cfg, "Combat", "RangedStandoff", 20f, "Preferred kiting distance for ranged stance", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 50f));
			RangedFleeRadius = Bind(cfg, "Combat", "RangedFleeRadius", 10f, "Flee when enemy is closer than this (ranged stance)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 30f));
			ParryDelay = Bind(cfg, "Combat", "ParryDelay", 0.2f, "Seconds into enemy swing before raising shield", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f));
			WeaponSwitchCooldown = Bind(cfg, "Combat", "WeaponSwitchCooldown", 4f, "Minimum seconds between weapon switches (Balanced stance)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 15f));
			FormationOffset = Bind(cfg, "AI", "FormationOffset", 3f, "Lateral spacing between companions in formation", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 10f));
			FormationCatchupDist = Bind(cfg, "AI", "FormationCatchupDist", 15f, "Distance to sprint to catch up with formation", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 50f));
			GiveUpTime = Bind(cfg, "AI", "GiveUpTime", 30f, "Seconds before abandoning a target", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f));
			UpdateTargetIntervalNear = Bind(cfg, "AI", "UpdateTargetIntervalNear", 2f, "Target scan interval when player is near", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			UpdateTargetIntervalFar = Bind(cfg, "AI", "UpdateTargetIntervalFar", 6f, "Target scan interval when player is far", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			SelfDefenseRange = Bind(cfg, "AI", "SelfDefenseRange", 10f, "Self-defense targeting range in gather mode", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 30f));
			AutoPickupRange = Bind(cfg, "AI", "AutoPickupRange", 2f, "Item auto-pickup radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			TombstoneScanInterval = Bind(cfg, "AI", "TombstoneScanInterval", 5f, "Seconds between tombstone scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			TombstoneNavTimeout = Bind(cfg, "AI", "TombstoneNavTimeout", 120f, "Max tombstone recovery navigation time", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 600f));
			WanderMoveInterval = Bind(cfg, "AI", "WanderMoveInterval", 5f, "Seconds between random wander movements at home", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			WalkSpeed = Bind(cfg, "Movement", "WalkSpeed", 2f, "Walking speed (requires world reload)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 5f));
			RunSpeed = Bind(cfg, "Movement", "RunSpeed", 7f, "Running speed (requires world reload)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 15f));
			SwimSpeed = Bind(cfg, "Movement", "SwimSpeed", 2f, "Swimming speed (requires world reload)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 5f));
			TurnSpeed = Bind(cfg, "Movement", "TurnSpeed", 300f, "Turn speed (requires world reload)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(50f, 600f));
			ViewRange = Bind(cfg, "Movement", "ViewRange", 40f, "Visual detection range (requires world reload)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			ViewAngle = Bind(cfg, "Movement", "ViewAngle", 90f, "Field of view angle (requires world reload)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 360f));
			FoodRegenInterval = Bind(cfg, "Food", "FoodRegenInterval", 10f, "Seconds between food regen ticks", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f));
			MeadCooldown = Bind(cfg, "Food", "MeadCooldown", 10f, "Mead consumption cooldown", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f));
			HealthMeadThreshold = Bind(cfg, "Food", "HealthMeadThreshold", 0.5f, "Use health mead below this HP %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 0.9f));
			StaminaMeadThreshold = Bind(cfg, "Food", "StaminaMeadThreshold", 0.25f, "Use stamina mead below this stamina %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 0.9f));
			ConsumeCheckInterval = Bind(cfg, "Food", "ConsumeCheckInterval", 1f, "Auto-consume check interval", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			StaminaRegenRate = Bind(cfg, "Stamina", "RegenRate", 6f, "Stamina per second while idle", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			StaminaRunDrain = Bind(cfg, "Stamina", "RunDrainRate", 10f, "Stamina drain per second while running", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			StaminaSneakDrain = Bind(cfg, "Stamina", "SneakDrainRate", 5f, "Stamina drain per second while sneaking", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			StaminaSwimDrain = Bind(cfg, "Stamina", "SwimDrainRate", 10f, "Stamina drain per second while swimming", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			StaminaRegenDelay = Bind(cfg, "Stamina", "RegenDelay", 1f, "Seconds after stamina use before regen starts", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 5f));
			HarvestScanRadius = Bind(cfg, "Harvest", "ScanRadius", 30f, "Resource scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			HarvestScanInterval = Bind(cfg, "Harvest", "ScanInterval", 4f, "Seconds between resource scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			HarvestAttackInterval = Bind(cfg, "Harvest", "AttackInterval", 2.5f, "Seconds between harvest swings", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 5f));
			HarvestOverweightThreshold = Bind(cfg, "Harvest", "OverweightThreshold", 298f, "Stop gathering at this carry weight", (AcceptableValueBase)(object)new AcceptableValueRange<float>(50f, 500f));
			HarvestDropScanRadius = Bind(cfg, "Harvest", "DropScanRadius", 8f, "Drop collection scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 20f));
			HarvestBlacklistDuration = Bind(cfg, "Harvest", "BlacklistDuration", 60f, "Seconds a failed target stays blacklisted", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 300f));
			ForageItems = Bind(cfg, "Harvest", "ForageItems", "*", "Comma-separated item prefab names to forage (e.g. Mushroom,Raspberry,Thistle). Use * for all pickables");
			HomeZoneRadius = Bind(cfg, "StayHome", "HomeZoneRadius", 50f, "Default gather/homestead zone radius in meters around home position", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 200f));
			SmeltScanRadius = Bind(cfg, "Smelting", "ScanRadius", 25f, "Smelter scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			SmeltScanInterval = Bind(cfg, "Smelting", "ScanInterval", 3f, "Seconds between smelter scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			SmeltMaxCarryOre = Bind(cfg, "Smelting", "MaxCarryOre", 20, "Maximum ore items per trip", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100));
			SmeltMaxCarryFuel = Bind(cfg, "Smelting", "MaxCarryFuel", 40, "Maximum fuel items per trip", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100));
			SmeltUseDistance = Bind(cfg, "Smelting", "UseDistance", 2.5f, "Smelter interaction range", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f));
			FarmScanRadius = Bind(cfg, "Farming", "ScanRadius", 30f, "Crop/soil scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			FarmScanInterval = Bind(cfg, "Farming", "ScanInterval", 4f, "Seconds between farm scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			FarmPlantSpacing = Bind(cfg, "Farming", "PlantSpacing", 0.75f, "Min spacing between crops (uses max of this and plant grow radius)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 3f));
			FarmUseDistance = Bind(cfg, "Farming", "UseDistance", 2f, "Interaction range for crops and chests", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f));
			FishScanRadius = Bind(cfg, "Fishing", "ScanRadius", 30f, "Water scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			FishWaitTimeMin = Bind(cfg, "Fishing", "WaitTimeMin", 15f, "Min seconds waiting for nibble", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 120f));
			FishWaitTimeMax = Bind(cfg, "Fishing", "WaitTimeMax", 20f, "Max seconds waiting for nibble", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 180f));
			FishReelTimeMin = Bind(cfg, "Fishing", "ReelTimeMin", 4f, "Min seconds to reel in", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			FishReelTimeMax = Bind(cfg, "Fishing", "ReelTimeMax", 6f, "Max seconds to reel in", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f));
			FishHookChance = Bind(cfg, "Fishing", "HookChance", 0.85f, "Chance to hook after nibble", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 1f));
			FishMissChance = Bind(cfg, "Fishing", "MissChance", 0.1f, "Chance of no nibble per wait", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 0.9f));
			FishReelStaminaDrain = Bind(cfg, "Fishing", "ReelStaminaDrain", 3f, "Stamina drain per second while reeling", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 20f));
			FishMinDepth = Bind(cfg, "Fishing", "MinFishingDepth", 2f, "Minimum water depth required for fishing", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			FishMaxConsecutiveMiss = Bind(cfg, "Fishing", "MaxConsecutiveMiss", 3, "Consecutive misses before relocating", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10));
			RepairDurabilityThreshold = Bind(cfg, "Repair", "DurabilityThreshold", 0.5f, "Repair items below this durability %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 0.9f));
			RepairScanRadius = Bind(cfg, "Repair", "ScanRadius", 20f, "Crafting station scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 60f));
			RepairScanInterval = Bind(cfg, "Repair", "ScanInterval", 5f, "Seconds between repair scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			RepairTickInterval = Bind(cfg, "Repair", "RepairTickInterval", 0.8f, "Seconds between individual item repairs", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 5f));
			RepairUseDistance = Bind(cfg, "Repair", "UseDistance", 2.5f, "Crafting station interaction range", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f));
			HomesteadScanRadius = Bind(cfg, "Homestead", "ScanRadius", 40f, "Maintenance scan radius from home position", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 100f));
			HomesteadScanInterval = Bind(cfg, "Homestead", "ScanInterval", 5f, "Seconds between maintenance scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			HomesteadFuelThreshold = Bind(cfg, "Homestead", "FuelThreshold", 0.5f, "Refuel fires below this fuel %", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 0.9f));
			HomesteadTaskSlotTime = Bind(cfg, "Homestead", "TaskSlotTime", 60f, "Seconds per task rotation (repair/refuel/sort)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 300f));
			HomesteadSmeltSlotTime = Bind(cfg, "Homestead", "SmeltSlotTime", 60f, "Seconds for smelting turn in rotation", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 300f));
			HomesteadScanBackoff = Bind(cfg, "Homestead", "ScanBackoff", 30f, "Extended scan interval when nothing to do", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f));
			HomesteadUseDistance = Bind(cfg, "Homestead", "UseDistance", 2f, "Interaction range for fires/chests", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f));
			HomesteadIdleDelayMin = Bind(cfg, "Homestead", "IdleDelayMin", 3f, "Minimum seconds of idle before picking next behavior", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 30f));
			HomesteadIdleDelayMax = Bind(cfg, "Homestead", "IdleDelayMax", 8f, "Maximum seconds of idle before picking next behavior", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f));
			RestHealRate = Bind(cfg, "Rest", "HealRate", 2f, "HP per second while resting", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 20f));
			RestPlayerRange = Bind(cfg, "Rest", "PlayerRange", 5f, "Max player distance for auto-sit", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 20f));
			RestFireRange = Bind(cfg, "Rest", "FireRange", 5f, "Campfire detection radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 20f));
			RestWarmupTime = Bind(cfg, "Rest", "WarmupTime", 20f, "Seconds of sitting before Rested buff applies", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f));
			RestDirectedTimeout = Bind(cfg, "Rest", "DirectedTimeout", 300f, "Directed sit timeout (seconds)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 1800f));
			SpeechMinInterval = Bind(cfg, "Speech", "MinInterval", 20f, "Minimum seconds between ambient chatter", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f));
			SpeechMaxInterval = Bind(cfg, "Speech", "MaxInterval", 40f, "Maximum seconds between ambient chatter", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 300f));
			SpeechCullDistance = Bind(cfg, "Speech", "CullDistance", 20f, "Max distance at which speech is visible", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 60f));
			SpeechDuration = Bind(cfg, "Speech", "Duration", 5f, "How long speech text stays visible", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 15f));
			SpeechSayCooldown = Bind(cfg, "Speech", "SayCooldown", 20f, "Directed speech cooldown", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f));
			SkillDeathLossFactor = Bind(cfg, "Skills", "DeathLossFactor", 0.25f, "Fraction of skills lost on death (0 = none)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f));
			DirectTargetKey = Bind<KeyCode>(cfg, "Controls", "DirectTargetKey", (KeyCode)122, "Directed command / focus-fire key");
			RadialMenuKey = Bind<KeyCode>(cfg, "Controls", "RadialMenuKey", (KeyCode)101, "Radial command menu key (hold while hovering companion)");
			ConfigPanelKey = Bind<KeyCode>(cfg, "Controls", "ConfigPanelKey", (KeyCode)289, "Open the in-game config panel");
			FarmZoneKey = Bind<KeyCode>(cfg, "Controls", "FarmZoneKey", (KeyCode)122, "Farm zone placement key (press with modifier to open zone editor)");
			FarmZoneModifier = Bind<KeyCode>(cfg, "Controls", "FarmZoneModifier", (KeyCode)308, "Modifier key for farm zone placement (hold this + FarmZoneKey)");
			MaleShowSpeechText = Bind(cfg, "Speech Display", "MaleShowOverheadText", defaultValue: false, "Show overhead speech text for male companions");
			MaleEnableVoiceAudio = Bind(cfg, "Speech Display", "MaleEnableVoiceAudio", defaultValue: true, "Play voice audio for male companions");
			FemaleShowSpeechText = Bind(cfg, "Speech Display", "FemaleShowOverheadText", defaultValue: true, "Show overhead speech text for female companions");
			FemaleEnableVoiceAudio = Bind(cfg, "Speech Display", "FemaleEnableVoiceAudio", defaultValue: false, "Play voice audio for female companions");
			DoorScanRadius = Bind(cfg, "Door", "ScanRadius", 5f, "Stuck-mode door scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 15f));
			DoorInteractDist = Bind(cfg, "Door", "InteractDist", 2f, "Door interaction range", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f));
			DoorProactiveScanRadius = Bind(cfg, "Door", "ProactiveScanRadius", 15f, "Proactive door scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 30f));
			DoorFollowDistMin = Bind(cfg, "Door", "FollowDistMin", 4f, "Min distance from target to engage door handler", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 15f));
			DoorCloseDistance = Bind(cfg, "Door", "CloseDistance", 3.5f, "Distance past door before closing it", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 10f));
			DoorOpenWaitTime = Bind(cfg, "Door", "OpenWaitTime", 1.2f, "Seconds to wait for door open animation", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.3f, 5f));
			DoorPassTimeout = Bind(cfg, "Door", "PassTimeout", 8f, "Seconds before giving up passing through", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 30f));
			DoorApproachTimeout = Bind(cfg, "Door", "ApproachTimeout", 10f, "Seconds before giving up approaching door", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 30f));
			DoorPostCloseCooldown = Bind(cfg, "Door", "PostCloseCooldown", 4f, "Cooldown after closing to prevent re-detection", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 15f));
			CookScanRadius = Bind(cfg, "Cooking", "ScanRadius", 25f, "Cauldron/fermenter scan radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			CookScanInterval = Bind(cfg, "Cooking", "ScanInterval", 5f, "Seconds between cooking scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f));
			CookUseDistance = Bind(cfg, "Cooking", "UseDistance", 2.5f, "Cauldron/fermenter interaction range", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f));
			CookCraftingTime = Bind(cfg, "Cooking", "CraftingTime", 3f, "Seconds to craft one recipe", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 15f));
			CookTapWaitTime = Bind(cfg, "Cooking", "TapWaitTime", 2f, "Seconds to wait after tapping fermenter", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f));
			HuntScanInterval = Bind(cfg, "Hunting", "ScanInterval", 3f, "Seconds between prey scans", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 15f));
			HuntScanRadius = Bind(cfg, "Hunting", "ScanRadius", 20f, "Prey detection radius", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 100f));
			HuntDropScanRadius = Bind(cfg, "Hunting", "DropScanRadius", 12f, "Radius to search for drops around kill", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 30f));
			HuntDropPickupRange = Bind(cfg, "Hunting", "DropPickupRange", 3f, "Distance to pick up drops", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 10f));
			HuntDropScanDelay = Bind(cfg, "Hunting", "DropScanDelay", 5f, "Wait time for death animation before collecting", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 15f));
			HuntDropTimeout = Bind(cfg, "Hunting", "DropTimeout", 12f, "Max seconds spent collecting drops", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 30f));
		}

		private static ConfigEntry<T> Bind<T>(ConfigFile cfg, string category, string key, T defaultValue, string desc, AcceptableValueBase range = null)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Expected O, but got Unknown
			ConfigEntry<T> val = cfg.Bind<T>(category, key, defaultValue, new ConfigDescription(desc, range, Array.Empty<object>()));
			if (!Categories.TryGetValue(category, out var value))
			{
				value = new List<ConfigEntryBase>();
				Categories[category] = value;
				CategoryOrder.Add(category);
			}
			value.Add((ConfigEntryBase)(object)val);
			return val;
		}
	}
	[HarmonyPatch]
	public static class ModLocalization
	{
		private static readonly FieldInfo TranslationsField = AccessTools.Field(typeof(Localization), "m_translations");

		private static Dictionary<string, string> _englishDefaults;

		private static string _translationsDir;

		private static bool _initialized;

		private static readonly Regex EntryRegex = new Regex("\"key\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"[^\"]*\"value\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"", RegexOptions.Compiled);

		private static readonly FieldInfo InstanceField = AccessTools.Field(typeof(Localization), "m_instance");

		public static string Loc(string key)
		{
			if (Localization.instance == null)
			{
				return key;
			}
			return Localization.instance.Localize("$" + key);
		}

		public static string LocFmt(string key, params object[] args)
		{
			string text = Loc(key);
			try
			{
				return string.Format(text, args);
			}
			catch
			{
				return text;
			}
		}

		public static void Init()
		{
			_translationsDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Translations");
			BuildEnglishDefaults();
			_initialized = true;
			object? obj = InstanceField?.GetValue(null);
			Localization val = (Localization)((obj is Localization) ? obj : null);
			if (val != null)
			{
				CompanionsPlugin.Log.LogInfo((object)"[Localization] Localization already initialized — injecting retroactively");
				InjectTranslations(val);
			}
		}

		public static void EnsureDefaultFile()
		{
			if (_englishDefaults == null)
			{
				return;
			}
			try
			{
				if (!Directory.Exists(_translationsDir))
				{
					Directory.CreateDirectory(_translationsDir);
				}
				string text = Path.Combine(_translationsDir, "English.json");
				if (File.Exists(text))
				{
					Dictionary<string, string> dictionary = ParseTranslationJson(File.ReadAllText(text));
					if (dictionary != null && dictionary.Count >= _englishDefaults.Count)
					{
						return;
					}
					CompanionsPlugin.Log.LogInfo((object)$"[Localization] English.json is stale ({dictionary?.Count ?? 0} vs {_englishDefaults.Count} keys) — regenerating.");
				}
				List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>(_englishDefaults);
				list.Sort((KeyValuePair<string, string> a, KeyValuePair<string, string> b) => string.Compare(a.Key, b.Key, StringComparison.Ordinal));
				StringBuilder stringBuilder = new StringBuilder();
				stringBuilder.AppendLine("{");
				stringBuilder.AppendLine("    \"entries\": [");
				for (int i = 0; i < list.Count; i++)
				{
					string text2 = ((i < list.Count - 1) ? "," : "");
					stringBuilder.AppendLine("        { \"key\": \"" + EscapeJson(list[i].Key) + "\", \"value\": \"" + EscapeJson(list[i].Value) + "\" }" + text2);
				}
				stringBuilder.AppendLine("    ]");
				stringBuilder.AppendLine("}");
				File.WriteAllText(text, stringBuilder.ToString(), Encoding.UTF8);
				CompanionsPlugin.Log.LogInfo((object)("[Localization] Created default " + text));
			}
			catch (Exception ex)
			{
				CompanionsPlugin.Log.LogWarning((object)("[Localization] Failed to write English.json: " + ex.Message));
			}
		}

		[HarmonyPatch(typeof(Localization), "SetupLanguage")]
		[HarmonyPostfix]
		private static void SetupLanguage_Postfix(Localization __instance)
		{
			if (_initialized)
			{
				InjectTranslations(__instance);
			}
		}

		private static void InjectTranslations(Localization loc)
		{
			if (loc == null)
			{
				return;
			}
			string selectedLanguage = loc.GetSelectedLanguage();
			if (!(TranslationsField?.GetValue(loc) is Dictionary<string, string> dictionary))
			{
				return;
			}
			if (_englishDefaults != null)
			{
				foreach (KeyValuePair<string, string> englishDefault in _englishDefaults)
				{
					dictionary[englishDefault.Key] = englishDefault.Value;
				}
			}
			Dictionary<string, string> dictionary2 = LoadTranslationFile((selectedLanguage != "English") ? selectedLanguage : null);
			if (dictionary2 == null && selectedLanguage != "English")
			{
				dictionary2 = LoadTranslationFile("English");
			}
			int num = 0;
			if (dictionary2 != null)
			{
				foreach (KeyValuePair<string, string> item in dictionary2)
				{
					dictionary[item.Key] = item.Value;
					num++;
				}
			}
			CompanionsPlugin.Log.LogInfo((object)$"[Localization] Injected {_englishDefaults?.Count ?? 0} defaults + {num} \"{selectedLanguage}\" overrides");
		}

		private static Dictionary<string, string> LoadTranslationFile(string language)
		{
			if (string.IsNullOrEmpty(language) || string.IsNullOrEmpty(_translationsDir))
			{
				return null;
			}
			string text = Path.Combine(_translationsDir, language + ".json");
			if (!File.Exists(text))
			{
				return null;
			}
			try
			{
				Dictionary<string, string> dictionary = ParseTranslationJson(File.ReadAllText(text));
				if (dictionary == null || dictionary.Count == 0)
				{
					return null;
				}
				CompanionsPlugin.Log.LogInfo((object)$"[Localization] Loaded {dictionary.Count} keys from {text}");
				return dictionary;
			}
			catch (Exception ex)
			{
				CompanionsPlugin.Log.LogWarning((object)("[Localization] Failed to load " + text + ": " + ex.Message));
				return null;
			}
		}

		private static Dictionary<string, string> ParseTranslationJson(string json)
		{
			if (string.IsNullOrEmpty(json))
			{
				return null;
			}
			Dictionary<string, string> dictionary = new Dictionary<string, string>();
			MatchCollection matchCollection = EntryRegex.Matches(json);
			for (int i = 0; i < matchCollection.Count; i++)
			{
				Match match = matchCollection[i];
				string text = UnescapeJson(match.Groups[1].Value);
				string value = UnescapeJson(match.Groups[2].Value);
				if (!string.IsNullOrEmpty(text))
				{
					dictionary[text] = value;
				}
			}
			return dictionary;
		}

		private static string EscapeJson(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n")
				.Replace("\r", "\\r")
				.Replace("\t", "\\t");
		}

		private static string UnescapeJson(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			return s.Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\t", "\t")
				.Replace("\\\"", "\"")
				.Replace("\\\\", "\\");
		}

		private static void BuildEnglishDefaults()
		{
			_englishDefaults = new Dictionary<string, string>
			{
				{ "hc_ui_title_companions", "Companions" },
				{ "hc_ui_title_choose", "Choose Your Companion" },
				{ "hc_ui_btn_spawn", "Spawn Companion" },
				{ "hc_ui_btn_buy", "Buy {0} ({1})" },
				{ "hc_ui_btn_need", "Need {0}" },
				{ "hc_ui_bank", "Bank: {0}" },
				{ "hc_ui_label_type", "Type" },
				{ "hc_ui_label_gender", "Gender" },
				{ "hc_ui_label_hair", "Hair Style" },
				{ "hc_ui_label_beard", "Beard Style" },
				{ "hc_ui_label_skin", "Skin Tone" },
				{ "hc_ui_label_hairtone", "Hair Tone" },
				{ "hc_ui_label_hairshade", "Hair Shade" },
				{ "hc_ui_label_subtype", "Sub-Type" },
				{ "hc_ui_btn_male", "Male" },
				{ "hc_ui_btn_female", "Female" },
				{ "hc_ui_btn_companion", "Companion" },
				{ "hc_ui_btn_dverger", "Dverger" },
				{ "hc_ui_beard_none", "None" },
				{ "hc_ui_desc_starter", "A loyal companion will join you on your journey. Customise their appearance on the left, then spawn them into the world.\n\nThey will follow you, fight by your side, and carry supplies. Equip them with gear and keep them fed to stay battle-ready.\n\nPress Escape to skip — the panel will appear again next session." },
				{ "hc_ui_desc_trader", "Hire a loyal companion to join your journey. They will follow you across the world and fight by your side against any threat.\n\nEach companion has their own health, stamina and inventory. You will need to equip them with gear and keep them fed to stay battle-ready.\n\nInteract with your companion to open their panel, where you can manage their equipment and issue commands.\n\nCustomise their appearance on the left, then confirm your purchase." },
				{ "hc_ui_placeholder_name", "Enter name..." },
				{ "hc_ui_label_food", "Food" },
				{ "hc_radial_follow", "Follow" },
				{ "hc_radial_wood", "Wood" },
				{ "hc_radial_stone", "Stone" },
				{ "hc_radial_ore", "Ore" },
				{ "hc_radial_forage", "Forage" },
				{ "hc_radial_smelt", "Smelt" },
				{ "hc_radial_stayhome", "Stay Home" },
				{ "hc_radial_sethome", "Set Home" },
				{ "hc_radial_wander", "Wander" },
				{ "hc_radial_pickup", "Pickup" },
				{ "hc_radial_command", "Command" },
				{ "hc_radial_balanced", "Balanced" },
				{ "hc_radial_aggressive", "Aggressive" },
				{ "hc_radial_defensive", "Defensive" },
				{ "hc_radial_passive", "Passive" },
				{ "hc_radial_melee", "Melee" },
				{ "hc_radial_ranged", "Ranged" },
				{ "hc_radial_hunt", "Hunt" },
				{ "hc_radial_farm", "Farm" },
				{ "hc_radial_fish", "Fish" },
				{ "hc_radial_cook", "Cook" },
				{ "hc_radial_repair", "Repair" },
				{ "hc_radial_restock", "Restock" },
				{ "hc_radial_despawn", "Despawn" },
				{ "hc_radial_despawn_confirm", "Confirm?" },
				{ "hc_radial_active", "ACTIVE" },
				{ "hc_radial_on", "ON" },
				{ "hc_radial_off", "OFF" },
				{ "hc_hover_inventory", "Inventory" },
				{ "hc_hover_commands", "Commands" },
				{ "hc_msg_not_yours", "This is not your companion." },
				{ "hc_msg_need_coins", "Not enough coins in bank! Need {0}" },
				{ "hc_msg_arrived", "Your new {0} has arrived!" },
				{ "hc_msg_returned", "{0} has returned at {1}!" },
				{ "hc_msg_location_home", "home" },
				{ "hc_msg_location_spawn", "the world spawn" },
				{ "hc_msg_location_bed", "your bed" },
				{ "hc_tomb_hover_resurrect", "$piece_tombstone {0}\n[<color=yellow><b>$KEY_Use</b></color>] Hold to Resurrect" },
				{ "hc_tomb_progress_resurrect", "Resurrecting {0}..." },
				{ "hc_msg_resurrected", "{0} has been resurrected!" },
				{ "hc_speech_resurrected", "I'm back!" },
				{ "hc_msg_starter_joined", "A companion has joined you on your journey!" },
				{ "hc_msg_name_default", "Companion" },
				{ "hc_menu_mod_options", "Mod Options" },
				{ "hc_minimap_grave", "Grave" },
				{ "hc_msg_sleep_waiting", "Waiting for {0} to sleep..." },
				{ "hc_msg_sleep_waiting_generic", "Waiting for companions to sleep..." },
				{ "hc_msg_rested", "{0} is Rested (Comfort: {1})" },
				{ "hc_msg_rested_suffix", "Rested" },
				{ "hc_msg_tools_weak", "{0}'s tools are not strong enough" },
				{ "hc_msg_reposition_on", "UI Reposition Mode — drag to move, F7 to confirm" },
				{ "hc_msg_reposition_off", "UI position saved" },
				{ "hc_msg_commanded", "Moving to position." },
				{ "hc_speech_repair_start", "Time for repairs." },
				{ "hc_speech_repair_done", "All fixed up!" },
				{ "hc_speech_smelt_monitoring", "Everything's running. I'll keep watch." },
				{ "hc_speech_smelt_done", "All done smelting." },
				{ "hc_speech_smelt_fuel", "Fetching fuel." },
				{ "hc_speech_smelt_materials", "Fetching materials." },
				{ "hc_speech_homestead_refuel", "Time to refuel." },
				{ "hc_speech_homestead_stoked", "Fire's stoked." },
				{ "hc_speech_homestead_repair", "I'll patch that up." },
				{ "hc_speech_homestead_repaired", "Good as new." },
				{ "hc_speech_homestead_tidy", "Let me tidy up." },
				{ "hc_speech_homestead_tidied", "All tidied up." },
				{ "hc_speech_homestead_cooked", "Something tasty!" },
				{ "hc_speech_repair_buildings_done", "Everything's patched up." },
				{ "hc_speech_repair_nothing", "Nothing needs repair." },
				{ "hc_speech_repair_no_hammer", "I need a hammer for that." },
				{ "hc_speech_restock_done", "Fires are stoked." },
				{ "hc_speech_restock_nothing", "Nothing needs fuel." },
				{ "hc_speech_farm_planting", "Time to plant some seeds." },
				{ "hc_speech_farm_harvesting", "Crops are ready!" },
				{ "hc_speech_farm_done", "All done farming." },
				{ "hc_speech_farm_no_seeds", "I need seeds to plant." },
				{ "hc_speech_farm_no_cultivator", "I need a cultivator to plant." },
				{ "hc_speech_no_chest", "No chest nearby to unload!" },
				{ "hc_speech_no_bow", "I need a bow and arrows to shoot!" },
				{ "hc_speech_overweight", "My back is hurting from all this weight!" },
				{ "hc_speech_cook_cooking", "Let me cook something." },
				{ "hc_speech_cook_brewing", "Brewing a batch." },
				{ "hc_speech_cook_fermenter", "Into the fermenter." },
				{ "hc_speech_cook_tapping", "This batch is ready!" },
				{ "hc_speech_cook_storing", "Putting these away." },
				{ "hc_speech_fish_need_rod", "I need a fishing rod." },
				{ "hc_speech_fish_need_bait", "I need bait to fish." },
				{ "hc_speech_fish_no_water", "I can't find water nearby..." },
				{ "hc_speech_fish_got_away", "It got away!" },
				{ "hc_speech_fish_caught", "I caught a {0}!" },
				{ "hc_speech_fish_full", "My bags are full!" },
				{ "hc_speech_tomb_found", "My belongings!" },
				{ "hc_speech_tomb_recovered", "Got everything back." },
				{ "hc_speech_tomb_empty", "Nothing left to take." },
				{ "hc_speech_deposit_done", "All stowed away." },
				{ "hc_speech_deposit_empty", "Nothing to deposit." },
				{ "hc_cmd_comehere_1", "Coming!" },
				{ "hc_cmd_comehere_2", "On my way back!" },
				{ "hc_cmd_comehere_3", "Right behind you." },
				{ "hc_cmd_attack_1", "On it!" },
				{ "hc_cmd_attack_2", "Going in!" },
				{ "hc_cmd_attack_3", "I'll take them down!" },
				{ "hc_cmd_attack_4", "For Odin!" },
				{ "hc_cmd_cart_pull_1", "I'll haul this." },
				{ "hc_cmd_cart_pull_2", "Got the cart!" },
				{ "hc_cmd_cart_pull_3", "Let me pull." },
				{ "hc_cmd_cart_release_1", "Letting go." },
				{ "hc_cmd_cart_release_2", "Cart's free." },
				{ "hc_cmd_cart_release_3", "Released!" },
				{ "hc_cmd_door_1", "Getting the door." },
				{ "hc_cmd_door_2", "I'll get it." },
				{ "hc_cmd_door_3", "Door's open!" },
				{ "hc_cmd_sit_1", "Nice and warm." },
				{ "hc_cmd_sit_2", "Good spot to rest." },
				{ "hc_cmd_sit_3", "I'll sit here." },
				{ "hc_cmd_sleep_1", "Time for some rest." },
				{ "hc_cmd_sleep_2", "I could use some sleep." },
				{ "hc_cmd_sleep_3", "Wake me if you need me." },
				{ "hc_cmd_wake_1", "I'm up!" },
				{ "hc_cmd_wake_2", "Already?" },
				{ "hc_cmd_wake_3", "Right, let's go." },
				{ "hc_cmd_deposit_1", "Dropping off my haul." },
				{ "hc_cmd_deposit_2", "Storing the goods." },
				{ "hc_cmd_deposit_3", "Lightening my load." },
				{ "hc_cmd_deposit_empty_1", "I've got nothing to drop off." },
				{ "hc_cmd_deposit_empty_2", "Already empty." },
				{ "hc_cmd_harvest_1", "I'll get that." },
				{ "hc_cmd_harvest_2", "On it!" },
				{ "hc_cmd_harvest_3", "Looks like good stuff." },
				{ "hc_cmd_cancel_1", "Standing by." },
				{ "hc_cmd_cancel_2", "Awaiting orders." },
				{ "hc_cmd_cancel_3", "Ready when you are." },
				{ "hc_cmd_move_1", "Heading over." },
				{ "hc_cmd_move_2", "On my way." },
				{ "hc_cmd_move_3", "Moving out." },
				{ "hc_cmd_repair_1", "I'll fix my gear up." },
				{ "hc_cmd_repair_2", "Time for repairs." },
				{ "hc_cmd_repair_3", "This needs some work." },
				{ "hc_cmd_board_1", "Coming aboard!" },
				{ "hc_cmd_board_2", "All aboard!" },
				{ "hc_cmd_board_3", "I'll hop on." },
				{ "hc_cmd_repair_nothing_1", "Nothing to fix here." },
				{ "hc_cmd_repair_nothing_2", "My gear's fine." },
				{ "hc_cmd_repair_nothing_3", "No repairs needed." },
				{ "hc_cmd_tombstone_1", "I'll grab my things." },
				{ "hc_cmd_tombstone_2", "Let me get that." },
				{ "hc_cmd_tombstone_3", "On my way to pick that up." },
				{ "hc_cmd_smelt_1", "I'll keep the fires burning." },
				{ "hc_cmd_smelt_2", "Time to smelt." },
				{ "hc_cmd_smelt_3", "I'll tend the furnace." },
				{ "hc_radial_farmzones", "Farm Zones" },
				{ "hc_farmzone_title", "Select Crop" },
				{ "hc_farmzone_any", "Any Crop" },
				{ "hc_farmzone_hint", "[LMB] Place  [RMB] Remove  [Scroll] Resize  [RMB+Scroll] Rotate  [Esc] Exit" },
				{ "hc_farmzone_hint_gamepad", "[A] Place  [X] Remove  [Bumpers] Resize  [D-Pad] Rotate  [Start] Exit" },
				{ "hc_farmzone_full", "Maximum zones reached (4)" }
			};
		}
	}
	[BepInPlugin("com.profmags.companions", "Offline Companions", "1.2.7")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class CompanionsPlugin : BaseUnityPlugin
	{
		public const string PluginGUID = "com.profmags.companions";

		public const string PluginName = "Offline Companions";

		public const string PluginVersion = "1.2.7";

		private static Harmony _harmony;

		internal static ManualLogSource Log;

		private bool _fontFixWarned;

		private void Awake()
		{
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Log.LogInfo((object)"Offline Companions v1.2.7 loading...");
			ModConfig.Init(((BaseUnityPlugin)this).Config);
			_harmony = new Harmony("com.profmags.companions");
			try
			{
				_harmony.PatchAll(Assembly.GetExecutingAssembly());
				int num = 0;
				foreach (MethodBase patchedMethod in _harmony.GetPatchedMethods())
				{
					_ = patchedMethod;
					num++;
				}
				Log.LogInfo((object)string.Format("{0} loaded successfully! ({1} methods patched)", "Offline Companions", num));
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"[Companions] Harmony PatchAll failed: {arg}");
			}
			ModLocalization.Init();
			ModLocalization.EnsureDefaultFile();
			SceneManager.sceneLoaded += OnSceneLoaded;
			Game.m_playerInitialSpawn += CompanionManager.SpawnStarterCompanion;
			((Component)this).gameObject.AddComponent<CompanionVoice>();
			ConfigPanel.Create();
			FarmZonePlacer.EnsureInstance();
		}

		private void Update()
		{
			CompanionManager.ProcessRespawns(Time.deltaTime);
			TombstonePatches.UpdateResurrectionHold();
		}

		private void OnDestroy()
		{
			SceneManager.sceneLoaded -= OnSceneLoaded;
			Game.m_playerInitialSpawn -= CompanionManager.SpawnStarterCompanion;
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			ManualLogSource log = Log;
			if (log != null)
			{
				log.LogInfo((object)"Offline Companions unloaded.");
			}
		}

		private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
		{
			EnsureTmpDefaultFont("scene:" + ((Scene)(ref scene)).name);
		}

		private static bool IsBrokenTmpFont(TMP_FontAsset font)
		{
			if (!((Object)(object)font == (Object)null))
			{
				return ((Object)font).name.IndexOf("LiberationSans", StringComparison.OrdinalIgnoreCase) >= 0;
			}
			return true;
		}

		private static TMP_FontAsset FindReplacementTmpFont()
		{
			TMP_Text[] array = Resources.FindObjectsOfTypeAll<TMP_Text>();
			foreach (TMP_Text val in array)
			{
				if (!((Object)(object)val == (Object)null))
				{
					TMP_FontAsset font = val.font;
					if (!IsBrokenTmpFont(font))
					{
						return font;
					}
				}
			}
			TMP_FontAsset[] array2 = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
			TMP_FontAsset val2 = null;
			foreach (TMP_FontAsset val3 in array2)
			{
				if (!IsBrokenTmpFont(val3))
				{
					string text = ((Object)val3).name.ToLowerInvariant();
					if (text.Contains("averia") || text.Contains("norse") || text.Contains("valheim"))
					{
						return val3;
					}
					if ((Object)(object)val2 == (Object)null)
					{
						val2 = val3;
					}
				}
			}
			return val2;
		}

		private void EnsureTmpDefaultFont(string source)
		{
			if (!IsBrokenTmpFont(TMP_Settings.defaultFontAsset))
			{
				return;
			}
			TMP_FontAsset val = FindReplacementTmpFont();
			if ((Object)(object)val == (Object)null)
			{
				if (!_fontFixWarned)
				{
					_fontFixWarned = true;
					ManualLogSource log = Log;
					if (log != null)
					{
						log.LogDebug((object)("[Fonts] No replacement TMP font found yet (" + source + ") — will retry on next scene load."));
					}
				}
				return;
			}
			TMP_Settings.defaultFontAsset = val;
			_fontFixWarned = false;
			int num = 0;
			TMP_Text[] array = Resources.FindObjectsOfTypeAll<TMP_Text>();
			foreach (TMP_Text val2 in array)
			{
				if (!((Object)(object)val2 == (Object)null) && IsBrokenTmpFont(val2.font))
				{
					val2.font = val;
					num++;
				}
			}
			ManualLogSource log2 = Log;
			if (log2 != null)
			{
				log2.LogInfo((object)$"[Fonts] TMP default font repaired to '{((Object)val).name}' ({source}), reassigned={num}.");
			}
		}
	}
	internal static class ReflectionHelper
	{
		private static readonly FieldInfo _blockingField = AccessTools.Field(typeof(Character), "m_blocking");

		internal static readonly FieldInfo LeftItemField = AccessTools.Field(typeof(Humanoid), "m_leftItem");

		internal static readonly FieldInfo RightItemField = AccessTools.Field(typeof(Humanoid), "m_rightItem");

		private static readonly FieldInfo _blockTimerField = AccessTools.Field(typeof(Humanoid), "m_blockTimer");

		private static readonly FieldInfo _attackDrawTimeField = AccessTools.Field(typeof(Humanoid), "m_attackDrawTime");

		internal static readonly MethodInfo UpdateVisualsMethod = AccessTools.Method(typeof(VisEquipment), "UpdateVisuals", (Type[])null, (Type[])null);

		private static bool _warnedBlocking;

		private static bool _warnedBlockTimer;

		private static readonly FieldInfo _allStationsField = AccessTools.Field(typeof(CraftingStation), "m_allStations");

		private static readonly FieldInfo _projOwnerField = AccessTools.Field(typeof(Projectile), "m_owner");

		internal static bool TrySetBlocking(Character c, bool value)
		{
			if ((Object)(object)c == (Object)null || _blockingField == null)
			{
				WarnOnce(ref _warnedBlocking, "Character.m_blocking");
				return false;
			}
			try
			{
				_blockingField.SetValue(c, value);
				return true;
			}
			catch (Exception)
			{
				WarnOnce(ref _warnedBlocking, "Character.m_blocking");
				return false;
			}
		}

		internal static bool GetBlocking(Character c)
		{
			if ((Object)(object)c == (Object)null || _blockingField == null)
			{
				return false;
			}
			try
			{
				return (bool)_blockingField.GetValue(c);
			}
			catch
			{
				return false;
			}
		}

		internal static ItemData GetLeftItem(Humanoid h)
		{
			if ((Object)(object)h == (Object)null || LeftItemField == null)
			{
				return null;
			}
			try
			{
				object? value = LeftItemField.GetValue(h);
				return (ItemData)((value is ItemData) ? value : null);
			}
			catch
			{
				return null;
			}
		}

		internal static ItemData GetRightItem(Humanoid h)
		{
			if ((Object)(object)h == (Object)null || RightItemField == null)
			{
				return null;
			}
			try
			{
				object? value = RightItemField.GetValue(h);
				return (ItemData)((value is ItemData) ? value : null);
			}
			catch
			{
				return null;
			}
		}

		internal static bool TrySetBlockTimer(Humanoid h, float value)
		{
			if ((Object)(object)h == (Object)null || _blockTimerField == null)
			{
				WarnOnce(ref _warnedBlockTimer, "Humanoid.m_blockTimer");
				return false;
			}
			try
			{
				_blockTimerField.SetValue(h, value);
				return true;
			}
			catch (Exception)
			{
				WarnOnce(ref _warnedBlockTimer, "Humanoid.m_blockTimer");
				return false;
			}
		}

		internal static float GetBlockTimer(Humanoid h)
		{
			if ((Object)(object)h == (Object)null || _blockTimerField == null)
			{
				return -1f;
			}
			try
			{
				return (float)_blockTimerField.GetValue(h);
			}
			catch
			{
				return -1f;
			}
		}

		internal static bool TrySetAttackDrawTime(Humanoid h, float value)
		{
			if ((Object)(object)h == (Object)null || _attackDrawTimeField == null)
			{
				return false;
			}
			try
			{
				_attackDrawTimeField.SetValue(h, value);
				return true;
			}
			catch
			{
				return false;
			}
		}

		internal static List<CraftingStation> GetAllCraftingStations()
		{
			if (_allStationsField == null)
			{
				return null;
			}
			try
			{
				return _allStationsField.GetValue(null) as List<CraftingStation>;
			}
			catch
			{
				return null;
			}
		}

		internal static Character GetProjectileOwner(Projectile proj)
		{
			if ((Object)(object)proj == (Object)null || _projOwnerField == null)
			{
				return null;
			}
			try
			{
				object? value = _projOwnerField.GetValue(proj);
				return (Character)((value is Character) ? value : null);
			}
			catch
			{
				return null;
			}
		}

		private static void WarnOnce(ref bool flag, string fieldName)
		{
			if (!flag)
			{
				flag = true;
				CompanionsPlugin.Log.LogWarning((object)("[ReflectionHelper] Failed to access " + fieldName + " — this field may have been renamed in a game update."));
			}
		}
	}
	public static class CompanionManager
	{
		public struct RespawnData
		{
			public string PrefabName;

			public string Name;

			public string AppearanceSerialized;

			public string OwnerId;

			public int ActionMode;

			public int ActionModeSchema;

			public bool Follow;

			public bool StayHome;

			public bool AutoPickup;

			public bool Wander;

			public bool IsCommandable;

			public int FormationSlot;

			public long TombstoneId;

			public string SkillsSerialized;

			public float Timer;

			public bool HasHomePos;

			public Vector3 HomePos;
		}

		private const string BankDataKey = "TraderSharedBank_Balance";

		private static readonly List<RespawnData> _respawnQueue = new List<RespawnData>();

		public static int GetBankBalance()
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return 0;
			}
			if (localPlayer.m_customData.TryGetValue("TraderSharedBank_Balance", out var value) && int.TryParse(value, out var result))
			{
				return result;
			}
			return 0;
		}

		public static bool CanAfford()
		{
			return GetBankBalance() >= CompanionTierData.Price;
		}

		public static bool Purchase(CompanionAppearance appearance, CompanionTierDef def = null)
		{
			if (def == null)
			{
				def = CompanionTierData.Companion;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return false;
			}
			int price = CompanionTierData.Price;
			int bankBalance = GetBankBalance();
			if (bankBalance < price)
			{
				MessageHud instance = MessageHud.instance;
				if (instance != null)
				{
					instance.ShowMessage((MessageType)2, ModLocalization.LocFmt("hc_msg_need_coins", price.ToString("N0")), 0, (Sprite)null, false);
				}
				return false;
			}
			bankBalance -= price;
			localPlayer.m_customData["TraderSharedBank_Balance"] = bankBalance.ToString();
			if (!SpawnCompanion(appearance, def))
			{
				bankBalance += price;
				localPlayer.m_customData["TraderSharedBank_Balance"] = bankBalance.ToString();
				return false;
			}
			MessageHud instance2 = MessageHud.instance;
			if (instance2 != null)
			{
				instance2.ShowMessage((MessageType)2, ModLocalization.LocFmt("hc_msg_arrived", def.DisplayName.ToLower()), 0, (Sprite)null, false);
			}
			return true;
		}

		public static void RestoreFollowTargets()
		{
			foreach (CompanionSetup allCompanion in CompanionSetup.AllCompanions)
			{
				allCompanion.RestoreFollowTarget();
			}
		}

		public static void QueueRespawn(RespawnData data)
		{
			for (int i = 0; i < _respawnQueue.Count; i++)
			{
				RespawnData respawnData = _respawnQueue[i];
				if (!(respawnData.OwnerId == data.OwnerId))
				{
					continue;
				}
				bool num;
				if (data.TombstoneId == 0L)
				{
					if (!(respawnData.Name == data.Name))
					{
						continue;
					}
					num = respawnData.AppearanceSerialized == data.AppearanceSerialized;
				}
				else
				{
					num = respawnData.TombstoneId == data.TombstoneId;
				}
				if (num)
				{
					CompanionsPlugin.Log.LogWarning((object)("[CompanionManager] Duplicate respawn blocked for \"" + data.Name + "\" " + $"(owner={data.OwnerId} tombstoneId={data.TombstoneId}) — already queued"));
					return;
				}
			}
			_respawnQueue.Add(data);
			CompanionsPlugin.Log.LogInfo((object)$"[CompanionManager] Queued respawn for \"{data.Name}\" in {data.Timer}s");
		}

		public static void ProcessRespawns(float dt)
		{
			if (_respawnQueue.Count == 0)
			{
				return;
			}
			for (int num = _respawnQueue.Count - 1; num >= 0; num--)
			{
				RespawnData respawnData = _respawnQueue[num];
				respawnData.Timer -= dt;
				_respawnQueue[num] = respawnData;
				if (respawnData.Timer <= 0f)
				{
					_respawnQueue.RemoveAt(num);
					DoRespawn(respawnData);
				}
			}
		}

		private static void DoRespawn(RespawnData data)
		{
			//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_0195: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ac: 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_018c: 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_0210: Unknown result type (might be due to invalid IL or missing references)
			//IL_0216: Unknown result type (might be due to invalid IL or missing references)
			//IL_021e: 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_023d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			//IL_0241: Unknown result type (might be due to invalid IL or missing references)
			//IL_0372: Unknown result type (might be due to invalid IL or missing references)
			//IL_0424: Unknown result type (might be due to invalid IL or missing references)
			//IL_04b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_0453: Unknown result type (might be due to invalid IL or missing references)
			CompanionsPlugin.Log.LogInfo((object)("[CompanionManager] Processing respawn for \"" + data.Name + "\" — prefab=" + data.PrefabName));
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				data.Timer = 1f;
				_respawnQueue.Add(data);
				CompanionsPlugin.Log.LogDebug((object)"[CompanionManager] Player not loaded — re-queued respawn");
				return;
			}
			foreach (CompanionSetup allCompanion in CompanionSetup.AllCompanions)
			{
				ZNetView component = ((Component)allCompanion).GetComponent<ZNetView>();
				ZDO val = ((component != null) ? component.GetZDO() : null);
				if (val == null)
				{
					continue;
				}
				string @string = val.GetString(CompanionSetup.OwnerHash, "");
				string string2 = val.GetString(CompanionSetup.NameHash, "");
				if (@string == data.OwnerId && string2 == data.Name)
				{
					Character component2 = ((Component)allCompanion).GetComponent<Character>();
					if ((Object)(object)component2 != (Object)null && component2.GetHealth() > 0f)
					{
						CompanionsPlugin.Log.LogWarning((object)("[CompanionManager] Duplicate respawn aborted — \"" + data.Name + "\" " + $"already exists in scene with health {component2.GetHealth():F0}"));
						return;
					}
				}
			}
			Vector3 val2;
			string text;
			if (data.HasHomePos)
			{
				val2 = data.HomePos;
				text = "home";
			}
			else if ((Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile().HaveCustomSpawnPoint())
			{
				val2 = Game.instance.GetPlayerProfile().GetCustomSpawnPoint();
				text = "bed";
			}
			else
			{
				val2 = GetWorldSpawnPoint();
				text = "world spawn";
			}
			CompanionsPlugin.Log.LogInfo((object)$"[CompanionManager] Respawning at {text}: {val2:F1}");
			ZNetScene instance = ZNetScene.instance;
			GameObject val3 = ((instance != null) ? instance.GetPrefab(data.PrefabName) : null);
			if ((Object)(object)val3 == (Object)null)
			{
				CompanionsPlugin.Log.LogError((object)("[CompanionManager] Respawn failed — prefab not found: " + data.PrefabName));
				return;
			}
			Vector3 val4 = ((data.HasHomePos || text == "bed") ? val2 : FindSpawnPosition(val2, 4f));
			Quaternion val5 = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
			GameObject val6 = Object.Instantiate<GameObject>(val3, val4, val5);
			ZNetView component3 = val6.GetComponent<ZNetView>();
			if (((component3 != null) ? component3.GetZDO() : null) == null)
			{
				CompanionsPlugin.Log.LogError((object)"[CompanionManager] Respawn failed — ZDO not available.");
				Object.Destroy((Object)(object)val6);
				return;
			}
			ZDO zDO = component3.GetZDO();
			zDO.Set(CompanionSetup.AppearanceHash, data.AppearanceSerialized);
			zDO.Set(CompanionSetup.OwnerHash, data.OwnerId);
			zDO.Set(CompanionSetup.NameHash, data.Name);
			zDO.Set(CompanionSetup.TombstoneIdHash, data.TombstoneId);
			zDO.Set(CompanionSetup.FollowHash, data.Follow);
			zDO.Set(CompanionSetup.ActionModeHash, data.ActionMode, false);
			zDO.Set(CompanionSetup.ActionModeSchemaHash, data.ActionModeSchema, false);
			zDO.Set(CompanionSetup.StayHomeHash, data.StayHome);
			zDO.Set(CompanionSetup.AutoPickupHash, data.AutoPickup);
			zDO.Set(CompanionSetup.WanderHash, data.Wander);
			zDO.Set(CompanionSetup.IsCommandableHash, data.IsCommandable ? 1 : 0, false);
			zDO.Set(CompanionSetup.FormationSlotHash, data.FormationSlot, false);
			if (data.HasHomePos)
			{
				zDO.Set(CompanionSetup.HomePosHash, data.HomePos);
				zDO.Set(CompanionSetup.HomePosSetHash, true);
			}
			if (!string.IsNullOrEmpty(data.SkillsSerialized))
			{
				zDO.Set(CompanionSkills.SkillsHash, data.SkillsSerialized);
			}
			zDO.Persistent = true;
			CompanionAppearance a = CompanionAppearance.Deserialize(data.AppearanceSerialized);
			CompanionSetup component4 = val6.GetComponent<CompanionSetup>();
			if ((Object)(object)component4 != (Object)null)
			{
				component4.ApplyAppearance(a);
			}
			Character component5 = val6.GetComponent<Character>();
			if ((Object)(object)component5 != (Object)null && !string.IsNullOrEmpty(data.Name))
			{
				component5.m_name = data.Name;
			}
			CompanionAI component6 = val6.GetComponent<CompanionAI>();
			if (data.HasHomePos && (Object)(object)component6 != (Object)null)
			{
				component6.SetPatrolPointAt(data.HomePos);
			}
			if (data.StayHome && data.HasHomePos && (Object)(object)component6 != (Object)null)
			{
				component6.SetFollowTarget(null);
				component6.SetPatrolPointAt(data.HomePos);
			}
			else if (data.Follow && (Object)(object)Player.m_localPlayer != (Object)null)
			{
				component6?.SetFollowTarget(((Component)Player.m_localPlayer).gameObject);
			}
			if ((Object)(object)component6 != (Object)null)
			{
				component6.FreezeTimer = 5f;
			}
			CompanionsPlugin.Log.LogInfo((object)($"[CompanionManager] Respawned \"{data.Name}\" at {text} {val4:F1} — " + $"tombstoneId={data.TombstoneId}"));
			string text2 = ((text == "home") ? ModLocalization.Loc("hc_msg_location_home") : ((!(text == "bed")) ? ModLocalization.Loc("hc_msg_location_spawn") : ModLocalization.Loc("hc_msg_location_bed")));
			MessageHud instance2 = MessageHud.instance;
			if (instance2 != null)
			{
				instance2.ShowMessage((MessageType)2, ModLocalization.LocFmt("hc_msg_returned", data.Name, text2), 0, (Sprite)null, false);
			}
		}

		private static Vector3 GetWorldSpawnPoint()
		{
			//IL_003b: 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_0025: 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)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			Vector3 val = default(Vector3);
			if ((Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.GetLocationIcon(Game.instance.m_StartLocation, ref val))
			{
				return val + Vector3.up * 2f;
			}
			return Vector3.up * 2f;
		}

		public static void SpawnStarterCompanion()
		{
			if (!ModConfig.SpawnStarterCompanion.Value)
			{
				CompanionsPlugin.Log.LogDebug((object)"[CompanionManager] Starter companion disabled in config");
				return;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			string key = $"HC_StarterCompanion_{ZNet.instance.GetWorldUID()}";
			if (localPlayer.m_customData.ContainsKey(key))
			{
				CompanionsPlugin.Log.LogDebug((object)"[CompanionManager] Starter companion already spawned for this world");
				return;
			}
			string text = localPlayer.GetPlayerID().ToString();
			foreach (CompanionSetup allCompanion in CompanionSetup.AllCompanions)
			{
				ZNetView component = ((Component)allCompanion).GetComponent<ZNetView>();
				ZDO val = ((component != null) ? component.GetZDO() : null);
				if (val != null && val.GetString(CompanionSetup.OwnerHash, "") == text)
				{
					CompanionsPlugin.Log.LogInfo((object)"[CompanionManager] Found existing companion in scene — marking world key and skipping panel");
					localPlayer.m_customData[key] = "1";
					return;
				}
			}
			localPlayer.m_customData[key] = "1";
			if ((Object)(object)Game.instance != (Object)null)
			{
				Game.instance.SavePlayerProfile(false);
			}
			CompanionsPlugin.Log.LogInfo((object)"[CompanionManager] First spawn in world — showing starter companion panel");
			StarterCompanionPanel.ShowPanel();
		}

		public static void SpawnStarterWithAppearance(CompanionAppearance appearance)
		{
			if (!((Object)(object)Player.m_localPlayer == (Object)null) && SpawnCompanion(appearance, CompanionTierData.Companion))
			{
				CompanionsPlugin.Log.LogInfo((object)"[CompanionManager] Starter companion spawned with custom appearance!");
				MessageHud instance = MessageHud.instance;
				if (instance != null)
				{
					instance.ShowMessage((MessageType)2, ModLocalization.Loc("hc_msg_starter_joined"), 0, (Sprite)null, false);
				}
			}
		}

		private static bool SpawnCompanion(CompanionAppearance appearance, CompanionTierDef def)
		{
			//IL_0049: 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_0058: 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_0077: 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)
			ZNetScene instance = ZNetScene.instance;
			GameObject val = ((instance != null) ? instance.GetPrefab(def.PrefabName) : null);
			if ((Object)(object)val == (Object)null)
			{
				CompanionsPlugin.Log.LogError((object)("[CompanionManager] Prefab not found: " + def.PrefabName));
				return false;
			}
			Player localPlayer = Player.m_localPlayer;
			Vector3 val2 = FindSpawnPosition(((Component)localPlayer).transform.position, 4f);
			Quaternion val3 = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
			GameObject val4 = Object.Instantiate<GameObject>(val, val2, val3);
			ZNetView component = val4.GetComponent<ZNetView>();
			if (((component != null) ? component.GetZDO() : null) == null)
			{
				CompanionsPlugin.Log.LogError((object)"[CompanionManager] ZDO not available after spawn — aborting.");
				Object.Destroy((Object)(object)val4);
				return false;
			}
			ZDO zDO = component.GetZDO();
			zDO.Set(CompanionSetup.AppearanceHash, appearance.Serialize());
			zDO.Set(CompanionSetup.OwnerHash, localPlayer.GetPlayerID().ToString());
			zDO.Persistent = true;
			CompanionSetup component2 = val4.GetComponent<CompanionSetup>();
			if ((Object)(object)component2 != (Object)null)
			{
				component2.ApplyAppearance(appearance);
			}
			val4.GetComponent<CompanionAI>()?.SetFollowTarget(((Component)localPlayer).gameObject);
			CompanionsPlugin.Log.LogInfo((object)$"[CompanionManager] Spawned companion for player {localPlayer.GetPlayerID()}");
			return true;
		}

		private static Vector3 FindSpawnPosition(Vector3 origin, float radius)
		{
			//IL_0004: 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_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)
			//IL_0011: 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_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_002c: 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_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_003f: 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)
			float y = default(float);
			for (int i = 0; i < 20; i++)
			{
				Vector2 val = Random.insideUnitCircle * radius;
				Vector3 val2 = origin + new Vector3(val.x, 0f, val.y);
				if ((Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.FindFloor(val2, ref y))
				{
					val2.y = y;
					return val2;
				}
			}
			return origin + Vector3.right * 2f;
		}
	}
	[Serializable]
	public class SpeechConfig
	{
		public string[] Action;

		public string[] Gather;

		public string[] Forage;

		public string[] Combat;

		public string[] Follow;

		public string[] Hungry;

		public string[] Repair;

		public string[] Overweight;

		public string[] Smelt;

		public string[] Idle;

		private static SpeechConfig _instance;

		public static SpeechConfig Instance
		{
			get
			{
				if (_instance == null)
				{
					Load();
				}
				return _instance;
			}
		}

		public static void Reload()
		{
			_instance = null;
			Load();
		}

		public static void Load()
		{
			string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			string text = "English";
			try
			{
				if (Localization.instance != null)
				{
					text = Localization.instance.GetSelectedLanguage();
				}
			}
			catch
			{
			}
			if (TryLoadFrom(Path.Combine(directoryName, "Translations", "speech", text + ".json")))
			{
				return;
			}
			string text2 = Path.Combine(directoryName, "speech.json");
			if (TryLoadFrom(text2))
			{
				return;
			}
			_instance = Defaults();
			try
			{
				string contents = JsonUtility.ToJson((object)_instance, true);
				File.WriteAllText(text2, contents);
				CompanionsPlugin.Log.LogInfo((object)("[Speech] Created default " + text2));
			}
			catch (Exception ex)
			{
				CompanionsPlugin.Log.LogWarning((object)("[Speech] Failed to write defaults to " + text2 + ": " + ex.Message));
			}
		}

		private static bool TryLoadFrom(string path)
		{
			if (!File.Exists(path))
			{
				return false;
			}
			try
			{
				_instance = JsonUtility.FromJson<SpeechConfig>(File.ReadAllText(path));
				CompanionsPlugin.Log.LogInfo((object)("[Speech] Loaded " + path));
				return true;
			}
			catch (Exception ex)
			{
				CompanionsPlugin.Log.LogWarning((object)("[Speech] Failed to parse " + path + ": " + ex.Message));
				return false;
			}
		}

		private static SpeechConfig Defaults()
		{
			SpeechConfig speechConfig = new SpeechConfig();
			speechConfig.Action = new string[5] { "By your word!", "So it shall be.", "The gods guide my hand.", "Aye, consider it done!", "I heed your call." };
			speechConfig.Gather = new string[6] { "The land provides, if you know where to look.", "Good timber here.", "Odin's bounty is plentiful.", "I'll strip this land bare if I must.", "These arms were made for more than swinging axes... but it'll do.", "The earth yields its riches." };
			speechConfig.Forage = new string[5] { "The meadows offer their gifts.", "Freya's garden blooms well here.", "Even warriors must forage.", "Ripe for the picking.", "The wild provides for those who seek." };
			speechConfig.Combat = new string[7] { "For Odin!", "Taste my steel!", "To Valhalla!", "Stand and fight!", "Skål! Come meet your end!", "They shall not pass!", "By Thor's hammer!" };
			speechConfig.Follow = new string[6] { "Lead on, I am your shield.", "Where the path takes us, I follow.", "A fine day to wander the wilds.", "My blade is yours to command.", "The road calls to us.", "I walk beside you, friend." };
			speechConfig.Hungry = new string[5] { "My belly roars like a troll...", "Even Fenrir ate better than this.", "A warrior fights on mead and meat, not empty guts.", "I'd trade my axe for a leg of boar right now.", "The hunger gnaws at my strength..." };
			speechConfig.Repair = new string[5] { "This blade has seen better days.", "My armor holds by thread and prayer.", "A dull edge brings a swift death.", "The smithy calls to my gear.", "Even the gods mend their weapons." };
			speechConfig.Overweight = new string[5] { "By Odin's beard, my back is breaking!", "I carry the weight of Jotunheim on my shoulders...", "Not even Thor could haul this much further!", "My legs buckle beneath this burden!", "We must lighten this load before I collapse." };
			speechConfig.Smelt = new string[6] { "The forge fire burns bright.", "Good ore makes good steel.", "The bellows sing their song.", "Another ingot for the hoard.", "Dwarven work, this smelting.", "The flames hunger for more." };
			speechConfig.Idle = new string[6] { "The winds whisper of adventure...", "Skål!", "A calm before the storm, perhaps.", "I could use a horn of mead.", "I sense something stirring in the mist.", "What tales will they sing of us, I wonder?" };
			return speechConfig;
		}
	}
	public static class CombatPatches
	{
		[HarmonyPatch(typeof(Character), "RPC_Damage")]
		private static class RPC_Damage_NoBackstab
		{
			private static void Prefix(Character __instance, HitData hit)
			{
				if (hit != null && !(hit.m_backstabBonus <= 1f) && !((Object)(object)((Component)__instance).GetComponent<CompanionSetup>() == (Object)null))
				{
					hit.m_backstabBonus = 1f;
				}
			}
		}

		[HarmonyPatch(typeof(Character), "GetMaxHealth")]
		private static class GetMaxHealth_Patch
		{
			private static void Postfix(Character __instance, ref float __result)
			{
				BaseAI baseAI = __instance.GetBaseAI();
				if ((Object)(object)baseAI == (Object)null || !(baseAI is CompanionAI))
				{
					return;
				}
				CompanionFood component = ((Component)__instance).GetComponent<CompanionFood>();
				if (!((Object)(object)component == (Object)null))
				{
					float baseHealth = CompanionFood.BaseHealth;
					float totalHealthBonus = component.TotalHealthBonus;
					float num = baseHealth + totalHealthBonus;
					if (Time.time - _lastHealthLogTime > 5f)
					{
						_lastHealthLogTime = Time.time;
						CompanionsPlugin.Log.LogDebug((object)($"[Combat] GetMaxHealth — base={baseHealth:F0} + food={totalHealthBonus:F1} " + $"= {num:F1} (vanilla was {__result:F1}) " + "companion=\"" + __instance.m_name + "\""));
					}
					__result = num;
				}
			}
		}

		[HarmonyPatch(typeof(BaseAI), "CanUseAttack")]
		public static class ToolCombatExclude
		{
			private static void Postfix(BaseAI __instance, ItemData item, ref bool __result)
			{
				//IL_0014: Unknown result type (might be due to invalid IL or missing references)
				//IL_001b: Invalid comparison between Unknown and I4
				//IL_0022: 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_0044: Invalid comparison between Unknown and I4
				if (__result && __instance is CompanionAI)
				{
					if ((int)item.m_shared.m_animationState == 9)
					{
						__result = false;
					}
					else if (item.GetDamage().m_pickaxe > 0f)
					{
						__result = false;
					}
					else if ((int)item.m_shared.m_itemType == 19)
					{
						__result = false;
					}
				}
			}
		}

		[HarmonyPatch(typeof(Humanoid), "EquipBestWeapon")]
		private static class EquipBestWeapon_PreferHighestDamage
		{
			private static void Postfix(Humanoid __instance)
			{
				//IL_007b: Unknown result type (might be due to invalid IL or missing references)
				BaseAI baseAI = ((Character)__instance).GetBaseAI();
				if ((Object)(object)baseAI == (Object)null || !(baseAI is CompanionAI))
				{
					return;
				}
				ItemData currentWeapon = __instance.GetCurrentWeapon();
				if (currentWeapon == null)
				{
					return;
				}
				float combatDamage = GetCombatDamage(currentWeapon);
				Inventory inventory = __instance.GetInventory();
				if (inventory == null)
				{
					return;
				}
				ItemData val = currentWeapon;
				float num = combatDamage;
				foreach (ItemData allItem in inventory.GetAllItems())
				{
					if (allItem != null && allItem.m_shared != null && allItem.IsWeapon() && baseAI.CanUseAttack(allItem) && (int)allItem.m_shared.m_aiTargetType == 0 && (!allItem.m_shared.m_useDurability || !(allItem.m_durability <= 0f)))
					{
						float combatDamage2 = GetCombatDamage(allItem);
						if (combatDamage2 > num)
						{
							val = allItem;
							num = combatDamage2;
						}
					}
				}
				if (val != currentWeapon)
				{
					__instance.EquipItem(val, true);
				}
			}

			private static float GetCombatDamage(ItemData item)
			{
				//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_0007: 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_0014: 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_0022: 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_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_003e: Unknown result type (might be due to invalid IL or missing references)
				DamageTypes damage = item.GetDamage();
				return damage.m_damage + damage.m_blunt + damage.m_slash + damage.m_pierce + damage.m_fire + damage.m_frost + damage.m_lightning + damage.m_poison + damage.m_spirit;
			}
		}

		[HarmonyPatch(typeof(Destructible), "Damage")]
		private static class Destructible_SuppressHarvestPhysical
		{
			private static bool Prefix(Destructible __instance, HitData hit)
			{
				if (HarvestController.IsApplyingAoeDamage)
				{
					return true;
				}
				Character attacker = hit.GetAttacker();
				if ((Object)(object)attacker == (Object)null)
				{
					return true;
				}
				HarvestController component = ((Component)attacker).GetComponent<HarvestController>();
				if ((Object)(object)component != (Object)null && component.IsAttackingRockTarget(((Component)__instance).gameObject))
				{
					component.TryApplyRockAoe();
					return false;
				}
				return true;
			}
		}

		[HarmonyPatch(typeof(MineRock), "Damage")]
		private static class MineRock_SuppressHarvestPhysical
		{
			private static bool Prefix(MineRock __instance, HitData hit)
			{
				if (HarvestController.IsApplyingAoeDamage)
				{
					return true;
				}
				Character attacker = hit.GetAttacker();
				if ((Object)(object)attacker == (Object)null)
				{
					return true;
				}
				HarvestController component = ((Component)attacker).GetComponent<HarvestController>();
				if ((Object)(object)component != (Object)null && component.IsAttackingRockTarget(((Component)__instance).gameObject))
				{
					component.TryApplyRockAoe();
					return false;
				}
				return true;
			}
		}

		[HarmonyPatch(typeof(MineRock5), "Damage")]
		private static class MineRock5_SuppressHarvestPhysical
		{
			private static bool Prefix(MineRock5 __instance, HitData hit)
			{
				if (HarvestController.IsApplyingAoeDamage)
				{
					return true;
				}
				Character attacker = hit.GetAttacker();
				if ((Object)(object)attacker == (Object)null)
				{
					return true;
				}
				HarvestController component = ((Component)attacker).GetComponent<HarvestController>();
				if ((Object)(object)component != (Object)null && component.IsAttackingRockTarget(((Component)__instance).gameObject))
				{
					component.TryApplyRockAoe();
					return false;
				}
				return true;
			}
		}

		[HarmonyPatch(typeof(Attack), "SpawnOnHitTerrain")]
		private static class SpawnOnHitTerrain_NoNpcTerrain
		{
			private static bool Prefix(Character character)
			{
				if ((Object)(object)character == (Object)null || character.IsPlayer())
				{
					return true;
				}
				((Component)character).GetComponent<HarvestController>()?.NotifyTerrainHit();
				return false;
			}
		}

		[HarmonyPatch(typeof(SE_Poison), "AddDamage")]
		private static class SE_Poison_AddDamage_Patch
		{
			private static void Prefix(SE_Poison __instance, ref float __state)
			{
				__state = __instance.m_TTLPerDamage;
				if ((Object)(object)((StatusEffect)__instance).m_character != (Object)null && (Object)(object)((Component)((StatusEffect)__instance).m_character).GetComponent<CompanionSetup>() != (Object)null)
				{
					__instance.m_TTLPerDamage = __instance.m_TTLPerDamagePlayer;
				}
			}

			private static void Postfix(SE_Poison __instance, float __state)
			{
				__instance.m_TTLPerDamage = __state;
			}
		}

		private static float _lastHealthLogTime;

		private const float HealthLogInterval = 5f;
	}
	public static class DurabilityPatches
	{
		[HarmonyPatch(typeof(Humanoid), "StartAttack")]
		private static class WeaponDurability_Patch
		{
			private static void Postfix(Humanoid __instance, bool __result)
			{
				if (!__result || (Object)(object)((Component)__instance).GetComponent<CompanionSetup>() == (Object)null)
				{
					return;
				}
				ItemData val = ReflectionHelper.GetRightItem(__instance);
				if (val == null)
				{
					val = ReflectionHelper.GetLeftItem(__instance);
				}
				if (val != null && val.m_shared.m_useDurability)
				{
					float durability = val.m_durability;
					float useDurabilityDrain = val.m_shared.m_useDurabilityDrain;
					ItemData obj = val;
					obj.m_durability -= useDurabilityDrain;
					float maxDurability = val.GetMaxDurability();
					float num = ((maxDurability > 0f) ? (val.m_durability / maxDurability * 100f) : 0f);
					CompanionsPlugin.Log.LogDebug((object)("[Durability] Weapon drain — \"" + val.m_shared.m_name + "\" " + $"durability {durability:F1} → {val.m_durability:F1} / {maxDurability:F0} " + $"({num:F0}%) drain={useDurabilityDrain:F1} " + "companion=\"" + ((Character)__instance).m_name + "\""));
					if (val.m_durability <= 0f)
					{
						val.m_durability = 0f;
						__instance.UnequipItem(val, false);
						CompanionsPlugin.Log.LogWarning((object)("[Durability] Weapon BROKEN — \"" + val.m_shared.m_name + "\" unequipped, companion=\"" + ((Character)__instance).m_name + "\""));
					}
				}
			}
		}

		[HarmonyPatch(typeof(Character), "RPC_Damage")]
		private static class ArmorDurability_Patch
		{
			private static readonly List<ItemData> _armorPieces = new List<ItemData>(4);

			private static void Prefix(Character __instance, HitData hit)
			{
				CompanionSetup component = ((Component)__instance).GetComponent<CompanionSetup>();
				if (!((Object)(object)component == (Object)null))
				{
					float totalArmor = component.GetTotalArmor();
					if (totalArmor > 0f)
					{
						float totalDamage = hit.GetTotalDamage();
						hit.ApplyArmor(totalArmor);
						float totalDamage2 = hit.GetTotalDamage();
						CompanionsPlugin.Log.LogDebug((object)($"[Durability] Armor reduction — totalArmor={totalArmor:F1} " + $"dmg {totalDamage:F1} → {totalDamage2:F1} " + $"(blocked {totalDamage - totalDamage2:F1}) " + "companion=\"" + __instance.m_name + "\""));
					}
				}
			}

			private static void Postfix(Character __instance, HitData hit)
			{
				if ((Object)(object)((Component)__instance).GetComponent<CompanionSetup>() == (Object)null)
				{
					return;
				}
				Humanoid val = (Humanoid)(object)((__instance is Humanoid) ? __instance : null);
				if ((Object)(object)val == (Object)null)
				{
					return;
				}
				_armorPieces.Clear();
				List<ItemData> armorPieces = _armorPieces;
				object? obj = _chestItem?.GetValue(val);
				AddIfValid(armorPieces, (ItemData)((obj is ItemData) ? obj : null));
				List<ItemData> armorPieces2 = _armorPieces;
				object? obj2 = _legItem?.GetValue(val);
				AddIfValid(armorPieces2, (ItemData)((obj2 is ItemData) ? obj2 : null));
				List<ItemData> armorPieces3 = _armorPieces;
				object? obj3 = _helmetItem?.GetValue(val);
				AddIfValid(armorPieces3, (ItemData)((obj3 is ItemData) ? obj3 : null));
				List<ItemData> armorPieces4 = _armorPieces;
				object? obj4 = _shoulderItem?.GetValue(val);
				AddIfValid(armorPieces4, (ItemData)((obj4 is ItemData) ? obj4 : null));
				if (_armorPieces.Count == 0)
				{
					CompanionsPlugin.Log.LogDebug((object)("[Durability] No armor with durability equipped — skipping drain companion=\"" + __instance.m_name + "\""));
					return;
				}
				float num = hit.GetTotalPhysicalDamage() + hit.GetTotalElementalDamage();
				if (!(num <= 0f))
				{
					ItemData val2 = _armorPieces[Random.Range(0, _armorPieces.Count)];
					float durability = val2.m_durability;
					val2.m_durability = Mathf.Max(0f, val2.m_durability - num);
					float maxDurability = val2.GetMaxDurability();
					float num2 = ((maxDurability > 0f) ? (val2.m_durability / maxDurability * 100f) : 0f);
					CompanionsPlugin.Log.LogDebug((object)("[Durability] Armor drain — \"" + val2.m_shared.m_name + "\" " + $"durability {durability:F1} → {val2.m_durability:F1} / {maxDurability:F0} " + $"({num2:F0}%) dmgTaken={num:F1} " + $"armorPieces={_armorPieces.Count} companion=\"{__instance.m_name}\""));
					if (val2.m_durability <= 0f)
					{
						val.UnequipItem(val2, false);
						CompanionsPlugin.Log.LogWarning((object)("[Durability] Armor BROKEN — \"" + val2.m_shared.m_name + "\" unequipped, companion=\"" + __instance.m_name + "\""));
					}
				}
			}
		}

		private static readonly FieldInfo _chestItem = AccessTools.Field(typeof(Humanoid), "m_chestItem");

		private static readonly FieldInfo _legItem = AccessTools.Field(typeof(Humanoid), "m_legItem");

		private static readonly FieldInfo _helmetItem = AccessTools.Field(typeof(Humanoid), "m_helmetItem");

		private static readonly FieldInfo _shoulderItem = AccessTools.Field(typeof(Humanoid), "m_shoulderItem");

		private static void AddIfValid(List<ItemData> list, ItemData item)
		{
			if (item != null && item.m_shared != null && item.m_shared.m_useDurability)
			{
				list.Add(item);
			}
		}
	}
	public static class EnemyHudPatches
	{
		private struct CompanionBars
		{
			public GuiBar StaminaBar;

			public GuiBar WeightBar;

			public CompanionStamina StaminaComp;
		}

		[HarmonyPatch(typeof(EnemyHud), "ShowHud")]
		private static class ShowHud_Patch
		{
			private static void Postfix(EnemyHud __instance, Character c)
			{
				//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_00af: 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)
				if ((Object)(object)c == (Object)null || (Object)(object)((Component)c).GetComponent<CompanionStamina>() == (Object)null)
				{
					return;
				}
				int instanceID = ((Object)c).GetInstanceID();
				if (_bars.ContainsKey(instanceID))
				{
					return;
				}
				IDictionary huds = GetHuds(__instance);
				if (huds == null || !huds.Contains(c))
				{
					return;
				}
				GameObject hudGui = GetHudGui(huds[c]);
				if ((Object)(object)hudGui == (Object)null)
				{
					return;
				}
				Transform val = hudGui.transform.Find("Health");
				if ((Object)(object)val == (Object)null)
				{
					return;
				}
				Rect rect = ((Component)val).GetComponent<RectTransform>().rect;
				float num = ((Rect)(ref rect)).height;
				if (num <= 0f)
				{
					num = 8f;
				}
				float num2 = num + 2f;
				GuiBar val2 = CreateBar(val, hudGui.transform, "CompanionStamina", StaminaYellow, num2);
				float yOffset = num2 + num + 2f;
				GuiBar val3 = CreateBar(val, hudGui.transform, "CompanionWeight", WeightBrown, yOffset);
				if ((Object)(object)val2 != (Object)null)
				{
					CompanionStamina component = ((Component)c).GetComponent<CompanionStamina>();
					val2.SetValue(component.GetStaminaPercentage());
				}
				if ((Object)(object)val3 != (Object)null)
				{
					Humanoid component2 = ((Component)c).GetComponent<Humanoid>();
					if ((Object)(object)component2 != (Object)null)
					{
						Inventory inventory = component2.GetInventory();
						float value = ((inventory != null) ? Mathf.Clamp01(inventory.GetTotalWeight() / CompanionTierData.MaxCarryWeight) : 0f);
						val3.SetValue(value);
					}
				}
				_bars[instanceID] = new CompanionBars
				{
					StaminaBar = val2,
					WeightBar = val3,
					StaminaComp = ((Component)c).GetComponent<CompanionStamina>()
				};
				_barCharacters[instanceID] = c;
				CompanionsPlugin.Log.LogDebug((object)($"[HUD] Created companion bars — stamina={(Object)(object)val2 != (Object)null} " + $"weight={(Object)(object)val3 != (Object)null} companion=\"{c.m_name}\""));
			}
		}

		[HarmonyPatch(typeof(EnemyHud), "OnDestroy")]
		private static class OnDestroy_Patch
		{
			private static void Postfix()
			{
				_bars.Clear();
				_barCharacters.Clear();
				_cachedHuds = null;
				_cachedHudInstance = null;
				_hudDataGuiField = null;
			}
		}

		[HarmonyPatch(typeof(EnemyHud), "UpdateHuds")]
		private static class UpdateHuds_Patch
		{
			private static void Postfix(EnemyHud __instance)
			{
				HideHudForRadialTarget(__instance);
				List<int> list = null;
				foreach (KeyValuePair<int, CompanionBars> bar in _bars)
				{
					_barCharacters.TryGetValue(bar.Key, out var value);
					if ((Object)(object)value == (Object)null || ((Object)(object)bar.Value.StaminaBar == (Object)null && (Object)(object)bar.Value.WeightBar == (Object)null))
					{
						if (list == null)
						{
							list = new List<int>();
						}
						list.Add(bar.Key);
						continue;
					}
					CompanionBars value2 = bar.Value;
					if ((Object)(object)value2.StaminaBar != (Object)null && (Object)(object)value2.StaminaComp != (Object)null)
					{
						value2.StaminaBar.SetValue(value2.StaminaComp.GetStaminaPercentage());
					}
					if ((Object)(object)value2.WeightBar != (Object)null)
					{
						Humanoid val = (Humanoid)(object)((value is Humanoid) ? value : null);
						if ((Object)(object)val != (Object)null)
						{
							Inventory inventory = val.GetInventory();
							float value3 = ((inventory != null) ? Mathf.Clamp01(inventory.GetTotalWeight() / CompanionTierData.MaxCarryWeight) : 0f);
							value2.WeightBar.SetValue(value3);
						}
					}
				}
				if (list != null)
				{
					for (int i = 0; i < list.Count; i++)
					{
						CompanionsPlugin.Log.LogDebug((object)"[HUD] Removing stale companion bars — character destroyed or null");
						_bars.Remove(list[i]);
						_barCharacters.Remove(list[i]);
					}
				}
			}
		}

		private static readonly Color StaminaYellow = new Color(1f, 0.8f, 0.1f, 1f);

		private static readonly Color WeightBrown = new Color(0.72f, 0.53f, 0.26f, 1f);

		private static readonly Dictionary<int, CompanionBars> _bars = new Dictionary<int, CompanionBars>();

		private static readonly Dictionary<int, Character> _barCharacters = new Dictionary<int, Character>();

		private static IDictionary _cachedHuds;

		private static EnemyHud _cachedHudInstance;

		private static FieldInfo _hudDataGuiField;

		private static IDictionary GetHuds(EnemyHud instance)
		{
			if ((Object)(object)_cachedHudInstance != (Object)(object)instance || _cachedHuds == null)
			{
				_cachedHuds = Traverse.Create((object)instance).Field("m_huds").GetValue() as IDictionary;
				_cachedHudInstance = instance;
			}
			return _cachedHuds;
		}

		private static GameObject GetHudGui(object hudData)
		{
			if (hudData == null)
			{
				return null;
			}
			if (_hudDataGuiField == null)
			{
				_hudDataGuiField = hudData.GetType().GetField("m_gui", BindingFlags.Instance | BindingFlags.Public);
			}
			object? obj = _hudDataGuiField?.GetValue(hudData);
			return (GameObject)((obj is GameObject) ? obj : null);
		}

		private static GuiBar CreateBar(Transform healthTransfor