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

FREE SimplePrefabEditor : 6.1.0 6.1.0

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Reflection;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Simple Prefab Editor", "belisario-afk", "6.1.0")]
    [Description("Spawn, select and live-edit prefab entities with mystery boxes, templates, SignArtist images, rigid group editing, and a Black Ops style Mystery Box with ImageLibrary GUI reel, cascading gun drops, and proximity-buy UI.")]
    public class SimplePrefabEditor : RustPlugin
    {
        private const string PermissionUse = "simpleprefabeditor.use";
        private const string CuiPanelName = "SimplePrefabEditor.Panel";
        private const string DataFileName = "SimplePrefabEditor";

        private const ulong DefaultWoodDoorSkinId = 3613584704;

        // Mystery Box root is now a decorative purple sphere
        private const string MysteryBoxRootPrefab =
            "assets/bundled/prefabs/modding/events/twitch/br_sphere_purple.prefab";

        // Proximity radius for showing the BUY UI
        private const float MysteryBoxProximityRadius = 3f;

        [PluginReference] private Plugin SignArtist;
        [PluginReference] private Plugin ImageLibrary;

        private MethodInfo _miUpdateHasPower;

        #region Data classes

        private class EditState
        {
            public BaseEntity Entity;
            public Vector3 Position;
            public Quaternion Rotation;
            public Vector3 Scale = Vector3.one;

            public float Sensitivity = 0.25f;
            public bool GuiOpen;
            public bool KeyboardMode = true;

            public HashSet<ulong> MysteryBoxEntities = new HashSet<ulong>();
            public bool BoxActive;

            public bool GroupEditEnabled;
            public BaseEntity GroupOrigin;

            // Legacy per-player display
            public bool DisplayActive;
            public BaseEntity DisplayEntity;
            public Vector3 DisplayOriginPos;
            public Quaternion DisplayOriginRot;
            public float DisplayCycleInterval = 5f;
            public float DisplayStartTime;
            public Timer DisplayCycleTimer;
        }

        private class FullEntityDef
        {
            public string Prefab;
            public ulong SkinId;
            public Vector3 LocalPosition;
            public Vector3 LocalEulerAngles;
            public Vector3 LocalScale;

            public bool IsPaintable;
            public string ImageUrl;
            public uint TextureIndex;
        }

        private class FullBoxTemplate
        {
            public string Name;
            public List<FullEntityDef> Entities = new List<FullEntityDef>();
        }

        private class MysteryBoxDef
        {
            public uint RootEntityId;
            public Vector3 RootPosition;
            public Vector3 DisplayOffset = new Vector3(0f, 1.2f, 0f);
        }

        private class BoxSaveData
        {
            public Dictionary<ulong, Dictionary<string, List<ulong>>> PlayerBoxes =
                new Dictionary<ulong, Dictionary<string, List<ulong>>>();

            public Dictionary<string, FullBoxTemplate> FullBoxTemplates =
                new Dictionary<string, FullBoxTemplate>(StringComparer.OrdinalIgnoreCase);

            public Dictionary<uint, MysteryBoxDef> MysteryBoxes =
                new Dictionary<uint, MysteryBoxDef>();
        }

        private class SavedEntity
        {
            public string Prefab;
            public ulong SkinId;
            public Vector3 Position;
            public Quaternion Rotation;
            public Vector3 Scale;
        }

        // Runtime mystery box state
        private class MysteryBoxRuntime
        {
            public bool Busy;                // spinning or waiting for pickup
            public string CurrentShortname;  // last chosen shortname
            public ulong ResultDropNetId;    // net ID of spawned drop for this roll

            public HashSet<ulong> ActivePlayers = new HashSet<ulong>();
        }

        #endregion

        private BoxSaveData _boxData;
        private readonly Dictionary<ulong, EditState> editing = new Dictionary<ulong, EditState>();
        private SavedEntity _lastSaved;
        private readonly Dictionary<ulong, string> _signUrls = new Dictionary<ulong, string>();

        private readonly (string shortname, float weight)[] DefaultGunPool =
        {
            ("rifle.ak",        1f),
            ("smg.thompson",    1f),
            ("smg.2",           1f), // custom SMG
            ("pistol.python",   1f),
            ("pistol.semiauto", 1f),
            ("shotgun.pump",    1f),
            ("lmg.m249",        0.2f),
            ("pistol.water",    0.1f) // extra rare
        };

        private List<(string shortname, float weight)> _gunPool = new List<(string, float)>();
        private readonly Dictionary<uint, MysteryBoxRuntime> _mysteryRuntime =
            new Dictionary<uint, MysteryBoxRuntime>();

        // Gun shortname -> ImageLibrary key
        private Dictionary<string, string> _gunImageKeys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        // Config
        private PluginConfig _config;

        // Proximity loop timer
        private Timer _mboxProximityTimer;

        // Track which box UI is visible for each player (only 1 box UI at a time per player)
        private readonly Dictionary<ulong, uint> _playerVisibleBox = new Dictionary<ulong, uint>();

        private class PluginConfig
        {
            public Dictionary<string, string> GunImages = new Dictionary<string, string>
            {
                // Default placeholders – replace with your own image URLs
                ["rifle.ak"]        = "https://via.placeholder.com/256x256.png?text=AK",
                ["smg.thompson"]    = "https://via.placeholder.com/256x256.png?text=Thompson",
                ["smg.2"]           = "https://via.placeholder.com/256x256.png?text=Custom+SMG",
                ["pistol.python"]   = "https://via.placeholder.com/256x256.png?text=Python",
                ["pistol.semiauto"] = "https://via.placeholder.com/256x256.png?text=Semi+Pistol",
                ["shotgun.pump"]    = "https://via.placeholder.com/256x256.png?text=Pump",
                ["lmg.m249"]        = "https://via.placeholder.com/256x256.png?text=M249",
                ["pistol.water"]    = "https://via.placeholder.com/256x256.png?text=Waterpipe"
            };
        }

        private PluginConfig GetDefaultConfig() => new PluginConfig();

        protected override void LoadDefaultConfig()
        {
            _config = GetDefaultConfig();
            SaveConfig();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _config = Config.ReadObject<PluginConfig>();
                if (_config == null)
                    throw new Exception("Config null");
            }
            catch
            {
                PrintWarning("Config invalid, loading default.");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_config, true);
        }

        #region Init / Data

        private void Init()
        {
            permission.RegisterPermission(PermissionUse, this);

            cmd.AddChatCommand("spawn_edit", this, CmdSpawnEdit);
            cmd.AddChatCommand("select_edit", this, CmdSelectEdit);
            cmd.AddChatCommand("save_edit", this, CmdSaveEdit);
            cmd.AddChatCommand("respawn_last", this, CmdRespawnLast);

            cmd.AddChatCommand("edit_gui", this, CmdEditGui);
            cmd.AddChatCommand("edit_close", this, CmdEditClose);
            cmd.AddChatCommand("edit_sensitivity", this, CmdEditSensitivity);

            cmd.AddChatCommand("edit_box_new", this, CmdEditBoxNew);
            cmd.AddChatCommand("edit_box_add", this, CmdEditBoxAdd);
            cmd.AddChatCommand("edit_box_list", this, CmdEditBoxList);
            cmd.AddChatCommand("edit_box_on", this, CmdEditBoxOn);
            cmd.AddChatCommand("edit_box_off", this, CmdEditBoxOff);

            cmd.AddChatCommand("edit_box_save", this, CmdEditBoxSave);
            cmd.AddChatCommand("edit_box_load", this, CmdEditBoxLoad);
            cmd.AddChatCommand("edit_box_saves", this, CmdEditBoxSaves);
            cmd.AddChatCommand("edit_box_delete", this, CmdEditBoxDelete);

            cmd.AddChatCommand("edit_box_save_full", this, CmdEditBoxSaveFull);
            cmd.AddChatCommand("edit_box_spawn", this, CmdEditBoxSpawn);

            cmd.AddChatCommand("edit_group_on", this, CmdEditGroupOn);
            cmd.AddChatCommand("edit_group_off", this, CmdEditGroupOff);

            cmd.AddChatCommand("edit_display_start", this, CmdEditDisplayStart);
            cmd.AddChatCommand("edit_display_stop", this, CmdEditDisplayStop);

            cmd.AddChatCommand("mbox_register", this, CmdMBoxRegister);
            cmd.AddChatCommand("mbox_unregister", this, CmdMBoxUnregister);
            cmd.AddChatCommand("mbox_use", this, CmdMBoxUse);
            cmd.AddChatCommand("mbox_test", this, CmdMBoxTest);
            cmd.AddChatCommand("mbox_clearall", this, CmdMBoxClearAll);
        }

        private void OnServerInitialized()
        {
            LoadData();
            CacheReflection();
            BuildGunPool();
            InitGunImages();
            StartMysteryBoxProximityLoop();
        }

        private void Unload()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                CloseGui(player);
                if (editing.TryGetValue(player.userID, out var state))
                    StopDisplayForState(state);

                DestroyMysteryGuiForPlayer(player);
                DestroyBuyUi(player);
            }

            _mboxProximityTimer?.Destroy();
            _mboxProximityTimer = null;

            SaveData();
            editing.Clear();
            _mysteryRuntime.Clear();
            _playerVisibleBox.Clear();
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;
            CloseGui(player);
            DestroyMysteryGuiForPlayer(player);
            DestroyBuyUi(player);

            if (editing.TryGetValue(player.userID, out var state))
            {
                StopDisplayForState(state);
                editing.Remove(player.userID);
            }

            _playerVisibleBox.Remove(player.userID);
        }

        private void LoadData()
        {
            try
            {
                _boxData = Interface.Oxide.DataFileSystem.ReadObject<BoxSaveData>(DataFileName);
                if (_boxData?.PlayerBoxes == null)
                    _boxData = new BoxSaveData();
                if (_boxData.FullBoxTemplates == null)
                    _boxData.FullBoxTemplates = new Dictionary<string, FullBoxTemplate>(StringComparer.OrdinalIgnoreCase);
                if (_boxData.MysteryBoxes == null)
                    _boxData.MysteryBoxes = new Dictionary<uint, MysteryBoxDef>();
            }
            catch
            {
                _boxData = new BoxSaveData
                {
                    FullBoxTemplates = new Dictionary<string, FullBoxTemplate>(StringComparer.OrdinalIgnoreCase),
                    MysteryBoxes = new Dictionary<uint, MysteryBoxDef>()
                };
            }
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFileName, _boxData);
        }

        #endregion

        #region Gun Pool & Images

        private void BuildGunPool()
        {
            _gunPool.Clear();

            foreach (var entry in DefaultGunPool)
            {
                var def = ItemManager.FindItemDefinition(entry.shortname);
                if (def == null)
                {
                    PrintWarning($"[SimplePrefabEditor] Gun shortname not found: {entry.shortname}");
                    continue;
                }
                _gunPool.Add((entry.shortname, entry.weight));
            }

            if (_gunPool.Count == 0)
                PrintWarning("[SimplePrefabEditor] Gun pool is empty; mystery box will not spawn anything.");
            else
                Puts($"[SimplePrefabEditor] Gun pool built with {_gunPool.Count} entries (pistol.water weighted low).");
        }

        private string GetRandomGunShortname()
        {
            if (_gunPool == null || _gunPool.Count == 0)
                return null;

            float totalWeight = 0f;
            foreach (var g in _gunPool)
                totalWeight += g.weight;

            if (totalWeight <= 0f)
                return null;

            float roll = UnityEngine.Random.Range(0f, totalWeight);
            float cumulative = 0f;

            foreach (var g in _gunPool)
            {
                cumulative += g.weight;
                if (roll <= cumulative)
                    return g.shortname;
            }

            return _gunPool[_gunPool.Count - 1].shortname;
        }

        private void InitGunImages()
        {
            _gunImageKeys.Clear();

            if (ImageLibrary == null)
            {
                PrintWarning("[SimplePrefabEditor] ImageLibrary not found. Mystery Box GUI will show text only.");
                return;
            }

            foreach (var kv in _config.GunImages)
            {
                string shortname = kv.Key;
                string url = kv.Value;
                if (string.IsNullOrEmpty(url))
                    continue;

                string key = $"spe_{shortname}";

                try
                {
                    ImageLibrary.Call("AddImage", url, key, (ulong)0);
                    _gunImageKeys[shortname] = key;
                }
                catch (Exception ex)
                {
                    PrintWarning($"[SimplePrefabEditor] Failed to add image for {shortname} via ImageLibrary: {ex}");
                }
            }

            Puts($"[SimplePrefabEditor] Registered {_gunImageKeys.Count} gun images via ImageLibrary.");
        }

        private string GetGunImageKey(string shortname)
        {
            if (string.IsNullOrEmpty(shortname))
                return null;

            if (_gunImageKeys.TryGetValue(shortname, out var key))
                return key;

            return null;
        }

        #endregion

        #region SignArtist hook

        private void OnImagePost(BasePlayer player, string url, bool raw, BaseEntity entity, uint textureIndex)
        {
            if (entity == null || entity.net == null || string.IsNullOrEmpty(url))
                return;
            if (textureIndex != 0) return;

            _signUrls[entity.net.ID.Value] = url;
        }

        #endregion

        #region Spawn with Skin

        private BaseEntity SpawnSkinnedEntity(string prefab, Vector3 position, Quaternion rotation, ulong skinId = 0)
        {
            BaseEntity ent;
            try
            {
                ent = GameManager.server.CreateEntity(prefab, position, rotation, true);
            }
            catch (Exception ex)
            {
                PrintWarning($"SpawnSkinnedEntity error for '{prefab}': {ex}");
                return null;
            }

            if (ent == null)
                return null;

            if (skinId != 0)
                ent.skinID = skinId;

            ent.Spawn();

            ent.transform.position   = position;
            ent.transform.rotation   = rotation;
            ent.transform.localScale = Vector3.one;
            ent.SendNetworkUpdate();

            return ent;
        }

        #endregion

        #region Core Commands

        private void CmdSpawnEdit(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (args == null || args.Length < 1)
            {
                PrintToChat(player,
                    "<color=#ffcc00>Usage:</color> /spawn_edit <prefabpath> [skinId]\n" +
                    $"Example:\n/spawn_edit assets/prefabs/deployable/door.hinged.wood/door.hinged.wood.prefab {DefaultWoodDoorSkinId}");
                return;
            }

            var prefabPath = args[0];
            ulong skinId = 0;
            if (args.Length >= 2)
                ulong.TryParse(args[1], out skinId);

            var pos = player.eyes.position + player.eyes.BodyForward() * 2f;
            var rot = Quaternion.identity;

            var ent = SpawnSkinnedEntity(prefabPath, pos, rot, skinId);
            if (ent == null)
            {
                PrintToChat(player, $"<color=#ff0000>Could not create entity:</color> {prefabPath}");
                return;
            }

            var state = GetOrCreateState(player);
            state.Entity   = ent;
            state.Position = ent.transform.position;
            state.Rotation = ent.transform.rotation;
            state.Scale    = ent.transform.localScale;

            PrintToChat(player,
                $"<color=#00ff00>Spawned & selected:</color> {prefabPath} {(skinId != 0 ? $"(skin {skinId})" : "")}");
        }

        private void CmdSelectEdit(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var ent = GetLookAtEntity(player);
            if (ent == null || ent.transform == null)
            {
                PrintToChat(player, "<color=#ffcc00>No valid entity where you’re looking.</color>");
                return;
            }

            var state = GetOrCreateState(player);
            state.Entity   = ent;
            state.Position = ent.transform.position;
            state.Rotation = ent.transform.rotation;
            state.Scale    = ent.transform.localScale;

            PrintToChat(player,
                $"<color=#00ff00>Selected:</color> {ent.ShortPrefabName} ({ent.PrefabName})");
        }

        private void CmdSaveEdit(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.Entity == null || state.Entity.IsDestroyed || state.Entity.transform == null)
            {
                PrintToChat(player, "<color=#ffcc00>No active entity selected.</color>");
                return;
            }

            var e = state.Entity;
            var t = e.transform;
            var pos = t.position;
            var rotEuler = t.rotation.eulerAngles;
            var scale = t.localScale;
            ulong skinId = e.skinID;

            _lastSaved = new SavedEntity
            {
                Prefab = e.PrefabName,
                SkinId = skinId,
                Position = pos,
                Rotation = Quaternion.Euler(rotEuler),
                Scale = scale
            };

            PrintToChat(player,
                "<color=#00ffff>Saved current entity.</color>\n" +
                "Use <color=#ffffff>/respawn_last</color> to spawn a copy later.");
        }

        private void CmdRespawnLast(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (_lastSaved == null || string.IsNullOrEmpty(_lastSaved.Prefab))
            {
                PrintToChat(player, "<color=#ffcc00>No last saved entity.</color>");
                return;
            }

            var e = SpawnSkinnedEntity(_lastSaved.Prefab, _lastSaved.Position, _lastSaved.Rotation, _lastSaved.SkinId);
            if (e == null)
            {
                PrintToChat(player, "<color=#ff0000>Failed to respawn last entity.</color>");
                return;
            }

            e.transform.localScale = _lastSaved.Scale;
            e.networkEntityScale = true;
            e.SendNetworkUpdate();

            var state = GetOrCreateState(player);
            state.Entity   = e;
            state.Position = e.transform.position;
            state.Rotation = e.transform.rotation;
            state.Scale    = e.transform.localScale;

            PrintToChat(player,
                $"<color=#00ff00>Respawned:</color> {e.ShortPrefabName} (skin {_lastSaved.SkinId})");
        }

        #endregion

        #region GUI / Sensitivity

        private void CmdEditGui(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var state = GetOrCreateState(player);
            if (state.Entity == null || state.Entity.IsDestroyed || state.Entity.transform == null)
            {
                PrintToChat(player, "<color=#ffcc00>No active entity. Use /spawn_edit or /select_edit first.</color>");
                return;
            }

            OpenGui(player, state);
        }

        private void CmdEditClose(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            CloseGui(player);
        }

        private void CmdEditSensitivity(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var state = GetOrCreateState(player);

            if (args == null || args.Length < 1)
            {
                PrintToChat(player,
                    $"<color=#ffcc00>Usage:</color> /edit_sensitivity <value> (current {state.Sensitivity:F3})");
                return;
            }

            if (!float.TryParse(args[0], System.Globalization.NumberStyles.Float,
                    System.Globalization.CultureInfo.InvariantCulture, out var value) || value <= 0f)
            {
                PrintToChat(player, "<color=#ff0000>Invalid sensitivity. Must be > 0.</color>");
                return;
            }

            state.Sensitivity = value;
            PrintToChat(player, $"<color=#00ff00>Sensitivity set to:</color> {state.Sensitivity:F3}");

            if (state.GuiOpen)
                OpenGui(player, state);
        }

        #endregion

        #region Mystery Box (ID-based editor only)

        private void CmdEditBoxNew(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var state = GetOrCreateState(player);
            state.MysteryBoxEntities.Clear();
            state.BoxActive = false;

            PrintToChat(player, "<color=#00ff00>New box started.</color> Use /edit_box_add for each piece.");
        }

        private void CmdEditBoxAdd(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.Entity == null || state.Entity.IsDestroyed || state.Entity.net == null)
            {
                PrintToChat(player, "<color=#ffcc00>No active entity. Use /select_edit first.</color>");
                return;
            }

            var ent = state.Entity;
            state.MysteryBoxEntities.Add(ent.net.ID.Value);
            PrintToChat(player, $"<color=#00ff00>Added:</color> {ent.ShortPrefabName} ({ent.net.ID.Value})");
        }

        private void CmdEditBoxList(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.MysteryBoxEntities.Count == 0)
            {
                PrintToChat(player, "<color=#ffcc00>Box is empty.</color>");
                return;
            }

            PrintToChat(player, "<color=#00ffff>=== Box Entities ===</color>");
            foreach (var id in state.MysteryBoxEntities)
            {
                var ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as BaseEntity;
                if (ent == null)
                    PrintToChat(player, $"{id} - <color=#ff0000>missing/destroyed</color>");
                else
                    PrintToChat(player, $"{id} - {ent.ShortPrefabName}");
            }
        }

        private void CmdEditBoxOn(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.MysteryBoxEntities.Count == 0)
            {
                PrintToChat(player, "<color=#ffcc00>No entities in box.</color>");
                return;
            }

            state.BoxActive = true;
            int count = 0;
            foreach (var id in state.MysteryBoxEntities)
            {
                var ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as BaseEntity;
                if (ent == null || ent.IsDestroyed) continue;
                if (ForceLightOn(ent)) count++;
            }

            PrintToChat(player, $"<color=#00ff00>Lights forced on for {count} entities.</color>");
        }

        private void CmdEditBoxOff(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state))
            {
                PrintToChat(player, "<color=#ffcc00>No current edit state.</color>");
                return;
            }

            state.BoxActive = false;
            PrintToChat(player, "<color=#ffcc00>Mystery box deactivated.</color>");
        }

        private void CmdEditBoxSave(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.MysteryBoxEntities.Count == 0)
            {
                PrintToChat(player, "<color=#ffcc00>Box is empty.</color>");
                return;
            }

            if (args == null || args.Length < 1 || string.IsNullOrEmpty(args[0]))
            {
                PrintToChat(player, "<color=#ffcc00>Usage:</color> /edit_box_save <name>");
                return;
            }

            var name = args[0].Trim();
            if (string.IsNullOrEmpty(name))
            {
                PrintToChat(player, "<color=#ffcc00>Name cannot be empty.</color>");
                return;
            }

            if (!_boxData.PlayerBoxes.TryGetValue(player.userID, out var map))
            {
                map = new Dictionary<string, List<ulong>>(StringComparer.OrdinalIgnoreCase);
                _boxData.PlayerBoxes[player.userID] = map;
            }

            map[name] = new List<ulong>(state.MysteryBoxEntities);
            SaveData();

            PrintToChat(player,
                $"<color=#00ff00>Saved ID-based box:</color> {name} ({state.MysteryBoxEntities.Count} IDs)");
        }

        private void CmdEditBoxLoad(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (args == null || args.Length < 1 || string.IsNullOrEmpty(args[0]))
            {
                PrintToChat(player, "<color=#ffcc00>Usage:</color> /edit_box_load <name>");
                return;
            }

            var name = args[0].Trim();

            if (!_boxData.PlayerBoxes.TryGetValue(player.userID, out var map) ||
                !map.TryGetValue(name, out var ids) ||
                ids == null || ids.Count == 0)
            {
                PrintToChat(player, $"<color=#ffcc00>No saved box with name:</color> {name}");
                return;
            }

            var state = GetOrCreateState(player);
            state.MysteryBoxEntities.Clear();

            int valid = 0;
            BaseEntity first = null;

            foreach (var id in ids)
            {
                var ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as BaseEntity;
                if (ent == null || ent.IsDestroyed || ent.transform == null)
                    continue;

                state.MysteryBoxEntities.Add(id);
                valid++;
                if (first == null)
                    first = ent;
            }

            if (valid == 0)
            {
                PrintToChat(player,
                    $"<color=#ffcc00>Saved box '{name}' has no valid entities alive now.</color>");
                return;
            }

            state.Entity   = first;
            state.Position = first.transform.position;
            state.Rotation = first.transform.rotation;
            state.Scale    = first.transform.localScale;

            PrintToChat(player,
                $"<color=#00ff00>Loaded:</color> {name} ({valid} valid entities)");
        }

        private void CmdEditBoxSaves(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!_boxData.PlayerBoxes.TryGetValue(player.userID, out var map) || map.Count == 0)
            {
                PrintToChat(player, "<color=#ffcc00>No ID-based box saves.</color>");
                return;
            }

            PrintToChat(player, "<color=#00ffff>=== Your ID-based boxes ===</color>");
            foreach (var kv in map)
            {
                int count = kv.Value?.Count ?? 0;
                PrintToChat(player, $"{kv.Key} ({count} IDs)");
            }
        }

        private void CmdEditBoxDelete(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (args == null || args.Length < 1 || string.IsNullOrEmpty(args[0]))
            {
                PrintToChat(player, "<color=#ffcc00>Usage:</color> /edit_box_delete <name>");
                return;
            }

            var name = args[0].Trim();

            if (!_boxData.PlayerBoxes.TryGetValue(player.userID, out var map) || !map.Remove(name))
            {
                PrintToChat(player, $"<color=#ffcc00>No saved box with name:</color> {name}");
                return;
            }

            SaveData();
            PrintToChat(player, $"<color=#ff0000>Deleted box:</color> {name}");
        }

        #endregion

        #region Full Templates

        private void CmdEditBoxSaveFull(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.MysteryBoxEntities.Count == 0)
            {
                PrintToChat(player, "<color=#ffcc00>Box is empty. Use /edit_box_add first.</color>");
                return;
            }

            if (args == null || args.Length < 1 || string.IsNullOrEmpty(args[0]))
            {
                PrintToChat(player, "<color=#ffcc00>Usage:</color> /edit_box_save_full <templateName>");
                return;
            }

            string templateName = args[0].Trim();
            if (string.IsNullOrEmpty(templateName))
            {
                PrintToChat(player, "<color=#ffcc00>Template name cannot be empty.</color>");
                return;
            }

            BaseEntity origin = null;
            if (state.Entity != null && state.Entity.net != null &&
                state.MysteryBoxEntities.Contains(state.Entity.net.ID.Value))
                origin = state.Entity;
            else
            {
                foreach (var id in state.MysteryBoxEntities)
                {
                    var ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as BaseEntity;
                    if (ent != null && !ent.IsDestroyed && ent.transform != null)
                    {
                        origin = ent;
                        break;
                    }
                }
            }

            if (origin == null || origin.transform == null)
            {
                PrintToChat(player, "<color=#ffcc00>Could not find valid origin in box.</color>");
                return;
            }

            Vector3 originPos = origin.transform.position;
            Quaternion originRot = origin.transform.rotation;

            var template = new FullBoxTemplate { Name = templateName };
            int saved = 0;

            foreach (var id in state.MysteryBoxEntities)
            {
                var ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as BaseEntity;
                if (ent == null || ent.IsDestroyed || ent.transform == null)
                    continue;
                if (string.IsNullOrEmpty(ent.PrefabName))
                    continue;

                Vector3 worldPos = ent.transform.position;
                Quaternion worldRot = ent.transform.rotation;
                Vector3 worldScale = ent.transform.localScale;
                ulong skinId = ent.skinID;

                Vector3 localPos = Quaternion.Inverse(originRot) * (worldPos - originPos);
                Quaternion localRot = Quaternion.Inverse(originRot) * worldRot;

                var def = new FullEntityDef
                {
                    Prefab = ent.PrefabName,
                    SkinId = skinId,
                    LocalPosition = localPos,
                    LocalEulerAngles = localRot.eulerAngles,
                    LocalScale = worldScale,
                    IsPaintable = false,
                    ImageUrl = null,
                    TextureIndex = 0
                };

                TryCapturePaintableUrl(ent, ref def);

                template.Entities.Add(def);
                saved++;
            }

            if (saved == 0)
            {
                PrintToChat(player, "<color=#ffcc00>No valid entities for template.</color>");
                return;
            }

            _boxData.FullBoxTemplates[templateName] = template;
            SaveData();

            PrintToChat(player,
                $"<color=#00ff00>Saved template:</color> {templateName} ({saved} entities, origin {origin.ShortPrefabName})");
        }

        private void CmdEditBoxSpawn(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (args == null || args.Length < 2 ||
                string.IsNullOrEmpty(args[0]) || string.IsNullOrEmpty(args[1]))
            {
                PrintToChat(player, "<color=#ffcc00>Usage:</color> /edit_box_spawn <templateName> <instanceName>");
                return;
            }

            string templateName = args[0].Trim();
            string instanceName = args[1].Trim();

            if (!_boxData.FullBoxTemplates.TryGetValue(templateName, out var template) ||
                template == null || template.Entities == null || template.Entities.Count == 0)
            {
                PrintToChat(player, $"<color=#ffcc00>No template with name:</color> {templateName}");
                return;
            }

            var rayPos = player.eyes.position;
            var rayDir = player.eyes.BodyForward();
            Vector3 spawnPos = rayPos + rayDir * 3f;
            RaycastHit hit;
            if (Physics.Raycast(rayPos, rayDir, out hit, 50f))
                spawnPos = hit.point;

            Quaternion rootRot = Quaternion.identity;

            var newEntities = new List<BaseEntity>();

            foreach (var def in template.Entities)
            {
                Vector3 worldPos = spawnPos + rootRot * def.LocalPosition;
                Quaternion localRot = Quaternion.Euler(def.LocalEulerAngles);
                Quaternion worldRot = rootRot * localRot;

                var e = SpawnSkinnedEntity(def.Prefab, worldPos, worldRot, def.SkinId);
                if (e == null) continue;

                e.transform.localScale = def.LocalScale;
                e.networkEntityScale = true;
                e.SendNetworkUpdate();

                newEntities.Add(e);

                if (def.IsPaintable && !string.IsNullOrEmpty(def.ImageUrl))
                    TryApplyPaintableUrl(player, e, def);
            }

            if (newEntities.Count == 0)
            {
                PrintToChat(player, "<color=#ff0000>Failed to spawn any entities from template.</color>");
                return;
            }

            var state = GetOrCreateState(player);
            state.MysteryBoxEntities.Clear();
            foreach (var e in newEntities)
                if (e?.net != null)
                    state.MysteryBoxEntities.Add(e.net.ID.Value);

            var root = newEntities[0];
            state.Entity   = root;
            state.Position = root.transform.position;
            state.Rotation = root.transform.rotation;
            state.Scale    = root.transform.localScale;

            PrintToChat(player,
                $"<color=#00ff00>Spawned instance:</color> {instanceName} from {templateName} " +
                $"({newEntities.Count} entities)");
        }

        #endregion

        #region Group Edit Mode

        private void CmdEditGroupOn(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state) ||
                state.Entity == null || state.Entity.IsDestroyed || state.Entity.transform == null)
            {
                PrintToChat(player, "<color=#ffcc00>No active entity. Use /select_edit first.</color>");
                return;
            }

            if (state.MysteryBoxEntities == null || state.MysteryBoxEntities.Count == 0)
            {
                PrintToChat(player, "<color=#ffcc00>Box is empty. Use /edit_box_add first.</color>");
                return;
            }

            state.GroupEditEnabled = true;
            state.GroupOrigin = state.Entity;

            PrintToChat(player,
                "<color=#00ff00>Group edit ON.</color> " +
                "Move/rotate while holding LMB will move the whole box as a rigid shape.");
        }

        private void CmdEditGroupOff(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            if (!editing.TryGetValue(player.userID, out var state))
                return;

            state.GroupEditEnabled = false;
            state.GroupOrigin = null;

            PrintToChat(player, "<color=#ffcc00>Group edit OFF.</color> Edits are single-entity again.");
        }

        #endregion

        #region Legacy Display (per-player floating model)

        private void CmdEditDisplayStart(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var state = GetOrCreateState(player);

            if (state.Entity == null || state.Entity.IsDestroyed || state.Entity.transform == null)
            {
                PrintToChat(player, "<color=#ffcc00>No active entity to anchor display.</color> Use /select_edit first.");
                return;
            }

            float interval = 5f;
            if (args != null && args.Length >= 1)
            {
                float.TryParse(args[0], System.Globalization.NumberStyles.Float,
                    System.Globalization.CultureInfo.InvariantCulture, out interval);
                if (interval <= 0f)
                    interval = 5f;
            }

            state.DisplayOriginPos = state.Entity.transform.position + new Vector3(0f, 1.0f, 0f);
            state.DisplayOriginRot = Quaternion.identity;
            state.DisplayCycleInterval = interval;

            StopDisplayForState(state, keepActiveFlag: false);

            state.DisplayActive = true;

            SpawnNextDisplayGunForStateLegacy(state);

            state.DisplayCycleTimer = timer.Every(state.DisplayCycleInterval, () =>
            {
                SpawnNextDisplayGunForStateLegacy(state);
            });

            PrintToChat(player,
                $"<color=#00ff00>Display started.</color> Random model every {interval:0.##}s.");
        }

        private void CmdEditDisplayStop(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var state = GetOrCreateState(player);
            StopDisplayForState(state);

            PrintToChat(player, "<color=#ffcc00>Display stopped.</color>");
        }

        private void StopDisplayForState(EditState state, bool keepActiveFlag = false)
        {
            if (state == null)
                return;

            state.DisplayCycleTimer?.Destroy();
            state.DisplayCycleTimer = null;

            if (state.DisplayEntity != null && !state.DisplayEntity.IsDestroyed)
            {
                state.DisplayEntity.Kill();
                state.DisplayEntity = null;
            }

            if (!keepActiveFlag)
                state.DisplayActive = false;
        }

        private void SpawnNextDisplayGunForStateLegacy(EditState state)
        {
            if (state == null || !state.DisplayActive)
                return;

            if (state.DisplayEntity != null && !state.DisplayEntity.IsDestroyed)
            {
                state.DisplayEntity.Kill();
                state.DisplayEntity = null;
            }

            string prefab = "assets/prefabs/misc/item drop/item_drop.prefab";
            var pos = state.DisplayOriginPos;
            var rot = state.DisplayOriginRot;

            BaseEntity ent;
            try
            {
                ent = GameManager.server.CreateEntity(prefab, pos, rot, true);
            }
            catch
            {
                return;
            }

            if (ent == null)
                return;

            ent.Spawn();
            ent.transform.position = pos;
            ent.transform.rotation = rot;
            ent.SendNetworkUpdate();

            state.DisplayEntity = ent;
            state.DisplayStartTime = Time.realtimeSinceStartup;
        }

        #endregion

        #region Mystery Box Registration & Trigger (Proximity UI)

        private void CmdMBoxRegister(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var ent = GetLookAtEntity(player, 5f);
            if (ent == null || ent.net == null)
            {
                PrintToChat(player, "<color=#ffcc00>Look at the purple sphere you want to register.</color>");
                return;
            }

            if (!string.Equals(ent.PrefabName, MysteryBoxRootPrefab, StringComparison.OrdinalIgnoreCase))
            {
                PrintToChat(player,
                    $"<color=#ffcc00>This is not the correct prefab for the Mystery Box root.</color>\nExpected: {MysteryBoxRootPrefab}\nGot: {ent.PrefabName}");
                return;
            }

            uint id = (uint)ent.net.ID.Value;

            var def = new MysteryBoxDef
            {
                RootEntityId = id,
                RootPosition = ent.transform.position,
                DisplayOffset = new Vector3(0f, 1.2f, 0f)
            };

            _boxData.MysteryBoxes[id] = def;
            SaveData();

            _mysteryRuntime[id] = new MysteryBoxRuntime();

            PrintToChat(player,
                $"<color=#00ff00>Registered Mystery Box root (purple sphere):</color> {ent.ShortPrefabName} ({id})");
            PrintToChat(player,
                "Players will see a BUY UI when close to this sphere and can click it to roll the Mystery Box.");
        }

        private void CmdMBoxUnregister(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var ent = GetLookAtEntity(player, 5f);
            if (ent == null || ent.net == null)
            {
                PrintToChat(player, "<color=#ffcc00>Look at the sphere root you want to unregister.</color>");
                return;
            }

            uint id = (uint)ent.net.ID.Value;

            if (!_boxData.MysteryBoxes.Remove(id))
            {
                PrintToChat(player, $"<color=#ffcc00>That entity ({id}) is not a registered Mystery Box.</color>");
                return;
            }

            if (_mysteryRuntime.TryGetValue(id, out var rt))
            {
                ClearMysteryBoxGuiForAll(rt);
                _mysteryRuntime.Remove(id);
            }

            // Clean any BUY UI for this box
            foreach (var p in BasePlayer.activePlayerList)
            {
                if (p == null) continue;
                if (_playerVisibleBox.TryGetValue(p.userID, out var current) && current == id)
                {
                    DestroyBuyUi(p);
                    _playerVisibleBox[p.userID] = 0;
                }
            }

            SaveData();

            PrintToChat(player,
                $"<color=#ff0000>Unregistered Mystery Box:</color> {ent.ShortPrefabName} ({id})");
        }

        // Optional: manual command to use the box while looking at it
        private void CmdMBoxUse(BasePlayer player, string command, string[] args)
        {
            if (!HasPermission(player))
                return;

            var ent = GetLookAtEntity(player, 5f);
            if (ent == null || ent.net == null)
            {
                PrintToChat(player, "<color=#ffcc00>Look at a registered Mystery Box sphere.</color>");
                return;
            }

            if (!string.Equals(ent.PrefabName, MysteryBoxRootPrefab, StringComparison.OrdinalIgnoreCase))
            {
                PrintToChat(player, "<color=#ffcc00>That is not a Mystery Box root (sphere).</color>");
                return;
            }

            uint id = (uint)ent.net.ID.Value;
            if (!_boxData.MysteryBoxes.ContainsKey(id))
            {
                PrintToChat(player, "<color=#ffcc00>This sphere is not registered as a Mystery Box. Ask an admin to run /mbox_register.</color>");
                return;
            }

            if (!_mysteryRuntime.TryGetValue(id, out var rt))
            {
                rt = new MysteryBoxRuntime();
                _mysteryRuntime[id] = rt;
            }

            if (rt.Busy)
            {
             
// ... [Code preview truncated due to length. Full source in attached file] ...

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