C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("GhostLabsCrateStack", "Ghost", "2.1.0")]
[Description("Safe, duplicate-resistant stacking for Rust small and large wooden storage boxes.")]
public class GhostLabsCrateStack : RustPlugin
{
[PluginReference]
private Plugin GhostLabsNotifications;
private const string PluginVersion = "2.1.0";
private const string PermUse = "ghostlabs.cratestack.use";
private const string PermAdmin = "ghostlabs.cratestack.admin";
private const string PermUnlimited = "ghostlabs.cratestack.unlimited";
private const int MaxTraversalDepth = 64;
private PluginConfig _config;
private readonly Dictionary<ulong, float> _cooldowns = new Dictionary<ulong, float>();
private readonly Collider[] _overlapBuffer = new Collider[256];
private readonly Dictionary<string, CrateInfo> _crates = new Dictionary<string, CrateInfo>(StringComparer.OrdinalIgnoreCase)
{
["box.wooden"] = new CrateInfo
{
DisplayName = "Small Wooden Box",
ItemShortname = "box.wooden",
Prefab = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab",
VerticalOffset = 0.55f,
CheckExtents = new Vector3(0.28f, 0.14f, 0.28f)
},
["box.wooden.large"] = new CrateInfo
{
DisplayName = "Large Wooden Box",
ItemShortname = "box.wooden.large",
Prefab = "assets/prefabs/deployable/large wood storage/box.wooden.large.prefab",
VerticalOffset = 0.76f,
CheckExtents = new Vector3(0.48f, 0.22f, 0.32f)
}
};
private sealed class CrateInfo
{
public string DisplayName;
public string ItemShortname;
public string Prefab;
public float VerticalOffset;
public Vector3 CheckExtents;
public int MaxStack;
}
private sealed class PluginConfig
{
public string ConfigVersion = PluginVersion;
public bool RequirePermission = true;
public bool RequireBuildingPrivilege = true;
public bool RequireOwner = true;
public bool AdminPermissionBypassesRestrictions = true;
public bool AllowSmallWoodenBox = true;
public bool AllowLargeWoodenBox = true;
public int MaxSmallWoodenBoxStack = 5;
public int MaxLargeWoodenBoxStack = 3;
public float RaycastDistance = 4f;
public float PlaceCooldown = 0.35f;
public float HorizontalAlignmentTolerance = 0.18f;
public float VerticalAlignmentTolerance = 0.12f;
public float RotationToleranceDegrees = 12f;
public float CollisionBoundsInset = 0.02f;
public bool OnlyAllowTopCratePickup = true;
public bool ProtectValidStacksFromGroundWatch = true;
public bool RespectCanDeployItemHook = true;
public bool ConsumeItemInCreativeMode = false;
public bool DebugMode = false;
public bool EnableChatMessages = true;
public bool UseSharedNotifications = true;
public string Prefix = "<color=#9efcff>[Ghost Labs]</color>";
}
#region Configuration
protected override void LoadDefaultConfig()
{
_config = new PluginConfig();
NormalizeConfig();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
PluginConfig loadedConfig = Config.ReadObject<PluginConfig>();
if (loadedConfig == null)
{
throw new JsonException("The configuration file was empty.");
}
_config = loadedConfig;
NormalizeConfig();
SaveConfig();
}
catch (Exception ex)
{
PrintWarning(string.Format(
CultureInfo.InvariantCulture,
"Unable to read the configuration. Defaults will be used. Reason: {0}",
ex.Message));
LoadDefaultConfig();
}
}
protected override void SaveConfig()
{
Config.WriteObject(_config, true);
}
private void NormalizeConfig()
{
if (_config == null)
{
_config = new PluginConfig();
}
_config.ConfigVersion = PluginVersion;
_config.MaxSmallWoodenBoxStack = Mathf.Clamp(_config.MaxSmallWoodenBoxStack, 1, 20);
_config.MaxLargeWoodenBoxStack = Mathf.Clamp(_config.MaxLargeWoodenBoxStack, 1, 20);
_config.RaycastDistance = Mathf.Clamp(_config.RaycastDistance, 1f, 8f);
_config.PlaceCooldown = Mathf.Clamp(_config.PlaceCooldown, 0.1f, 5f);
_config.HorizontalAlignmentTolerance = Mathf.Clamp(_config.HorizontalAlignmentTolerance, 0.03f, 0.30f);
_config.VerticalAlignmentTolerance = Mathf.Clamp(_config.VerticalAlignmentTolerance, 0.03f, 0.30f);
_config.RotationToleranceDegrees = Mathf.Clamp(_config.RotationToleranceDegrees, 0f, 45f);
_config.CollisionBoundsInset = Mathf.Clamp(_config.CollisionBoundsInset, 0f, 0.08f);
if (string.IsNullOrWhiteSpace(_config.Prefix))
{
_config.Prefix = "<color=#9efcff>[Ghost Labs]</color>";
}
}
private void ApplyConfigValues()
{
_crates["box.wooden"].MaxStack = _config.MaxSmallWoodenBoxStack;
_crates["box.wooden.large"].MaxStack = _config.MaxLargeWoodenBoxStack;
}
#endregion
#region Lifecycle
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermAdmin, this);
permission.RegisterPermission(PermUnlimited, this);
NormalizeConfig();
ApplyConfigValues();
}
private void OnServerInitialized()
{
Puts(string.Format(
CultureInfo.InvariantCulture,
"GhostLabsCrateStack v{0} loaded. Small max: {1}, large max: {2}.",
Version,
_crates["box.wooden"].MaxStack,
_crates["box.wooden.large"].MaxStack));
}
private void Unload()
{
_cooldowns.Clear();
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player != null)
{
_cooldowns.Remove(player.userID);
}
}
#endregion
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoCommandPermission"] = "You do not have permission to use this command.",
["CommandHelp"] = "Ghost Labs Crate Stack v{0}\n/cratestack info\n/cratestack debug\n/cratestack reload",
["CommandInfo"] = "Version: {0}\nSmall box maximum: {1}\nLarge box maximum: {2}\nTop-only pickup: {3}\nSafe ground support: {4}\nExternal placement hooks: {5}\nDebug mode: {6}",
["UnknownCommand"] = "Unknown command. Use /cratestack info, /cratestack debug, or /cratestack reload.",
["DebugChanged"] = "Debug mode is now {0}.",
["ConfigReloaded"] = "Configuration reloaded.",
["TypeDisabled"] = "Stacking this crate type is disabled.",
["NoUsePermission"] = "You do not have permission to stack crates.",
["NeedBuildingPrivilege"] = "You need building privilege at the new crate position.",
["NotOwner"] = "You can only stack crates in a stack you own.",
["StackLimit"] = "Stack limit reached: {0}/{1}.",
["StackTooLarge"] = "This crate stack is too large to validate safely.",
["PlacementTooFar"] = "Move closer to the top of the crate stack.",
["Blocked"] = "Something is blocking the next crate position: {0}.",
["DeploymentUnavailable"] = "The held crate could not be deployed safely. Re-equip it and try again.",
["CreationFailed"] = "The crate could not be placed.",
["Stacked"] = "Stacked {0}. Height: {1}/{2}.",
["StackedUnlimited"] = "Stacked {0}. Height: {1} (limit bypass).",
["PickupTopFirst"] = "Pick up the top crate first."
}, this);
}
private string Message(string key, BasePlayer player, params object[] args)
{
string value = lang.GetMessage(key, this, player == null ? null : player.UserIDString);
return args == null || args.Length == 0
? value
: string.Format(CultureInfo.InvariantCulture, value, args);
}
#endregion
#region Commands
[ChatCommand("cratestack")]
private void CmdCrateStack(BasePlayer player, string command, string[] args)
{
if (player == null)
{
return;
}
if (!CanUseAdminCommand(player))
{
Notify(player, Message("NoCommandPermission", player), "error");
return;
}
if (args == null || args.Length == 0)
{
Notify(player, Message("CommandHelp", player, Version), "info");
return;
}
string subCommand = args[0] ?? string.Empty;
if (string.Equals(subCommand, "info", StringComparison.OrdinalIgnoreCase))
{
Notify(player, Message(
"CommandInfo",
player,
Version,
_crates["box.wooden"].MaxStack,
_crates["box.wooden.large"].MaxStack,
_config.OnlyAllowTopCratePickup,
_config.ProtectValidStacksFromGroundWatch,
_config.RespectCanDeployItemHook,
_config.DebugMode), "info");
return;
}
if (string.Equals(subCommand, "debug", StringComparison.OrdinalIgnoreCase))
{
_config.DebugMode = !_config.DebugMode;
SaveConfig();
Notify(player, Message("DebugChanged", player, _config.DebugMode ? "enabled" : "disabled"), "success");
return;
}
if (string.Equals(subCommand, "reload", StringComparison.OrdinalIgnoreCase))
{
LoadConfig();
ApplyConfigValues();
Notify(player, Message("ConfigReloaded", player), "success");
return;
}
Notify(player, Message("UnknownCommand", player), "warning");
}
private bool CanUseAdminCommand(BasePlayer player)
{
return player != null &&
(player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermAdmin));
}
private bool HasRestrictionBypass(BasePlayer player)
{
if (player == null)
{
return false;
}
if (player.IsAdmin)
{
return true;
}
return _config.AdminPermissionBypassesRestrictions &&
permission.UserHasPermission(player.UserIDString, PermAdmin);
}
#endregion
#region Rust Hooks
private object OnPlayerInput(BasePlayer player, InputState input)
{
if (player == null || input == null || !input.WasJustPressed(BUTTON.FIRE_PRIMARY))
{
return null;
}
return TryHandleStackInput(player) ? (object)false : null;
}
private bool? CanPickupEntity(BasePlayer player, BaseCombatEntity entity)
{
if (player == null || entity == null || !_config.OnlyAllowTopCratePickup)
{
return null;
}
CrateInfo crate;
if (!TryGetCrateInfo(entity, out crate))
{
return null;
}
if (FindAdjacentCrate(entity, crate, 1, null) == null)
{
return null;
}
Notify(player, Message("PickupTopFirst", player), "warning");
return false;
}
private object OnEntityGroundMissing(BaseEntity entity)
{
if (entity == null || !_config.ProtectValidStacksFromGroundWatch)
{
return null;
}
CrateInfo crate;
if (!TryGetCrateInfo(entity, out crate))
{
return null;
}
BaseEntity support = FindAdjacentCrate(entity, crate, -1, null);
if (support == null)
{
return null;
}
if (_config.DebugMode)
{
Puts(string.Format(
CultureInfo.InvariantCulture,
"[Debug] Preserved stacked crate {0} because valid support {1} is directly below it.",
GetNetworkId(entity),
GetNetworkId(support)));
}
return true;
}
#endregion
#region Placement
private bool TryHandleStackInput(BasePlayer player)
{
if (player == null || player.IsDead() || player.IsSleeping())
{
return false;
}
Item heldItem = player.GetActiveItem();
if (heldItem == null || heldItem.info == null || heldItem.amount <= 0)
{
return false;
}
CrateInfo crate;
if (!_crates.TryGetValue(heldItem.info.shortname, out crate))
{
return false;
}
BaseEntity target = GetLookEntity(player);
if (target == null || !IsSameCrate(target, crate))
{
return false;
}
if (IsOnCooldown(player))
{
return true;
}
if (!IsCrateTypeEnabled(crate))
{
RejectPlacement(player, "TypeDisabled", "warning");
return true;
}
bool bypassRestrictions = HasRestrictionBypass(player);
if (_config.RequirePermission &&
!bypassRestrictions &&
!permission.UserHasPermission(player.UserIDString, PermUse))
{
RejectPlacement(player, "NoUsePermission", "error");
return true;
}
bool traversalLimitReached;
List<BaseEntity> stack = GetConnectedStack(target, crate, out traversalLimitReached);
if (traversalLimitReached || stack.Count == 0)
{
RejectPlacement(player, "StackTooLarge", "warning");
return true;
}
if (_config.RequireOwner && !bypassRestrictions && !PlayerOwnsStack(player, stack))
{
RejectPlacement(player, "NotOwner", "warning");
return true;
}
bool unlimited = bypassRestrictions || permission.UserHasPermission(player.UserIDString, PermUnlimited);
int height = stack.Count;
if (!unlimited && height >= crate.MaxStack)
{
SetCooldown(player);
Notify(player, Message("StackLimit", player, height, crate.MaxStack), "warning");
return true;
}
BaseEntity top = stack[stack.Count - 1];
Vector3 position = top.transform.position + Vector3.up * crate.VerticalOffset;
Quaternion rotation = top.transform.rotation;
if (!IsPlacementWithinReach(player, position, crate))
{
RejectPlacement(player, "PlacementTooFar", "warning");
return true;
}
Bounds placementBounds = GetPlacementBounds(crate);
if (_config.RequireBuildingPrivilege &&
!bypassRestrictions &&
player.IsBuildingBlocked(position, rotation, placementBounds))
{
RejectPlacement(player, "NeedBuildingPrivilege", "warning");
return true;
}
Deployer deployer;
ItemModDeployable modDeployable;
if (!TryGetDeploymentLifecycle(player, heldItem, out deployer, out modDeployable))
{
RejectPlacement(player, "DeploymentUnavailable", "error");
return true;
}
if (_config.RespectCanDeployItemHook &&
deployer != null &&
IsBlockedByCanDeployItem(player, deployer, target))
{
SetCooldown(player);
if (_config.DebugMode)
{
Puts(string.Format(
CultureInfo.InvariantCulture,
"[Debug] An external CanDeployItem hook blocked {0} for {1} ({2}).",
crate.ItemShortname,
player.displayName,
player.userID));
}
return true;
}
BaseEntity blocker;
string blockerName;
if (IsBlocked(position, rotation, crate, top, out blocker, out blockerName))
{
SetCooldown(player);
Notify(player, Message("Blocked", player, blockerName), "warning");
if (_config.DebugMode)
{
DebugBlocker(player, blocker, blockerName, position);
}
return true;
}
if (!TryDeployCrate(player, heldItem, deployer, modDeployable, crate, top, position, rotation, height + 1))
{
RejectPlacement(player, "CreationFailed", "error");
return true;
}
SetCooldown(player);
Notify(
player,
unlimited
? Message("StackedUnlimited", player, crate.DisplayName, height + 1)
: Message("Stacked", player, crate.DisplayName, height + 1, crate.MaxStack),
"success");
return true;
}
private bool IsCrateTypeEnabled(CrateInfo crate)
{
if (crate == null)
{
return false;
}
if (string.Equals(crate.ItemShortname, "box.wooden", StringComparison.OrdinalIgnoreCase))
{
return _config.AllowSmallWoodenBox;
}
if (string.Equals(crate.ItemShortname, "box.wooden.large", StringComparison.OrdinalIgnoreCase))
{
return _config.AllowLargeWoodenBox;
}
return false;
}
private bool PlayerOwnsStack(BasePlayer player, List<BaseEntity> stack)
{
foreach (BaseEntity existing in stack)
{
if (existing == null || existing.IsDestroyed)
{
continue;
}
if (existing.OwnerID != 0UL && existing.OwnerID != player.userID)
{
return false;
}
}
return true;
}
private bool IsPlacementWithinReach(BasePlayer player, Vector3 position, CrateInfo crate)
{
if (player == null || player.eyes == null || crate == null)
{
return false;
}
float maximumDistance = _config.RaycastDistance + crate.VerticalOffset + 0.15f;
return Vector3.Distance(player.eyes.position, position) <= maximumDistance;
}
private Bounds GetPlacementBounds(CrateInfo crate)
{
Vector3 size = crate.CheckExtents * 2f;
return new Bounds(Vector3.zero, size);
}
private bool TryGetDeploymentLifecycle(
BasePlayer player,
Item heldItem,
out Deployer deployer,
out ItemModDeployable modDeployable)
{
deployer = player == null ? null : player.GetHeldEntity() as Deployer;
modDeployable = heldItem == null || heldItem.info == null
? null
: heldItem.info.GetComponent<ItemModDeployable>();
if (modDeployable == null)
{
return false;
}
if (deployer == null && _config.DebugMode && player != null)
{
Puts(string.Format(
CultureInfo.InvariantCulture,
"[Debug] No Deployer instance was exposed for {0} ({1}); native deploy metadata is valid, so stacking will continue without Deployer-only hooks.",
player.displayName,
player.userID));
}
return true;
}
private bool IsBlockedByCanDeployItem(BasePlayer player, Deployer deployer, BaseEntity target)
{
if (player == null || deployer == null || target == null || target.net == null)
{
return true;
}
object hookResult = Interface.CallHook("CanDeployItem", player, deployer, target.net.ID);
return hookResult != null;
}
private bool TryDeployCrate(
BasePlayer player,
Item heldItem,
Deployer deployer,
ItemModDeployable modDeployable,
CrateInfo crate,
BaseEntity support,
Vector3 position,
Quaternion rotation,
int newHeight)
{
BaseEntity placedEntity = GameManager.server.CreateEntity(crate.Prefab, position, rotation, true);
if (placedEntity == null)
{
return false;
}
try
{
placedEntity.skinID = heldItem.skin;
placedEntity.SendMessage("SetDeployedBy", player, SendMessageOptions.DontRequireReceiver);
placedEntity.OwnerID = player.userID;
placedEntity.Spawn();
if (support != null && support.HasParent())
{
placedEntity.SetParent(support.GetParentEntity(), true, true);
}
modDeployable.OnDeployed(placedEntity, player);
if (deployer != null)
{
Interface.CallHook("OnItemDeployed", deployer, modDeployable, placedEntity);
}
if (_config.ConsumeItemInCreativeMode || !player.IsInCreativeMode || !ConVar.Creative.freeBuild)
{
heldItem.UseItem(1);
}
Interface.CallHook("OnGhostLabsCrateStacked", player, placedEntity, support, newHeight);
return true;
}
catch (Exception ex)
{
PrintError(string.Format(
CultureInfo.InvariantCulture,
"Failed to deploy {0} for {1} ({2}): {3}",
crate.ItemShortname,
player.displayName,
player.userID,
ex));
if (placedEntity != null && !placedEntity.IsDestroyed)
{
placedEntity.Kill();
}
return false;
}
}
private void RejectPlacement(BasePlayer player, string messageKey, string notificationType)
{
SetCooldown(player);
Notify(player, Message(messageKey, player), notificationType);
}
#endregion
#region Connected Stack Detection
private List<BaseEntity> GetConnectedStack(BaseEntity anchor, CrateInfo crate, out bool traversalLimitReached)
{
List<BaseEntity> lower = new List<BaseEntity>();
List<BaseEntity> upper = new List<BaseEntity>();
HashSet<BaseEntity> visited = new HashSet<BaseEntity>();
traversalLimitReached = false;
if (anchor == null || crate == null)
{
return upper;
}
visited.Add(anchor);
BaseEntity cursor = anchor;
for (int i = 0; i < MaxTraversalDepth; i++)
{
BaseEntity next = FindAdjacentCrate(cursor, crate, -1, visited);
if (next == null)
{
break;
}
lower.Add(next);
visited.Add(next);
cursor = next;
if (i == MaxTraversalDepth - 1)
{
traversalLimitReached = true;
}
}
lower.Reverse();
upper.AddRange(lower);
upper.Add(anchor);
cursor = anchor;
for (int i = 0; i < MaxTraversalDepth; i++)
{
BaseEntity next = FindAdjacentCrate(cursor, crate, 1, visited);
if (next == null)
{
break;
}
upper.Add(next);
visited.Add(next);
cursor = next;
if (i == MaxTraversalDepth - 1)
{
traversalLimitReached = true;
}
}
return upper;
}
private BaseEntity FindAdjacentCrate(
BaseEntity origin,
CrateInfo crate,
int direction,
HashSet<BaseEntity> excluded)
{
if (origin == null || origin.IsDestroyed || crate == null || direction == 0)
{
return null;
}
direction = direction > 0 ? 1 : -1;
Vector3 expectedPosition = origin.transform.position + Vector3.up * crate.VerticalOffset * direction;
float searchRadius = Mathf.Sqrt(
_config.HorizontalAlignmentTolerance * _config.HorizontalAlignmentTolerance +
_config.VerticalAlignmentTolerance * _config.VerticalAlignmentTolerance) + 0.05f;
int hitCount = Physics.OverlapSphereNonAlloc(
expectedPosition,
searchRadius,
_overlapBuffer,
Physics.AllLayers,
QueryTriggerInteraction.Ignore);
if (hitCount >= _overlapBuffer.Length)
{
ClearOverlapBuffer(hitCount);
return null;
}
BaseEntity best = null;
float bestScore = float.MaxValue;
for (int index = 0; index < hitCount; index++)
{
Collider collider = _overlapBuffer[index];
if (collider == null)
{
continue;
}
BaseEntity candidate = collider.GetComponentInParent<BaseEntity>();
if (candidate == null || candidate.IsDestroyed || candidate == origin)
{
continue;
}
if (excluded != null && excluded.Contains(candidate))
{
continue;
}
if (!IsSameCrate(candidate, crate))
{
continue;
}
Vector3 delta = candidate.transform.position - origin.transform.position;
float signedVerticalDistance = delta.y * direction;
if (signedVerticalDistance <= 0f)
{
continue;
}
float verticalError = Mathf.Abs(signedVerticalDistance - crate.VerticalOffset);
if (verticalError > _config.VerticalAlignmentTolerance)
{
continue;
}
float horizontalDistance = Vector2.Distance(
new Vector2(origin.transform.position.x, origin.transform.position.z),
new Vector2(candidate.transform.position.x, candidate.transform.position.z));
if (horizontalDistance > _config.HorizontalAlignmentTolerance)
{
continue;
}
if (Quaternion.Angle(origin.transform.rotation, candidate.transform.rotation) >
_config.RotationToleranceDegrees)
{
continue;
}
float score = horizontalDistance * horizontalDistance + verticalError * verticalError;
if (score >= bestScore)
{
continue;
}
best = candidate;
bestScore = score;
}
ClearOverlapBuffer(hitCount);
return best;
}
#endregion
#region Collision and Entity Helpers
private bool IsBlocked(
Vector3 position,
Quaternion rotation,
CrateInfo crate,
BaseEntity support,
out BaseEntity blocker,
out string blockerName)
{
blocker = null;
blockerName = "object";
Vector3 extents = crate.CheckExtents;
extents.x = Mathf.Max(0.05f, extents.x - _config.CollisionBoundsInset);
extents.y = Mathf.Max(0.05f, extents.y - _config.CollisionBoundsInset);
extents.z = Mathf.Max(0.05f, extents.z - _config.CollisionBoundsInset);
int hitCount = Physics.OverlapBoxNonAlloc(
position,
extents,
_overlapBuffer,
rotation,
Physics.AllLayers,
QueryTriggerInteraction.Ignore);
if (hitCount >= _overlapBuffer.Length)
{
blockerName = "crowded area";
ClearOverlapBuffer(hitCount);
return true;
}
for (int index = 0; index < hitCount; index++)
{
Collider collider = _overlapBuffer[index];
if (collider == null)
{
continue;
}
BaseEntity entity = collider.GetComponentInParent<BaseEntity>();
if (entity == support)
{
continue;
}
if (entity != null && entity.IsDestroyed)
{
continue;
}
blocker = entity;
blockerName = entity == null
? GetColliderName(collider)
: GetEntityName(entity);
ClearOverlapBuffer(hitCount);
return true;
}
ClearOverlapBuffer(hitCount);
return false;
}
private void ClearOverlapBuffer(int count)
{
int maximum = Mathf.Min(count, _overlapBuffer.Length);
for (int index = 0; index < maximum; index++)
_overlapBuffer[index] = null;
}
private BaseEntity GetLookEntity(BasePlayer player)
{
if (player == null || player.eyes == null)
{
return null;
}
RaycastHit hit;
if (!Physics.Raycast(
player.eyes.HeadRay(),
out hit,
_config.RaycastDistance,
Physics.AllLayers,
QueryTriggerInteraction.Ignore))
{
return null;
}
return hit.GetEntity();
}
private bool TryGetCrateInfo(BaseEntity entity, out CrateInfo crateInfo)
{
crateInfo = null;
if (entity == null || entity.IsDestroyed || string.IsNullOrEmpty(entity.PrefabName))
{
return false;
}
foreach (CrateInfo crate in _crates.Values)
{
if (!entity.PrefabName.Equals(crate.Prefab, StringComparison.OrdinalIgnoreCase))
{
continue;
}
crateInfo = crate;
return true;
}
return false;
}
private bool IsSameCrate(BaseEntity entity, CrateInfo crate)
{
return entity != null &&
!entity.IsDestroyed &&
crate != null &&
!string.IsNullOrEmpty(entity.PrefabName) &&
entity.PrefabName.Equals(crate.Prefab, StringComparison.OrdinalIgnoreCase);
}
private string GetEntityName(BaseEntity entity)
{
if (entity == null)
{
return "object";
}
if (!string.IsNullOrEmpty(entity.ShortPrefabName))
{
return entity.ShortPrefabName;
}
if (!string.IsNullOrEmpty(entity.PrefabName))
{
return entity.PrefabName;
}
return entity.GetType().Name;
}
private string GetColliderName(Collider collider)
{
if (collider == null)
{
return "world geometry";
}
if (collider.gameObject != null && !string.IsNullOrEmpty(collider.gameObject.name))
{
return collider.gameObject.name;
}
return collider.GetType().Name;
}
private string GetNetworkId(BaseEntity entity)
{
return entity == null || entity.net == null
? "unknown"
: entity.net.ID.ToString();
}
private void DebugBlocker(BasePlayer player, BaseEntity blocker, string blockerName, Vector3 attemptedPosition)
{
string prefab = blocker == null ? "world geometry" : blocker.PrefabName;
string type = blocker == null ? "Collider" : blocker.GetType().Name;
string message = string.Format(
CultureInfo.InvariantCulture,
"Debug blocker\nName: {0}\nType: {1}\nPrefab: {2}\nAttempted position: {3}",
blockerName,
type,
prefab,
attemptedPosition);
Notify(player, message, "info");
Puts(string.Format(
CultureInfo.InvariantCulture,
"[Debug] Blocked crate stack for {0} ({1}): {2} | {3} | {4} | Position: {5}",
player.displayName,
player.userID,
blockerName,
type,
prefab,
attemptedPosition));
}
#endregion
#region Cooldown and Notifications
private bool IsOnCooldown(BasePlayer player)
{
float nextAllowedTime;
return player != null &&
_cooldowns.TryGetValue(player.userID, out nextAllowedTime) &&
Time.realtimeSinceStartup < nextAllowedTime;
}
private void SetCooldown(BasePlayer player)
{
if (player != null)
{
_cooldowns[player.userID] = Time.realtimeSinceStartup + _config.PlaceCooldown;
}
}
private void Notify(BasePlayer player, string message, string type)
{
if (player == null || string.IsNullOrEmpty(message))
{
return;
}
if (_config.UseSharedNotifications)
{
Plugin notifications = GhostLabsNotifications ??
plugins.Find("GhostLabs.Notifications") ??
plugins.Find("GhostLabsNotifications");
if (notifications != null)
{
object result = notifications.Call(
"SendSimpleNotification",
player.userID,
message,
string.IsNullOrEmpty(type) ? "info" : type);
if (result is bool && (bool)result)
{
return;
}
}
}
if (_config.EnableChatMessages)
{
player.ChatMessage(string.Format(
CultureInfo.InvariantCulture,
"{0} {1}",
_config.Prefix,
message));
}
}
#endregion
}
}