Иконка ресурса

FREE CustomCrateSpawns : 1.1.1 1.1.1

Нет прав для скачивания
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] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху