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

FREE RRemove : 1.6.7 1.6.7

Нет прав для скачивания
C#:
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using Pool = Facepunch.Pool;
using Rust;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("RRemove", "RustInnovate", "1.6.7")]
    [Description("Режимное удаление построек и предметов с возвратом ресурсов и GUI-индикатором")]
    public sealed class RRemove : RustPlugin {
#region Конфигурация
        private ConfigData configData = new();
        private sealed class ConfigData {
            // CHANGE: Единый раздел «Основные» разбит на два независимых:
            // CHANGE: настройки ремува и настройки апгрейда (функции ортогональны).[JsonProperty("Настройки ремува")]
            public RemoveSettings Remove = new();
            [JsonProperty("Настройки апгрейда")]
            public UpgradeSettings Upgrade = new();
            [JsonProperty("Настройки скинов построек")]
            public SkinSettings Skins = new();
            [JsonProperty("GUI")]
            public GuiSettings Gui = new();
        }
        /// <summary>
        /// Настройки режима удаления: длительность режима, проценты возврата ресурсов/объектов,
        /// правила доступа к чужим объектам, лимит времени на удаление, чёрный список shortname,
        /// активация клавишей и встроенная система рейд/комбат-блокировки./// </summary>
        internal sealed class RemoveSettings {
            [JsonProperty("Время действия режима удаления")]
            public float ModeDuration = 30f;
            [JsonProperty("Процент возвращаемых ресурсов с построек (Максимум 1.0 - это 100%)")]
            public float BuildingRefundPercent = 1.0f;
            [JsonProperty("Процент возвращаемых ресурсов с Items (Максимум 1.0 - это 100%)")]
            public float ItemsRefundPercent = 1.0f;
            [JsonProperty("Процент выпадающих ресурсов (не вещей) с удаляемых ящиков (Максимум 1.0 - это 100%)")]
            public float ContainerResourceDropPercent = 1.0f;
            [JsonProperty("Включить запрет на удаление объекта если в его инвентаре есть предметы")]
            public bool BlockWithItems = true;
            [JsonProperty("Разрешить удаление чужих объектов при наличии авторизации в шкафу")]
            public bool AllowOthersWithAuth = false;
            [JsonProperty("Разрешить удаление собственных объектов без авторизации в шкафу")]
            public bool AllowOwnWithoutAuth = false;
            [JsonProperty("Разрешить удаление объектов соклановцев")]
            public bool AllowClanmates = false;
            [JsonProperty("Разрешить удаление обьектов команде игрока (Team)")]
            public bool AllowTeam = true;
            [JsonProperty("Включить возрат объектов (При удаление объектов(сундуки, печки и тд.) будет возращать объект а не ресурсы)")]
            public bool ReturnDeployables = true;
            [JsonProperty("Включить потерю прочности предмета (Item lose condition)")]
            public bool ItemConditionLoss = true;
            [JsonProperty("Сколько процентов прочности теряет предмет при удалении (дефолт - 10 (250 - 10% = 25))")]
            public float ConditionLossPercent = 10f;
            [JsonProperty("Включить запрет на удаление объекта для игрока после истечения N времени указанным в конфигурации")]
            public bool EnablePlaceTimeLimit = false;
            [JsonProperty("Время на запрет удаление объекта после истечения указаного времени (в секундах)")]
            public float PlaceTimeLimit = 3600.0f;
            [JsonProperty("Привилегия игнорирования запрета удаления объектов какие были установлены N времени назад (Если включено)")]
            public string IgnorePermission = "rremove.ignore";
            // CHANGE: 1.6.6 — дефолт выверен по ПРЕФАБАМ энтити (запрос владельца): нефтекубрик —
            // CHANGE: refinery_small_deployed (small.oil.refinery — shortname ПРЕДМЕТА); имена
            // CHANGE: предметов (chineselantern/tunalight/lantern) легитимны — IsBlockedShortname
            // CHANGE: с 1.6.6 сравнивает и их.Единый источник: инициализатор + миграция.internal static readonly string[] DefaultBlockedShortnames =
            {
                "industrial.furnace.large",  // Large Industrial Furnace
                "furnace.large",  // Large Furnace
                "chineselantern",  // Chinese Lantern (энтити: chineselantern.deployed)
                "campfire",  // Camp Fire
                "skull_fire_pit",  // Skull Fire Pit
                "refinery_small_deployed",  // Small Oil Refinery (предмет: small.oil.refinery)
                "tunalight",  // Tuna Can Lamp (энтити: tunalight.deployed)
                "lantern",  // Lantern (энтити: lantern.deployed)
            }
            ;
            // CHANGE: Снимок дефолта 1.6.3 — для канонической миграции списков,
            // CHANGE: которые владелец не редактировал вручную.internal static readonly string[] LegacyBlockedShortnames =
            {
                "industrial.furnace.large", "furnace.large", "chineselantern", "chineselanternwhite", "campfire", "skull_fire_pit", "small.oil.refinery", "tunalight", "lantern",
            }
            ;
            // CHANGE: ObjectCreationHandling.Replace — иначе Newtonsoft дописывает массив из файла
            // CHANGE: в список-по-умолчанию при каждой перезагрузке, и пустые строки множатся.[JsonProperty("Список запрещенных для удаления Entity shortname (Не Item)", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<string> BlockedShortnames = new(DefaultBlockedShortnames);
            [JsonProperty("Клавиша активации режима удаления")]
            public string ActivationKey = "z";
            // CHANGE: Открытие радиального меню без бинда: перехват клиентской консольной команды
            // CHANGE: кнопки.Работает только для клавиш со своей консольной командой:
            // CHANGE: F = inventory.lighttoggle (включает фонарик — конфликт), X = vehicle.swapseats.// CHANGE: По умолчанию OFF — активация строго по Z через одноразовый клиентский бинд
            // CHANGE (bind z rremove.menu): клавиша Z консольной команды не имеет и сервером
            // CHANGE без клиентского бинда не ловится.[JsonProperty("Кнопка открытия радиального меню без бинда (X | F | OFF)")]
            public string MenuOpenButton = "OFF";
            [JsonProperty("Показывать подсказку кнопки открытия рад меню при взятии киянки или плана постройки в руки")]
            public bool ShowBindHint = true;
            [JsonProperty("Запретить использование радиального меню и функции ремува/апгрейда в рейд-блоке (true — через внешний плагин, false — встроенная система)")]
            public bool BlockInRaidZone = true;
            [JsonProperty("Запретить использование радиального меню и функции ремува/апгрейда в комбат-блоке (true — через внешний плагин, false — встроенная система)")]
            public bool BlockInCombatBlock = true;
            // CHANGE: Встроенная система блокировки (используется при false выше): блок по взрыву
            // CHANGE: по строению (рейдер + владелец) и по ПВП-урону (обе стороны).[JsonProperty("Длительность встроенной блокировки рейда (секунды)")]
            public int BuiltinRaidBlockDuration = 300;
            [JsonProperty("Длительность встроенной блокировки комбата (секунды)")]
            public int BuiltinCombatBlockDuration = 300;
        }
        /// <summary>
        /// Настройки режима автоапгрейда построек: длительность режима, включение функции,
        /// привилегии использования и апгрейда без затрат, требование авторизации в шкафу./// </summary>
        internal sealed class UpgradeSettings {
            // CHANGE: Таймер режима апгрейда: каждое успешное действие сбрасывает отсчёт,
            // CHANGE: бездействие приводит к выключению режима.[JsonProperty("Время действия режима апгрейда (сбрасывается при каждом апгрейде)")]
            public float UpgradeModeDuration = 30f;
            // CHANGE: Блок настроек автоапгрейда (строительный план + удар киянкой).[JsonProperty("Включить автоапгрейд (строительный план и удар киянкой)")]
            public bool EnableAutoUpgrade = true;
            [JsonProperty("Привилегия использования автоапгрейда")]
            public string UpgradePermission = PermUpgrade;
            [JsonProperty("Привилегия апгрейда без затрат ресурсов (для администраторов)")]
            public string NoCostPermission = PermNoCost;
            // CHANGE: Требование шкафа: без авторизации в шкафу зоны автоапгрейд не работает.[JsonProperty("Требовать наличие шкафа для автоапгрейда (план+киянка)")]
            public bool UpgradeRequireCupboard = true;
        }
        // CHANGE: Скины построек (DLC): скин выбирается для КАЖДОГО типа апгрейда отдельно и
        // CHANGE: распространяется и на киянку, и на строительный план (единый UpgradeState.Skin).// CHANGE: Бесплатно скины никому не выдаются: без владения DLC они доступны только
        // CHANGE: по привилегии FreeSkinPermission (для администраторов)./// <summary>
        /// Настройки скинов построек (DLC): списки скинов по каждому типу апгрейда
        /// (выбор — пейджер Q/E у пунктов апгрейда в главном меню), привилегия бесплатного
        /// доступа и автозаполнение./// Инварианты: скин применяется только если пара (тип, ID скина) есть в данных префаба
        /// блока, а игрок владеет Steam-предметом скина / имеет привилегию / скин alwaysUnlock./// </summary>
        internal sealed class SkinSettings {
            [JsonProperty("Включить подменю скинов построек (работает вместе с автоапгрейдом)")]
            public bool EnableBuildSkins = true;
            // CHANGE: Ванильный пейджер: у пункта типа апгрейда в главном меню клиент рисует
            // CHANGE: кнопки Q/E (nextCommand/prevCommand) — листают скины типа без подменю.[JsonProperty("Листание скинов Q/E прямо в главном меню у типов апгрейда")]
            public bool EnableInlineSkinPaging = true;
            // CHANGE: true — при листании Q/E колесо мгновенно переоткрывается с новым скином
            // CHANGE: (клиент сам закрывает pie при исполнении команды — иначе обновление не показать);
            // CHANGE: false — колесо не переоткрывается (без мерцания и сброса курсора), скин показывается тостом.[JsonProperty("Переоткрывать радиальное меню при листании скинов Q/E (false — без мерцания, скин тостом)")]
            public bool ReopenMenuOnSkinPage = true;
            [JsonProperty("Привилегия бесплатного использования любых скинов построек (для администраторов, мимо проверки владения DLC)")]
            public string FreeSkinPermission = PermSkinsFree;
            // CHANGE: 1.6.0 — выбор цвета для скинов с палитрой (контейнеры): перед запуском
            // CHANGE: режима апгрейда открывается радиальное меню выбора цвета (колесо баллончика).[JsonProperty("Включить выбор цвета для скинов с палитрой (контейнеры) — перед запуском режима апгрейда")]
            public bool EnableColourSelection = true;
            [JsonProperty("Показывать пункт «Случайный цвет» в меню выбора цвета")]
            public bool ShowRandomColour = true;
            [JsonProperty("Иконка пункта «Случайный цвет»")]
            public string RandomColourIcon = "assets/icons/palette.png";
            // CHANGE: 1.6.6 — возвращён сплошной circle_closed.png: у контурного circle_open
            // CHANGE: тонируется только кольцо, центр остаётся белым — заливки цветом нет.// CHANGE: На наведении клиент всё равно перекрашивает иконку своим hover-цветом —
            // CHANGE: цвет в этот момент несут сектор и центральное превью.[JsonProperty("Иконка цветных кружков (окрашивается цветом палитры)")]
            public string ColourIcon = "assets/icons/circle_closed.png";
            // CHANGE: 1.6.4 — настройка «Режим отображения цвета пункта меню» (ColourOptionMode)
            // CHANGE: удалена по требованию владельца: colorMode пункта всегда CustomColor (0) —
            // CHANGE: иконка кружка окрашивается цветом палитры (AddPieOption).// CHANGE: Названия скинов больше не хранятся в конфиге — только ID; имена живут
            // CHANGE: в файлах локализации (lang/<язык>/RRemove.json, ключи Skin{ID скина}).[JsonProperty("Автоматически добавлять найденные в игре скины в списки ниже (названия берутся из локализации lang/<язык>/RRemove.json — ключи Skin{ID скина}; выключите, чтобы полностью управлять списками вручную)")]
            public bool AutoFillSkins = true;
            [JsonProperty("Скины — Дерево (ID скина — Steam item definition ID скина постройки; запись с ID 0 — «Без скина»; найденные ID: консольная команда rremove.skins)", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<SkinEntry> WoodSkins = new() {
                new SkinEntry()
            }
            ;
            [JsonProperty("Скины — Камень", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<SkinEntry> StoneSkins = new() {
                new SkinEntry()
            }
            ;
            [JsonProperty("Скины — Металл", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<SkinEntry> MetalSkins = new() {
                new SkinEntry()
            }
            ;
            [JsonProperty("Скины — МВК", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<SkinEntry> HqmSkins = new() {
                new SkinEntry()
            }
            ;
        }
        // CHANGE: Поля «Название» и «Иконка» удалены из конфига: имя скина — ключ локализации
        // CHANGE: Skin{ID скина} (создаётся автоматически при старте, правится в lang-файле),
        // CHANGE: иконки скинов не использовались (автозаполнение всегда писало пустую строку).// CHANGE: Старый конфиг читается без потерь: Newtonsoft игнорирует неизвестные поля,
        // CHANGE: лишние ключи исчезнут из файла при следующем сохранении конфига./// <summary>
        /// Запись скина постройки в конфиге: только Steam item definition ID скина./// Отображаемое имя — из локализации: ключ Skin{ID скина}, для ID 0 — «Без скина»
        /// (ключ PieSkinNone).Инвариант: имя всегда непустое — SkinDisplayName даёт
        /// fallback на имя реестра и «ID {id}»./// </summary>
        internal sealed class SkinEntry {
            [JsonProperty("ID скина (0 — без скина)")]
            public ulong SkinId = 0UL;
        }
#region Классы настроек GUI (каждая панель — отдельный класс)
        internal sealed class GuiSettings {
            [JsonProperty("Включить GUI-индикатор режима")]
            public bool Enabled = true;
            [JsonProperty("Корневая панель")]
            public RootPanelConfig Root = new();
            [JsonProperty("Заголовок")]
            public TitleConfig Title = new();
            [JsonProperty("Таймер")]
            public TimerConfig Timer = new();
            [JsonProperty("Подсказка")]
            public HintConfig Hint = new();
            [JsonProperty("Радиальное меню (нативное pie-меню клиента)")]
            public PieMenuSettings Pie = new();
        }
        internal sealed class PieMenuSettings {
            // CHANGE: Валидная иконка киянки из клиента — hammer.icon.png (hammer.png не существует).[JsonProperty("Иконка — простой ремув")]
            public string DefaultIcon = "assets/prefabs/weapons/hammer/hammer.icon.png";
            [JsonProperty("Иконка — админский ремув")]
            public string AdminIcon = "assets/icons/explosion_sprite.png";
            [JsonProperty("Иконка — удаление всех строений")]
            public string AllIcon = "assets/icons/clear.png";
            [JsonProperty("Иконка — закрыть меню")]
            public string CloseIcon = "assets/icons/close.png";
            [JsonProperty("Иконка — апгрейд: дерево")]
            public string WoodIcon = "assets/icons/level_wood.png";
            [JsonProperty("Иконка — апгрейд: камень")]
            public string StoneIcon = "assets/icons/level_stone.png";
            [JsonProperty("Иконка — апгрейд: металл")]
            public string MetalIcon = "assets/icons/level_metal.png";
            [JsonProperty("Иконка — апгрейд: МВК")]
            public string HqmIcon = "assets/icons/level_top.png";
        }
        internal sealed class RootPanelConfig {
            [JsonProperty("Высота")]
            public float Height = 64f;
            [JsonProperty("Ширина")]
            public float Width = 240f;
            [JsonProperty("Вверх/вниз")]
            public float OffsetY = 297f;
            [JsonProperty("Влево/вправо")]
            public float OffsetX = 0f;
            [JsonProperty("Цвет фона")]
            public string BgColor = "0.16 0.16 0.17 0.85";
            [JsonProperty("Цвет фона блока заголовка")]
            public string HeaderBgColor = "0.20 0.62 0.90 0.95";
            [JsonProperty("Высота блока заголовка")]
            public float HeaderHeight = 24f;
        }
        // CHANGE: Тексты заголовка и подсказки перенесены из конфига в локализацию (ключи GuiTitle/GuiHint).internal sealed class TitleConfig
        {
            [JsonProperty("Размер шрифта")]
            public int FontSize = 13;
            [JsonProperty("Цвет текста")]
            public string Color = "1 1 1 1";
        }
        internal sealed class TimerConfig {
            [JsonProperty("Размер шрифта")]
            public int FontSize = 18;
            [JsonProperty("Цвет текста")]
            public string Color = "0.25 0.69 1 1";
        }
        internal sealed class HintConfig {
            [JsonProperty("Высота")]
            public float Height = 18f;
            [JsonProperty("Размер шрифта")]
            public int FontSize = 11;
            [JsonProperty("Цвет текста")]
            public string Color = "0.75 0.75 0.75 1";
        }
#endregion Классы настроек GUI
        /// <inheritdoc />
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                // CHANGE: Миграция v1→v2: раздел «Основные» разбит на два.Переносим старые
                // CHANGE: значения ДО основной десериализации, иначе они обнулятся на дефолты.MigrateLegacyConfig();
                configData = Config.ReadObject<ConfigData>();
                if (configData == null) throw new Exception();
                // CHANGE: 1.6.3 — старый пустой дефолт [""] заполняется содержательным списком.MigrateBlockedShortnames();
                NormalizeBlockedShortnames();
                // CHANGE: Автофикс старого конфига: несуществующая иконка hammer.png давала
                // CHANGE: флуд "FileSystem Not Found" на клиенте при каждом открытии меню.if (configData.Gui.Pie.DefaultIcon == "assets/icons/hammer.png")
                configData.Gui.Pie.DefaultIcon = "assets/prefabs/weapons/hammer/hammer.icon.png";
                if (configData.Gui.Pie.AllIcon == "assets/icons/device_add.png") configData.Gui.Pie.AllIcon = "assets/icons/clear.png";
                // CHANGE: 1.6.6 — обратный автофикс 1.6.5: у контурного circle_open белая
                // CHANGE: заливка (тонируется только кольцо), возвращаем сплошной кружок.if (configData.Skins.ColourIcon == "assets/icons/circle_open.png")
                configData.Skins.ColourIcon = "assets/icons/circle_closed.png";
                SaveConfig();
            }
            catch {
                PrintError("Файл конфигурации поврежден или отсутствует.Создаем новый.");
                LoadDefaultConfig();
                SaveConfig();
            }
        }
        /// <inheritdoc />
        protected override void LoadDefaultConfig() {
            PrintWarning("Создание нового файла конфигурации...");
            configData = new ConfigData();
        }
        /// <inheritdoc />
        protected override void SaveConfig() => Config.WriteObject(configData);
        /// <summary>
        /// Переносит конфигурацию предыдущей версии: единый раздел «Основные» разбит на
        /// «Настройки ремува» и «Настройки апгрейда».Значения копируются 1:1 (лишние для
        /// каждого раздела поля молча игнорируются Newtonsoft при последующей десериализации),
        /// после чего устаревший раздел удаляется из файла./// Предусловие: файл конфигурации существует и читается как JSON./// Постусловие: при отсутствии раздела «Основные» файл не модифицируется./// Сложность: O(1) — размер дерева фиксирован числом настроек конфигурации./// </summary>
        private void MigrateLegacyConfig() {
            // CHANGE: Newtonsoft игнорирует неизвестные свойства — без миграции настройки уже
            // CHANGE: работающих серверов (проценты, привилегии, чёрный список) слетели бы на дефолты.if (Config.ReadObject<JObject>() is not { } root)
            return;
            if (root["Основные"] is not {
                Type : JTokenType.Object
            }
            legacy) return;
            // CHANGE: DeepClone обязателен: один JToken не может принадлежать двум разделам дерева.root["Настройки ремува"] ??= legacy.DeepClone();
            root["Настройки апгрейда"] ??= legacy.DeepClone();
            root.Remove("Основные");
            Config.WriteObject(root);
        }
        /// <summary>
        /// Миграция чёрного списка.Конфиги до 1.6.3 содержат только пустую строку ([""]) —
        /// такой список заменяется дефолтом; список, в точности равный дефолту 1.6.3
        /// (LegacyBlockedShortnames), канонически обновляется до текущего дефолта
        /// (small.oil.refinery → refinery_small_deployed).Пользовательские списки (не равные
        /// ни одному из дефолтов) не трогаются.Внимание: полностью очищенный список вернётся
        /// к дефолту при перезагрузке — «очищено» и «старый дефолт» в файле неразличимы
        /// (NormalizeBlockedShortnames сохраняет пустой список как [""])./// Предусловие: configData десериализован; вызывается до NormalizeBlockedShortnames./// Постусловие: в списке есть хотя бы одна непустая запись./// Сложность: O(n), n — записей списка./// </summary>
        private void MigrateBlockedShortnames() {
            var entries = configData.Remove.BlockedShortnames.Where(e => !string.IsNullOrWhiteSpace(e)).Select(e => e.Trim()).ToList();
            if (entries.Count == 0 || (entries.Count == RemoveSettings.LegacyBlockedShortnames.Length && !entries.Except(RemoveSettings.LegacyBlockedShortnames, StringComparer.OrdinalIgnoreCase).Any() ) ) {
                configData.Remove.BlockedShortnames = new List<string>(RemoveSettings.DefaultBlockedShortnames);
            }
        }
        /// <summary>
        /// Чистит чёрный список shortname от пустых и дублирующихся записей./// Постусловие: список не пуст — при полном отсутствии записей остается одна пустая строка ""./// </summary>
        private void NormalizeBlockedShortnames() {
            var entries = new List<string>();
            foreach (var raw in configData.Remove.BlockedShortnames) {
                var entry = raw?.Trim();
                if (string.IsNullOrEmpty(entry) || entries.Contains(entry)) continue;
                entries.Add(entry);
            }
            if (entries.Count == 0) entries.Add(string.Empty);
            configData.Remove.BlockedShortnames = entries;
        }
#endregion Конфигурация
#region Поля и константы
        private const string PermUse = "rremove.use";
        private const string PermAdmin = "rremove.admin";
        private const string PermUpgrade = "rremove.upgrade";
        private const string PermNoCost = "rremove.nocost";
        // CHANGE: Привилегия бесплатного использования скинов построек (обход проверки владения DLC).private const string PermSkinsFree = "rremove.skinsfree";
        // CHANGE: Ванильный источник списка скинов построек — префаб, чьи ConstructionGrade
        // CHANGE: перечисляет Admin.FindBuildingSkin (Admin.FindBuildingSkin в декомпиле,
        // CHANGE: Assembly-CSharp_chunk25:17582); по нему игра сама определяет канонический
        // CHANGE: набор пар (тип апгрейда, Steam ID скина).private const uint SkinListPrefabId = 2194854973u;
        private const string UiRoot = "RRemove_Root";
        private const string UiTimerLabel = "RRemove_Timer";
        private const string UiGradeLabel = "RRemove_Grade";
        // CHANGE: Единый путь data-файла (RSystem/RRemove/RRemove.json) — читается и пишется только через него.private const string DataFile = "RSystem/RRemove/RRemove";
        private enum RemoveMode {
            Default, Admin, All,
        }
        private sealed class RemoveState {
            public RemoveMode Mode;
            public float ExpiresAt;
        }
        // CHANGE: Состояние режима апгрейда: тип блока + скин постройки + момент истечения бездействия.private sealed class UpgradeState
        {
            public BuildingGrade.Enum Grade;
            // CHANGE: Выбранный скин постройки (Steam item definition ID, 0 — стандартный):
            // CHANGE: применяется и к автоапгрейду планом, и к ударам киянкой.public ulong Skin;
            // CHANGE: 1.6.0 — выбранный в цветовом меню индекс цвета палитры (1-основной,
            // CHANGE: 0 — случайный цвет, ванильная семантика GetStartingDetailColour(0)).public uint Colour;
            public float ExpiresAt;
        }
        // Инвариант: ключ — userID игрока; состояние существует только пока режим активен.private readonly Dictionary<ulong, RemoveState> activeRemovers = new();
        // CHANGE: Режим автоапгрейда: ключ — userID, значение — выбранный в меню тип блока.// CHANGE: Взаимоисключающ с activeRemovers: включение апгрейда гасит режим удаления и наоборот.private readonly Dictionary<ulong, UpgradeState> upgradeGrades = new();
        // CHANGE: Скины построек: выбранный игроком скин на КАЖДЫЙ тип апгрейда (ключ — userID).// CHANGE: Хранится в памяти до рестарта: скин — временное предпочтение, расширять
        // CHANGE: data-файл (knownUsers) ради него не нужно.private readonly Dictionary<ulong, Dictionary<BuildingGrade.Enum, ulong>> selectedSkins = new();
        // CHANGE: Реестр игровых DLC-скинов построек, сгруппированный по типу апгрейда
        // CHANGE: (источник — ванильный SkinListPrefabId).private readonly Dictionary<BuildingGrade.Enum, List<SkinInfo>> gameSkinRegistry = new();
        // CHANGE: Плоская карта «ID скина → английское имя из игровых данных»: источник
        // CHANGE: ключей локализации Skin{ID} и fallback имени без перебора реестра.O(1).private readonly Dictionary<ulong, string> skinNamesById = new();
        // CHANGE: 1.6.0 — реестр палитр скинов с поддержкой цвета (колесо баллончика):
        // CHANGE: ключ — Steam item definition ID скина, значение — цвета палитры
        // CHANGE (ConstructionSkin_CustomDetail.ColourLookup.AllColours игровых данных).private readonly Dictionary<ulong, Color[]> colourPalettes = new();
        // CHANGE: 1.6.0 — выбранный игроком цвет палитры на каждый тип апгрейда (ключ — userID,
        // CHANGE: значение — 1-основной индекс цвета, 0 — случайный).Живёт в памяти до рестарта.private readonly Dictionary<ulong, Dictionary<BuildingGrade.Enum, uint>> selectedColours = new();
        // CHANGE: 1.6.0 — тип апгрейда, ожидающий выбора цвета (между открытием цветового
        // CHANGE: меню и выбором цвета).Инвариант: запись существует только пока открыто меню.private readonly Dictionary<ulong, BuildingGrade.Enum> pendingColourSelections = new();
        // CHANGE: Игровой скин постройки: BuildingGrade.skin — это Steam item definition ID
        // CHANGE: (владение проверяется по Steam-инвентарю игрока), alwaysUnlock — ванильный
        // CHANGE: обход проверки владения, EnglishName — английское имя из upgradeMenu.private sealed class SkinInfo
        {
            public ulong SkinId;
            public bool AlwaysUnlock;
            public string EnglishName = string.Empty;
            // CHANGE: 1.6.0 — ванильный флаг BuildingGrade.supportsColourChange: скин имеет
            // CHANGE: палитру цветов (ConstructionSkin_CustomDetail + ColourLookup).public bool SupportsColourChange;
        }
        // CHANGE: Кэш ссылок на плагины рейд/комбат-блока; ищутся лениво (плагины могут грузиться позже нас).private Plugin? raidBlockPlugin;
        private Plugin? combatBlockPlugin;
        // CHANGE: Встроенная система блокировки (активна при false у флагов конфига):
        // CHANGE: ключ — userID, значение — момент снятия блокировки (Time.realtimeSinceStartup).private readonly Dictionary<ulong, float> builtinRaidBlocked = new();
        private readonly Dictionary<ulong, float> builtinCombatBlocked = new();
        // Инвариант: ключ — NetworkableId сущности; время её установки (только что поставленные объекты).private readonly Dictionary<NetworkableId, float> placedTimes = new();
        // CHANGE: Кэш «prefabID энтити → ItemDefinition» через ItemModDeployable.entityPrefab:
        // CHANGE: у DLC-объектов shortname энтити не совпадает с shortname предмета,
        // CHANGE: поэтому FindItemDefinition(ShortPrefabName) их не находит.private readonly Dictionary<uint, ItemDefinition> deployableItems = new();
        // Инвариант: userID игроков, уже использовавших режим удаления; подсказка о бинде им не показывается.// Изменения сохраняются в data-файле — знание переживает переподключения и рестарт сервера.private HashSet<ulong> knownUsers = new();
        // CHANGE: Тикер не сохраняется в поле: тип Timer из Oxide.Core недоступен при нативной
        // CHANGE: компиляции Carbon, а таймеры плагина Carbon/Oxide убивают автоматически при выгрузке.// CHANGE: Нормализованная кнопка открытия меню (0 — OFF, 1 — X, 2 — F).Кэшируется при старте:
        // хук OnServerCommand срабатывает на КАЖДУЮ консольную команду сервера, и аллокация строк
        // (Trim/ToUpperInvariant) на каждый вызов давила на GC и роняла FPS.private int menuOpenButton = 0;
#endregion Поля и константы
#region Инициализация
        /// <summary>Регистрирует права, запускает секундный тикер обновления GUI.</summary>
        private void OnServerInitialized() {
            // CHANGE: Одноразовая нормализация кнопки открытия меню (без аллокаций в горячих хуках)
            menuOpenButton = configData.Remove.MenuOpenButton?.Trim().ToUpperInvariant() switch {
                "X" => 1, "F" => 2, _ => 0,
            }
            ;
            permission.RegisterPermission(PermUse, this);
            permission.RegisterPermission(PermAdmin, this);
            permission.RegisterPermission(PermUpgrade, this);
            permission.RegisterPermission(PermNoCost, this);
            // CHANGE: Привилегия бесплатных скинов построек (обход проверки владения DLC) берётся из конфига.permission.RegisterPermission(configData.Skins.FreeSkinPermission, this);
            permission.RegisterPermission(configData.Remove.IgnorePermission, this);
            // CHANGE: Подсказка о бинде показывается до первого использования режима — факт
            // использования сохраняется в data-файле и переживает рестарты.knownUsers =
            Interface.Oxide.DataFileSystem.ReadObject<HashSet<ulong>>(DataFile) ?? new HashSet<ulong>();
            BuildDeployableItemCache();
            // CHANGE: Реестр скинов построек строится после загрузки конфига: автозаполнение
            // CHANGE: списков скинов найденными в игре DLC-скинами + сохранение дополненного конфига.BuildSkinRegistry();
            // CHANGE: 1.6.0 — реестр палитр скинов с поддержкой цвета (колесо баллончика).BuildColourPaletteRegistry();
            // CHANGE: Имена скинов — ключи локализации Skin{ID}: регистрируются из игровых
            // CHANGE: данных при каждом старте и редактируются сервером в lang-файлах.RegisterSkinNameMessages();
            SyncSkinConfig();
            timer.Every(1f, UpdateTickers);
            // CHANGE: Встроенная система блокировки (OnEntityTakeDamage) нужна только когда
            // CHANGE: хотя бы один из запретов работает по ней (false в конфиге).if (configData.Remove.BlockInRaidZone && configData.Remove.BlockInCombatBlock)
            Unsubscribe(nameof(OnEntityTakeDamage));
        }
        /// <summary>Гасит GUI и состояние всех игроков (таймеры плагина снимаются автоматически).</summary>
        private void Unload() {
            foreach (var userId in activeRemovers.Keys.ToArray()) Deactivate(userId);
            foreach (var userId in upgradeGrades.Keys.ToArray()) DeactivateUpgrade(userId);
            // CHANGE: 1.6.2 — заглушка открытых pie (ReplaceOpenPiesWithStub) удалена.// CHANGE: Принудительно закрыть pie с сервера нельзя (RPC закрытия нет —
            // CHANGE: CommunityEntity.Pie.cs, OpenPie только открывает), а трекинг «вероятно
            // CHANGE: открытых» колёс лгал: ESC закрывает pie без всякого серверного события,
            // CHANGE: поэтому после перезагрузки плагина заглушка САМА открывала белое меню
            // CHANGE: с единственной «Закрыть» на пустом экране.Открытое колесо теперь
            // CHANGE: переживает перезагрузку без замены: команды перерегистрируются теми же
            // CHANGE: именами при загрузке плагина, а вовсе без плагина колесо закрывается
            // CHANGE: первым же кликом по любому пункту (клиент закрывает pie при исполнении
            // CHANGE: команды пункта — паттерн ванильного pietest).}
            /// <summary>Сбрасывает режим игрока при выходе (GUI исчезает вместе с клиентом).</summary>
            private void OnPlayerDisconnected(BasePlayer player, string reason) {
                if (player != null) {
                    Deactivate(player.userID, destroyUi : false);
                    DeactivateUpgrade(player.userID, destroyUi : false);
                    // CHANGE: 1.6.0 — незавершённый выбор цвета и выбранные цвета больше не живут
                    // CHANGE: после выхода игрока (состояние — только пока игрок подключен).pendingColourSelections.Remove(player.userID);
                    selectedColours.Remove(player.userID);
                }
            }
            /// <summary>Запоминает время установки новой постройки/предмета для лимита времени удаления.</summary>
            private void OnEntityBuilt(Planner plan, GameObject go) {
                var entity = go?.GetComponent<BaseEntity>();
                if (entity?.net == null) return;
                placedTimes[entity.net.ID] = Time.realtimeSinceStartup;
                // CHANGE: Автоапгрейд: строительный план ставит твинь — сразу поднимаем блок
                // CHANGE: до типа, выбранного игроком в радиальном меню.if (
                configData.Upgrade.EnableAutoUpgrade && entity is BuildingBlock block && plan.GetOwnerPlayer() is {
                }
                builder && upgradeGrades.TryGetValue(builder.userID, out var upgradeState) ) {
                    // CHANGE: В рейд/комбат-блоке автоапгрейд постройки запрещён (конфиг).var buildDeny = GetBlockDenyReason(builder, out var buildLeft);
                    if (buildDeny != null) {
                        DenyToast(builder, buildDeny, buildLeft);
                        return;
                    }
                    UpgradeBlock(builder, block, upgradeState);
                }
            }
            /// <summary>
            /// Встроенная система блокировки (работает при false у флагов конфига):
            /// взрыв по строению — рейдер (инициатор) и владелец получают рейд-блок;
            /// ПВП-урон между игроками — обе стороны получают комбат-блок./// Длительности — из конфига (по умолчанию 5 минут)./// Сложность: O(1) на вызов./// </summary>
            private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
                if (info?.InitiatorPlayer is not {
                }
                attacker || entity == null || attacker.IsNpc) return;
                var now = Time.realtimeSinceStartup;
                // Взрыв по любой постройке (включая свою) — рейд-блок инициатору и владельцу.// CHANGE: Запись и уведомление — только когда встроенная система активна (флаг false).if (
                !configData.Remove.BlockInRaidZone && info.damageTypes != null && info.damageTypes.Get(Rust.DamageType.Explosion) > 0f && entity is StabilityEntity ) {
                    var raidDuration = Mathf.Max(1, configData.Remove.BuiltinRaidBlockDuration);
                    builtinRaidBlocked[attacker.userID] = now + raidDuration;
                    if (entity.OwnerID != 0u && entity.OwnerID != attacker.userID) builtinRaidBlocked[entity.OwnerID] = now + raidDuration;
                    // CHANGE: GameTip «Инициализировался рейд» — рейдеру и владельцу (если онлайн).NotifyRaidInitialized(attacker, raidDuration);
                    if (entity.OwnerID != 0u && BasePlayer.FindByID(entity.OwnerID) is {
                    }
                    owner && owner.userID != attacker.userID) NotifyRaidInitialized(owner, raidDuration);
                }
                // ПВП-урон — комбат-блок обеим сторонам (только при встроенной системе).if (
                !configData.Remove.BlockInCombatBlock && entity is BasePlayer victim && !victim.IsNpc && victim.userID != attacker.userID ) {
                    var until = now + Mathf.Max(1, configData.Remove.BuiltinCombatBlockDuration);
                    builtinCombatBlocked[attacker.userID] = until;
                    builtinCombatBlocked[victim.userID] = until;
                }
            }
            /// <summary>GameTip «Инициализировался рейд» с длительностью из конфига ({0} — секунды).</summary>
            private void NotifyRaidInitialized(BasePlayer player, int duration) => Toast(player, Msg(player, "RaidInitialized").Replace("{0}", duration.ToString(CultureInfo.InvariantCulture)));
            /// <summary>Чистит время установки уничтоженных сущностей (защита от роста словаря).</summary>
            private void OnEntityDestroyed(NetworkableId id) => placedTimes.Remove(id);
            /// <summary>
            /// Подсказка с готовой командой бинда клавиши активации./// Показывается при каждом взятии в руки киянки (hammer/toolgun — оба вида в Rust),
            /// но только пока игрок ни разу не воспользовался режимом удаления
            /// (сервер не может проверить наличие клиентского бинда, поэтому сигнал — использование режима)./// </summary>
            /// <summary>
            /// Подсказка об открытии радиального меню при взятии в руки киянки, пистолета-строителя (toolgun)
            /// или плана постройки (planner)./// X/F (открытие по кнопке, без бинда) — показывается при КАЖДОМ взятии инструмента в руки:
            /// бинд не требуется, скрывать подсказку после первого использования не нужно./// OFF (активация по бинду) — только пока игрок ни разу не воспользовался режимом удаления
            /// (сервер не может проверить наличие клиентского бинда, сигнал — использование режима)./// </summary>
            private void OnActiveItemChanged(BasePlayer player, Item oldItem, Item newItem) {
                if (player == null || !player.IsConnected || !configData.Remove.ShowBindHint) return;
                if (newItem?.info == null) return;
                // CHANGE: menuOpenButton кэшируется при старте — без аллокаций строк в хуке
                bool openByButton = menuOpenButton != 0;
                // Режим OFF: подсказка про бинд нужна только до первого использования режима.if (!openByButton && knownUsers.Contains(player.userID))
                return;
                var shortname = newItem.info.shortname;
                if (!shortname.Equals("hammer", StringComparison.Ordinal) && !shortname.Equals("toolgun", StringComparison.Ordinal) && !shortname.Equals("planner", StringComparison.Ordinal) ) return;
                player.Command("gametip.showtoast", 0, GetBindHintMessage(player));
            }
            /// <summary>
            /// Текст подсказки об открытии радиального меню:
            /// X/F — «нажмите кнопку» (открытие без бинда через перехват команды);
            /// OFF — готовая команда бинда rremove.menu для консоли./// </summary>
            private string GetBindHintMessage(BasePlayer player) {
                if (menuOpenButton == 1) return Msg(player, "MenuOpenHint").Replace("{key}", "X");
                if (menuOpenButton == 2) return Msg(player, "MenuOpenHint").Replace("{key}", "F");
                return Msg(player, "BindHint").Replace("{key}", configData.Remove.ActivationKey);
            }
#endregion Инициализация
#region Локализация
            /// <inheritdoc />
            protected override void LoadDefaultMessages() {
                // CHANGE: 1.5.0 — английский словарь переведён на английский (раньше был копией русского).lang.RegisterMessages(
                new Dictionary<string, string> {
                    ["NoPermission"]
                    = "You don't have permission to use this command.", ["BindHint"] = "Remove mode: run this once in the console (F1): bind {key} rremove.menu", ["MenuOpenHint"] = "Remove/upgrade mode: press [{key}] to open the radial menu", ["BlockedShortname"] = "This entity is blocked from removal.", ["CannotRemovePlayers"] = "Players cannot be removed.", ["OwnNoCupboard"] = "No tool cupboard nearby: removing your own entities requires cupboard authorization.", ["OwnNotAuthed"] = "This is your entity, but you are not authorized in this building's tool cupboard.", ["OthersRemovalDisabled"] = "This entity belongs to another player.You are authorized in the cupboard, but removing others' entities is disabled in the server settings.", ["TeamRemovalDisabled"] = "This entity belongs to your teammate, but teammate removal is disabled in the server settings.", ["ClanRemovalDisabled"] = "This entity belongs to your clanmate, but clanmate removal is disabled in the server settings.", ["NotOwner"] = "This entity belongs to another player, and you are not allowed to remove it.", ["ContainerNotEmpty"] = "Cannot remove: the entity contains items.", ["QuarryRunning"] = "Cannot remove: the quarry is running.Turn it off first.", ["QuarryHasFuel"] = "Cannot remove: the quarry's fuel tank still has diesel.Take it out before removal.", ["QuarryFuelNotSpent"] = "Cannot remove: the diesel in the quarry is not fully spent yet.Let the quarry finish its work.", ["QuarryLocked"] = "Cannot remove: a lock is installed on the quarry.", ["PlaceTimeLimit"] = "This entity can no longer be removed: the removal time limit has expired.", ["AlreadyDestroyed"] = "The entity is already destroyed.", ["TimeLeft"] = "Time left: {0} s", ["GuiTitle"] = "REMOVE MODE", ["GuiAdminTitle"] = "ADMIN REMOVE", ["GuiAllTitle"] = "REMOVE BUILDING", ["GuiHint"] = "Hit an entity with the hammer to remove it", ["NoBuilding"] = "This entity is not attached to a building.", ["PieDefault"] = "Simple remove", ["PieDefaultDesc"] = "Remove your own entities with a resource refund", ["PieAdmin"] = "Admin remove", ["PieAdminDesc"] = "Remove any entities without ownership checks", ["PieAll"] = "Remove building", ["PieAllDesc"] = "Remove every entity connected to this building", ["PieClose"] = "Close", ["PieCloseDesc"] = "Exit the menu without choosing a mode", ["PieWood"] = "Wood", ["PieWoodDesc"] = "Auto-upgrade the building to wood", ["PieStone"] = "Stone", ["PieStoneDesc"] = "Auto-upgrade the building to stone", ["PieMetal"] = "Metal", ["PieMetalDesc"] = "Auto-upgrade the building to metal", ["PieHqm"] = "HQM", ["PieHqmDesc"] = "Auto-upgrade the building to high quality metal", ["GuiUpgradeTitle"] = "UPGRADE MODE", ["GuiUpgradeHint"] = "Hit with the hammer or build with the plan", ["GradeWood"] = "Wood", ["GradeStone"] = "Stone", ["GradeMetal"] = "Metal", ["GradeHqm"] = "HQM", ["UpgradeNoResources"] = "Not enough resources for the upgrade.", ["UpgradeNoBuildAuth"] = "You are not authorized to build here.", ["UpgradeNoCupboard"] = "Auto-upgrade requires authorization in this building's tool cupboard.", ["RaidBlockDenied"] = "You are raid-blocked: the radial menu, remove and upgrade are unavailable.Time left: {0} s", ["CombatBlockDenied"] = "You are combat-blocked: the radial menu, remove and upgrade are unavailable.Time left: {0} s", ["RaidInitialized"] = "Raid initialized — the radial menu, remove and upgrade are unavailable for {0} s", ["PieSkinNone"] = "No skin", ["PieSkinCurrent"] = "Skin: {0}", ["SkinNotOwned"] = "This skin is unavailable: the DLC purchase is required.", ["SkinFallback"] = "The skin is unavailable for this building — the default one is applied.",  // CHANGE: 1.6.0 — цветовое колесо (палитра скина-контейнера) перед запуском режима апгрейда.// CHANGE: 1.6.1 — нумерация цветов убрана (ключ PieColourOption удалён):
                    // CHANGE: у колеса баллончика номеров нет, пункт называется просто «Цвет».["PieColourSwatch"] = "Colour",
                    ["PieColourRandom"]
                    = "Random colour", ["PieColourRandomDesc"] = "Each upgraded block gets a random palette colour", ["PieColourCancelDesc"] = "Cancel: the upgrade mode will not start",
                }
                , this, "en" );
                lang.RegisterMessages(new Dictionary<string, string> {
                    ["NoPermission"]
                    = "У вас нет прав на использование этой команды.", ["BindHint"] = "Режим удаления: выполните один раз в консоли (F1): bind {key} rremove.menu", ["MenuOpenHint"] = "Режим удаления/апгрейда: нажмите [{key}] для открытия меню", ["BlockedShortname"] = "Этот объект запрещён к удалению.", ["CannotRemovePlayers"] = "Игроков удалять нельзя.", ["OwnNoCupboard"] = "Рядом нет шкафа: удаление своих объектов требует авторизации в шкафу.", ["OwnNotAuthed"] = "Это ваш объект, но вы не авторизованы в шкафу этого строения.", ["OthersRemovalDisabled"] = "Объект чужой, вы авторизованы в шкафу, но удаление чужих объектов отключено в настройках сервера.", ["TeamRemovalDisabled"] = "Это объект товарища по команде, но удаление объектов команды отключено в настройках сервера.",  // CHANGE: Отдельная причина для клана (клановый стол) — раньше соклановцы
                    // CHANGE: получали сообщение про команду (Team), хотя системы разные.["ClanRemovalDisabled"] = "Это объект соклановца, но удаление объектов клана отключено в настройках сервера.",
                    ["NotOwner"]
                    = "Это чужой объект, и у вас нет доступа к его удалению.", ["ContainerNotEmpty"] = "Нельзя удалить: в объекте есть предметы.", ["QuarryRunning"] = "Нельзя удалить: карьер работает.Сначала выключите его.", ["QuarryHasFuel"] = "Нельзя удалить: в топливном баке карьера есть дизель.Заберите его перед удалением.", ["QuarryFuelNotSpent"] = "Нельзя удалить: дизель в карьере не израсходован полностью.Дайте карьеру доработать.", ["QuarryLocked"] = "Нельзя удалить: на карьере установлен замок.", ["PlaceTimeLimit"] = "Этот объект уже нельзя удалить: истекло время на удаление.", ["AlreadyDestroyed"] = "Объект уже уничтожен.", ["TimeLeft"] = "Осталось: {0} с", ["GuiTitle"] = "РЕЖИМ УДАЛЕНИЯ", ["GuiAdminTitle"] = "АДМИНСКОЕ УДАЛЕНИЕ", ["GuiAllTitle"] = "УДАЛЕНИЕ СТРОЕНИЯ", ["GuiHint"] = "Ударьте молотком по объекту для удаления", ["NoBuilding"] = "Этот объект не привязан к строению.", ["PieDefault"] = "Простой ремув", ["PieDefaultDesc"] = "Удаление своих объектов с возвратом ресурсов", ["PieAdmin"] = "Админский ремув", ["PieAdminDesc"] = "Удаление любых объектов без проверок владения", ["PieAll"] = "Удаление строения", ["PieAllDesc"] = "Удалить все связанные объекты строения", ["PieClos
// ... [Code preview truncated due to length. Full source in attached file] ...

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