Иконка ресурса

FREE SmartSmelt : 1.2.4 1.2.4

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("SmartSmelt", "SeesAll", "1.2.4")]
    [Description("Preset-based accelerated smelting with instant sync, adaptive scaling, and smart fuel pull.")]
    public class SmartSmelt : RustPlugin {
#region Configuration
        private const string ItemWood = "wood";
        private const string ItemCharcoal = "charcoal";
        private const string ItemLowGradeFuel = "lowgradefuel";
        private const string ItemMetalFragments = "metal.fragments";
        private const string ItemSulfur = "sulfur";
        private const string ItemMetalRefined = "metal.refined";
        private const string ItemCrudeOil = "crude.oil";
        private const string ItemMetalOre = "metal.ore";
        private const string ItemSulfurOre = "sulfur.ore";
        private const string ItemHqMetalOre = "hq.metal.ore";
        private static readonly string[] DefaultOvenWhitelist = {
            "furnace", "furnace.large", "refinery", "electric.furnace"
        }
        ;
        private Configuration _config;
        private readonly List<string> _cachedWhitelistFragmentsLower = new List<string>();
        private bool _cachedWhitelistAllowsAnyRefinery;
        private ItemDefinition _charcoalDefinition;
        private ItemDefinition _woodDefinition;
        private ItemDefinition _lowGradeFuelDefinition;
        private ItemDefinition _metalFragmentsDefinition;
        private ItemDefinition _sulfurDefinition;
        private ItemDefinition _metalRefinedDefinition;
        private ItemDefinition _crudeOilDefinition;
        private ItemDefinition _metalOreDefinition;
        private ItemDefinition _sulfurOreDefinition;
        private ItemDefinition _hqMetalOreDefinition;
        private class Configuration {
            public bool Enabled = true;
            public int ConfigVersion = 3;
            public string Preset = "10x";
            public string PresetOptions = "2x, 3x, 5x, 10x, 25x, 50x, 100x, 1000x, instant";
            public bool AutoTuneEnabled = true;
            public int AveragePopulation = 100;
            public string AutoTuneBias = "Balanced";
            public bool AutoTuneWriteToConfig = false;
            public bool EnableOreSplitting = true;
            public bool EnableMixingTableScaling = true;
            public List<string> OvenWhitelist = new List<string>(DefaultOvenWhitelist);
            public bool ForceStartCookingOnToggle = true;
            public bool VerboseTrackingLogs = false;
            public bool VerboseCycleLogs = false;
            public bool AdaptiveScaling = true;
            public int AdaptiveMinOvensPerTick = 100;
            public int AdaptiveMaxOvensPerTick = 800;
            public bool DynamicTickInterval = true;
            public float DynamicMinGlobalLoopInterval = 0.10f;
            public float DynamicMaxGlobalLoopInterval = 0.25f;
            public int DynamicLowOvenCount = 100;
            public int DynamicHighOvenCount = 1200;
            public float FixedGlobalLoopInterval = 0.25f;
            public bool AutoPullFuelFromPlayer = true;
            public float AutoPullFuelBufferPercent = 0f;
            public bool ReducedWoodCostEnabled = false;
            public float WoodCostScale = 0.5f;
            public bool EnableElectricFurnaceNativeScaling = true;
            public float ElectricFurnaceThroughputScale = 2.0f;
            public float ElectricFurnaceCycleSpeedScale = 0.5f;
            public bool ProduceCharcoalFromFuel = true;
            public float CharcoalPerWood = 0.75f;
            public string CharcoalOverflowMode = "Skip";
            public bool Debug = false;
        }
        protected override void LoadDefaultConfig() {
            _config = new Configuration();
            RebuildRuntimeCaches();
            SaveConfig();
        }
        protected override void LoadConfig() {
            base.LoadConfig();
            bool shouldSaveConfig = false;
            if (MigrateConfigSchema()) {
                base.LoadConfig();
            }
            try {
                _config = Config.ReadObject<Configuration>() ?? new Configuration();
            }
            catch {
                PrintWarning("Config was invalid; generating a new one.");
                _config = new Configuration();
                shouldSaveConfig = true;
            }
            if (NormalizeConfig()) shouldSaveConfig = true;
            if (shouldSaveConfig) SaveConfig();
            RebuildRuntimeCaches();
            RefreshEffectiveScheduling(allowWriteToConfig : true);
        }
        protected override void SaveConfig() => Config.WriteObject(_config, true);
        protected override void LoadDefaultMessages() {
            lang.RegisterMessages(new Dictionary<string, string> {
                ["NoPermission"]
                = "<color=#ff6b6b>SmartSmelt:</color> You don't have permission to use this command.", ["DebugTitle"] = "<color=#9be7ff>SmartSmelt Debug</color> v{0}", ["DebugLine1"] = "Enabled: {0} | Preset: {1} | Multiplier: {2}x", ["DebugLine2"] = "Tracked ovens: {0} | Ovens/tick cap: {1} (Adaptive: {2})", ["DebugLine3"] = "Global loop interval: {0:0.000}s | Desired: {1:0.000}s (Dynamic: {2})", ["ConsoleDebug"] = "SmartSmelt Debug v{0} | Enabled={1} Preset={2} Mult={3}x | Tracked={4} Cap={5} | Interval={6:0.000}s Desired={7:0.000}s", ["InfoLine1"] = "SmartSmelt v{0} | Preset={1} | Enabled={2} | OreSplitting={3} | AutoPullFuel={4} (Buffer={5:0.###}%) | WoodCost={6:0.##}x", ["InfoLine2"] = "AutoTune={0} (AvgPop={1}, Bias={2}, WriteToConfig={3})", ["InfoLine3"] = "TrackedOvens={0} | PerTickCap={1} (Adaptive={2}) | LoopInterval={3:0.000}s (Desired={4:0.000}s, Dynamic={5})", ["ConsoleInfo"] = "SmartSmelt v{0} | Preset={1} | Enabled={2} | AutoPullFuel={3} (Buffer={4:0.###}%) | WoodCost={11:0.##}x\nTrackedOvens={5} | PerTickCap={6} (Adaptive={7}) | LoopInterval={8:0.000}s (Desired={9:0.000}s, Dynamic={10})"
            }
            , this);
        }
        private string Msg(string key, string userId = null) => lang.GetMessage(key, this, userId);
        private bool MigrateConfigSchema() {
            try {
                var raw = Config.ReadObject<Dictionary<string, object>>();
                if (raw == null) return false;
                int version = 0;
                if (raw.TryGetValue("ConfigVersion", out var vObj)) {
                    try {
                        version = Convert.ToInt32(vObj);
                    }
                    catch {
                        version = 0;
                    }
                }
                bool changed = false;
                if (version < 1) {
                    if (raw.Remove("AutoPullFuelTargetAmount")) changed = true;
                    if (raw.Remove("AutoPullFuelMaxPullPerInteraction")) changed = true;
                    if (raw.Remove("AutoPullFuelExactNeeded")) changed = true;
                    raw["ConfigVersion"] = 1;
                    changed = true;
                }
                if (version < 2) {
                    // Version 1 used FuelUsageMultiplier. Preserve its effective behavior
                    // when translating to the clearer opt-in reduced-wood-cost settings.
                    if (raw.TryGetValue("FuelUsageMultiplier", out var legacyFuelMultiplier)) {
                        try {
                            float legacyScale = Mathf.Clamp(Convert.ToSingle(legacyFuelMultiplier), 0f, 1f);
                            if (!raw.ContainsKey("WoodCostScale")) raw["WoodCostScale"] = legacyScale;
                            if (!raw.ContainsKey("ReducedWoodCostEnabled")) raw["ReducedWoodCostEnabled"] = legacyScale < 0.999f;
                        }
                        catch {
                            PrintWarning("Could not read legacy FuelUsageMultiplier; using the new wood-cost defaults.");
                        }
                        raw.Remove("FuelUsageMultiplier");
                    }
                    raw["ConfigVersion"] = 2;
                    changed = true;
                }
                if (version < 3) {
                    if (!raw.ContainsKey("EnableMixingTableScaling")) raw["EnableMixingTableScaling"] = true;
                    raw["ConfigVersion"] = 3;
                    changed = true;
                }
                if (!raw.ContainsKey("ConfigVersion")) {
                    raw["ConfigVersion"] = 3;
                    changed = true;
                }
                if (!changed) return false;
                Config.WriteObject(raw, true);
                return true;
            }
            catch {
                return false;
            }
        }
        private void RebuildRuntimeCaches() {
            _cachedWhitelistFragmentsLower.Clear();
            _cachedWhitelistAllowsAnyRefinery = false;
            if (_config?.OvenWhitelist != null) {
                for (int i = 0; i < _config.OvenWhitelist.Count; i++) {
                    var fragment = (_config.OvenWhitelist[i] ?? string.Empty).Trim().ToLowerInvariant();
                    if (fragment.Length == 0) continue;
                    _cachedWhitelistFragmentsLower.Add(fragment);
                    if (fragment.Contains("refinery_small")) _cachedWhitelistAllowsAnyRefinery = true;
                }
            }
            _charcoalDefinition = ItemManager.FindItemDefinition(ItemCharcoal);
            _woodDefinition = ItemManager.FindItemDefinition(ItemWood);
            _lowGradeFuelDefinition = ItemManager.FindItemDefinition(ItemLowGradeFuel);
            _metalFragmentsDefinition = ItemManager.FindItemDefinition(ItemMetalFragments);
            _sulfurDefinition = ItemManager.FindItemDefinition(ItemSulfur);
            _metalRefinedDefinition = ItemManager.FindItemDefinition(ItemMetalRefined);
            _crudeOilDefinition = ItemManager.FindItemDefinition(ItemCrudeOil);
            _metalOreDefinition = ItemManager.FindItemDefinition(ItemMetalOre);
            _sulfurOreDefinition = ItemManager.FindItemDefinition(ItemSulfurOre);
            _hqMetalOreDefinition = ItemManager.FindItemDefinition(ItemHqMetalOre);
        }
        private ItemDefinition GetCachedItemDefinition(string shortname) {
            if (string.IsNullOrEmpty(shortname)) return null;
            switch (shortname) {
                case ItemCharcoal : return _charcoalDefinition;
                case ItemWood : return _woodDefinition;
                case ItemLowGradeFuel : return _lowGradeFuelDefinition;
                case ItemMetalFragments : return _metalFragmentsDefinition;
                case ItemSulfur : return _sulfurDefinition;
                case ItemMetalRefined : return _metalRefinedDefinition;
                case ItemCrudeOil : return _crudeOilDefinition;
                case ItemMetalOre : return _metalOreDefinition;
                case ItemSulfurOre : return _sulfurOreDefinition;
                case ItemHqMetalOre : return _hqMetalOreDefinition;
                default : return ItemManager.FindItemDefinition(shortname);
            }
        }
        private bool NormalizeConfig() {
            bool changed = false;
            if (_config == null) {
                _config = new Configuration();
                return true;
            }
            // Version 3 introduced mixing-table preset scaling; bumping the version forces a
            // save so the new key appears without replacing existing administrator settings.
            if (_config.ConfigVersion != 3) {
                _config.ConfigVersion = 3;
                changed = true;
            }
            if (string.IsNullOrWhiteSpace(_config.PresetOptions)) {
                _config.PresetOptions = "2x, 3x, 5x, 10x, 25x, 50x, 100x, 1000x, instant";
                changed = true;
            }
            if (_config.AutoPullFuelBufferPercent < 0f) {
                _config.AutoPullFuelBufferPercent = 0f;
                changed = true;
            }
            else if (_config.AutoPullFuelBufferPercent > 10f) {
                _config.AutoPullFuelBufferPercent = 10f;
                changed = true;
            }
            if (_config.WoodCostScale < 0f) {
                _config.WoodCostScale = 0f;
                changed = true;
            }
            else if (_config.WoodCostScale > 1f) {
                _config.WoodCostScale = 1f;
                changed = true;
            }
            var presetKey = ResolvePresetKey(_config.Preset);
            if (!PresetDefinitions.ContainsKey(presetKey)) {
                PrintWarning($"Unknown Preset '{_config.Preset}', defaulting to {DefaultPresetKey}.");
                presetKey = DefaultPresetKey;
                changed = true;
            }
            if (!string.Equals(_config.Preset, presetKey, StringComparison.Ordinal)) {
                _config.Preset = presetKey;
                changed = true;
            }
            if (_config.AveragePopulation < 0) {
                _config.AveragePopulation = 0;
                changed = true;
            }
            if (_config.AveragePopulation > 5000) {
                _config.AveragePopulation = 5000;
                changed = true;
            }
            var bias = (_config.AutoTuneBias ?? "Balanced").Trim();
            if (bias.Length == 0) bias = "Balanced";
            if (bias.Equals("balanced", StringComparison.OrdinalIgnoreCase)) bias = "Balanced";
            else if (bias.Equals("performance", StringComparison.OrdinalIgnoreCase)) bias = "Performance";
            else if (bias.Equals("responsiveness", StringComparison.OrdinalIgnoreCase)) bias = "Responsiveness";
            else {
                PrintWarning($"Unknown AutoTuneBias '{_config.AutoTuneBias}', defaulting to Balanced.");
                bias = "Balanced";
                changed = true;
            }
            if (!string.Equals(_config.AutoTuneBias, bias, StringComparison.Ordinal)) {
                _config.AutoTuneBias = bias;
                changed = true;
            }
            if (_config.OvenWhitelist == null) {
                _config.OvenWhitelist = new List<string>(DefaultOvenWhitelist);
                changed = true;
            }
            else {
                var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                var cleaned = new List<string>();
                foreach (var s in _config.OvenWhitelist) {
                    var v = (s ?? string.Empty).Trim();
                    if (v.Length == 0) continue;
                    if (seen.Add(v)) cleaned.Add(v);
                }
                if (cleaned.Count == 0) cleaned.AddRange(DefaultOvenWhitelist);
                if (cleaned.Count != _config.OvenWhitelist.Count) changed = true;
                _config.OvenWhitelist = cleaned;
            }
            if (_config.CharcoalPerWood < 0f) {
                _config.CharcoalPerWood = 0f;
                changed = true;
            }
            else if (_config.CharcoalPerWood > 2f) {
                _config.CharcoalPerWood = 2f;
                changed = true;
            }
            if (_config.ElectricFurnaceThroughputScale < 0.25f) {
                _config.ElectricFurnaceThroughputScale = 0.25f;
                changed = true;
            }
            else if (_config.ElectricFurnaceThroughputScale > 10f) {
                _config.ElectricFurnaceThroughputScale = 10f;
                changed = true;
            }
            if (_config.ElectricFurnaceCycleSpeedScale < 0.1f) {
                _config.ElectricFurnaceCycleSpeedScale = 0.1f;
                changed = true;
            }
            else if (_config.ElectricFurnaceCycleSpeedScale > 2f) {
                _config.ElectricFurnaceCycleSpeedScale = 2f;
                changed = true;
            }
            var com = (_config.CharcoalOverflowMode ?? "Skip").Trim();
            if (com.Length == 0) com = "Skip";
            com = com.Equals("Pause", StringComparison.OrdinalIgnoreCase) ? "Pause" : "Skip";
            if (_config.CharcoalOverflowMode != com) {
                _config.CharcoalOverflowMode = com;
                changed = true;
            }
            return changed;
        }
        private bool _effAdaptiveScaling;
        private int _effAdaptiveMinOvensPerTick;
        private int _effAdaptiveMaxOvensPerTick;
        private bool _effDynamicTickInterval;
        private float _effDynamicMinGlobalLoopInterval;
        private float _effDynamicMaxGlobalLoopInterval;
        private int _effDynamicLowOvenCount;
        private int _effDynamicHighOvenCount;
        private float _effFixedGlobalLoopInterval;
        private readonly Dictionary<ulong, ulong> _pendingFuelRecalcPlayers = new Dictionary<ulong, ulong>();
        private readonly Dictionary<ulong, bool> _pendingAutomationToggleStates = new Dictionary<ulong, bool>();
        private void RefreshEffectiveScheduling(bool allowWriteToConfig) {
            if (_config == null) return;
            _effAdaptiveScaling = _config.AdaptiveScaling;
            _effAdaptiveMinOvensPerTick = _config.AdaptiveMinOvensPerTick;
            _effAdaptiveMaxOvensPerTick = _config.AdaptiveMaxOvensPerTick;
            _effDynamicTickInterval = _config.DynamicTickInterval;
            _effDynamicMinGlobalLoopInterval = _config.DynamicMinGlobalLoopInterval;
            _effDynamicMaxGlobalLoopInterval = _config.DynamicMaxGlobalLoopInterval;
            _effDynamicLowOvenCount = _config.DynamicLowOvenCount;
            _effDynamicHighOvenCount = _config.DynamicHighOvenCount;
            _effFixedGlobalLoopInterval = _config.FixedGlobalLoopInterval;
            if (!_config.AutoTuneEnabled) return;
            int pop = Mathf.Clamp(_config.AveragePopulation, 0, 5000);
            int[] anchors = {
                10, 25, 50, 100, 200, 300, 400, 500
            }
            ;
            int anchor = anchors[0];
            int bestDist = Math.Abs(pop - anchor);
            for (int i = 1; i < anchors.Length; i++) {
                int d = Math.Abs(pop - anchors[i]);
                if (d < bestDist) {
                    bestDist = d;
                    anchor = anchors[i];
                }
            }
            int baseMin;
            int baseMax;
            float baseMinInterval;
            float baseMaxInterval;
            int baseLowCount;
            int baseHighCount;
            float baseFixed;
            switch (anchor) {
                case 10 : baseMin = 300;
                baseMax = 900;
                baseMinInterval = 0.05f;
                baseMaxInterval = 0.15f;
                baseLowCount = 50;
                baseHighCount = 300;
                baseFixed = 0.15f;
                break;
                case 25 : baseMin = 250;
                baseMax = 900;
                baseMinInterval = 0.07f;
                baseMaxInterval = 0.18f;
                baseLowCount = 75;
                baseHighCount = 400;
                baseFixed = 0.18f;
                break;
                case 50 : baseMin = 200;
                baseMax = 800;
                baseMinInterval = 0.10f;
                baseMaxInterval = 0.22f;
                baseLowCount = 100;
                baseHighCount = 600;
                baseFixed = 0.22f;
                break;
                case 100 : baseMin = 160;
                baseMax = 700;
                baseMinInterval = 0.12f;
                baseMaxInterval = 0.25f;
                baseLowCount = 150;
                baseHighCount = 900;
                baseFixed = 0.25f;
                break;
                case 200 : baseMin = 140;
                baseMax = 650;
                baseMinInterval = 0.14f;
                baseMaxInterval = 0.28f;
                baseLowCount = 200;
                baseHighCount = 1100;
                baseFixed = 0.28f;
                break;
                case 300 : baseMin = 120;
                baseMax = 600;
                baseMinInterval = 0.15f;
                baseMaxInterval = 0.32f;
                baseLowCount = 250;
                baseHighCount = 1300;
                baseFixed = 0.32f;
                break;
                case 400 : baseMin = 110;
                baseMax = 550;
                baseMinInterval = 0.16f;
                baseMaxInterval = 0.35f;
                baseLowCount = 300;
                baseHighCount = 1500;
                baseFixed = 0.35f;
                break;
                default : baseMin = 100;
                baseMax = 500;
                baseMinInterval = 0.18f;
                baseMaxInterval = 0.40f;
                baseLowCount = 350;
                baseHighCount = 1700;
                baseFixed = 0.40f;
                break;
            }
            float ovensFactor = 1.0f;
            float intervalFactor = 1.0f;
            switch (ResolvePresetKey(_config.Preset)) {
                case "2x" : ovensFactor = 1.15f;
                intervalFactor = 0.90f;
                break;
                case "3x" : ovensFactor = 1.10f;
                intervalFactor = 0.92f;
                break;
                case "5x" : ovensFactor = 1.05f;
                intervalFactor = 0.95f;
                break;
                case "10x" : ovensFactor = 1.00f;
                intervalFactor = 1.00f;
                break;
                case "25x" : ovensFactor = 0.85f;
                intervalFactor = 1.10f;
                break;
                case "50x" : ovensFactor = 0.75f;
                intervalFactor = 1.20f;
                break;
                case "100x" : ovensFactor = 0.65f;
                intervalFactor = 1.30f;
                break;
                case "1000x" : ovensFactor = 0.45f;
                intervalFactor = 1.60f;
                break;
                case "instant" : ovensFactor = 0.35f;
                intervalFactor = 1.80f;
                break;
            }
            string bias = (_config.AutoTuneBias ?? "Balanced").Trim();
            if (bias.Equals("Responsiveness", StringComparison.OrdinalIgnoreCase)) {
                ovensFactor *= 1.10f;
                intervalFactor *= 0.90f;
            }
            else if (bias.Equals("Performance", StringComparison.OrdinalIgnoreCase)) {
                ovensFactor *= 0.90f;
                intervalFactor *= 1.10f;
            }
            int tunedMin = Mathf.Clamp(Mathf.RoundToInt(baseMin * ovensFactor), 25, 5000);
            int tunedMax = Mathf.Clamp(Mathf.RoundToInt(baseMax * ovensFactor), tunedMin, 10000);
            float tunedMinInterval = Mathf.Clamp(baseMinInterval * intervalFactor, 0.03f, 2f);
            float tunedMaxInterval = Mathf.Clamp(baseMaxInterval * intervalFactor, tunedMinInterval, 2f);
            float tunedFixed = Mathf.Clamp(baseFixed * intervalFactor, 0.03f, 2f);
            _effAdaptiveScaling = true;
            _effAdaptiveMinOvensPerTick = tunedMin;
            _effAdaptiveMaxOvensPerTick = tunedMax;
            _effDynamicTickInterval = true;
            _effDynamicMinGlobalLoopInterval = tunedMinInterval;
            _effDynamicMaxGlobalLoopInterval = tunedMaxInterval;
            _effDynamicLowOvenCount = baseLowCount;
            _effDynamicHighOvenCount = baseHighCount;
            _effFixedGlobalLoopInterval = tunedFixed;
            if (!allowWriteToConfig || !_config.AutoTuneWriteToConfig) return;
            bool changed = false;
            if (_config.AdaptiveScaling != _effAdaptiveScaling) {
                _config.AdaptiveScaling = _effAdaptiveScaling;
                changed = true;
            }
            if (_config.AdaptiveMinOvensPerTick != _effAdaptiveMinOvensPerTick) {
                _config.AdaptiveMinOvensPerTick = _effAdaptiveMinOvensPerTick;
                changed = true;
            }
            if (_config.AdaptiveMaxOvensPerTick != _effAdaptiveMaxOvensPerTick) {
                _config.AdaptiveMaxOvensPerTick = _effAdaptiveMaxOvensPerTick;
                changed = true;
            }
            if (_config.DynamicTickInterval != _effDynamicTickInterval) {
                _config.DynamicTickInterval = _effDynamicTickInterval;
                changed = true;
            }
            if (!Mathf.Approximately(_config.DynamicMinGlobalLoopInterval, _effDynamicMinGlobalLoopInterval)) {
                _config.DynamicMinGlobalLoopInterval = _effDynamicMinGlobalLoopInterval;
                changed = true;
            }
            if (!Mathf.Approximately(_config.DynamicMaxGlobalLoopInterval, _effDynamicMaxGlobalLoopInterval)) {
                _config.DynamicMaxGlobalLoopInterval = _effDynamicMaxGlobalLoopInterval;
                changed = true;
            }
            if (_config.DynamicLowOvenCount != _effDynamicLowOvenCount) {
                _config.DynamicLowOvenCount = _effDynamicLowOvenCount;
                changed = true;
            }
            if (_config.DynamicHighOvenCount != _effDynamicHighOvenCount) {
                _config.DynamicHighOvenCount = _effDynamicHighOvenCount;
                changed = true;
            }
            if (!Mathf.Approximately(_config.FixedGlobalLoopInterval, _effFixedGlobalLoopInterval)) {
                _config.FixedGlobalLoopInterval = _effFixedGlobalLoopInterval;
                changed = true;
            }
            if (changed) SaveConfig();
            RefreshCachedPreset();
        }
        private void QueueFuelRecalcNextTick(BaseOven oven, BasePlayer player) {
            if (!_config.Enabled) return;
            if (!_config.AutoPullFuelFromPlayer) return;
            if (oven == null || oven.IsDestroyed) return;
            if (player == null || !player.IsConnected) return;
            ulong id = 0ul;
            if (oven.net != null) id = oven.net.ID.Value;
            if (id == 0ul) {
                NextTick(() => {
                    if (oven == null || oven.IsDestroyed) return;
                    if (player == null || !player.IsConnected) return;
                    TryAutoPullFuel(oven, player);
                }
                );
                return;
            }
            ulong playerId = player.userID;
            if (_pendingFuelRecalcPlayers.TryGetValue(id, out var pendingPlayerId)) {
                // Multiple requests from the same player still collapse into one scan. A
                // different player makes ownership ambiguous, so skip automatic withdrawal.
                if (pendingPlayerId != playerId) _pendingFuelRecalcPlayers[id] = 0ul;
                return;
            }
            _pendingFuelRecalcPlayers[id] = playerId;
            NextTick(() => {
                if (!_pendingFuelRecalcPlayers.TryGetValue(id, out var resolvedPlayerId)) return;
                _pendingFuelRecalcPlayers.Remove(id);
                if (resolvedPlayerId == 0ul || resolvedPlayerId != playerId) return;
                if (oven == null || oven.IsDestroyed) return;
                if (player == null || !player.IsConnected) return;
                TryAutoPullFuel(oven, player);
            }
            );
        }
#endregion
#region Presets
        private struct PresetTuning {
            public float CycleSeconds;
            public int MaxTotalConsumedPerCycle;
            public int MaxConsumedPerStackPerCycle;
        }
        private struct PresetDefinition {
            public PresetTuning Tuning;
            public int Multiplier;
        }
        private const string DefaultPresetKey = "10x";
        private const string InstantPresetKey = "instant";
        private static readonly Dictionary<string, string> PresetAliases = new Dictionary<string, string>(StringComparer.Ordinal) {
            ["2"]
            = "2x", ["3"] = "3x", ["5"] = "5x", ["10"] = "10x", ["25"] = "25x", ["50"] = "50x", ["100"] = "100x", ["1000"] = "1000x", ["inst"] = InstantPresetKey
        }
        ;
        private static readonly Dictionary<string, PresetDefinition> PresetDefinitions = new Dictionary<string, PresetDefinition>(StringComparer.Ordinal) {
            ["2x"]
            = MakePreset(0.5f, 20, 10, 2), ["3x"] = MakePreset(0.5f, 30, 15, 3), ["5x"] = MakePreset(0.5f, 50, 25, 5), ["10x"] = MakePreset(0.5f, 100, 50, 10), ["25x"] = MakePreset(0.25f, 500, 250, 25), ["50x"] = MakePreset(0.2f, 1000, 500, 50), ["100x"] = MakePreset(0.1f, 2000, 1000, 100), ["1000x"] = MakePreset(0.05f, 20000, 10000, 1000), [InstantPresetKey] = MakePreset(0.1f, int.MaxValue, int.MaxValue, 1000000)
        }
        ;
        private static PresetDefinition MakePreset(float cycleSeconds, int maxTotalPerCycle, int maxPerStackPerCycle, int multiplier) {
            return new PresetDefinition {
                Tuning = new PresetTuning {
                    CycleSeconds = cycleSeconds, MaxTotalConsumedPerCycle = maxTotalPerCycle, MaxConsumedPerStackPerCycle = maxPerStackPerCycle
                }
                , Multiplier = multiplier
            }
            ;
        }
        private static string ResolvePresetKey(string preset) {
            var key = (preset ?? DefaultPresetKey).Trim().ToLowerInvariant();
            if (key.Length == 0) key = DefaultPresetKey;
            if (PresetAliases.TryGetValue(key, out var canonical)) key = canonical;
            return key;
        }
        private PresetTuning GetPreset(string preset) {
            return PresetDefinitions.TryGetValue(ResolvePresetKey(preset), out var def) ? def.Tuning : PresetDefinitions[DefaultPresetKey].Tuning;
        }
        private int GetMultiplier(string preset) {
            return PresetDefinitions.TryGetValue(ResolvePresetKey(preset), out var def) ? def.Multiplier : PresetDefinitions[DefaultPresetKey].Multiplier;
        }
        private string _cachedPresetKey = null;
        private PresetTuning _cachedPresetTuning;
        private int _cachedPresetMultiplier = 10;
        private bool _cachedIsInstantPreset = false;
        private void RefreshCachedPreset() {
            string key = ResolvePresetKey(_config?.Preset);
            if (key == _cachedPresetKey) return;
            _cachedPresetKey = key;
            _cachedPresetTuning = GetPreset(key);
            _cachedPresetMultiplier = GetMultiplier(key);
            _cachedIsInstantPreset = string.Equals(key, InstantPresetKey, StringComparison.Ordinal);
        }
        private PresetTuning GetEffectivePresetForKind(PresetTuning basePreset, OvenKind kind) {
            if (_config == null || kind != OvenKind.ElectricFurnace || !_config.EnableElectricFurnaceNativeScaling) return basePreset;
            if (basePreset.MaxTotalConsumedPerCycle == int.MaxValue || basePreset.MaxConsumedPerStackPerCycle == int.MaxValue) return basePreset;
            var tuned = basePreset;
            tuned.CycleSeconds = Mathf.Clamp(basePreset.CycleSeconds * _config.ElectricFurnaceCycleSpeedScale, 0.02f, 2f);
            long totalScaled = (long)Mathf.RoundToInt(basePreset.MaxTotalConsumedPerCycle * _config.ElectricFurnaceThroughputScale);
            long stackScaled = (long)Mathf.RoundToInt(basePreset.MaxConsumedPerStackPerCycle * _config.ElectricFurnaceThroughputScale);
            if (totalScaled < 1L) totalScaled = 1L;
            if (stackScaled < 1L) stackScaled = 1L;
            if (totalScaled > int.MaxValue) totalScaled = int.MaxValue;
            if (stackScaled > int.MaxValue) stackScaled = int.MaxValue;
            tuned.MaxTotalConsumedPerCycle = (int)totalScaled;
            tuned.MaxConsumedPerStackPerCycle = (int)stackScaled;
            return tuned;
        }
#endregion
#region State
        private readonly Dictionary<ulong, OvenTracker> _active = new Dictionary<ulong, OvenTracker>();
        private readonly Dictionary<ulong, string> _cachedOvenPrefabNamesLower = new Dictionary<ulong, string>();
        private readonly HashSet<Item> _customMovesInProgress = new HashSet<Item>();
        private readonly HashSet<ulong> _pendingPostMoveRebalances = new HashSet<ulong>();
        private readonly HashSet<ulong> _postMoveRebalancesInProgress = new HashSet<ulong>();
        private int _activeVersion = 0;
        private int _tmpTrackerIdsBuiltForVersion = -1;
        private void MarkActiveChanged() {
            _activeVersion++;
            _tmpTrackerIdsBuiltForVersion = -1;
            if (_activeVersion == int.MaxValue) _activeVersion = 0;
        }
        private Timer _globalTimer;
        private float _currentGlobalLoopInterval = 0.25f;
        private bool _rescheduleQueued;
        private float _queuedRescheduleInterval = DefaultGlobalLoopInterval;
        private readonly List<ulong> _tmpTrackerIds = new List<ulong>(256);
        private readonly Stack<List<Item>> _itemListPool = new Stack<List<Item>>();
        private const int DefaultMaxOvensPerGlobalTick = 200;
        private const float DefaultGlobalLoopInterval = 0.25f;
        private const float MinGlobalLoopInterval = 0.05f;
        private const float MaxGlobalLoopInterval = 2f;
        private const float TimerRescheduleThreshold = 0.05f;
        private const float ForceStartCookingDelaySeconds = 0.2f;
        private const int MaxOffCyclesBeforeEvict = 50;
        private const int StartupScanEntitiesPerSlice = 500;
        private const float StartupScanSliceDelaySeconds = 0.02f;
        private int _globalCursor = 0;
        private IEnumerator<BaseNetworkable> _startupScanEnumerator;
        private int _startupScanTracked;
        private int GetOvensPerGlobalTickCap(int trackedCount) {
            if (!_effAdaptiveScaling) return DefaultMaxOvensPerGlobalTick;
            int cap = trackedCount;
            if (cap < _effAdaptiveMinOvensPerTick) cap = _effAdaptiveMinOvensPerTick;
            if (cap > _effAdaptiveMaxOvensPerTick) cap = _effAdaptiveMaxOvensPerTick;
            return cap;
        }
        private float ComputeGlobalLoopInterval(int trackedCount) {
            if (_config == null) return DefaultGlobalLoopInterval;
            if (!_effDynamicTickInterval) return Mathf.Clamp(_effFixedGlobalLoopInterval, MinGlobalLoopInterval, MaxGlobalLoopInterval);
            float min = Mathf.Clamp(_effDynamicMinGlobalLoopInterval, MinGlobalLoopInterval, MaxGlobalLoopInterval);
            float max = Mathf.Clamp(_effDynamicMaxGlobalLoopInterval, MinGlobalLoopInterval, MaxGlobalLoopInterval);
            if (max < min) {
                var t = min;
                min = max;
                max = t;
            }
            int low = Math.Max(0, _effDynamicLowOvenCount);
            int high = Math.Max(low + 1, _effDynamicHighOvenCount);
            float t01 = Mathf.InverseLerp(low, high, trackedCount);
            return Mathf.Lerp(min, max, t01);
        }
        private void EnsureGlobalTimer(float interval) {
            interval = Mathf.Clamp(interval, MinGlobalLoopInterval, MaxGlobalLoopInterval);
            if (_globalTimer == null) {
                _currentGlobalLoopInterval = interval;
                _globalTimer = timer.Every(_currentGlobalLoopInterval, GlobalTick);
                return;
            }
            if (Mathf.Abs(_currentGlobalLoopInterval - interval) < TimerRescheduleThreshold) return;
            // Always remember the latest requested interval so a queued reschedule uses it
            // instead of the value captured when the reschedule was first queued.
            _queuedRescheduleInterval = interval;
            if (_rescheduleQueued) return;
            _rescheduleQueued = true;
            timer.Once(0f, () => {
                _rescheduleQueued = false;
                _globalTimer?.Destroy();
                _globalTimer = null;
                _currentGlobalLoopInterval = Mathf.Clamp(_queuedRescheduleInterval, MinGlobalLoopInterval, MaxGlobalLoopInterval);
                _globalTimer = timer.Every(_currentGlobalLoopInterval, GlobalTick);
            }
            );
        }
        private const string PermAdmin = "smartsmelt.admin";
        private const string PermDebug = "smartsmelt.debug";
        private class OvenTracker {
            public BaseOven Oven;
            public OvenKind Kind;
            public bool GateOnWood;
            public float NextTickAt;
            public int Cycles;
            public int OffCycles;
            public float CharcoalRemainder;
            public float FuelDebt;
            public float LastBalanceTime;
            public readonly List<Item> InputsBuffer = new List<Item>(8);
        }
#endregion
#region Hooks
        private void OnServerInitialized() {
            // ItemManager is fully initialized at this point. Rebuild cached definitions so
            // a very early config load cannot leave the runtime with unresolved item entries.
            RebuildRuntimeCaches();
            RefreshEffectiveScheduling(allowWriteToConfig : true);
            _active.Clear();
            _cachedOvenPrefabNamesLower.Clear();
            _pendingFuelRecalcPlayers.Clear();
            _pendingAutomationToggleStates.Clear();
            _customMovesInProgress.Clear();
            _pendingPostMoveRebalances.Clear();
            _postMoveRebalancesInProgress.Clear();
            MarkActiveChanged();
            permission.RegisterPermission(PermAdmin, this);
            permission.RegisterPermission(PermDebug, this);
            _globalTimer?.Destroy();
            _globalTimer = null;
            EnsureGlobalTimer(ComputeGlobalLoopInterval(0));
            timer.Once(1f, ScanAndTrackRunningOvens);
        }
        private void Unload() {
            _globalTimer?.Destroy();
            _globalTimer = null;
            DisposeStartupScanEnumerator();
            _active.Clear();
            _cachedOvenPrefabNamesLower.Clear();
            _pendingFuelRecalcPlayers.Clear();
            _pendingAutomationToggleStates.Clear();
            _customMovesInProgress.Clear();
            _pendingPostMoveRebalances.Clear();
            _postMoveRebalancesInProgress.Clear();
            MarkActiveChanged();
        }
        private void OnItemAddedToContainer(ItemContainer container, Item item) {
            if (_config == null || !_config.Enabled) return;
            if (container == null || item?.info == null) return;
            var oven = container.entityOwner as BaseOven;
            if (oven == null || oven.IsDestroyed || container != oven.inventory) return;
            if (!IsWhitelistedSmeltingOven(oven)) return;
            var kind = GetKind(oven);
            string shortname = item.info.shortname;
            if (kind == OvenKind.SmallRefinery) {
                if (shortname != ItemCrudeOil) return;
            }
            else if (!IsOreShortname(shortname)) {
                return;
            }
            // Capture the item type that caused the hook. For mixed-ore furnaces this lets
            // the fallback spread only the newly-added type into its own and empty slots,
            // without disturbing ore types the player already arranged.
            QueuePostMoveInputRebalance(oven, null, item.info, item.skin, kind == OvenKind.SmallRefinery || _config.EnableOreSplitting);
        }
        private void OnEntityKill(BaseNetworkable ent) {
            var oven = ent as BaseOven;
            if (oven == null) return;
            StopTracking(oven);
        }
        private void OnOvenToggle(BaseOven oven, BasePlayer player) {
            if (!_config.Enabled || oven == null) return;
            if (!IsWhitelistedSmeltingOven(oven)) {
                if (_config.Debug) Puts($"Ignoring oven (not whitelisted): {oven.ShortPrefabName}");
                return;
            }
            NextTick(() => {
                if (oven == null || oven.IsDestroyed) return;
                if (oven.IsOn()) {
                    StartTracking(oven);
                    if (_config.ForceStartCookingOnToggle) {
                        timer.Once(ForceStartCookingDelaySeconds, () => {
                            if (oven == null || oven.IsDestroyed) return;
                            if (!oven.IsOn()) {
                                if (_config.Debug) Puts("Toggle ON but oven still OFF after 0.2s. Attempting StartCooking().");
                                TryStartCooking(oven);
                            }
                        }
                        );
                    }
                }
                else {
                    StopTracking(oven);
                }
            }
            );
        }
        private void OnMixingTableToggle(MixingTable table, BasePlayer player) {
            if (_config == null || !_config.Enabled || !_config.EnableMixingTableScaling) return;
            if (table == null || table.IsDestroyed || table.IsOn()) return;
            // Rust calculates the selected recipe and its full duration as the table starts.
            // Apply the preset on the next tick so those vanilla values are ready first.
            NextTick(() => {
                if (table == null || table.IsDestroyed || !table.IsOn()) return;
                RefreshCachedPreset();
                float multiplier = Mathf.Max(1f, _cachedPresetMultiplier);
                if (multiplier <= 1f || table.RemainingMixTime <= 0f) return;
                table.RemainingMixTime /= multiplier;
                table.TotalMixTime /= multiplier;
                table.SendNetworkUpdateImmediate();
                // Vanilla ticks once per second. Reschedule sub-second recipes so high and
                // Instant presets do not wait for an unnecessary full vanilla tick.
                if (table.RemainingMixTime < 1f) {
                    table.CancelInvoke(table.TickMix);
                    table.Invoke(table.TickMix, Mathf.Max(0.01f, table.RemainingMixTime));
                }
                if (_config.Debug) Puts($"Mixing table started at {ResolvePresetKey(_config.Preset)} ({multiplier:0}x); remaining time is {table.RemainingMixTime:0.###}s.");
            }
            );
        }
        private void OnEntityFlagsChanged(BaseEntity entity, BaseEntity.Flags oldFlags, BaseEntity.Flags newFlags) {
            if (!_config.Enabled || entity == null) return;
            bool oldOn = (oldFlags & BaseEntity.Flags.On) != 0;
            bool newOn = (newFlags & BaseEntity.Flags.On) != 0;
            if (oldOn == newOn) return;
            HandleAutomationOvenToggle(entity, newOn);
        }
        private void HandleAutomationOvenToggle(BaseEntity entity, bool newOnState) {
            var oven = entity as BaseOven;
            if (oven == null) return;
            if (!IsWhitelistedSmeltingOven(oven)) return;
            ulong id = oven.net != null ? oven.net.ID.Value : 0ul;
            if (id == 0ul) {
                NextTick(() => {
                    if (oven == null || oven.IsDestroyed) return;
                    if (newOnState && oven.IsOn()) StartTracking(oven);
                    else if (!newOnState && !oven.IsOn()) StopTracking(oven);
                }
                );
                return;
            }
            if (_pendingAutomationToggleStates.ContainsKey(id)) {
                _pendingAutomationToggleStates[id] = newOnState;
                return;
            }
            _pendingAutomationToggleStates[id] = newOnState;
            NextTick(() => {
                if (!_pendingAutomationToggleStates.TryGetValue(id, out var desiredState)) return;
                _pendingAutomationToggleStates.Remove(id);
                if (oven == null || oven.IsDestroyed) return;
                if (desiredState && oven.IsOn()) StartTracking(oven);
                else if (!desiredState && !oven.IsOn()) StopTracking(oven);
            }
            );
        }
        private void OnOvenCook(BaseOven oven) {
            EnsureTrackedFromCook(oven);
        }
        private void EnsureTrackedFromCook(BaseOven oven) {
            if (!_config.Enabled || oven == null || oven.IsDestroyed) return;
            if (!oven.IsOn()) return;
            if (!IsWhitelistedSmeltingOven(oven)) return;
            StartTracking(oven);
        }
#region Commands
        [ChatCommand("ss.debug")]
        private void CmdSmartSmeltDebug(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermDebug)) {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            int tracked = _active?.Count ?? 0;
            int cap = GetOvensPerGlobalTickCap(tracked);
            float desiredInterval = ComputeGlobalLoopInterval(tracked);
            string presetName = _config?.Preset ?? "unknown";
            int mult = GetMultiplier(presetName);
            player.ChatMessage(string.Format(Msg("DebugTitle", player.UserIDString), Version));
            player.ChatMessage(string.Format(Msg("DebugLine1", player.UserIDString), _config?.Enabled ?? false, presetName, mult));
            player.ChatMessage(string.Format(Msg("DebugLine2", player.UserIDString), tracked, cap, _effAdaptiveScaling));
            player.ChatMessage(string.Format(Msg("DebugLine3", player.UserIDString), _currentGlobalLoopInterval, desiredInterval, _config?.DynamicTickInterval ?? false));
        }
        [ConsoleCommand("ss.debug")]
        private void CCmdSmartSmeltDebug(ConsoleSystem.Arg arg) {
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermDebug)) {
                arg.ReplyWith(Msg("NoPermission", player.UserIDString));
                return;
            }
            int tracked = _active?.Count ?? 0;
            int cap = GetOvensPerGlobalTickCap(tracked);
            float desiredInterval = ComputeGlobalLoopInterval(tracked);
            string presetName = _config?.Preset ?? "unknown";
            int mult = GetMultiplier(presetName);
            arg.ReplyWith(string.Format(Msg("ConsoleDebug"), Version, _config?.Enabled ?? false, presetName, mult, tracked, cap, _currentGlobalLoopInterval, desiredInterval));
        }
        [ChatCommand("ss.info")]
        private void CmdSmartSmeltInfo(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermDebug)) {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            var desiredInterval = ComputeGlobalLoopInterval(_active.Count);
            var cap = GetOvensPerGlobalTickCap(_active.Count);
            SendReply(player, string.Format(Msg("InfoLine1", player.UserIDString), Version, _config.Preset, _config.Enabled, _config.EnableOreSplitting, _config.AutoPullFuelFromPlayer, _config.AutoPullFuelBufferPercent, GetWoodCostScale()));
            SendReply(player, string.Format(Msg("InfoLine2", player.UserIDString), _config.AutoTuneEnabled, _config.AveragePopulation, _config.AutoTuneBias, _config.AutoTuneWriteToConfig));
            SendReply(player, string.Format(Msg("InfoLine3", player.UserIDString), _active.Count, 
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху