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 ReliableSmelting v0.3.1
BepInEx/plugins/ReliableSmelting/ReliableSmelting.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using HarmonyLib; using UnityEngine; [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: CompilationRelaxations(8)] [assembly: AssemblyMetadata("AI_Assisted_Creation", "Coded with the assistance of ChatGPT 6.0; packaged with OpenAI Codex.")] [assembly: AssemblyMetadata("AI_Model_Vendor", "OpenAI")] [assembly: AssemblyVersion("0.0.0.0")] namespace ReliableSmelting; [BepInPlugin("au.tyvalheim.reliablesmelting", "Reliable Smelting", "0.3.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { [HarmonyPatch(typeof(Smelter), "OnAddFuel")] internal static class FreeSmelterFuel { [HarmonyBefore(new string[] { "au.tyvalheim.reliablecrafting" })] [HarmonyPriority(800)] private static bool Prefix(Smelter __instance, Humanoid __1, ref bool __result, bool __runOriginal) { if (!__runOriginal) { return false; } Plugin instance = Instance; if (!((Object)(object)instance == (Object)null) && !((Object)(object)__1 != (Object)(object)Player.m_localPlayer)) { return instance.ManualFree((Component)(object)__instance, instance.setFuel, instance.getFuel, __instance.m_maxFuel, ref __result); } return true; } } [HarmonyPatch(typeof(CookingStation), "OnAddFuelSwitch")] internal static class FreeFoundryFuel { [HarmonyBefore(new string[] { "au.tyvalheim.reliablecrafting" })] [HarmonyPriority(800)] private static bool Prefix(CookingStation __instance, Humanoid __1, ref bool __result, bool __runOriginal) { if (!__runOriginal) { return false; } Plugin instance = Instance; if (!((Object)(object)instance == (Object)null) && !((Object)(object)__1 != (Object)(object)Player.m_localPlayer)) { return instance.ManualFree((Component)(object)__instance, instance.foundrySetFuel, instance.foundryFuel, __instance.m_maxFuel, ref __result); } return true; } } private const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic; private static Plugin Instance; private Harmony fuelHarmony; private CookingStation[] cachedFoundries = (CookingStation[])(object)new CookingStation[0]; private ConfigEntry<bool> feedCasts; private ConfigEntry<bool> collectCastOutputs; private MethodInfo foundryFuel; private MethodInfo foundrySetFuel; private MethodInfo freeSlot; private MethodInfo addCast; private MethodInfo haveDone; private MethodInfo removeDone; private ConfigEntry<bool> automation; private ConfigEntry<bool> collect; private ConfigEntry<bool> outputs; private ConfigEntry<bool> diagnostics; private ConfigEntry<bool> respectEternalFuel; private ConfigEntry<bool> keepFueled; private ConfigEntry<float> interval; private ConfigEntry<float> radius; private ConfigEntry<float> playerRadius; private ConfigEntry<float> discoveryInterval; private ConfigEntry<OperatingMode> operatingMode; private Smelter[] cachedMachines = (Smelter[])(object)new Smelter[0]; private Container[] cachedChests = (Container[])(object)new Container[0]; private float nextDiscovery; private ConfigEntry<bool> claimMachines; private readonly OwnershipPolicy ownership = new OwnershipPolicy(); private readonly Dictionary<int, string> machineStatus = new Dictionary<int, string>(); private float nextOwnershipNotice; private Player sessionPlayer; private ConfigEntry<int> budget; private ConfigEntry<string> prohibitedWood; private ConfigEntry<float> diagnosticInterval; private float nextDiagnostic; private readonly Dictionary<int, int> fed = new Dictionary<int, int>(); private float nextScan; private bool faulted; private int remaining; private HashSet<string> blocked; private PrivateArea[] wards = (PrivateArea[])(object)new PrivateArea[0]; private readonly HashSet<string> resources = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<int, float> firstSeen = new Dictionary<int, float>(); private static readonly HashSet<string> Machines = Policy.Parse("charcoal_kiln,smelter,blastfurnace,piece_FrostKiln"); private static readonly HashSet<string> Chests = Policy.Parse("piece_chest_wood,piece_chest,piece_chest_blackmetal"); private MethodInfo getFuel; private MethodInfo setFuel; private MethodInfo getQueue; private MethodInfo queueOre; private MethodInfo loadChest; private MethodInfo saveDrop; private MethodInfo wardEnabled; private MethodInfo wardInside; private void InitializeFoundry() { //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Expected O, but got Unknown Instance = this; feedCasts = ((BaseUnityPlugin)this).Config.Bind<bool>("Foundry", "AutoLoadCasts", false, "Opt in to processing casts in nearby owned public chests. Uses native foundry recipes, one cast per slot; leave off to select casts manually."); collectCastOutputs = ((BaseUnityPlugin)this).Config.Bind<bool>("Foundry", "AutoCollectCastOutputs", false, "Opt in to native output ejection and collecting foundry products near supported machines. Leave off to collect your equipment manually. Requires a living local player for crafter attribution."); foundryFuel = Require(typeof(CookingStation), "GetFuel", Type.EmptyTypes); foundrySetFuel = Require(typeof(CookingStation), "SetFuel", new Type[1] { typeof(float) }); freeSlot = Require(typeof(CookingStation), "GetFreeSlot", Type.EmptyTypes); addCast = Require(typeof(CookingStation), "RPC_AddItem", new Type[3] { typeof(long), typeof(string), typeof(bool) }); haveDone = Require(typeof(CookingStation), "HaveDoneItem", Type.EmptyTypes); removeDone = Require(typeof(CookingStation), "RPC_RemoveDoneItem", new Type[3] { typeof(long), typeof(Vector3), typeof(int) }); fuelHarmony = new Harmony("au.tyvalheim.reliablesmelting"); fuelHarmony.PatchAll(typeof(Plugin).Assembly); } private void OnDestroy() { if (fuelHarmony != null) { fuelHarmony.UnpatchSelf(); } if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void FillAny(Component machine, Container[] chests) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown Smelter val = (Smelter)(object)((machine is Smelter) ? machine : null); if ((Object)(object)val != (Object)null) { Fill(val, chests); } else { FillFoundry((CookingStation)machine, chests); } } private bool FoundryEligible(CookingStation station) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)station) && Owned((Component)(object)station) && OperatingRange((Component)(object)station)) { return Access(((Component)station).transform.position); } return false; } private void FillFoundry(CookingStation station, Container[] chests) { //IL_0467: Unknown result type (might be due to invalid IL or missing references) //IL_0478: Unknown result type (might be due to invalid IL or missing references) //IL_047d: Unknown result type (might be due to invalid IL or missing references) if (!FoundryEligible(station)) { return; } FuelSupply fuelSupply = Policy.SelectFuelSupply("piece_FrostFoundry", respectEternalFuel.Value && FuelEternalCompatibility.Manages("piece_FrostFoundry", foundrySetFuel), keepFueled.Value); Policy.MaintainFuel(fuelSupply, () => (float)foundryFuel.Invoke(station, null), delegate(float value) { if (FoundryEligible(station)) { foundrySetFuel.Invoke(station, new object[1] { value }); } }, station.m_maxFuel); foreach (Container chest in chests.Where((Container c) => InRange(((Component)c).transform.position, ((Component)station).transform.position, radius.Value)).OrderBy(delegate(Container c) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: 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_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val = ((Component)c).transform.position - ((Component)station).transform.position; return ((Vector3)(ref val)).sqrMagnitude; }).ThenBy((Container c) => ((Object)c).GetInstanceID())) { if (!UsableChest(chest)) { continue; } Inventory inventory = chest.GetInventory(); ItemData[] array = inventory.GetAllItems().ToArray(); foreach (ItemData item in array) { string name = ItemName(item); bool flag = fuelSupply == FuelSupply.Chests && Object.op_Implicit((Object)(object)station.m_fuelItem) && name == Prefab((Object)(object)((Component)station.m_fuelItem).gameObject) && !blocked.Contains(name); bool flag2 = feedCasts.Value && !blocked.Contains(name) && item.m_quality == 1 && item.m_customData.Count == 0 && station.m_conversion.Any((ItemConversion c) => Object.op_Implicit((Object)(object)c.m_from) && name == Prefab((Object)(object)((Component)c.m_from).gameObject)); if (!flag && !flag2) { continue; } while (remaining > 0 && inventory.ContainsItem(item) && item.m_stack > 0) { if (!FoundryEligible(station) || !UsableChest(chest)) { return; } float beforeFuel = (float)foundryFuel.Invoke(station, null); int num2 = (int)freeSlot.Invoke(station, null); bool useFuel = flag && Policy.FuelFits(beforeFuel, station.m_maxFuel); if (!useFuel && (!flag2 || num2 < 0)) { break; } ZNetView view = ((Component)station).GetComponent<ZNetView>(); string slotKey = "slot" + num2; TransferItem(inventory, item, () => FoundryEligible(station) && UsableChest(chest), () => (!useFuel) ? (view.GetZDO().GetString(slotKey, "") != "") : ((float)foundryFuel.Invoke(station, null) != beforeFuel), delegate { if (useFuel) { foundrySetFuel.Invoke(station, new object[1] { beforeFuel + 1f }); } else { addCast.Invoke(station, new object[3] { 0L, name, item.m_cheated }); } }); remaining--; } } } if (collectCastOutputs.Value && FoundryEligible(station) && Object.op_Implicit((Object)(object)Player.m_localPlayer) && !((Character)Player.m_localPlayer).IsDead() && (bool)haveDone.Invoke(station, null) && FoundryEligible(station)) { removeDone.Invoke(station, new object[3] { 0L, ((Component)station).transform.position + ((Component)station).transform.forward, 1 }); } } internal static void TransferItem(Inventory inventory, ItemData item, Func<bool> valid, Func<bool> committed, Action write) { if (!valid() || !inventory.ContainsItem(item) || item.m_stack <= 0) { throw new InvalidOperationException("Transfer eligibility changed before debit."); } List<ItemData> allItems = inventory.GetAllItems(); int val = allItems.IndexOf(item); int stack = item.m_stack; item.m_stack--; if (item.m_stack == 0) { allItems.Remove(item); } try { if (!valid()) { throw new InvalidOperationException("Ownership/access changed before station commit."); } write(); if (!committed()) { throw new InvalidOperationException("Station did not accept its input."); } } catch { if (!committed()) { item.m_stack = stack; if (!allItems.Contains(item)) { allItems.Insert(Math.Min(val, allItems.Count), item); } } AccessTools.Method(typeof(Inventory), "Changed", (Type[])null, (Type[])null).Invoke(inventory, new object[2] { false, false }); throw; } AccessTools.Method(typeof(Inventory), "Changed", (Type[])null, (Type[])null).Invoke(inventory, new object[2] { false, false }); } private bool ManualFree(Component station, MethodInfo setter, MethodInfo getter, int capacity, ref bool result) { //IL_00b0: Unknown result type (might be due to invalid IL or missing references) if (!automation.Value || !keepFueled.Value || !Object.op_Implicit((Object)(object)Player.m_localPlayer) || ((Character)Player.m_localPlayer).IsDead() || ((Character)Player.m_localPlayer).IsTeleporting()) { return true; } if (Policy.SelectFuelSupply(Prefab((Object)(object)station.gameObject), external: false, keepFueled: true) != FuelSupply.BuiltIn) { return true; } result = false; if (faulted) { return false; } if (!Owned(station) || !OperatingRange(station) || !Access(station.transform.position)) { return false; } try { Policy.MaintainFuel(FuelSupply.BuiltIn, () => (float)getter.Invoke(station, null), delegate(float value) { setter.Invoke(station, new object[1] { value }); }, capacity); result = true; return false; } catch (Exception ex) { StopOnError(ex); return false; } } private void Awake() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected O, but got Unknown //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Expected O, but got Unknown //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Expected O, but got Unknown //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Expected O, but got Unknown //IL_0234: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Expected O, but got Unknown //IL_0294: Unknown result type (might be due to invalid IL or missing references) //IL_029e: Expected O, but got Unknown automation = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable automation in loaded areas."); operatingMode = ((BaseUnityPlugin)this).Config.Bind<OperatingMode>("Multiplayer", "OperatingMode", OperatingMode.WhileLoaded, "WhileLoaded: automate loaded locally owned objects beyond PlayerRangeMeters. NearbyOnly: every machine, chest and drop must also be within PlayerRangeMeters. Neither mode keeps areas loaded or works offline. New ownership claims always require proximity."); discoveryInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Performance", "DiscoveryIntervalSeconds", 15f, new ConfigDescription("Seconds between discovering machines and chests. New arrivals/builds may wait this long plus a processing pass. Access and ownership are rechecked for transfers.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), new object[0])); playerRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Multiplayer", "PlayerRangeMeters", 20f, new ConfigDescription("Takeover distance in both modes. NearbyOnly also requires machines, chests and drops within this 3D distance. WhileLoaded allows owned loaded objects beyond this range.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 30f), new object[0])); claimMachines = ((BaseUnityPlugin)this).Config.Bind<bool>("Multiplayer", "ClaimNearbyMachines", true, "Experimental: claim supported player-built machines only when you are at least 1m nearer than every other loaded player. Wait for ownership to settle; retry no more than every 30s. Never claims chests or drops. Open and close nearby public chests normally to acquire them."); respectEternalFuel = ((BaseUnityPlugin)this).Config.Bind<bool>("Compatibility", "RespectFuelEternal", true, "When Fuel Eternal's fuel patch and settings manage a supported machine, leave chest fuel alone and let its patch initialize fuel. Ore feeding and collection remain enabled. Install/configure both mods on the owning peer."); keepFueled = ((BaseUnityPlugin)this).Config.Bind<bool>("Fuel", "KeepFurnacesFueled", false, "Opt-in free refills for locally owned smelters, blast furnaces, Frigid Kilns (Ice) and Frost Foundries (Liquid Frost). No chest fuel consumed. Free kiln ice creates free Liquid Frost. Fuel Eternal takes priority when detected on the matching setter. Does not affect charcoal-kiln wood, ovens, fireplaces, remote-owned or unloaded machines. Use consistent settings on owning clients."); interval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "CheckIntervalSeconds", 5f, new ConfigDescription("Seconds between collection/refill passes.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), new object[0])); radius = ((BaseUnityPlugin)this).Config.Bind<float>("General", "RangeMeters", 10f, new ConfigDescription("3D radius around each machine; collected drops must also be within this distance of their chest.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 50f), new object[0])); prohibitedWood = ((BaseUnityPlugin)this).Config.Bind<string>("Fuel", "ProhibitedWoodTypes", "FineWood,RoundLog", "Comma-separated prefab names forbidden as machine input or fuel. RoundLog = core wood. Still collected into chests."); collect = ((BaseUnityPlugin)this).Config.Bind<bool>("Collection", "CollectGroundResources", true, "Collect wood, coal, and inputs accepted by kiln/smelter/blast furnace recipes."); outputs = ((BaseUnityPlugin)this).Config.Bind<bool>("Collection", "CollectMetalProducts", true, "Also collect the products of supported machines, including metal bars."); budget = ((BaseUnityPlugin)this).Config.Bind<int>("Performance", "MaxItemsPerPass", 200, new ConfigDescription("Global item transfer cap per pass; remaining work continues next pass.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 2000), new object[0])); diagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "LogTransfers", false, "Log counts per pass for troubleshooting."); diagnosticInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "DetailIntervalSeconds", 15f, new ConfigDescription("When LogTransfers is true, log per-machine/chest rejection reasons this often.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 120f), new object[0])); try { getFuel = Require(typeof(Smelter), "GetFuel", Type.EmptyTypes); setFuel = Require(typeof(Smelter), "SetFuel", new Type[1] { typeof(float) }); getQueue = Require(typeof(Smelter), "GetQueueSize", Type.EmptyTypes); queueOre = Require(typeof(Smelter), "QueueOre", new Type[2] { typeof(string), typeof(bool) }); loadChest = Require(typeof(Container), "Load", Type.EmptyTypes); saveDrop = Require(typeof(ItemDrop), "Save", Type.EmptyTypes); wardEnabled = Require(typeof(PrivateArea), "IsEnabled", Type.EmptyTypes); wardInside = Require(typeof(PrivateArea), "IsInside", new Type[2] { typeof(Vector3), typeof(float) }); InitializeFoundry(); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Concat("Reliable Smelting 0.3.1 loaded. Frigid Kiln and Frost Foundry supported. Mode=", operatingMode.Value, "; takeover range=", playerRadius.Value, "m; machine/chest range=", radius.Value, "m. Open/close public chests to obtain ownership. Nearby machine claims=", claimMachines.Value, "; free furnace/Deep North fuel=", keepFueled.Value, ". Settling is not a network acknowledgement; multiplayer testing required.")); } catch (Exception ex) { StopOnError(ex); } } private static MethodInfo Require(Type type, string name, Type[] args) { MethodInfo method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, args, null); if (method == null) { throw new MissingMethodException(type.FullName, name); } return method; } private void Update() { if (faulted || !automation.Value || (Object)(object)ZNetScene.instance == (Object)null || (Object)(object)ZNet.instance == (Object)null) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)(object)sessionPlayer) { ownership.Clear(); machineStatus.Clear(); firstSeen.Clear(); sessionPlayer = localPlayer; nextDiscovery = 0f; } if ((Object)(object)localPlayer == (Object)null || ((Character)localPlayer).IsDead() || ((Character)localPlayer).IsTeleporting()) { ownership.Clear(); firstSeen.Clear(); nextDiscovery = 0f; cachedMachines = (Smelter[])(object)new Smelter[0]; cachedChests = (Container[])(object)new Container[0]; } else if (!(Time.unscaledTime < nextScan)) { nextScan = Time.unscaledTime + interval.Value; try { Scan(); } catch (Exception ex) { StopOnError(ex); } } } private void StopOnError(Exception ex) { faulted = true; ((BaseUnityPlugin)this).Logger.LogError((object)("Automation paused until restart following an unexpected error. Preserve this log and check affected inventories: " + ex)); } private static string Prefab(Object obj) { if (obj == (Object)null) { return ""; } return obj.name.Replace("(Clone)", "").Trim(); } private static string ItemName(ItemData item) { if (item != null) { return Prefab((Object)(object)item.m_dropPrefab); } return ""; } private static bool Owned(Component component) { ZNetView component2 = component.GetComponent<ZNetView>(); if ((Object)(object)component2 != (Object)null && component2.IsValid()) { return component2.IsOwner(); } return false; } private static bool InRange(Vector3 a, Vector3 b, float range) { //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) Vector3 val = a - b; return Policy.InRange(((Vector3)(ref val)).sqrMagnitude, range); } private bool Access(Vector3 position) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Player.m_localPlayer != (Object)null) { return PrivateArea.CheckAccess(position, 0f, false, true); } return !wards.Any((PrivateArea w) => (Object)(object)w != (Object)null && (bool)wardEnabled.Invoke(w, null) && (bool)wardInside.Invoke(w, new object[2] { position, 0f })); } private bool UsableChest(Container chest) { return ChestStatus(chest) == "eligible"; } private bool OperatingRange(Component component) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)component != (Object)null) { OperatingMode value = operatingMode.Value; bool activeInHierarchy = component.gameObject.activeInHierarchy; bool playerReady = (Object)(object)localPlayer != (Object)null && !((Character)localPlayer).IsDead() && !((Character)localPlayer).IsTeleporting(); float distanceSquared; if (!((Object)(object)localPlayer == (Object)null)) { Vector3 val = component.transform.position - ((Component)localPlayer).transform.position; distanceSquared = ((Vector3)(ref val)).sqrMagnitude; } else { distanceSquared = float.PositiveInfinity; } return Policy.CanOperate(value, activeInHierarchy, playerReady, distanceSquared, playerRadius.Value); } return false; } private string PrepareMachine(Component machine) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) ZNetView view = machine.GetComponent<ZNetView>(); if ((Object)(object)view == (Object)null || !view.IsValid()) { return "invalid-network-view"; } Piece component = machine.GetComponent<Piece>(); bool eligible = OperatingRange(machine) && Access(machine.transform.position) && (Object)(object)component != (Object)null && component.IsPlacedByPlayer(); float localDistance = Vector3.Distance(((Component)Player.m_localPlayer).transform.position, machine.transform.position); float num = float.PositiveInfinity; foreach (Player allPlayer in Player.GetAllPlayers()) { if ((Object)(object)allPlayer != (Object)null && (Object)(object)allPlayer != (Object)(object)Player.m_localPlayer) { num = Math.Min(num, Vector3.Distance(((Component)allPlayer).transform.position, machine.transform.position)); } } return ownership.Observe(((Object)machine).GetInstanceID(), view.GetZDO().GetOwner(), view.IsOwner(), eligible, claimMachines.Value && OwnershipPolicy.MayClaim(localDistance, num, playerRadius.Value), Time.unscaledTime, delegate { view.ClaimOwnership(); }); } private string ChestStatus(Container chest) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Invalid comparison between Unknown and I4 //IL_00a5: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)chest == (Object)null) { return "destroyed"; } if (!Chests.Contains(Prefab((Object)(object)((Component)chest).gameObject))) { return "unsupported-container-type"; } if (!OperatingRange((Component)(object)chest)) { return "outside-player-vicinity"; } if (!Owned((Component)(object)chest)) { return "not-owned-by-this-peer"; } Piece component = ((Component)chest).GetComponent<Piece>(); if ((Object)(object)component == (Object)null || !component.IsPlacedByPlayer()) { return "not-player-built"; } if ((int)chest.m_privacy != 2) { return "private-container"; } ZNetView component2 = ((Component)chest).GetComponent<ZNetView>(); if (chest.IsInUse() || component2.GetZDO().GetInt(ZDOVars.s_inUse, 0) != 0) { return "chest-open-or-in-use"; } if (!Access(((Component)chest).transform.position)) { return "ward-access-denied"; } return "eligible"; } private void BuildResourceSet(Smelter[] machines) { resources.Clear(); resources.UnionWith(Policy.Parse("Wood,FineWood,RoundLog,YggdrasilWood,Blackwood,Coal")); foreach (string machine2 in Machines) { GameObject prefab = ZNetScene.instance.GetPrefab(machine2); if ((Object)(object)prefab != (Object)null) { AddRecipes(prefab.GetComponent<Smelter>()); } } foreach (Smelter machine in machines) { AddRecipes(machine); } } private void AddRecipes(Smelter machine) { if ((Object)(object)machine == (Object)null) { return; } if ((Object)(object)machine.m_fuelItem != (Object)null) { resources.Add(Prefab((Object)(object)((Component)machine.m_fuelItem).gameObject)); } foreach (ItemConversion item in machine.m_conversion) { if ((Object)(object)item.m_from != (Object)null) { resources.Add(Prefab((Object)(object)((Component)item.m_from).gameObject)); } if (outputs.Value && (Object)(object)item.m_to != (Object)null) { resources.Add(Prefab((Object)(object)((Component)item.m_to).gameObject)); } } } private void Scan() { blocked = Policy.Parse(prohibitedWood.Value); remaining = budget.Value; fed.Clear(); if (Time.unscaledTime >= nextDiscovery) { cachedMachines = (from s in Object.FindObjectsByType<Smelter>((FindObjectsSortMode)0) where Machines.Contains(Prefab((Object)(object)((Component)s).gameObject)) select s).ToArray(); cachedFoundries = (from s in Object.FindObjectsByType<CookingStation>((FindObjectsSortMode)0) where Prefab((Object)(object)((Component)s).gameObject) == "piece_FrostFoundry" select s).ToArray(); cachedChests = (from c in Object.FindObjectsByType<Container>((FindObjectsSortMode)0) where Chests.Contains(Prefab((Object)(object)((Component)c).gameObject)) select c).ToArray(); nextDiscovery = Time.unscaledTime + discoveryInterval.Value; } Component[] array = (from s in cachedMachines.Cast<Component>().Concat(cachedFoundries.Cast<Component>()) where (Object)(object)s != (Object)null && s.gameObject.activeInHierarchy select s).ToArray(); ownership.Prune(new HashSet<int>(array.Select((Component s) => ((Object)s).GetInstanceID()))); machineStatus.Clear(); Component[] array2 = array; foreach (Component val in array2) { machineStatus[((Object)val).GetInstanceID()] = PrepareMachine(val); } Component[] array3 = (from s in array where machineStatus[((Object)s).GetInstanceID()] == "eligible" && Owned(s) orderby ((Object)s).GetInstanceID() select s).ToArray(); Container[] array4 = cachedChests.Where((Container c) => (Object)(object)c != (Object)null && ((Component)c).gameObject.activeInHierarchy).ToArray(); Container[] array5 = array4.Where(UsableChest).ToArray(); if (Time.unscaledTime >= nextOwnershipNotice) { Component[] nearby = array.Where(OperatingRange).ToArray(); int num2 = nearby.Count((Component s) => machineStatus[((Object)s).GetInstanceID()] != "eligible"); int num3 = array4.Count((Container c) => ChestStatus(c) == "not-owned-by-this-peer" && nearby.Any((Component s) => InRange(((Component)c).transform.position, s.transform.position, radius.Value))); if (num2 > 0 || num3 > 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Multiplayer] nearby machines waiting=" + num2 + "; nearby remote chests=" + num3 + ". To acquire ownership, move within " + playerRadius.Value + "m, stand at least 1m closer to the machine than other players, and open then close its public chest. Enable LogTransfers for per-object reasons.")); } nextOwnershipNotice = Time.unscaledTime + 60f; } if (array3.Length == 0) { firstSeen.Clear(); if (diagnostics.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Transferred 0 items; ready machines=0; loaded supported machines=" + array.Length)); LogDetails(array.OfType<Smelter>().ToArray(), array4); } return; } Container[] array6 = array5; foreach (Container obj in array6) { loadChest.Invoke(obj, null); } BuildResourceSet(array3.OfType<Smelter>().ToArray()); resources.Add("Ice"); resources.Add("FrozenFuel"); if (collectCastOutputs.Value && outputs.Value) { foreach (CookingStation item in array3.OfType<CookingStation>()) { foreach (ItemConversion item2 in item.m_conversion) { if ((Object)(object)item2.m_to != (Object)null) { resources.Add(Prefab((Object)(object)((Component)item2.m_to).gameObject)); } } } } Component[] array7 = array3; foreach (Component machine in array7) { FillAny(machine, array5); } if (collect.Value && remaining > 0) { Collect(array3, array5); } Component[] array8 = array3; foreach (Component machine2 in array8) { FillAny(machine2, array5); } if (diagnostics.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("Transferred " + (budget.Value - remaining) + " items; owned/accessible machines=" + array3.Length + ", eligible chests across loaded area=" + array5.Length)); LogDetails(array.OfType<Smelter>().ToArray(), array4); } } private FuelSupply FuelMode(Smelter machine) { string prefab = Prefab((Object)(object)((Component)machine).gameObject); return Policy.SelectFuelSupply(prefab, respectEternalFuel.Value && FuelEternalCompatibility.Manages(prefab, setFuel), keepFueled.Value); } private void Fill(Smelter machine, Container[] chests) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_029d: Unknown result type (might be due to invalid IL or missing references) if (!Owned((Component)(object)machine) || !OperatingRange((Component)(object)machine) || !Access(((Component)machine).transform.position)) { return; } FuelSupply fuelSupply = FuelMode(machine); Policy.MaintainFuel(fuelSupply, () => (float)getFuel.Invoke(machine, null), delegate(float num2) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) if (Owned((Component)(object)machine) && OperatingRange((Component)(object)machine) && Access(((Component)machine).transform.position)) { setFuel.Invoke(machine, new object[1] { num2 }); } }, machine.m_maxFuel); if (remaining <= 0) { return; } HashSet<string> recipes = new HashSet<string>(from c in machine.m_conversion where (Object)(object)c.m_from != (Object)null select Prefab((Object)(object)((Component)c.m_from).gameObject), StringComparer.OrdinalIgnoreCase); string text = (((Object)(object)machine.m_fuelItem == (Object)null) ? "" : Prefab((Object)(object)((Component)machine.m_fuelItem).gameObject)); foreach (Container chest in chests.Where((Container c) => InRange(((Component)c).transform.position, ((Component)machine).transform.position, radius.Value)).OrderBy(delegate(Container c) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: 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_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val = ((Component)c).transform.position - ((Component)machine).transform.position; return ((Vector3)(ref val)).sqrMagnitude; }).ThenBy((Container c) => ((Object)c).GetInstanceID())) { if (!UsableChest(chest)) { continue; } Inventory inventory = chest.GetInventory(); ItemData[] array = inventory.GetAllItems().ToArray(); foreach (ItemData item in array) { string name = ItemName(item); bool flag = fuelSupply == FuelSupply.Chests && name == text && !blocked.Contains(name); bool flag2 = Policy.CanFeed(name, recipes, blocked); if (!flag && !flag2) { continue; } while (remaining > 0 && inventory.GetAllItems().Contains(item) && item.m_stack > 0) { if (!Owned((Component)(object)machine) || !OperatingRange((Component)(object)machine) || !Access(((Component)machine).transform.position) || !UsableChest(chest)) { return; } float beforeFuel = (float)getFuel.Invoke(machine, null); int beforeQueue = (int)getQueue.Invoke(machine, null); bool feedFuel = flag && Policy.FuelFits(beforeFuel, machine.m_maxFuel); bool flag3 = !feedFuel && flag2 && beforeQueue < machine.m_maxOre; if (!feedFuel && !flag3) { break; } TransferItem(inventory, item, () => Owned((Component)(object)machine) && OperatingRange((Component)(object)machine) && Access(((Component)machine).transform.position) && UsableChest(chest), () => (!feedFuel) ? ((int)getQueue.Invoke(machine, null) != beforeQueue) : ((float)getFuel.Invoke(machine, null) != beforeFuel), delegate { if (feedFuel) { setFuel.Invoke(machine, new object[1] { beforeFuel + 1f }); } else { queueOre.Invoke(machine, new object[2] { name, item.m_cheated }); } }); remaining--; int instanceID = ((Object)machine).GetInstanceID(); fed.TryGetValue(instanceID, out var value); fed[instanceID] = value + 1; } } } } private static string Number(float value) { return value.ToString("0.00", CultureInfo.InvariantCulture); } private static string Location(Component component) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) Vector3 position = component.transform.position; return Prefab((Object)(object)component.gameObject) + "#" + ((Object)component).GetInstanceID() + "@(" + Number(position.x) + "," + Number(position.y) + "," + Number(position.z) + ")"; } private static string Owner(Component component) { ZNetView component2 = component.GetComponent<ZNetView>(); if ((Object)(object)component2 == (Object)null || !component2.IsValid()) { return "invalid-network-view"; } return "local=" + component2.IsOwner() + ",owner=" + component2.GetZDO().GetOwner(); } private void LogDetails(Smelter[] machines, Container[] chests) { //IL_0480: Unknown result type (might be due to invalid IL or missing references) //IL_0491: Unknown result type (might be due to invalid IL or missing references) //IL_0638: Unknown result type (might be due to invalid IL or missing references) //IL_0649: Unknown result type (might be due to invalid IL or missing references) if (Time.unscaledTime < nextDiagnostic) { return; } nextDiagnostic = Time.unscaledTime + diagnosticInterval.Value; try { Player player = Player.m_localPlayer; Smelter[] array = machines.OrderBy(delegate(Smelter m) { //IL_0014: 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_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)player == (Object)null)) { Vector3 val = ((Component)m).transform.position - ((Component)player).transform.position; return ((Vector3)(ref val)).sqrMagnitude; } return 0f; }).ToArray(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Detail] range=" + Number(radius.Value) + "m; prohibited=" + prohibitedWood.Value + "; pass-budget-left=" + remaining + "; showing nearest " + Math.Min(10, array.Length) + "/" + array.Length + " loaded machines. Chest counts are observed local inventory; remote/open chest snapshots may be stale.")); foreach (Smelter machine in array.Take(10)) { if (!machineStatus.TryGetValue(((Object)machine).GetInstanceID(), out var value)) { value = "not-scanned"; } ZNetView component = ((Component)machine).GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid()) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Machine] " + Location((Component)(object)machine) + "; skipped=invalid-network-view")); continue; } HashSet<string> accepted = new HashSet<string>(from c in machine.m_conversion where (Object)(object)c.m_from != (Object)null select Prefab((Object)(object)((Component)c.m_from).gameObject), StringComparer.OrdinalIgnoreCase); int num = (int)getQueue.Invoke(machine, null); fed.TryGetValue(((Object)machine).GetInstanceID(), out var value2); Container[] array2 = chests.Where((Container c) => (Object)(object)c != (Object)null).OrderBy(delegate(Container c) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: 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_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val = ((Component)c).transform.position - ((Component)machine).transform.position; return ((Vector3)(ref val)).sqrMagnitude; }).ToArray(); int num2 = array2.Count((Container c) => InRange(((Component)c).transform.position, ((Component)machine).transform.position, radius.Value)); int num3 = array2.Count((Container c) => InRange(((Component)c).transform.position, ((Component)machine).transform.position, radius.Value) && UsableChest(c)); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Concat("[Machine] ", Location((Component)(object)machine), "; ", Owner((Component)(object)machine), "; status=", value, "; input-queue=", num, "/", machine.m_maxOre, "; fuel=", Number((float)getFuel.Invoke(machine, null)), "/", machine.m_maxFuel, "; fed-this-pass=", value2, "; input-full=", num >= machine.m_maxOre, "; fuel-manager=", FuelMode(machine), "; chests-in-range=", num2, "; usable-in-range=", num3, "; recipes=", string.Join(",", accepted.ToArray()))); foreach (Container item in array2.Take(8)) { float value3 = Vector3.Distance(((Component)item).transform.position, ((Component)machine).transform.position); Inventory inventory = item.GetInventory(); ItemData[] source = (ItemData[])((inventory == null) ? ((Array)new ItemData[0]) : ((Array)inventory.GetAllItems().ToArray())); int num4 = source.Where((ItemData i) => string.Equals(ItemName(i), "Wood", StringComparison.OrdinalIgnoreCase)).Sum((ItemData i) => i.m_stack); int num5 = source.Where((ItemData i) => Policy.CanFeed(ItemName(i), accepted, blocked)).Sum((ItemData i) => i.m_stack); int num6 = source.Where((ItemData i) => accepted.Contains(ItemName(i)) && blocked.Contains(ItemName(i))).Sum((ItemData i) => i.m_stack); int num7 = source.Where((ItemData i) => (Object)(object)i.m_dropPrefab == (Object)null).Sum((ItemData i) => i.m_stack); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Chest for " + ((Object)machine).GetInstanceID() + "] " + Location((Component)(object)item) + "; distance=" + Number(value3) + "m; in-range=" + InRange(((Component)item).transform.position, ((Component)machine).transform.position, radius.Value) + "; " + Owner((Component)(object)item) + "; status=" + ChestStatus(item) + "; normal-Wood=" + num4 + "; allowed-input=" + num5 + "; prohibited-input=" + num6 + "; missing-prefab=" + num7)); } if (array2.Length > 8) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Detail] " + (array2.Length - 8) + " farther chests omitted for machine " + ((Object)machine).GetInstanceID() + ".")); } } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Detailed diagnostic snapshot failed (automation remains enabled): " + ex)); } } private void Collect(Component[] machines, Container[] chests) { //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_038a: Unknown result type (might be due to invalid IL or missing references) ItemDrop[] array = Object.FindObjectsByType<ItemDrop>((FindObjectsSortMode)0); HashSet<int> seen = new HashSet<int>(); ItemDrop[] array2 = array; foreach (ItemDrop drop in array2) { int instanceID = ((Object)drop).GetInstanceID(); seen.Add(instanceID); if (!firstSeen.ContainsKey(instanceID)) { firstSeen[instanceID] = Time.unscaledTime; } else { if (Time.unscaledTime - firstSeen[instanceID] < interval.Value || !OperatingRange((Component)(object)drop) || !Owned((Component)(object)drop) || !drop.CanPickup(false)) { continue; } drop.Load(); if (!resources.Contains(ItemName(drop.m_itemData)) || drop.m_itemData.m_stack <= 0) { continue; } Component[] nearbyMachines = machines.Where((Component m) => InRange(m.transform.position, ((Component)drop).transform.position, radius.Value)).ToArray(); if (nearbyMachines.Length == 0 || !Access(((Component)drop).transform.position)) { continue; } foreach (Container item in chests.Where((Container c) => InRange(((Component)c).transform.position, ((Component)drop).transform.position, radius.Value) && nearbyMachines.Any((Component m) => InRange(((Component)c).transform.position, m.transform.position, radius.Value))).OrderBy(delegate(Container c) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: 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_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val2 = ((Component)c).transform.position - ((Component)drop).transform.position; return ((Vector3)(ref val2)).sqrMagnitude; }).ThenBy((Container c) => ((Object)c).GetInstanceID())) { if (!UsableChest(item)) { continue; } Inventory inventory = item.GetInventory(); while (remaining > 0 && drop.m_itemData.m_stack > 0 && Owned((Component)(object)drop) && OperatingRange((Component)(object)drop) && Access(((Component)drop).transform.position) && UsableChest(item) && nearbyMachines.Any((Component m) => Owned(m) && OperatingRange(m) && Access(m.transform.position))) { ItemData val = drop.m_itemData.Clone(); val.m_stack = 1; if (!inventory.CanAddItem(val, 1)) { break; } int num = inventory.GetAllItems().Sum((ItemData val2) => val2.m_stack); bool flag = false; ItemData itemData = drop.m_itemData; itemData.m_stack--; try { saveDrop.Invoke(drop, null); if (!inventory.AddItem(val)) { ItemData itemData2 = drop.m_itemData; itemData2.m_stack++; flag = true; saveDrop.Invoke(drop, null); break; } } catch { if (!flag && inventory.GetAllItems().Sum((ItemData val2) => val2.m_stack) == num) { ItemData itemData3 = drop.m_itemData; itemData3.m_stack++; saveDrop.Invoke(drop, null); } throw; } remaining--; } if (drop.m_itemData.m_stack == 0) { ZNetScene.instance.Destroy(((Component)drop).gameObject); firstSeen.Remove(instanceID); break; } if (remaining <= 0) { break; } } if (remaining <= 0) { break; } } } if (remaining > 0) { int[] array3 = firstSeen.Keys.Where((int k) => !seen.Contains(k)).ToArray(); foreach (int key in array3) { firstSeen.Remove(key); } } } } internal static class FuelEternalCompatibility { internal const string Guid = "Marfinator.FuelEternal"; internal static bool Manages(string prefab, MethodInfo fuelSetter) { if (!Chainloader.PluginInfos.TryGetValue("Marfinator.FuelEternal", out var value) || (Object)(object)value.Instance == (Object)null) { return false; } Patches patchInfo = Harmony.GetPatchInfo((MethodBase)fuelSetter); if (patchInfo == null || !patchInfo.Prefixes.Any((Patch p) => p.owner == "Marfinator.FuelEternal")) { return false; } ConfigFile config = value.Instance.Config; string text = ((prefab == "smelter") ? "AllowSmelter" : ((prefab == "blastfurnace") ? "AllowBlastFurnace" : null)); ConfigEntry<bool> val = default(ConfigEntry<bool>); bool selected = text != null && config.TryGetEntry<bool>("Smelters", text, ref val) && val.Value; ConfigEntry<string> val2 = default(ConfigEntry<string>); string customItems = (config.TryGetEntry<string>("Custom", "CustomItems", ref val2) ? val2.Value : ""); return Policy.EternalFuelSelected(prefab, selected, customItems); } } internal sealed class OwnershipPolicy { private sealed class State { internal long Owner; internal float Since; internal float NextClaim; internal bool Observed; } internal const float SettleSeconds = 3f; internal const float RetrySeconds = 30f; private readonly Dictionary<int, State> states = new Dictionary<int, State>(); internal static bool MayClaim(float localDistance, float nearestOtherDistance, float range) { if (localDistance >= 0f && localDistance <= range) { return nearestOtherDistance > localDistance + 1f; } return false; } internal string Observe(int key, long owner, bool local, bool eligible, bool mayClaim, float now, Action claim) { if (!states.TryGetValue(key, out var value)) { value = (states[key] = new State()); } if (!value.Observed || value.Owner != owner) { if (value.Observed) { value.NextClaim = Math.Max(value.NextClaim, now + 30f); } value.Owner = owner; value.Since = now; value.Observed = true; } if (!eligible) { value.Since = now; return "outside-player-range-or-access"; } if (local) { if (!(now - value.Since >= 3f)) { return "ownership-settling"; } return "eligible"; } if (!mayClaim) { return "remote-owner-open-close-chest-or-move-closer"; } if (now < value.NextClaim) { return "ownership-retry-cooldown"; } value.NextClaim = now + 30f; value.Since = now; claim(); return "ownership-claimed-waiting"; } internal void Prune(HashSet<int> live) { List<int> list = new List<int>(); foreach (int key in states.Keys) { if (!live.Contains(key)) { list.Add(key); } } foreach (int item in list) { states.Remove(item); } } internal void Clear() { states.Clear(); } } public enum OperatingMode { WhileLoaded, NearbyOnly } internal enum FuelSupply { Chests, FuelEternal, BuiltIn } internal static class Policy { internal static FuelSupply SelectFuelSupply(string prefab, bool external, bool keepFueled) { if (external) { return FuelSupply.FuelEternal; } if (keepFueled) { switch (prefab) { case "smelter": case "blastfurnace": case "piece_FrostKiln": case "piece_FrostFoundry": return FuelSupply.BuiltIn; } } return FuelSupply.Chests; } internal static void MaintainFuel(FuelSupply supply, Func<float> read, Action<float> write, int capacity) { if (supply != FuelSupply.Chests) { InitializeExternalFuel(read, delegate(float value) { write((supply == FuelSupply.BuiltIn) ? ((float)capacity) : value); }, capacity); } } internal static HashSet<string> Parse(string value) { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array = value.Split(new char[1] { ',' }); foreach (string text in array) { if (text.Trim().Length > 0) { hashSet.Add(text.Trim()); } } return hashSet; } internal static bool InRange(float distanceSquared, float radius) { return distanceSquared <= radius * radius; } internal static bool CanOperate(OperatingMode mode, bool loaded, bool playerReady, float distanceSquared, float range) { if (loaded && playerReady) { return mode switch { OperatingMode.NearbyOnly => InRange(distanceSquared, range), OperatingMode.WhileLoaded => true, _ => false, }; } return false; } internal static bool FuelFits(float current, int capacity) { if (current >= 0f) { return current + 1f <= (float)capacity; } return false; } internal static bool EternalFuelSelected(string prefab, bool selected, string customItems) { if (!selected) { return Array.IndexOf((customItems ?? "").Split(new char[1] { ',' }), prefab) >= 0; } return true; } internal static void InitializeExternalFuel(Func<float> read, Action<float> write, int capacity) { float num = read(); if (capacity > 0 && !float.IsNaN(num) && !float.IsInfinity(num) && !(num < 0f) && !(num >= (float)capacity)) { write(num); } } internal static bool CanFeed(string prefab, HashSet<string> recipes, HashSet<string> prohibited) { if (recipes.Contains(prefab)) { return !prohibited.Contains(prefab); } return false; } }