Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ItemVariantSync v1.0.1
BepInEx/plugins/ItemVariantSync/ItemVariantSync.dll
Decompiled 11 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using ItemVariantSync.Patches; using Microsoft.CodeAnalysis; using Newtonsoft.Json.Linq; using Unity.Collections; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ItemVariantSync")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+bdd05f79510b75023348ca1bd704b02628a3db46")] [assembly: AssemblyProduct("ItemVariantSync")] [assembly: AssemblyTitle("ItemVariantSync")] [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 ItemVariantSync { internal static class DawnLibBridge { private const string EventsTypeName = "Dawn.Internal.DawnItemSaveEvents"; private const string ExtraDataKey = "LVS_variant"; private static bool _initTried; internal static bool Active { get; private set; } internal static void TryInit() { if (_initTried) { return; } _initTried = true; try { Type type = AccessTools.TypeByName("Dawn.Internal.DawnItemSaveEvents"); if (type == null) { Plugin.Log.LogInfo((object)"DawnLib not installed - variant persistence disabled (sync and icons unaffected)."); return; } EventInfo eventInfo = type.GetEvent("OnCollectExtraSaveData", BindingFlags.Static | BindingFlags.Public); EventInfo eventInfo2 = type.GetEvent("OnLoadExtraSaveData", BindingFlags.Static | BindingFlags.Public); if (eventInfo == null || eventInfo2 == null) { Plugin.Log.LogWarning((object)"DawnLib found but its item-save events are missing (version change?) - variant persistence disabled."); return; } eventInfo.AddEventHandler(null, Delegate.CreateDelegate(eventInfo.EventHandlerType, typeof(DawnLibBridge).GetMethod("OnCollect", BindingFlags.Static | BindingFlags.NonPublic))); eventInfo2.AddEventHandler(null, Delegate.CreateDelegate(eventInfo2.EventHandlerType, typeof(DawnLibBridge).GetMethod("OnLoad", BindingFlags.Static | BindingFlags.NonPublic))); Active = true; Plugin.Log.LogInfo((object)"DawnLib item pipeline detected - variant persistence rides DawnLib's per-item save data."); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"DawnLib integration failed to initialize - variant persistence disabled: {arg}"); } } private static void OnCollect(GrabbableObject item, JObject extraData) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown try { NetworkObject val = default(NetworkObject); if (!((Object)(object)item == (Object)null) && extraData != null && ((Component)item).TryGetComponent<NetworkObject>(ref val) && val.IsSpawned && VariantRegistry.TryGet(val.NetworkObjectId, out var data) && !data.IsEmpty) { JObject val2 = new JObject(); if (data.MeshIndex >= 0) { val2["m"] = JToken.op_Implicit(data.MeshIndex); } if (data.MaterialIndex >= 0) { val2["t"] = JToken.op_Implicit(data.MaterialIndex); } extraData["LVS_variant"] = (JToken)(object)val2; } } catch (Exception arg) { ManualLogSource log = Plugin.Log; object arg2; if (item == null) { arg2 = null; } else { GameObject gameObject = ((Component)item).gameObject; arg2 = ((gameObject != null) ? ((Object)gameObject).name : null); } log.LogWarning((object)$"Failed embedding variant data for '{arg2}': {arg}"); } } private static void OnLoad(GrabbableObject item, JObject extraData) { try { if ((Object)(object)item == (Object)null || extraData == null) { return; } JToken obj = extraData["LVS_variant"]; JObject val = (JObject)(object)((obj is JObject) ? obj : null); if (val == null) { return; } VariantData variantData = new VariantData(); JToken obj2 = val["m"]; variantData.MeshIndex = (short)((obj2 != null) ? obj2.ToObject<short>() : (-1)); JToken obj3 = val["t"]; variantData.MaterialIndex = (short)((obj3 != null) ? obj3.ToObject<short>() : (-1)); VariantData variantData2 = variantData; if (variantData2.IsEmpty) { return; } NetworkObject val2 = default(NetworkObject); if (!((Component)item).TryGetComponent<NetworkObject>(ref val2) || !val2.IsSpawned) { Plugin.Log.LogWarning((object)("DawnLib restore fired for '" + ((Object)((Component)item).gameObject).name + "' before spawn - skipping (unexpected ordering).")); return; } VariantRegistry.Set(val2.NetworkObjectId, variantData2); VariantManager.Apply(item, variantData2); if (VariantManager.IsServer) { VariantManager.QueueBroadcast(val2.NetworkObjectId); } } catch (Exception arg) { ManualLogSource log = Plugin.Log; object arg2; if (item == null) { arg2 = null; } else { GameObject gameObject = ((Component)item).gameObject; arg2 = ((gameObject != null) ? ((Object)gameObject).name : null); } log.LogWarning((object)$"Failed restoring variant data for '{arg2}': {arg}"); } } } internal static class RuntimeIconsBridge { private enum State { Unchecked, Ready, Unavailable } private struct PendingIcon { public Item Clone; public Sprite Fallback; public float Deadline; } private const string RuntimeIconsGuid = "com.github.lethalcompanymodding.runtimeicons"; private const float RestoreTimeout = 10f; private static State _state; private static MethodInfo _enqueueMethod; private static Func<object> _resolveQueue; private static Func<object, object> _queueGetter; private static readonly List<PendingIcon> Pending = new List<PendingIcon>(8); internal static void OnCloneAssigned(GrabbableObject item, Item clone, Item baseItem, bool newClone) { try { if (!newClone || (Object)(object)item == (Object)null || (Object)(object)clone == (Object)null) { return; } EnsureInitialized(); if (_state != State.Ready) { return; } object obj = _resolveQueue(); if (obj != null && EnsureEnqueueResolved(obj)) { Sprite val = (((Object)(object)clone.itemIcon != (Object)null) ? clone.itemIcon : baseItem?.itemIcon); clone.itemIcon = null; try { _enqueueMethod.Invoke(obj, new object[3] { item, val, 2L }); } catch { clone.itemIcon = val; throw; } Pending.Add(new PendingIcon { Clone = clone, Fallback = val, Deadline = Time.unscaledTime + 10f }); } } catch (Exception arg) { _state = State.Unavailable; Plugin.Log.LogWarning((object)$"RuntimeIcons integration disabled after error: {arg}"); } } internal static void Tick() { if (Pending.Count == 0) { return; } float unscaledTime = Time.unscaledTime; for (int num = Pending.Count - 1; num >= 0; num--) { PendingIcon pendingIcon = Pending[num]; bool flag; if ((Object)(object)pendingIcon.Clone == (Object)null || (Object)(object)pendingIcon.Clone.itemIcon != (Object)null) { flag = true; } else if (unscaledTime > pendingIcon.Deadline) { pendingIcon.Clone.itemIcon = pendingIcon.Fallback; flag = true; } else { flag = false; } if (flag) { int index = Pending.Count - 1; Pending[num] = Pending[index]; Pending.RemoveAt(index); } } } private static void EnsureInitialized() { if (_state != State.Unchecked) { return; } _state = State.Unavailable; try { Type type = AccessTools.TypeByName("RuntimeIcons.RuntimeIcons"); if (type == null) { Plugin.Log.LogInfo((object)"RuntimeIcons not loaded (type RuntimeIcons.RuntimeIcons not found) - variant icon rendering off; variant identities keep base icons."); return; } string text = type.Assembly.GetName().Version?.ToString() ?? "?"; if (!Chainloader.PluginInfos.ContainsKey("com.github.lethalcompanymodding.runtimeicons")) { List<string> list = new List<string>(); foreach (string key in Chainloader.PluginInfos.Keys) { if (key.IndexOf("runtime", StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(key); } } Plugin.Log.LogInfo((object)("RuntimeIcons assembly " + text + " is loaded but GUID 'com.github.lethalcompanymodding.runtimeicons' is not in the plugin registry (similar keys: " + ((list.Count > 0) ? string.Join(", ", list) : "none") + "). Proceeding via type binding.")); } Func<object> stageGetter = GetStaticGetter(type, "RenderingStage"); if (stageGetter == null) { Plugin.Log.LogWarning((object)"RuntimeIcons binding failed at step RenderingStage (member missing - version drift). Icon rendering off."); return; } object obj = stageGetter(); _resolveQueue = delegate { object obj3 = stageGetter(); if (obj3 == null) { return (object)null; } EnsureQueueAccessors(obj3); return _queueGetter?.Invoke(obj3); }; if (obj != null) { EnsureQueueAccessors(obj); if (_queueGetter == null) { Plugin.Log.LogWarning((object)("RuntimeIcons binding failed at step CameraQueue (member missing on " + obj.GetType().Name + " - version drift). Icon rendering off.")); return; } object obj2 = _queueGetter(obj); if (obj2 != null && !EnsureEnqueueResolved(obj2)) { Plugin.Log.LogWarning((object)"RuntimeIcons binding failed at step EnqueueObject (signature changed - version drift). Icon rendering off."); return; } } _state = State.Ready; Plugin.Log.LogInfo((object)("RuntimeIcons " + text + " bound - per-variant identities get their own rendered icons.")); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"RuntimeIcons integration unavailable (exception during binding): {arg}"); } } private static void EnsureQueueAccessors(object stage) { if (_queueGetter != null || stage == null) { return; } Type type = stage.GetType(); PropertyInfo prop = type.GetProperty("CameraQueue", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (prop != null && prop.CanRead) { _queueGetter = (object o) => prop.GetValue(o); return; } FieldInfo field = type.GetField("CameraQueue", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { _queueGetter = (object o) => field.GetValue(o); } } private static bool EnsureEnqueueResolved(object queue) { if (_enqueueMethod != null) { return true; } _enqueueMethod = queue.GetType().GetMethod("EnqueueObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[3] { typeof(GrabbableObject), typeof(Sprite), typeof(long) }, null); return _enqueueMethod != null; } private static Func<object> GetStaticGetter(Type type, string memberName) { PropertyInfo prop = type.GetProperty(memberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (prop != null && prop.CanRead) { return () => prop.GetValue(null); } FieldInfo field = type.GetField(memberName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return () => field.GetValue(null); } return null; } } internal static class Messaging { [CompilerGenerated] private static class <>O { public static HandleNamedMessageDelegate <0>__OnAssignReceived; public static HandleNamedMessageDelegate <1>__OnRequestReceived; } private const string AssignMsg = "LVS_Assign"; private const string RequestMsg = "LVS_Request"; private const byte ProtocolVersion = 2; private const int MaxRecordsPerMessage = 400; private static CustomMessagingManager Cmm => ((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null; internal static void RegisterHandlers() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown CustomMessagingManager cmm = Cmm; if (cmm != null) { object obj = <>O.<0>__OnAssignReceived; if (obj == null) { HandleNamedMessageDelegate val = OnAssignReceived; <>O.<0>__OnAssignReceived = val; obj = (object)val; } cmm.RegisterNamedMessageHandler("LVS_Assign", (HandleNamedMessageDelegate)obj); object obj2 = <>O.<1>__OnRequestReceived; if (obj2 == null) { HandleNamedMessageDelegate val2 = OnRequestReceived; <>O.<1>__OnRequestReceived = val2; obj2 = (object)val2; } cmm.RegisterNamedMessageHandler("LVS_Request", (HandleNamedMessageDelegate)obj2); } } internal static void BroadcastAssignments(List<ulong> ids) { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer) { return; } List<ulong> list = null; foreach (ulong connectedClientsId in singleton.ConnectedClientsIds) { if (connectedClientsId != 0) { (list ?? (list = new List<ulong>())).Add(connectedClientsId); } } if (list != null) { SendAssignments(ids, list); } } internal static void SendFullTable(ulong clientId) { List<ulong> list = new List<ulong>(VariantRegistry.Table.Keys); if (list.Count != 0) { SendAssignments(list, new List<ulong> { clientId }); } } private static void SendAssignments(List<ulong> ids, List<ulong> targetClients) { //IL_00bb: 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_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) CustomMessagingManager cmm = Cmm; if (cmm == null) { return; } FastBufferWriter val = default(FastBufferWriter); for (int i = 0; i < ids.Count; i += 400) { int num = Math.Min(400, ids.Count - i); List<KeyValuePair<ulong, VariantData>> list = new List<KeyValuePair<ulong, VariantData>>(num); int num2 = 5; for (int j = 0; j < num; j++) { if (VariantRegistry.TryGet(ids[i + j], out var data)) { list.Add(new KeyValuePair<ulong, VariantData>(ids[i + j], data)); num2 += 12; } } if (list.Count == 0) { continue; } ((FastBufferWriter)(ref val))..ctor(num2 + 16, (Allocator)2, -1); try { byte b = 2; ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); int count = list.Count; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref count, default(ForPrimitives)); foreach (KeyValuePair<ulong, VariantData> item in list) { ulong key = item.Key; ((FastBufferWriter)(ref val)).WriteValueSafe<ulong>(ref key, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<short>(ref item.Value.MeshIndex, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<short>(ref item.Value.MaterialIndex, default(ForPrimitives)); } foreach (ulong targetClient in targetClients) { cmm.SendNamedMessage("LVS_Assign", targetClient, val, (NetworkDelivery)4); } } finally { ((FastBufferWriter)(ref val)).Dispose(); } } } internal static void SendTableRequest() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) CustomMessagingManager cmm = Cmm; if (cmm == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(4, (Allocator)2, -1); try { byte b = 2; ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); cmm.SendNamedMessage("LVS_Request", 0uL, val, (NetworkDelivery)2); } finally { ((FastBufferWriter)(ref val)).Dispose(); } } private static void OnAssignReceived(ulong senderClientId, FastBufferReader reader) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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_0076: 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_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: 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) try { if (senderClientId != 0) { return; } byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); if (b != 2) { Plugin.Log.LogWarning((object)$"Ignoring LVS_Assign with protocol v{b} (mine: v{(byte)2}). Update the mod on all peers."); return; } int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); ulong networkObjectId = default(ulong); for (int i = 0; i < num; i++) { ((FastBufferReader)(ref reader)).ReadValueSafe<ulong>(ref networkObjectId, default(ForPrimitives)); VariantData variantData = new VariantData(); ((FastBufferReader)(ref reader)).ReadValueSafe<short>(ref variantData.MeshIndex, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<short>(ref variantData.MaterialIndex, default(ForPrimitives)); VariantManager.OnAssignmentReceived(networkObjectId, variantData); } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed reading LVS_Assign: {arg}"); } } private static void OnRequestReceived(ulong senderClientId, FastBufferReader reader) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) try { NetworkManager singleton = NetworkManager.Singleton; if (!((Object)(object)singleton == (Object)null) && singleton.IsServer) { byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); if (b != 2) { Plugin.Log.LogWarning((object)$"Client {senderClientId} runs protocol v{b} (mine: v{(byte)2}); replying anyway."); } SendFullTable(senderClientId); } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed handling LVS_Request: {arg}"); } } } [BepInPlugin("com.luth.itemvariantsync", "ItemVariantSync", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string Guid = "com.luth.itemvariantsync"; public const string Name = "ItemVariantSync"; public const string Version = "1.0.0"; private Harmony _harmony; private static int _lastPumpFrame = -1; private static bool _announcedPump; public static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static int UnityUpdates { get; private set; } private void Awake() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; _harmony = new Harmony("com.luth.itemvariantsync"); int num = TryPatch(typeof(GrabbableObjectPatches), "item spawn hook"); int num2 = TryPatch(typeof(LifecyclePatches), "session lifecycle"); int num3 = TryPatch(typeof(RoundManagerPatches), "vanilla re-roll override"); int num4 = TryPatch(typeof(SaveLoadPatches), "persistence"); int num5 = TryPatch(typeof(HudScrapDisplayPatches), "collected-scrap popup mesh"); int num6 = TryPatch(typeof(TickPatches), "per-frame pump"); string text = $"hooks: spawn={num} lifecycle={num2} reroll={num3} persistence={num4} popup={num5} tick={num6}"; if (num > 0 && num2 > 0) { Log.LogInfo((object)("ItemVariantSync 1.0.0 (build " + BuildStamp() + ") loaded; " + text + ".")); } else { Log.LogError((object)("ItemVariantSync 1.0.0 in degraded mode (" + text + ") - core hooks missing (game update?), behavior falls back to vanilla.")); } } private static string BuildStamp() { try { string location = typeof(Plugin).Assembly.Location; return string.IsNullOrEmpty(location) ? "unknown" : File.GetLastWriteTime(location).ToString("MM-dd HH:mm:ss"); } catch { return "unknown"; } } private int TryPatch(Type patchClass, string feature) { try { int num = _harmony.CreateClassProcessor(patchClass, true).Patch()?.Count ?? 0; if (num == 0) { Log.LogWarning((object)(feature + ": no methods patched - feature inactive.")); } return num; } catch (Exception arg) { Log.LogError((object)$"Failed to apply {feature} patches - that feature is disabled, vanilla behavior applies. {arg}"); return 0; } } internal static void Pump(bool fromHarmony) { int frameCount = Time.frameCount; if (frameCount == _lastPumpFrame) { return; } _lastPumpFrame = frameCount; if (fromHarmony && !_announcedPump) { _announcedPump = true; Log.LogMessage((object)("Pump driving from " + TickPatches.ChosenTarget + "; BaseUnityPlugin.Update had fired " + $"{UnityUpdates} time(s) by this point.")); } try { VariantManager.Tick(); RuntimeIconsBridge.Tick(); } catch (Exception arg) { Log.LogWarning((object)$"Pump tick failed: {arg}"); } } private void Update() { UnityUpdates++; Pump(fromHarmony: false); } } public class VariantData { public short MeshIndex = -1; public short MaterialIndex = -1; public bool IsEmpty => MeshIndex < 0 && MaterialIndex < 0; public int Signature() { return ((MeshIndex + 1) * 397) ^ ((MaterialIndex + 1) * 31); } } internal static class VariantRegistry { internal static readonly Dictionary<ulong, VariantData> Table = new Dictionary<ulong, VariantData>(); internal static bool TryGet(ulong networkObjectId, out VariantData data) { return Table.TryGetValue(networkObjectId, out data); } internal static void Set(ulong networkObjectId, VariantData data) { Table[networkObjectId] = data; } internal static void Clear() { Table.Clear(); } } internal static class VariantIdentity { private const char ZeroWidthSpace = '\u200b'; private static readonly Dictionary<Item, Dictionary<int, Item>> Clones = new Dictionary<Item, Dictionary<int, Item>>(); private static readonly Dictionary<Item, Item> BaseOf = new Dictionary<Item, Item>(); private static readonly Dictionary<Item, int> SuffixCounter = new Dictionary<Item, int>(); private static readonly HashSet<GrabbableObject> ActiveSwapped = new HashSet<GrabbableObject>(); private static readonly List<GrabbableObject> DeadScratch = new List<GrabbableObject>(16); private static readonly List<(GrabbableObject item, Item clone)> SwappedDuringSave = new List<(GrabbableObject, Item)>(); internal static Item BaseFor(Item item) { Item value; return ((Object)(object)item != (Object)null && BaseOf.TryGetValue(item, out value)) ? value : item; } internal static Item GetOrCreate(Item baseItem, int signature, out bool created) { created = false; if (!Clones.TryGetValue(baseItem, out var value)) { value = (Clones[baseItem] = new Dictionary<int, Item>()); } if (value.TryGetValue(signature, out var value2) && (Object)(object)value2 != (Object)null) { return value2; } value2 = Object.Instantiate<Item>(baseItem); Object.DontDestroyOnLoad((Object)(object)value2); ((Object)value2).hideFlags = (HideFlags)61; if (!SuffixCounter.TryGetValue(baseItem, out var value3)) { value3 = 0; } value3 = (SuffixCounter[baseItem] = value3 + 1); ((Object)value2).name = ((Object)baseItem).name + "_LVS" + value3; value2.itemName = baseItem.itemName + new string('\u200b', value3); value2.itemIcon = baseItem.itemIcon; value[signature] = value2; BaseOf[value2] = baseItem; created = true; return value2; } internal static void NoteSwapped(GrabbableObject item) { ActiveSwapped.Add(item); } internal static void OnSessionReset() { ActiveSwapped.Clear(); SwappedDuringSave.Clear(); } internal static void SwapBackForSave() { SwappedDuringSave.Clear(); if (ActiveSwapped.Count == 0) { return; } DeadScratch.Clear(); foreach (GrabbableObject item in ActiveSwapped) { Item value; if ((Object)(object)item == (Object)null) { DeadScratch.Add(item); } else if ((Object)(object)item.itemProperties != (Object)null && BaseOf.TryGetValue(item.itemProperties, out value)) { SwappedDuringSave.Add((item, item.itemProperties)); item.itemProperties = value; } } foreach (GrabbableObject item2 in DeadScratch) { ActiveSwapped.Remove(item2); } } internal static void ReswapAfterSave() { foreach (var (val, val2) in SwappedDuringSave) { if ((Object)(object)val != (Object)null && (Object)(object)val2 != (Object)null) { val.itemProperties = val2; } } SwappedDuringSave.Clear(); } } internal static class VariantManager { private struct PendingItem { public GrabbableObject Item; public float Deadline; } private static readonly List<PendingItem> Pending = new List<PendingItem>(64); private static readonly HashSet<ulong> Outbox = new HashSet<ulong>(); private static bool _requestWanted; private static float _nextRequestAllowed; private static int _applied; private static int _unresolved; private static int _eligibleSeen; private static int _passTotal; private static int _passEligible; private static float _reportDue; private static readonly List<ulong> FlushScratch = new List<ulong>(128); internal static bool InSession => (Object)(object)NetworkManager.Singleton != (Object)null && NetworkManager.Singleton.IsListening; internal static bool IsServer { get { NetworkManager singleton = NetworkManager.Singleton; return (Object)(object)singleton != (Object)null && singleton.IsListening && singleton.IsServer; } } internal static bool IsEligible(GrabbableObject item) { if ((Object)(object)item == (Object)null || (Object)(object)item.itemProperties == (Object)null) { return false; } Item itemProperties = item.itemProperties; if (itemProperties.meshVariants != null && itemProperties.meshVariants.Length != 0) { return true; } if (itemProperties.materialVariants != null && itemProperties.materialVariants.Length != 0) { return true; } return false; } private static bool TryGetSpawnedNetworkObject(GrabbableObject item, out NetworkObject netObj) { if (((Component)item).TryGetComponent<NetworkObject>(ref netObj) && netObj.IsSpawned) { return true; } netObj = null; return false; } internal static void OnItemStarted(GrabbableObject item) { if (InSession && IsEligible(item)) { _eligibleSeen++; _reportDue = Time.unscaledTime + 3f; if (!TryProcess(item)) { Pending.Add(new PendingItem { Item = item, Deadline = Time.unscaledTime + 10f }); } } } private static bool TryProcess(GrabbableObject item) { if (!TryGetSpawnedNetworkObject(item, out var netObj)) { return false; } if (IsServer) { EnsureAssignedServer(item, netObj, broadcast: true); return true; } return ApplyOrRequest(item, netObj); } internal static void Tick() { if (Pending.Count > 0) { PumpPending(); } FlushReport(); if (_requestWanted && Time.unscaledTime >= _nextRequestAllowed) { SendRequestNow(); } if (Outbox.Count > 0) { FlushOutbox(); } } private static void PumpPending() { for (int num = Pending.Count - 1; num >= 0; num--) { PendingItem pendingItem = Pending[num]; bool flag = (Object)(object)pendingItem.Item == (Object)null; bool flag2 = !flag && TryProcess(pendingItem.Item); bool flag3 = !flag && !flag2 && Time.unscaledTime > pendingItem.Deadline; if (flag || flag2 || flag3) { if (flag3) { _unresolved++; _reportDue = Time.unscaledTime + 2f; } int index = Pending.Count - 1; Pending[num] = Pending[index]; Pending.RemoveAt(index); } } } internal static void NoteScrapPass(int total, int eligible) { _passTotal += total; _passEligible += eligible; _reportDue = Time.unscaledTime + 3f; } private static void FlushReport() { if (_reportDue <= 0f || Time.unscaledTime < _reportDue) { return; } _reportDue = 0f; if (_passEligible == 0 && _applied == 0 && _unresolved == 0) { if (_passTotal > 0) { Plugin.Log.LogMessage((object)($"{_passTotal} scrap item(s) synced, none of them use vanilla's variant arrays - " + "nothing for this mod to sync here.")); } Reset(); return; } if (_unresolved > 0) { Plugin.Log.LogWarning((object)($"{_applied} item(s) took the host's variant, {_unresolved} never received one and keep " + "whatever this client rolled locally - those will not match the host. The host either has no assignment for them or the table did not arrive in time.")); } else if (_applied > 0) { Plugin.Log.LogMessage((object)($"{_applied} of {((_passEligible > _eligibleSeen) ? _passEligible : _eligibleSeen)} " + "eligible item(s) took the host's variant; none outstanding.")); } Reset(); } private static void Reset() { _applied = (_unresolved = (_eligibleSeen = 0)); _passTotal = (_passEligible = 0); } internal static void EnsureAssignedServer(GrabbableObject item, NetworkObject netObj, bool broadcast) { ulong networkObjectId = netObj.NetworkObjectId; if (!VariantRegistry.TryGet(networkObjectId, out var data)) { data = Roll(item); if (data.IsEmpty) { return; } VariantRegistry.Set(networkObjectId, data); } Apply(item, data); if (broadcast) { Outbox.Add(networkObjectId); } } private static bool ApplyOrRequest(GrabbableObject item, NetworkObject netObj) { if (VariantRegistry.TryGet(netObj.NetworkObjectId, out var data)) { Apply(item, data); _applied++; _reportDue = Time.unscaledTime + 2f; return true; } _requestWanted = true; return false; } private static void SendRequestNow() { _requestWanted = false; if (InSession && !IsServer) { _nextRequestAllowed = Time.unscaledTime + 2f; Messaging.SendTableRequest(); } } internal static void OnAssignmentReceived(ulong networkObjectId, VariantData data) { VariantRegistry.Set(networkObjectId, data); NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null && singleton.SpawnManager != null && singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var value) && (Object)(object)value != (Object)null) { GrabbableObject component = ((Component)value).GetComponent<GrabbableObject>(); if ((Object)(object)component != (Object)null) { Apply(component, data); } } } internal static void ReapplyIfKnown(GrabbableObject item) { if (!((Object)(object)item == (Object)null) && TryGetSpawnedNetworkObject(item, out var netObj)) { if (VariantRegistry.TryGet(netObj.NetworkObjectId, out var data)) { Apply(item, data); return; } _requestWanted = true; Pending.Add(new PendingItem { Item = item, Deadline = Time.unscaledTime + 10f }); } } private static VariantData Roll(GrabbableObject item) { Item itemProperties = item.itemProperties; VariantData variantData = new VariantData(); if (itemProperties.meshVariants != null && itemProperties.meshVariants.Length != 0) { variantData.MeshIndex = (short)Random.Range(0, itemProperties.meshVariants.Length); } if (itemProperties.materialVariants != null && itemProperties.materialVariants.Length != 0) { variantData.MaterialIndex = (short)Random.Range(0, itemProperties.materialVariants.Length); } return variantData; } internal static void Apply(GrabbableObject item, VariantData data) { if ((Object)(object)item == (Object)null || data == null) { return; } Item itemProperties = item.itemProperties; try { if ((Object)(object)itemProperties != (Object)null && data.MeshIndex >= 0 && itemProperties.meshVariants != null && data.MeshIndex < itemProperties.meshVariants.Length && (Object)(object)itemProperties.meshVariants[data.MeshIndex] != (Object)null) { MeshFilter componentInChildren = default(MeshFilter); if (!((Component)item).TryGetComponent<MeshFilter>(ref componentInChildren)) { componentInChildren = ((Component)item).gameObject.GetComponentInChildren<MeshFilter>(); } if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.mesh = itemProperties.meshVariants[data.MeshIndex]; } } if ((Object)(object)itemProperties != (Object)null && data.MaterialIndex >= 0 && itemProperties.materialVariants != null && data.MaterialIndex < itemProperties.materialVariants.Length && (Object)(object)itemProperties.materialVariants[data.MaterialIndex] != (Object)null) { MeshRenderer componentInChildren2 = default(MeshRenderer); if (!((Component)item).TryGetComponent<MeshRenderer>(ref componentInChildren2)) { componentInChildren2 = ((Component)item).gameObject.GetComponentInChildren<MeshRenderer>(); } if ((Object)(object)componentInChildren2 != (Object)null) { ((Renderer)componentInChildren2).sharedMaterial = itemProperties.materialVariants[data.MaterialIndex]; } } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed applying mesh/material variant to '{((Object)((Component)item).gameObject).name}': {arg}"); } if (!((Object)(object)item.itemProperties != (Object)null)) { return; } try { Item baseItem = VariantIdentity.BaseFor(item.itemProperties); bool created; Item orCreate = VariantIdentity.GetOrCreate(baseItem, data.Signature(), out created); if (item.itemProperties != orCreate) { item.itemProperties = orCreate; } VariantIdentity.NoteSwapped(item); RuntimeIconsBridge.OnCloneAssigned(item, orCreate, baseItem, created); } catch (Exception arg2) { Plugin.Log.LogWarning((object)$"Identity swap failed for '{((Object)((Component)item).gameObject).name}': {arg2}"); } } internal static void QueueBroadcast(ulong networkObjectId) { Outbox.Add(networkObjectId); } private static void FlushOutbox() { if (!IsServer) { Outbox.Clear(); return; } try { FlushScratch.Clear(); foreach (ulong item in Outbox) { FlushScratch.Add(item); } Messaging.BroadcastAssignments(FlushScratch); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed broadcasting variant assignments: {arg}"); } Outbox.Clear(); } internal static void ResetSession() { VariantRegistry.Clear(); Outbox.Clear(); Pending.Clear(); VariantIdentity.OnSessionReset(); _requestWanted = false; _nextRequestAllowed = 0f; Reset(); _reportDue = 0f; } } } namespace ItemVariantSync.Patches { [HarmonyPatch(typeof(GrabbableObject))] internal static class GrabbableObjectPatches { [HarmonyPatch("Start")] [HarmonyPostfix] private static void Start_Postfix(GrabbableObject __instance) { try { VariantManager.OnItemStarted(__instance); } catch (Exception arg) { ManualLogSource log = Plugin.Log; object arg2; if (__instance == null) { arg2 = null; } else { GameObject gameObject = ((Component)__instance).gameObject; arg2 = ((gameObject != null) ? ((Object)gameObject).name : null); } log.LogWarning((object)$"GrabbableObject.Start postfix failed for '{arg2}': {arg}"); } } } [HarmonyPatch(typeof(HUDManager))] internal static class HudScrapDisplayPatches { internal struct DisplayCapture { public GrabbableObject Source; public int BoxIndex; public int PrevChildCount; } [HarmonyPatch("DisplayNewScrapFound")] [HarmonyPrefix] private static void Prefix(HUDManager __instance, List<GrabbableObject> ___itemsToBeDisplayed, int ___nextBoxIndex, out DisplayCapture __state) { __state = default(DisplayCapture); __state.BoxIndex = -1; try { if (___itemsToBeDisplayed != null && ___itemsToBeDisplayed.Count != 0 && __instance.ScrapItemBoxes != null && ___nextBoxIndex >= 0 && ___nextBoxIndex < __instance.ScrapItemBoxes.Length) { __state.Source = ___itemsToBeDisplayed[0]; __state.BoxIndex = ___nextBoxIndex; Transform itemObjectContainer = __instance.ScrapItemBoxes[___nextBoxIndex].itemObjectContainer; __state.PrevChildCount = (((Object)(object)itemObjectContainer != (Object)null) ? itemObjectContainer.childCount : (-1)); } } catch { __state.BoxIndex = -1; } } [HarmonyPatch("DisplayNewScrapFound")] [HarmonyPostfix] private static void Postfix(HUDManager __instance, DisplayCapture __state) { try { if ((Object)(object)__state.Source == (Object)null || __state.BoxIndex < 0) { return; } Transform itemObjectContainer = __instance.ScrapItemBoxes[__state.BoxIndex].itemObjectContainer; if ((Object)(object)itemObjectContainer == (Object)null || itemObjectContainer.childCount == 0 || (__state.PrevChildCount >= 0 && itemObjectContainer.childCount <= __state.PrevChildCount)) { return; } MeshFilter val = ResolveFilter(((Component)__state.Source).gameObject); if (!((Object)(object)val == (Object)null) && !((Object)(object)val.sharedMesh == (Object)null)) { MeshFilter val2 = ResolveFilter(((Component)itemObjectContainer.GetChild(itemObjectContainer.childCount - 1)).gameObject); if ((Object)(object)val2 != (Object)null && (Object)(object)val2.sharedMesh != (Object)(object)val.sharedMesh) { val2.mesh = val.sharedMesh; } } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Collected-scrap popup mesh fix failed: {arg}"); } } private static MeshFilter ResolveFilter(GameObject go) { MeshFilter result = default(MeshFilter); if (go.TryGetComponent<MeshFilter>(ref result)) { return result; } return go.GetComponentInChildren<MeshFilter>(); } } internal static class LifecyclePatches { private static bool _loggedCapableItems; [HarmonyPatch(typeof(StartOfRound), "Awake")] [HarmonyPostfix] private static void StartOfRound_Awake_Postfix() { try { bool inSession = VariantManager.InSession; bool isServer = VariantManager.IsServer; Plugin.Log.LogMessage((object)($"StartOfRound.Awake: InSession={inSession} IsServer={isServer} " + "(" + ((!inSession) ? "no session yet - variants cannot sync from here" : (isServer ? "host, assigns authoritatively" : "client, will pull the host's table")) + ")")); LogVariantCapableItemsOnce(); DawnLibBridge.TryInit(); VariantManager.ResetSession(); Messaging.RegisterHandlers(); if (inSession && !isServer) { Messaging.SendTableRequest(); } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"StartOfRound.Awake postfix failed: {arg}"); } } private static void LogVariantCapableItemsOnce() { if (_loggedCapableItems) { return; } _loggedCapableItems = true; List<Item> list = StartOfRound.Instance?.allItemsList?.itemsList; if (list == null) { Plugin.Log.LogWarning((object)"Item pool unavailable - cannot report which items are variant-capable."); return; } List<string> list2 = new List<string>(); foreach (Item item in list) { if (!((Object)(object)item == (Object)null)) { bool flag = item.meshVariants != null && item.meshVariants.Length != 0; bool flag2 = item.materialVariants != null && item.materialVariants.Length != 0; if (flag || flag2) { list2.Add(item.itemName + "(" + (flag ? "M" : "") + (flag2 ? "m" : "") + ")"); } } } if (list2.Count == 0) { Plugin.Log.LogWarning((object)($"None of the {list.Count} loaded items carry vanilla variant arrays - this mod has nothing " + "to act on in this modpack. M=meshVariants, m=materialVariants.")); } else { Plugin.Log.LogMessage((object)($"{list2.Count} of {list.Count} items are variant-capable (M=mesh, m=material): " + string.Join(", ", list2.ToArray()))); } } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect", new Type[] { })] [HarmonyPostfix] private static void Disconnect_Postfix() { VariantManager.ResetSession(); } } [HarmonyPatch(typeof(RoundManager))] internal static class RoundManagerPatches { private static int _lastPassFrame = -1; private static NetworkObjectReference[] _lastPassArray; [HarmonyPatch("SyncScrapValuesClientRpc")] [HarmonyPostfix] private static void SyncScrapValues_Postfix(NetworkObjectReference[] spawnedScrap) { if (spawnedScrap == null || !VariantManager.InSession || (Time.frameCount == _lastPassFrame && spawnedScrap == _lastPassArray)) { return; } _lastPassFrame = Time.frameCount; _lastPassArray = spawnedScrap; bool isServer = VariantManager.IsServer; int num = 0; NetworkObject val = default(NetworkObject); for (int i = 0; i < spawnedScrap.Length; i++) { try { if (!((NetworkObjectReference)(ref spawnedScrap[i])).TryGet(ref val, (NetworkManager)null) || (Object)(object)val == (Object)null) { continue; } GrabbableObject component = ((Component)val).GetComponent<GrabbableObject>(); if (!((Object)(object)component == (Object)null) && VariantManager.IsEligible(component)) { num++; if (isServer) { VariantManager.EnsureAssignedServer(component, val, broadcast: true); } else { VariantManager.ReapplyIfKnown(component); } } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"SyncScrapValues postfix failed for entry {i}: {arg}"); } } VariantManager.NoteScrapPass(spawnedScrap.Length, num); } } internal static class SaveLoadPatches { [HarmonyPatch(typeof(GameNetworkManager), "SaveItemsInShip")] [HarmonyPrefix] [HarmonyPriority(800)] private static void SaveItemsInShip_Prefix() { try { VariantIdentity.SwapBackForSave(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Identity swap-back before save failed: {arg}"); } } [HarmonyPatch(typeof(GameNetworkManager), "SaveItemsInShip")] [HarmonyFinalizer] private static void SaveItemsInShip_Finalizer() { try { VariantIdentity.ReswapAfterSave(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Identity re-swap after save failed: {arg}"); } } } internal static class TickPatches { private static readonly Type[] Candidates = new Type[3] { typeof(StartOfRound), typeof(HUDManager), typeof(RoundManager) }; internal static string ChosenTarget { get; private set; } = "none"; [HarmonyTargetMethods] private static IEnumerable<MethodBase> TargetMethods() { Type[] candidates = Candidates; foreach (Type type in candidates) { MethodInfo method = AccessTools.DeclaredMethod(type, "Update", (Type[])null, (Type[])null); if (!(method == null)) { ChosenTarget = type.Name + ".Update"; yield return method; yield break; } } Plugin.Log.LogError((object)"No declared Update found on StartOfRound, HUDManager or RoundManager - the per-frame pump has no driver, so variant sync will not complete. Game update?"); } [HarmonyPostfix] private static void Update_Postfix() { Plugin.Pump(fromHarmony: true); } } }