C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
/*
* 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.0")]
[Description("Lets you add additional spawn points for loot containers.")]
public class CustomCrateSpawns : RustPlugin
{
#region Fields
private static CustomCrateSpawns _plugin;
private System.Random _randomGenerator = new System.Random();
private SpawnGroupData _spawnGroupBeingEdited;
private List<LootSpawnerComponent> _lootSpawners = new List<LootSpawnerComponent>();
public const int LAYER_LOOT_CRATES = 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> _lootContainerPrefabs = 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"},
{"ammo", "assets/bundled/prefabs/radtown/dmloot/dm ammo.prefab"},
{"explosives", "assets/bundled/prefabs/radtown/dmloot/dm c4.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("Alias")]
public string Alias { get; set; }
[JsonProperty("Id")]
public Guid Id { get; set; }
[JsonProperty("Active")]
public bool Active { get; set; }
[JsonProperty("Maximum Population")]
public int MaximumPopulation { get; set; }
[JsonProperty("Minimum Number To Spawn Per Tick")]
public int MinimumNumberToSpawnPerTick { get; set; }
[JsonProperty("Maximum Number To Spawn Per Tick")]
public int MaximumNumberToSpawnPerTick { get; set; }
[JsonProperty("Initial Spawn")]
public bool InitialSpawn { get; set; }
[JsonProperty("Minimum Respawn Delay Seconds")]
public float MinimumRespawnDelaySeconds { get; set; }
[JsonProperty("Maximum Respawn Delay Seconds")]
public float MaximumRespawnDelaySeconds { get; set; }
[JsonProperty("Randomize Y Rotation")]
public bool RandomizeYRotation { get; set; }
[JsonProperty("Replace Looted Containers")]
public bool ReplaceLootedContainers { get; set; }
[JsonProperty("Prefabs")]
public List<PrefabData> Prefabs { get; set; } = new List<PrefabData>();
[JsonProperty("Spawn Points")]
public List<SpawnPointData> SpawnPoints { get; set; } = new List<SpawnPointData>();
}
public class PrefabData
{
[JsonProperty("Prefab")]
public string Prefab { get; set; }
[JsonProperty("Weight")]
public int Weight { get; set; }
}
public class SpawnPointData
{
[JsonProperty("Id")]
public string Id { get; set; }
[JsonProperty("Position")]
public Vector3 Position { get; set; }
[JsonProperty("Rotation")]
public Vector3 Rotation { get; set; }
[JsonProperty("Radius")]
public float Radius { get; set; }
}
#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)
{
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 = GetSpawnerForLootContainer(lootContainer);
if (lootSpawner != null)
{
lootSpawner.OnLootContainerLooted(lootContainer);
}
}
}
#endregion Oxide Hooks
#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.Active)
return;
float initialSpawnTime = GetNextSpawnTime();
if (Data.InitialSpawn)
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.MaximumPopulation)
{
int numberToSpawn = Random.Range(Data.MinimumNumberToSpawnPerTick, Data.MaximumNumberToSpawnPerTick + 1);
Spawn(numberToSpawn);
}
}
private float GetNextSpawnTime()
{
return Random.Range(Data.MinimumRespawnDelaySeconds, Data.MaximumRespawnDelaySeconds);
}
public void Spawn(int numberToSpawn)
{
numberToSpawn = Mathf.Min(numberToSpawn, Data.MaximumPopulation - CurrentPopulation);
for (int i = 0; i < numberToSpawn; i++)
{
string prefabPath = GetPrefab();
if (string.IsNullOrEmpty(prefabPath))
continue;
if (GetSpawnPoint(prefabPath, out Vector3 position, out Quaternion rotation) == null)
continue;
if (OnCustomCrateSpawn(Data.Alias, prefabPath, position, rotation))
continue;
LootContainer lootContainer = GameManager.server.CreateEntity(prefabPath, position, rotation) as LootContainer;
if (lootContainer == null)
continue;
// TODO: Consider parenting the spawned entity if it's placed on another entity.lootContainer.Spawn();
lootContainer.EnableSaving(false);
SpawnedEntities.Add(lootContainer);
}
}
public void Fill()
{
int numberToFill = Data.MaximumPopulation - 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 string GetPrefab()
{
int totalWeight = Data.Prefabs.Sum(prefab => prefab.Weight);
if (totalWeight <= 0)
return null;
float randomValue = Random.Range(0, totalWeight);
foreach (PrefabData prefab in Data.Prefabs)
{
randomValue -= prefab.Weight;
if (randomValue <= 0)
{
if (_lootContainerPrefabs.TryGetValue(prefab.Prefab, out string fullPrefabPath))
return fullPrefabPath;
else
return null;
}
}
PrefabData lastPrefab = Data.Prefabs.LastOrDefault();
if (lastPrefab != null && _lootContainerPrefabs.TryGetValue(lastPrefab.Prefab, out string lastPrefabPath))
return lastPrefabPath;
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.RandomizeYRotation)
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.ReplaceLootedContainers)
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.Radius > 0)
targetPosition = TerrainUtil.GetRandomPositionAround(Data.Position, 0f, Data.Radius);
else
targetPosition = Data.Position;
if (TerrainUtil.GetGroundInfo(targetPosition, out RaycastHit hit, Data.Radius, LAYER_GROUND | LAYER_BUILDINGS))
targetPosition = hit.point;
return targetPosition;
}
public Quaternion GetRotation()
{
Quaternion baseRotation = Quaternion.Euler(Data.Rotation);
if (TerrainUtil.GetGroundInfo(Data.Position, 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_LOOT_CRATES,
QueryTriggerInteraction.Ignore
);
if (colliders.Length > 0)
return false;
return true;
}
public bool HasPlayersIntersecting()
{
return PlayerUtil.HasPlayerNearby(Data.Position, Mathf.Max(Data.Radius, 2f));
}
#endregion Collision and Space Validation
}
#endregion Spawn Point Component
#region API
[HookMethod(nameof(API_SpawnCratesForGroup))]
public void API_SpawnCratesForGroup(string spawnGroupAlias)
{
LootSpawnerComponent lootSpawner = GetSpawnerByAlias(spawnGroupAlias);
if (lootSpawner != null)
{
lootSpawner.Fill();
}
}
[HookMethod(nameof(API_ClearCratesForGroup))]
public void API_ClearCratesForGroup(string spawnGroupAlias)
{
LootSpawnerComponent lootSpawner = GetSpawnerByAlias(spawnGroupAlias);
if (lootSpawner != null)
{
lootSpawner.Clear();
}
}
[HookMethod(nameof(API_IsCrateFromCustomSpawner))]
public bool API_IsCrateFromCustomSpawner(LootContainer lootContainer)
{
return GetSpawnerForLootContainer(lootContainer) != null;
}
[HookMethod(nameof(API_SpawnGroupExists))]
public bool API_SpawnGroupExists(string spawnGroupAlias)
{
if (string.IsNullOrEmpty(spawnGroupAlias))
return false;
string filePath = DataFileUtil.GetFilePath(spawnGroupAlias);
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.Alias;
if (string.IsNullOrEmpty(alias))
continue;
if (seen.Add(alias))
result.Add(alias);
}
return result;
}
#endregion API
#region Exposed Hooks
private static bool OnCustomCrateSpawn(string spawnGroupAlias, string prefab, Vector3 position, Quaternion rotation)
{
object hookResult = Interface.CallHook("OnCustomCrateSpawn", spawnGroupAlias, prefab, position, rotation);
return hookResult is bool && (bool)hookResult == false;
}
#endregion Exposed Hooks
#region Helper Functions
private LootSpawnerComponent GetSpawnerByAlias(string spawnGroupAlias)
{
if (string.IsNullOrEmpty(spawnGroupAlias))
return null;
foreach (LootSpawnerComponent lootSpawner in _lootSpawners)
{
if (lootSpawner.Data.Alias.Equals(spawnGroupAlias, StringComparison.OrdinalIgnoreCase))
return lootSpawner;
}
return null;
}
private LootSpawnerComponent GetSpawnerForLootContainer(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._randomGenerator.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 bool OnTopology(Vector3 position, TerrainTopology.Enum topology)
{
return (TerrainMeta.TopologyMap.GetTopology(position) & (int)topology) != 0;
}
public static bool OnRoadOrRail(Vector3 position)
{
var combinedTopology = TerrainTopology.Enum.Road | TerrainTopology.Enum.Roadside |
TerrainTopology.Enum.Rail | TerrainTopology.Enum.Railside;
return OnTopology(position, combinedTopology);
}
public static bool OnTerrain(Vector3 position, float radius)
{
return Physics.CheckSphere(position, radius, Layers.Mask.Terrain, QueryTriggerInteraction.Ignore);
}
public static bool InNoBuildZone(Vector3 position, float radius)
{
return Physics.CheckSphere(position, radius, Layers.Mask.Prevent_Building);
}
public static bool InWater(Vector3 position)
{
return WaterLevel.Test(position, false, false);
}
public static bool InsideRock(Vector3 position, float radius)
{
List<Collider> colliders = Pool.Get<List<Collider>>();
Vis.Colliders(position, radius, colliders, Layers.Mask.World, QueryTriggerInteraction.Ignore);
bool result = false;
foreach (Collider collider in colliders)
{
if (collider.name.Contains("rock", CompareOptions.OrdinalIgnoreCase)
|| collider.name.Contains("cliff", CompareOptions.OrdinalIgnoreCase)
|| collider.name.Contains("formation", CompareOptions.OrdinalIgnoreCase))
{
result = true;
break;
}
}
Pool.FreeUnmanaged(ref colliders);
return result;
}
public static bool InRadTown(Vector3 position, bool shouldDisplayOnMap = false)
{
foreach (var monumentInfo in TerrainMeta.Path.Monuments)
{
bool inBounds = monumentInfo.IsInBounds(position);
bool hasLandMarker = true;
if (shouldDisplayOnMap)
hasLandMarker = monumentInfo.shouldDisplayOnMap;
if (inBounds && hasLandMarker)
return true;
}
return OnTopology(position, TerrainTopology.Enum.Monument);
}
public static bool HasEntityNearby(Vector3 position, float radius, LayerMask mask, string prefabName = null)
{
List<Collider> hitColliders = Pool.Get<List<Collider>>();
GamePhysics.OverlapSphere(position, radius, hitColliders, mask, QueryTriggerInteraction.Ignore);
bool hasEntityNearby = false;
foreach (Collider collider in hitColliders)
{
BaseEntity entity = collider.gameObject.ToBaseEntity();
if (entity != null)
{
if (prefabName == null || entity.PrefabName == prefabName)
{
hasEntityNearby = true;
break;
}
}
}
Pool.FreeUnmanaged(ref hitColliders);
return hasEntityNearby;
}
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 Vector3 GetPositionOnCircle(Vector3 centerPosition, float radius, float angle)
{
float radians = angle * Mathf.Deg2Rad;
return new Vector3(centerPosition.x + Mathf.Cos(radians) * radius, centerPosition.y, centerPosition.z + Mathf.Sin(radians) * radius);
}
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";
public static void EnsureFolderCreated()
{
string path = Path.Combine(Interface.Oxide.DataDirectory, FOLDER);
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
public static string GetFilePath(string filename = null)
{
if (filename == null)
filename = _plugin.Name;
return Path.Combine(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 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);
}
}
public static class PlayerUtil
{
public static BasePlayer FindById(ulong playerId)
{
return RelationshipManager.FindByID(playerId);
}
public static bool HasPlayerNearby(Vector3 position, float radius)
{
return BaseNetworkable.HasCloseConnections(position, radius);
}
}
#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
/// <summary>
/// ccs.spawngroup <spawnGroupAlias>
/// ccs.spawngroup edit <spawnGroupAlias>
/// ccs.spawngroup remove
/// ccs.spawngroup set <property> <value>
/// - active
/// - initialspawn
/// - maxpop
/// - minspawn
/// - maxspawn
/// - mindelay
/// - maxdelay
/// - randrot
/// - replacelooted
/// </summary>
[ConsoleCommand("ccs.spawngroup")]
private void cmdConsoleSpawnGroup(ConsoleSystem.Arg conArgs)
{
if (conArgs == null)
return;
BasePlayer player = conArgs.Player();
if (player == null)
return;
if (!PermissionUtil.HasPermission(player, PermissionUtil.ADMIN))
{
ReplyToPlayer(player, Lang.NoPermission);
return;
}
string[] args = conArgs.Args;
if (args == null || args.Length == 0)
{
ReplyToPlayer(player, Lang.SpawnGroupUsage);
return;
}
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
{
Alias = alias,
Id = Guid.NewGuid(),
Active = true,
MaximumPopulation = 5,
MinimumNumberToSpawnPerTick = 1,
MaximumNumberToSpawnPerTick = 2,
InitialSpawn = true,
MinimumRespawnDelaySeconds = 30f,
MaximumRespawnDelaySeconds = 60f,
RandomizeYRotation = true,
ReplaceLootedContainers = 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 = GetSpawnerByAlias(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.Alias;
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"
});
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.Active = active;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidActive);
isValueValid = false;
}
break;
case "initialspawn":
if (bool.TryParse(value, out bool initialSpawn))
{
_spawnGroupBeingEdited.InitialSpawn = initialSpawn;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidInitialSpawn);
isValueValid = false;
}
break;
case "maxpop":
if (int.TryParse(value, out int maxPopulation) && maxPopulation >= 0)
{
_spawnGroupBeingEdited.MaximumPopulation = maxPopulation;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMaximumPop);
isValueValid = false;
}
break;
case "minspawn":
if (int.TryParse(value, out int minSpawn) && minSpawn >= 0)
{
_spawnGroupBeingEdited.MinimumNumberToSpawnPerTick = minSpawn;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMinimumSpawn);
isValueValid = false;
}
break;
case "maxspawn":
if (int.TryParse(value, out int maxSpawn) && maxSpawn >= _spawnGroupBeingEdited.MinimumNumberToSpawnPerTick)
{
_spawnGroupBeingEdited.MaximumNumberToSpawnPerTick = maxSpawn;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMaximumSpawn);
isValueValid = false;
}
break;
case "mindelay":
if (float.TryParse(value, out float minDelay) && minDelay > 0)
{
_spawnGroupBeingEdited.MinimumRespawnDelaySeconds = minDelay;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMinimumDelay);
isValueValid = false;
}
break;
case "maxdelay":
if (float.TryParse(value, out float maxDelay) && maxDelay >= _spawnGroupBeingEdited.MinimumRespawnDelaySeconds)
{
_spawnGroupBeingEdited.MaximumRespawnDelaySeconds = maxDelay;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidMaximumDelay);
isValueValid = false;
}
break;
case "randrot":
if (bool.TryParse(value, out bool randomizeYRotation))
{
_spawnGroupBeingEdited.RandomizeYRotation = randomizeYRotation;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidRandomYRotation);
isValueValid = false;
}
break;
case "replacelooted":
if (bool.TryParse(value, out bool replaceLootContainers))
{
_spawnGroupBeingEdited.ReplaceLootedContainers = replaceLootContainers;
}
else
{
ReplyToPlayer(player, Lang.SpawnGroupSetInvalidReplaceLootedContainers);
isValueValid = false;
}
break;
default:
string editableProperties = string.Join("\n", new[]
{
"- active",
"- initialspawn",
"- maxpop",
"- minspawn",
"- maxspawn",
"- mindelay",
"- maxdelay",
"- randrot",
"- replacelooted"
});
ReplyToPlayer(player, Lang.SpawnGroupSetUnknownProperty, property, editableProperties);
return;
}
if (isValueValid)
{
DataFileUtil.Save(DataFileUtil.GetFilePath(_spawnGroupBeingEdited.Alias), _spawnGroupBeingEdited);
ReplyToPlayer(player, Lang.SpawnGroupSetPropertyUpdated, property, value, _spawnGroupBeingEdited.Alias);
VisualizeSpawnGroup(player);
}
break;
}
case "done":
{
if (_spawnGroupBeingEdited == null)
{
ReplyToPlayer(player, Lang.SpawnGroupDoneNoGroupBeingEdited);
return;
}
string alias = _spawnGroupBeingEdited.Alias;
string filePath = DataFileUtil.GetFilePath(alias);
DataFileUtil.Save(filePath, _spawnGroupBeingEdited);
LootSpawnerComponent.Create(_spawnGroupBeingEdited);
_spawnGroupBeingEdited = null;
ReplyToPlayer(player, Lang.SpawnGroupDoneFinishedEditing, alias);
break;
}
default:
ReplyToPlayer(player, Lang.SpawnGroupUnknownCommand, subCommand);
break;
}
}
/// <summary>
/// ccs.spawnpoint add <position> [radius]
/// ccs.spawnpoint remove [spawnPointId]
/// </summary>
[ConsoleCommand("ccs.spawnpoint
// ... [Code preview truncated due to length. Full source in attached file] ...