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

FREE RIzolenta : 1.1.1 1.1.1

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
/*
 * CHANGE: создан плагин RIzolenta — починка предметов изолентой (ducttape)
 * без верстака и ресурсов, добавление изоленты в лут ящиков,
 * консольная команда giveizolenta <steamid> <кол-во>.*
 * Инварианты:
 *  - Изолента = предмет с shortname "ducttape" и скином из конфига.*  - Ремонт через перетаскивание изоленты НА предмет (перехват CanMoveItem
 *    из PlayerInventory.MoveItem — подтверждено декомпиляцией Assembly-CSharp,
 *    chunk7:20517; иного серверного события drag-onto-item в игре нет).*  - Ремонт поднимает condition на RepairPercent% от maxCondition и
 *    одновременно снижает maxCondition на WearPercent% (деградация).*  - Чинятся только предметы категорий Weapon / Attire / Tool с condition-системой,
 *    не входящие в чёрный список.*  - Изолента появляется в луте только у контейнеров из whitelist при первом
 *    лутании (контейнер помечается — повторного спавна нет).*
 * Сложность:
 *  - CanMoveItem: O(1) по времени/памяти (поиск в HashSet/списках константен для игрока).*  - OnLootEntity: O(k), k — кол-во добавляемых предметов (<= MaxAmount).*/
namespace Oxide.Plugins {
    [Info("RIzolenta", "RustInnovate", "1.1.1")]
    [Description("Починка оружия, брони и инструментов изолентой (ducttape) без верстака и ресурсов")]
    public class RIzolenta : RustPlugin {
#region Configuration
        private class Configuration {
            [JsonProperty("Список ящиков и бочек где будет появляться изолента")]
            public List<string> LootCrates = new List<string> {
                "crate_normal_2", "crate_tools", "loot_barrel_1", "loot_barrel_2"
            }
            ;
            [JsonProperty("Чёрный список")]
            public List<string> Blacklist = new List<string> {
                "rifle.l96"
            }
            ;
            [JsonProperty("Мин кол-во выпадение")]
            public int MinAmount = 1;
            [JsonProperty("Макс кол-во выпадение")]
            public int MaxAmount = 2;
            [JsonProperty("Шанс выпадения")]
            public float DropChance = 50.0f;
            [JsonProperty("Скин изоленты")]
            public ulong TapeSkin = 3811198690UL;
            [JsonProperty("Сколько чинить?")]
            public float RepairPercent = 10.0f;
            [JsonProperty("Сколько снимать хп от максимального хп?")]
            public float WearPercent = 5.0f;
        }
        private Configuration config;
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                config = Config.ReadObject<Configuration>();
                if (config == null) LoadDefaultConfig();
            }
            catch (Exception ex) {
                // CHANGE: при битом конфиге не затираем файл дефолтами —
                // сохраняем копию рядом и логируем причину, чтобы не терять настройки
                var path = $"{Interface.Oxide.ConfigDirectory}{System.IO.Path.DirectorySeparatorChar}RIzolenta.json";
                try {
                    if (System.IO.File.Exists(path)) System.IO.File.Copy(path, path + ".bak", true);
                }
                catch (Exception ioEx) {
                    Puts($"Не удалось создать бэкап конфига: {ioEx.Message}");
                }
                Puts($"ОШИБКА чтения конфигурации (файл повреждён или был удалён): {ex.Message}.Дефолты применены, старый файл сохранён как RIzolenta.json.bak");
                LoadDefaultConfig();
            }
            // CHANGE: дедупликация списков конфига — ручные дубли в JSON не сохраняются обратно
            if (config != null) {
                config.LootCrates = config.LootCrates?.Distinct().ToList() ?? new List<string>();
                config.Blacklist = config.Blacklist?.Distinct().ToList() ?? new List<string>();
            }
            SaveConfig();
        }
        protected override void LoadDefaultConfig() => config = new Configuration();
        protected override void SaveConfig() => Config.WriteObject(config);
