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

FREE DynamicBubbleController : 2.4.4 2.4.4

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("DynamicBubbleController", "Zim", "2.4.4")]
    [Description("PvE bubble zones with live map markers + TruePVE enforcement, an Advanced Status HUD, player self-managed zone rules via /mybase, Everlight everlasting-light control, a solar/wind electrical max-output override, and automatic water refill.")]
    public class DynamicBubbleController : RustPlugin {
        [PluginReference]
        private Plugin AdvancedStatus;
        [PluginReference]
        private Plugin TruePVE;
        private const string PermAdmin = "dynamicbubblecontroller.admin";
        private const string RadiusMarkerPrefab = "assets/prefabs/tools/map/genericradiusmarker.prefab";
        private const string VendingMarkerPrefab = "assets/prefabs/deployable/vendingmachine/vending_mapmarker.prefab";
        private const string UiMain = "PveZoneMap.UI.Main";
        private const string UiPlayer = "PveZoneMap.UI.Player";
        private const int EverlightItemsPerPage = 9;
        private Configuration _config;
        private StoredData _data;
        private readonly Dictionary<string, MarkerPair> _liveMarkers = new Dictionary<string, MarkerPair>(StringComparer.OrdinalIgnoreCase);
        private readonly HashSet<ulong> _playersInsideZone = new HashSet<ulong>();
        private readonly Dictionary<ulong, string> _lastZoneByPlayer = new Dictionary<ulong, string>();
        private readonly Dictionary<ulong, string> _selectedZoneByPlayer = new Dictionary<ulong, string>();
        private readonly Dictionary<ulong, string> _selectedTabByPlayer = new Dictionary<ulong, string>();
        private readonly Dictionary<ulong, string> _selectedPlayerTabByPlayer = new Dictionary<ulong, string>();
        private readonly HashSet<ulong> _previewEnabledPlayers = new HashSet<ulong>();
        private readonly Dictionary<ulong, PendingInputType> _pendingInputByPlayer = new Dictionary<ulong, PendingInputType>();
        private readonly Dictionary<ulong, string> _zoneNameDraftByPlayer = new Dictionary<ulong, string>();
        private readonly Dictionary<ulong, string> _statusTextDraftByPlayer = new Dictionary<ulong, string>();
        private readonly Dictionary<ulong, int> _everlightPageByPlayer = new Dictionary<ulong, int>();
        private readonly Dictionary<ulong, int> _waterPageByPlayer = new Dictionary<ulong, int>();
        private readonly Dictionary<ulong, int> _electricalPageByPlayer = new Dictionary<ulong, int>();
        private readonly Dictionary<ulong, string> _zoneSearchByPlayer = new Dictionary<ulong, string>();
        private readonly Dictionary<ulong, string> _lastStatusSignatureByPlayer = new Dictionary<ulong, string>();
        private Timer _previewTimer;
        private Timer _markerMaintenanceTimer;
        private Timer _waterRefillTimer;
        private Timer _electricalRefillTimer;
        // Harmony patches force AlwaysOn entities (Solar / Wind) to peak output
        // by intercepting their GetCurrentEnergy() — sidesteps the game's per-tick
        // wind/sun recalc that a reflection write would lose the race against.
        private Harmony _harmony;
        private const string HarmonyId = "com.zim.dynamicbubblecontroller.electricaloverride";
        private static DynamicBubbleController Instance;
        private bool _suppressMarkerRespawn;
        private readonly Dictionary<string, EverlightEntry> _everlightByShortname = new Dictionary<string, EverlightEntry>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<ItemModBurnable, ItemDefinition> _burnableDefinitions = new Dictionary<ItemModBurnable, ItemDefinition>();
        private readonly Dictionary<string, EverlightEntry> _everlightDefaults = new Dictionary<string, EverlightEntry>(StringComparer.OrdinalIgnoreCase) {
            ["Barbeque"]
            = new EverlightEntry("bbq.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.bbq")), ["Camp Fire"] = new EverlightEntry("campfire", new EverlightSettings(false, false, "dynamicbubblecontroller.light.campfire")), ["Cursed Cauldron"] = new EverlightEntry("cursedcauldron.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.cursedcauldron")), ["Chinese Lantern"] = new EverlightEntry("chineselantern.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.chineselantern")), ["Stone Fireplace"] = new EverlightEntry("fireplace.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.fireplace")), ["Furnace"] = new EverlightEntry("furnace", new EverlightSettings(false, false, "dynamicbubblecontroller.light.furnace")), ["Large Furnace"] = new EverlightEntry("furnace.large", new EverlightSettings(false, false, "dynamicbubblecontroller.light.furnace.large")), ["Jack O Lantern Angry"] = new EverlightEntry("jackolantern.angry", new EverlightSettings(false, false, "dynamicbubblecontroller.light.jackolantern.angry")), ["Jack O Lantern Happy"] = new EverlightEntry("jackolantern.happy", new EverlightSettings(false, false, "dynamicbubblecontroller.light.jackolantern.happy")), ["Carvable Pumpkin"] = new EverlightEntry("carvable.pumpkin", new EverlightSettings(false, false, "dynamicbubblecontroller.light.carvable.pumpkin")), ["Lantern"] = new EverlightEntry("lantern.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.lantern")), ["Skull Fire Pit"] = new EverlightEntry("skull_fire_pit", new EverlightSettings(false, false, "dynamicbubblecontroller.light.skull_fire_pit")), ["Small Oil Refinery"] = new EverlightEntry("refinery_small_deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.refinery_small")), ["Tuna Can Lamp"] = new EverlightEntry("tunalight.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.tunalight")), ["Hobo Barrel"] = new EverlightEntry("hobobarrel.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.hobobarrel")), ["Sky Lantern Green"] = new EverlightEntry("skylantern.skylantern.green", new EverlightSettings(false, false, "dynamicbubblecontroller.light.skylanterngreen")), ["Sky Lantern Orange"] = new EverlightEntry("skylantern.skylantern.orange", new EverlightSettings(false, false, "dynamicbubblecontroller.light.skylanternorange")), ["Sky Lantern Purple"] = new EverlightEntry("skylantern.skylantern.purple", new EverlightSettings(false, false, "dynamicbubblecontroller.light.skylanternpurple")), ["Sky Lantern Red"] = new EverlightEntry("skylantern.skylantern.red", new EverlightSettings(false, false, "dynamicbubblecontroller.light.skylanternred")), ["Sky Lantern"] = new EverlightEntry("skylantern.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.skylantern")), ["Chinese Lantern White"] = new EverlightEntry("chineselantern_white.deployed", new EverlightSettings(false, false, "dynamicbubblecontroller.light.chineselanternwhite")), ["Legacy Furnace"] = new EverlightEntry("legacy_furnace", new EverlightSettings(false, false, "dynamicbubblecontroller.light.legacyfurnace")), ["Night Vision Goggles"] = new EverlightEntry("nightvisiongoggles", new EverlightSettings(false, false, "dynamicbubblecontroller.light.nightvisiongoggles")), ["Torch"] = new EverlightEntry("torch", new EverlightSettings(false, false, "dynamicbubblecontroller.light.torch")), ["Cultist Deer Torch"] = new EverlightEntry("torch.torch.skull", new EverlightSettings(false, false, "dynamicbubblecontroller.light.torch.torch.skull")), ["Abyss Torch"] = new EverlightEntry("divertorch", new EverlightSettings(false, false, "dynamicbubblecontroller.light.divertorch")), ["Miners Hat"] = new EverlightEntry("hat.miner", new EverlightSettings(false, false, "dynamicbubblecontroller.light.hat.miner")), ["Candle Hat"] = new EverlightEntry("hat.candle", new EverlightSettings(false, false, "dynamicbubblecontroller.light.hat.candle")),
        }
        ;
        private readonly Dictionary<string, WaterEntry> _waterByShortname = new Dictionary<string, WaterEntry>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, WaterEntry> _waterDefaults = new Dictionary<string, WaterEntry>(StringComparer.OrdinalIgnoreCase) {
            // Values below are entity ShortPrefabNames (what BaseNetworkable.serverEntities
            // reports), NOT item shortnames. Confirmed via the /waterdiag command.
            ["Small Water Catcher"]
            = new WaterEntry("water_catcher_small", new WaterSettings(false, false, "dynamicbubblecontroller.water.catcher.small")), ["Large Water Catcher"] = new WaterEntry("water_catcher_large", new WaterSettings(false, false, "dynamicbubblecontroller.water.catcher.large")), ["Water Barrel"] = new WaterEntry("waterbarrel", new WaterSettings(false, false, "dynamicbubblecontroller.water.barrel")),
        }
        ;
        private readonly Dictionary<string, ElectricalEntry> _electricalByShortname = new Dictionary<string, ElectricalEntry>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, ElectricalEntry> _electricalDefaults = new Dictionary<string, ElectricalEntry>(StringComparer.OrdinalIgnoreCase) {
            // Best-guess ShortPrefabNames — verify with /electricaldiag and ping me
            // if any are off; same pattern we used for the water entities.
            ["Small Generator"]
            = new ElectricalEntry("generator.small", ElectricalKind.FuelGenerator, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.generator.small")), ["Test Generator"] = new ElectricalEntry("generator.static", ElectricalKind.AlwaysOn, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.generator.test")), ["Small Battery"] = new ElectricalEntry("small.rechargable.battery.deployed", ElectricalKind.Battery, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.battery.small")), ["Medium Battery"] = new ElectricalEntry("medium.rechargable.battery.deployed", ElectricalKind.Battery, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.battery.medium")), ["Large Battery"] = new ElectricalEntry("large.rechargable.battery.deployed", ElectricalKind.Battery, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.battery.large")), ["Solar Panel"] = new ElectricalEntry("solarpanel.large.deployed", ElectricalKind.AlwaysOn, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.solar")), ["Wind Turbine"] = new ElectricalEntry("electric.windmill.small", ElectricalKind.AlwaysOn, new ElectricalSettings(false, false, "dynamicbubblecontroller.electrical.wind")),
        }
        ;
        // Obsolete entries from earlier defaults — pruned from saved configs
        // on plugin load so the Water Controller UI doesn't list dead toggles.
        private static readonly string[] _waterObsoleteKeys = {
            "Water Purifier", "Powered Water Purifier", "Sprinkler"
        }
        ;
