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 EasyAutoHarvest v1.1.0
BepInEx/plugins/EasyAutoHarvest/EasyAutoHarvest.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using HarmonyLib; using JetBrains.Annotations; using Loon.EasyAutoHarvest.Core; using Microsoft.CodeAnalysis; using ServerSync; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("Loon")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Automatic harvesting through normal Valheim interactions.")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0+fc7739b52cc8255407e4188b5c6283e9e4a41e89")] [assembly: AssemblyProduct("EasyAutoHarvest")] [assembly: AssemblyTitle("EasyAutoHarvest")] [assembly: AssemblyVersion("1.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 Loon.EasyAutoHarvest { internal sealed class Discovery { private readonly Dictionary<int, Resource> byId = new Dictionary<int, Resource>(); private readonly List<Resource> loaded = new List<Resource>(); private readonly HashSet<int> seen = new HashSet<int>(); private readonly Dictionary<string, HashSet<string>> catalog = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal); private readonly List<Pickable> pickables = new List<Pickable>(); private readonly List<PickableItem> items = new List<PickableItem>(); private Collider[] overlaps = (Collider[])(object)new Collider[256]; private int fallbackCursor; public readonly List<Resource> Candidates = new List<Resource>(); public int LoadedCount => loaded.Count; public int PrefabCount => catalog.Count; public bool QuerySaturated { get; private set; } public void Clear() { byId.Clear(); loaded.Clear(); seen.Clear(); catalog.Clear(); Candidates.Clear(); pickables.Clear(); items.Clear(); Array.Clear(overlaps, 0, overlaps.Length); fallbackCursor = 0; } public void Add(MonoBehaviour component) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)component)) { return; } Scene scene = ((Component)component).gameObject.scene; if (!((Scene)(ref scene)).IsValid()) { return; } int instanceID = ((Object)component).GetInstanceID(); if (byId.TryGetValue(instanceID, out Resource value)) { if ((Object)(object)value.Component == (Object)(object)component) { return; } Remove(instanceID); } Resource resource = new Resource(component); byId[instanceID] = resource; resource.RegistryIndex = loaded.Count; loaded.Add(resource); (((Component)component).GetComponent<ResourceLifetime>() ?? ((Component)component).gameObject.AddComponent<ResourceLifetime>()).Watch(instanceID); Note(resource.Prefab, resource.Kind); } public void Remove(int id) { if (byId.TryGetValue(id, out Resource value)) { byId.Remove(id); int registryIndex = value.RegistryIndex; Resource resource = loaded[loaded.Count - 1]; loaded[registryIndex] = resource; resource.RegistryIndex = registryIndex; loaded.RemoveAt(loaded.Count - 1); if (fallbackCursor > registryIndex) { fallbackCursor = registryIndex; } } } private void Note(string prefab, string kind) { if (!catalog.TryGetValue(prefab, out HashSet<string> value)) { value = (catalog[prefab] = new HashSet<string>(StringComparer.Ordinal)); } value.Add(kind); } public void SeedCatalog() { if (!Object.op_Implicit((Object)(object)ZNetScene.instance)) { return; } foreach (string prefabName in ZNetScene.instance.GetPrefabNames()) { GameObject prefab = ZNetScene.instance.GetPrefab(prefabName); if (Object.op_Implicit((Object)(object)prefab)) { prefab.GetComponentsInChildren<Pickable>(true, pickables); prefab.GetComponentsInChildren<PickableItem>(true, items); if (pickables.Count != 0) { Note(((Object)prefab).name, "Pickable"); } if (items.Count != 0) { Note(((Object)prefab).name, "PickableItem"); } } } pickables.Clear(); items.Clear(); } public void Gather(Vector3 eye, float reach, float now, int mask) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_0216: Unknown result type (might be due to invalid IL or missing references) Candidates.Clear(); seen.Clear(); int num; while (true) { num = Physics.OverlapSphereNonAlloc(eye, reach, overlaps, mask, (QueryTriggerInteraction)2); if (num < overlaps.Length || overlaps.Length >= 4096) { break; } Array.Resize(ref overlaps, overlaps.Length * 2); } QuerySaturated = num == overlaps.Length; int num2 = Math.Min(256, loaded.Count); for (int i = 0; i < num2; i++) { if (loaded.Count == 0) { break; } if (fallbackCursor >= loaded.Count) { fallbackCursor = 0; } Resource resource = loaded[fallbackCursor]; if (!Object.op_Implicit((Object)(object)resource.Component)) { Remove(resource.Id); continue; } fallbackCursor++; AddCandidate(resource, eye, reach, now); if (Candidates.Count >= 8) { break; } } for (int j = 0; j < num; j++) { Collider val = overlaps[j]; overlaps[j] = null; if (!Object.op_Implicit((Object)(object)val)) { continue; } Pickable componentInParent = ((Component)val).GetComponentInParent<Pickable>(); PickableItem componentInParent2 = ((Component)val).GetComponentInParent<PickableItem>(); if (Object.op_Implicit((Object)(object)componentInParent)) { Resolve((MonoBehaviour)(object)componentInParent, eye, reach, now); } if (Object.op_Implicit((Object)(object)componentInParent2)) { Resolve((MonoBehaviour)(object)componentInParent2, eye, reach, now); } if (Object.op_Implicit((Object)(object)componentInParent) || Object.op_Implicit((Object)(object)componentInParent2)) { continue; } GameObject obj = (Object.op_Implicit((Object)(object)val.attachedRigidbody) ? ((Component)val.attachedRigidbody).gameObject : ((Component)val).gameObject); obj.GetComponentsInChildren<Pickable>(true, pickables); obj.GetComponentsInChildren<PickableItem>(true, items); foreach (Pickable pickable in pickables) { Resolve((MonoBehaviour)(object)pickable, eye, reach, now); } foreach (PickableItem item in items) { Resolve((MonoBehaviour)(object)item, eye, reach, now); } } pickables.Clear(); items.Clear(); } private void Resolve(MonoBehaviour component, Vector3 eye, float reach, float now) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) Add(component); if (byId.TryGetValue(((Object)component).GetInstanceID(), out Resource value)) { AddCandidate(value, eye, reach, now); } } private void AddCandidate(Resource resource, Vector3 eye, float reach, float now) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (!seen.Add(resource.Id) || resource.RetryAt > now) { return; } try { if (resource.Near(eye, reach, now, out var _)) { Candidates.Add(resource); } } catch (Exception error) { resource.RetryAt = now + 10f; Plugin.Instance?.ReportError("discovery:" + resource.Kind, "Could not inspect " + resource.Prefab + "; retrying after 10 seconds.", error); } } public IEnumerable<string> CatalogRows(HashSet<string> blacklist, HashSet<string> exclusions) { foreach (KeyValuePair<string, HashSet<string>> item in catalog.OrderBy<KeyValuePair<string, HashSet<string>>, string>((KeyValuePair<string, HashSet<string>> kv) => kv.Key, StringComparer.Ordinal)) { yield return Clean(item.Key) + "\t" + string.Join(",", item.Value.OrderBy<string, string>((string k) => k, StringComparer.Ordinal)) + "\t" + HarvestPolicy.PrefabStatusText(HarvestPolicy.ForPrefab(item.Key, blacklist, exclusions)); } foreach (string item2 in (from name in blacklist.Union<string>(exclusions, StringComparer.Ordinal) where !catalog.ContainsKey(name) select name).OrderBy<string, string>((string name) => name, StringComparer.Ordinal)) { yield return Clean(item2) + "\tunknown\t" + HarvestPolicy.PrefabStatusText(HarvestPolicy.ForPrefab(item2, blacklist, exclusions)) + " (not discovered)"; } } public void Export(string path, HashSet<string> blacklist, HashSet<string> exclusions) { Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllLines(path, new string[1] { "prefab\tcomponent\tpolicy" }.Concat(CatalogRows(blacklist, exclusions)), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } private static string Clean(string value) { return value.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); } } internal static class GameAccess { private static readonly MethodInfo TakeInput = RequiredMethod(typeof(Player), "TakeInput"); private static readonly MethodInfo Interact = RequiredMethod(typeof(Player), "Interact"); private static readonly FieldInfo AutoPickup = RequiredField(typeof(Player), "m_enableAutoPickup"); private static readonly FieldInfo ItemPicked = RequiredField(typeof(PickableItem), "m_picked"); private static readonly FieldInfo InteractionMask = RequiredField(typeof(Player), "m_interactMask"); private static readonly FieldInfo LastHoverInteractTime = RequiredField(typeof(Player), "m_lastHoverInteractTime"); private static readonly MethodInfo StackSize = RequiredMethod(typeof(PickableItem), "GetStackSize"); private static readonly object[] InteractionArguments = new object[3] { null, false, false }; public static bool AutoPickupEnabled => (bool)AutoPickup.GetValue(null); internal static MethodInfo RequiredMethod(Type type, string name) { return AccessTools.DeclaredMethod(type, name, (Type[])null, (Type[])null) ?? throw new MissingMethodException(type.FullName, name); } private static FieldInfo RequiredField(Type type, string name) { return AccessTools.DeclaredField(type, name) ?? throw new MissingFieldException(type.FullName, name); } public static void Validate() { _ = TakeInput; } public static bool Picked(PickableItem item) { return (bool)ItemPicked.GetValue(item); } public static int GetStackSize(PickableItem item) { return (int)StackSize.Invoke(item, null); } public static int GetInteractionMask(Player player) { return (int)InteractionMask.GetValue(player); } public static bool CanInteract(Player player) { //IL_0090: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)player) || !Object.op_Implicit((Object)(object)Game.instance) || !Object.op_Implicit((Object)(object)ZNetScene.instance) || !Object.op_Implicit((Object)(object)Hud.instance) || Time.timeScale <= 0f || ((Character)player).IsDead() || ((Character)player).IsTeleporting() || ((Character)player).InCutscene() || ((Character)player).InIntro() || ((Character)player).InAttack() || ((Character)player).InDodge() || ((Character)player).InPlaceMode() || player.GetDoodadController() != null || Hud.InRadial() || !ZNetScene.instance.IsAreaReady(((Component)player).transform.position)) { return false; } ZNetView component = ((Component)player).GetComponent<ZNetView>(); if (Object.op_Implicit((Object)(object)component) && component.IsValid() && component.IsOwner()) { return (bool)TakeInput.Invoke(player, null); } return false; } public static void Harvest(Player player, Resource resource) { GameObject gameObject = ((Component)resource.Component).gameObject; using (new AutomaticHarvestScope(player, gameObject)) { object obj = InteractionArguments[0]; float num = (float)LastHoverInteractTime.GetValue(player); InteractionArguments[0] = gameObject; try { Interact.Invoke(player, InteractionArguments); } finally { InteractionArguments[0] = obj; LastHoverInteractTime.SetValue(player, num); } } } } internal sealed class HarvestScanner { private readonly struct Pending { public readonly float Until; public readonly YieldEstimate Yield; public Pending(float until, YieldEstimate yield) { Until = until; Yield = yield; } } private readonly Discovery discovery; private readonly Plugin plugin; private readonly InventoryBudget budget = new InventoryBudget(); private readonly List<Pending> pending = new List<Pending>(); private readonly RaycastHit[] hits = (RaycastHit[])(object)new RaycastHit[128]; private float nextPatchCheck; private bool modifiedPickable; private bool modifiedItem; public long Attempts { get; private set; } public HarvestScanner(Plugin plugin, Discovery discovery) { this.plugin = plugin; this.discovery = discovery; } public void Clear() { pending.Clear(); Attempts = 0L; nextPatchCheck = 0f; } public void Scan(Player player, float now) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0245: Unknown result type (might be due to invalid IL or missing references) //IL_0262: Unknown result type (might be due to invalid IL or missing references) //IL_0275: Unknown result type (might be due to invalid IL or missing references) //IL_029a: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Unknown result type (might be due to invalid IL or missing references) Settings settings = plugin.Settings; if (!GameAccess.CanInteract(player) || (settings.RespectAutoPickup.Value && !GameAccess.AutoPickupEnabled)) { return; } float maxInteractDistance = player.m_maxInteractDistance; if (float.IsNaN(maxInteractDistance) || float.IsInfinity(maxInteractDistance) || maxInteractDistance <= 0f) { return; } int interactionMask = GameAccess.GetInteractionMask(player); Vector3 eyePoint = ((Character)player).GetEyePoint(); discovery.Gather(eyePoint, maxInteractDistance, now, interactionMask); if (discovery.QuerySaturated) { plugin.DebugReason("query", "Physics query saturated; rotating loaded-instance fallback remains active."); } RefreshPatchStatus(now); for (int num = pending.Count - 1; num >= 0; num--) { if (pending[num].Until <= now) { pending.RemoveAt(num); } } if (settings.SkipFull.Value) { Inventory inventory = ((Humanoid)player).GetInventory(); budget.Reset(inventory.GetEmptySlots(), player.GetMaxCarryWeight(), inventory.GetTotalWeight()); foreach (ItemData allItem in inventory.GetAllItems()) { budget.AddStackSpace(new StackKey(allItem.m_shared.m_name, allItem.m_quality, allItem.m_worldLevel), allItem.m_shared.m_maxStackSize - allItem.m_stack); } foreach (Pending item in pending) { budget.Reserve(item.Yield); } } int num2 = 0; foreach (Resource candidate in discovery.Candidates) { if (num2 >= 8) { break; } if (!Object.op_Implicit((Object)(object)candidate.Component) || candidate.RetryAt > now) { continue; } try { PrefabStatus prefabStatus = HarvestPolicy.ForPrefab(candidate.Prefab, settings.Blacklist, settings.Exclusions); if (prefabStatus != PrefabStatus.Allowed) { Reject(candidate, HarvestPolicy.PrefabStatusText(prefabStatus)); } else { if (!candidate.Ready() || !candidate.Near(eyePoint, maxInteractDistance, now, out var target)) { continue; } if (!PrivateArea.CheckAccess(((Component)candidate.Component).transform.position, 0f, false, false) || !PrivateArea.CheckAccess(target, 0f, false, false)) { Reject(candidate, "ward access denied"); continue; } if (!HarvestVisibility.Visible(player, candidate, eyePoint, target, interactionMask, hits)) { Reject(candidate, "obstructed"); continue; } if ((object)((Component)candidate.Component).gameObject.GetComponentInParent<Interactable>() != candidate.Component) { Reject(candidate, "another interaction owns this object"); continue; } YieldEstimate yield = (((candidate.Kind == "Pickable") ? modifiedPickable : modifiedItem) ? default(YieldEstimate) : candidate.Estimate()); if (settings.SkipFull.Value && (!budget.CanFit(yield) || (yield.Known && !candidate.InventoryAllows(((Humanoid)player).GetInventory(), yield.Count)))) { Reject(candidate, "inventory/carry budget"); continue; } if (!GameAccess.CanInteract(player)) { break; } candidate.RetryAt = now + 2f; num2++; long attempts = Attempts + 1; Attempts = attempts; GameAccess.Harvest(player, candidate); if (settings.SkipFull.Value) { budget.Reserve(yield); if (yield.Known) { pending.Add(new Pending(now + 2f, yield)); } } continue; } } catch (Exception error) { candidate.RetryAt = now + 10f; plugin.ReportError("adapter:" + candidate.Kind, "Could not harvest " + candidate.Prefab + "; retrying after 10 seconds. Manual interaction remains available.", error); } } } private void Reject(Resource resource, string reason) { plugin.DebugReason(reason, resource.Prefab + ": " + reason); } private void RefreshPatchStatus(float now) { if (!(now < nextPatchCheck)) { nextPatchCheck = now + 5f; bool flag = Patched(AccessTools.Method(typeof(Game), "ScaleDrops", new Type[2] { typeof(GameObject), typeof(int) }, (Type[])null)) || Patched(AccessTools.Method(typeof(Game), "ScaleDrops", new Type[2] { typeof(ItemData), typeof(int) }, (Type[])null)); modifiedPickable = flag || HasYieldPatches(typeof(Pickable)); modifiedItem = HasYieldPatches(typeof(PickableItem)); } } private static bool HasYieldPatches(Type type) { if (!Patched(AccessTools.Method(type, "Interact", (Type[])null, (Type[])null)) && !Patched(AccessTools.Method(type, "RPC_Pick", (Type[])null, (Type[])null)) && !Patched(AccessTools.Method(type, "Drop", (Type[])null, (Type[])null))) { if (type == typeof(PickableItem)) { return Patched(AccessTools.Method(type, "GetStackSize", (Type[])null, (Type[])null)); } return false; } return true; } private static bool Patched(MethodInfo method) { Patches patchInfo = Harmony.GetPatchInfo((MethodBase)method); if (patchInfo == null) { return false; } return patchInfo.Owners.Any((string owner) => owner != "com.loon.easyautoharvest"); } } internal static class HarvestVisibility { public static bool Visible(Player player, Resource resource, Vector3 eye, Vector3 target, int mask, RaycastHit[] hits) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) Vector3 val = target - eye; float magnitude = ((Vector3)(ref val)).magnitude; if (magnitude == 0f) { return true; } int num = Physics.RaycastNonAlloc(eye, val / magnitude, hits, magnitude + 0.01f, mask, (QueryTriggerInteraction)1); bool result = num < hits.Length; for (int i = 0; i < num; i++) { Collider collider = ((RaycastHit)(ref hits[i])).collider; if (Object.op_Implicit((Object)(object)collider) && !resource.Owns(collider) && !((Component)collider).transform.IsChildOf(((Component)player).transform)) { result = false; } hits[i] = default(RaycastHit); } return result; } } internal static class Hooks { [HarmonyPatch(typeof(Humanoid), "DoInteractAnimation", new Type[] { typeof(GameObject) })] internal static class AutomaticHarvestAnimation { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(Humanoid __instance, GameObject __0) { return !AutomaticHarvestScope.Matches(__instance, __0); } } [HarmonyPatch(typeof(Pickable), "Awake")] internal static class PickableAwake { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Pickable __instance) { Plugin.Instance?.ResourceLoaded((MonoBehaviour)(object)__instance); } } [HarmonyPatch(typeof(PickableItem), "Awake")] internal static class ItemAwake { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(PickableItem __instance) { Plugin.Instance?.ResourceLoaded((MonoBehaviour)(object)__instance); } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class WorldStart { [HarmonyPrefix] private static void Prefix() { Plugin.Instance?.ResetSession(); } } [HarmonyPatch(typeof(ZNet), "Shutdown")] internal static class WorldStop { [HarmonyPrefix] private static void Prefix() { Plugin.Instance?.ResetSession(); } } [HarmonyPatch(typeof(ZNetScene), "Awake")] internal static class ContentReady { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix() { Plugin.Instance?.ContentLoaded(); } } [HarmonyPatch] internal static class ProtectSettings { private static IEnumerable<MethodBase> TargetMethods() { yield return AccessTools.PropertySetter(typeof(ConfigEntry<bool>), "Value"); yield return AccessTools.PropertySetter(typeof(ConfigEntry<string>), "Value"); yield return AccessTools.PropertySetter(typeof(ConfigEntry<float>), "Value"); } [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ConfigEntryBase __instance) { return Plugin.Instance?.Settings?.CanWrite(__instance) ?? true; } } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] internal static class PreserveLocalSettings { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { Plugin? instance = Plugin.Instance; string serialized = default(string); if (instance == null || instance.Settings?.TryLocalSerialization(__instance, out serialized) != true) { return true; } __result = serialized; return false; } } [HarmonyPatch(typeof(ConfigFile), "Save")] internal static class ConfigSaved { [HarmonyPostfix] private static void Postfix(ConfigFile __instance) { if (Object.op_Implicit((Object)(object)Plugin.Instance) && __instance == ((BaseUnityPlugin)Plugin.Instance).Config) { Plugin.Instance.Settings?.RememberSave(); } } } public static readonly Type[] Types = new Type[9] { typeof(PickableAwake), typeof(ItemAwake), typeof(WorldStart), typeof(WorldStop), typeof(ContentReady), typeof(AutomaticHarvestAnimation), typeof(ProtectSettings), typeof(PreserveLocalSettings), typeof(ConfigSaved) }; } [HarmonyPatch] internal static class PickupRangeHooks { internal struct State { public bool Applied; public float Previous; } private static MethodBase TargetMethod() { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(Player), "AutoPickup", new Type[1] { typeof(float) }, (Type[])null); FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(Player), "m_autoPickupRange"); if (methodInfo == null || methodInfo.IsStatic || methodInfo.ReturnType != typeof(void) || fieldInfo == null || fieldInfo.IsStatic || !fieldInfo.IsPublic || fieldInfo.FieldType != typeof(float)) { throw new MissingMemberException("Expected Player.AutoPickup(float) and public float m_autoPickupRange."); } return methodInfo; } internal static void Install(Harmony harmony) { MethodBase methodBase = TargetMethod(); try { harmony.CreateClassProcessor(typeof(PickupRangeHooks)).Patch(); } catch { harmony.Unpatch(methodBase, (HarmonyPatchType)0, "com.loon.easyautoharvest"); throw; } } internal static PickupStatus GetStatus(Plugin plugin, Player? player) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Invalid comparison between Unknown and I4 ZNet instance = ZNet.instance; Settings settings = plugin.Settings; PickupStatus pickupStatus = PickupPolicy.Evaluate(((Behaviour)plugin).enabled && plugin.PickupHookReady, Object.op_Implicit((Object)(object)instance) && Object.op_Implicit((Object)(object)ZNetScene.instance), Object.op_Implicit((Object)(object)instance) && instance.IsDedicated(), Object.op_Implicit((Object)(object)instance) && instance.IsServer(), (int)ZNet.GetConnectionStatus() == 2, settings.Sync.InitialSyncDone && !settings.Sync.IsSourceOfTruth, settings.AllReceived, settings.ManagePickupRange.Value); if (pickupStatus != PickupStatus.Ready) { return pickupStatus; } if (!Object.op_Implicit((Object)(object)player) || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return PickupStatus.NoLocalPlayer; } ZNetView component = ((Component)player).GetComponent<ZNetView>(); if (!Object.op_Implicit((Object)(object)component) || !component.IsValid() || !component.IsOwner()) { return PickupStatus.NotOwner; } return PickupStatus.Ready; } [HarmonyPrefix] internal static void Prefix(Player __instance, out State __state) { __state = default(State); Plugin instance = Plugin.Instance; if ((Object)(object)instance == (Object)null) { return; } try { if (GetStatus(instance, __instance) == PickupStatus.Ready) { float autoPickupRange = PickupPolicy.Range(instance.Settings.PickupRange.Value); __state.Previous = __instance.m_autoPickupRange; __state.Applied = true; __instance.m_autoPickupRange = autoPickupRange; } } catch (Exception error) { instance.DisablePickupManagement(error); } } [HarmonyFinalizer] internal static void Finalizer(Player __instance, State __state) { if (__state.Applied) { __instance.m_autoPickupRange = __state.Previous; } } } [BepInPlugin("com.loon.easyautoharvest", "Easy Auto Harvest", "1.1.0")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "com.loon.easyautoharvest"; public const string ModName = "Easy Auto Harvest"; public const string ModVersion = "1.1.0"; private const string PickupCompanionGuid = "TastyChickenLegs.SuperConfigurablePickupRadius"; private bool companionWarningIssued; private readonly Discovery discovery = new Discovery(); private readonly Dictionary<string, float> logTimes = new Dictionary<string, float>(StringComparer.Ordinal); private HarvestScanner scanner; private Harmony harmony; private float nextScan; private float faultUntil; private float catalogAt = -1f; private PolicyStatus? lastStatus; private bool clientApiChecked; internal static Plugin? Instance { get; private set; } internal Settings Settings { get; private set; } internal bool PickupHookReady { get; private set; } private void Awake() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown Instance = this; try { Settings = new Settings(this, ((BaseUnityPlugin)this).Config); scanner = new HarvestScanner(this, discovery); harmony = new Harmony("com.loon.easyautoharvest"); Type[] types = Hooks.Types; foreach (Type type in types) { harmony.CreateClassProcessor(type).Patch(); } try { PickupRangeHooks.Install(harmony); PickupHookReady = true; } catch (Exception error) { DisablePickupManagement(error); } RegisterCommands(); LogInfo("Easy Auto Harvest 1.1.0 loaded; waiting for a world."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Initialization failed; automatic harvesting disabled. " + ex)); ((Behaviour)this).enabled = false; Settings?.Dispose(); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } } private void Update() { //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) try { Settings.Tick(); if (!companionWarningIssued && PickupCompanionConflict()) { companionWarningIssued = true; ((BaseUnityPlugin)this).Logger.LogWarning((object)"Pickup range management is on while SuperConfigurablePickupRadius is loaded. Use one range controller: turn ManagePickupRange off, or disable/remove the other mod and restart."); } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup < faultUntil) { return; } if (catalogAt >= 0f && realtimeSinceStartup >= catalogAt) { catalogAt = -1f; discovery.SeedCatalog(); LogInfo("Discovered " + discovery.PrefabCount + " supported prefab identifiers. Use eah catalog or eah export for policy status."); } if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return; } PolicyStatus policyStatus = Status(); if (lastStatus != policyStatus) { lastStatus = policyStatus; string text = StatusText(policyStatus); LogInfo(text); if (!ZNet.instance.IsDedicated() && Object.op_Implicit((Object)(object)Player.m_localPlayer)) { ((Character)Player.m_localPlayer).Message((MessageType)1, text, 0, (Sprite)null, false); } } if (ZNet.instance.IsDedicated()) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } if (!clientApiChecked) { GameAccess.Validate(); clientApiChecked = true; } KeyboardShortcut value = Settings.Toggle.Value; if ((int)((KeyboardShortcut)(ref value)).MainKey != 0 && GameAccess.CanInteract(localPlayer)) { value = Settings.Toggle.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { SetLocal(!Settings.LocalEnabled.Value); } } if (!(realtimeSinceStartup < nextScan)) { nextScan = realtimeSinceStartup + HarvestPolicy.Interval(Settings.ScanInterval.Value); if (Status() == PolicyStatus.Ready) { scanner.Scan(localPlayer, realtimeSinceStartup); } } } catch (Exception error) { faultUntil = (nextScan = Time.realtimeSinceStartup + 10f); ReportError("scan", "Automatic harvesting encountered an error; inspect compatibility and retry after 10 seconds.", error); } } internal void ResetSession() { discovery.Clear(); scanner?.Clear(); logTimes.Clear(); nextScan = 0f; faultUntil = 0f; catalogAt = -1f; lastStatus = null; } internal void ContentLoaded() { catalogAt = Time.realtimeSinceStartup + 1f; } internal void ResourceUnloaded(int id) { discovery.Remove(id); } internal void ResourceLoaded(MonoBehaviour component) { if (!((Behaviour)this).enabled || !Object.op_Implicit((Object)(object)ZNet.instance) || ZNet.instance.IsDedicated()) { return; } try { discovery.Add(component); } catch (Exception error) { ReportError("discovery", "Could not register a supported resource; manual interaction remains available.", error); } } private PolicyStatus Status() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Invalid comparison between Unknown and I4 ZNet instance = ZNet.instance; return HarvestPolicy.Evaluate(Object.op_Implicit((Object)(object)instance) && Object.op_Implicit((Object)(object)ZNetScene.instance), Object.op_Implicit((Object)(object)instance) && instance.IsDedicated(), Object.op_Implicit((Object)(object)instance) && instance.IsServer(), (int)ZNet.GetConnectionStatus() == 2, Settings.Sync.InitialSyncDone && !Settings.Sync.IsSourceOfTruth, Settings.AllReceived, Settings.Enabled.Value, Settings.LocalEnabled.Value); } private static string StatusText(PolicyStatus status) { return status switch { PolicyStatus.Ready => "Easy Auto Harvest: ready.", PolicyStatus.WaitingForServer => "Easy Auto Harvest: inactive until the server supplies policy. If the server lacks this mod or sync fails, manual harvesting remains available.", PolicyStatus.DisabledByPolicy => "Easy Auto Harvest: disabled by policy.", PolicyStatus.PausedLocally => "Easy Auto Harvest: paused locally.", PolicyStatus.DedicatedServer => "Easy Auto Harvest: server policy active; no player scanning.", _ => "Easy Auto Harvest: no world loaded.", }; } internal void DisablePickupManagement(Exception error) { PickupHookReady = false; ReportError("pickup-hook", "Pickup range management unavailable; ordinary pickup and automatic harvesting remain available.", error); } private bool PickupCompanionConflict() { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Invalid comparison between Unknown and I4 ZNet instance = ZNet.instance; if (((Behaviour)this).enabled && PickupHookReady && Object.op_Implicit((Object)(object)instance) && Object.op_Implicit((Object)(object)ZNetScene.instance) && Settings.ManagePickupRange.Value && (instance.IsServer() || ((int)ZNet.GetConnectionStatus() == 2 && Settings.Sync.InitialSyncDone && !Settings.Sync.IsSourceOfTruth && Settings.AllReceived))) { return Chainloader.PluginInfos.ContainsKey("TastyChickenLegs.SuperConfigurablePickupRadius"); } return false; } private void SetLocal(bool value) { Settings.LocalEnabled.Value = value; string text = "Easy Auto Harvest: local automation " + (value ? "enabled." : "paused."); LogInfo(text); if (Object.op_Implicit((Object)(object)Player.m_localPlayer)) { ((Character)Player.m_localPlayer).Message((MessageType)1, text, 0, (Sprite)null, false); } } private void RegisterCommands() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_0020: Unknown result type (might be due to invalid IL or missing references) new ConsoleCommand("eah", "Easy Auto Harvest: status | pause | resume | toggle | catalog | export | reload", (ConsoleEvent)delegate(ConsoleEventArgs args) { try { switch ((args.Length > 1) ? args[1].ToLowerInvariant() : "status") { case "pause": SetLocal(value: false); break; case "resume": SetLocal(value: true); break; case "toggle": SetLocal(!Settings.LocalEnabled.Value); break; case "reload": Settings.Reload(force: true); break; case "catalog": discovery.SeedCatalog(); args.Context.AddString("prefab\tcomponent\tpolicy"); { foreach (string item in discovery.CatalogRows(Settings.Blacklist, Settings.Exclusions)) { args.Context.AddString(item); } break; } case "export": { discovery.SeedCatalog(); string text = Path.Combine(Paths.ConfigPath, "EasyAutoHarvest", "discovery.catalog.tsv"); discovery.Export(text, Settings.Blacklist, Settings.Exclusions); args.Context.AddString("Exported catalog: " + text); break; } case "status": args.Context.AddString(StatusText(Status())); args.Context.AddString("Pickup range: ManagePickupRange=" + Settings.ManagePickupRange.Value + "; configured radius=" + PickupPolicy.Range(Settings.PickupRange.Value) + " m; " + PickupPolicy.StatusText(PickupRangeHooks.GetStatus(this, Player.m_localPlayer)) + "."); if (PickupCompanionConflict()) { args.Context.AddString("SuperConfigurablePickupRadius is also loaded; use one active pickup range controller."); } args.Context.AddString("Known prefabs: " + discovery.PrefabCount + "; loaded resources: " + discovery.LoadedCount + "; interaction attempts: " + scanner.Attempts); args.Context.AddString("Policy locked for this player: " + Settings.Sync.IsLocked + "; administrator: " + Settings.Sync.IsAdmin); break; default: args.Context.AddString("Usage: eah status | pause | resume | toggle | catalog | export | reload"); break; } } catch (Exception error) { args.Context.AddString("Easy Auto Harvest command failed; see BepInEx log."); ReportError("command", "Diagnostic command failed.", error); } }, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } internal void LogInfo(string text) { ((BaseUnityPlugin)this).Logger.LogInfo((object)text); } internal void DebugReason(string key, string text) { if (Settings.Debug.Value && AllowLog("debug:" + key, 10f)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)text); } } internal void ReportError(string key, string text, Exception error) { if (AllowLog(key, 30f)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)(text + " " + error.GetBaseException())); } } private bool AllowLog(string key, float interval) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (logTimes.TryGetValue(key, out var value) && realtimeSinceStartup < value) { return false; } logTimes[key] = realtimeSinceStartup + interval; return true; } private void OnDestroy() { Settings?.Dispose(); Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } ResetSession(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } internal sealed class Resource { public readonly MonoBehaviour Component; public readonly int Id; public readonly string Prefab; public readonly string Kind; public readonly ZNetView? View; private readonly Pickable? pickable; private readonly PickableItem? item; private readonly List<Collider> colliders = new List<Collider>(); private float refreshAt; public float RetryAt; public int RegistryIndex; public Resource(MonoBehaviour component) { Component = component; Id = ((Object)component).GetInstanceID(); pickable = (Pickable?)(object)((component is Pickable) ? component : null); item = (PickableItem?)(object)((component is PickableItem) ? component : null); Kind = (Object.op_Implicit((Object)(object)pickable) ? "Pickable" : "PickableItem"); View = ((Component)component).GetComponent<ZNetView>(); GameObject val = ((Object.op_Implicit((Object)(object)View) && View.IsValid() && Object.op_Implicit((Object)(object)ZNetScene.instance)) ? ZNetScene.instance.GetPrefab(View.GetZDO().GetPrefab()) : null); Prefab = (Object.op_Implicit((Object)(object)val) ? ((Object)val).name : PrefabNames.Canonical(((Object)((Component)component).gameObject).name)); ((Component)component).GetComponentsInChildren<Collider>(true, colliders); } public bool Ready() { if (!Object.op_Implicit((Object)(object)Component) || !((Behaviour)Component).isActiveAndEnabled || !Object.op_Implicit((Object)(object)View) || !View.IsValid()) { return false; } if (Object.op_Implicit((Object)(object)pickable)) { if (pickable.GetPicked() || pickable.GetEnabled != 1 || !Object.op_Implicit((Object)(object)pickable.m_itemPrefab)) { return false; } if (pickable.m_tarPreventsPicking) { Floating component = ((Component)Component).GetComponent<Floating>(); if (Object.op_Implicit((Object)(object)component) && component.IsInTar()) { return false; } } return true; } if (Object.op_Implicit((Object)(object)item) && Object.op_Implicit((Object)(object)item.m_itemPrefab)) { return !GameAccess.Picked(item); } return false; } public YieldEstimate Estimate() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) if (((object)Component).GetType() != typeof(Pickable) && ((object)Component).GetType() != typeof(PickableItem)) { return default(YieldEstimate); } ItemDrop val; int count; if (Object.op_Implicit((Object)(object)pickable)) { if (!pickable.m_extraDrops.IsEmpty() || (int)pickable.m_pickRaiseSkill != 0) { return default(YieldEstimate); } val = pickable.m_itemPrefab.GetComponent<ItemDrop>(); if (!Object.op_Implicit((Object)(object)val)) { return default(YieldEstimate); } count = (pickable.m_dontScale ? pickable.m_amount : Math.Max(pickable.m_minAmountScaled, Game.instance.ScaleDrops(pickable.m_itemPrefab, pickable.m_amount))); } else { val = item.m_itemPrefab; if (!Object.op_Implicit((Object)(object)val)) { return default(YieldEstimate); } count = GameAccess.GetStackSize(item); } ItemData val2 = ExpectedDrop(val); return new YieldEstimate(new StackKey(val2.m_shared.m_name, val2.m_quality, val2.m_worldLevel), count, val2.m_shared.m_maxStackSize, val2.GetWeight(1)); } public bool InventoryAllows(Inventory inventory, int count) { ItemDrop val = (Object.op_Implicit((Object)(object)pickable) ? pickable.m_itemPrefab.GetComponent<ItemDrop>() : item.m_itemPrefab); if (Object.op_Implicit((Object)(object)val)) { return inventory.CanAddItem(ExpectedDrop(val), count); } return false; } private static ItemData ExpectedDrop(ItemDrop drop) { ItemData obj = drop.m_itemData.Clone(); obj.m_worldLevel = (byte)Game.m_worldLevel; return obj; } public bool Near(Vector3 eye, float reach, float now, out Vector3 target) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) target = ((Component)Component).transform.position; Vector3 val = target - eye; float num = ((Vector3)(ref val)).sqrMagnitude; if (now >= refreshAt && num <= (reach + 10f) * (reach + 10f)) { ((Component)Component).GetComponentsInChildren<Collider>(true, colliders); refreshAt = now + 5f; } bool flag = false; foreach (Collider collider in colliders) { if (!Object.op_Implicit((Object)(object)collider) || !collider.enabled || !((Component)collider).gameObject.activeInHierarchy || collider.isTrigger) { continue; } if (!(collider is BoxCollider) && !(collider is SphereCollider) && !(collider is CapsuleCollider)) { MeshCollider val2 = (MeshCollider)(object)((collider is MeshCollider) ? collider : null); if (val2 == null || !val2.convex) { continue; } } Vector3 val3 = collider.ClosestPoint(eye); val = val3 - eye; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; if (!flag || sqrMagnitude < num) { num = sqrMagnitude; target = val3; } flag = true; } if (reach > 0f) { return num < reach * reach; } return false; } public bool Owns(Collider collider) { if (!((Object)(object)((Component)collider).transform == (Object)(object)((Component)Component).transform)) { return ((Component)collider).transform.IsChildOf(((Component)Component).transform); } return true; } } internal sealed class ResourceLifetime : MonoBehaviour { private readonly List<int> ids = new List<int>(2); internal void Watch(int id) { if (!ids.Contains(id)) { ids.Add(id); } } private void OnDestroy() { foreach (int id in ids) { Plugin.Instance?.ResourceUnloaded(id); } } } internal sealed class Settings : IDisposable { private sealed class PickupRangeValueRange : AcceptableValueRange<float> { public PickupRangeValueRange() : base(2f, 10f) { } public override object Clamp(object value) { return PickupPolicy.Range((float)value); } } private readonly ConfigFile config; private readonly Plugin plugin; private readonly Dictionary<ConfigEntryBase, OwnConfigEntryBase> shared = new Dictionary<ConfigEntryBase, OwnConfigEntryBase>(); private readonly FileSystemWatcher watcher; private long changedAt; private string lastContent = ""; public readonly ConfigSync Sync; public readonly ConfigEntry<bool> Lock; public readonly ConfigEntry<bool> Enabled; public readonly ConfigEntry<string> Blacklisted; public readonly ConfigEntry<string> Excluded; public readonly ConfigEntry<bool> RespectAutoPickup; public readonly ConfigEntry<bool> SkipFull; public readonly ConfigEntry<float> ScanInterval; public readonly ConfigEntry<bool> ManagePickupRange; public readonly ConfigEntry<float> PickupRange; public readonly ConfigEntry<bool> LocalEnabled; public readonly ConfigEntry<KeyboardShortcut> Toggle; public readonly ConfigEntry<bool> Debug; public HashSet<string> Exclusions { get; private set; } = new HashSet<string>(StringComparer.Ordinal); public HashSet<string> Blacklist { get; private set; } = new HashSet<string>(StringComparer.Ordinal); public bool AllReceived { get { foreach (OwnConfigEntryBase value in shared.Values) { if (value.LocalBaseValue == null) { return false; } } return true; } } public bool IsRemote { get { if (Object.op_Implicit((Object)(object)ZNet.instance)) { return !ZNet.instance.IsServer(); } return false; } } public Settings(Plugin plugin, ConfigFile config) { //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_025a: Expected O, but got Unknown this.plugin = plugin; this.config = config; List<ConfigSeed> seeds = ConfigMigration.GetSeeds(File.Exists(config.ConfigFilePath) ? File.ReadAllText(config.ConfigFilePath) : ""); config.SaveOnConfigSet = false; Sync = new ConfigSync("com.loon.easyautoharvest") { DisplayName = "Easy Auto Harvest", CurrentVersion = "1.1.0", MinimumRequiredVersion = "1.1.0", ModRequired = false }; Lock = config.Bind<bool>("Server", "LockConfiguration", true, "Lock shared settings to server administrators. Unlocking permits edits to shared policy, not personal policy."); shared.Add((ConfigEntryBase)(object)Lock, Sync.AddLockingConfigEntry<bool>(Lock)); Enabled = Bind("Enabled", value: true, "Allow automatic harvesting. Local pause cannot override this setting."); Blacklisted = Bind("BlacklistedPrefabs", "", "Never automatically harvest these exact, case-sensitive prefab identifiers. Overrides local preferences. Separate names with commas, semicolons, or newlines. Unknown names are retained; manual harvesting remains available."); ScanInterval = Bind("ScanIntervalSeconds", 0.25f, "Seconds between scans (0.1 to 2).", (AcceptableValueBase?)(object)new AcceptableValueRange<float>(0.1f, 2f)); ManagePickupRange = Bind("ManagePickupRange", value: false, "Manage automatic dropped-item pickup range. Off means this mod makes no range writes, leaving the game or another mod in control. Independent of harvesting enable/pause."); PickupRange = Bind("PickupRange", 8f, "Automatic dropped-item pickup radius in meters (2 to 10), used only when ManagePickupRange is on. Does not extend harvesting or manual interaction reach. Uses normal pickup rules, including the game's pickup toggle. Nonfinite values use 8.", (AcceptableValueBase?)(object)new PickupRangeValueRange()); Excluded = config.Bind<string>("Local", "ExcludedPrefabs", "", "Additional exact, case-sensitive prefab identifiers to skip for this player only. Does not override the server blacklist. Separate names with commas, semicolons, or newlines. Unknown names are retained."); RespectAutoPickup = config.Bind<bool>("Local", "RespectAutoPickupToggle", true, "Pause your own harvesting while your normal auto-pickup toggle is off."); SkipFull = config.Bind<bool>("Local", "SkipWhenInventoryFull", true, "Estimate known yields including stacking and weight for this player. Unknown yields are skipped while clearly full or overweight."); LocalEnabled = config.Bind<bool>("Local", "AutoHarvestEnabled", true, "Pause only your own automatic harvesting."); Toggle = config.Bind<KeyboardShortcut>("Local", "ToggleShortcut", KeyboardShortcut.Empty, "Optional local pause shortcut. Unassigned by default."); Debug = config.Bind<bool>("Local", "DebugLogging", false, "Rate-limited rejection reasons and diagnostics."); foreach (ConfigSeed item in seeds) { config[new ConfigDefinition(item.Section, item.Key)].SetSerializedValue(item.SerializedValue); } string[] array = new string[3] { "ExcludedPrefabs", "RespectAutoPickupToggle", "SkipWhenInventoryFull" }; foreach (string text in array) { config.Remove(((ConfigEntryBase)config.Bind<string>("Server", text, "", (ConfigDescription)null)).Definition); } if (seeds.Count != 0) { plugin.LogInfo("Migrated legacy harvest preferences. Existing new keys were preserved; old exclusions seed both the server blacklist and local exclusions."); } Blacklist = PrefabNames.ParseExclusions(Blacklisted.Value); Blacklisted.SettingChanged += delegate { Blacklist = PrefabNames.ParseExclusions(Blacklisted.Value); }; Exclusions = PrefabNames.ParseExclusions(Excluded.Value); Excluded.SettingChanged += delegate { Exclusions = PrefabNames.ParseExclusions(Excluded.Value); }; config.SaveOnConfigSet = true; config.Save(); RememberSave(); watcher = new FileSystemWatcher(Path.GetDirectoryName(config.ConfigFilePath), Path.GetFileName(config.ConfigFilePath)) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite) }; watcher.Changed += Changed; watcher.Created += Changed; watcher.Renamed += delegate { QueueReload(); }; watcher.Error += delegate { QueueReload(); }; watcher.EnableRaisingEvents = true; } private ConfigEntry<T> Bind<T>(string key, T value, string description, AcceptableValueBase? range = null) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown ConfigEntry<T> val = config.Bind<T>("Server", key, value, new ConfigDescription(description, range, Array.Empty<object>())); shared.Add((ConfigEntryBase)(object)val, Sync.AddConfigEntry<T>(val)); return val; } public bool CanWrite(ConfigEntryBase entry) { if (shared.ContainsKey(entry)) { return HarvestPolicy.CanEdit(IsRemote, ConfigSync.ProcessingServerUpdate, Sync.InitialSyncDone && AllReceived, Sync.IsAdmin, Sync.IsLocked, (object)entry == Lock); } return true; } public bool TryLocalSerialization(ConfigEntryBase entry, out string serialized) { serialized = ""; if (!Sync.IsSourceOfTruth && shared.TryGetValue(entry, out OwnConfigEntryBase value) && value.LocalBaseValue != null) { serialized = TomlTypeConverter.ConvertToString(value.LocalBaseValue, entry.SettingType); return true; } return false; } private void Changed(object sender, FileSystemEventArgs args) { QueueReload(); } private void QueueReload() { Interlocked.Exchange(ref changedAt, DateTime.UtcNow.Ticks); } public void Tick() { long num = Interlocked.Read(in changedAt); if (num != 0L && DateTime.UtcNow.Ticks - num >= 5000000 && !ConfigSync.ProcessingServerUpdate && Interlocked.CompareExchange(ref changedAt, 0L, num) == num) { Reload(force: false); } } public void Reload(bool force) { try { if (!File.Exists(config.ConfigFilePath)) { return; } string text = Fingerprint(); if (force || !(text == lastContent)) { bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; try { config.Reload(); } finally { config.SaveOnConfigSet = saveOnConfigSet; } lastContent = text; plugin.LogInfo("Configuration reloaded. Remote shared policy remains controlled by ServerSync."); } } catch (Exception error) { plugin.ReportError("reload", "Could not reload configuration; check the file and use eah reload to retry.", error); } } public void RememberSave() { try { lastContent = Fingerprint(); } catch (IOException) { QueueReload(); } } private string Fingerprint() { using SHA256 sHA = SHA256.Create(); return Convert.ToBase64String(sHA.ComputeHash(File.ReadAllBytes(config.ConfigFilePath))); } public void Dispose() { watcher.Dispose(); } } } namespace Loon.EasyAutoHarvest.Core { internal sealed class AutomaticHarvestScope : IDisposable { [ThreadStatic] private static AutomaticHarvestScope? current; private readonly AutomaticHarvestScope? previous; private readonly object actor; private readonly object target; public AutomaticHarvestScope(object actor, object target) { this.actor = actor; this.target = target; previous = current; current = this; } public static bool Matches(object actor, object target) { if (current != null && current.actor == actor) { return current.target == target; } return false; } public void Dispose() { current = previous; } } internal readonly struct ConfigSeed { public readonly string Section; public readonly string Key; public readonly string SerializedValue; public ConfigSeed(string section, string key, string value) { Section = section; Key = key; SerializedValue = value; } } internal static class ConfigMigration { public static List<ConfigSeed> GetSeeds(string text) { Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.Ordinal); using (StringReader stringReader = new StringReader(text)) { string text2 = ""; string text3; while ((text3 = stringReader.ReadLine()) != null) { text3 = text3.Trim(); if (text3.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text3.StartsWith("[", StringComparison.Ordinal) && text3.EndsWith("]", StringComparison.Ordinal)) { text2 = text3.Substring(1, text3.Length - 2); continue; } int num = text3.IndexOf('='); if (num >= 0) { values[text2 + "\n" + text3.Substring(0, num).Trim()] = text3.Substring(num + 1).Trim(); } } } List<ConfigSeed> seeds = new List<ConfigSeed>(); Seed("ExcludedPrefabs", "Server", "BlacklistedPrefabs"); Seed("ExcludedPrefabs", "Local", "ExcludedPrefabs"); Seed("RespectAutoPickupToggle", "Local", "RespectAutoPickupToggle"); Seed("SkipWhenInventoryFull", "Local", "SkipWhenInventoryFull"); return seeds; void Seed(string oldKey, string section, string key) { if (!values.ContainsKey(section + "\n" + key) && values.TryGetValue("Server\n" + oldKey, out string value) && value != null) { seeds.Add(new ConfigSeed(section, key, value)); } } } } internal static class PrefabNames { public static string Canonical(string name) { if (!name.EndsWith("(Clone)", StringComparison.Ordinal)) { return name; } return name.Substring(0, name.Length - 7); } public static HashSet<string> ParseExclusions(string value) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); string[] array = value.Split(new char[4] { ',', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { hashSet.Add(text); } } return hashSet; } } internal enum PolicyStatus { NoWorld, DedicatedServer, WaitingForServer, DisabledByPolicy, PausedLocally, Ready } internal enum PrefabStatus { Allowed, BlacklistedByServer, ExcludedLocally } internal static class HarvestPolicy { public static PrefabStatus ForPrefab(string prefab, ISet<string> blacklist, ISet<string> exclusions) { if (blacklist.Contains(prefab)) { return PrefabStatus.BlacklistedByServer; } if (!exclusions.Contains(prefab)) { return PrefabStatus.Allowed; } return PrefabStatus.ExcludedLocally; } public static string PrefabStatusText(PrefabStatus status) { return status switch { PrefabStatus.BlacklistedByServer => "server blacklisted", PrefabStatus.ExcludedLocally => "locally excluded", _ => "allowed", }; } public static PolicyStatus Evaluate(bool inWorld, bool dedicated, bool authority, bool connected, bool initialSync, bool receivedAllSettings, bool enabled, bool localEnabled) { if (!inWorld) { return PolicyStatus.NoWorld; } if (dedicated) { return PolicyStatus.DedicatedServer; } if (!authority && (!connected || !initialSync || !receivedAllSettings)) { return PolicyStatus.WaitingForServer; } if (!enabled) { return PolicyStatus.DisabledByPolicy; } if (!localEnabled) { return PolicyStatus.PausedLocally; } return PolicyStatus.Ready; } public static float Interval(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(0.1f, Math.Min(2f, value)); } return 0.25f; } public static bool CanEdit(bool remote, bool applyingServer, bool ready, bool admin, bool locked, bool lockingEntry) { if (!(!remote || applyingServer)) { if (ready) { if (!admin) { if (!locked) { return !lockingEntry; } return false; } return true; } return false; } return true; } } internal readonly struct StackKey : IEquatable<StackKey> { public readonly string Name; public readonly int Quality; public readonly int WorldLevel; public StackKey(string name, int quality, int worldLevel) { Name = name; Quality = quality; WorldLevel = worldLevel; } public bool Equals(StackKey other) { if (Name == other.Name && Quality == other.Quality) { return WorldLevel == other.WorldLevel; } return false; } public override bool Equals(object? obj) { if (obj is StackKey other) { return Equals(other); } return false; } public override int GetHashCode() { return (((Name.GetHashCode() * 397) ^ Quality) * 397) ^ WorldLevel; } } internal readonly struct YieldEstimate { public readonly StackKey Key; public readonly int Count; public readonly int MaxStack; public readonly float UnitWeight; public readonly bool Known; public YieldEstimate(StackKey key, int count, int maxStack, float unitWeight) { Key = key; Count = count; MaxStack = maxStack; UnitWeight = unitWeight; Known = true; } } internal sealed class InventoryBudget { private readonly Dictionary<StackKey, long> space = new Dictionary<StackKey, long>(); private int emptySlots; private double weightLeft; public void Reset(int slots, float carryLimit, float currentWeight) { space.Clear(); emptySlots = Math.Max(0, slots); weightLeft = (double)carryLimit - (double)currentWeight; } public void AddStackSpace(StackKey key, int available) { space.TryGetValue(key, out var value); space[key] = value + Math.Max(0, available); } public bool CanFit(YieldEstimate yield) { if (!yield.Known) { if (emptySlots > 0) { return weightLeft > 0.0; } return false; } if (yield.Count < 1 || yield.MaxStack < 1 || float.IsNaN(yield.UnitWeight) || yield.UnitWeight < 0f) { return false; } space.TryGetValue(yield.Key, out var value); if ((Math.Max(0L, yield.Count - value) + yield.MaxStack - 1) / yield.MaxStack <= emptySlots) { return (double)yield.UnitWeight * (double)yield.Count <= weightLeft; } return false; } public void Reserve(YieldEstimate yield) { if (!CanFit(yield)) { emptySlots = 0; weightLeft = -1.0; return; } if (!yield.Known) { emptySlots = 0; weightLeft = -1.0; return; } space.TryGetValue(yield.Key, out var value); long num = Math.Max(0L, yield.Count - value); long num2 = (num + Math.Max(1, yield.MaxStack) - 1) / Math.Max(1, yield.MaxStack); emptySlots = (int)Math.Max(0L, emptySlots - num2); space[yield.Key] = Math.Max(0L, value - yield.Count) + num2 * yield.MaxStack - num; weightLeft -= (double)yield.UnitWeight * (double)yield.Count; } } internal enum PickupStatus { Unavailable, NoWorld, DedicatedServer, WaitingForServer, ManagementOff, NoLocalPlayer, NotOwner, Ready } internal static class PickupPolicy { public const float DefaultRange = 8f; public const float MinimumRange = 2f; public const float MaximumRange = 10f; public static float Range(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(2f, Math.Min(10f, value)); } return 8f; } public static PickupStatus Evaluate(bool available, bool inWorld, bool dedicated, bool authority, bool connected, bool initialSync, bool receivedAllSettings, bool manage) { if (!available) { return PickupStatus.Unavailable; } if (!inWorld) { return PickupStatus.NoWorld; } if (dedicated) { return PickupStatus.DedicatedServer; } if (!authority && (!connected || !initialSync || !receivedAllSettings)) { return PickupStatus.WaitingForServer; } if (!manage) { return PickupStatus.ManagementOff; } return PickupStatus.Ready; } public static string StatusText(PickupStatus status) { return status switch { PickupStatus.Ready => "active during native pickup calls", PickupStatus.ManagementOff => "management off; game or other mods control range", PickupStatus.WaitingForServer => "waiting for complete server policy; range untouched", PickupStatus.DedicatedServer => "server policy only; no local pickup", PickupStatus.NoWorld => "no world loaded", PickupStatus.NoLocalPlayer => "no current local player", PickupStatus.NotOwner => "local player network ownership not ready", _ => "pickup hook unavailable or plugin disabled; range untouched", }; } } } namespace ServerSync { [PublicAPI] internal abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } [PublicAPI] internal class SyncedConfigEntry<T>(ConfigEntry<T> sourceConfig) : OwnConfigEntryBase() { public readonly ConfigEntry<T> SourceConfig = sourceConfig; public override ConfigEntryBase BaseConfig => (ConfigEntryBase)(object)SourceConfig; public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public void AssignLocalValue(T value) { if (LocalBaseValue == null) { Value = value; } else { LocalBaseValue = value; } } } internal abstract class CustomSyncedValueBase { public object? LocalBaseValue; public readonly string Identifier; public readonly Type Type; private object? boxedValue; protected bool localIsOwner; public readonly int Priority; public object? BoxedValue { get { return boxedValue; } set { boxedValue = value; this.ValueChanged?.Invoke(); } } public event Action? ValueChanged; protected CustomSyncedValueBase(ConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } } [PublicAPI] internal sealed class CustomSyncedValue<T> : CustomSyncedValueBase { public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } public CustomSyncedValue(ConfigSync configSync, string identifier, T value = default(T), int priority = 0) : base(configSync, identifier, typeof(T), priority) { Value = value; } public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { LocalBaseValue = value; } } } internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } [PublicAPI] internal class ConfigSync { [HarmonyPatch(typeof(ZRpc), "HandlePackage")] private static class SnatchCurrentlyHandlingRPC { public static ZRpc? currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { isServer = __instance.IsServer(); foreach (ConfigSync configSync2 in configSyncs) { ZRoutedRpc.instance.Register<ZPackage>(configSync2.Name + " ConfigSync", (Action<long, ZPackage>)configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log((object)("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections")); } } if (isServer) { ((MonoBehaviour)__instance).StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List<ZNetPeer> peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = isAdmin } }); ConfigSync configSync = configSyncs.First(); if (configSync != null) { ((MonoBehaviour)ZNet.instance).StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List<string> CurrentList = new List<string>(adminList.GetList()); while (true) { yield return (object)new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List<string>(adminList.GetList()); List<ZNetPeer> adminPeer = ZNet.instance.GetPeers().Where(delegate(ZNetPeer p) { string hostName = p.m_rpc.GetSocket().GetHostName(); return ((object)listContainsId == null) ? adminList.Contains(hostName) : ((bool)listContainsId.Invoke(ZNet.instance, new object[2] { adminList, hostName })); }).ToList(); List<ZNetPeer> nonAdminPeer = ZNet.instance.GetPeers().Except(adminPeer).ToList(); SendAdmin(nonAdminPeer, isAdmin: false); SendAdmin(adminPeer, isAdmin: true); } } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in configSyncs) { peer.m_rpc.Register<ZPackage>(configSync.Name + " ConfigSync", (Action<ZRpc, ZPackage>)configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary<OwnConfigEntryBase, object?> configValues = new Dictionary<OwnConfigEntryBase, object>(); public readonly Dictionary<CustomSyncedValueBase, object?> customValues = new Dictionary<CustomSyncedValueBase, object>(); } [HarmonyPatch(typeof(ZNet), "Shutdown")] private class ResetConfigsOnShutdown { [HarmonyPostfix] private static void Postfix() { ProcessingServerUpdate = true; foreach (ConfigSync configSync in configSyncs) { configSync.resetConfigsFromServer(); configSync.IsSourceOfTruth = true; configSync.InitialSyncDone = false; } ProcessingServerUpdate = false; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private class SendConfigsAfterLogin { private class BufferingSocket : ZPlayFabSocket, ISocket { public volatile bool finished = false; public volatile int versionMatchQueued = -1; public readonly List<ZPackage> Package = new List<ZPackage>(); public readonly ISocket Original; public BufferingSocket(ISocket original) { Original = original; ((ZPlayFabSocket)this)..ctor(); } public bool IsConnected() { return Original.IsConnected(); } public ZPackage Recv() { return Original.Recv(); } public int GetSendQueueSize() { return Original.GetSendQueueSize(); } public int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public bool IsHost() { return Original.IsHost(); } public void Dispose() { Original.Dispose(); } public bool GotNewData() { return Original.GotNewData(); } public void Close() { Original.Close(); } public string GetEndPointString() { return Original.GetEndPointString(); } public void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(ref totalSent, ref totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(ref localQuality, ref remoteQuality, ref ping, ref outByteSec, ref inByteSec); } public ISocket Accept() { return Original.Accept(); } public int GetHostPort() { return Original.GetHostPort(); } public bool Flush() { return Original.Flush(); } public string GetHostName() { return Original.GetHostName(); } public void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == StringExtensionMethods.GetStableHashCode("PeerInfo") || num == StringExtensionMethods.GetStableHashCode("RoutedRPC") || num == StringExtensionMethods.GetStableHashCode("ZDOData")) && !finished) { ZPackage val = new ZPackage(pkg.GetArray()); val.SetPos(pos); Package.Add(val); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPriority(800)] [HarmonyPrefix] private static void Prefix(ref Dictionary<Assembly, BufferingSocket>? __state, ZNet __instance, ZRpc rpc) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Invalid comparison between Unknown and I4 if (!__instance.IsServer()) { return; } BufferingSocket bufferingSocket = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket); object? obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null && (int)ZNet.m_onlineBackend > 0) { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket"); object? value = fieldInfo.GetValue(val); ZPlayFabSocket val2 = (ZPlayFabSocket)((value is ZPlayFabSocket) ? value : null); if (val2 != null) { typeof(ZPlayFabSocket).GetField("m_remotePlayerId").SetValue(bufferingSocket, val2.m_remotePlayerId); } fieldInfo.SetValue(val, bufferingSocket); } if (__state == null) { __state = new Dictionary<Assembly, BufferingSocket>(); } __state[Assembly.GetExecutingAssembly()] = bufferingSocket; } [HarmonyPostfix] private static void Postfix(Dictionary<Assembly, BufferingSocket> __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (__instance.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); peer = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (peer == null) { SendBufferedData(); } else { ((MonoBehaviour)__instance).StartCoroutine(sendAsync()); } } void SendBufferedData() { if (rpc.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket.Original); object? obj2 = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj2 is ZNetPeer) ? obj2 : null); if (val != null) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(val, bufferingSocket.Original); } } BufferingSocket bufferingSocket2 = __state[Assembly.GetExecutingAssembly()]; bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } bufferingSocket2.Original.Send(bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List<PackageEntry> entries = new List<PackageEntry>(); if (configSync.CurrentVersion != null) { entries.Add(new PackageEntry { section = "Internal", key = "serverversion", type = typeof(string), value = configSync.CurrentVersion }); } MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); entries.Add(new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = (((object)listContainsId == null) ? ((object)adminList.Contains(rpc.GetSocket().GetHostName())) : listContainsId.Invoke(ZNet.instance, new object[2] { adminList, rpc.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, entries, partial: false); yield return ((MonoBehaviour)__instance).StartCoroutine(configSync.sendZPackage(new List<ZNetPeer> { peer }, package)); } SendBufferedData(); } } } private class PackageEntry { public string section = null; public string key = null; public Type type = null; public object? value; } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] private static class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSerializedValue")] private static class PreventConfigRereadChangingValues { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || ownConfigEntryBase.LocalBaseValue == null) { return true; } try { ownConfigEntryBase.LocalBaseValue = TomlTypeConverter.ConvertToValue(value, __instance.SettingType); } catch (Exception ex) { Debug.LogWarning((object)$"Config value of setting \"{__instance.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}; Value: {value}"); } return false; } } private class InvalidDeserializationTypeException : Exception { public string expected = null; public string received = null; public string field = ""; } public static bool ProcessingServerUpdate; public readonly string Name; public string? DisplayName; public string? CurrentVersion; public string? MinimumRequiredVersion; public bool ModRequired = false; private bool? forceConfigLocking; private bool isSourceOfTruth = true; private static readonly HashSet<ConfigSync> configSyncs; private readonly HashSet<OwnConfigEntryBase> allConfigs = new HashSet<OwnConfigEntryBase>(); private HashSet<CustomSyncedValueBase> allCustomValues = new HashSet<CustomSyncedValueBase>(); private static bool isServer; private static bool lockExempt; private OwnConfigEntryBase? lockedConfig = null; private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private readonly Dictionary<string, SortedDictionary<int, byte[]>> configValueCache = new Dictionary<string, SortedDictionary<int, byte[]>>(); private readonly List<KeyValuePair<long, string>> cacheExpirations = new List<KeyValuePair<long, string>>(); private static long packageCounter; public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { if (lockedConfig == null) { goto IL_0052; } num = ((IConvertible)lockedConfig.BaseConfig.BoxedValue).ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag == true; } if (!num) { goto IL_0052; } int result = ((!lockExempt) ? 1 : 0); goto IL_0053; IL_0052: result = 0; goto IL_0053; IL_0053: return (byte)result != 0; } set { forceConfigLocking = value; } } public bool IsAdmin => lockExempt || isSourceOfTruth; public bool IsSourceOfTruth { get { return isSourceOfTruth; } private set { if (value != isSourceOfTruth) { isSourceOfTruth = value; this.SourceOfTruthChanged?.Invoke(value); } } } public bool InitialSyncDone { get; private set; } = false; public event Action<bool>? SourceOfTruthChanged; private event Action? lockedConfigChanged; static ConfigSync() { ProcessingServerUpdate = false; configSyncs = new HashSet<ConfigSync>(); lockExempt = false; packageCounter = 0L; RuntimeHelpers.RunClassConstructor(typeof(VersionCheck).TypeHandle); } public ConfigSync(string name) { Name = name; configSyncs.Add(this); new VersionCheck(this); } public SyncedConfigEntry<T> AddConfigEntry<T>(ConfigEntry<T> configEntry) { OwnConfigEntryBase ownConfigEntryBase = configData((ConfigEntryBase)(object)configEntry); SyncedConfigEntry<T> syncedEntry = ownConfigEntryBase as SyncedConfigEntry<T>; if (syncedEntry == null) { syncedEntry = new SyncedConfigEntry<T>(configEntry); AccessTools.DeclaredField(typeof(ConfigDescription), "<Tags>k__BackingField").SetValue(((ConfigEntryBase)configEntry).Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(((ConfigEntryBase)configEntry).Description.Tags ?? Array.Empty<object>()).Concat(new SyncedConfigEntry<T>[1] { syncedEntry }).ToArray()); configEntry.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(0L, (ConfigEntryBase)configEntry); } }; allConfigs.Add(syncedEntry); } return syncedEntry; } public SyncedConfigEntry<T> AddLockingConfigEntry<T>(ConfigEntry<T> lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry<T>(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry<T>)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { if (allCustomValues.Select((CustomSyncedValueBase v) => v.Identifier).Concat(new string[1] { "serverversion" }).Contains(customValue.Identifier)) { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion)"); } allCustomValues.Add(customValue); allCustomValues = new HashSet<CustomSyncedValueBase>(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(0L, customValue); } }; } private void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { lockedConfigChanged += serverLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false)) { InitialSyncDone = true; } } private void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Expected O, but got Unknown //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_0257: Expected O, but got Unknown //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Expected O, but got Unknown try { if (isServer && IsLocked) { ZRpc? currentRpc = SnatchCurrentlyHandlingRPC.currentRpc; object obj; if (currentRpc == null) { obj = null; } else { ISocket socket = currentRpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (text != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList val = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (!(((object)methodInfo == null) ? val.Contains(text) : ((bool)methodInfo.Invoke(ZNet.instance, new object[2] { val, text })))) { return false; } } } cacheExpirations.RemoveAll(delegate(KeyValuePair<long, string> kv) { if (kv.Key < DateTimeOffset.Now.Ticks) { configValueCache.Remove(kv.Value); return true; } return false; }); byte b = package.ReadByte(); if ((b & 2) != 0) { long num = package.ReadLong(); string text2 = sender.ToString() + num; if (!configValueCache.TryGetValue(text2, out SortedDictionary<int, byte[]> value)) { value = new SortedDictionary<int, byte[]>(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair<long, string>(DateTimeOffset.Now.AddSeconds(60.0).Ticks, text2)); } int key = package.ReadInt(); int num2 = package.ReadInt(); value.Add(key, package.ReadByteArray()); if (value.Count < num2) { return false; } configValueCache.Remove(text2); package = new ZPackage(value.Values.SelectMany((byte[] a) => a).ToArray()); b = package.ReadByte(); } ProcessingServerUpdate = true; if ((b & 4) != 0) { byte[] buffer = package.ReadByteArray(); MemoryStream stream = new MemoryStream(buffer); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { deflateStream.CopyTo(memoryStream); } package = new ZPackage(memoryStream.ToArray()); b = package.ReadByte(); } if ((b & 1) == 0) { resetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package); ConfigFile val2 = null; bool saveOnConfigSet = false; foreach (KeyValuePair<OwnConfigEntryBase, object> configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } if (val2 == null) { val2 = configValue.Key.BaseConfig.ConfigFile; saveOnConfigSet = val2.SaveOnConfigSet; val2.SaveOnConfigSet = false; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } if (val2 != null) { val2.SaveOnConfigSet = saveOnConfigSet; val2.Save(); } foreach (KeyValuePair<CustomSyncedValueBase, object> customValue in parsedConfigs.customValues) { if (!isServer) { CustomSyncedValueBase key2 = customValue.Key; if (key2.LocalBaseValue == null) { key2.LocalBaseValue = customValue.Key.BoxedValue; } } customValue.Key.BoxedValue = customValue.Value; } Debug.Log((object)string.Format("Received {0} configs and {1} custom values from {2} for mod {3}", parsedConfigs.configValues.Count, parsedConfigs.customValues.Count, (isServer || clientUpdate) ? $"client {sender}" : "the server", DisplayName ?? Name)); if (!isServer) { serverLockedSettingChanged(); } return true; } finally { ProcessingServerUpdate = false; } } private ParsedConfigs ReadConfigsFromPackage(ZPackage package) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary<string, OwnConfigEntryBase> dictionary = allConfigs.Where((OwnConfigEntryBase c) => c.SynchronizedConfig).ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "_" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary<string, CustomSyncedValueBase> dictionary2 = allCustomValues.ToDictionary((CustomSyncedValueBase c) => c.Identifier, (CustomSyncedValueBase c) => c); int num = package.ReadInt(); for (int num2 = 0; num2 < num; num2++) { string text = package.ReadString(); string text2 = package.ReadString(); string text3 = package.ReadString(); Type type = Type.GetType(text3); if (text3 == "" || type != null) { object obj; try { obj = ((text3 == "") ? null : ReadValueWithTypeFromZPackage(package, type)); } catch (InvalidDeserializationTypeException ex) { Debug.LogWarning((object)("Got unexpected struct internal type " + ex.received + " for field " + ex.field + " struct " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + ex.expected)); continue; } OwnConfigEntryBase value2; if (text == "Internal") { CustomSyncedValueBase value; if (text2 == "serverversion") { if (obj?.ToString() != CurrentVersion) { Debug.LogWarning((object)("Received server version is not equal: server version = " + (obj?.ToString() ?? "null") + "; local version = " + (CurrentVersion ?? "unknown"))); } } else if (text2 == "lockexempt") { if (obj is bool flag) { lockExempt = flag; } } else if (dictionary2.TryGetValue(text2, out value)) { if ((text3 == "" && (!value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null)) || GetZPackageTypeString(value.Type) == text3) { parsedConfigs.customValues[value] = obj; continue; } Debug.LogWarning((object)("Got unexpected type " + text3 + " for internal value " + text2 + " for mod " + (DisplayName ?? Name) + ", expecting " + value.Type.AssemblyQualifiedName)); } } else if (dictionary.TryGetValue(text + "_" + text2, out value2)) { Type type2 = configType(value2.BaseConfig); if ((text3 == "" && (!type2.IsValueType || Nullable.GetUnderlyingType(type2) != null)) || GetZPackageTypeString(type2) == text3) { parsedConfigs.configValues[value2] = obj; continue; } Debug.LogWarning((object)("Got unexpected type " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName)); } else { Debug.LogWarning((object)("Received unknown config entry " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ". This may happen if client and server versions of the mod do not match.")); } continue; } Debug.LogWarning((object)("Got invalid type " + text3 + ", abort reading of received configs")); return new ParsedConfigs(); } return parsedConfigs; } private static bool isWritableConfig(OwnConfigEntryBase config) { ConfigSync configSync = configSyncs.FirstOrDefault((ConfigSync cs) => cs.allConfigs.Contains(config)); if (configSync == null) { return true; } return configSync.IsSourceOfTruth || !config.SynchronizedConfig || config.LocalBaseValue == null || (!configSync.IsLocked && (config != configSync.lockedConfig || lockExempt)); } private void serverLockedSettingChanged() { foreach (OwnConfigEntryBase allConfig in allConfigs) { configAttribute<ConfigurationManagerAttributes>(allConfig.BaseConfig).ReadOnly = !isWritableConfig(allConfig); } } private void resetConfigsFromServer() { ConfigFile val = null; bool saveOnConfigSet = false; foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.LocalBaseValue != null)) { if (val == null) { val = item.BaseConfig.ConfigFile; saveOnConfigSet = val.SaveOnConfigSet; val.SaveOnConfigSet = false; } item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } if (val != null) { val.SaveOnConfigSet = saveOnConfigSet; } foreach (CustomSyncedValueBase item2 in allCustomValues.Where((CustomSyncedValueBase config) => config.LocalBaseValue != null)) { item2.BoxedValue = item2.LocalBaseValue; item2.LocalBaseValue = null; } lockedConfigChanged -= serverLockedSettingChanged; serverLockedSettingChanged(); } private IEnumerator<bool> distributeConfigToPeers(ZNetPeer peer, ZPackage package) { ZRoutedRpc rpc = ZRoutedRpc.instance; if (rpc == null) { yield break; } byte[] data = package.GetArray(); if (data != null && data.LongLength > 250000) { int fragments = (int)(1 + (data.LongLength - 1) / 250000); long packageIdentifier = ++packageCounter; int fragment = 0; while (fragment < fragments) { foreach (bool item in waitForQueue()) { yield return item; } if (peer.m_socket.IsConnected()) { ZPackage fragmentedPackage = new ZPackage(); fragmentedPackage.Write((byte)2); fragmentedPackage.Write(packageIdentifier); fragmentedPackage.Write(fragment); fragmentedPackage.Write(fragments); fragmentedPackage.Write(data.Skip(250000 * fragment).Take(250000).ToArray()); SendPackage(fragmentedPackage); if (fragment != fragments - 1) { yield return true; } int num = fragment + 1; fragment = num; continue; } break; } yield break; } foreach (bool item2 in waitForQueue()) { yield return item2; } SendPackage(package); void SendPackage(ZPackage pkg) { string text = Name + " ConfigSync"; if (isServer) { peer.m_rpc.Invoke(text, new object[1] { pkg }); } else { rpc.InvokeRoutedRPC(peer.m_server ? 0 : peer.m_uid, text, new object[1] { pkg }); } } IEnumerable<bool> waitForQueue() { float timeout = Time.time + 30f; while (peer.m_socket.GetSendQueueSize() > 20000) { if (Time.time > timeout) { Debug.Log((object)$"Disconnecting {peer.m_uid} after 30 seconds config sending timeout"); peer.m_rpc.Invoke("Error", new object[1] { (object)(ConnectionStatus)5 }); ZNet.instance.Disconnect(peer); break; } yield return false; } } } private IEnumerator sendZPackage(long target, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return Enumerable.Empty<object>().GetEnumerator(); } List<ZNetPeer> list = (List<ZNetPeer>)AccessTools.DeclaredField(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); if (target != 0) { list = list.Where((ZNetPeer p) => p.m_uid == target).ToList(); } return sendZPackage(list, package); } private IEnumerator sendZPackage(List<ZNetPeer> peers, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { yield break; } byte[] rawData = package.GetArray(); if (rawData != null && rawData.LongLength > 10000) { ZPackage compressedPackage = new ZPackage(); compressedPackage.Write((byte)4); MemoryStream output = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(output, CompressionLevel.Optimal)) { deflateStream.Write(rawData, 0, rawData.Length); } compressedPackage.Write(output.ToArray()); package = compressedPackage; } List<IEnumerator<bool>> writers = (from p in peers where p.IsReady() select distributeConfigToPeers(p, package)).ToList(); writers.RemoveAll((IEnumerator<bool> writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator<bool> writer) => !writer.MoveNext()); } } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(configs); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(null, customValues); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private static OwnConfigEntryBase? configData(ConfigEntryBase config) { return config.Description.Tags?.OfType<OwnConfigEntryBase>().SingleOrDefault(); } public static SyncedConfigEntry<T>? ConfigData<T>(ConfigEntry<T> config) { return ((ConfigEntryBase)config).Description.Tags?.OfType<SyncedConfigEntry<T>>().SingleOrDefault(); } private static T configAttribute<T>(ConfigEntryBase config) { return config.Description.Tags.OfType<T>().First(); } private static Type configType(ConfigEntryBase config) { return configType(config.SettingType); } private static Type configType(Type type) { return type.IsEnum ? Enum.GetUnderlyingType(type) : type; } private static ZPackage ConfigsToPackage(IEnumerable<ConfigEntryBase>? configs = null, IEnumerable<CustomSyncedValueBase>? customValues = null, IEnumerable<PackageEntry>? packageEntries = null, bool partial = true) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown List<ConfigEntryBase> list = configs?.Where((ConfigEntryBase config) => configData(config).SynchronizedConfig).ToList() ?? new List<ConfigEntryBase>(); List<CustomSyncedValueBase> list2 = customValues?.ToList() ?? new List<CustomSyncedValueBase>(); ZPackage val = new ZPackage(); val.Write((byte)(partial ? 1 : 0)); val.Write(list.Count + list2.Count + (packageEntries?.Count() ?? 0)); foreach (PackageEntry item in packageEntries ?? Array.Empty<PackageEntry>()) { AddEntryToPackage(val, item); } foreach (CustomSyncedValueBase item2 in list2) { AddEntryToPackage(val, new PackageEntry { section = "Internal", key = item2.Identifier, type = item2.Type, value = item2.BoxedValue }); } foreach (ConfigEntryBase item3 in list) { AddEntryToPackage(val, new PackageEntry { section = item3.Definition.Section, key = item3.Definition.Key, type = configType(item3), value = item3.BoxedValue }); } return val; } private static void AddEntryToPackage(ZPackage package, PackageEntry entry) { package.Write(entry.section); package.Write(entry.key); package.Write((entry.value == null) ? "" : GetZPackageTypeString(entry.type)); AddValueToZPackage(package, entry.value); } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void AddValueToZPackage(ZPackage package, object? value) { Type type = value?.GetType(); if (value is Enum) { value = ((IConvertible)value).ToType(Enum.GetUnderlyingType(value.GetType()