C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("VaultDoors", "da7h_9", "1.0.0")]
[Description("Vault door with workbench craft menu, scrap research unlock, code lock, explosive-only damage and rebuild blocker")]
public class VaultDoors : RustPlugin
{
#region Fields
private const string PermUse = "vaultdoors.use";
private const string PermGive = "vaultdoors.give";
private const string PermCraft = "vaultdoors.craft";
private const string CodeLockPrefab = "assets/prefabs/locks/keypad/lock.code.prefab";
private const string UiButton = ".VaultDoorButton";
private const string UiPanel = ".VaultDoorPanel";
private const string UiPanelBody = ".VaultDoorPanelB";
private Configuration config;
private SavedData data = new SavedData();
private Dictionary<string, int> itemIds = new Dictionary<string, int>();
private HashSet<ulong> buttonShown = new HashSet<ulong>();
private HashSet<ulong> pickedUp = new HashSet<ulong>();
private struct BaseTransform
{
public Vector3 Pos;
public Quaternion Rot;
}
private Dictionary<ulong, BaseTransform> baseTransforms = new Dictionary<ulong, BaseTransform>();
#endregion
#region Config
private class Configuration
{
[JsonProperty("Vault door prefab path")]
public string VaultPrefab = "assets/bundled/prefabs/modding/asset_store/bankheist_package/bankheist_vol03/prefabs/door.vault.static.prefab";
[JsonProperty("Base item given to players (door.hinged.metal = single doorway)")]
public string BaseItemShortname = "door.hinged.metal";
[JsonProperty("Custom item display name")]
public string ItemName = "Vault Door";
[JsonProperty("Skin ID for the item in inventory")]
public ulong SkinId = 3770358915UL;
[JsonProperty("Vault door health (spawn and max)")]
public float Health = 2000f;
[JsonProperty("Limit to one vault door per Tool Cupboard")]
public bool OnePerTC = true;
[JsonProperty("Require building privilege (TC) to place")]
public bool RequireTC = true;
[JsonProperty("Automatically attach a code lock on placement")]
public bool AutoCodeLock = false;
[JsonProperty("Allow picking the vault door back up (hold E, no lock attached)")]
public bool AllowPickup = true;
[JsonProperty("Destroy the vault door when its frame is destroyed")]
public bool KillWithFrame = true;
[JsonProperty("Explosives deal normal damage")]
public bool ExplosivesNormal = true;
[JsonProperty("Non-explosive hits needed to remove 1 HP")]
public float HitsPerHealthPoint = 80f;
[JsonProperty("Rebuild blocker - radius in meters")]
public float BlockRadius = 1f;
[JsonProperty("Rebuild blocker - seconds before the spot frees up")]
public float BlockSeconds = 300f;
[JsonProperty("Research - scrap cost to unlock")]
public int ResearchCost = 300;
[JsonProperty("Research - prerequisite item shortname that must be learned first")]
public string PrerequisiteShortname = "door.double.hinged.toptier";
[JsonProperty("Research - friendly name of the prerequisite")]
public string PrerequisiteName = "Armored Double Door";
[JsonProperty("Crafting workbench level. [ 0 - workbench is not required ]")]
public int WorkBench = 3;
[JsonProperty("List of crafting resources")]
public Dictionary<string, int> ItemList = new Dictionary<string, int>
{
["gears"] = 10,
["metal.fragments"] = 200,
["metal.refined"] = 40,
["metalspring"] = 5
};
[JsonProperty("Alignment - forward/back offset (meters)")]
public float OffsetForward = -0.9f;
[JsonProperty("Alignment - left/right offset (meters)")]
public float OffsetRight = 0.54f;
[JsonProperty("Alignment - up/down offset (meters)")]
public float OffsetUp = 0.222f;
[JsonProperty("Alignment - yaw rotation offset (degrees)")]
public float YawOffset = 0f;
[JsonProperty("Code lock - forward offset")]
public float LockOffsetForward = -0.35f;
[JsonProperty("Code lock - right offset")]
public float LockOffsetRight = 0.60f;
[JsonProperty("Code lock - up offset")]
public float LockOffsetUp = 0f;
}
protected override void LoadDefaultConfig() => config = new Configuration();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<Configuration>();
if (config == null) throw new JsonException();
if (config.ItemList == null || config.ItemList.Count == 0)
config.ItemList = new Configuration().ItemList;
SaveConfig();
}
catch
{
PrintWarning("Config file is corrupt, loading defaults.");
LoadDefaultConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(config);
#endregion
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You don't have permission to do that.",
["Given"] = "You received a <color=#ffa500>Vault Door</color>.",
["GivenTo"] = "Gave a Vault Door to {0}.",
["NoTC"] = "Vault doors can only be placed inside Tool Cupboard range. The door was refunded.",
["AlreadyHasVault"] = "This base already has a vault door. Only <color=#ffa500>one per TC</color> is allowed. The door was refunded.",
["SpawnFailed"] = "Failed to spawn the vault door. The door was refunded.",
["Blocked"] = "A vault door was recently destroyed here. Blocked for <color=#ffa500>{0}</color> more seconds.",
["PlayerNotFound"] = "Player not found: {0}",
["PickupHasLock"] = "Remove the lock from the vault door before picking it up.", ["PickupNoAuth"] = "You need building privilege to pick up this vault door.",
["PickedUp"] = "Picked up the <color=#ffa500>Vault Door</color>.",
["PickupDisabled"] = "Picking up vault doors is disabled on this server.",
["PickupNothingNear"] = "No vault door nearby. Stand next to one and try again.",
["TITLE"] = "VAULT DOOR",
["BUTTON_CRAFT"] = "CRAFT",
["BUTTON_RESEARCH"] = "RESEARCH",
["BUTTON_OPEN"] = "VAULT DOOR",
["LVL_WORKBENCH"] = "WORKBENCH LVL {0}",
["MSG_RESOURCE"] = "INSUFFICIENT RESOURCES FOR CRAFTING!",
["MSG_WORKBENCH"] = "INSUFFICIENT WORKBENCH LEVEL FOR CRAFTING!",
["MSG_CRAFT"] = "VAULT DOOR SUCCESSFULLY CRAFTED!",
["MSG_LOCKED"] = "LEARN THE {0} FIRST!",
["MSG_NOT_RESEARCHED"] = "RESEARCH THIS BLUEPRINT FIRST!",
["MSG_NO_SCRAP"] = "YOU NEED {0} SCRAP TO RESEARCH THIS!",
["MSG_RESEARCHED"] = "VAULT DOOR BLUEPRINT UNLOCKED!",
["MSG_ALREADY"] = "ALREADY RESEARCHED!",
["LOCKED_TITLE"] = "LOCKED - LEARN {0}",
["RESEARCH_TITLE"] = "RESEARCH COST: {0} SCRAP"
}, this);
}
private string Msg(string key, BasePlayer player = null, params object[] args)
=> string.Format(lang.GetMessage(key, this, player?.UserIDString), args);
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermGive, this);
permission.RegisterPermission(PermCraft, this);
LoadData();
}
private void OnServerInitialized()
{
Puts($"Using vault prefab path: \"{config.VaultPrefab}\"");
itemIds.Clear();
foreach (var kv in config.ItemList)
{
var def = ItemManager.FindItemDefinition(kv.Key);
if (def == null)
{
PrintWarning($"Unknown item shortname in craft list: {kv.Key}");
continue;
}
itemIds[kv.Key] = def.itemid;
}
var scrapDef = ItemManager.FindItemDefinition("scrap");
if (scrapDef != null) itemIds["scrap"] = scrapDef.itemid;
if (data.SecondLocks != null && data.SecondLocks.Count > 0)
{
foreach (var id in data.SecondLocks.Values.ToList())
{
var ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as BaseEntity;
if (ent != null && !ent.IsDestroyed) ent.Kill();
}
data.SecondLocks.Clear();
SaveData();
}
foreach (var ent in BaseNetworkable.serverEntities.ToList())
{
var door = ent as Door;
if (door == null || door.IsDestroyed || !IsVaultDoor(door)) continue;
ApplyHealth(door);
EnablePickup(door);
}
PruneBlockers();
timer.Every(60f, PruneBlockers);
timer.Every(1f, CheckBenchProximity);
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
{
CuiHelper.DestroyUi(player, UiButton);
CuiHelper.DestroyUi(player, UiPanel);
}
buttonShown.Clear();
SaveData();
}
// The crafting window is client-side and never reports to the server, so
// there is no hook for it opening. Closest we can do: show the button when
// the player presses USE on a bench of the right tier, hide when they leave.
private void OnPlayerInput(BasePlayer player, InputState input)
{
if (player == null || input == null) return;
if (buttonShown.Contains(player.userID)) return;
if (!input.WasJustPressed(BUTTON.USE)) return;
if (config.WorkBench > 0 && player.currentCraftLevel < config.WorkBench) return;
ShowButton(player);
buttonShown.Add(player.userID);
}
// Hides the button once the player walks away from the bench
private void CheckBenchProximity()
{
if (buttonShown.Count == 0) return;
foreach (var id in buttonShown.ToList())
{
var player = BasePlayer.FindByID(id);
if (player == null || !player.IsConnected)
{
buttonShown.Remove(id);
continue;
}
bool atBench = config.WorkBench <= 0 || player.currentCraftLevel >= config.WorkBench;
if (atBench) continue;
CuiHelper.DestroyUi(player, UiButton);
CuiHelper.DestroyUi(player, UiPanel);
buttonShown.Remove(id);
}
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (player == null) return;
buttonShown.Remove(player.userID);
CuiHelper.DestroyUi(player, UiButton);
CuiHelper.DestroyUi(player, UiPanel);
}
private bool IsVaultDoor(BaseEntity entity)
{
if (entity == null || entity.IsDestroyed) return false;
return entity.skinID == config.SkinId && entity.PrefabName.Equals(config.VaultPrefab);
}
private void OnEntityBuilt(Planner plan, GameObject go)
{
BasePlayer player = plan?.GetOwnerPlayer();
BaseEntity entity = go?.ToBaseEntity();
if (player == null || entity == null) return;
if (entity.skinID != config.SkinId) return;
NextTick(() => ReplaceWithVault(player, entity));
}
private void OnEntityDeath(Door door, HitInfo info) => HandleVaultRemoved(door);
private void OnEntityKill(Door door) => HandleVaultRemoved(door);
private void HandleVaultRemoved(Door door)
{
if (!IsVaultDoor(door)) return;
ulong id = door.net?.ID.Value ?? 0;
if (id == 0) return;
bool wasPickedUp = pickedUp.Remove(id);
if (config.BlockSeconds > 0f && !wasPickedUp)
{
data.Blockers.Add(new Blocker
{
X = door.transform.position.x,
Y = door.transform.position.y,
Z = door.transform.position.z,
Expires = CurrentTime() + config.BlockSeconds
});
}
foreach (var key in data.TcVaults.Where(kv => kv.Value == id).Select(kv => kv.Key).ToList())
data.TcVaults.Remove(key);
data.DoorFrames.Remove(id);
baseTransforms.Remove(id);
SaveData();
}
private void OnEntityKill(BuildingPrivlidge priv)
{
ulong id = priv?.net?.ID.Value ?? 0;
if (id != 0 && data.TcVaults.Remove(id)) SaveData();
}
private void OnEntityKill(BuildingBlock block)
{
if (!config.KillWithFrame || block?.net == null) return;
ulong frameId = block.net.ID.Value;
foreach (var kv in data.DoorFrames.Where(kv => kv.Value == frameId).ToList())
{
var door = BaseNetworkable.serverEntities.Find(new NetworkableId(kv.Key)) as BaseEntity;
if (door != null && !door.IsDestroyed) door.Kill();
data.DoorFrames.Remove(kv.Key);
}
SaveData();
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null || !IsVaultDoor(entity)) return null;
var major = info.damageTypes.GetMajorityDamageType();
bool explosive = major == Rust.DamageType.Explosion || major == Rust.DamageType.Heat;
if (explosive && config.ExplosivesNormal) return null;
float perHit = config.HitsPerHealthPoint > 0f ? 1f / config.HitsPerHealthPoint : 0f;
info.damageTypes.ScaleAll(0f);
info.damageTypes.Add(Rust.DamageType.Generic, perHit);
return null;
}
#endregion
#region Research
private bool HasLearned(BasePlayer player)
=> player != null && data.Learned.Contains(player.userID);
private bool HasPrerequisite(BasePlayer player)
{
if (player?.blueprints == null) return false;
if (string.IsNullOrEmpty(config.PrerequisiteShortname)) return true;
var def = ItemManager.FindItemDefinition(config.PrerequisiteShortname);
if (def == null) return true;
return player.blueprints.IsUnlocked(def);
}
private bool GetItemAmount(BasePlayer player, int itemId, int need)
=> player.inventory.GetAmount(itemId) >= need;
#endregion
#region UI
private void ShowButton(BasePlayer player)
{
CuiHelper.DestroyUi(player, UiButton);
var container = new CuiElementContainer();
// Bottom right, above the hotbar - no cursor component so it does
// not interfere with the workbench UI that is already open
container.Add(new CuiButton
{
RectTransform =
{
AnchorMin = "1 0", AnchorMax = "1 0",
OffsetMin = "-180 175", OffsetMax = "-15 205"
},
Button = { Color = "0.35 0.45 0.25 0.95", Command = "vaultdoor.ui open" },
Text =
{
Text = Msg("BUTTON_OPEN", player),
Align = TextAnchor.MiddleCenter,
FontSize = 14,
Font = "robotocondensed-regular.ttf",
Color = "0.75 0.95 0.41 1"
}
}, "Overlay", UiButton);
CuiHelper.AddUi(player, container);
}
private void ShowPanel(BasePlayer player)
{
CuiHelper.DestroyUi(player, UiPanel);
bool prereq = HasPrerequisite(player);
bool learned = HasLearned(player);
var container = new CuiElementContainer();
container.Add(new CuiElement
{
Parent = "Overlay",
Name = UiPanel,
DestroyUi = UiPanel,
Components =
{
new CuiImageComponent { Color = "0.517 0.521 0.509 0.95", Material = "assets/icons/greyout.mat" },
new CuiRectTransformComponent { AnchorMin = "0.5 0", AnchorMax = "0.5 0", OffsetMin = "-337 90", OffsetMax = "318 268.5" },
new CuiNeedsCursorComponent {}
}
});
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "5 5", OffsetMax = "-5 -5" },
Image = { Color = "0.217 0.221 0.209 0.95" }
}, UiPanel, UiPanelBody);
// Title changes with state
string title = !prereq
? Msg("LOCKED_TITLE", player, config.PrerequisiteName.ToUpper())
: (!learned ? Msg("RESEARCH_TITLE", player, config.ResearchCost) : Msg("TITLE", player));
container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -36.75", OffsetMax = "0 0" },
Text =
{
Text = title,
Align = TextAnchor.MiddleCenter,
FontSize = 18,
Font = "robotocondensed-regular.ttf",
Color = !prereq ? "1 0.4 0.4 0.9" : "1 1 1 0.75"
}
}, UiPanelBody);
// Ingredient row - real item icons, red when short
if (!prereq)
{
container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0.25", AnchorMax = "1 0.75" },
Text =
{
Text = Msg("MSG_LOCKED", player, config.PrerequisiteName.ToUpper()),
Align = TextAnchor.MiddleCenter,
FontSize = 16,
Font = "robotocondensed-regular.ttf",
Color = "0.8 0.8 0.8 1"
}
}, UiPanelBody);
}
else if (!learned)
{
DrawScrapCost(container, player);
}
else
{
DrawIngredients(container, player);
}
// Divider bars
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -41.75", OffsetMax = "0 -36.75" },
Image = { Color = "0.517 0.521 0.509 0.95", Material = "assets/icons/greyout.mat" }
}, UiPanelBody);
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0", OffsetMin = "0 36.75", OffsetMax = "0 41.75" },
Image = { Color = "0.517 0.521 0.509 0.95", Material = "assets/icons/greyout.mat" }
}, UiPanelBody);
// Close button
container.Add(new CuiButton
{
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-31.75 5", OffsetMax = "-5 31.75" },
Button = { Color = "1 1 1 0.75", Sprite = "assets/icons/close.png", Close = UiPanel },
Text = { Text = "" }
}, UiPanelBody);
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-41.75 0", OffsetMax = "-36.75 41.75" },
Image = { Color = "0.517 0.521 0.509 0.95", Material = "assets/icons/greyout.mat" }
}, UiPanelBody);
// Workbench badge
if (config.WorkBench > 0)
{
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-296.75 5", OffsetMax = "-151.75 31.75" },
Image = { Color = config.WorkBench == 1 ? "0.71 0.95 0.29 0.57" : config.WorkBench == 2 ? "0.26 0.69 1 0.57" : "1 0.58 0.27 0.57" }
}, UiPanelBody, ".VDWorkBench");
container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text =
{
Text = Msg("LVL_WORKBENCH", player, config.WorkBench),
Align = TextAnchor.MiddleCenter,
FontSize = 16,
Font = "robotocondensed-regular.ttf",
Color = "0.8 0.8 0.8 1"
}
}, ".VDWorkBench");
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-306.75 0", OffsetMax = "-301.75 41.75" },
Image = { Color = "0.517 0.521 0.509 0.95", Material = "assets/icons/greyout.mat" }
}, UiPanelBody);
}
// Action button - research or craft
if (prereq)
{
container.Add(new CuiButton
{
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-136.75 5", OffsetMax = "-46.75 31.75" },
Button = { Color = "0.35 0.45 0.25 1", Command = learned ? "vaultdoor.ui craft" : "vaultdoor.ui research" },
Text =
{
Text = learned ? Msg("BUTTON_CRAFT", player) : Msg("BUTTON_RESEARCH", player),
Align = TextAnchor.MiddleCenter,
FontSize = 16,
Font = "robotocondensed-regular.ttf",
Color = "0.75 0.95 0.41 1"
}
}, UiPanelBody);
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-146.75 0", OffsetMax = "-141.75 41.75" },
Image = { Color = "0.517 0.521 0.509 0.95", Material = "assets/icons/greyout.mat" }
}, UiPanelBody);
}
CuiHelper.AddUi(player, container);
}
private void DrawIngredients(CuiElementContainer container, BasePlayer player)
{
int count = config.ItemList.Count;
foreach (var item in config.ItemList)
{
int itemId;
if (!itemIds.TryGetValue(item.Key, out itemId)) continue;
double offset = -(37.5 * count--) + -(2.5 * count--);
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.5 0.5", AnchorMax = "0.5 0.5", OffsetMin = $"{offset} -37.5", OffsetMax = $"{offset + 75} 37.5" },
Image = { Color = "0.517 0.521 0.509 0.5", Material = "assets/icons/greyout.mat" }
}, UiPanelBody, ".VDItem");
container.Add(new CuiElement
{
Parent = ".VDItem",
Components =
{
new CuiImageComponent { ItemId = itemId },
new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "5 5", OffsetMax = "-5 -5" }
}
});
container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0.5", OffsetMax = "-2 0" },
Text =
{
Text = $"x{item.Value}",
Align = TextAnchor.LowerRight,
FontSize = 12,
Font = "robotocondensed-regular.ttf",
Color = GetItemAmount(player, itemId, item.Value) ? "0.3 1 0.3 1" : "1 0.3 0.3 1"
}
}, ".VDItem");
}
}
private void DrawScrapCost(CuiElementContainer container, BasePlayer player)
{
int scrapId;
if (!itemIds.TryGetValue("scrap", out scrapId)) return;
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.5 0.5", AnchorMax = "0.5 0.5", OffsetMin = "-37.5 -37.5", OffsetMax = "37.5 37.5" },
Image = { Color = "0.517 0.521 0.509 0.5", Material = "assets/icons/greyout.mat" }
}, UiPanelBody, ".VDScrap");
container.Add(new CuiElement
{
Parent = ".VDScrap",
Components =
{
new CuiImageComponent { ItemId = scrapId },
new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "5 5", OffsetMax = "-5 -5" }
}
});
container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 0.5", OffsetMax = "-2 0" },
Text =
{
Text = $"x{config.ResearchCost}",
Align = TextAnchor.LowerRight,
FontSize = 12,
Font = "robotocondensed-regular.ttf",
Color = GetItemAmount(player, scrapId, config.ResearchCost) ? "0.3 1 0.3 1" : "1 0.3 0.3 1"
}
}, ".VDScrap");
}
private void SendInfo(BasePlayer player, string message)
{
var container = new CuiElementContainer();
container.Add(new CuiLabel
{
FadeOut = 0.5f,
RectTransform = { AnchorMin = "0 0", AnchorMax = "0 0", OffsetMax = config.WorkBench > 0 ? "337.75 36.75" : "497.75 36.75" },
Text =
{
FadeIn = 0.5f,
Text = message,
Align = TextAnchor.MiddleCenter,
Font = "robotocondensed-regular.ttf",
FontSize = 14,
Color = "0.75 0.75 0.75 1"
}
}, UiPanelBody, ".VDInfo", ".VDInfo");
CuiHelper.AddUi(player, container);
player.Invoke(() => CuiHelper.DestroyUi(player, ".VDInfo"), 3);
}
[ConsoleCommand("vaultdoor.ui")]
private void CmdUi(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player == null) return;
string action = arg.HasArgs() ? arg.GetString(0).ToLower() : "close";
switch (action)
{
case "open":
ShowPanel(player);
break;
case "close":
CuiHelper.DestroyUi(player, UiPanel);
break;
case "research":
DoResearch(player);
break;
case "craft":
DoCraft(player);
break;
}
}
private void DoResearch(BasePlayer player)
{
if (HasLearned(player))
{
SendInfo(player, Msg("MSG_ALREADY", player));
return;
}
if (!HasPrerequisite(player))
{
SendInfo(player, Msg("MSG_LOCKED", player, config.PrerequisiteName.ToUpper()));
return;
}
if (config.WorkBench > 0 && player.currentCraftLevel < config.WorkBench)
{
SendInfo(player, Msg("MSG_WORKBENCH", player));
return;
}
int scrapId;
if (!itemIds.TryGetValue("scrap", out scrapId)) return;
if (player.inventory.GetAmount(scrapId) < config.ResearchCost)
{
SendInfo(player, Msg("MSG_NO_SCRAP", player, config.ResearchCost));
return;
}
player.inventory.Take(null, scrapId, config.ResearchCost);
data.Learned.Add(player.userID);
SaveData();
ShowPanel(player);
SendInfo(player, Msg("MSG_RESEARCHED", player));
}
private void DoCraft(BasePlayer player)
{
if (!permission.UserHasPermission(player.UserIDString, PermCraft))
{
SendInfo(player, Msg("NoPermission", player));
return;
}
if (!HasLearned(player))
{
SendInfo(player, Msg("MSG_NOT_RESEARCHED", player));
return;
}
if (config.WorkBench > 0 && player.currentCraftLevel < config.WorkBench)
{
SendInfo(player, Msg("MSG_WORKBENCH", player));
return;
}
foreach (var item in config.ItemList)
{
int id;
if (!itemIds.TryGetValue(item.Key, out id)) continue;
if (player.inventory.GetAmount(id) < item.Value)
{
SendInfo(player, Msg("MSG_RESOURCE", player));
return;
}
}
foreach (var item in config.ItemList)
{
int id;
if (!itemIds.TryGetValue(item.Key, out id)) continue;
player.inventory.Take(null, id, item.Value);
}
Item created = CreateVaultItem();
if (created == null) return;
player.GiveItem(created, BaseEntity.GiveItemReason.Crafted);
ShowPanel(player);
SendInfo(player, Msg("MSG_CRAFT", player));
}
#endregion
#region Core
private void ReplaceWithVault(BasePlayer player, BaseEntity placed)
{
if (placed == null || placed.IsDestroyed) return;
Vector3 pos = placed.transform.position;
Quaternion rot = placed.transform.rotation;
float remaining;
if (IsBlocked(pos, out remaining))
{
placed.Kill();
Refund(player);
player.ChatMessage(Msg("Blocked", player, Mathf.CeilToInt(remaining)));
return;
}
BuildingPrivlidge priv = player.GetBuildingPrivilege(placed.WorldSpaceBounds());
if (config.RequireTC && priv == null)
{
placed.Kill();
Refund(player);
player.ChatMessage(Msg("NoTC", player));
return;
}
if (config.OnePerTC && priv != null)
{
ulong tcId = priv.net.ID.Value;
ulong existingId;
if (data.TcVaults.TryGetValue(tcId, out existingId))
{
var existing = BaseNetworkable.serverEntities.Find(new NetworkableId(existingId)) as BaseEntity;
if (existing != null && !existing.IsDestroyed)
{
placed.Kill();
Refund(player);
player.ChatMessage(Msg("AlreadyHasVault", player));
return;
}
data.TcVaults.Remove(tcId);
}
}
BuildingBlock frame = FindFrame(pos);
placed.Kill();
Vector3 basePos = pos;
Quaternion baseRot = rot;
Quaternion finalRot = baseRot * Quaternion.Euler(0f, config.YawOffset, 0f);
Vector3 finalPos = basePos
+ finalRot * Vector3.forward * config.OffsetForward
+ finalRot * Vector3.right * config.OffsetRight
+ Vector3.up * config.OffsetUp;
BaseEntity created = GameManager.server.CreateEntity(config.VaultPrefab, finalPos, finalRot);
if (created == null)
{
Refund(player);
player.ChatMessage(Msg("SpawnFailed", player));
PrintError($"CreateEntity returned null for path: \"{config.VaultPrefab}\"");
return;
}
created.skinID = config.SkinId;
created.OwnerID = player.userID;
created.Spawn();
Door vault = created as Door;
if (vault != null)
{
ApplyHealth(vault);
EnablePickup(vault);
if (config.AutoCodeLock) AttachLock(vault, player);
}
created.SendNetworkUpdateImmediate();
ulong doorId = created.net.ID.Value;
baseTransforms[doorId] = new BaseTransform { Pos = basePos, Rot = baseRot };
if (frame?.net != null)
data.DoorFrames[doorId] = frame.net.ID.Value;
if (priv != null)
data.TcVaults[priv.net.ID.Value] = doorId;
SaveData();
}
// Manually placed locks land at the prefab's own anchor, which is wrong for
// this oversized door, so apply the configured offset to those as well.
private void OnEntitySpawned(BaseNetworkable entity)
{
var baseLock = entity as BaseLock;
if (baseLock == null) return;
NextTick(() =>
{
if (baseLock == null || baseLock.IsDestroyed) return;
var parent = baseLock.GetParentEntity();
if (!IsVaultDoor(parent)) return;
baseLock.transform.localPosition += new Vector3(
config.LockOffsetRight, config.LockOffsetUp, config.LockOffsetForward);
baseLock.SendNetworkUpdateImmediate();
});
}
private object OnHammerHit(BasePlayer player, HitInfo info)
{
if (player == null || info == null) return null;
var door = info.HitEntity as Door;
if (!IsVaultDoor(door)) return null;
if (!config.AllowPickup) return null;
TryPickup(player, door);
return null;
}
private bool TryPickup(BasePlayer player, Door door)
{
if (door.GetSlot(BaseEntity.Slot.Lock) != null)
{
player.ChatMessage(Msg("PickupHasLock", player));
return false;
}
if (!player.CanBuild())
{
player.ChatMessage(Msg("PickupNoAuth", player));
return false;
}
if (door.OwnerID != player.userID && !HasTcAuth(player, door))
{
player.ChatMessage(Msg("PickupNoAuth", player));
return false;
}
ulong id = door.net?.ID.Value ?? 0;
if (id != 0) pickedUp.Add(id);
Item item = CreateVaultItem();
if (item != null)
player.GiveItem(item, BaseEntity.GiveItemReason.PickedUp);
player.ChatMessage(Msg("PickedUp", player));
door.Kill();
return true;
}
private bool HasTcAuth(BasePlayer player, BaseEntity door)
{
var priv = player.GetBuildingPrivilege();
return priv != null && priv.IsAuthed(player);
}
private void EnablePickup(Door door)
{
if (door == null || !config.AllowPickup) return;
door.pickup.enabled = true;
door.pickup.requireBuildingPrivilege = true;
door.pickup.requireHammer = false;
}
// We handle pickup ourselves so the player gets the skinned vault door
// item back rather than a plain metal door, and so picking it up does
// not leave a rebuild blocker behind.
private object CanPickupEntity(BasePlayer player, BaseEntity entity)
{
if (player == null || !IsVaultDoor(entity)) return null;
var door = entity as Door;
if (door == null) return null;
if (!config.AllowPickup) return false;
TryPickup(player, door);
return false;
}
private void ApplyHealth(Door door)
{
SetHealth(door);
NextTick(() => SetHealth(door));
timer.Once(0.5f, () => SetHealth(door));
timer.Once(2f, () => SetHealth(door));
}
private void SetHealth(Door door)
{
if (door == null || door.IsDestroyed) return;
door.InitializeHealth(config.Health, config.Health);
door._maxHealth = config.Health;
door.health = config.Health;
door.SendNetworkUpdate();
}
private void AttachLock(Door door, BasePlayer player)
{
if (door.GetSlot(BaseEntity.Slot.Lock) != null) return;
var codeLock = GameManager.server.CreateEntity(CodeLockPrefab) as CodeLock;
if (codeLock == null) return;
codeLock.OwnerID = player.userID;
codeLock.SetParent(door, door.GetSlotAnchorName(BaseEntity.Slot.Lock));
codeLock.Spawn();
door.SetSlot(BaseEntity.Slot.Lock, codeLock);
codeLock.transform.localPosition += new Vector3(
config.LockOffsetRight, config.LockOffsetUp, config.LockOffsetForward);
codeLock.SendNetworkUpdateImmediate();
}
private BuildingBlock FindFrame(Vector3 pos)
{
var list = new List<BuildingBlock>();
Vis.Entities(pos, 2f, list, Rust.Layers.Mask.Construction);
BuildingBlock best = null;
float closest = float.MaxValue;
foreach (var b in list)
{
if (b == null || b.IsDestroyed) continue;
float d = Vector3.Distance(b.transform.position, pos);
if (d < closest) { closest = d; best = b; }
}
return best;
}
private bool IsBlocked(Vector3 pos, out float secondsLeft)
{
secondsLeft = 0f;
double now = CurrentTime();
foreach (var b in data.Blockers)
{
if (b.Expires <= now) continue;
if (Vector3.Distance(new Vector3(b.X, b.Y, b.Z), pos) <= config.BlockRadius)
{
secondsLeft = (float)(b.Expires - now);
return true;
}
}
return false;
}
private void PruneBlockers()
{
double now = CurrentTime();
int before = data.Blockers.Count;
data.Blockers.RemoveAll(b => b.Expires <= now);
if (data.Blockers.Count != before) SaveData();
}
private double CurrentTime() => DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
private Item CreateVaultItem()
{
Item item = ItemManager.CreateByName(config.BaseItemShortname, 1, config.SkinId);
if (item != null) item.name = config.ItemName;
return item;
}
private void Refund(BasePlayer player)
{
Item item = CreateVaultItem();
if (item != null) player.GiveItem(item, BaseEntity.GiveItemReason.PickedUp);
}
#endregion
#region Commands
[ChatCommand("vaultpickup")]
private void CmdVaultPickup(BasePlayer player, string command, string[] args)
{
if (!config.AllowPickup)
{
player.ChatMessage(Msg("PickupDisabled", player));
return;
}
var door = FindNearestVault(player, 4f) as Door;
if (door == null)
{
player.ChatMessage(Msg("PickupNothingNear", player));
return;
}
TryPickup(player, door);
}
[ConsoleCommand("vaultdoor.debug")]
private void CmdDebug(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player == null || !permission.UserHasPermission(player.UserIDString, PermGive))
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
var door = FindNearestVault(player, 10f);
if (door == null)
{
arg.ReplyWith("No vault door within 10m.\n" +
$"Config skin ID: {config.SkinId}\n" +
$"Config prefab: {config.VaultPrefab}");
return;
}
var d = door as Door;
arg.ReplyWith(
$"Found: {door.ShortPrefabName}\n" +
$"Type: {door.GetType().Name}\n" +
$"Skin: {door.skinID} (config {config.SkinId})\n" +
$"Prefab match: {door.PrefabName.Equals(config.VaultPrefab)}\n" +
$"Health: {(d != null ? d.health.ToString("0") : "n/a")} / {(d != null ? d.MaxHealth().ToString("0") : "n/a")}\n" +
$"Lock attached: {(d != null && d.GetSlot(BaseEntity.Slot.Lock) != null)}\n" +
$"Owner: {door.OwnerID} (you: {player.userID})\n" +
$"Pickup enabled in config: {config.AllowPickup}");
}
[ChatCommand("vault")]
private void CmdVaultPanel(BasePlayer player, string command, string[] args)
{
ShowPanel(player);
}
[ChatCommand("vaultdoor")]
private void CmdVaultDoor(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
ShowPanel(player);
return;
}
Item item = CreateVaultItem();
if (item == null) return;
player.GiveItem(item, BaseEntity.GiveItemReason.PickedUp);
player.ChatMessage(Msg("Given", player));
}
[ConsoleCommand("vaultdoor.unlock")]
private void CmdUnlock(ConsoleSystem.Arg arg)
{
if (arg.Player() != null && !permission.UserHasPermission(arg.Player().UserIDString, PermGive))
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
if (!arg.HasArgs())
{
arg.ReplyWith("Usage: vaultdoor.unlock <name or steamID>");
return;
}
BasePlayer target = BasePlayer.FindAwakeOrSleeping(arg.GetString(0));
if (target == null)
{
arg.ReplyWith(Msg("PlayerNotFound", null, arg.GetString(0)));
return;
}
data.Learned.Add(target.userID);
SaveData();
arg.ReplyWith($"{target.displayName} can now craft the Vault Door.");
}
[ConsoleCommand("vaultdoor.give")]
private void CmdGive(ConsoleSystem.Arg arg)
{
if (arg.Player() != null && !permission.UserHasPermission(arg.Player().UserIDString, PermGive))
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
if (!arg.HasArgs())
{
arg.ReplyWith("Usage: vaultdoor.give <name or steamID>");
return;
}
BasePlayer target = BasePlayer.FindAwakeOrSleeping(arg.GetString(0));
if (target == null)
{
arg.ReplyWith(Msg("PlayerNotFound", null, arg.GetString(0)));
return;
}
Item item = CreateVaultItem();
if (item == null) return;
target.GiveItem(item, BaseEntity.GiveItemReason.PickedUp);
arg.ReplyWith(Msg("GivenTo", null, target.displayName));
}
[ConsoleCommand("vaultdoor.align")]
private void CmdAlign(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player == null || !permission.UserHasPermission(player.UserIDString, PermGive))
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
if (arg.Args == null || arg.Args.Length < 4)
{
arg.ReplyWith("Usage: vaultdoor.align <forward> <right> <up> <yaw>\n" +
$"Current: {config.OffsetForward} {config.OffsetRight} {config.OffsetUp} {config.YawOffset}");
return;
}
float fwd, right, up, yaw;
if (!float.TryParse(arg.Args[0], out fwd) || !float.TryParse(arg.Args[1], out right) ||
!float.TryParse(arg.Args[2], out up) || !float.TryParse(arg.Args[3], out yaw))
{
arg.ReplyWith("All four values must be numbers.");
return;
}
BaseEntity door = FindNearestVault(player);
if (door == null)
{
arg.ReplyWith("No vault door found within 10m.");
return;
}
BaseTransform bt;
if (!baseTransforms.TryGetValue(door.net.ID.Value, out bt))
{
bt = new BaseTransform { Pos = door.transform.position, Rot = door.transform.rotation };
baseTransforms[door.net.ID.Value] = bt;
}
Quaternion newRot = bt.Rot * Quaternion.Euler(0f, yaw, 0f);
Vector3 newPos = bt.Pos
+ newRot * Vector3.forward * fwd
+ newRot * Vector3.right * right
+ Vector3.up * up;
door.transform.position = newPos;
door.transform.rotation = newRot;
door.SendNetworkUpdateImmediate();
config.OffsetForward = fwd;
config.OffsetRight = right;
config.OffsetUp = up;
config.YawOffset = yaw;
arg.ReplyWith("Applied. Run vaultdoor.align.save to keep it.");
}
[ConsoleCommand("vaultdoor.align.save")]
private void CmdAlignSave(ConsoleSystem.Arg arg)
{
if (arg.Player() != null && !permission.UserHasPermission(arg.Player().UserIDString, PermGive))
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
SaveConfig();
arg.ReplyWith("Alignment saved.");
}
[ConsoleCommand("vaultdoor.lock")]
private void CmdLockOffset(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player == null || !permission.UserHasPermissi
// ... [Code preview truncated due to length. Full source in attached file] ...