#region Models
        private enum PendingInputType {
            None, RenameZone, StatusText
        }
        private class ZoneEntry {
            public string Name;
            public SerializableVector3 Position;
            public float Radius = 100f;
            public float BubblePercent = 100f;
            public string ZoneColorHex = "#27AE60";
            public string OutlineColorHex = "#0B0B0B";
            public bool ShowOnMap = true;
            public bool CreateLabelMarker = false;
            public bool NoDecay = true;
            public bool AllowTeleport = true;
            public bool BlockPlayerVsPlayer = true;
            public bool BlockPlayerVsStructures = true;
            public bool BlockPlayerVsNPC = true;
            public bool BlockTrapsAndTurrets = true;
            public bool UseAdvancedStatus = true;
            public string StatusText = "PVE ZONE";
            public string StatusBackgroundColor = "#27AE60";
            public string StatusTextColor = "#FFFFFF";
            public string StatusIcon = "";
            // When set in the future, blocks player-side rule toggles (PvP /
            // Structure / NPC / Traps) until this time has passed. Bumped by
            // 6 hours whenever a player flips a rule via /mybase.
            public DateTime RuleChangeLockedUntil = DateTime.MinValue;
        }
        private const int PlayerRuleCooldownHours = 6;
        private class StoredData {
            public List<ZoneEntry> Zones = new List<ZoneEntry>();
        }
        private class SerializableVector3 {
            public float X;
            public float Y;
            public float Z;
            public SerializableVector3() {
            }
            public SerializableVector3(Vector3 position) {
                X = position.x;
                Y = position.y;
                Z = position.z;
            }
            public Vector3 ToVector3() => new Vector3(X, Y, Z);
        }
        private class MarkerPair {
            public MapMarkerGenericRadius RadiusMarker;
            public VendingMachineMapMarker LabelMarker;
        }
        private class Configuration {
            public float MarkerAlpha = 0.55f;
            public bool NotifyPlayers = true;
            public string ChatPrefix = "<color=#27AE60>[PVE ZONE]</color>: ";
            public string EnterMessage = "You entered the zone.";
            public string LeaveMessage = "You left the zone.";
            public string BlockMessage = "Damage is blocked inside this PvE zone.";
            public float EnterLeaveCheckInterval = 1.0f;
            public int StatusOrder = 12;
            public string DefaultStatusIcon = "";
            public string NoDecayStatusSuffix = "Will Not Decay";
            public string UiAccentColor = "0.15 0.65 0.25 0.95";
            public string UiPanelColor = "0.10 0.10 0.10 0.96";
            public string UiPanelSecondaryColor = "0.16 0.16 0.16 0.96";
            public string UiTextColor = "1 1 1 1";
            public string UiMutedTextColor = "0.75 0.75 0.75 1";
            public string UiDangerColor = "0.75 0.20 0.20 0.95";
            public float MarkerRefreshInterval = 30f;
            [JsonProperty(PropertyName = "Everlight entity list")]
            public Dictionary<string, EverlightSettings> EverlightList = new Dictionary<string, EverlightSettings>(StringComparer.OrdinalIgnoreCase);
            [JsonProperty(PropertyName = "Water entity list")]
            public Dictionary<string, WaterSettings> WaterList = new Dictionary<string, WaterSettings>(StringComparer.OrdinalIgnoreCase);
            [JsonProperty(PropertyName = "Water refill interval (seconds)")]
            public float WaterRefillInterval = 30f;
            [JsonProperty(PropertyName = "Electrical entity list")]
            public Dictionary<string, ElectricalSettings> ElectricalList = new Dictionary<string, ElectricalSettings>(StringComparer.OrdinalIgnoreCase);
            [JsonProperty(PropertyName = "Electrical refill interval (seconds)")]
            public float ElectricalRefillInterval = 30f;
        }
        private class EverlightEntry {
            public string ShortName;
            public EverlightSettings Settings;
            public EverlightEntry(string shortName, EverlightSettings settings) {
                ShortName = shortName;
                Settings = settings;
            }
        }
        private class EverlightSettings {
            [JsonProperty(PropertyName = "Enabled")]
            public bool Enabled;
            [JsonProperty(PropertyName = "Use permission")]
            public bool UsePermission;
            [JsonProperty(PropertyName = "Permission")]
            public string Permission;
            public EverlightSettings() {
            }
            public EverlightSettings(bool enabled, bool usePermission, string permission) {
                Enabled = enabled;
                UsePermission = usePermission;
                Permission = permission;
            }
        }
        private class WaterEntry {
            public string ShortName;
            public WaterSettings Settings;
            public WaterEntry(string shortName, WaterSettings settings) {
                ShortName = shortName;
                Settings = settings;
            }
        }
        private class WaterSettings {
            [JsonProperty(PropertyName = "Enabled")]
            public bool Enabled;
            [JsonProperty(PropertyName = "Use permission")]
            public bool UsePermission;
            [JsonProperty(PropertyName = "Permission")]
            public string Permission;
            public WaterSettings() {
            }
            public WaterSettings(bool enabled, bool usePermission, string permission) {
                Enabled = enabled;
                UsePermission = usePermission;
                Permission = permission;
            }
        }
        // Electrical entities cover three different mechanics, so each entry
        // declares which one the refill tick should apply.
        private enum ElectricalKind {
            FuelGenerator,  // top up lowgradefuel in the entity's inventory
            Battery,  // set the battery's stored charge to its max
            AlwaysOn  // force the IO output / passthrough to max
        }
        private class ElectricalEntry {
            public string ShortName;
            public ElectricalKind Kind;
            public ElectricalSettings Settings;
            public ElectricalEntry(string shortName, ElectricalKind kind, ElectricalSettings settings) {
                ShortName = shortName;
                Kind = kind;
                Settings = settings;
            }
        }
        private class ElectricalSettings {
            [JsonProperty(PropertyName = "Enabled")]
            public bool Enabled;
            [JsonProperty(PropertyName = "Use permission")]
            public bool UsePermission;
            [JsonProperty(PropertyName = "Permission")]
            public string Permission;
            public ElectricalSettings() {
            }
            public ElectricalSettings(bool enabled, bool usePermission, string permission) {
                Enabled = enabled;
                UsePermission = usePermission;
                Permission = permission;
            }
        }
