using System; using System.Collections.Generic; using UnityEngine; using Oxide.Core; using Newtonsoft.Json; namespace Oxide.Plugins { [Info("VirtualRepair", "LosPibes Gaming", "1.2.0")] [Description("Allows players to open a virtual portable repair bench anywhere using /rep or /repair")] public class VirtualRepair : RustPlugin { #region Constants & Fields private const string PermUse = "virtualrepair.use"; private const string PermBypassCooldown = "virtualrepair.bypasscooldown"; private const string PrefabRepair = "assets/bundled/prefabs/static/repairbench_static.prefab"; private readonly Dictionary _activeBenches = new Dictionary(); private readonly Dictionary _cooldowns = new Dictionary(); #endregion #region Configuration private Configuration _config; public class Configuration { [JsonProperty(PropertyName = "Chat Prefix")] public string ChatPrefix { get; set; } = "[Repair]"; [JsonProperty(PropertyName = "Cooldown Seconds (0 = Disabled)")] public int CooldownSeconds { get; set; } = 0; [JsonProperty(PropertyName = "Require Building Privilege (Cupboard Zone)")] public bool RequireBuildingPrivilege { get; set; } = false; [JsonProperty(PropertyName = "Block While Building Blocked")] public bool BlockWhileBuildingBlocked { get; set; } = true; } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new Exception(); } catch { PrintError("Failed to load configuration file. Generating default settings..."); LoadDefaultConfig(); } SaveConfig(); } protected override void LoadDefaultConfig() => _config = new Configuration(); protected override void SaveConfig() => Config.WriteObject(_config, true); #endregion #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["NoPermission"] = "{0} You do not have permission to use the virtual repair bench.", ["CannotUseNow"] = "{0} You cannot open the repair bench right now (wounded, swimming, or mounted).", ["BuildingBlocked"] = "{0} You cannot open the repair bench while building blocked.", ["NoBuildingPrivilege"] = "{0} You must be within building privilege zone to use this.", ["Cooldown"] = "{0} Please wait {1} seconds before using this command again.", ["SpawnError"] = "{0} An error occurred while creating the virtual repair bench.", ["InventoryFull"] = "{0} Your inventory was full! The item from the repair bench was dropped at your feet." }, this, "en"); lang.RegisterMessages(new Dictionary { ["NoPermission"] = "{0} Você não possui permissão para usar a mesa de reparo portátil.", ["CannotUseNow"] = "{0} Você não pode abrir a mesa de reparo agora (ferido, nadando ou montado).", ["BuildingBlocked"] = "{0} Você não pode usar a mesa de reparo em área de building blocked.", ["NoBuildingPrivilege"] = "{0} Você precisa estar dentro da área do seu armário (privilégio de construção).", ["Cooldown"] = "{0} Aguarde {1} segundos para usar este comando novamente.", ["SpawnError"] = "{0} Erro ao criar a mesa de reparo virtual.", ["InventoryFull"] = "{0} Seu inventário estava cheio! O item da bancada caiu aos seus pés." }, this, "pt-BR"); } private string GetMsg(string key, ulong playerId = 0, params object[] args) { string msg = lang.GetMessage(key, this, playerId == 0 ? null : playerId.ToString()); if (args != null && args.Length > 0) { object[] formatArgs = new object[args.Length + 1]; formatArgs[0] = _config.ChatPrefix; Array.Copy(args, 0, formatArgs, 1, args.Length); return string.Format(msg, formatArgs); } return string.Format(msg, _config.ChatPrefix); } #endregion #region Oxide Lifecycle private void Init() { LoadConfig(); permission.RegisterPermission(PermUse, this); permission.RegisterPermission(PermBypassCooldown, this); cmd.AddChatCommand("rep", this, nameof(CmdRepair)); cmd.AddChatCommand("repair", this, nameof(CmdRepair)); } private void Unload() { foreach (var kvp in new Dictionary(_activeBenches)) { BasePlayer player = BasePlayer.FindByID(kvp.Key) ?? BasePlayer.FindSleeping(kvp.Key); if (kvp.Value != null && !kvp.Value.IsDestroyed) { DropAllItems(kvp.Value, player); kvp.Value.Kill(); } } _activeBenches.Clear(); _cooldowns.Clear(); } #endregion #region Commands private void CmdRepair(BasePlayer player, string command, string[] args) { if (player == null) return; if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermUse)) { player.ChatMessage(GetMsg("NoPermission", player.userID)); return; } if (!player.IsAlive() || player.IsWounded() || player.IsCrawling() || player.WaterFactor() > 0.5f || player.isMounted) { player.ChatMessage(GetMsg("CannotUseNow", player.userID)); return; } if (_config.BlockWhileBuildingBlocked && player.IsBuildingBlocked()) { player.ChatMessage(GetMsg("BuildingBlocked", player.userID)); return; } if (_config.RequireBuildingPrivilege && !player.IsBuildingAuthed()) { player.ChatMessage(GetMsg("NoBuildingPrivilege", player.userID)); return; } // Cooldown check if (_config.CooldownSeconds > 0 && !player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermBypassCooldown)) { if (_cooldowns.TryGetValue(player.userID, out float lastUsed)) { float timeLeft = (lastUsed + _config.CooldownSeconds) - Time.realtimeSinceStartup; if (timeLeft > 0f) { player.ChatMessage(GetMsg("Cooldown", player.userID, Mathf.CeilToInt(timeLeft))); return; } } } // Re-open existing bench if still active if (_activeBenches.TryGetValue(player.userID, out RepairBench existing) && existing != null && !existing.IsDestroyed) { OpenRepairLoot(player, existing); return; } // Spawn invisible entity scaled down and positioned safely Vector3 spawnPos = player.transform.position - new Vector3(0, 1.0f, 0); BaseEntity ent = GameManager.server.CreateEntity(PrefabRepair, spawnPos, Quaternion.identity); if (ent == null) { player.ChatMessage(GetMsg("SpawnError", player.userID)); return; } RepairBench bench = ent as RepairBench; bench.enableSaving = false; bench.OwnerID = player.userID; bench.networkEntityScale = true; bench.Spawn(); // Hide mesh and prevent physical collision bench.transform.localScale = new Vector3(0.001f, 0.001f, 0.001f); LootDistanceOverride lootDist = bench.gameObject.GetComponent() ?? bench.gameObject.AddComponent(); lootDist.amount = 50f; foreach (var col in bench.GetComponentsInChildren()) { col.enabled = false; } bench.SetFlagLocal(BaseEntity.Flags.Locked, true, false); bench.UpdateNetworkGroup(); bench.gameObject.layer = 0; bench.SendNetworkUpdateImmediate(); _activeBenches[player.userID] = bench; _cooldowns[player.userID] = Time.realtimeSinceStartup; OpenRepairLoot(player, bench); } #endregion #region Hooks private object CanNetworkTo(BaseNetworkable entity, BasePlayer target) { if (entity == null || target == null) return null; if (entity is RepairBench bench && IsActiveBench(bench)) { // Only network to the owner if (target.userID != bench.OwnerID) return false; } return null; } private object OnEntityDistanceCheck(BaseEntity entity, BasePlayer player, uint rpc, string rpcName, float maximumDistance, bool checkParent) { if (entity is RepairBench bench && IsActiveBench(bench) && player != null && player.userID == bench.OwnerID) { return true; } return null; } private void OnPlayerLootEnd(PlayerLoot inventory) { if (inventory == null) return; BasePlayer player = inventory.baseEntity; if (player == null) return; if (_activeBenches.TryGetValue(player.userID, out RepairBench bench)) { if (inventory.entitySource == bench || (bench != null && inventory.containers != null && inventory.containers.Contains(bench.inventory))) { CloseAndCleanup(player.userID); } } } private void OnPlayerDisconnected(BasePlayer player) { if (player != null) { CloseAndCleanup(player.userID); } } private void OnPlayerDeath(BasePlayer player, HitInfo info) { if (player != null) { CloseAndCleanup(player.userID); } } #endregion #region Helpers private bool IsActiveBench(RepairBench bench) { if (bench == null || bench.net == null) return false; return _activeBenches.TryGetValue(bench.OwnerID, out RepairBench active) && active == bench; } private void OpenRepairLoot(BasePlayer player, RepairBench bench) { timer.Once(0.1f, () => { if (player == null || !player.IsConnected || bench == null || bench.IsDestroyed) return; player.EndLooting(); if (!player.inventory.loot.StartLootingEntity(bench, false)) return; player.inventory.loot.AddContainer(bench.inventory); player.inventory.loot.SendImmediate(); player.ClientRPC(RpcTarget.Player("RPC_OpenLootPanel", player), bench.panelName); player.SendNetworkUpdate(); }); } private void CloseAndCleanup(ulong userId) { if (_activeBenches.TryGetValue(userId, out RepairBench bench)) { if (bench != null && !bench.IsDestroyed) { BasePlayer player = BasePlayer.FindByID(userId) ?? BasePlayer.FindSleeping(userId); DropAllItems(bench, player); bench.Kill(); } _activeBenches.Remove(userId); } } private void DropAllItems(RepairBench bench, BasePlayer player = null) { if (bench == null || bench.inventory == null) return; if (player == null && bench.OwnerID != 0) { player = BasePlayer.FindByID(bench.OwnerID) ?? BasePlayer.FindSleeping(bench.OwnerID); } Vector3 dropPos; if (player != null) { dropPos = player.transform.position + Vector3.up * 0.5f; } else { float groundY = TerrainMeta.HeightMap != null ? TerrainMeta.HeightMap.GetHeight(bench.transform.position) : bench.transform.position.y; dropPos = new Vector3(bench.transform.position.x, Mathf.Max(bench.transform.position.y + 1.5f, groundY + 0.5f), bench.transform.position.z); } bool droppedSome = false; for (int i = bench.inventory.itemList.Count - 1; i >= 0; i--) { Item item = bench.inventory.itemList[i]; if (item == null) continue; if (player != null && player.inventory != null) { if (player.inventory.GiveItem(item)) continue; } item.Drop(dropPos, Vector3.up * 2f); droppedSome = true; } if (droppedSome && player != null && player.IsConnected) { player.ChatMessage(GetMsg("InventoryFull", player.userID)); } } #endregion } }