#endregion
#region Localization
        protected override void LoadDefaultMessages() {
            lang.RegisterMessages(new Dictionary<string, string> {
                ["PlayerNotFound"]
                = "Player \"{0}\" not found or offline.", ["NoPermission"] = "Only admins can use this command.", ["Given"] = "Given {0} izolenta to {1}.", ["ItemName"] = "Изолента", ["Blacklisted"] = "Этот предмет нельзя починить изолентой."
            }
            , this, "en");
            lang.RegisterMessages(new Dictionary<string, string> {
                ["PlayerNotFound"]
                = "Игрок «{0}» не найден или не в сети.", ["NoPermission"] = "Эта команда доступна только администраторам.", ["Given"] = "Выдано {0} шт.изоленты игроку {1}.", ["ItemName"] = "Изолента", ["Blacklisted"] = "Этот предмет нельзя починить изолентой."
            }
            , this, "ru");
        }
        private string Msg(BasePlayer player, string key, params object[] args) => string.Format(lang.GetMessage(key, this, player?.UserIDString), args);
#endregion
#region Fields
        // CHANGE: shortname изоленты зафиксирован константой по требованию заказчика
        private const string TapeShortname = "ducttape";
        // CHANGE: разрешённые категории предметов к починке (оружие, броня, инструменты)
        private static readonly ItemCategory[] RepairableCategories = {
            ItemCategory.Weapon, ItemCategory.Attire, ItemCategory.Tool
        }
        ;
        private HashSet<string> crateLookup;
        private HashSet<string> blacklistLookup;
#endregion
#region Lifecycle
        private void Init() {
            crateLookup = new HashSet<string>(config.LootCrates ?? new List<string>());
            blacklistLookup = new HashSet<string>(config.Blacklist ?? new List<string>(), StringComparer.OrdinalIgnoreCase);
        }
        // CHANGE: Unload не требуется — состояние плагина не хранит ничего, что нужно освобождать
#endregion
#region Repair (drag duct tape onto item)
        /// <summary>
        /// Перехват перетаскивания предмета внутри инвентаря игрока./// Предусловия: игра вызывает хук для каждого MoveItem (drag&drop) с аргументами
        /// (Item, PlayerInventory, ItemContainerId, int slot, int amount, ItemMoveModifier) —
        /// Assembly-CSharp chunk7:20517; третий аргумент — ID контейнера, НЕ ItemContainer./// Инвариант: если переносимая изолента падает на чинимый предмет —
        /// выполняем ремонт, списываем 1 шт.изоленты и блокируем дефолтный swap./// Постусловие: null — дефолтное поведение игры; иначе — move заблокирован./// </summary>
        private object CanMoveItem(Item item, PlayerInventory playerInventory, ItemContainerId targetContainerId, int targetSlot, int amount, ItemMoveModifier modifier) {
            if (item == null || playerInventory == null) return null;
            if (!IsOurTape(item)) return null;
            // CHANGE: резолвим контейнер назначения по ID (игра передаёт ItemContainerId, chunk7:20582)
            var targetContainer = playerInventory.FindContainer(targetContainerId);
            if (targetContainer == null) return null;
            // CHANGE: drop на пустой слот — обычное перемещение, не мешаем
            var target = targetContainer.GetSlot(targetSlot);
            if (target == null || target == item) return null;
            var player = item.GetOwnerPlayer();
            if (player == null) return null;
            // CHANGE: предмет из чёрного списка — сообщаем игроку и блокируем перенос;
            // прочие непочинимые случаи (не та категория / целое) блокируем молча
            if (blacklistLookup.Contains(target.info.shortname)) {
                player.ChatMessage(Msg(player, "Blacklisted"));
                return true;
            }
            if (!CanRepairItem(target) || target.condition >= target.maxCondition) return true;
            // CHANGE: расходуем стак изоленты целиком до полной починки предмета:
            // каждая шт.даёт +RepairPercent% к condition, а износ -WearPercent%
            // списывается ОДИН раз за всю починку (как у верстака), а не за каждую шт.—
            // это стимулирует чиниться большим стаком, а не по 1 шт.// Цикл гарантированно конечен: condition растёт и упирается в maxCondition.int used = 0;
            while (target.condition < target.maxCondition && item.amount > 0 && used < 100) {
                target.condition += target.maxCondition * (config.RepairPercent / 100f);
                item.UseItem(1);
                used++;
            }
            if (used > 0) ApplyWear(target);
            return true;
        }
        /// <summary>
        /// Проверка, что предмет вообще подлежит починке изолентой./// Предусловия: target != null./// Инварианты: категория в whitelist, shortname не в чёрном списке,
        /// у предмета есть condition-система./// Сложность: O(1)./// </summary>
        private bool CanRepairItem(Item target) {
            if (target == null || !target.hasCondition) return false;
            if (blacklistLookup.Contains(target.info.shortname)) return false;
            return RepairableCategories.Contains(target.info.category);
        }
        /// <summary>
        /// Списывает износ за починку: -WearPercent% от maxCondition, один раз за всю починку
        /// (аналог штрафа верстака), независимо от кол-ва использованной изоленты./// Инварианты: condition <= maxCondition; maxCondition >= 1 после износа./// Сложность: O(1)./// </summary>
        private void ApplyWear(Item target) {
            target.condition -= target.maxCondition * (config.WearPercent / 100f);
            target.maxCondition *= 1f - config.WearPercent / 100f;
            // CHANGE: деградация не должна «убить» предмет — гарантируем минимум 1 HP макс.прочности
            if (target.maxCondition < 1f) target.maxCondition = 1f;
            if (target.condition > target.maxCondition) target.condition = target.maxCondition;
            if (target.condition < 0f) target.condition = 0f;
            target.MarkDirty();
        }
        /// <summary>
        /// Создаёт стопку изоленты с кастомным отображаемым именем «Изолента»./// Инвариант: item.text задаётся только предметам, созданным плагином —
        /// ванильный ducttape без имени остаётся без изменений./// Сложность: O(1)./// </summary>
        private Item CreateTapeItem(int amount) {
            var def = ItemManager.FindItemDefinition(TapeShortname);
            if (def == null) return null;
            var tape = ItemManager.Create(def, amount, config.TapeSkin);
            if (tape == null) return null;
            // CHANGE: item.name — переопределение отображаемого имени предмета
            // (механизм из Airstrike.cs:1352 — сериализуется клиенту); текст из локализации плагина
            tape.name = lang.GetMessage("ItemName", this);
            tape.MarkDirty();
            return tape;
        }