#endregion
#region Oxide Hooks
        private void Init() {
            Instance = this;
            permission.RegisterPermission(PermAdmin, this);
            LoadConfigValues();
            LoadData();
            InitializeEverlight();
            InitializeWater();
            InitializeElectrical();
        }
        private void OnServerInitialized() {
            RefreshAllMarkers();
            timer.Once(1f, RefreshAllMarkers);
            ApplyTruePveMappings();
            CacheBurnables();
            _previewTimer = timer.Every(1f, DrawPreviewSpheres);
            _markerMaintenanceTimer = timer.Every(Mathf.Max(5f, _config.MarkerRefreshInterval), EnsureMarkersHealthy);
            _waterRefillTimer = timer.Every(Mathf.Max(5f, _config.WaterRefillInterval), RefillEnabledWaterEntities);
            _electricalRefillTimer = timer.Every(Mathf.Max(5f, _config.ElectricalRefillInterval), RefillEnabledElectricalEntities);
            try {
                _harmony = new Harmony(HarmonyId);
                _harmony.PatchAll(typeof(DynamicBubbleController).Assembly);
            }
            catch (Exception ex) {
                PrintError("Failed to apply Harmony patches for AlwaysOn output override: " + ex.Message);
                _harmony = null;
            }
            foreach (var player in BasePlayer.activePlayerList) {
                if (player == null || !player.IsConnected) continue;
                var zone = GetZoneAtPosition(player.transform.position);
                if (zone == null) continue;
                _playersInsideZone.Add(player.userID);
                _lastZoneByPlayer[player.userID] = zone.Name;
                CreateOrUpdateStatus(player);
            }
            timer.Every(Mathf.Max(0.5f, _config.EnterLeaveCheckInterval), CheckPlayersInZones);
        }
        private void Unload() {
            _previewTimer?.Destroy();
            _markerMaintenanceTimer?.Destroy();
            _waterRefillTimer?.Destroy();
            _electricalRefillTimer?.Destroy();
            try {
                _harmony?.UnpatchAll(HarmonyId);
            }
            catch {
            }
            _harmony = null;
            Instance = null;
            foreach (var player in BasePlayer.activePlayerList) {
                DestroyUi(player);
                DestroyPlayerUi(player);
                DeleteStatus(player);
            }
            _previewEnabledPlayers.Clear();
            _pendingInputByPlayer.Clear();
            _zoneNameDraftByPlayer.Clear();
            _statusTextDraftByPlayer.Clear();
            _everlightPageByPlayer.Clear();
            _waterPageByPlayer.Clear();
            _electricalPageByPlayer.Clear();
            _zoneSearchByPlayer.Clear();
            _lastStatusSignatureByPlayer.Clear();
            _suppressMarkerRespawn = true;
            DestroyAllMarkers();
            _suppressMarkerRespawn = false;
        }
        private void OnNewSave(string filename) {
            NextTick(() => {
                RefreshAllMarkers();
                ApplyTruePveMappings();
            }
            );
        }
        private void OnPlayerConnected(BasePlayer player) {
            timer.Once(3f, () => {
                if (player == null || !player.IsConnected) return;
                RefreshAllMarkers();
                var zone = GetZoneAtPosition(player.transform.position);
                if (zone == null) return;
                _playersInsideZone.Add(player.userID);
                _lastZoneByPlayer[player.userID] = zone.Name;
                CreateOrUpdateStatus(player);
            }
            );
        }
        private void OnPlayerDisconnected(BasePlayer player, string reason) {
            if (player == null) return;
            _playersInsideZone.Remove(player.userID);
            _selectedZoneByPlayer.Remove(player.userID);
            _selectedTabByPlayer.Remove(player.userID);
            _selectedPlayerTabByPlayer.Remove(player.userID);
            _previewEnabledPlayers.Remove(player.userID);
            _pendingInputByPlayer.Remove(player.userID);
            _zoneNameDraftByPlayer.Remove(player.userID);
            _statusTextDraftByPlayer.Remove(player.userID);
            _everlightPageByPlayer.Remove(player.userID);
            _lastZoneByPlayer.Remove(player.userID);
            _lastStatusSignatureByPlayer.Remove(player.userID);
            DestroyUi(player);
            DestroyPlayerUi(player);
            DeleteStatus(player);
        }
        private void OnEntityKill(BaseNetworkable entity) {
            if (_suppressMarkerRespawn || entity == null) return;
            var mapMarker = entity as MapMarkerGenericRadius;
            var labelMarker = entity as VendingMachineMapMarker;
            if (mapMarker == null && labelMarker == null) return;
            var zone = FindZoneByMarker(entity);
            if (zone == null) return;
            NextTick(() => {
                if (_suppressMarkerRespawn) return;
                var latestZone = FindZone(zone.Name);
                if (latestZone == null) return;
                if (!NeedsMarkerRefresh(latestZone)) return;
                RefreshZone(latestZone);
            }
            );
        }
        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
            if (entity == null || info == null) return null;
            var zone = GetRelevantZoneForDamage(entity, info);
            if (zone == null) return null;
            if (zone.NoDecay && info.damageTypes != null && info.damageTypes.Has(Rust.DamageType.Decay)) {
                info.damageTypes = new Rust.DamageTypeList();
                info.DoHitEffects = false;
                info.HitMaterial = 0;
                return true;
            }
            if (!ShouldBlockDamage(zone, entity)) return null;
            var attacker = FindResponsiblePlayer(info);
            if (attacker != null && _config.NotifyPlayers) SendReply(attacker, $"{FormatZonePrefix(zone)} Damage is blocked in this zone.");
            info.damageTypes = new Rust.DamageTypeList();
            info.DoHitEffects = false;
            info.HitMaterial = 0;
            return true;
        }
        private object OnFindBurnable(BaseOven oven) {
            if (oven == null || oven.net == null) return null;
            if (!CanEverlight(oven.ShortPrefabName, oven.OwnerID.ToString())) return null;
            foreach (var entry in _burnableDefinitions) {
                if (oven.fuelType == null || entry.Value == oven.fuelType) return ItemManager.CreateByItemID(entry.Value.itemid, 1);
            }
            return null;
        }
        private void OnLoseCondition(Item item, ref float amount) {
            if (item?.info == null) return;
            var shortname = item.info.shortname;
            if (shortname != "torch" && shortname != "torch.torch.skull" && shortname != "divertorch" && shortname != "nightvisiongoggles") return;
            var owner = item.GetOwnerPlayer();
            var playerId = owner != null ? owner.UserIDString : string.Empty;
            if (!CanEverlight(shortname, playerId)) return;
            amount = 0f;
        }
        private object OnItemUse(Item item, int amount) {
            if (item?.info?.shortname != "lowgradefuel") return null;
            var wornItem = item.parent?.parent;
            var shortname = wornItem?.info?.shortname;
            if (shortname != "hat.miner" && shortname != "hat.candle") return null;
            var playerId = item.GetRootContainer()?.GetOwnerPlayer()?.UserIDString;
            if (!CanEverlight(shortname, playerId)) return null;
            return 0;
        }
        private void OnEntityDistanceCheck(BaseOven oven, BasePlayer player, uint id, string debugName, float maximumDistance) {
            if (oven == null || player == null) return;
            if (id != 4167839872u || debugName != "SVSwitch") return;
            if (oven.Distance(player.eyes.position) > maximumDistance) return;
            if (oven.IsOn()) return;
            if (!CanEverlight(oven.ShortPrefabName, oven.OwnerID.ToString())) return;
            var hasFuel = oven.inventory?.itemList?.Any(x => x?.info != null && x.info.GetComponent<ItemModBurnable>() != null && (oven.fuelType == null || x.info == oven.fuelType));
            if (hasFuel.HasValue && hasFuel.Value) return;
            if (Interface.CallHook("OnOvenToggle", oven, player) == null && (!oven.needsBuildingPrivilegeToUse || player.CanBuild())) {
                oven.StartCooking();
            }
        }
