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 modEnabled; private ConfigEntry snapRange; private ConfigEntry gridWidth; private ConfigEntry gridDepth; private ConfigEntry gridHeight; private ConfigEntry verticalSnapRange; private ConfigEntry placementYOffset; private ConfigEntry usePlacementHeightForVerticalSnap; private ConfigEntry occupiedRadius; private ConfigEntry chestCacheInterval; private ConfigEntry snapRotation; private ConfigEntry disableWhileShiftHeld; private ConfigEntry normalChestPrefabNames; private ConfigEntry normalChestPieceNames; private readonly Harmony harmony = new Harmony("ChestGrid"); private readonly List cachedChestPieces = new List(); private HashSet chestPrefabNameSet = new HashSet(StringComparer.OrdinalIgnoreCase); private HashSet chestPieceNameSet = new HashSet(StringComparer.OrdinalIgnoreCase); private float nextChestCacheRefreshTime; private void Awake() { instance = this; modEnabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Enable ChestGrid."); snapRange = ((BaseUnityPlugin)this).Config.Bind("Snapping", "SnapRange", 1.55f, "Maximum distance from the placement ghost to a chest grid slot before snapping."); gridWidth = ((BaseUnityPlugin)this).Config.Bind("Snapping", "GridWidth", 1.28f, "Local left/right spacing between normal wooden chests."); gridDepth = ((BaseUnityPlugin)this).Config.Bind("Snapping", "GridDepth", 0.74f, "Local front/back spacing between normal wooden chests."); gridHeight = ((BaseUnityPlugin)this).Config.Bind("Snapping", "GridHeight", 1.05f, "Vertical spacing for stacked normal wooden chests."); verticalSnapRange = ((BaseUnityPlugin)this).Config.Bind("Snapping", "VerticalSnapRange", 1.8f, "Maximum distance to vertical chest stack slots. Higher values make stacking easier to hit."); placementYOffset = ((BaseUnityPlugin)this).Config.Bind("Snapping", "PlacementYOffset", 0.08f, "Small upward offset applied to snapped vertical chest positions when placement height is not used."); usePlacementHeightForVerticalSnap = ((BaseUnityPlugin)this).Config.Bind("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("Snapping", "OccupiedRadius", 0.35f, "Minimum distance to another normal wooden chest before a grid slot is treated as occupied."); chestCacheInterval = ((BaseUnityPlugin)this).Config.Bind("Performance", "ChestCacheInterval", 1f, "Seconds between expensive chest list refreshes while placing a wooden chest."); snapRotation = ((BaseUnityPlugin)this).Config.Bind("Snapping", "SnapRotation", true, "Rotate the placement ghost to match the chest it snaps to."); normalChestPrefabNames = ((BaseUnityPlugin)this).Config.Bind("Chest Detection", "NormalChestPrefabNames", "piece_chest_wood", "Comma-separated prefab names treated as normal wooden chests."); normalChestPieceNames = ((BaseUnityPlugin)this).Config.Bind("Chest Detection", "NormalChestPieceNames", "$piece_chestwood", "Comma-separated Piece.m_name values treated as normal wooden chests."); disableWhileShiftHeld = ((BaseUnityPlugin)this).Config.Bind("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 ParseNameSet(string value) { return new HashSet(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 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 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(); 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 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 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(); 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(); } }