#endregion
        /// <summary>
        /// Проверка, что предмет — наша изолента (ducttape со скином из конфига)./// Предусловия: item может быть null./// Инварианты: совпадение shortname И скина — ванильный ducttape без скина не считается нашей изолентой./// Сложность: O(1)./// </summary>
        private bool IsOurTape(Item item) => item != null && item.info.shortname == TapeShortname && item.skin == config.TapeSkin;
        /// <summary>
        /// Запрещает фактическую переработку нашей изоленты, не ограничивая её укладку./// Точка вызова игры: Recycler.RecycleThink —
        /// Interface.CallHook("OnItemRecycleAmount", slot, amount, this)
        /// (Assembly-CSharp chunk8:1735); если результат — int, он ЗАМЕНЯЕТ количество
        /// предметов на переработку за цикл.Возврат 0 означает: списывается 0 шт./// изоленты, не производится scrap/ингредиентов, цикл продолжает обработку
        /// остальных слотов (в отличие от OnItemRecycle, где non-null даёт break
        /// всего цикла — chunk8:1718)./// ВАЖНО: хук CanBeRecycled для запрета непригоден — он же используется
        /// RecyclerItemFilter (chunk8:1611) для приёма предмета в слоты ввода (0-5):
        /// false выталкивал бы изоленту в слоты вывода./// Постусловие: 0 только для нашей изоленты, остальные предметы — null (ванильное поведение)./// Сложность: O(1)./// </summary>
        private object OnItemRecycleAmount(Item item, int amount, Recycler recycler) {
            if (IsOurTape(item)) return 0;
            return null;
        }
        /// <summary>
        /// Блокирует изучение изоленты на исследовательском столе./// Точка вызова игры: ResearchTable, chunk8:1412 — CallHook("CanResearchItem", player, item);
        /// non-null результат запрещает исследование./// Постусловие: для изоленты — блокировка, для остальных предметов — null (ванильное поведение)./// </summary>
        private object CanResearchItem(BasePlayer player, Item targetItem) {
            if (targetItem != null && targetItem.info.shortname == TapeShortname) return false;
            return null;
        }
