Decompiled source of ChestGrid v1.0.0

plugins\ChestGrid\ChestGrid.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ChestGrid")]
[assembly: AssemblyProduct("ChestGrid")]
[assembly: AssemblyDescription("Valheim chest placement snapping mod.")]
[assembly: AssemblyCompany("LEGIOmods")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was partially created with the assistance of OpenAI Codex for code generation, packaging, and documentation.")]
[assembly: AssemblyMetadata("AI_Model_Vendor", "OpenAI")]
[assembly: AssemblyMetadata("AI_Agent", "OpenAI Codex")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace ChestGrid;

[BepInPlugin("ChestGrid", "ChestGrid", "1.0.0")]
public class ChestGridPlugin : BaseUnityPlugin
{
	private struct SnapCandidate
	{
		public readonly Vector3 Position;

		public readonly Quaternion AnchorRotation;

		public SnapCandidate(Vector3 position, Quaternion anchorRotation)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_0009: Unknown result type (might be due to invalid IL or missing references)
			Position = position;
			AnchorRotation = anchorRotation;
		}
	}

	public const string GUID = "ChestGrid";

	public const string NAME = "ChestGrid";

	public const string VERSION = "1.0.0";

	private const float DefaultChestCacheInterval = 1f;

	private static readonly FieldInfo PlacementGhostField = AccessTools.Field(typeof(Player), "m_placementGhost");

	private static ChestGridPlugin instance;

	private ConfigEntry<bool> modEnabled;

	private ConfigEntry<float> snapRange;

	private ConfigEntry<float> gridWidth;

	private ConfigEntry<float> gridDepth;

	private ConfigEntry<float> gridHeight;

	private ConfigEntry<float> verticalSnapRange;

	private ConfigEntry<float> placementYOffset;

	private ConfigEntry<bool> usePlacementHeightForVerticalSnap;

	private ConfigEntry<float> occupiedRadius;

	private ConfigEntry<float> chestCacheInterval;

	private ConfigEntry<bool> snapRotation;

	private ConfigEntry<bool> disableWhileShiftHeld;

	private ConfigEntry<string> normalChestPrefabNames;

	private ConfigEntry<string> normalChestPieceNames;

	private readonly Harmony harmony = new Harmony("ChestGrid");

	private readonly List<Piece> cachedChestPieces = new List<Piece>();

	private HashSet<string> chestPrefabNameSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private HashSet<string> chestPieceNameSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private float nextChestCacheRefreshTime;

	private void Awake()
	{
		instance = this;
		modEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable ChestGrid.");
		snapRange = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "SnapRange", 1.55f, "Maximum distance from the placement ghost to a chest grid slot before snapping.");
		gridWidth = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "GridWidth", 1.28f, "Local left/right spacing between normal wooden chests.");
		gridDepth = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "GridDepth", 0.74f, "Local front/back spacing between normal wooden chests.");
		gridHeight = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "GridHeight", 1.05f, "Vertical spacing for stacked normal wooden chests.");
		verticalSnapRange = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "VerticalSnapRange", 1.8f, "Maximum distance to vertical chest stack slots. Higher values make stacking easier to hit.");
		placementYOffset = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "PlacementYOffset", 0.08f, "Small upward offset applied to snapped vertical chest positions when placement height is not used.");
		usePlacementHeightForVerticalSnap = ((BaseUnityPlugin)this).Config.Bind<bool>("Snapping", "UsePlacementHeightForVerticalSnap", true, "For vertical rows, keep the hammer placement height from the wood/floor and only snap horizontal chest grid coordinates.");
		occupiedRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Snapping", "OccupiedRadius", 0.35f, "Minimum distance to another normal wooden chest before a grid slot is treated as occupied.");
		chestCacheInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Performance", "ChestCacheInterval", 1f, "Seconds between expensive chest list refreshes while placing a wooden chest.");
		snapRotation = ((BaseUnityPlugin)this).Config.Bind<bool>("Snapping", "SnapRotation", true, "Rotate the placement ghost to match the chest it snaps to.");
		normalChestPrefabNames = ((BaseUnityPlugin)this).Config.Bind<string>("Chest Detection", "NormalChestPrefabNames", "piece_chest_wood", "Comma-separated prefab names treated as normal wooden chests.");
		normalChestPieceNames = ((BaseUnityPlugin)this).Config.Bind<string>("Chest Detection", "NormalChestPieceNames", "$piece_chestwood", "Comma-separated Piece.m_name values treated as normal wooden chests.");
		disableWhileShiftHeld = ((BaseUnityPlugin)this).Config.Bind<bool>("Controls", "DisableWhileShiftHeld", true, "Disable ChestGrid snapping while Left Shift or Right Shift is held.");
		RefreshNameSets();
		normalChestPrefabNames.SettingChanged += delegate
		{
			RefreshNameSets();
			ForceRefreshChestCache();
		};
		normalChestPieceNames.SettingChanged += delegate
		{
			RefreshNameSets();
			ForceRefreshChestCache();
		};
		PatchPlacementGhost();
		((BaseUnityPlugin)this).Logger.LogInfo((object)"ChestGrid 1.0.0 loaded. GUID=ChestGrid");
	}

	private void OnDestroy()
	{
		harmony.UnpatchSelf();
		if (instance == this)
		{
			instance = null;
		}
	}

	private void RefreshNameSets()
	{
		chestPrefabNameSet = ParseNameSet(normalChestPrefabNames.Value);
		chestPieceNameSet = ParseNameSet(normalChestPieceNames.Value);
	}

	private static HashSet<string> ParseNameSet(string value)
	{
		return new HashSet<string>(from name in (value ?? string.Empty).Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries)
			select name.Trim() into name
			where name.Length > 0
			select name, StringComparer.OrdinalIgnoreCase);
	}

	private void PatchPlacementGhost()
	{
		//IL_005a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0067: Expected O, but got Unknown
		MethodInfo methodInfo = AccessTools.Method(typeof(Player), "UpdatePlacementGhost", (Type[])null, (Type[])null);
		MethodInfo methodInfo2 = AccessTools.Method(typeof(ChestGridPlugin), "UpdatePlacementGhostPostfix", (Type[])null, (Type[])null);
		if (methodInfo == null || methodInfo2 == null)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not patch Player.UpdatePlacementGhost. Chest snapping will be disabled.");
		}
		else
		{
			harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		}
	}

	private static void UpdatePlacementGhostPostfix(Player __instance)
	{
		if (!((Object)(object)instance == (Object)null) && instance.modEnabled.Value)
		{
			instance.TrySnapPlacementGhost(__instance);
		}
	}

	private void TrySnapPlacementGhost(Player player)
	{
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		//IL_004e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Unknown result type (might be due to invalid IL or missing references)
		//IL_0063: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_008b: Unknown result type (might be due to invalid IL or missing references)
		//IL_008e: Unknown result type (might be due to invalid IL or missing references)
		//IL_009d: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)player == (Object)null || PlacementGhostField == null || IsShiftDisableHeld())
		{
			return;
		}
		object? value = PlacementGhostField.GetValue(player);
		GameObject val = (GameObject)((value is GameObject) ? value : null);
		if ((Object)(object)val == (Object)null || !IsNormalWoodChest(val))
		{
			return;
		}
		Vector3 position = val.transform.position;
		if (TryFindBestSnap(position, val, out var best))
		{
			val.transform.position = best.Position;
			if (snapRotation.Value)
			{
				Transform transform = val.transform;
				Quaternion anchorRotation = best.AnchorRotation;
				transform.rotation = Quaternion.Euler(0f, ((Quaternion)(ref anchorRotation)).eulerAngles.y, 0f);
			}
		}
	}

	private bool TryFindBestSnap(Vector3 desiredPosition, GameObject ghost, out SnapCandidate best)
	{
		//IL_005f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0064: Unknown result type (might be due to invalid IL or missing references)
		//IL_0066: Unknown result type (might be due to invalid IL or missing references)
		//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
		//IL_00db: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
		//IL_0108: Unknown result type (might be due to invalid IL or missing references)
		//IL_011b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0124: Unknown result type (might be due to invalid IL or missing references)
		best = default(SnapCandidate);
		bool result = false;
		float num = float.MaxValue;
		List<Piece> list = GetCachedChestPieces(ghost);
		foreach (Piece item in list)
		{
			if ((Object)(object)item == (Object)null || (Object)(object)((Component)item).gameObject == (Object)(object)ghost)
			{
				continue;
			}
			foreach (Vector3 gridOffset in GetGridOffsets())
			{
				bool flag = Math.Abs(gridOffset.y) > 0.01f;
				float num2 = Math.Max(0f, flag ? verticalSnapRange.Value : snapRange.Value);
				Vector3 val = ((Component)item).transform.TransformPoint(gridOffset);
				if (flag)
				{
					val.y = (usePlacementHeightForVerticalSnap.Value ? desiredPosition.y : (val.y + placementYOffset.Value));
				}
				Vector3 val2 = desiredPosition - val;
				float sqrMagnitude = ((Vector3)(ref val2)).sqrMagnitude;
				if (!(sqrMagnitude > num2 * num2) && !(sqrMagnitude > num) && !IsOccupied(val, ghost, item, list))
				{
					result = true;
					num = sqrMagnitude;
					best = new SnapCandidate(val, ((Component)item).transform.rotation);
				}
			}
		}
		return result;
	}

	private List<Piece> GetCachedChestPieces(GameObject ghost)
	{
		cachedChestPieces.RemoveAll((Piece piece) => (Object)(object)piece == (Object)null || (Object)(object)((Component)piece).gameObject == (Object)(object)ghost);
		if (Time.time < nextChestCacheRefreshTime)
		{
			return cachedChestPieces;
		}
		nextChestCacheRefreshTime = Time.time + Math.Max(0.1f, chestCacheInterval.Value);
		cachedChestPieces.Clear();
		Piece[] array = Object.FindObjectsOfType<Piece>();
		foreach (Piece val in array)
		{
			if ((Object)(object)val != (Object)null && (Object)(object)((Component)val).gameObject != (Object)(object)ghost && IsNormalWoodChest(((Component)val).gameObject))
			{
				cachedChestPieces.Add(val);
			}
		}
		return cachedChestPieces;
	}

	private void ForceRefreshChestCache()
	{
		nextChestCacheRefreshTime = 0f;
		cachedChestPieces.Clear();
	}

	private bool IsShiftDisableHeld()
	{
		if (disableWhileShiftHeld.Value)
		{
			if (!Input.GetKey((KeyCode)304))
			{
				return Input.GetKey((KeyCode)303);
			}
			return true;
		}
		return false;
	}

	private IEnumerable<Vector3> GetGridOffsets()
	{
		float width = Math.Max(0.01f, gridWidth.Value);
		float depth = Math.Max(0.01f, gridDepth.Value);
		float height = Math.Max(0.01f, gridHeight.Value);
		for (int y = -1; y <= 1; y++)
		{
			for (int z = -1; z <= 1; z++)
			{
				for (int x = -1; x <= 1; x++)
				{
					if (x != 0 || y != 0 || z != 0)
					{
						yield return new Vector3((float)x * width, (float)y * height, (float)z * depth);
					}
				}
			}
		}
	}

	private bool IsOccupied(Vector3 candidatePosition, GameObject ghost, Piece anchor, List<Piece> chestPieces)
	{
		//IL_004a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0051: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_005b: Unknown result type (might be due to invalid IL or missing references)
		float num = Math.Max(0.01f, occupiedRadius.Value);
		float num2 = num * num;
		foreach (Piece chestPiece in chestPieces)
		{
			if (!((Object)(object)chestPiece == (Object)null) && !((Object)(object)chestPiece == (Object)(object)anchor) && !((Object)(object)((Component)chestPiece).gameObject == (Object)(object)ghost))
			{
				Vector3 val = candidatePosition - ((Component)chestPiece).transform.position;
				if (((Vector3)(ref val)).sqrMagnitude <= num2)
				{
					return true;
				}
			}
		}
		return false;
	}

	private bool IsNormalWoodChest(GameObject gameObject)
	{
		if ((Object)(object)gameObject == (Object)null)
		{
			return false;
		}
		string item = NormalizeObjectName(((Object)gameObject).name);
		if (chestPrefabNameSet.Contains(item))
		{
			return true;
		}
		Piece component = gameObject.GetComponent<Piece>();
		if ((Object)(object)component != (Object)null)
		{
			return chestPieceNameSet.Contains(component.m_name ?? string.Empty);
		}
		return false;
	}

	private static string NormalizeObjectName(string name)
	{
		if (string.IsNullOrEmpty(name))
		{
			return string.Empty;
		}
		int num = name.IndexOf("(Clone)", StringComparison.OrdinalIgnoreCase);
		if (num >= 0)
		{
			name = name.Substring(0, num);
		}
		return name.Trim();
	}
}