#endregion
#region Chat Commands
        private void CmdAddZone(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            if (args.Length < 2) {
                SendReply(player, "Usage: /pvezone_add <name> <radius>");
                return;
            }
            float radius;
            if (!float.TryParse(args[1], out radius) || radius <= 0f) {
                SendReply(player, "Radius must be a number greater than 0.");
                return;
            }
            var zoneName = args[0];
            if (FindZone(zoneName) != null) {
                SendReply(player, $"A zone called '{zoneName}' already exists.");
                return;
            }
            var zone = CreateDefaultZone(zoneName, player.transform.position, radius);
            _data.Zones.Add(zone);
            SaveData();
            RefreshZone(zone);
            SyncTruePveMapping(zone);
            SelectZone(player, zone.Name);
            OpenMainUi(player);
            SendReply(player, $"Created PvE zone '{zone.Name}'.");
        }
        private void CmdRemoveZone(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            if (args.Length < 1) {
                SendReply(player, "Usage: /pvezone_remove <name>");
                return;
            }
            var zone = FindZone(args[0]);
            if (zone == null) {
                SendReply(player, $"No PvE zone found named '{args[0]}'.");
                return;
            }
            RemoveZone(zone.Name);
            OpenMainUi(player);
            SendReply(player, $"Removed PvE zone '{zone.Name}'.");
        }
        private void CmdListZones(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            if (_data.Zones.Count == 0) {
                SendReply(player, "No PvE zones have been created yet.");
                return;
            }
            SendReply(player, $"PvE zones ({_data.Zones.Count}):");
            foreach (var zone in _data.Zones) {
                var pos = zone.Position.ToVector3();
                SendReply(player, $"- {zone.Name}: radius {zone.Radius:0.#}, bubble fixed at 1% map size, pos ({pos.x:0.#}, {pos.y:0.#}, {pos.z:0.#})");
            }
        }
        private void CmdZoneHere(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            var zone = GetZoneAtPosition(player.transform.position);
            SendReply(player, zone == null ? "You are not standing inside a PvE zone." : $"You are inside PvE zone '{zone.Name}'.");
        }
        // Diagnostic: dumps ShortPrefabName + container type + inventory state
        // for any nearby water-ish entity so we can pin down exactly where the
        // refill path is failing.
        [ChatCommand("waterdiag")]
        private void CmdWaterDiag(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            var pos = player.transform.position;
            var enabledShortnames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var pair in _waterByShortname) {
                if (pair.Value?.Settings != null && pair.Value.Settings.Enabled) enabledShortnames.Add(pair.Key);
            }
            var waterDef = ItemManager.FindItemDefinition("water");
            var found = new List<string>();
            foreach (var entity in BaseNetworkable.serverEntities) {
                if (entity == null || entity.IsDestroyed) continue;
                var name = entity.ShortPrefabName ?? string.Empty;
                if (name.IndexOf("water", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("barrel", StringComparison.OrdinalIgnoreCase) < 0) continue;
                var dist = Vector3.Distance(entity.transform.position, pos);
                if (dist > 50f) continue;
                var marker = enabledShortnames.Contains(name) ? "[ON]" : "[off]";
                var typeName = entity.GetType().Name;
                var inventory = GetEntityInventory(entity);
                // Reflect the same permission gate the refill uses so it's
                // obvious when an entity is matched + enabled but skipped
                // because the owner lacks the registered permission.
                string permInfo = string.Empty;
                WaterEntry entry;
                if (_waterByShortname.TryGetValue(name, out entry) && entry?.Settings != null && entry.Settings.UsePermission) {
                    var ownerId = (entity as BaseEntity)?.OwnerID ?? 0UL;
                    var permName = entry.Settings.Permission ?? string.Empty;
                    var hasPerm = ownerId != 0UL && !string.IsNullOrEmpty(permName) && permission.UserHasPermission(ownerId.ToString(), permName);
                    permInfo = $" perm={(hasPerm ? "yes" : "NO")}({permName})";
                }
                string invInfo;
                if (inventory == null) {
                    invInfo = "no-inventory";
                }
                else {
                    var existing = waterDef != null ? inventory.FindItemByItemID(waterDef.itemid) : null;
                    invInfo = $"slots={inventory.capacity} maxStack={inventory.maxStackSize} items={inventory.itemList?.Count ?? 0} water={(existing == null ? "none" : existing.amount.ToString())}";
                }
                found.Add($"{marker} {name} <{typeName}> ({dist:0.0}m) {invInfo}{permInfo}");
            }
            if (found.Count == 0) {
                SendReply(player, "No water-related entities within 50m. Stand next to one and try again.");
                return;
            }
            found.Sort();
            SendReply(player, "Nearby water entities:\n" + string.Join("\n", found));
        }
        // Manually trigger a refill tick. Saves waiting 30s between code changes.
        [ChatCommand("waterforce")]
        private void CmdWaterForce(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            RefillEnabledWaterEntities();
            SendReply(player, "Forced water refill tick. Run /waterdiag to verify results.");
        }
        // Electrical diagnostic — same shape as /waterdiag. Lists nearby entities
        // whose ShortPrefabName contains "generator", "battery", "solar", or
        // "wind" / "mill", and reports the current refill state.
        [ChatCommand("electricaldiag")]
        private void CmdElectricalDiag(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            var pos = player.transform.position;
            var enabledShortnames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var pair in _electricalByShortname) {
                if (pair.Value?.Settings != null && pair.Value.Settings.Enabled) enabledShortnames.Add(pair.Key);
            }
            var found = new List<string>();
            foreach (var entity in BaseNetworkable.serverEntities) {
                if (entity == null || entity.IsDestroyed) continue;
                var name = entity.ShortPrefabName ?? string.Empty;
                if (name.IndexOf("generator", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("battery", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("solar", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("wind", StringComparison.OrdinalIgnoreCase) < 0 && name.IndexOf("mill", StringComparison.OrdinalIgnoreCase) < 0) continue;
                var dist = Vector3.Distance(entity.transform.position, pos);
                if (dist > 50f) continue;
                var marker = enabledShortnames.Contains(name) ? "[ON]" : "[off]";
                var typeName = entity.GetType().Name;
                ElectricalEntry entry;
                var kindLabel = _electricalByShortname.TryGetValue(name, out entry) && entry != null ? ElectricalKindLabel(entry.Kind) : "[unknown]";
                string permInfo = string.Empty;
                if (entry?.Settings != null && entry.Settings.UsePermission) {
                    var ownerId = (entity as BaseEntity)?.OwnerID ?? 0UL;
                    var permName = entry.Settings.Permission ?? string.Empty;
                    var hasPerm = ownerId != 0UL && !string.IsNullOrEmpty(permName) && permission.UserHasPermission(ownerId.ToString(), permName);
                    permInfo = $" perm={(hasPerm ? "yes" : "NO")}";
                }
                found.Add($"{marker} {name} <{typeName}> {kindLabel} ({dist:0.0}m){permInfo}");
            }
            if (found.Count == 0) {
                SendReply(player, "No electrical entities within 50m. Stand next to one and try again.");
                return;
            }
            found.Sort();
            SendReply(player, "Nearby electrical entities:\n" + string.Join("\n", found));
        }
        // Diagnostic: dump every field name + type + current value of the nearest
        // electrical entity to oxide/logs/DynamicBubbleController/dbc_dump.txt.
        // Used to discover correct field names when AlwaysOn output overrides
        // don't work because Rust renamed an internal field.
        [ChatCommand("DynamicBubbleDebug")]
        private void CmdDynamicBubbleDebug(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            DumpNearestElectrical(player);
        }
        // Server-console callable (e.g. via RCON) — no player needed.
        [ConsoleCommand("pvezonemap.dumpelectrical")]
        private void CcmdDumpElectricalServer(ConsoleSystem.Arg arg) {
            var player = arg?.Player();
            if (player != null && !HasAdminPermission(player)) return;
            DumpNearestElectrical(player);
            arg?.ReplyWith("Dumped electrical types to oxide/logs/DynamicBubbleController/dbc_dump-*.txt");
        }
        // Reports GetPassthroughAmount() for every windmill / solar on the map,
        // alongside whether the override gate fires. Used to verify the Harmony
        // patches are doing what we expect without needing to wire it up in-game.
        [ConsoleCommand("pvezonemap.electricaltest")]
        private void CcmdElectricalTest(ConsoleSystem.Arg arg) {
            var player = arg?.Player();
            if (player != null && !HasAdminPermission(player)) return;
            var sb = new System.Text.StringBuilder();
            int count = 0;
            foreach (var ent in BaseNetworkable.serverEntities) {
                if (ent == null || ent.IsDestroyed) continue;
                var be = ent as BaseEntity;
                if (be == null) continue;
                var wm = be as ElectricWindmill;
                if (wm != null) {
                    var passthrough = wm.GetPassthroughAmount(0);
                    var shouldOverride = ShouldOverrideAlwaysOn(wm);
                    ElectricalEntry entry;
                    var enabled = _electricalByShortname.TryGetValue(wm.ShortPrefabName, out entry) && entry?.Settings != null && entry.Settings.Enabled;
                    sb.AppendLine($"Windmill at {wm.transform.position} | passthrough={passthrough} | shouldOverride={shouldOverride} | guiEnabled={enabled} | windSpeed={wm.serverWindSpeed:F3}");
                    if (++count >= 20) break;
                    continue;
                }
                var sp = be as SolarPanel;
                if (sp != null) {
                    var passthrough = sp.GetPassthroughAmount(0);
                    var shouldOverride = ShouldOverrideAlwaysOn(sp);
                    ElectricalEntry entry;
                    var enabled = _electricalByShortname.TryGetValue(sp.ShortPrefabName, out entry) && entry?.Settings != null && entry.Settings.Enabled;
                    sb.AppendLine($"Solar at {sp.transform.position} | passthrough={passthrough} | shouldOverride={shouldOverride} | guiEnabled={enabled} | maxOut={sp.maximalPowerOutput}");
                    if (++count >= 20) break;
                }
            }
            if (count == 0) sb.AppendLine("No windmills or solar panels found on the map.");
            var report = sb.ToString();
            arg?.ReplyWith(report);
            PrintWarning(report);
        }
        private void DumpNearestElectrical(BasePlayer player) {
            // Find ONE representative of each electrical-entity shortname known
            // to the plugin. Dump fields + properties for each, one section per
            // type, so the same log lets us compare wind / solar / battery / etc.
            var wantedShortnames = new HashSet<string>(_electricalByShortname.Keys, StringComparer.OrdinalIgnoreCase);
            var samples = new Dictionary<string, BaseEntity>(StringComparer.OrdinalIgnoreCase);
            foreach (var entity in BaseNetworkable.serverEntities) {
                if (entity == null || entity.IsDestroyed) continue;
                var n = entity.ShortPrefabName ?? string.Empty;
                if (!wantedShortnames.Contains(n)) continue;
                if (samples.ContainsKey(n)) continue;
                // one sample per type is enough
                var be = entity as BaseEntity;
                if (be == null) continue;
                samples[n] = be;
                if (samples.Count >= wantedShortnames.Count) break;
            }
            if (samples.Count == 0) {
                if (player != null) SendReply(player, "No electrical entities found on the map to dump.");
                else PrintWarning("No electrical entities found on the map to dump.");
                return;
            }
            const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
            var sb = new System.Text.StringBuilder();
            sb.AppendLine($"=== DBC dump generated {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===");
            sb.AppendLine($"Found {samples.Count} of {wantedShortnames.Count} known electrical types on the map.");
            sb.AppendLine();
            foreach (var kvp in samples.OrderBy(x => x.Key, StringComparer.Ordinal)) {
                var ent = kvp.Value;
                sb.AppendLine("--------------------------------------------------------");
                sb.AppendLine($"Shortname     : {ent.ShortPrefabName}");
                sb.AppendLine($"Concrete type : {ent.GetType().FullName}");
                sb.AppendLine($"Inheritance   :");
                for (var t = ent.GetType();
                t != null && t != typeof(object);
                t = t.BaseType) sb.AppendLine($"   - {t.FullName}");
                sb.AppendLine();
                sb.AppendLine("Fields:");
                var seen = new HashSet<string>();
                for (var t = ent.GetType();
                t != null && t != typeof(object);
                t = t.BaseType) {
                    foreach (var f in t.GetFields(flags)) {
                        if (!seen.Add(f.Name)) continue;
                        string val;
                        try {
                            val = (f.GetValue(ent) ?? "null").ToString();
                        }
                        catch {
                            val = "<read-error>";
                        }
                        if (val.Length > 120) val = val.Substring(0, 120) + "...";
                        sb.AppendLine($"  [{t.Name}] {f.FieldType.Name} {f.Name} = {val}");
                    }
                }
                sb.AppendLine();
                sb.AppendLine("Properties:");
                var seenP = new HashSet<string>();
                for (var t = ent.GetType();
                t != null && t != typeof(object);
                t = t.BaseType) {
                    foreach (var p in t.GetProperties(flags)) {
                        if (!seenP.Add(p.Name)) continue;
                        if (p.GetIndexParameters().Length > 0) continue;
                        string val;
                        try {
                            val = (p.GetValue(ent, null) ?? "null").ToString();
                        }
                        catch {
                            val = "<read-error>";
                        }
                        if (val.Length > 120) val = val.Substring(0, 120) + "...";
                        sb.AppendLine($"  [{t.Name}] {p.PropertyType.Name} {p.Name} = {val}");
                    }
                }
                sb.AppendLine();
                sb.AppendLine("Methods (declared, walking inheritance):");
                var seenM = new HashSet<string>();
                for (var t = ent.GetType();
                t != null && t != typeof(object);
                t = t.BaseType) {
                    foreach (var m in t.GetMethods(flags | System.Reflection.BindingFlags.DeclaredOnly)) {
                        if (m.IsSpecialName) continue;
                        // skip property getters/setters
                        var pars = string.Join(", ", m.GetParameters().Select(pp => pp.ParameterType.Name + " " + pp.Name));
                        var sig = $"{m.ReturnType.Name} {m.Name}({pars})";
                        if (!seenM.Add(sig)) continue;
                        sb.AppendLine($"  [{t.Name}] {sig}");
                    }
                }
                sb.AppendLine();
            }
            // Note which types weren't found on the map
            var missing = wantedShortnames.Except(samples.Keys, StringComparer.OrdinalIgnoreCase).ToList();
            if (missing.Count > 0) {
                sb.AppendLine("--------------------------------------------------------");
                sb.AppendLine("Types not found on this map (no sample to dump):");
                foreach (var m in missing) sb.AppendLine($"  - {m}");
            }
            LogToFile("dbc_dump", sb.ToString(), this);
            if (player != null) SendReply(player, $"Dumped {samples.Count} electrical type(s) to oxide/logs/DynamicBubbleController/dbc_dump.txt");
            else PrintWarning($"Dumped {samples.Count} electrical type(s) to oxide/logs/DynamicBubbleController/dbc_dump.txt");
        }
        [ChatCommand("electricalforce")]
        private void CmdElectricalForce(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            RefillEnabledElectricalEntities();
            SendReply(player, "Forced electrical refill tick. Run /electricaldiag to verify results.");
        }
        [ChatCommand("bubble")]
        private void CmdZoneUi(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            // Always land on the Hub so the user picks which module to enter.
            // Power users can still use /light to skip straight to Light Controller.
            _selectedTabByPlayer[player.userID] = "hub";
            if (_data.Zones.Count > 0 && GetSelectedZone(player) == null) SelectZone(player, _data.Zones[0].Name);
            OpenMainUi(player);
        }
        [ChatCommand("light")]
        private void CmdLightUi(BasePlayer player, string command, string[] args) {
            if (!HasAdminPermission(player)) return;
            _selectedTabByPlayer[player.userID] = "everlight";
            OpenMainUi(player);
        }
        [ChatCommand("mybase")]
        private void CmdPlayerZoneRules(BasePlayer player, string command, string[] args) {
            if (player == null || !player.IsConnected) return;
            ZoneEntry zone;
            string reason;
            if (!TryGetPlayerControlledZone(player, out zone, out reason)) {
                SendReply(player, reason);
                return;
            }
            _zoneNameDraftByPlayer[player.userID] = zone.Name;
            OpenPlayerRulesUi(player, zone);
        }
        private void CmdCancelInput(BasePlayer player, string command, string[] args) {
            if (player == null) return;
            
// ... [Code preview truncated due to length. Full source in attached file] ...

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