using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using LazyBearTechnology;
using UnityEngine;
using UnityEngine.UI;
[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("GraveyardKeeper2.SharedStorage")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.2.0.0")]
[assembly: AssemblyInformationalVersion("1.2.0+7168379e59da8bee74a4bc896cfc6df7e06e4acb")]
[assembly: AssemblyProduct("GraveyardKeeper2.SharedStorage")]
[assembly: AssemblyTitle("GraveyardKeeper2.SharedStorage")]
[assembly: AssemblyVersion("1.2.0.0")]
namespace GK2.SharedStorage;
public sealed class SharedContainer
{
public WgoData WgoData { get; }
public Inventory Inventory { get; }
public WorldZoneData Zone { get; }
public bool IsCurrentZone { get; }
internal SharedContainer(WgoData wgoData, Inventory inventory, WorldZoneData zone, bool isCurrentZone)
{
WgoData = wgoData ?? throw new ArgumentNullException("wgoData");
Inventory = inventory ?? throw new ArgumentNullException("inventory");
Zone = zone ?? throw new ArgumentNullException("zone");
IsCurrentZone = isCurrentZone;
}
}
public static class SharedStorageApi
{
public const int ApiVersion = 1;
public static bool IsOperational
{
get
{
if (Plugin.ApiSnapshotReady)
{
return ModSettings.SharingEnabled;
}
return false;
}
}
public static bool IsCurrentZoneEligible(WorldZoneData currentZone)
{
try
{
return CrossZoneStorage.IsCurrentZoneEligible(currentZone);
}
catch (Exception ex)
{
Plugin.Log.LogError((object)("Shared storage API: zone eligibility check failed. " + ex));
return false;
}
}
public static bool TryGetSnapshot(WorldZoneData currentZone, out StorageSnapshot snapshot)
{
snapshot = null;
if (!IsOperational || currentZone == null)
{
return false;
}
try
{
return CrossZoneStorage.TryBuildSnapshot(currentZone, out snapshot);
}
catch (Exception ex)
{
Plugin.Log.LogError((object)("Shared storage API: snapshot failed. " + ex));
snapshot = null;
return false;
}
}
}
internal static class SnapshotAssembler
{
private sealed class InventoryReferenceComparer : IEqualityComparer<Inventory>
{
internal static readonly InventoryReferenceComparer Instance = new InventoryReferenceComparer();
public bool Equals(Inventory x, Inventory y)
{
return x == y;
}
public int GetHashCode(Inventory obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
internal static List<SharedContainer> AppendOtherContainers(List<SharedContainer> local, List<SharedContainer> otherCandidates)
{
List<SharedContainer> result = new List<SharedContainer>();
HashSet<Inventory> seen = new HashSet<Inventory>(InventoryReferenceComparer.Instance);
SeedSeen(local, seen);
AddUnique(otherCandidates, seen, result);
return result;
}
private static void SeedSeen(List<SharedContainer> local, HashSet<Inventory> seen)
{
if (local == null)
{
return;
}
for (int i = 0; i < local.Count; i++)
{
SharedContainer sharedContainer = local[i];
if (sharedContainer != null && sharedContainer.Inventory != null)
{
seen.Add(sharedContainer.Inventory);
}
}
}
private static void AddUnique(List<SharedContainer> containers, HashSet<Inventory> seen, List<SharedContainer> result)
{
if (containers == null)
{
return;
}
for (int i = 0; i < containers.Count; i++)
{
SharedContainer sharedContainer = containers[i];
if (sharedContainer != null && sharedContainer.Inventory != null && seen.Add(sharedContainer.Inventory))
{
result.Add(sharedContainer);
}
}
}
}
public sealed class StorageSnapshot
{
private readonly ReadOnlyCollection<SharedContainer> local;
private readonly ReadOnlyCollection<SharedContainer> other;
private readonly ReadOnlyCollection<SharedContainer> all;
public WorldZoneData CurrentZone { get; }
public IReadOnlyList<SharedContainer> LocalContainers => local;
public IReadOnlyList<SharedContainer> OtherZoneContainers => other;
public IReadOnlyList<SharedContainer> AllContainers => all;
internal StorageSnapshot(WorldZoneData currentZone, List<SharedContainer> localContainers, List<SharedContainer> otherContainers)
{
CurrentZone = currentZone;
List<SharedContainer> list = ((localContainers != null) ? new List<SharedContainer>(localContainers) : new List<SharedContainer>());
List<SharedContainer> list2 = ((otherContainers != null) ? new List<SharedContainer>(otherContainers) : new List<SharedContainer>());
local = new ReadOnlyCollection<SharedContainer>(list);
other = new ReadOnlyCollection<SharedContainer>(list2);
List<SharedContainer> list3 = new List<SharedContainer>(list.Count + list2.Count);
list3.AddRange(list);
list3.AddRange(list2);
all = new ReadOnlyCollection<SharedContainer>(list3);
}
}
internal static class ZoneEligibility
{
internal static bool IsEligible(bool isContainer, bool isActive, bool includeInactiveZones, bool excluded)
{
if (!isContainer)
{
return false;
}
if (!isActive && !includeInactiveZones)
{
return false;
}
return !excluded;
}
}
internal static class CrossZoneStorage
{
private sealed class ZoneEntry
{
internal WorldZoneData Zone;
internal List<Inventory> Inventories;
internal List<WgoData> WgoDataList;
}
private const int MaxCachedZones = 256;
private static readonly object Gate = new object();
private static readonly List<ZoneEntry> Cache = new List<ZoneEntry>();
private static long worldSignature = long.MinValue;
private static int lastLoggedContainerCount = -1;
private static int faultCount;
private static float nextFaultLogTime;
[ThreadStatic]
private static int suppressDepth;
[ThreadStatic]
private static int pickerDepth;
internal static void Invalidate()
{
lock (Gate)
{
Cache.Clear();
worldSignature = long.MinValue;
lastLoggedContainerCount = -1;
}
}
internal static bool TryBuildSnapshot(WorldZoneData currentZone, out StorageSnapshot snapshot)
{
snapshot = null;
try
{
if (!IsCurrentZoneEligible(currentZone))
{
return false;
}
WorldData worldData = MainGame.WorldData;
if (worldData == null)
{
return false;
}
List<SharedContainer> list = BuildLocalContainers(worldData, currentZone);
ZoneEntry zoneEntry = GetZoneEntry(worldData, currentZone);
List<SharedContainer> list2 = new List<SharedContainer>();
if (zoneEntry.WgoDataList != null)
{
for (int i = 0; i < zoneEntry.WgoDataList.Count; i++)
{
WgoData val = zoneEntry.WgoDataList[i];
if (val == null)
{
continue;
}
Inventory inventory = val.Inventory;
if (inventory != null)
{
WorldZoneData worldZoneData = val.WorldZoneData;
if (worldZoneData != null)
{
list2.Add(new SharedContainer(val, inventory, worldZoneData, isCurrentZone: false));
}
}
}
}
List<SharedContainer> otherContainers = SnapshotAssembler.AppendOtherContainers(list, list2);
snapshot = new StorageSnapshot(currentZone, list, otherContainers);
return true;
}
catch (Exception e)
{
ReportFault(e);
snapshot = null;
return false;
}
}
internal static bool IsCurrentZoneEligible(WorldZoneData currentZone)
{
try
{
if (currentZone == null)
{
return false;
}
HashSet<string> hashSet = ModSettings.BuildExcludedZoneSet();
return ZoneEligibility.IsEligible(currentZone.IsContainer, currentZone.IsActive, ModSettings.IncludeInactiveZones.Value, hashSet.Contains(((ObjectLinkedToDefinition<WorldZoneDef>)(object)currentZone).id));
}
catch
{
return false;
}
}
private static List<SharedContainer> BuildLocalContainers(WorldData worldData, WorldZoneData currentZone)
{
List<SharedContainer> list = new List<SharedContainer>();
HashSet<Inventory> hashSet = new HashSet<Inventory>();
List<SGuid> wgoDataList = currentZone.wgoDataList;
if (wgoDataList == null)
{
return list;
}
for (int i = 0; i < wgoDataList.Count; i++)
{
WgoData val = ResolveSharedWgoData(worldData, wgoDataList[i]);
if (val != null && val.Inventory != null && hashSet.Add(val.Inventory))
{
list.Add(new SharedContainer(val, val.Inventory, currentZone, isCurrentZone: true));
}
}
return list;
}
internal static void PushSuppress()
{
suppressDepth++;
}
internal static void PopSuppress()
{
if (suppressDepth > 0)
{
suppressDepth--;
}
}
internal static void PushPickerScope()
{
pickerDepth++;
}
internal static void PopPickerScope()
{
if (pickerDepth > 0)
{
pickerDepth--;
}
}
internal static void Apply(WgoData craftable, bool excludeWorkerInventory, MultiInventory result)
{
if (craftable == null)
{
return;
}
Inventory excluded = null;
if (excludeWorkerInventory)
{
IWorker worker = craftable.Worker;
if (worker != null)
{
excluded = worker.WorkerInventory;
}
}
ApplyCore(craftable.WorldZoneData, excluded, result, ((ObjectLinkedToDefinition<WGODef>)(object)craftable).id);
}
internal static void ApplyForPlayerPool(MultiInventory result, PlayerData playerData, bool addCurrentPlayerWorldZone)
{
if (!addCurrentPlayerWorldZone || playerData == null)
{
return;
}
if (pickerDepth > 0)
{
if (ModSettings.ShareWorkbenchPickers)
{
ApplyCore(playerData.CurrentWorldZoneData, null, result, "workbench item selection");
}
}
else if (ModSettings.ShareBuildAndPlanting)
{
ApplyCore(playerData.CurrentWorldZoneData, null, result, "build, planting and tooltips");
}
}
private static void ApplyCore(WorldZoneData currentZone, Inventory excluded, MultiInventory result, string contextId)
{
if (result == null || currentZone == null || !ModSettings.SharingEnabled || suppressDepth > 0)
{
return;
}
List<Inventory> inventoryList = result.inventoryList;
if (inventoryList == null)
{
return;
}
try
{
if ((Object)(object)MainGame.Instance == (Object)null)
{
return;
}
WorldData worldData = MainGame.WorldData;
if (worldData == null)
{
return;
}
List<Inventory> sharedInventories = GetSharedInventories(worldData, currentZone);
if (sharedInventories.Count == 0)
{
return;
}
int num = 0;
for (int i = 0; i < sharedInventories.Count; i++)
{
Inventory val = sharedInventories[i];
if (val != null && (excluded == null || val != excluded) && !ContainsReference(inventoryList, val))
{
inventoryList.Add(val);
num++;
}
}
if (num > 0 && ModSettings.VerboseLogging.Value)
{
Plugin.Log.LogInfo((object)("Shared storage: added " + num + " container(s) from other zones to the pool used for '" + contextId + "'."));
}
}
catch (Exception e)
{
ReportFault(e);
}
}
private static bool ContainsReference(List<Inventory> list, Inventory candidate)
{
for (int i = 0; i < list.Count; i++)
{
if (list[i] == candidate)
{
return true;
}
}
return false;
}
private static List<Inventory> GetSharedInventories(WorldData worldData, WorldZoneData currentZone)
{
return GetZoneEntry(worldData, currentZone).Inventories;
}
private static ZoneEntry GetZoneEntry(WorldData worldData, WorldZoneData currentZone)
{
long num = ComputeSignature(worldData);
lock (Gate)
{
if (worldSignature == num)
{
for (int i = 0; i < Cache.Count; i++)
{
ZoneEntry zoneEntry = Cache[i];
if (zoneEntry.Zone == currentZone)
{
if (IsCacheEntryValid(worldData, zoneEntry))
{
return zoneEntry;
}
Cache.Clear();
worldSignature = long.MinValue;
break;
}
}
}
}
ZoneEntry zoneEntry2 = BuildZoneEntry(worldData, currentZone);
lock (Gate)
{
if (worldSignature != num)
{
Cache.Clear();
worldSignature = num;
}
for (int j = 0; j < Cache.Count; j++)
{
ZoneEntry zoneEntry3 = Cache[j];
if (zoneEntry3.Zone == currentZone)
{
if (IsCacheEntryValid(worldData, zoneEntry3))
{
return zoneEntry3;
}
Cache.Clear();
worldSignature = long.MinValue;
break;
}
}
Cache.Add(zoneEntry2);
if (Cache.Count > 256)
{
Cache.RemoveAt(0);
}
}
return zoneEntry2;
}
private static bool IsCacheEntryValid(WorldData worldData, ZoneEntry entry)
{
if (worldData == null || entry == null || entry.Zone == null || entry.WgoDataList == null)
{
return false;
}
try
{
for (int i = 0; i < entry.WgoDataList.Count; i++)
{
WgoData val = entry.WgoDataList[i];
if (val == null)
{
return false;
}
if (worldData.GetWgoData(val.UniqueId) != val)
{
return false;
}
}
return true;
}
catch
{
return false;
}
}
private static ZoneEntry BuildZoneEntry(WorldData worldData, WorldZoneData currentZone)
{
List<Inventory> list = new List<Inventory>();
List<WgoData> list2 = new List<WgoData>();
HashSet<Inventory> hashSet = new HashSet<Inventory>();
HashSet<string> excludedZoneIds = ModSettings.BuildExcludedZoneSet();
bool value = ModSettings.IncludeInactiveZones.Value;
bool value2 = ModSettings.VerboseLogging.Value;
List<string> list3 = (value2 ? new List<string>() : null);
int num = 0;
List<GameSceneData> gameSceneDataList = worldData.gameSceneDataList;
if (gameSceneDataList != null)
{
for (int i = 0; i < gameSceneDataList.Count; i++)
{
GameSceneData val = gameSceneDataList[i];
if (val == null)
{
continue;
}
List<WorldZoneData> worldZones = val.worldZones;
if (worldZones == null)
{
continue;
}
for (int j = 0; j < worldZones.Count; j++)
{
WorldZoneData val2 = worldZones[j];
if (val2 == null || val2 == currentZone || !val2.IsContainer || (!value && !val2.IsActive) || IsExcludedZone(val2, excludedZoneIds))
{
continue;
}
List<SGuid> wgoDataList = val2.wgoDataList;
if (wgoDataList == null)
{
continue;
}
int count = list.Count;
for (int k = 0; k < wgoDataList.Count; k++)
{
WgoData val3 = ResolveSharedWgoData(worldData, wgoDataList[k]);
if (val3 != null)
{
Inventory inventory = val3.Inventory;
if (inventory != null && hashSet.Add(inventory))
{
list.Add(inventory);
list2.Add(val3);
}
}
}
int num2 = list.Count - count;
if (num2 > 0)
{
num++;
list3?.Add(((ObjectLinkedToDefinition<WorldZoneDef>)(object)val2).id + "=" + num2);
}
}
}
}
ReportBuildResult(currentZone, num, list.Count, list3, value2);
return new ZoneEntry
{
Zone = currentZone,
Inventories = list,
WgoDataList = list2
};
}
private static WgoData ResolveSharedWgoData(WorldData worldData, SGuid uniqueId)
{
if (uniqueId == (SGuid)null)
{
return null;
}
WgoData wgoData = worldData.GetWgoData(uniqueId);
if (wgoData == null)
{
return null;
}
WGODef definition = ((ObjectLinkedToDefinition<WGODef>)(object)wgoData).Definition;
if (definition == null || definition.inventorySize == 0)
{
return null;
}
if (!definition.OpenInMultiInventory)
{
return null;
}
return wgoData;
}
private static bool IsExcludedZone(WorldZoneData zone, HashSet<string> excludedZoneIds)
{
if (excludedZoneIds.Count == 0)
{
return false;
}
string id = ((ObjectLinkedToDefinition<WorldZoneDef>)(object)zone).id;
if (!string.IsNullOrEmpty(id))
{
return excludedZoneIds.Contains(id);
}
return false;
}
private static long ComputeSignature(WorldData worldData)
{
long num = 17L;
num = num * 31 + RuntimeHelpers.GetHashCode(worldData);
List<GameSceneData> gameSceneDataList = worldData.gameSceneDataList;
if (gameSceneDataList == null)
{
return num;
}
num = num * 31 + gameSceneDataList.Count;
for (int i = 0; i < gameSceneDataList.Count; i++)
{
GameSceneData val = gameSceneDataList[i];
if (val == null)
{
num = num * 31 + 7;
continue;
}
List<WorldZoneData> worldZones = val.worldZones;
if (worldZones == null)
{
num = num * 31 + 11;
continue;
}
num = num * 31 + worldZones.Count;
for (int j = 0; j < worldZones.Count; j++)
{
WorldZoneData val2 = worldZones[j];
if (val2 == null)
{
num = num * 31 + 13;
continue;
}
num = num * 31 + (val2.wgoDataList?.Count ?? 0);
num = num * 31 + (val2.IsActive ? 1 : 0);
}
}
return num;
}
private static void ReportBuildResult(WorldZoneData currentZone, int sharedZoneCount, int containerCount, List<string> zoneSummary, bool verbose)
{
bool flag = containerCount != lastLoggedContainerCount;
lastLoggedContainerCount = containerCount;
if (containerCount == 0)
{
if (verbose || flag)
{
Plugin.Log.LogInfo((object)("Shared storage: no shareable container found outside zone '" + DescribeZone(currentZone) + "', so cross-zone sharing stays inactive until you build storage elsewhere."));
}
}
else if (verbose || flag)
{
string text = ((zoneSummary != null && zoneSummary.Count > 0) ? (" [" + string.Join(", ", zoneSummary.ToArray()) + "]") : string.Empty);
Plugin.Log.LogInfo((object)("Shared storage: sharing " + containerCount + " container(s) from " + sharedZoneCount + " other zone(s) with zone '" + DescribeZone(currentZone) + "'" + text + "."));
}
}
private static string DescribeZone(WorldZoneData zone)
{
if (zone == null)
{
return "<none>";
}
if (!string.IsNullOrEmpty(((ObjectLinkedToDefinition<WorldZoneDef>)(object)zone).id))
{
return ((ObjectLinkedToDefinition<WorldZoneDef>)(object)zone).id;
}
return "<unnamed>";
}
private static void ReportFault(Exception e)
{
faultCount++;
float realtimeSinceStartup = Time.realtimeSinceStartup;
if (faultCount <= 3 || !(realtimeSinceStartup < nextFaultLogTime))
{
nextFaultLogTime = realtimeSinceStartup + 60f;
Plugin.Log.LogError((object)("Shared storage: could not extend a material pool with other zones (" + faultCount + " failure(s) so far); that call used vanilla behaviour only. " + e));
}
}
}
internal static class ModSettings
{
internal const string SectionGeneral = "General";
internal const string SectionAdvanced = "Advanced";
internal const string SectionPerformance = "Performance";
internal static ConfigEntry<bool> Enabled;
internal static ConfigEntry<bool> IncludeOtherZones;
internal static ConfigEntry<bool> ShareBuildAndBrowsePools;
internal static ConfigEntry<bool> ShareBuildAndPlantingPools;
internal static ConfigEntry<bool> ShareWorkbenchItemSelectionPools;
internal static ConfigEntry<bool> IncludeInactiveZones;
internal static ConfigEntry<string> ExcludedZoneIds;
internal static ConfigEntry<bool> VerboseLogging;
internal static ConfigEntry<bool> PoolDiagnostics;
internal static ConfigEntry<bool> OptimizeInventoryMaterials;
internal static bool Ready
{
get
{
if (Enabled != null && IncludeOtherZones != null && ShareBuildAndBrowsePools != null && ShareBuildAndPlantingPools != null && ShareWorkbenchItemSelectionPools != null && IncludeInactiveZones != null && ExcludedZoneIds != null && VerboseLogging != null && PoolDiagnostics != null)
{
return OptimizeInventoryMaterials != null;
}
return false;
}
}
internal static bool SharingEnabled
{
get
{
if (Ready && Enabled.Value)
{
return IncludeOtherZones.Value;
}
return false;
}
}
internal static bool ExtraPoolsEnabled
{
get
{
if (Ready)
{
return ShareBuildAndBrowsePools.Value;
}
return false;
}
}
internal static bool ShareBuildAndPlanting
{
get
{
if (SharingEnabled && ExtraPoolsEnabled)
{
return ShareBuildAndPlantingPools.Value;
}
return false;
}
}
internal static bool ShareWorkbenchPickers
{
get
{
if (SharingEnabled && ExtraPoolsEnabled)
{
return ShareWorkbenchItemSelectionPools.Value;
}
return false;
}
}
internal static bool Diagnostics
{
get
{
if (Ready && Enabled.Value)
{
return PoolDiagnostics.Value;
}
return false;
}
}
internal static bool MaterialCache
{
get
{
if (Ready && Enabled.Value)
{
return OptimizeInventoryMaterials.Value;
}
return false;
}
}
internal static void Bind(ConfigFile config, Action onChanged)
{
Enabled = config.Bind<bool>("General", "Enabled", true, "Master switch. When false this mod does nothing and the game behaves exactly like vanilla.");
IncludeOtherZones = config.Bind<bool>("General", "IncludeOtherZones", true, "When true, storage containers that sit in OTHER world zones are added to the pool of items a crafting station can consume from. Containers in the zone you are standing in always keep priority, so nearby chests are emptied before distant ones.");
ShareBuildAndBrowsePools = config.Bind<bool>("General", "ShareBuildAndBrowsePools", true, "Deprecated master switch for the two pooled families below. The 1.1.0 mod used this one switch for both of them. Leave it at true and use ShareBuildAndPlantingPools and ShareWorkbenchItemSelectionPools instead; set it to false only if you want neither of them to share anything.");
ShareBuildAndPlantingPools = config.Bind<bool>("General", "ShareBuildAndPlantingPools", true, "Build mode, garden planting, town repair and the seed count of a garden bed use a second material pool that is anchored to the PLAYER instead of to a building. Without this option that pool would still only see the zone the player stands in, so a house built in the basement could only be paid for with material stored in the basement. These pools only count items, they draw no extra UI, so sharing them is cheap.");
ShareWorkbenchItemSelectionPools = config.Bind<bool>("General", "ShareWorkbenchItemSelectionPools", false, "The windows in which you pick an item BY HAND - embalming, autopsy, grave element slots, praying, alchemy boosts, the garden bed fertilizer and seed pickers, resurrection and the resource survey picker - can also see the containers of the other zones. Every container of such a pool becomes its own inventory panel inside the window, so with a large base these windows become slow to open. Off by default: with this off the pickers show local storage only, while crafting, building, planting and the automatic consumption of materials keep working across zones. Turn it on if you want to hand pick a material that is stored in another base.");
IncludeInactiveZones = config.Bind<bool>("General", "IncludeInactiveZones", false, "When false (recommended), world zones that are currently switched off - battle / fighting-stage zones - are ignored. Turn on only if you want those containers shared too.");
ExcludedZoneIds = config.Bind<string>("General", "ExcludedZoneIds", "", "Optional comma separated list of world zone ids that must never be shared. Example: graveyard, church. Combined with VerboseLogging you can read the exact zone ids from BepInEx/LogOutput.log and exclude any zone you do not want reachable.");
VerboseLogging = config.Bind<bool>("Advanced", "VerboseLogging", false, "When true, every cache rebuild logs how many containers were shared and from which zones, and every pool that pulled in extra containers is logged. Useful to verify the mod works and to find zone ids for ExcludedZoneIds.");
OptimizeInventoryMaterials = config.Bind<bool>("Performance", "OptimizeInventoryMaterials", true, "Performance option, on by default. Every time the game draws an inventory slot it creates a fresh copy of the icon material for that slot, so a base with many chests ends up with one unique material per slot: the UI canvas can no longer batch those icons and every redraw allocates hundreds of materials, which is exactly what made the bag and the storage windows feel slow. While this option is on the mod reuses one material per colour instead, which looks the same because the colour is the only difference between those copies. Set it to false only if another mod tints item icons by writing straight into Image.material, which would then affect every slot that shares the same colour.");
PoolDiagnostics = config.Bind<bool>("Advanced", "PoolDiagnostics", false, "Optional measurement mode used to investigate performance reports. While true the mod writes one line per second with how often each patched material pool was built, how long that took, and how long the character window (the bag opened with Tab) needed to build its storage panels. Only for troubleshooting. NOTE: this option is read when the game starts, changing it needs a restart of the game.");
Enabled.SettingChanged += delegate
{
onChanged();
};
IncludeOtherZones.SettingChanged += delegate
{
onChanged();
};
ShareBuildAndBrowsePools.SettingChanged += delegate
{
onChanged();
};
ShareBuildAndPlantingPools.SettingChanged += delegate
{
onChanged();
};
ShareWorkbenchItemSelectionPools.SettingChanged += delegate
{
onChanged();
};
IncludeInactiveZones.SettingChanged += delegate
{
onChanged();
};
ExcludedZoneIds.SettingChanged += delegate
{
onChanged();
};
VerboseLogging.SettingChanged += delegate
{
onChanged();
};
OptimizeInventoryMaterials.SettingChanged += delegate
{
onChanged();
};
PoolDiagnostics.SettingChanged += delegate
{
onChanged();
};
}
internal static HashSet<string> BuildExcludedZoneSet()
{
HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (!Ready)
{
return hashSet;
}
string value = ExcludedZoneIds.Value;
if (string.IsNullOrEmpty(value))
{
return hashSet;
}
string[] array = value.Split(new char[3] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (text.Length > 0)
{
hashSet.Add(text);
}
}
return hashSet;
}
}
[HarmonyPatch(/*Could not decode attribute arguments.*/)]
internal static class Diag_Patch_CharMainPageWidgetDataCtor
{
[HarmonyPrefix]
private static void Prefix(ref long __state)
{
__state = Stopwatch.GetTimestamp();
}
[HarmonyPostfix]
private static void Postfix(long __state)
{
PoolDiagnostics.NoteDataBuild(Stopwatch.GetTimestamp() - __state);
}
}
[HarmonyPatch]
internal static class Diag_Patch_MultiInventoryWidgetSetData
{
private static MethodBase TargetMethod()
{
return AccessTools.DeclaredMethod(typeof(MultiInventoryWidget), "SetData", new Type[1] { typeof(MultiInventoryWidgetData) }, (Type[])null);
}
[HarmonyPrefix]
private static void Prefix(ref long __state)
{
__state = Stopwatch.GetTimestamp();
}
[HarmonyPostfix]
private static void Postfix(long __state)
{
PoolDiagnostics.NotePanelSetup(Stopwatch.GetTimestamp() - __state);
}
}
[HarmonyPatch(typeof(InventoryWidget), "Redraw")]
internal static class Diag_Patch_InventoryWidgetRedraw
{
[HarmonyPrefix]
private static void Prefix(ref long __state)
{
__state = Stopwatch.GetTimestamp();
}
[HarmonyPostfix]
private static void Postfix(InventoryWidget __instance, long __state)
{
int cells = 0;
InventoryWidgetData data = __instance.Data;
if (data != null && ((InventoryWidgetDataBase)data).Inventory != null && ((InventoryWidgetDataBase)data).Inventory.Data != null)
{
cells = ((InventoryWidgetDataBase)data).Inventory.Data.InventorySize;
}
PoolDiagnostics.NotePanelRedraw(Stopwatch.GetTimestamp() - __state, cells);
}
}
[HarmonyPatch(typeof(CharMainPageWidget), "Redraw")]
internal static class Diag_Patch_CharMainPageWidgetRedraw
{
[HarmonyPrefix]
private static void Prefix()
{
PoolDiagnostics.BeginInventoryPage();
}
[HarmonyPostfix]
private static void Postfix()
{
PoolDiagnostics.EndInventoryPage();
}
}
[HarmonyPatch]
internal static class Diag_Patch_MultiInventoryPlayerCtor
{
private static MethodBase TargetMethod()
{
return AccessTools.Constructor(typeof(MultiInventory), new Type[2]
{
typeof(PlayerData),
typeof(bool)
}, false);
}
[HarmonyPrefix]
private static void Prefix(ref long __state)
{
__state = Stopwatch.GetTimestamp();
}
[HarmonyPostfix]
private static void Postfix(long __state)
{
PoolDiagnostics.NotePoolCall(Stopwatch.GetTimestamp() - __state);
PoolDiagnostics.ReportPoolTrafficIfDue();
}
}
[HarmonyPatch(typeof(WgoData), "GetCraftableMultiInventory")]
internal static class Diag_Patch_WgoDataCraftablePool
{
[HarmonyPrefix]
private static void Prefix(ref long __state)
{
__state = Stopwatch.GetTimestamp();
}
[HarmonyPostfix]
private static void Postfix(long __state)
{
PoolDiagnostics.NoteCraftableCall(Stopwatch.GetTimestamp() - __state);
PoolDiagnostics.ReportPoolTrafficIfDue();
}
}
[HarmonyPatch]
internal static class Patch_MultiInventory_PlayerDataCtor
{
private static MethodBase TargetMethod()
{
return AccessTools.Constructor(typeof(MultiInventory), new Type[2]
{
typeof(PlayerData),
typeof(bool)
}, false);
}
[HarmonyPostfix]
private static void Postfix(PlayerData playerData, bool addCurrentPlayerWorldZone, MultiInventory __instance)
{
CrossZoneStorage.ApplyForPlayerPool(__instance, playerData, addCurrentPlayerWorldZone);
}
}
[HarmonyPatch(typeof(ImageExtensions), "BlueColorReplace")]
internal static class Patch_UiMaterialCache
{
private struct Key : IEquatable<Key>
{
internal Material Source;
internal float R;
internal float G;
internal float B;
internal float A;
public bool Equals(Key other)
{
if (Source == other.Source && R == other.R && G == other.G && B == other.B)
{
return A == other.A;
}
return false;
}
public override bool Equals(object obj)
{
if (obj is Key)
{
return Equals((Key)obj);
}
return false;
}
public override int GetHashCode()
{
return (((((((((!((Object)(object)Source == (Object)null)) ? RuntimeHelpers.GetHashCode(Source) : 0) * 397) ^ R.GetHashCode()) * 397) ^ G.GetHashCode()) * 397) ^ B.GetHashCode()) * 397) ^ A.GetHashCode();
}
}
private const int MaxCachedMaterials = 256;
private static readonly int TintId = Shader.PropertyToID("_Color");
private static readonly Dictionary<Key, Material> Cache = new Dictionary<Key, Material>();
private static readonly Dictionary<Material, Material> SourceOfInstance = new Dictionary<Material, Material>();
private static int faultCount;
private static float nextFaultLogTime;
[HarmonyPrefix]
private static bool Prefix(Image image, Color color)
{
//IL_006f: Unknown result type (might be due to invalid IL or missing references)
//IL_007c: Unknown result type (might be due to invalid IL or missing references)
//IL_0089: Unknown result type (might be due to invalid IL or missing references)
//IL_0096: Unknown result type (might be due to invalid IL or missing references)
//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
//IL_00e0: Expected O, but got Unknown
//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
if (!ModSettings.MaterialCache)
{
return true;
}
if ((Object)(object)image == (Object)null)
{
return true;
}
try
{
Material material = ((Graphic)image).material;
if ((Object)(object)material == (Object)null)
{
return true;
}
if (!SourceOfInstance.TryGetValue(material, out var value))
{
value = material;
}
string name = ((Object)value).name;
if (name != null && name.EndsWith("(Instance)"))
{
return true;
}
Key key = new Key
{
Source = value,
R = color.r,
G = color.g,
B = color.b,
A = color.a
};
if (Cache.TryGetValue(key, out var value2))
{
if (material != value2)
{
((Graphic)image).material = value2;
}
return false;
}
if (Cache.Count >= 256)
{
return true;
}
Material val = new Material(value);
((Object)val).name = ((Object)value).name + " (shared)";
val.SetColor(TintId, color);
Cache[key] = val;
SourceOfInstance[val] = value;
((Graphic)image).material = val;
return false;
}
catch (Exception e)
{
ReportFault(e);
return true;
}
}
private static void ReportFault(Exception e)
{
faultCount++;
float realtimeSinceStartup = Time.realtimeSinceStartup;
if (faultCount <= 3 || !(realtimeSinceStartup < nextFaultLogTime))
{
nextFaultLogTime = realtimeSinceStartup + 60f;
Plugin.Log.LogError((object)("Shared Base Storage: could not reuse an inventory material (" + faultCount + " failure(s) so far), those slots used the vanilla material handling. " + e));
}
}
}
[HarmonyPatch(typeof(WgoData), "GetCraftableMultiInventory")]
internal static class Patch_WgoData_GetCraftableMultiInventory
{
[HarmonyPostfix]
private static void Postfix(WgoData __instance, bool excludeWorkerInventory, ref MultiInventory __result)
{
CrossZoneStorage.Apply(__instance, excludeWorkerInventory, __result);
}
}
[HarmonyPatch]
internal static class Patch_WorkbenchPickerScope
{
private static MethodBase TargetMethod()
{
return AccessTools.DeclaredConstructor(typeof(UIMultiInventoryWindowData), new Type[6]
{
typeof(PlayerData),
typeof(Action<UIItemCell>),
typeof(Func<Item, bool>),
typeof(bool),
typeof(string),
typeof(Func<Item, string>)
}, false);
}
[HarmonyPrefix]
private static void Prefix()
{
CrossZoneStorage.PushPickerScope();
}
[HarmonyFinalizer]
private static void Finalizer()
{
CrossZoneStorage.PopPickerScope();
}
}
[HarmonyPatch(typeof(ZombieEquipmentInventoryWidgetData), "OnArmorCellPressed2", new Type[] { typeof(UIItemCell) })]
internal static class Patch_ZombieArmor_KeepDropsLocal
{
[HarmonyPrefix]
private static void Prefix()
{
CrossZoneStorage.PushSuppress();
}
[HarmonyFinalizer]
private static void Finalizer()
{
CrossZoneStorage.PopSuppress();
}
}
[HarmonyPatch(typeof(ZombieEquipmentInventoryWidgetData), "OnInstrumentCellPressed2", new Type[] { typeof(UIItemCell) })]
internal static class Patch_ZombieInstrument_KeepDropsLocal
{
[HarmonyPrefix]
private static void Prefix()
{
CrossZoneStorage.PushSuppress();
}
[HarmonyFinalizer]
private static void Finalizer()
{
CrossZoneStorage.PopSuppress();
}
}
[BepInPlugin("gk2.sharedstorage", "Shared Base Storage", "1.2.0")]
public sealed class Plugin : BaseUnityPlugin
{
public const string PluginGuid = "gk2.sharedstorage";
public const string PluginName = "Shared Base Storage";
public const string PluginVersion = "1.2.0";
internal static ManualLogSource Log;
internal static Plugin Instance;
internal static bool ApiSnapshotReady;
private static readonly Type[] AlwaysPatched = new Type[6]
{
typeof(Patch_WgoData_GetCraftableMultiInventory),
typeof(Patch_MultiInventory_PlayerDataCtor),
typeof(Patch_ZombieArmor_KeepDropsLocal),
typeof(Patch_ZombieInstrument_KeepDropsLocal),
typeof(Patch_WorkbenchPickerScope),
typeof(Patch_UiMaterialCache)
};
private static readonly Type[] DiagnosticPatched = new Type[6]
{
typeof(Diag_Patch_CharMainPageWidgetDataCtor),
typeof(Diag_Patch_MultiInventoryWidgetSetData),
typeof(Diag_Patch_InventoryWidgetRedraw),
typeof(Diag_Patch_CharMainPageWidgetRedraw),
typeof(Diag_Patch_MultiInventoryPlayerCtor),
typeof(Diag_Patch_WgoDataCraftablePool)
};
private Harmony harmony;
private int appliedPatches;
private int requestedPatches;
private void Awake()
{
//IL_004b: Unknown result type (might be due to invalid IL or missing references)
//IL_0055: Expected O, but got Unknown
Log = ((BaseUnityPlugin)this).Logger;
Instance = this;
ModSettings.Bind(((BaseUnityPlugin)this).Config, OnSettingsChanged);
List<Type> list = new List<Type>(AlwaysPatched);
if (ModSettings.Diagnostics)
{
list.AddRange(DiagnosticPatched);
}
harmony = new Harmony("gk2.sharedstorage");
for (int i = 0; i < list.Count; i++)
{
Type type = list[i];
try
{
harmony.CreateClassProcessor(type).Patch();
appliedPatches++;
}
catch (Exception ex)
{
Log.LogError((object)("Shared Base Storage: patch " + type.Name + " could not be applied, so only that part of the mod stays vanilla (this usually means the game was updated). " + ex.Message));
}
}
requestedPatches = list.Count;
ApiSnapshotReady = requestedPatches > 0 && appliedPatches == requestedPatches;
LogPatchedMethods();
if (appliedPatches == 0)
{
Log.LogError((object)"Shared Base Storage disabled: none of the Harmony patches could be applied. Vanilla behaviour is untouched.");
return;
}
Log.LogInfo((object)("Shared Base Storage 1.2.0 loaded (" + appliedPatches + "/" + requestedPatches + " patches). Base storage is now shared between world zones. Settings: " + ((BaseUnityPlugin)this).Config.ConfigFilePath));
LogConfiguration();
}
private void LogConfiguration()
{
if (!ModSettings.Enabled.Value)
{
Log.LogInfo((object)"Shared Base Storage is installed but disabled in the config file.");
return;
}
if (!ModSettings.IncludeOtherZones.Value)
{
Log.LogInfo((object)"Shared Base Storage is installed but IncludeOtherZones is off, so nothing is shared.");
return;
}
Log.LogInfo((object)("Shared storage: crafting stations share storage with the other zones. Extra pools: build/planting/repair " + (ModSettings.ShareBuildAndPlanting ? "ON" : "OFF") + ", workbench item pickers " + (ModSettings.ShareWorkbenchPickers ? "ON" : "OFF") + " (set ShareWorkbenchItemSelectionPools to true to share those windows as well, they draw one panel per container and are the slowest part of the UI)."));
Log.LogInfo((object)("Shared storage: inventory material reuse " + (ModSettings.MaterialCache ? "ON" : "OFF") + " (OptimizeInventoryMaterials)."));
if (ModSettings.Diagnostics)
{
Log.LogInfo((object)"Shared storage: PoolDiagnostics is ON, one measurement line per second is written to the log. Set PoolDiagnostics to false and restart the game when you are done.");
}
}
private void LogPatchedMethods()
{
try
{
List<string> list = new List<string>();
foreach (MethodBase patchedMethod in harmony.GetPatchedMethods())
{
if (patchedMethod != null)
{
list.Add(patchedMethod.DeclaringType.Name + "." + patchedMethod.Name);
}
}
list.Sort(StringComparer.Ordinal);
Log.LogInfo((object)("Shared storage: patched " + list.Count + " method(s): " + string.Join(", ", list.ToArray())));
}
catch (Exception ex)
{
Log.LogWarning((object)("Shared storage: could not list the patched methods. " + ex.Message));
}
}
private void OnSettingsChanged()
{
CrossZoneStorage.Invalidate();
if (Log != null)
{
Log.LogInfo((object)"Shared storage: settings changed, the cached container lists were cleared.");
}
}
private void OnDestroy()
{
if (harmony != null)
{
try
{
harmony.UnpatchSelf();
}
catch
{
}
}
ApiSnapshotReady = false;
if ((Object)(object)Instance == (Object)(object)this)
{
Instance = null;
}
}
}
internal static class PoolDiagnostics
{
private static readonly double MsPerTick = 1000.0 / (double)Stopwatch.Frequency;
private static long nextPoolReport;
private static long poolCalls;
private static long poolTicks;
private static long craftableCalls;
private static long craftableTicks;
private static int inventoryPageDepth;
private static long pageStart;
private static long pendingDataBuildTicks;
private static long panelSetupTicks;
private static long panelRedrawTicks;
private static int panelCount;
private static int cellCount;
internal static void NoteDataBuild(long ticks)
{
pendingDataBuildTicks += ticks;
}
internal static void NotePanelSetup(long ticks)
{
if (inventoryPageDepth > 0)
{
panelSetupTicks += ticks;
}
}
internal static void NotePanelRedraw(long ticks, int cells)
{
if (inventoryPageDepth > 0)
{
panelRedrawTicks += ticks;
panelCount++;
cellCount += cells;
}
}
internal static void NotePoolCall(long ticks)
{
poolCalls++;
poolTicks += ticks;
}
internal static void NoteCraftableCall(long ticks)
{
craftableCalls++;
craftableTicks += ticks;
}
internal static void BeginInventoryPage()
{
if (inventoryPageDepth == 0)
{
pageStart = Stopwatch.GetTimestamp();
panelSetupTicks = 0L;
panelRedrawTicks = 0L;
panelCount = 0;
cellCount = 0;
}
inventoryPageDepth++;
}
internal static void EndInventoryPage()
{
if (inventoryPageDepth <= 0)
{
return;
}
inventoryPageDepth--;
if (inventoryPageDepth <= 0)
{
long ticks = Stopwatch.GetTimestamp() - pageStart;
long ticks2 = pendingDataBuildTicks;
pendingDataBuildTicks = 0L;
if (Plugin.Log != null)
{
Plugin.Log.LogInfo((object)("Shared storage diag: inventory page (Tab) - storage data " + Ms(ticks2) + " ms, panel setup " + Ms(panelSetupTicks) + " ms, panel redraw " + Ms(panelRedrawTicks) + " ms, whole page " + Ms(ticks) + " ms | " + panelCount + " panel(s), " + cellCount + " cell(s)"));
}
}
}
internal static void ReportPoolTrafficIfDue()
{
long timestamp = Stopwatch.GetTimestamp();
if (timestamp < nextPoolReport)
{
return;
}
nextPoolReport = timestamp + Stopwatch.Frequency;
if (poolCalls != 0L || craftableCalls != 0L)
{
if (Plugin.Log != null)
{
Plugin.Log.LogInfo((object)("Shared storage diag: material pools in the last second - MultiInventory(player) x" + poolCalls + " " + Ms(poolTicks) + " ms, crafting pools x" + craftableCalls + " " + Ms(craftableTicks) + " ms"));
}
poolCalls = 0L;
poolTicks = 0L;
craftableCalls = 0L;
craftableTicks = 0L;
}
}
private static string Ms(long ticks)
{
return ((double)ticks * MsPerTick).ToString("0.0");
}
}