Decompiled source of Skaft v1.0.0

plugins/Skaft/Skaft.dll

Decompiled 10 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Ezomic.Core;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("Thijssen Software")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 Robbin Thijssen")]
[assembly: AssemblyDescription("Hammer repair reaches further the higher your Crafting skill.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+b29df4b0293d401c53fa0369728bad9ae8d8e1b1")]
[assembly: AssemblyProduct("Skaft")]
[assembly: AssemblyTitle("Skaft")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[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 Skaft
{
	internal static class SkaftConfig
	{
		public static ConfigEntry<bool> Enabled;

		public static ConfigEntry<float> MinRadius;

		public static ConfigEntry<float> MaxRadius;

		public static ConfigEntry<float> FullLevel;

		public static ConfigEntry<float> Curve;

		public static ConfigEntry<float> CostMultiplier;

		public static ConfigEntry<float> DurabilityFloor;

		public static ConfigEntry<int> MaxPieces;

		public static ConfigEntry<bool> OwnBuildingsOnly;

		public static ConfigEntry<bool> ShowReachInBuildMenu;

		public static ConfigEntry<string> ReachEntries;

		public static ConfigEntry<bool> Verbose;

		private static string _reachRaw;

		private static HashSet<string> _reachSet;

		internal static bool IsReachEntry(string prefabName)
		{
			string text = ReachEntries.Value ?? string.Empty;
			if (text != _reachRaw || _reachSet == null)
			{
				_reachRaw = text;
				_reachSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				string[] array = text.Split(new char[1] { ',' });
				for (int i = 0; i < array.Length; i++)
				{
					string text2 = array[i].Trim();
					if (text2.Length > 0)
					{
						_reachSet.Add(text2);
					}
				}
			}
			return _reachSet.Contains(prefabName);
		}

		public static void Bind(ConfigFile config)
		{
			Enabled = config.Bind<bool>("Skaft", "Enabled", true, "Whether the repair sweep runs at all. Off leaves vanilla single-piece repair untouched - which is also exactly what this mod does at Crafting 0, so turning it off is only useful for telling the mod apart from a low skill level.");
			MinRadius = config.Bind<float>("Skaft", "MinRadius", 0f, "Sweep radius in metres at Crafting 0, measured from the piece under your cursor. Zero on purpose: a new character gets vanilla repair, one piece per swing, and the mod is invisible until the skill is worth something. At zero the sweep does not run at all, so a new character cannot meet a bug in it. Raise this only if you want the reach handed over rather than earned.");
			MaxRadius = config.Bind<float>("Skaft", "MaxRadius", 8f, "Sweep radius in metres once Crafting reaches FullLevel. Vanilla lets you touch a piece from 5 metres away, so 8 metres around what you are already touching is the outer edge of \"working on this building\". At 8 a mid-wall swing covers a 10x6 longhouse end to end. Larger numbers repair things you cannot see, and your stamina stops paying for them long before the radius runs out.");
			FullLevel = config.Bind<float>("Skaft", "FullLevel", 60f, "The Crafting level at which the radius reaches MaxRadius. Above it nothing changes. 100 is the wrong number to design against: it costs roughly 20,300 crafts, and Crafting only rises by crafting or upgrading at a station and by repairing a worn item - repairing buildings trains nothing at all. 60 is about 5,700, which a long playthrough actually reaches, so the reward arrives instead of dangling.");
			Curve = config.Bind<float>("Skaft", "Curve", 0.8f, "Exponent on the skill fraction before it is lerped between MinRadius and MaxRadius: radius = min + (max-min) * (level/FullLevel)^Curve. 1.0 is a straight line. Below 1 opens the reach earlier, above 1 hoards it for the top levels. The default keeps the first ten levels indistinguishable from vanilla - 1.9m cannot reach a neighbour two metres away - then grows steadily, because the experience curve is already brutal at the top and stacking a second brake on it puts the whole mod somewhere nobody goes.");
			CostMultiplier = config.Bind<float>("Skaft", "CostMultiplier", 1f, "Multiplies the stamina, eitr and hammer durability charged for each piece the sweep actually repairs. 1 means every piece costs exactly what vanilla charges to repair one piece by hand. That per-piece price is what stops a wide radius being free: your stamina bar, not the radius, decides how many pieces a swing fixes. Above 1 makes a sweep dearer than doing it by hand. Below 1 is the setting that deletes the point of this mod. Pieces already at full health cost nothing either way.");
			DurabilityFloor = config.Bind<float>("Skaft", "DurabilityFloor", 1f, "The sweep stops before the hammer would drop to or below this. Repairing subtracts durability without checking zero, so a wide sweep can spend an entire hammer inside one click - the game tells you it broke, but it broke on a single press rather than over the swings that wore it down, and it unequips, which drops you out of build mode mid-job. Zero lets a sweep spend the hammer to its last point and break it on the next swing, the normal way.");
			MaxPieces = config.Bind<int>("Skaft", "MaxPieces", 200, "Hard ceiling on pieces repaired in one swing, whatever the radius says. This is a network guard, not a balance number - each repaired piece is one message to the piece's owner and one broadcast back, so 200 is 400 messages in a single frame. Balance is stamina and durability.");
			OwnBuildingsOnly = config.Bind<bool>("Skaft", "OwnBuildingsOnly", false, "Restrict the sweep to pieces you placed yourself. False matches vanilla, which happily repairs anyone's building and leaves permission entirely to wards. True is for shared bases where you would rather not top up a neighbour's walls with your own stamina. The piece under your cursor is always vanilla's business, not this setting's.");
			ShowReachInBuildMenu = config.Bind<bool>("Skaft", "ShowReachInBuildMenu", true, "Add a line to the Repair entry in the build menu showing your current reach in metres and the Crafting level it came from. This is the only place the number is shown; after a swing, the \"Repaired x12\" count tells you the rest.");
			ReachEntries = config.Bind<string>("Skaft", "ReachEntries", "piece_repair", "Comma separated prefab names of the build menu entries the reach line is written on. It is not simply \"whatever is flagged as a repair entry\", because that flag means \"this entry clicks on the world instead of placing into it\" and other mods use it for their own tools - Vaettir's Transplant entry on the cultivator wears it, and the sweep can never run there. Add a name here if some other mod's repair entry does fall through to the game's own repair; the sweep itself is unaffected either way.");
			Verbose = config.Bind<bool>("Diagnostics", "Verbose", false, "One line per swing to BepInEx/LogOutput.log: skill level, radius, candidates found, pieces repaired, and what stopped the sweep. For tuning the curve, noisy in normal play.");
		}
	}
	internal static class SkaftPatches
	{
		private const float ReachInterval = 1f;

		private static float _nextReach;

		private static readonly HashSet<string> _unknownEntries = new HashSet<string>();

		private static Piece _described;

		private static string _originalDescription;

		private static string _writtenDescription;

		[HarmonyPostfix]
		[HarmonyPatch(typeof(Player), "Repair", new Type[]
		{
			typeof(ItemData),
			typeof(Piece)
		})]
		private static void Repair(Player __instance, ItemData toolItem)
		{
			if (SkaftConfig.Enabled.Value && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && toolItem != null)
			{
				Piece hoveringPiece = __instance.GetHoveringPiece();
				WearNTear val = default(WearNTear);
				if (!((Object)(object)hoveringPiece == (Object)null) && ((Component)hoveringPiece).TryGetComponent<WearNTear>(ref val) && Sweep.JustRepaired(val))
				{
					Sweep.Run(__instance, toolItem, hoveringPiece, val);
				}
			}
		}

		[HarmonyPostfix]
		[HarmonyPatch(typeof(Player), "UpdatePlacement", new Type[]
		{
			typeof(bool),
			typeof(float)
		})]
		private static void UpdatePlacement(Player __instance)
		{
			if ((Object)(object)__instance == (Object)null || (Object)(object)__instance != (Object)(object)Player.m_localPlayer)
			{
				return;
			}
			Piece selectedPiece = __instance.GetSelectedPiece();
			if ((Object)(object)selectedPiece != (Object)(object)_described)
			{
				Restore();
			}
			if (!SkaftConfig.Enabled.Value || !SkaftConfig.ShowReachInBuildMenu.Value)
			{
				Restore();
			}
			else
			{
				if ((Object)(object)selectedPiece == (Object)null || !selectedPiece.m_repairPiece)
				{
					return;
				}
				string prefabName = Utils.GetPrefabName(((Object)((Component)selectedPiece).gameObject).name);
				if (!SkaftConfig.IsReachEntry(prefabName))
				{
					if (SkaftConfig.Verbose.Value && _unknownEntries.Add(prefabName))
					{
						SkaftPlugin.Log.LogInfo((object)("Repair entry '" + prefabName + "' is not in ReachEntries, so no reach line is written on it. That is correct for another mod's tool; add the name if it is a repair entry the sweep can actually serve."));
					}
				}
				else if (!(Time.time < _nextReach))
				{
					_nextReach = Time.time + 1f;
					if ((Object)(object)_described == (Object)null)
					{
						_described = selectedPiece;
						_originalDescription = selectedPiece.m_description;
					}
					if (selectedPiece.m_description != _writtenDescription && selectedPiece.m_description != null && selectedPiece.m_description != _originalDescription)
					{
						_originalDescription = selectedPiece.m_description;
					}
					string text = "Reach: " + Sweep.Radius(__instance).ToString("0.0") + "m (Crafting " + Sweep.Level(__instance) + ")";
					_writtenDescription = (string.IsNullOrEmpty(_originalDescription) ? text : (_originalDescription + "\n" + text));
					selectedPiece.m_description = _writtenDescription;
				}
			}
		}

		internal static void Restore()
		{
			if ((Object)(object)_described != (Object)null && _described.m_description == _writtenDescription)
			{
				_described.m_description = _originalDescription;
			}
			_described = null;
			_originalDescription = null;
			_writtenDescription = null;
		}
	}
	[BepInPlugin("ezomic.valheim.skaft", "Skaft", "1.0.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class SkaftPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "ezomic.valheim.skaft";

		public const string PluginName = "Skaft";

		public const string PluginVersion = "1.0.0";

		public const string PluginAuthor = "Robbin Thijssen";

		private const string CoreGuid = "ezomic.valheim.core";

		internal static ManualLogSource Log;

		internal static bool CorePresent;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			SkaftConfig.Bind(((BaseUnityPlugin)this).Config);
			TryRegisterWithCore();
			_harmony = new Harmony("ezomic.valheim.skaft");
			_harmony.PatchAll(typeof(SkaftPatches));
			Log.LogInfo((object)"Skaft 1.0.0 by Robbin Thijssen - ready.");
		}

		private void TryRegisterWithCore()
		{
			CorePresent = Chainloader.PluginInfos.ContainsKey("ezomic.valheim.core");
			if (!CorePresent)
			{
				Log.LogInfo((object)"Core not installed - running standalone, without the version gate.");
			}
			else
			{
				RegisterWithCore();
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void RegisterWithCore()
		{
			Suite.Register("ezomic.valheim.skaft", "Skaft", "1.0.0", ((BaseUnityPlugin)this).Config, (Requirement)1, (Assembly)null);
			Suite.Sync((ConfigEntryBase[])(object)new ConfigEntryBase[6]
			{
				(ConfigEntryBase)SkaftConfig.Enabled,
				(ConfigEntryBase)SkaftConfig.MinRadius,
				(ConfigEntryBase)SkaftConfig.MaxRadius,
				(ConfigEntryBase)SkaftConfig.FullLevel,
				(ConfigEntryBase)SkaftConfig.Curve,
				(ConfigEntryBase)SkaftConfig.CostMultiplier
			});
			Suite.Local((ConfigEntryBase[])(object)new ConfigEntryBase[2]
			{
				(ConfigEntryBase)SkaftConfig.ShowReachInBuildMenu,
				(ConfigEntryBase)SkaftConfig.Verbose
			});
		}

		private void OnDestroy()
		{
			SkaftPatches.Restore();
			if (_harmony != null)
			{
				_harmony.UnpatchSelf();
			}
		}
	}
	internal static class Sweep
	{
		private static readonly List<WearNTear> Candidates = new List<WearNTear>();

		private static Vector3 _center;

		private static readonly Comparison<WearNTear> ByDistance = delegate(WearNTear a, WearNTear b)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_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 = ((Component)a).transform.position - _center;
			float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude;
			val = ((Component)b).transform.position - _center;
			return sqrMagnitude.CompareTo(((Vector3)(ref val)).sqrMagnitude);
		};

		private static FieldRef<WearNTear, float> _lastRepair;

		private static FieldRef<Player, PieceTable> _buildPieces;

		private static Func<Player, float> _buildStamina;

		private static bool _bound;

		private static bool _bindFailed;

		private static bool _toolReported;

		internal static float Radius(Player player)
		{
			float num = ((Character)player).GetSkillFactor((SkillType)107) * 100f;
			float num2 = Mathf.Max(1f, SkaftConfig.FullLevel.Value);
			float num3 = Mathf.Pow(Mathf.Clamp01(num / num2), Mathf.Max(0.01f, SkaftConfig.Curve.Value));
			return Mathf.Lerp(SkaftConfig.MinRadius.Value, SkaftConfig.MaxRadius.Value, num3);
		}

		internal static int Level(Player player)
		{
			return Mathf.RoundToInt(((Character)player).GetSkillFactor((SkillType)107) * 100f);
		}

		internal static void Run(Player player, ItemData tool, Piece hovered, WearNTear hoveredWear)
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_0255: 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)
			float num = Radius(player);
			if (num <= 0f || !Bind())
			{
				return;
			}
			_center = ((Component)hovered).transform.position;
			float num2 = num * num;
			Candidates.Clear();
			List<WearNTear> allInstances = WearNTear.GetAllInstances();
			for (int i = 0; i < allInstances.Count; i++)
			{
				WearNTear val = allInstances[i];
				if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)hoveredWear))
				{
					Vector3 val2 = ((Component)val).transform.position - _center;
					if (!(((Vector3)(ref val2)).sqrMagnitude > num2) && !(val.GetHealthPercentage() >= 1f))
					{
						Candidates.Add(val);
					}
				}
			}
			Candidates.Sort(ByDistance);
			float num3 = Mathf.Max(0f, SkaftConfig.CostMultiplier.Value);
			float num4 = _buildStamina(player) * num3;
			float num5 = tool.m_shared.m_attack.m_attackEitr * num3;
			float num6 = tool.m_shared.m_useDurabilityDrain * num3;
			ReportTool(player, tool, num4);
			int num7 = 0;
			string text = "radius";
			Piece val4 = default(Piece);
			for (int j = 0; j < Candidates.Count; j++)
			{
				if (num7 >= SkaftConfig.MaxPieces.Value)
				{
					text = "MaxPieces";
					break;
				}
				if (!((Character)player).HaveStamina(num4 * Game.m_staminaRate))
				{
					text = "stamina";
					break;
				}
				if (tool.m_shared.m_useDurability && tool.m_durability - num6 <= SkaftConfig.DurabilityFloor.Value)
				{
					text = "durability";
					break;
				}
				WearNTear val3 = Candidates[j];
				if ((Object)(object)val3 == (Object)null || !((Component)val3).TryGetComponent<Piece>(ref val4))
				{
					continue;
				}
				ZNetView component = ((Component)val3).GetComponent<ZNetView>();
				if ((Object)(object)component == (Object)null || !component.IsValid() || (SkaftConfig.OwnBuildingsOnly.Value && component.GetZDO().GetLong(ZDOVars.s_creator, 0L) != player.GetPlayerID()) || (!player.NoCostCheat() && (Object)(object)val4.m_craftingStation != (Object)null && (Object)(object)CraftingStation.HaveBuildStationInRange(val4.m_craftingStation.m_name, ((Component)player).transform.position) == (Object)null && (!((Object)(object)ZoneSystem.instance != (Object)null) || !ZoneSystem.instance.GetGlobalKey((GlobalKeys)22))) || !PrivateArea.CheckAccess(((Component)val4).transform.position, 0f, false, false) || val3.GetHealthPercentage() >= 1f)
				{
					continue;
				}
				if (!component.HasOwner())
				{
					component.ClaimOwnership();
				}
				if (val3.Repair())
				{
					((Character)player).UseStamina(num4);
					((Character)player).UseEitr(num5);
					if (tool.m_shared.m_useDurability)
					{
						tool.m_durability -= num6;
					}
					num7++;
				}
			}
			if (SkaftConfig.Verbose.Value)
			{
				SkaftPlugin.Log.LogInfo((object)("Sweep: crafting " + Level(player) + ", radius " + num.ToString("0.0") + "m, " + Candidates.Count + " damaged in range, " + num7 + " repaired, stopped on " + text + "."));
			}
			Candidates.Clear();
			if (num7 > 0)
			{
				((Character)player).Message((MessageType)1, Localization.instance.Localize("$msg_repaired", new string[1] { hovered.m_name }), num7 + 1, (Sprite)null);
			}
		}

		private static void ReportTool(Player player, ItemData tool, float stamina)
		{
			if (!_toolReported && SkaftConfig.Verbose.Value)
			{
				_toolReported = true;
				string text = "unknown (m_buildPieces unreachable)";
				if (_buildPieces != null)
				{
					PieceTable val = _buildPieces.Invoke(player);
					text = (((Object)(object)val == (Object)null) ? "no piece table" : ((object)Unsafe.As<SkillType, SkillType>(ref val.m_skill)/*cast due to .constrained prefix*/).ToString());
				}
				SkaftPlugin.Log.LogInfo((object)("Tool: " + tool.m_shared.m_name + ", attackStamina " + tool.m_shared.m_attack.m_attackStamina + ", attackEitr " + tool.m_shared.m_attack.m_attackEitr + ", useDurability " + tool.m_shared.m_useDurability + ", useDurabilityDrain " + tool.m_shared.m_useDurabilityDrain + ", durability " + tool.m_durability.ToString("0.0") + "/" + tool.GetMaxDurability().ToString("0.0") + ". Charging " + stamina.ToString("0.00") + " stamina a piece. Build table skill: " + text + " (if that is Crafting, it already discounts build stamina by up to half at level 100, on top of the reach this mod grants)."));
			}
		}

		internal static bool JustRepaired(WearNTear wear)
		{
			if (!Bind())
			{
				return false;
			}
			try
			{
				return _lastRepair.Invoke(wear) == Time.time;
			}
			catch
			{
				return false;
			}
		}

		private static bool Bind()
		{
			if (_bound)
			{
				return !_bindFailed;
			}
			_bound = true;
			try
			{
				_lastRepair = AccessTools.FieldRefAccess<WearNTear, float>("m_lastRepair");
				_buildStamina = AccessTools.MethodDelegate<Func<Player, float>>(AccessTools.Method(typeof(Player), "GetBuildStamina", (Type[])null, (Type[])null), (object)null, true);
				if (_lastRepair == null || _buildStamina == null)
				{
					throw new MissingMemberException();
				}
				try
				{
					_buildPieces = AccessTools.FieldRefAccess<Player, PieceTable>("m_buildPieces");
				}
				catch
				{
					_buildPieces = null;
				}
			}
			catch (Exception ex)
			{
				_bindFailed = true;
				SkaftPlugin.Log.LogWarning((object)("Could not reach Player.GetBuildStamina or WearNTear.m_lastRepair - the sweep is off and repair stays vanilla. " + ex.Message));
			}
			return !_bindFailed;
		}
	}
}