#region Loot
        /// <summary>
        /// Добавляет изоленту в лут контейнеров из whitelist при генерации лута./// Используется OnLootSpawn (LootContainer.SpawnLoot, Assembly-CSharp chunk12:34198):
        /// он срабатывает и у ящиков, и у лоомаемых бочек — в отличие от OnLootEntity,
        /// который вызывается только при открытии контейнера игроком (chunk7:22258),
        /// а бочки игроки не открывают, а разбивают./// Постусловие: return void — ванильный PopulateLoot выполняется после добавления изоленты./// Сложность: O(k), k — кол-во выпавших стопок изоленты./// </summary>
        private void OnLootSpawn(LootContainer container) {
            if (container == null || container.inventory == null) return;
            if (!crateLookup.Contains(container.ShortPrefabName)) return;
            if (UnityEngine.Random.Range(0f, 100f) >= config.DropChance) return;
            int count = UnityEngine.Random.Range(config.MinAmount, config.MaxAmount + 1);
            // CHANGE: изолента добавляется до ванильного PopulateLoot —
            // при разбитии бочки падает на землю вместе с остальным лутом
            for (int i = 0; i < count; i++) {
                var tape = CreateTapeItem(1);
                if (tape == null) continue;
                if (!tape.MoveToContainer(container.inventory)) {
                    tape.Remove();
                }
            }
        }
#endregion
#region Console command
        /// <summary>
        /// Консольная команда: giveizolenta <steamid|ник> <кол-во>./// Работает из серверной консоли и из F1-консоли игрока./// Из F1 команда доступна только админам (authlevel > 0) — F1 открыта всем клиентам./// Инварианты: игрок ищется по SteamID, точному нику или части ника (без учёта регистра);
        /// выдача только игрокам, находящимся в сети./// </summary>
        [ConsoleCommand("giveizolenta")]
        private void ConsoleGiveIsoTape(ConsoleSystem.Arg arg) {
            // CHANGE: вызов из F1-консоли клиента разрешён только админам
            var caller = arg.Player();
            if (caller != null && !caller.IsAdmin) {
                arg.ReplyWith(Msg(caller, "NoPermission"));
                return;
            }
            if (arg.Args == null || arg.Args.Length < 2) {
                arg.ReplyWith("giveizolenta <steamid|ник> <кол-во>");
                return;
            }
            // CHANGE: arg.Args в текущей сборке — Facepunch.StringView[], читаем через GetString
            string nameOrId = arg.GetString(0);
            if (string.IsNullOrEmpty(nameOrId)) {
                arg.ReplyWith("giveizolenta <steamid|ник> <кол-во>");
                return;
            }
            if (!int.TryParse(arg.GetString(1), out int amount) || amount <= 0) {
                arg.ReplyWith("giveizolenta <steamid|ник> <кол-во>");
                return;
            }
            var target = FindPlayer(nameOrId);
            if (target == null) {
                arg.ReplyWith(Msg(caller, "PlayerNotFound", nameOrId));
                return;
            }
            var tape = CreateTapeItem(amount);
            if (tape == null) {
                arg.ReplyWith($"Ошибка: предмет с shortname '{TapeShortname}' не найден!");
                return;
            }
            if (!tape.MoveToContainer(target.inventory.containerMain) && !tape.MoveToContainer(target.inventory.containerBelt)) {
                tape.Drop(target.GetDropPosition(), target.GetDropVelocity());
            }
            // CHANGE: уведомление в консоль о выдаче (без сообщений игроку в чат)
            arg.ReplyWith(Msg(caller, "Given", amount, target.displayName));
        }
        /// <summary>
        /// Поиск игрока в сети по SteamID, точному нику или части ника (без учёта регистра)./// Предусловия: nameOrId не пустое./// Постусловие: null — игрок не найден или не в сети./// Сложность: O(n), n — кол-во игроков в сети (только при неполном совпадении)./// </summary>
        private static BasePlayer FindPlayer(string nameOrId) {
            // CHANGE: BasePlayer.Find матчит и SteamID, и точный ник
            var exact = BasePlayer.Find(nameOrId);
            if (exact != null) return exact;
            return BasePlayer.activePlayerList.FirstOrDefault(p => p != null && p.displayName.Contains(nameOrId, StringComparison.OrdinalIgnoreCase));
        }
#endregion
    }
}

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

Другие ресурсы пользователя 15FPS

Сверху