using 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(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() : (-1)); JToken obj3 = val["t"]; variantData.MaterialIndex = (short)((obj3 != null) ? obj3.ToObject() : (-1)); VariantData variantData2 = variantData; if (variantData2.IsEmpty) { return; } NetworkObject val2 = default(NetworkObject); if (!((Component)item).TryGetComponent(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 _resolveQueue; private static Func _queueGetter; private static readonly List Pending = new List(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 list = new List(); 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 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 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 ids) { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer) { return; } List list = null; foreach (ulong connectedClientsId in singleton.ConnectedClientsIds) { if (connectedClientsId != 0) { (list ?? (list = new List())).Add(connectedClientsId); } } if (list != null) { SendAssignments(ids, list); } } internal static void SendFullTable(ulong clientId) { List list = new List(VariantRegistry.Table.Keys); if (list.Count != 0) { SendAssignments(list, new List { clientId }); } } private static void SendAssignments(List ids, List 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> list = new List>(num); int num2 = 5; for (int j = 0; j < num; j++) { if (VariantRegistry.TryGet(ids[i + j], out var data)) { list.Add(new KeyValuePair(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(ref b, default(ForPrimitives)); int count = list.Count; ((FastBufferWriter)(ref val)).WriteValueSafe(ref count, default(ForPrimitives)); foreach (KeyValuePair item in list) { ulong key = item.Key; ((FastBufferWriter)(ref val)).WriteValueSafe(ref key, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(ref item.Value.MeshIndex, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(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(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(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(ref num, default(ForPrimitives)); ulong networkObjectId = default(ulong); for (int i = 0; i < num; i++) { ((FastBufferReader)(ref reader)).ReadValueSafe(ref networkObjectId, default(ForPrimitives)); VariantData variantData = new VariantData(); ((FastBufferReader)(ref reader)).ReadValueSafe(ref variantData.MeshIndex, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe(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(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 Table = new Dictionary(); 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> Clones = new Dictionary>(); private static readonly Dictionary BaseOf = new Dictionary(); private static readonly Dictionary SuffixCounter = new Dictionary(); private static readonly HashSet ActiveSwapped = new HashSet(); private static readonly List DeadScratch = new List(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()); } if (value.TryGetValue(signature, out var value2) && (Object)(object)value2 != (Object)null) { return value2; } value2 = Object.Instantiate(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 Pending = new List(64); private static readonly HashSet Outbox = new HashSet(); 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 FlushScratch = new List(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(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(); 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(ref componentInChildren)) { componentInChildren = ((Component)item).gameObject.GetComponentInChildren(); } 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(ref componentInChildren2)) { componentInChildren2 = ((Component)item).gameObject.GetComponentInChildren(); } 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 ___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(ref result)) { return result; } return go.GetComponentInChildren(); } } 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 list = StartOfRound.Instance?.allItemsList?.itemsList; if (list == null) { Plugin.Log.LogWarning((object)"Item pool unavailable - cannot report which items are variant-capable."); return; } List list2 = new List(); 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(); 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 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); } } }