C#:
/*
* Copyright (C) 2024 Game4Freak.io
* This mod is provided under the Game4Freak EULA.
* Full legal terms can be found at https://game4freak.io/eula/
*/
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Oxide.Plugins
{
[Info("Custom Crate Spawns", "VisEntities", "1.1.1")]
[Description("Lets you add additional spawn points for loot containers.")]
public class CustomCrateSpawns : RustPlugin
{
#region Fields
private static CustomCrateSpawns _plugin;
private System.Random _random = new System.Random();
private SpawnGroupData _spawnGroupBeingEdited;
private LootTableData _lootTableBeingEdited;
private List<LootSpawnerComponent> _lootSpawners = new List<LootSpawnerComponent>();
private Dictionary<string, LootTableData> _lootTables = new Dictionary<string, LootTableData>(StringComparer.OrdinalIgnoreCase);
public const int LAYER_CONTAINERS = Layers.Mask.Default;
public const int LAYER_PLAYERS = Layers.Mask.Player_Server;
public const int LAYER_BUILDINGS = Layers.Mask.Construction | Layers.Mask.Deployed;
public const int LAYER_GROUND = Layers.Mask.Terrain | Layers.Mask.World | Layers.Mask.Default;
private static readonly Dictionary<string, string> _containerPrefabs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{"junk", "assets/bundled/prefabs/radtown/crate_normal_2.prefab"},
{"military", "assets/bundled/prefabs/radtown/crate_normal.prefab"},
{"elite", "assets/bundled/prefabs/radtown/crate_elite.prefab"},
{"medical", "assets/bundled/prefabs/radtown/crate_normal_2_medical.prefab"},
{"food", "assets/bundled/prefabs/radtown/crate_normal_2_food.prefab"},
{"dmammo", "assets/bundled/prefabs/radtown/dmloot/dm ammo.prefab"},
{"dmc4", "assets/bundled/prefabs/radtown/dmloot/dm c4.prefab"},
{"dmfood", "assets/bundled/prefabs/radtown/dmloot/dm food.prefab"},
{"foodbox", "assets/bundled/prefabs/radtown/foodbox.prefab"},
{"tools", "assets/bundled/prefabs/radtown/crate_tools.prefab"},
{"vehicleparts", "assets/bundled/prefabs/radtown/vehicle_parts.prefab"},
{"waterammo", "assets/bundled/prefabs/radtown/underwater_labs/crate_ammunition.prefab"},
{"watermedical", "assets/bundled/prefabs/radtown/underwater_labs/crate_medical.prefab"},
{"waterfood", "assets/bundled/prefabs/radtown/underwater_labs/crate_food_1.prefab"},
{"waterfoodbox", "assets/bundled/prefabs/radtown/underwater_labs/crate_food_2.prefab"},
{"waterfuel", "assets/bundled/prefabs/radtown/underwater_labs/crate_fuel.prefab"},
{"watertechparts", "assets/bundled/prefabs/radtown/underwater_labs/tech_parts_2.prefab"},
{"hackable", "assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate.prefab"},
{"hackableoilrig", "assets/prefabs/deployable/chinooklockedcrate/codelockedhackablecrate_oilrig.prefab"},
{"bradley", "assets/prefabs/npc/m2bradley/bradley_crate.prefab"},
{"patrolheli", "assets/prefabs/npc/patrol helicopter/heli_crate.prefab"},
{"supplydrop", "assets/prefabs/misc/supply drop/supply_drop.prefab"},
{"barrel", "assets/bundled/prefabs/radtown/loot_barrel_2.prefab"},
{"barrelblue", "assets/bundled/prefabs/radtown/loot_barrel_1.prefab"},
{"barreloil", "assets/bundled/prefabs/radtown/oil_barrel.prefab"},
{"barreldiesel", "assets/prefabs/resource/diesel barrel/diesel_barrel_world.prefab"},
};
#endregion Fields
#region Stored Data
public class SpawnGroupData
{
[JsonProperty("Unique Name")]
public string UniqueName { get; set; }
[JsonProperty("Internal Id (do not modify)")]
public Guid InternalId { get; set; }
[JsonProperty("Enabled")]
public bool Enabled { get; set; }
[JsonProperty("Maximum Containers At Once")]
public int MaximumContainersAtOnce { get; set; }
[JsonProperty("Minimum Containers To Spawn Per Cycle")]
public int MinimumContainersToSpawnPerCycle { get; set; }
[JsonProperty("Maximum Containers To Spawn Per Cycle")]
public int MaximumContainersToSpawnPerCycle { get; set; }
[JsonProperty("Spawn Immediately On Server Start")]
public bool SpawnImmediatelyOnServerStart { get; set; }
[JsonProperty("Minimum Respawn Delay In Seconds")]
public float MinimumRespawnDelayInSeconds { get; set; }
[JsonProperty("Maximum Respawn Delay In Seconds")]
public float MaximumRespawnDelayInSeconds { get; set; }
[JsonProperty("Randomize Container Rotation")]
public bool RandomizeContainerRotation { get; set; }
[JsonProperty("Respawn When Looted Empty")]
public bool RespawnWhenLootedEmpty { get; set; }
[JsonProperty("Loot Table (null = game default loot)")]
public string LootTable { get; set; }
[JsonProperty("Container Types")]
public List<PrefabData> ContainerTypes { get; set; } = new List<PrefabData>();
[JsonProperty("Spawn Points")]
public List<SpawnPointData> SpawnPoints { get; set; } = new List<SpawnPointData>();
}
public class PrefabData
{
[JsonProperty("Container Type")]
public string ContainerType { get; set; }
[JsonProperty("Selection Weight (higher = more likely)")]
public int SelectionWeight { get; set; }
[JsonProperty("Loot Table (null = use group setting)")]
public string LootTable { get; set; }
}
public class SpawnPointData
{
[JsonProperty("Unique Id (do not modify)")]
public string UniqueId { get; set; }
[JsonProperty("World Position (x, y, z)")]
public Vector3 WorldPosition { get; set; }
[JsonProperty("Rotation (euler angles)")]
public Vector3 RotationEulerAngles { get; set; }
[JsonProperty("Spawn Radius (0 = exact position)")]
public float SpawnRadius { get; set; }
}
public class LootTableData
{
[JsonProperty("Loot Table Name")]
public string LootTableName { get; set; }
[JsonProperty("Minimum Items To Spawn")]
public int MinimumItemsToSpawn { get; set; } = 1;
[JsonProperty("Maximum Items To Spawn")]
public int MaximumItemsToSpawn { get; set; } = 6;
[JsonProperty("Loot Items")]
public List<LootItemData> LootItems { get; set; } = new List<LootItemData>();
}
public class LootItemData
{
[JsonProperty("Item Shortname")]
public string ItemShortname { get; set; }
[JsonProperty("Selection Weight (higher = more likely)")]
public int SelectionWeight { get; set; } = 1;
[JsonProperty("Minimum Stack Size")]
public int MinimumStackSize { get; set; } = 1;
[JsonProperty("Maximum Stack Size")]
public int MaximumStackSize { get; set; } = 1;
[JsonProperty("Skin Id (0 = default skin)")]
public ulong SkinId { get; set; } = 0;
[JsonProperty("Custom Display Name (null = default name)")]
public string CustomDisplayName { get; set; }
[JsonProperty("Spawn As Blueprint")]
public bool SpawnAsBlueprint { get; set; } = false;
[JsonProperty("Condition (0.0-1.0, 1.0 = full durability)")]
public float Condition { get; set; } = 1f;
}
#endregion Stored Data
#region Oxide Hooks
private void Init()
{
_plugin = this;
DataFileUtil.EnsureFolderCreated();
PermissionUtil.RegisterPermissions();
}
private void Unload()
{
if (_lootSpawners != null)
{
for (int i = _lootSpawners.Count - 1; i >= 0; i--)
{
LootSpawnerComponent lootSpawner = _lootSpawners[i];
if (lootSpawner != null)
lootSpawner.Destroy();
}
_lootSpawners.Clear();
}
_plugin = null;
}
private void OnServerInitialized(bool isStartup)
{
LoadAllLootTables();
string[] spawnGroupFiles = DataFileUtil.GetAllFilePaths();
foreach (string filePath in spawnGroupFiles)
{
SpawnGroupData spawnGroup = DataFileUtil.LoadIfExists<SpawnGroupData>(filePath);
if (spawnGroup != null)
{
LootSpawnerComponent.Create(spawnGroup);
}
}
}
private void OnLootEntityEnd(BasePlayer player, LootContainer lootContainer)
{
if (player == null || lootContainer == null)
return;
if (lootContainer.inventory == null || lootContainer.inventory.itemList == null || lootContainer.inventory.itemList.Count == 0)
{
LootSpawnerComponent lootSpawner = GetSpawnerForContainer(lootContainer);
if (lootSpawner != null)
{
lootSpawner.OnLootContainerLooted(lootContainer);
}
}
}
#endregion Oxide Hooks
#region Loot Table Management
private void LoadAllLootTables()
{
_lootTables.Clear();
string[] lootTableFiles = DataFileUtil.GetAllLootTableFilePaths();
foreach (string filePath in lootTableFiles)
{
LootTableData lootTable = DataFileUtil.LoadIfExists<LootTableData>(filePath);
if (lootTable != null && !string.IsNullOrEmpty(lootTable.LootTableName))
{
_lootTables[lootTable.LootTableName] = lootTable;
}
}
}
private LootTableData GetLootTable(string name)
{
if (string.IsNullOrEmpty(name))
return null;
_lootTables.TryGetValue(name, out LootTableData lootTable);
return lootTable;
}
private void PopulateContainerLoot(LootContainer container, LootTableData lootTable)
{
if (container == null || lootTable == null || lootTable.LootItems == null || lootTable.LootItems.Count == 0)
return;
if (container.inventory != null)
{
container.inventory.Clear();
ItemManager.DoRemoves();
}
int slotsToFill = Random.Range(lootTable.MinimumItemsToSpawn, lootTable.MaximumItemsToSpawn + 1);
slotsToFill = Mathf.Min(slotsToFill, lootTable.LootItems.Count);
List<LootItemData> availableItems = new List<LootItemData>(lootTable.LootItems);
for (int i = 0; i < slotsToFill && availableItems.Count > 0; i++)
{
LootItemData selectedItem = SelectWeightedItem(availableItems);
if (selectedItem == null)
break;
availableItems.Remove(selectedItem);
Item item = CreateLootItem(selectedItem);
if (item == null)
continue;
if (!item.MoveToContainer(container.inventory))
{
item.Remove();
}
}
}
private LootItemData SelectWeightedItem(List<LootItemData> items)
{
if (items == null || items.Count == 0)
return null;
int totalWeight = items.Sum(item => item.SelectionWeight);
if (totalWeight <= 0)
return items[0];
int randomValue = Random.Range(0, totalWeight);
foreach (LootItemData item in items)
{
randomValue -= item.SelectionWeight;
if (randomValue < 0)
return item;
}
return items[items.Count - 1];
}
private Item CreateLootItem(LootItemData itemData)
{
if (itemData == null || string.IsNullOrEmpty(itemData.ItemShortname))
return null;
ItemDefinition itemDef = ItemManager.FindItemDefinition(itemData.ItemShortname);
if (itemDef == null)
return null;
int amount = Random.Range(itemData.MinimumStackSize, itemData.MaximumStackSize + 1);
amount = Mathf.Max(1, amount);
Item item;
if (itemData.SpawnAsBlueprint)
{
item = ItemManager.CreateByName("blueprintbase", 1);
if (item != null)
{
item.blueprintTarget = itemDef.itemid;
}
}
else
{
item = ItemManager.Create(itemDef, amount, itemData.SkinId);
}
if (item == null)
return null;
if (!string.IsNullOrEmpty(itemData.CustomDisplayName))
{
item.name = itemData.CustomDisplayName;
}
if (!itemData.SpawnAsBlueprint && item.hasCondition)
{
item.condition = item.maxCondition * Mathf.Clamp01(itemData.Condition);
}
return item;
}
#endregion Loot Table Management
#region Loot Spawner Component
public class LootSpawnerComponent : FacepunchBehaviour
{
#region Fields
private bool _isDestroying;
public SpawnGroupData Data { get; set; }
public List<SpawnPointComponent> SpawnPoints { get; set; } = new List<SpawnPointComponent>();
public List<LootContainer> SpawnedEntities { get; set; } = new List<LootContainer>();
public int CurrentPopulation
{
get
{
return SpawnedEntities.Count(lootContainer => lootContainer != null);
}
}
#endregion Fields
#region Initialization
public static LootSpawnerComponent Create(SpawnGroupData data)
{
LootSpawnerComponent spawner = new GameObject().AddComponent<LootSpawnerComponent>();
spawner.Initialize(data);
return spawner;
}
public void Initialize(SpawnGroupData data)
{
Data = data;
foreach (SpawnPointData spawnPointData in Data.SpawnPoints)
{
SpawnPointComponent.Create(spawnPointData, this);
}
_plugin._lootSpawners.Add(this);
}
public void Destroy()
{
_isDestroying = true;
DestroyImmediate(this);
}
#endregion Initialization
#region Component Lifecycle
private void Start()
{
if (!Data.Enabled)
return;
float initialSpawnTime = GetNextSpawnTime();
if (Data.SpawnImmediatelyOnServerStart)
initialSpawnTime = 0f;
InvokeRepeating(nameof(TimedSpawn), initialSpawnTime, GetNextSpawnTime());
}
private void OnDestroy()
{
CancelInvoke(nameof(TimedSpawn));
Clear();
for (int i = SpawnPoints.Count - 1; i >= 0; i--)
{
if (SpawnPoints[i] != null)
SpawnPoints[i].Destroy();
}
_plugin._lootSpawners.Remove(this);
}
#endregion Component Lifecycle
#region Spawn Lifecycle
private void TimedSpawn()
{
if (!_isDestroying && CurrentPopulation < Data.MaximumContainersAtOnce)
{
int numberToSpawn = Random.Range(Data.MinimumContainersToSpawnPerCycle, Data.MaximumContainersToSpawnPerCycle + 1);
Spawn(numberToSpawn);
}
}
private float GetNextSpawnTime()
{
return Random.Range(Data.MinimumRespawnDelayInSeconds, Data.MaximumRespawnDelayInSeconds);
}
public void Spawn(int numberToSpawn)
{
numberToSpawn = Mathf.Min(numberToSpawn, Data.MaximumContainersAtOnce - CurrentPopulation);
for (int i = 0; i < numberToSpawn; i++)
{
PrefabData selectedPrefab = SelectContainerType();
if (selectedPrefab == null)
continue;
if (!_containerPrefabs.TryGetValue(selectedPrefab.ContainerType, out string prefabPath))
continue;
if (GetSpawnPoint(prefabPath, out Vector3 position, out Quaternion rotation) == null)
continue;
if (OnCustomCrateSpawn(Data.UniqueName, prefabPath, position, rotation))
continue;
LootContainer lootContainer = GameManager.server.CreateEntity(prefabPath, position, rotation) as LootContainer;
if (lootContainer == null)
continue;
lootContainer.Spawn();
lootContainer.EnableSaving(false);
SpawnedEntities.Add(lootContainer);
ApplyCustomLoot(lootContainer, selectedPrefab);
}
}
private void ApplyCustomLoot(LootContainer lootContainer, PrefabData prefabData)
{
string lootTableName = prefabData.LootTable;
if (string.IsNullOrEmpty(lootTableName))
lootTableName = Data.LootTable;
if (string.IsNullOrEmpty(lootTableName))
return;
LootTableData lootTable = _plugin.GetLootTable(lootTableName);
if (lootTable != null)
{
_plugin.PopulateContainerLoot(lootContainer, lootTable);
}
}
public void Fill()
{
int numberToFill = Data.MaximumContainersAtOnce - CurrentPopulation;
Spawn(numberToFill);
}
public void Clear()
{
for (int i = SpawnedEntities.Count - 1; i >= 0; i--)
{
LootContainer lootContainer = SpawnedEntities[i];
if (lootContainer != null)
lootContainer.Kill();
SpawnedEntities.RemoveAt(i);
}
}
#endregion Spawn Lifecycle
#region Prefab Selection
public PrefabData SelectContainerType()
{
int totalWeight = Data.ContainerTypes.Sum(prefab => prefab.SelectionWeight);
if (totalWeight <= 0)
return null;
float randomValue = Random.Range(0, totalWeight);
foreach (PrefabData prefab in Data.ContainerTypes)
{
randomValue -= prefab.SelectionWeight;
if (randomValue <= 0)
{
if (_containerPrefabs.ContainsKey(prefab.ContainerType))
return prefab;
else
return null;
}
}
PrefabData lastPrefab = Data.ContainerTypes.LastOrDefault();
if (lastPrefab != null && _containerPrefabs.ContainsKey(lastPrefab.ContainerType))
return lastPrefab;
return null;
}
public string SelectContainerPrefab()
{
PrefabData prefabData = SelectContainerType();
if (prefabData != null && _containerPrefabs.TryGetValue(prefabData.ContainerType, out string fullPrefabPath))
return fullPrefabPath;
return null;
}
#endregion Prefab Selection
#region Spawn Point Selection
public SpawnPointComponent GetSpawnPoint(string prefabPath, out Vector3 position, out Quaternion rotation)
{
Shuffle(SpawnPoints);
foreach (SpawnPointComponent spawnPoint in SpawnPoints)
{
if (spawnPoint.HasPlayersIntersecting())
continue;
for (int attempts = 0; attempts < 5; attempts++)
{
position = spawnPoint.GetPosition();
rotation = spawnPoint.GetRotation();
if (Data.RandomizeContainerRotation)
rotation *= Quaternion.Euler(0, Random.Range(0f, 360f), 0);
if (!spawnPoint.HasSpaceToSpawn(prefabPath, position, rotation))
continue;
return spawnPoint;
}
}
position = default;
rotation = default;
return null;
}
#endregion Spawn Point Selection
#region Loot Containers Removal
public void OnLootContainerLooted(LootContainer lootContainer)
{
if (!_isDestroying && Data.RespawnWhenLootedEmpty)
Spawn(1);
}
#endregion Loot Containers Removal
}
#endregion Loot Spawner Component
#region Spawn Point Component
public class SpawnPointComponent : FacepunchBehaviour
{
#region Fields
public LootSpawnerComponent LootSpawner { get; set; }
public SpawnPointData Data { get; set; }
#endregion Fields
#region Initialization
public static SpawnPointComponent Create(SpawnPointData data, LootSpawnerComponent lootSpawner)
{
SpawnPointComponent spawnPoint = new GameObject().AddComponent<SpawnPointComponent>();
spawnPoint.Initialize(data, lootSpawner);
return spawnPoint;
}
public void Initialize(SpawnPointData data, LootSpawnerComponent lootSpawner)
{
Data = data;
LootSpawner = lootSpawner;
LootSpawner.SpawnPoints.Add(this);
}
public void Destroy()
{
DestroyImmediate(this);
}
#endregion Initialization
#region Component Lifecycle
private void OnDestroy()
{
LootSpawner.SpawnPoints.Remove(this);
}
#endregion Component Lifecycle
#region Position and Rotation Retrieval
public Vector3 GetPosition()
{
Vector3 targetPosition;
if (Data.SpawnRadius > 0)
targetPosition = TerrainUtil.GetRandomPositionAround(Data.WorldPosition, 0f, Data.SpawnRadius);
else
targetPosition = Data.WorldPosition;
if (TerrainUtil.GetGroundInfo(targetPosition, out RaycastHit hit, Data.SpawnRadius, LAYER_GROUND | LAYER_BUILDINGS))
targetPosition = hit.point;
return targetPosition;
}
public Quaternion GetRotation()
{
Quaternion baseRotation = Quaternion.Euler(Data.RotationEulerAngles);
if (TerrainUtil.GetGroundInfo(Data.WorldPosition, out RaycastHit hit, 2f, LAYER_GROUND | LAYER_BUILDINGS))
{
Quaternion terrainAlignment = Quaternion.FromToRotation(Vector3.up, hit.normal);
return terrainAlignment * baseRotation;
}
return baseRotation;
}
#endregion Position and Rotation Retrieval
#region Collision and Space Validation
public bool HasSpaceToSpawn(string prefabPath, Vector3 position, Quaternion rotation)
{
GameObject prefab = GameManager.server.FindPrefab(prefabPath);
if (prefab == null)
return false;
BaseEntity prefabEntity = prefab.GetComponent<BaseEntity>();
if (prefabEntity == null)
return false;
OBB entityBounds = new OBB(position, rotation, prefabEntity.bounds);
Collider[] colliders = Physics.OverlapBox(
entityBounds.position,
entityBounds.extents,
entityBounds.rotation,
LAYER_CONTAINERS,
QueryTriggerInteraction.Ignore
);
if (colliders.Length > 0)
return false;
return true;
}
public bool HasPlayersIntersecting()
{
return BaseNetworkable.HasCloseConnections(Data.WorldPosition, Mathf.Max(Data.SpawnRadius, 2f));
}
#endregion Collision and Space Validation
}
#endregion Spawn Point Component
#region API
[HookMethod(nameof(API_SpawnCratesForGroup))]
public void API_SpawnCratesForGroup(string spawnGroupName)
{
LootSpawnerComponent lootSpawner = GetSpawnerByName(spawnGroupName);
if (lootSpawner != null)
{
lootSpawner.Fill();
}
}
[HookMethod(nameof(API_ClearCratesForGroup))]
public void API_ClearCratesForGroup(string spawnGroupName)
{
LootSpawnerComponent lootSpawner = GetSpawnerByName(spawnGroupName);
if (lootSpawner != null)
{
lootSpawner.Clear();
}
}
[HookMethod(nameof(API_IsCrateFromCustomSpawner))]
public bool API_IsCrateFromCustomSpawner(LootContainer lootContainer)
{
return GetSpawnerForContainer(lootContainer) != null;
}
[HookMethod(nameof(API_SpawnGroupExists))]
public bool API_SpawnGroupExists(string spawnGroupName)
{
if (string.IsNullOrEmpty(spawnGroupName))
return false;
string filePath = DataFileUtil.GetFilePath(spawnGroupName);
return DataFileUtil.Exists(filePath);
}
[HookMethod(nameof(API_GetAllSpawnGroupAliases))]
public List<string> API_GetAllSpawnGroupAliases()
{
var result = new List<string>();
if (_lootSpawners == null)
return result;
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var spawner in _lootSpawners)
{
if (spawner == null || spawner.Data == null)
continue;
var alias = spawner.Data.UniqueName;
if (string.IsNullOrEmpty(alias))
continue;
if (seen.Add(alias))
result.Add(alias);
}
return result;
}
[HookMethod(nameof(API_RefreshLootForGroup))]
public void API_RefreshLootForGroup(string spawnGroupName)
{
LootSpawnerComponent lootSpawner = GetSpawnerByName(spawnGroupName);
if (lootSpawner == null)
return;
foreach (LootContainer container in lootSpawner.SpawnedEntities)
{
if (container == null)
continue;
string lootTableName = lootSpawner.Data.LootTable;
LootTableData lootTable = GetLootTable(lootTableName);
if (lootTable != null)
{
PopulateContainerLoot(container, lootTable);
}
}
}
#endregion API
#region Exposed Hooks
private static bool OnCustomCrateSpawn(string spawnGroupName, string prefab, Vector3 position, Quaternion rotation)
{
object hookResult = Interface.CallHook("OnCustomCrateSpawn", spawnGroupName, prefab, position, rotation);
return hookResult is bool && (bool)hookResult == false;
}
#endregion Exposed Hooks
#region Helper Functions
private LootSpawnerComponent GetSpawnerByName(string spawnGroupName)
{
if (string.IsNullOrEmpty(spawnGroupName))
return null;
foreach (LootSpawnerComponent lootSpawner in _lootSpawners)
{
if (lootSpawner.Data.UniqueName.Equals(spawnGroupName, StringComparison.OrdinalIgnoreCase))
return lootSpawner;
}
return null;
}
private LootSpawnerComponent GetSpawnerForContainer(LootContainer lootContainer)
{
foreach (LootSpawnerComponent lootSpawner in _lootSpawners)
{
if (lootSpawner.SpawnedEntities.Contains(lootContainer))
return lootSpawner;
}
return null;
}
#endregion Helper Functions
#region Helper Functions
private static void Shuffle<T>(List<T> list)
{
int remainingItems = list.Count;
while (remainingItems > 1)
{
remainingItems--;
int randomIndex = _plugin._random.Next(remainingItems + 1);
T itemToSwap = list[randomIndex];
list[randomIndex] = list[remainingItems];
list[remainingItems] = itemToSwap;
}
}
private static string FindPrefabPath(string shortPrefabName)
{
foreach (string prefabPath in GameManifest.Current.entities)
{
string prefabFileName = prefabPath.Substring(prefabPath.LastIndexOf("/") + 1).Replace(".prefab", "");
if (string.Equals(prefabFileName, shortPrefabName, StringComparison.OrdinalIgnoreCase))
return prefabPath;
}
return null;
}
private static string GetShortPrefabName(string fullPrefabPath)
{
if (string.IsNullOrEmpty(fullPrefabPath))
return string.Empty;
int lastSlashIndex = fullPrefabPath.LastIndexOf("/");
if (lastSlashIndex == -1)
return fullPrefabPath.Replace(".prefab", "");
return fullPrefabPath.Substring(lastSlashIndex + 1).Replace(".prefab", "");
}
#endregion Helper Functions
#region Helper Classes
public static class TerrainUtil
{
public static Vector3 GetRandomPositionAround(Vector3 centerPosition, float minimumRadius, float maximumRadius)
{
Vector3 randomDirection = Random.onUnitSphere;
randomDirection.y = 0;
float randomDistance = Random.Range(minimumRadius, maximumRadius);
Vector3 randomPosition = centerPosition + randomDirection * randomDistance;
return randomPosition;
}
public static bool GetGroundInfo(Vector3 startPosition, out RaycastHit raycastHit, float range, LayerMask mask)
{
return Physics.Linecast(startPosition + new Vector3(0.0f, range, 0.0f), startPosition - new Vector3(0.0f, range, 0.0f), out raycastHit, mask);
}
public static bool GetGroundInfo(Vector3 startPosition, out RaycastHit raycastHit, float range, LayerMask mask, Transform ignoreTransform = null)
{
startPosition.y += 0.25f;
range += 0.25f;
raycastHit = default;
RaycastHit hit;
if (!GamePhysics.Trace(new Ray(startPosition, Vector3.down), 0f, out hit, range, mask, QueryTriggerInteraction.UseGlobal, null))
return false;
if (ignoreTransform != null && hit.collider != null
&& (hit.collider.transform == ignoreTransform || hit.collider.transform.IsChildOf(ignoreTransform)))
{
return GetGroundInfo(startPosition - new Vector3(0f, 0.01f, 0f), out raycastHit, range, mask, ignoreTransform);
}
raycastHit = hit;
return true;
}
}
public static class DataFileUtil
{
private const string FOLDER = "CustomCrateSpawns";
private const string LOOT_TABLES_FOLDER = "LootTables";
public static void EnsureFolderCreated()
{
string path = Path.Combine(Interface.Oxide.DataDirectory, FOLDER);
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
string lootTablesPath = Path.Combine(path, LOOT_TABLES_FOLDER);
if (!Directory.Exists(lootTablesPath))
Directory.CreateDirectory(lootTablesPath);
}
public static string GetFilePath(string filename = null)
{
if (filename == null)
filename = _plugin.Name;
return Path.Combine(FOLDER, filename);
}
public static string GetLootTableFilePath(string filename)
{
return Path.Combine(FOLDER, LOOT_TABLES_FOLDER, filename);
}
public static string[] GetAllFilePaths()
{
string[] filePaths = Interface.Oxide.DataFileSystem.GetFiles(FOLDER);
for (int i = 0; i < filePaths.Length; i++)
{
filePaths[i] = filePaths[i].Substring(0, filePaths[i].Length - 5);
}
return filePaths;
}
public static string[] GetAllLootTableFilePaths()
{
string lootTablesPath = Path.Combine(FOLDER, LOOT_TABLES_FOLDER);
if (!Interface.Oxide.DataFileSystem.ExistsDatafile(lootTablesPath))
return new string[0];
string[] filePaths = Interface.Oxide.DataFileSystem.GetFiles(lootTablesPath);
for (int i = 0; i < filePaths.Length; i++)
{
filePaths[i] = filePaths[i].Substring(0, filePaths[i].Length - 5);
}
return filePaths;
}
public static bool Exists(string filePath)
{
return Interface.Oxide.DataFileSystem.ExistsDatafile(filePath);
}
public static T Load<T>(string filePath) where T : class, new()
{
T data = Interface.Oxide.DataFileSystem.ReadObject<T>(filePath);
if (data == null)
data = new T();
return data;
}
public static T LoadIfExists<T>(string filePath) where T : class, new()
{
if (Exists(filePath))
return Load<T>(filePath);
else
return null;
}
public static T LoadOrCreate<T>(string filePath) where T : class, new()
{
T data = LoadIfExists<T>(filePath);
if (data == null)
data = new T();
return data;
}
public static void Save<T>(string filePath, T data)
{
Interface.Oxide.DataFileSystem.WriteObject<T>(filePath, data);
}
public static void Delete(string filePath)
{
Interface.Oxide.DataFileSystem.DeleteDataFile(filePath);
}
}
public static class DrawUtil
{
public static void Box(BasePlayer player, float durationSeconds, Color color, Vector3 position, float radius)
{
player.SendConsoleCommand("ddraw.box", durationSeconds, color, position, radius);
}
public static void Sphere(BasePlayer player, float durationSeconds, Color color, Vector3 position, float radius)
{
player.SendConsoleCommand("ddraw.sphere", durationSeconds, color, position, radius);
}
public static void Line(BasePlayer player, float durationSeconds, Color color, Vector3 fromPosition, Vector3 toPosition)
{
player.SendConsoleCommand("ddraw.line", durationSeconds, color, fromPosition, toPosition);
}
public static void Arrow(BasePlayer player, float durationSeconds, Color color, Vector3 fromPosition, Vector3 toPosition, float headSize)
{
player.SendConsoleCommand("ddraw.arrow", durationSeconds, color, fromPosition, toPosition, headSize);
}
public static void Text(BasePlayer player, float durationSeconds, Color color, Vector3 position, string text)
{
player.SendConsoleCommand("ddraw.text", durationSeconds, color, position, text);
}
}
#endregion Helper Classes
#region Permissions
private static class PermissionUtil
{
public const string ADMIN = "customcratespawns.admin";
private static readonly List<string> _permissions = new List<string>
{
ADMIN,
};
public static void RegisterPermissions()
{
foreach (var permission in _permissions)
{
_plugin.permission.RegisterPermission(permission, _plugin);
}
}
public static bool HasPermission(BasePlayer player, string permissionName)
{
return _plugin.permission.UserHasPermission(player.UserIDString, permissionName);
}
}
#endregion Permissions
#region Commands
[ConsoleCommand("ccs.spawngroup")]
private void cmdSpawnGroup(ConsoleSystem.Arg arg)
{
if (arg == null)
return;
BasePlayer player = arg.Player();
if (player == null)
return;
if (!PermissionUtil.HasPermission(player, PermissionUtil.ADMIN))
{
ReplyToPlayer(player, Lang.NoPermission);
return;
}
if (arg.Args == null || arg.Args.Length == 0)
{
ReplyToPlayer(player, Lang.SpawnGroupUsage);
return;
}
string[] args = arg.Args.Select(a => a.ToString()).ToArray();
string subCommand = args[0];
switch (subCommand)
{
case "create":
{
if (args.Length < 2)
{
ReplyToPlayer(player, Lang.SpawnGroupCreateUsage);
return;
}
string alias = args[1];
string filePath = DataFileUtil.GetFilePath(alias);
if (DataFileUtil.Exists(filePath))
{
ReplyToPlayer(player, Lang.SpawnGroupCreateAlreadyExists, alias);
return;
}
SpawnGroupData newGroup = new SpawnGroupData
{
UniqueName = alias,
InternalId = Guid.NewGuid(),
Enabled = true,
MaximumContainersAtOnce = 5,
MinimumContainersToSpawnPerCycle = 1,
MaximumContainersToSpawnPerCycle = 2,
SpawnImmediatelyOnServerStart = true,
MinimumRespawnDelayInSeconds = 30f,
MaximumRespawnDelayInSeconds = 60f,
RandomizeContainerRotation = true,
RespawnWhenLootedEmpty = true
};
DataFileUtil.Save(filePath, newGroup);
_spawnGroupBeingEdited = newGroup;
ReplyToPlayer(player, Lang.SpawnGroupCreated, alias);
break;
}
case "edit":
{
if (args.Length < 2)
{
ReplyToPlayer(player, Lang.SpawnGroupEditUsage);
return;
}
string alias = args[1];
string filePath = DataFileUtil.GetFilePath(alias);
SpawnGroupData group = DataFileUtil.LoadIfExists<SpawnGroupData>(filePath);
if (group == null)
{
ReplyToPlayer(player, Lang.SpawnGroupNotFound, alias);
return;
}
LootSpawnerComponent lootSpawner = GetSpawnerByName(alias);
if (lootSpawner != null)
lootSpawner.Destroy();
_spawnGroupBeingEdited = group;
ReplyToPlayer(player, Lang.SpawnGroupEdited, alias);
VisualizeSpawnGroup(player);
break;
}
case "remove":
{
if (_spawnGroupBeingEdited == null)
{
ReplyToPlayer(player, Lang.SpawnGroupNoGroupBeingEdited);
return;
}
string alias = _spawnGroupBeingEdited.UniqueName;
string filePath = DataFileUtil.GetFilePath(alias);
DataFileUtil.Delete(filePath);
_spawnGroupBeingEdited = null;
ReplyToPlayer(player, Lang.SpawnGroupRemoved, alias);
break;
}
case "set":
{
if (_spawnGroupBeingEdited == null)
{
ReplyToPlayer(player, Lang.SpawnGroupSetNoGroupBeingEdited);
return;
}
if (args.Length < 3)
{
string editableProperties = string.Join("\n", new[]
{
"- active",
"- initialspawn",
"- maxpop",
"- minspawn",
"- maxspawn",
"- mindelay",
"- maxdelay",
"- randrot",
"- replacelooted",
"- loottable"
});
ReplyToPlayer(player, Lang.SpawnGroupSetUsage, editableProperties);
return;
}
string property = args[1].ToLower();
string value = args[2];
bool isValueValid = true;
switch (property)
{
case "active":
if (bool.TryParse(value, out bool active))
{
_spawnGroupBeingEdited.Enabled = active;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidActive);
isValueValid = false;
}
break;
case "initialspawn":
if (bool.TryParse(value, out bool initialSpawn))
{
_spawnGroupBeingEdited.SpawnImmediatelyOnServerStart = initialSpawn;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidInitialSpawn);
isValueValid = false;
}
break;
case "maxpop":
if (int.TryParse(value, out int maxPopulation) && maxPopulation >= 0)
{
_spawnGroupBeingEdited.MaximumContainersAtOnce = maxPopulation;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMaximumPop);
isValueValid = false;
}
break;
case "minspawn":
if (int.TryParse(value, out int minSpawn) && minSpawn >= 0)
{
_spawnGroupBeingEdited.MinimumContainersToSpawnPerCycle = minSpawn;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMinimumSpawn);
isValueValid = false;
}
break;
case "maxspawn":
if (int.TryParse(value, out int maxSpawn) && maxSpawn >= _spawnGroupBeingEdited.MinimumContainersToSpawnPerCycle)
{
_spawnGroupBeingEdited.MaximumContainersToSpawnPerCycle = maxSpawn;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMaximumSpawn);
isValueValid = false;
}
break;
case "mindelay":
if (float.TryParse(value, out float minDelay) && minDelay > 0)
{
_spawnGroupBeingEdited.MinimumRespawnDelayInSeconds = minDelay;
// ... [Code preview truncated due to length. Full source in attached file] ...