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] ...