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 BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("CupboardWipeProtection")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: AssemblyProduct("CupboardWipeProtection")] [assembly: AssemblyTitle("CupboardWipeProtection")] [assembly: AssemblyVersion("1.2.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 CupboardWipeProtection { [BepInPlugin("cupboardwipeprotection.plugin", "Cupboard Wipe Protection", "1.2.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "cupboardwipeprotection.plugin"; public const string PluginName = "Cupboard Wipe Protection"; public const string PluginVersion = "1.2.0"; internal static ManualLogSource Log; internal static ConfigEntry Enabled; internal static ConfigEntry EnableTotalValueLimit; internal static ConfigEntry MaximumProtectedTotalScrapValue; internal static ConfigEntry EnablePerItemValueLimit; internal static ConfigEntry MaximumProtectedItemScrapValue; internal static ConfigEntry SurfaceTolerance; internal static ConfigEntry CabinetBoundsPadding; internal static ConfigEntry VerboseLogging; private Harmony _harmony; private void Awake() { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Expected O, but got Unknown //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Keep items stored in the ship cupboard when the entire crew is lost."); EnableTotalValueLimit = ((BaseUnityPlugin)this).Config.Bind("Value limits", "Enable total value limit", false, "Limit the combined value of all scrap protected by the cupboard."); MaximumProtectedTotalScrapValue = ((BaseUnityPlugin)this).Config.Bind("Value limits", "Maximum total protected scrap value", 200, new ConfigDescription("Maximum combined scrap value protected after a wipe when the total value limit is enabled.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 1000000), Array.Empty())); EnablePerItemValueLimit = ((BaseUnityPlugin)this).Config.Bind("Value limits", "Enable per-item value limit", true, "Remove individual scrap items worth more than the configured per-item limit during a full-crew wipe."); MaximumProtectedItemScrapValue = ((BaseUnityPlugin)this).Config.Bind("Value limits", "Maximum protected value per scrap item", 200, new ConfigDescription("Maximum value of a single scrap item that the cupboard can protect when the per-item limit is enabled.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 1000000), Array.Empty())); SurfaceTolerance = ((BaseUnityPlugin)this).Config.Bind("Detection", "Shelf tolerance", 0.14f, "Maximum distance in metres between an item and a cupboard shelf surface."); CabinetBoundsPadding = ((BaseUnityPlugin)this).Config.Bind("Detection", "Cabinet bounds padding", 0.08f, "Extra margin in metres around the cupboard model used by fallback detection."); VerboseLogging = ((BaseUnityPlugin)this).Config.Bind("Diagnostics", "Verbose logging", false, "Log every item classified as inside or outside the cupboard during a wipe."); _harmony = new Harmony("cupboardwipeprotection.plugin"); _harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Cupboard Wipe Protection 1.2.0 loaded."); } private void OnDestroy() { ProtectionState.Clear(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class ProtectionState { private sealed class ItemGroup { internal readonly List Items; internal readonly int ScrapValue; internal readonly int MaximumIndividualScrapValue; internal readonly string Label; internal readonly ulong NetworkObjectId; private ItemGroup(List items, int scrapValue, int maximumIndividualScrapValue, string label, ulong networkObjectId) { Items = items; ScrapValue = scrapValue; MaximumIndividualScrapValue = maximumIndividualScrapValue; Label = label; NetworkObjectId = networkObjectId; } internal static ItemGroup FromRoot(GrabbableObject root) { List list = new List(); CollectGroupItems(root, list, new HashSet()); int scrapValue = ((IEnumerable)list).Sum((Func)ItemScrapValue); int maximumIndividualScrapValue = ((list.Count != 0) ? ((IEnumerable)list).Max((Func)ItemScrapValue) : 0); ulong networkObjectId = (((Object)(object)((NetworkBehaviour)root).NetworkObject != (Object)null) ? ((NetworkBehaviour)root).NetworkObjectId : ulong.MaxValue); return new ItemGroup(list, scrapValue, maximumIndividualScrapValue, ItemName(root), networkObjectId); } private static void CollectGroupItems(GrabbableObject item, List items, HashSet visited) { if ((Object)(object)item == (Object)null || !visited.Add(item)) { return; } items.Add(item); BeltBagItem val = (BeltBagItem)(object)((item is BeltBagItem) ? item : null); if (val != null && val.objectsInBag != null) { GrabbableObject[] array = val.objectsInBag.ToArray(); for (int i = 0; i < array.Length; i++) { CollectGroupItems(array[i], items, visited); } } } private static int ItemScrapValue(GrabbableObject item) { if (!((Object)(object)item?.itemProperties != (Object)null) || !item.itemProperties.isScrap) { return 0; } return Math.Max(0, item.scrapValue); } } private static readonly HashSet ProtectedObjects = new HashSet(); private static bool _active; internal static bool IsProtected(NetworkObject networkObject) { if (_active && (Object)(object)networkObject != (Object)null) { return ProtectedObjects.Contains(networkObject); } return false; } internal static void Begin() { Clear(); if (!Plugin.Enabled.Value) { return; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || !((NetworkBehaviour)instance).IsServer || !instance.allPlayersDead) { return; } CabinetDetector cabinetDetector = CabinetDetector.Create(instance); if (cabinetDetector == null) { Plugin.Log.LogWarning((object)"A full-crew wipe was detected, but the ship cupboard could not be located. No items were protected."); return; } GrabbableObject[] array = Object.FindObjectsByType((FindObjectsInactive)1, (FindObjectsSortMode)0); HashSet hashSet = FindContainedItems(array); List list = new List(); GrabbableObject[] array2 = array; foreach (GrabbableObject val in array2) { if (!((Object)(object)val == (Object)null) && !val.deactivated && !val.isHeld && !val.isHeldByEnemy && !hashSet.Contains(val)) { bool flag = cabinetDetector.Contains(val); if (Plugin.VerboseLogging.Value) { Plugin.Log.LogInfo((object)("Cupboard check: " + ItemName(val) + " => " + (flag ? "inside" : "outside"))); } if (flag) { list.Add(ItemGroup.FromRoot(val)); } } } if (list.Count == 0) { Plugin.Log.LogInfo((object)"Full-crew wipe: the cupboard was empty, so no items were protected."); Clear(); return; } bool perItemLimitEnabled = Plugin.EnablePerItemValueLimit.Value; int perItemLimit = Plugin.MaximumProtectedItemScrapValue.Value; List list2 = list.Where((ItemGroup group) => !perItemLimitEnabled || group.MaximumIndividualScrapValue <= perItemLimit).ToList(); List list3 = list.Where((ItemGroup group) => perItemLimitEnabled && group.MaximumIndividualScrapValue > perItemLimit).ToList(); bool value = Plugin.EnableTotalValueLimit.Value; int value2 = Plugin.MaximumProtectedTotalScrapValue.Value; List list4 = (value ? SelectGroupsWithinValueLimit(list2, value2) : list2.ToList()); int num = 0; foreach (ItemGroup item in list4) { foreach (GrabbableObject item2 in item.Items) { num += (ProtectSingle(item2) ? 1 : 0); } } int num2 = list.Sum((ItemGroup group) => group.ScrapValue); int num3 = list4.Sum((ItemGroup group) => group.ScrapValue); int num4 = list2.Count - list4.Count; int num5 = list3.Count + num4; if (Plugin.VerboseLogging.Value && num5 > 0) { foreach (ItemGroup item3 in list3) { Plugin.Log.LogInfo((object)$"Per-item limit: removing {item3.Label} group because it contains scrap worth ${item3.MaximumIndividualScrapValue}, above the ${perItemLimit} limit."); } HashSet selectedSet = new HashSet(list4); foreach (ItemGroup item4 in list2.Where((ItemGroup group) => !selectedSet.Contains(group))) { Plugin.Log.LogInfo((object)$"Total value limit: removing {item4.Label} group worth ${item4.ScrapValue}."); } } if (num == 0) { Plugin.Log.LogInfo((object)$"Full-crew wipe: cupboard value was ${num2}, but no item group passed the enabled value limits."); Clear(); return; } _active = true; string text = (perItemLimitEnabled ? $"${perItemLimit}" : "off"); string text2 = (value ? $"${value2}" : "off"); Plugin.Log.LogInfo((object)$"Full-crew wipe: protected {num} cupboard item object(s) worth ${num3} of ${num2}. Per-item limit: {text}; total limit: {text2}. {num5} over-limit group(s) will be removed normally."); } private static HashSet FindContainedItems(IEnumerable items) { HashSet hashSet = new HashSet(); foreach (BeltBagItem item in items.OfType()) { AddContainedItems(item, hashSet); } return hashSet; } private static void AddContainedItems(BeltBagItem beltBag, HashSet contained) { if (beltBag.objectsInBag == null) { return; } GrabbableObject[] array = beltBag.objectsInBag.ToArray(); foreach (GrabbableObject val in array) { if (!((Object)(object)val == (Object)null) && contained.Add(val)) { BeltBagItem val2 = (BeltBagItem)(object)((val is BeltBagItem) ? val : null); if (val2 != null) { AddContainedItems(val2, contained); } } } } private static List SelectGroupsWithinValueLimit(List groups, int configuredLimit) { List list = groups.Where((ItemGroup group) => group.ScrapValue == 0).ToList(); List list2 = (from @group in groups where @group.ScrapValue > 0 orderby @group.NetworkObjectId select @group).ThenBy((ItemGroup group) => group.Label, StringComparer.Ordinal).ToList(); int val = list2.Sum((ItemGroup group) => group.ScrapValue); int num = Math.Min(configuredLimit, val); if (num <= 0) { return list; } bool[] array = new bool[num + 1]; int[] array2 = Enumerable.Repeat(-1, num + 1).ToArray(); int[] array3 = Enumerable.Repeat(-1, num + 1).ToArray(); array[0] = true; for (int num2 = 0; num2 < list2.Count; num2++) { int scrapValue = list2[num2].ScrapValue; if (scrapValue > num) { continue; } for (int num3 = num - scrapValue; num3 >= 0; num3--) { int num4 = num3 + scrapValue; if (array[num3] && !array[num4]) { array[num4] = true; array2[num4] = num3; array3[num4] = num2; } } } int num5 = num; while (num5 > 0 && !array[num5]) { num5--; } while (num5 > 0) { int num6 = array3[num5]; if (num6 < 0) { break; } list.Add(list2[num6]); num5 = array2[num5]; } return list; } private static bool ProtectSingle(GrabbableObject item) { NetworkObject networkObject = ((NetworkBehaviour)item).NetworkObject; if ((Object)(object)networkObject == (Object)null || !networkObject.IsSpawned) { Plugin.Log.LogWarning((object)("Could not protect " + ItemName(item) + " because it has no spawned NetworkObject.")); return false; } item.isInShipRoom = true; item.isInElevator = true; item.isInFactory = false; item.scrapPersistedThroughRounds = true; return ProtectedObjects.Add(networkObject); } internal static void Clear() { _active = false; ProtectedObjects.Clear(); } private static string ItemName(GrabbableObject item) { object obj; if (!((Object)(object)item?.itemProperties != (Object)null)) { obj = ((item != null) ? ((Object)item).name : null); if (obj == null) { return "unknown item"; } } else { obj = item.itemProperties.itemName; } return (string)obj; } } internal sealed class CabinetDetector { private static readonly string[] CabinetNameHints = new string[4] { "closet", "cupboard", "cabinet", "storage" }; private readonly Transform _root; private readonly PlaceableObjectsSurface[] _surfaces; private readonly Bounds? _cabinetBounds; private CabinetDetector(Transform root, PlaceableObjectsSurface[] surfaces, Bounds? cabinetBounds) { _root = root; _surfaces = surfaces; _cabinetBounds = cabinetBounds; } internal static CabinetDetector Create(StartOfRound round) { //IL_004b: 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_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)round.closetLeftDoor == (Object)null || (Object)(object)round.closetRightDoor == (Object)null) { return null; } Transform transform = ((Component)round.closetLeftDoor).transform; Transform transform2 = ((Component)round.closetRightDoor).transform; Transform root = FindCabinetRoot(transform, transform2); Vector3 doorMidpoint = (transform.position + transform2.position) * 0.5f; Bounds? cabinetBounds = TryGetCabinetBounds(root, transform, transform2); if (!cabinetBounds.HasValue) { root = null; } PlaceableObjectsSurface[] source = Object.FindObjectsByType((FindObjectsInactive)1, (FindObjectsSortMode)0); PlaceableObjectsSurface[] array = source.Where((PlaceableObjectsSurface surface) => IsCupboardSurface(surface, root)).ToArray(); if (array.Length == 0) { array = (from surface in source where (Object)(object)surface != (Object)null && (Object)(object)surface.placeableBounds != (Object)null where SurfaceDistanceSquared(surface, doorMidpoint) <= 2.25f orderby SurfaceDistanceSquared(surface, doorMidpoint) select surface).Take(4).ToArray(); } if (array.Length == 0 && !cabinetBounds.HasValue) { return null; } if (Plugin.VerboseLogging.Value) { string arg = (((Object)(object)root != (Object)null) ? HierarchyPath(root) : "unresolved"); Plugin.Log.LogInfo((object)string.Format("Cupboard detector: root={0}, shelf surfaces={1}, bounds={2}", arg, array.Length, cabinetBounds.HasValue ? ((object)cabinetBounds.Value/*cast due to .constrained prefix*/).ToString() : "none")); } return new CabinetDetector(root, array, cabinetBounds); } internal bool Contains(GrabbableObject item) { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)item.parentObject != (Object)null && IsWithinRoot(item.parentObject, _root)) { return true; } Transform parent = ((Component)item).transform.parent; if ((Object)(object)parent != (Object)null && IsWithinRoot(parent, _root)) { return true; } PlaceableObjectsSurface[] surfaces = _surfaces; foreach (PlaceableObjectsSurface surface in surfaces) { if (IsOnSurface(item, surface)) { return true; } } if (!_cabinetBounds.HasValue) { return false; } Bounds value = _cabinetBounds.Value; ((Bounds)(ref value)).Expand(Mathf.Max(0f, Plugin.CabinetBoundsPadding.Value) * 2f); if (((Bounds)(ref value)).Contains(((Component)item).transform.position)) { return true; } Renderer mainObjectRenderer = (Renderer)(object)item.mainObjectRenderer; if ((Object)(object)mainObjectRenderer != (Object)null) { Bounds bounds = mainObjectRenderer.bounds; return ((Bounds)(ref value)).Contains(((Bounds)(ref bounds)).center); } return false; } private static bool IsOnSurface(GrabbableObject item, PlaceableObjectsSurface surface) { //IL_0040: 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_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: 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_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)surface == (Object)null || (Object)(object)surface.placeableBounds == (Object)null) { return false; } float num = (((Object)(object)item.itemProperties != (Object)null) ? item.itemProperties.verticalOffset : 0f); Vector3 val = ((Component)item).transform.position - ((Component)surface.placeableBounds).transform.up * num; Vector3 val2 = surface.placeableBounds.ClosestPoint(val); float num2 = Mathf.Max(0.01f, Plugin.SurfaceTolerance.Value); Vector3 val3 = val2 - val; return ((Vector3)(ref val3)).sqrMagnitude <= num2 * num2; } private static bool IsCupboardSurface(PlaceableObjectsSurface surface, Transform root) { if ((Object)(object)surface == (Object)null || (Object)(object)surface.placeableBounds == (Object)null || (Object)(object)root == (Object)null) { return false; } if (IsWithinRoot(((Component)surface).transform, root) || IsWithinRoot(((Component)surface.placeableBounds).transform, root)) { return true; } if ((Object)(object)surface.parentTo != (Object)null) { return IsWithinRoot(((Component)surface.parentTo).transform, root); } return false; } private static float SurfaceDistanceSquared(PlaceableObjectsSurface surface, Vector3 point) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) Vector3 val = surface.placeableBounds.ClosestPoint(point) - point; return ((Vector3)(ref val)).sqrMagnitude; } private static Transform FindCabinetRoot(Transform leftDoor, Transform rightDoor) { HashSet hashSet = new HashSet(); Transform val = rightDoor; while ((Object)(object)val != (Object)null) { hashSet.Add(val); val = val.parent; } Transform val2 = null; while ((Object)(object)leftDoor != (Object)null) { if (hashSet.Contains(leftDoor)) { if (val2 == null) { val2 = leftDoor; } if (CabinetNameHints.Any((string hint) => ((Object)leftDoor).name.IndexOf(hint, StringComparison.OrdinalIgnoreCase) >= 0)) { return leftDoor; } } leftDoor = leftDoor.parent; } return val2; } private static Bounds? TryGetCabinetBounds(Transform root, Transform leftDoor, Transform rightDoor) { //IL_0027: 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_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)root == (Object)null) { return null; } Renderer[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); Collider[] componentsInChildren2 = ((Component)root).GetComponentsInChildren(true); bool flag = false; Bounds value = default(Bounds); Renderer[] array = componentsInChildren; foreach (Renderer val in array) { if (!((Object)(object)val == (Object)null) && !IsDoorPart(((Component)val).transform, root, leftDoor, rightDoor)) { if (!flag) { value = val.bounds; flag = true; } else { ((Bounds)(ref value)).Encapsulate(val.bounds); } } } if (!flag) { Collider[] array2 = componentsInChildren2; foreach (Collider val2 in array2) { if (!((Object)(object)val2 == (Object)null) && !IsDoorPart(((Component)val2).transform, root, leftDoor, rightDoor)) { if (!flag) { value = val2.bounds; flag = true; } else { ((Bounds)(ref value)).Encapsulate(val2.bounds); } } } } if (!flag) { return null; } Vector3 size = ((Bounds)(ref value)).size; if (size.x > 7f || size.y > 7f || size.z > 7f) { Plugin.Log.LogWarning((object)$"Rejected unusually large cupboard bounds ({size}); shelf detection will be used instead."); return null; } return value; } private static bool IsDoorPart(Transform candidate, Transform root, Transform leftDoor, Transform rightDoor) { bool num = (Object)(object)leftDoor != (Object)(object)root && IsWithinRoot(candidate, leftDoor); bool flag = (Object)(object)rightDoor != (Object)(object)root && IsWithinRoot(candidate, rightDoor); return num || flag; } private static bool IsWithinRoot(Transform candidate, Transform root) { if ((Object)(object)candidate != (Object)null && (Object)(object)root != (Object)null) { if (!((Object)(object)candidate == (Object)(object)root)) { return candidate.IsChildOf(root); } return true; } return false; } private static string HierarchyPath(Transform transform) { List list = new List(); Transform val = transform; while ((Object)(object)val != (Object)null) { list.Add(((Object)val).name); val = val.parent; } list.Reverse(); return string.Join("/", list); } } [HarmonyPatch(typeof(RoundManager), "DespawnPropsAtEndOfRound")] internal static class RoundCleanupPatch { [HarmonyPrefix] private static void Prefix() { ProtectionState.Begin(); } [HarmonyFinalizer] private static Exception Finalizer(Exception __exception) { ProtectionState.Clear(); return __exception; } } [HarmonyPatch(typeof(NetworkObject), "Despawn", new Type[] { typeof(bool) })] internal static class NetworkObjectDespawnPatch { [HarmonyPrefix] private static bool Prefix(NetworkObject __instance) { return !ProtectionState.IsProtected(__instance); } } }