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

FREE DungeonEventsLootEditor : 1.1.0 1.1.0

Нет прав для скачивания
C#:
/*
 * Dungeon Events Loot Editor
 * Copyright (c) 2026 illknown. All rights reserved.
 *
 * FREE-TO-USE SERVER LICENCE
 *
 * Permission is granted to users who obtain this plugin from its official
 * Codefling listing to install and use it on Rust servers they own or manage.
 * Private modifications are permitted for use on those servers only, provided
 * this copyright and licence notice remains complete and unchanged.
 *
 * You may not redistribute, re-upload, share, sell, sublicense, publish, or
 * otherwise make the original or a modified copy of this plugin available to
 * any third party. You may not remove or obscure the author attribution.
 * Written permission from illknown is required for any other use.
 *
 * Official distribution:
 * https://codefling.com/plugins/dungeon-events-loot-editor
 *
 * CHANGELOG
 * v1.1.0
 * - Completely redesigned the editor with a polished Rust-themed interface.
 * - Added an original paginated item catalogue using live Rust definitions,
 *   native categories, compact icons, search, and selectable detail rows.
 * - Removed automatic and manual tier-folder creation; only existing
 *   DungeonEvents tier folders are scanned.
 * - Added a true/false option to fill every missing or empty tier loot profile
 *   from Default_Loot.json on first load without overwriting customised loot.
 * - Added Rust shortname validation, safer paging, and clearer backup controls.
 * - Changed search inputs to start genuinely empty with external field labels,
 *   so clicking a search bar never requires deleting instructional text.
 * - Smoothed navigation by batching rapid UI actions into one next-frame
 *   redraw and keeping a persistent visual frame behind changing content.
 * - Added live item icons and Rust display names to the current loot table for
 *   each tier and loot source, plus a selected-item preview in the editor.
 * - Added the free-to-use server licence and permanent author attribution.
 *
 * This software is provided "as is", without warranty of any kind, express or
 * implied. The author is not liable for any claim, damage, or other liability
 * arising from its use.
 */
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins {
    [Info("Dungeon Events Loot Editor", "illknown", "1.1.0")]
    [Description("Polished in-game loot table editor for existing DungeonEvents tier data files.")]
    public class DungeonEventsLootEditor : RustPlugin {
        private const string PermUse = "dungeoneventslooteditor.admin";
        private const string Ui = "DungeonEventsLootEditor.UI";
        private const string Content = "DungeonEventsLootEditor.Content";
        private const string ColorBackdrop = "0.025 0.023 0.021 0.985";
        private const string ColorHeader = "0.055 0.050 0.046 0.995";
        private const string ColorSurface = "0.075 0.070 0.066 0.990";
        private const string ColorSurfaceRaised = "0.105 0.098 0.091 0.990";
        private const string ColorSurfaceHover = "0.135 0.125 0.115 0.990";
        private const string ColorAccent = "0.820 0.325 0.090 1.000";
        private const string ColorAccentDark = "0.410 0.150 0.045 1.000";
        private const string ColorSuccess = "0.235 0.455 0.245 1.000";
        private const string ColorDanger = "0.600 0.125 0.105 1.000";
        private const string ColorWarning = "0.470 0.245 0.075 1.000";
        private const string ColorText = "0.965 0.945 0.920 1.000";
        private const string ColorMuted = "0.675 0.640 0.600 1.000";
        private PluginConfig _config;
        private Timer _lockTimer;
        private readonly Dictionary<ulong, UiState> _states = new Dictionary<ulong, UiState>();
        private readonly Dictionary<ulong, EditBuffer> _buffers = new Dictionary<ulong, EditBuffer>();
        private readonly Dictionary<ulong, Vector3> _lockedPositions = new Dictionary<ulong, Vector3>();
        private readonly HashSet<ulong> _openPlayers = new HashSet<ulong>();
        private readonly HashSet<ulong> _backupClearConfirm = new HashSet<ulong>();
        private readonly HashSet<ulong> _queuedDraws = new HashSet<ulong>();
        private static readonly string[] PickerCategories = {
            "All", "Weapon", "Ammunition", "Resources", "Tool", "Attire", "Medical", "Food", "Traps", "Misc", "Items", "Component", "Construction", "Electrical", "Fun"
        }
        ;
        private class PluginConfig {
            public string DataRootFolder = "DungeonEvents";
            public string BackupFolder = "DungeonEventsLootEditor_Backups";
            [JsonProperty("Fill empty tier profiles with Default_Loot.json on first load")]
            public bool FillEmptyProfilesWithDefaultLoot = true;
            [JsonProperty("Default loot seed completed")]
            public bool DefaultLootSeedCompleted = false;
            public bool ReloadDungeonEventsAfterSave = false;
            public bool ShowReloadButton = false;
            public bool BlockMovementWhileOpen = true;
            public float MovementLockCheckInterval = 0.20f;
            public float MovementLockDistance = 0.60f;
            public int ItemsPerPage = 15;
            public int PickerItemsPerPage = 10;
            public int MaxBackupsPerLootFile = 30;
            public List<string> LootFiles = new List<string> {
                "lootbox.json", "npc_loot.json", "boss_loot.json"
            }
            ;
            [JsonProperty("Preferred tier order")]
            public List<string> PreferredTierOrder = new List<string> {
                "Easy", "Medium", "Hard", "Expert", "Nightmare", "Impossible"
            }
            ;
        }
        private class UiState {
            public string Tier = "";
            public string File = "lootbox.json";
            public string View = "Browse";
            public int Page = 0;
            public int Selected = -1;
            public string Search = "";
            public int PickerPage = 0;
            public string PickerSearch = "";
            public string PickerCategory = "All";
        }
        private class EditBuffer {
            public string ShortName = "scrap";
            public string Chance = "100";
            public string Min = "1";
            public string Max = "1";
            public string SkinID = "0";
            public string CustomName = "";
        }
        protected override void LoadDefaultConfig() {
            _config = new PluginConfig();
            SaveConfig();
        }
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                _config = Config.ReadObject<PluginConfig>();
                if (_config == null) throw new Exception("Config returned null");
                NormalizeConfig();
                SaveConfig();
            }
            catch {
                PrintWarning("Config invalid. Creating a new default config.");
                LoadDefaultConfig();
            }
        }
        protected override void SaveConfig() {
            Config.WriteObject(_config, true);
        }
        private void NormalizeConfig() {
            if (_config.LootFiles == null || _config.LootFiles.Count == 0) _config.LootFiles = new List<string> {
                "lootbox.json", "npc_loot.json", "boss_loot.json"
            }
            ;
            _config.LootFiles = _config.LootFiles .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(CleanFile) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList();
            if (_config.PreferredTierOrder == null || _config.PreferredTierOrder.Count == 0) _config.PreferredTierOrder = new List<string> {
                "Easy", "Medium", "Hard", "Expert", "Nightmare", "Impossible"
            }
            ;
            _config.PreferredTierOrder = _config.PreferredTierOrder .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => CleanName(CleanArg(x))) .Where(x => !string.IsNullOrEmpty(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList();
            if (_config.ItemsPerPage < 8) _config.ItemsPerPage = 8;
            if (_config.ItemsPerPage > 24) _config.ItemsPerPage = 24;
            if (_config.PickerItemsPerPage < 8) _config.PickerItemsPerPage = 8;
            if (_config.PickerItemsPerPage > 12) _config.PickerItemsPerPage = 12;
            if (_config.MovementLockCheckInterval < 0.10f) _config.MovementLockCheckInterval = 0.10f;
            if (_config.MovementLockCheckInterval > 1.00f) _config.MovementLockCheckInterval = 1.00f;
            if (_config.MovementLockDistance < 0.15f) _config.MovementLockDistance = 0.15f;
            if (_config.MovementLockDistance > 3.00f) _config.MovementLockDistance = 3.00f;
            if (_config.MaxBackupsPerLootFile < 0) _config.MaxBackupsPerLootFile = 0;
        }
        private void Init() {
            permission.RegisterPermission(PermUse, this);
            _lockTimer = timer.Every(_config.MovementLockCheckInterval, LockOpenPlayers);
            timer.Once(1f, TrySeedDefaultLootProfiles);
        }
        private void Unload() {
            _lockTimer?.Destroy();
            foreach (BasePlayer player in BasePlayer.activePlayerList) Destroy(player);
            _states.Clear();
            _buffers.Clear();
            _lockedPositions.Clear();
            _openPlayers.Clear();
            _backupClearConfirm.Clear();
            _queuedDraws.Clear();
        }
        private void OnPlayerDisconnected(BasePlayer player, string reason) {
            if (player == null) return;
            _states.Remove(player.userID);
            _buffers.Remove(player.userID);
            _lockedPositions.Remove(player.userID);
            _openPlayers.Remove(player.userID);
            _backupClearConfirm.Remove(player.userID);
            _queuedDraws.Remove(player.userID);
        }
        private object OnPlayerInput(BasePlayer player, InputState input) {
            if (!_config.BlockMovementWhileOpen || player == null || input == null) return null;
            if (!_openPlayers.Contains(player.userID)) return null;
            input.current.buttons = 0;
            input.previous.buttons = 0;
            return null;
        }
        private void LockOpenPlayers() {
            if (!_config.BlockMovementWhileOpen || _openPlayers.Count == 0) return;
            foreach (ulong userId in _openPlayers.ToArray()) {
                BasePlayer player = BasePlayer.FindByID(userId);
                if (player == null || !player.IsConnected) {
                    _openPlayers.Remove(userId);
                    _lockedPositions.Remove(userId);
                    continue;
                }
                Vector3 lockedPos;
                if (!_lockedPositions.TryGetValue(userId, out lockedPos)) {
                    _lockedPositions[userId] = player.transform.position;
                    continue;
                }
                if (Vector3.Distance(player.transform.position, lockedPos) > _config.MovementLockDistance) player.Teleport(lockedPos);
            }
        }
        private bool HasAccess(BasePlayer player) {
            return player != null && (player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermUse));
        }
        private string DataRootPath {
            get {
                return Path.Combine(Interface.Oxide.DataDirectory, _config.DataRootFolder);
            }
        }
        private string BackupRootPath {
            get {
                return Path.Combine(Interface.Oxide.DataDirectory, _config.BackupFolder);
            }
        }
        private string CleanArg(string value) {
            if (string.IsNullOrWhiteSpace(value)) return "";
            value = value.Trim();
            while (value.Length >= 2 && ((value[0] == '"' && value[value.Length - 1] == '"') || (value[0] == '\'' && value[value.Length - 1] == '\''))) value = value.Substring(1, value.Length - 2).Trim();
            value = value.Replace("\\\"", "\"");
            value = value.Replace("\\'", "'");
            return value.Trim();
        }
        private string CleanName(string value) {
            value = CleanArg(value);
            if (string.IsNullOrWhiteSpace(value)) return "";
            foreach (char c in Path.GetInvalidFileNameChars()) value = value.Replace(c.ToString(), "");
            return value.Trim();
        }
        private string CleanFile(string value) {
            value = CleanName(value);
            if (string.IsNullOrEmpty(value)) value = "lootbox.json";
            if (!value.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) value += ".json";
            return value;
        }
        private string Quote(string value) {
            if (value == null) value = "";
            return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
        }
        private string TierPath(string tier) {
            return Path.Combine(DataRootPath, CleanName(tier));
        }
        private string LootPath(string tier, string file) {
            return Path.Combine(TierPath(tier), CleanFile(file));
        }
        private List<string> GetTiers() {
            if (!Directory.Exists(DataRootPath)) return new List<string>();
            List<string> tiers = Directory.GetDirectories(DataRootPath) .Select(Path.GetFileName) .Where(x => !string.IsNullOrWhiteSpace(x)) .ToList();
            List<string> ordered = new List<string>();
            foreach (string preferred in _config.PreferredTierOrder) {
                string match = tiers.FirstOrDefault(x => x.Equals(preferred, StringComparison.OrdinalIgnoreCase));
                if (match == null) continue;
                ordered.Add(match);
                tiers.Remove(match);
            }
            ordered.AddRange(tiers.OrderBy(x => x));
            return ordered;
        }
        private UiState GetState(BasePlayer player) {
            UiState state;
            if (!_states.TryGetValue(player.userID, out state)) {
                state = new UiState();
                _states[player.userID] = state;
            }
            List<string> tiers = GetTiers();
            if (tiers.Count == 0) state.Tier = "";
            else if (string.IsNullOrWhiteSpace(state.Tier) || !tiers.Any(x => x.Equals(state.Tier, StringComparison.OrdinalIgnoreCase))) state.Tier = tiers[0];
            state.File = CleanFile(state.File);
            List<string> files = _config.LootFiles.Select(CleanFile).ToList();
            if (files.Count > 0 && !files.Any(x => x.Equals(state.File, StringComparison.OrdinalIgnoreCase))) state.File = files[0];
            state.View = NormalizeView(state.View);
            if (string.IsNullOrWhiteSpace(state.PickerCategory) || !PickerCategories.Any(x => x.Equals(state.PickerCategory, StringComparison.OrdinalIgnoreCase))) state.PickerCategory = "All";
            return state;
        }
        private EditBuffer GetBuffer(BasePlayer player) {
            EditBuffer buffer;
            if (!_buffers.TryGetValue(player.userID, out buffer)) {
                buffer = NewBuffer();
                _buffers[player.userID] = buffer;
            }
            return buffer;
        }
        private bool TierExists(UiState state) {
            return state != null && !string.IsNullOrWhiteSpace(state.Tier) && Directory.Exists(TierPath(state.Tier));
        }
        private EditBuffer NewBuffer() {
            return new EditBuffer {
                ShortName = "scrap", Chance = "100", Min = "1", Max = "1", SkinID = "0", CustomName = ""
            }
            ;
        }
        private EditBuffer BufferFromItem(JObject item) {
            if (item == null) return NewBuffer();
            return new EditBuffer {
                ShortName = item["ShortName"]?.ToString() ?? "scrap", Chance = item["Inclusion Chance Percentage"]?.ToString() ?? "100", Min = item["Minimum Amount"]?.ToString() ?? "1", Max = item["Maximum Amount"]?.ToString() ?? "1", SkinID = item["SkinID"]?.ToString() ?? "0", CustomName = item["CustomName"]?.ToString() ?? ""
            }
            ;
        }
        private JObject NewLootObject() {
            return new JObject {
                ["Items"]
                = new JArray()
            }
            ;
        }
        private void TrySeedDefaultLootProfiles() {
            if (!_config.FillEmptyProfilesWithDefaultLoot || _config.DefaultLootSeedCompleted) return;
            if (!Directory.Exists(DataRootPath)) return;
            string defaultPath = Path.Combine(DataRootPath, "Default_Loot.json");
            if (!File.Exists(defaultPath)) {
                PrintWarning("First-run loot seeding is waiting for DungeonEvents/Default_Loot.json.");
                return;
            }
            JObject defaultLoot;
            try {
                JToken token = JToken.Parse(File.ReadAllText(defaultPath));
                if (token is JObject) defaultLoot = (JObject)token;
                else if (token is JArray) defaultLoot = new JObject {
                    ["Items"]
                    = token
                }
                ;
                else throw new Exception("root must be a JSON object or array");
                JArray defaultItems = defaultLoot["Items"] as JArray;
                if (defaultItems == null || defaultItems.Count == 0) throw new Exception("the Items list is missing or empty");
            }
            catch (Exception ex) {
                PrintWarning("Cannot seed tier loot from Default_Loot.json: " + ex.Message);
                return;
            }
            List<string> tiers = GetTiers();
            if (tiers.Count == 0) return;
            int seededFiles = 0;
            bool hadErrors = false;
            foreach (string tier in tiers) {
                foreach (string configuredFile in _config.LootFiles) {
                    string file = CleanFile(configuredFile);
                    string path = LootPath(tier, file);
                    bool shouldSeed;
                    if (!File.Exists(path)) {
                        shouldSeed = true;
                    }
                    else {
                        try {
                            string existingJson = File.ReadAllText(path);
                            if (string.IsNullOrWhiteSpace(existingJson)) {
                                shouldSeed = true;
                            }
                            else {
                                JToken existingToken = JToken.Parse(existingJson);
                                JObject existingObject = existingToken as JObject;
                                JArray existingItems = existingObject?["Items"] as JArray;
                                if (existingItems != null) shouldSeed = existingItems.Count == 0;
                                else if (existingToken is JArray && ((JArray)existingToken).Count == 0) shouldSeed = true;
                                else {
                                    PrintWarning("Skipped first-run seed for invalid loot file: " + path);
                                    hadErrors = true;
                                    continue;
                                }
                            }
                        }
                        catch (Exception ex) {
                            PrintWarning("Skipped first-run seed for '" + path + "': " + ex.Message);
                            hadErrors = true;
                            continue;
                        }
                    }
                    if (!shouldSeed) continue;
                    try {
                        File.WriteAllText(path, defaultLoot.DeepClone().ToString(Formatting.Indented));
                        seededFiles++;
                    }
                    catch (Exception ex) {
                        PrintWarning("Failed to seed '" + path + "': " + ex.Message);
                        hadErrors = true;
                    }
                }
            }
            if (hadErrors) {
                PrintWarning("First-run loot seeding was not marked complete because one or more files could not be processed.");
                return;
            }
            _config.DefaultLootSeedCompleted = true;
            SaveConfig();
            Puts("First-run default loot seed complete. Populated " + seededFiles + " empty tier loot file(s) across " + tiers.Count + " existing tier folder(s).");
        }
        private JObject LoadLoot(string tier, string file) {
            string path = LootPath(tier, file);
            if (!File.Exists(path)) return NewLootObject();
            try {
                JObject obj = JObject.Parse(File.ReadAllText(path));
                if (!(obj["Items"] is JArray)) obj["Items"] = new JArray();
                return obj;
            }
            catch (Exception ex) {
                PrintWarning("Failed to read " + path + ": " + ex.Message);
                return NewLootObject();
            }
        }
        private JObject MakeItem(EditBuffer buffer) {
            double chance;
            int min;
            int max;
            ulong skin;
            if (!double.TryParse(buffer.Chance, out chance)) chance = 0;
            if (!int.TryParse(buffer.Min, out min)) min = 1;
            if (!int.TryParse(buffer.Max, out max)) max = min;
            if (!ulong.TryParse(buffer.SkinID, out skin)) skin = 0;
            if (chance < 0) chance = 0;
            if (chance > 100) chance = 100;
            if (min < 0) min = 0;
            if (max < min) max = min;
            return new JObject {
                ["ShortName"]
                = string.IsNullOrWhiteSpace(buffer.ShortName) ? "scrap" : buffer.ShortName.Trim(), ["Inclusion Chance Percentage"] = chance, ["Minimum Amount"] = min, ["Maximum Amount"] = max, ["CustomName"] = buffer.CustomName ?? "", ["SkinID"] = skin
            }
            ;
        }
        private bool SaveLoot(string tier, string file, JObject obj) {
            if (string.IsNullOrWhiteSpace(tier) || !Directory.Exists(TierPath(tier))) return false;
            Backup(tier, file);
            File.WriteAllText(LootPath(tier, file), obj.ToString(Formatting.Indented));
            TrimBackups(tier, file);
            if (_config.ReloadDungeonEventsAfterSave) Interface.Oxide.ReloadPlugin("DungeonEvents");
            return true;
        }
        private void Backup(string tier, string file) {
            string source = LootPath(tier, file);
            if (!File.Exists(source)) return;
            string folder = Path.Combine(BackupRootPath, CleanName(tier));
            Directory.CreateDirectory(folder);
            string backupName = Path.GetFileNameWithoutExtension(file) + "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") + ".json";
            File.Copy(source, Path.Combine(folder, backupName), true);
        }
        private void TrimBackups(string tier, string file) {
            if (_config.MaxBackupsPerLootFile <= 0) return;
            string folder = Path.Combine(BackupRootPath, CleanName(tier));
            if (!Directory.Exists(folder)) return;
            string prefix = Path.GetFileNameWithoutExtension(CleanFile(file)) + "_";
            List<string> backups = Directory.GetFiles(folder, prefix + "*.json") .OrderByDescending(File.GetCreationTimeUtc) .ToList();
            foreach (string oldBackup in backups.Skip(_config.MaxBackupsPerLootFile)) {
                try {
                    File.Delete(oldBackup);
                }
                catch {
                }
            }
        }
        [ChatCommand("dloot")]
        private void CmdLootEditor(BasePlayer player, string command, string[] args) {
            if (!HasAccess(player)) {
                SendReply(player, "No permission.");
                return;
            }
            if (args.Length > 0 && args[0].Equals("close", StringComparison.OrdinalIgnoreCase)) {
                Destroy(player);
                return;
            }
            TrySeedDefaultLootProfiles();
            Draw(player);
        }
        [ConsoleCommand("dlootui.close")]
        private void UiClose(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (player != null) Destroy(player);
        }
        [ConsoleCommand("dlootui.view")]
        private void UiView(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.View = NormalizeView(CleanArg(arg.GetString(0, "Browse")));
            Draw(player);
        }
        [ConsoleCommand("dlootui.tier")]
        private void UiTier(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.Tier = CleanName(arg.GetString(0, state.Tier));
            state.Page = 0;
            state.Selected = -1;
            state.View = "Browse";
            _buffers[player.userID] = NewBuffer();
            Draw(player);
        }
        [ConsoleCommand("dlootui.file")]
        private void UiFile(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.File = CleanFile(arg.GetString(0, state.File));
            state.Page = 0;
            state.Selected = -1;
            state.View = "Browse";
            _buffers[player.userID] = NewBuffer();
            Draw(player);
        }
        [ConsoleCommand("dlootui.page")]
        private void UiPage(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.Page = Mathf.Max(0, state.Page + arg.GetInt(0, 0));
            state.Selected = -1;
            Draw(player);
        }
        [ConsoleCommand("dlootui.select")]
        private void UiSelect(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            int index = arg.GetInt(0, -1);
            JObject obj = LoadLoot(state.Tier, state.File);
            JArray items = (JArray)obj["Items"];
            if (index < 0 || index >= items.Count) return;
            state.Selected = index;
            state.View = "Browse";
            _buffers[player.userID] = BufferFromItem(items[index] as JObject);
            Draw(player);
        }
        [ConsoleCommand("dlootui.new")]
        private void UiNew(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.Selected = -1;
            state.View = "Browse";
            _buffers[player.userID] = NewBuffer();
            Draw(player);
        }
        [ConsoleCommand("dlootui.set")]
        private void UiSet(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            string field = CleanArg(arg.GetString(0, ""));
            string value = string.Join(" ", (arg.Args ?? new Facepunch.StringView[0]).Skip(1).Select(x => x.ToString()).ToArray()).Trim();
            UiState state = GetState(player);
            EditBuffer edit = GetBuffer(player);
            switch (field.ToLower()) {
                case "shortname" : edit.ShortName = CleanArg(value);
                break;
                case "chance" : edit.Chance = CleanArg(value);
                break;
                case "min" : edit.Min = CleanArg(value);
                break;
                case "max" : edit.Max = CleanArg(value);
                break;
                case "skin" : edit.SkinID = CleanArg(value);
                break;
                case "name" : edit.CustomName = value;
                break;
                case "search" : state.Search = value;
                state.Page = 0;
                state.Selected = -1;
                break;
                case "picksearch" : state.PickerSearch = value;
                state.PickerPage = 0;
                state.View = "Picker";
                break;
            }
            Draw(player);
        }
        [ConsoleCommand("dlootui.quick")]
        private void UiQuick(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            string field = CleanArg(arg.GetString(0, ""));
            string value = CleanArg(arg.GetString(1, ""));
            EditBuffer edit = GetBuffer(player);
            switch (field.ToLower()) {
                case "chance" : edit.Chance = value;
                break;
                case "min" : edit.Min = value;
                break;
                case "max" : edit.Max = value;
                break;
                case "skin" : edit.SkinID = value;
                break;
            }
            Draw(player);
        }
        [ConsoleCommand("dlootui.adjust")]
        private void UiAdjust(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            string field = CleanArg(arg.GetString(0, ""));
            int amount = arg.GetInt(1, 0);
            EditBuffer edit = GetBuffer(player);
            if (field.Equals("chance", StringComparison.OrdinalIgnoreCase)) {
                double chance;
                if (!double.TryParse(edit.Chance, out chance)) chance = 0;
                chance = Math.Max(0, Math.Min(100, chance + amount));
                edit.Chance = chance.ToString("0.##");
            }
            else if (field.Equals("min", StringComparison.OrdinalIgnoreCase)) {
                int min;
                if (!int.TryParse(edit.Min, out min)) min = 1;
                min = Math.Max(0, min + amount);
                edit.Min = min.ToString();
                int max;
                if (!int.TryParse(edit.Max, out max)) max = min;
                if (max < min) edit.Max = min.ToString();
            }
            else if (field.Equals("max", StringComparison.OrdinalIgnoreCase)) {
                int max;
                if (!int.TryParse(edit.Max, out max)) max = 1;
                max = Math.Max(0, max + amount);
                int min;
                if (!int.TryParse(edit.Min, out min)) min = 0;
                if (max < min) max = min;
                edit.Max = max.ToString();
            }
            Draw(player);
        }
        [ConsoleCommand("dlootui.save")]
        private void UiSave(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            EditBuffer edit = GetBuffer(player);
            if (!TierExists(state)) {
                SendReply(player, "No existing DungeonEvents tier is selected.");
                Draw(player);
                return;
            }
            ItemDefinition definition = ItemManager.FindItemDefinition((edit.ShortName ?? "").Trim());
            if (definition == null) {
                SendReply(player, "Invalid Rust item shortname: " + (edit.ShortName ?? ""));
                Draw(player);
                return;
            }
            edit.ShortName = definition.shortname;
            JObject obj = LoadLoot(state.Tier, state.File);
            JArray items = (JArray)obj["Items"];
            JObject item = MakeItem(edit);
            if (state.Selected >= 0 && state.Selected < items.Count) items[state.Selected] = item;
            else {
                items.Add(item);
                state.Selected = items.Count - 1;
            }
            if (!SaveLoot(state.Tier, state.File, obj)) {
                SendReply(player, "Save cancelled because the selected tier folder no longer exists.");
                Draw(player);
                return;
            }
            state.View = "Browse";
            SendReply(player, "Loot item saved.");
            Draw(player);
        }
        [ConsoleCommand("dlootui.delete")]
        private void UiDelete(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            if (!TierExists(state)) {
                SendReply(player, "No existing DungeonEvents tier is selected.");
                Draw(player);
                return;
            }
            if (state.Selected < 0) {
                SendReply(player, "No item selected.");
                return;
            }
            JObject obj = LoadLoot(state.Tier, state.File);
            JArray items = (JArray)obj["Items"];
            if (state.Selected >= items.Count) {
                state.Selected = -1;
                Draw(player);
                return;
            }
            string name = items[state.Selected]["ShortName"]?.ToString() ?? "item";
            items.RemoveAt(state.Selected);
            state.Selected = -1;
            _buffers[player.userID] = NewBuffer();
            if (!SaveLoot(state.Tier, state.File, obj)) {
                SendReply(player, "Delete cancelled because the selected tier folder no longer exists.");
                Draw(player);
                return;
            }
            SendReply(player, "Deleted " + name + ".");
            Draw(player);
        }
        [ConsoleCommand("dlootui.duplicate")]
        private void UiDuplicate(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            if (!TierExists(state)) {
                SendReply(player, "No existing DungeonEvents tier is selected.");
                Draw(player);
                return;
            }
            if (state.Selected < 0) return;
            JObject obj = LoadLoot(state.Tier, state.File);
            JArray items = (JArray)obj["Items"];
            if (state.Selected >= items.Count) return;
            items.Add(items[state.Selected].DeepClone());
            state.Selected = items.Count - 1;
            _buffers[player.userID] = BufferFromItem(items[state.Selected] as JObject);
            if (!SaveLoot(state.Tier, state.File, obj)) {
                SendReply(player, "Duplicate cancelled because the selected tier folder no longer exists.");
                Draw(player);
                return;
            }
            SendReply(player, "Duplicated selected loot item.");
            Draw(player);
        }
        [ConsoleCommand("dlootui.clearsearch")]
        private void UiClearSearch(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.Search = "";
            state.Page = 0;
            state.Selected = -1;
            Draw(player);
        }
        [ConsoleCommand("dlootui.pickcat")]
        private void UiPickCategory(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            string category = CleanArg(arg.GetString(0, "All"));
            state.PickerCategory = PickerCategories.FirstOrDefault(x => x.Equals(category, StringComparison.OrdinalIgnoreCase)) ?? "All";
            state.PickerPage = 0;
            state.View = "Picker";
            Draw(player);
        }
        [ConsoleCommand("dlootui.pickpage")]
        private void UiPickPage(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.PickerPage = Mathf.Max(0, state.PickerPage + arg.GetInt(0, 0));
            state.View = "Picker";
            Draw(player);
        }
        [ConsoleCommand("dlootui.pickclear")]
        private void UiPickClear(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            UiState state = GetState(player);
            state.PickerSearch = "";
            state.PickerPage = 0;
            state.View = "Picker";
            Draw(player);
        }
        [ConsoleCommand("dlootui.pickitem")]
        private void UiPickItem(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            string shortName = CleanArg(arg.GetString(0, ""));
            if (string.IsNullOrWhiteSpace(shortName)) return;
            UiState state = GetState(player);
            EditBuffer edit = GetBuffer(player);
            edit.ShortName = shortName.Trim();
            state.View = "Browse";
            Draw(player);
        }
        [ConsoleCommand("dlootui.reload")]
        private void UiReload(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            Interface.Oxide.ReloadPlugin("DungeonEvents");
            SendReply(player, "Reload command sent for DungeonEvents.");
        }
        [ConsoleCommand("dlootui.clearbackups")]
        private void UiClearBackups(ConsoleSystem.Arg arg) {
            BasePlayer player = arg.Player();
            if (!HasAccess(player)) return;
            if (!_backupClearConfirm.Contains(player.userID)) {
                _backupClearConfirm.Add(player.userID);
                SendReply(player, "Click Clear Backups again to confirm.");
                timer.Once(10f, () => {
                    if (player != null) _backupClearConfirm.Remove(player.userID);
                }
                );
                Draw(player);
                return;
            }
            _backupClearConfirm.Remove(player.userID);
            int deleted = ClearBackups();
            SendReply(player, "Deleted " + deleted + " backup file(s).");
            Draw(player);
        }
        private int ClearBackups() {
            if (!Directory.Exists(BackupRootPath)) return 0;
            int deleted = 0;
            foreach (string file in Directory.GetFiles(BackupRootPath, "*.json", SearchOption.AllDirectories)) {
                try {
                    File.Delete(file);
                    deleted++;
                }
                catch (Exception ex) {
                    PrintWarning("Failed deleting backup '" + file + "': " + ex.Message);
                }
            }
            foreach (string dir in Directory.GetDirectories(BackupRootPath, "*", SearchOption.AllDirectories).OrderByDescending(x => x.Length)) {
                try {
                    if (!Directory.EnumerateFileSystemEntries(dir).Any()) Directory.Delete(dir);
                }
                catch {
                }
            }
            return deleted;
        }
        private string NormalizeView(string view) {
            view = CleanArg(view);
            if (view.Equals("Picker", StringComparison.OrdinalIgnoreCase)) return "Picker";
            return "Browse";
        }
        private List<KeyValuePair<int, JObject>> FilteredItems(UiState state, JArray items) {
            string query = (state.Search ?? "").Trim().ToLower();
            return items .Select((token, index) => new KeyValuePair<int, JObject>(index, token as JObject)) .Where(x => x.Value != null) .Where(x => {
                if (string.IsNullOrEmpty(query)) return true;
                string shortName = x.Value["ShortName"]?.ToString().ToLower() ?? "";
                string custom = x.Value["CustomName"]?.ToString().ToLower() ?? "";
                string skin = x.Value["SkinID"]?.ToString().ToLower() ?? "";
                return shortName.Contains(query) || custom.Contains(query) || skin.Contains(query);
            }
            ) .ToList();
        }
        private int CountBackups() {
            try {
                if (!Directory.Exists(BackupRootPath)) return 0;
                return Directory.GetFiles(BackupRootPath, "*.json", SearchOption.AllDirectories).Length;
            }
            catch {
                return 0;
            }
        }
        private void Destroy(BasePlayer player) {
            if (player == null) return;
            CuiHelper.DestroyUi(player, Ui);
            _openPlayers.Remove(player.userID);
            _lockedPositions.Remove(player.userID);
            _queuedDraws.Remove(player.userID);
        }
        private void Draw(BasePlayer player) {
            if (player == null || !player.IsConnected) return;
            ulong userId = player.userID;
            if (!_queuedDraws.Add(userId)) return;
            NextTick(() => {
                if (!_queuedDraws.Remove(userId)) return;
                if (player == null || !player.IsConnected) return;
                DrawNow(player);
            }
            );
        }
        private void DrawNow(BasePlayer player) {
            if (player == null) return;
            bool wasOpen = _openPlayers.Contains(player.userID);
            if (wasOpen) CuiHelper.DestroyUi(player, Content);
            else CuiHelper.DestroyUi(player, Ui);
            UiState state = GetState(player);
            bool hasTier = TierExists(state);
            JObject obj = hasTier ? LoadLoot(state.Tier, state.File) : NewLootObject();
            JArray items = (JArray)obj["Items"];
            List<KeyValuePair<int, JObject>> filtered = FilteredItems(state, items);
            int per = Mathf.Max(1, _config.ItemsPerPage);
            int maxPage = Mathf.Max(0, (int)Math.Ceiling(filtered.Count / (double)per) - 1);
            state.Page = Mathf.Clamp(state.Page, 0, maxPage);
            List<KeyValuePair<int, JObject>> pageItems = filtered.Skip(state.Page * per).Take(per).ToList();
            CuiElementContainer c = new CuiElementContainer();
            if (!wasOpen) {
                c.Add(new CuiPanel {
                    Image = {
                        Color = ColorBackdrop
                    }
                    , RectTransform = {
                        AnchorMin = "0.035 0.055", AnchorMax = "0.965 0.945"
                    }
                    , CursorEnabled = true, KeyboardEnabled = true
                }
                , "Overlay", Ui);
                DrawPersistentFrame(c);
            }
            c.Add(new CuiPanel {
                Image = {
                    Color = "0 0 0 0"
                }
                , RectTransform = {
                    AnchorMin = "0 0", AnchorMax = "1 1"
                }
                , CursorEnabled = false
            }
            , Ui, Content);
            Panel(c, Content, ColorAccent, "0 0.992", "1 1");
            Header(c, state);
            TierPanel(c, state);
            if (!hasTier) {
                NoTiersPage(c);
            }
            else {
                FileTabs(c, state);
                PageTabs(c, state);
                if (state.View.Equals("Picker", StringComparison.OrdinalIgnoreCase)) ItemDatabasePage(c, player, state);
                else BrowsePage(c, player, state, items, filtered.Count, maxPage, pageItems);
            }
            Footer(c, player);
            CuiHelper.AddUi(player, c);
            _openPlayers.Add(player.userID);
            if (!_lockedPositions.ContainsKey(player.userID)) _lockedPositions[player.userID] = player.transform.position;
        }
        private void DrawPersistentFrame(CuiElementContainer c) {
            Panel(c, Ui, ColorAccent, "0 0.992", "1 1");
            Panel(c, Ui, ColorHeader, "0 0.900", "1 0.992");
            Panel(c, Ui, ColorSurface, "0.020 0.105", "0.158 0.885");
            Panel(c, Ui, ColorSurface, "0.175 0.105", "0.980 0.885");
            Panel(c, Ui, ColorHeader, "0.175 0.025", "0.980 0.088");
        }
        private void Header(CuiElementContainer c, UiState state) {
            Panel(c, Content, ColorHeader, "0 0.900", "1 0.992");
            Panel(c, Content, ColorAccent, "0.020 0.920", "0.025 0.972");
            Label(c, Content, "DUNGEON EVENTS", 24, "0.035 0.935", "0.390 0.978", TextAnchor.MiddleLeft);
            MutedLabel(c, Content, "LOOT EDITOR  •  v1.1.0", 9, "0.036 0.912", "0.390 0.940", TextAnchor.MiddleLeft);
            string location = TierExists(state) ? state.Tier.ToUpperInvariant() + "   /   " + FormatFileLabel(state.File) : "NO EXISTING TIERS FOUND";
            Panel(c, Content, ColorSurfaceRaised, "0.585 0.925", "0.925 0.973");
            MutedLabel(c, Content, "ACTIVE SOURCE", 8, "0.600 0.950", "0.705 0.969", TextAnchor.MiddleLeft);
            Label(c, Content, location, 11, "0.600 0.928", "0.910 0.952", TextAnchor.MiddleLeft);
            Button(c, Content, "X", "dlootui.close", "0.945 0.925", "0.980 0.973", ColorDanger, 14);
        }
        private void TierPanel(CuiElementContainer c, UiState state) {
            List<string> tiers = GetTiers();
            Panel(c, Content, ColorSurface, "0.020 0.105", "0.158 0.885");
            Panel(c, Content, ColorAccent, "0.020 0.879", "0.158 0.885");
            Label(c, Content, "DIFFICULTY TIERS", 12, "0.036 0.835", "0.142 0.870", TextAnchor.MiddleLeft);
            MutedLabel(c, Content, tiers.Count + " FOLDER" + (tiers.Count == 1 ? "" : "S") + " DETECTED", 8, "0.036 0.808", "0.142 0.838", TextAnchor.MiddleLeft);
            float y = 0.755f;
            foreach (string tier in tiers.Take(12)) {
                bool selected = tier.Equals(state.Tier, StringComparison.OrdinalIgnoreCase);
                string min = "0.036 " + y.ToString("0.000", CultureInfo.InvariantCulture);
                string max = "0.142 " + (y + 0.042f).ToString("0.000", CultureInfo.InvariantCulture);
                Button(c, Content, "   " + tier.ToUpperInvariant(), "dlootui.tier " + Quote(tier), min, max, selected ? ColorAccentDark : ColorSurfaceRaised, 10, TextAnchor.MiddleLeft);
                if (selected) Panel(c, Content, ColorAccent, "0.036 " + y.ToString("0.000", CultureInfo.InvariantCulture), "0.041 " + (y + 0.042f).ToString("0.000", CultureInfo.InvariantCulture));
                y -= 0.050f;
            }
            if (tiers.Count > 12) MutedLabel(c, Content, "+ " + (tiers.Count - 12) + " MORE FOLDERS", 8, "0.036 0.178", "0.142 0.202", TextAnchor.MiddleCenter);
            Panel(c, Content, ColorHeader, "0.036 0.115", "0.142 0.172");
            MutedLabel(c, Content, "EXISTING FOLDERS ONLY\nManaged by DungeonEvents", 8, "0.044 0.120", "0.134 0.167", TextAnchor.MiddleCenter);
        }
        private void FileTabs(CuiElementContainer c, UiState state) {
            Panel(c, Content, ColorSurface, "0.175 0.818", "0.980 0.885");
            MutedLabel(c, Content, "LOOT SOURCE", 8, "0.192 0.836", "0.295 0.869", TextAnchor.MiddleLeft);
            List<string> files = _config.LootFiles.Select(CleanFile).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
            float start = 0.300f;
            float end = _config.ShowReloa
// ... [Code preview truncated due to length. Full source in attached file] ...

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