diff --git a/API/BaseClusters/BaseCluster.cs b/API/BaseClusters/BaseCluster.cs deleted file mode 100644 index 9bdc6db..0000000 --- a/API/BaseClusters/BaseCluster.cs +++ /dev/null @@ -1,361 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Delegates; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Pustalorc.Plugins.BaseClustering.Config; -using SDG.Unturned; -using UnityEngine; -using Random = UnityEngine.Random; - -namespace Pustalorc.Plugins.BaseClustering.API.BaseClusters; - -/// -/// This class defines the structure of a BaseCluster. -/// -public sealed class BaseCluster -{ - private readonly BaseClusteringPluginConfiguration m_PluginConfiguration; - private readonly BaseClusterDirectory m_BaseClusterDirectory; - private readonly List m_Buildables; - - /// - /// This event is raised if is called. - /// - [UsedImplicitly] - public event VoidDelegate? OnClusterReset; - - /// - /// This event is raised if new buildables are added to the cluster. - /// - [UsedImplicitly] - public event BuildablesChanged? OnBuildablesAdded; - - /// - /// This event is raised if buildables are removed from the cluster. - /// - [UsedImplicitly] - public event BuildablesChanged? OnBuildablesRemoved; - - /// - /// Gets the common owner of the entire cluster based on who owns the most things. - /// - public ulong CommonOwner => m_Buildables.GroupBy(k => k.Owner) - .OrderByDescending(k => k.Count()) - .Select(g => g.Key).ToList().FirstOrDefault(); - - /// - /// Gets the common group of the entire cluster based on which group owns the most things. - /// - [UsedImplicitly] - public ulong CommonGroup => m_Buildables.GroupBy(k => k.Group) - .OrderByDescending(k => k.Count()) - .Select(g => g.Key).ToList().FirstOrDefault(); - - /// - /// Gets the average center position of the cluster. - /// - public Vector3 AverageCenterPosition => - m_Buildables.OfType().AverageCenter(k => k.Position); - - /// - /// Gets the unique instanceId of this cluster. - /// - public int InstanceId { get; } - - /// - /// Defines if this cluster is a global cluster. - /// - public bool IsGlobalCluster { get; } - - /// - /// Defines if this cluster is being destroyed, and therefore integrity check operations shouldn't be handled. - /// - public bool IsBeingDestroyed { get; set; } - - /// - /// Gets a copied of all the buildables in the cluster. - /// - public IReadOnlyCollection Buildables => new ReadOnlyCollection(m_Buildables.ToList()); - - internal BaseCluster(BaseClusteringPluginConfiguration pluginConfiguration, - BaseClusterDirectory baseClusterDirectory, int instanceId, bool isGlobalCluster = false) - { - m_PluginConfiguration = pluginConfiguration; - m_BaseClusterDirectory = baseClusterDirectory; - m_Buildables = new List(); - InstanceId = instanceId; - IsGlobalCluster = isGlobalCluster; - } - - /// - /// Destroys this base, including all the buildables in it. - /// - /// - /// If , s will drop all their contents on the ground. - ///
- /// If , s will not drop any of their contents on the ground. - /// - public void Destroy(bool shouldDropItems = true) - { - IsBeingDestroyed = true; - - foreach (var buildable in Buildables.ToList()) - { - if (buildable.Interactable is InteractableStorage store) - store.despawnWhenDestroyed = !shouldDropItems; - - buildable.SafeDestroy(); - } - - m_BaseClusterDirectory.Return(this); - - if (IsGlobalCluster) - IsBeingDestroyed = false; - } - - /// - /// Checks if another buildable is within range of this base cluster. - /// - /// The buildable to check. - /// - /// if the buildable is within range. - ///
- /// if the buildable is outside range. - ///
- public bool IsWithinRange(Buildable buildable) - { - var structures = Buildables.OfType(); - var distanceCheck = buildable is StructureBuildable - ? Mathf.Pow(m_PluginConfiguration.MaxDistanceBetweenStructures, 2) - : Mathf.Pow(m_PluginConfiguration.MaxDistanceToConsiderPartOfBase, 2); - - return structures.Any(k => (k.Position - buildable.Position).sqrMagnitude <= distanceCheck); - } - - /// - /// Checks if a position is within range of this base cluster. - /// - /// The position to check. - /// - /// If the position is within range. - ///
- /// If the position is outside range. - ///
- /// - /// Unlike , this method only checks with not with . - /// - public bool IsWithinRange(Vector3 position) - { - var distanceCheck = Mathf.Pow(m_PluginConfiguration.MaxDistanceToConsiderPartOfBase, 2); - - return Buildables.OfType() - .Any(k => (k.Position - position).sqrMagnitude <= distanceCheck); - } - - /// - /// Resets a base to the default state (empty). - ///
- /// This method only clears the buildables that are in it and raises . - ///
- public void Reset() - { - m_Buildables.Clear(); - OnClusterReset?.Invoke(); - } - - /// - /// Adds a buildable to the base. This method does not spawn in a buildable. - /// - /// The buildable to add to the base. - public void AddBuildable(Buildable build) - { - var isStruct = build is StructureBuildable; - if (IsGlobalCluster && isStruct) - throw new NotSupportedException("StructureBuildables are not supported by global clusters."); - - m_Buildables.Add(build); - var gCluster = m_BaseClusterDirectory.GetOrCreateGlobalCluster(); - var buildsInRange = gCluster.Buildables.Where(IsWithinRange).ToList(); - AddBuildables(buildsInRange); - gCluster.RemoveBuildables(buildsInRange); - // Include the buildables from the global cluster that got added. - OnBuildablesAdded?.Invoke(buildsInRange.Concat(new[] { build })); - } - - /// - /// Adds multiple buildables to the base. This method does not spawn in any of the buildables. - /// - /// The of s to add to the base. - public void AddBuildables(IEnumerable builds) - { - var consolidate = builds.ToList(); - m_Buildables.AddRange(consolidate); - OnBuildablesAdded?.Invoke(consolidate); - } - - /// - /// Removes a buildable from the base. This method does not destroy the buildable. - /// - /// The buildable to remove from the base. - [UsedImplicitly] - public void RemoveBuildable(Buildable build) - { - var removedSomething = m_Buildables.Remove(build); - if (removedSomething) - OnBuildablesRemoved?.Invoke(new[] { build }); - - if (removedSomething && !IsBeingDestroyed && !IsGlobalCluster) - VerifyAndCorrectIntegrity(); - } - - /// - /// Removes multiple buildables from the base. This method does not destroy any of the buildables. - /// - /// The of s to remove from the base. - public void RemoveBuildables(List builds) - { - var removed = new List(); - - foreach (var build in m_Buildables.Where(builds.Remove).ToList()) - { - m_Buildables.Remove(build); - removed.Add(build); - } - - if (removed.Count > 0) - OnBuildablesRemoved?.Invoke(removed); - - if (removed.Count > 0 && !IsBeingDestroyed && !IsGlobalCluster) - VerifyAndCorrectIntegrity(); - } - - private bool VerifyStructureIntegrity() - { - var allStructures = Buildables.OfType().ToList(); - - if (allStructures.Count <= 0) - return false; - - var maxStructureDistance = Mathf.Pow(m_PluginConfiguration.MaxDistanceBetweenStructures, 2); - var succeeded = new List(); - - var random = allStructures[Random.Range(0, allStructures.Count)]; - succeeded.Add(random); - allStructures.Remove(random); - - for (var i = 0; i < succeeded.Count; i++) - { - var element = succeeded[i]; - - var result = allStructures - .Where(k => (element.Position - k.Position).sqrMagnitude <= maxStructureDistance) - .ToList(); - succeeded.AddRange(result); - allStructures.RemoveAll(result.Contains); - } - - return allStructures.Count == 0; - } - - private bool VerifyBarricadeIntegrity() - { - var structures = Buildables.OfType().ToList(); - - if (structures.Count <= 0) - return false; - - var maxBuildableDistance = - Mathf.Pow(m_PluginConfiguration.MaxDistanceToConsiderPartOfBase, 2); - - return Buildables.OfType().All(br => - structures.Exists(k => (br.Position - k.Position).sqrMagnitude <= maxBuildableDistance)); - } - - /// - /// This will verify the base integrity (that all the elements are still within range of configured limits) and if not, it will correct that. - /// - private void VerifyAndCorrectIntegrity() - { - var structureIntegrity = VerifyStructureIntegrity(); - var barricadeIntegrity = VerifyBarricadeIntegrity(); - - // If the base is still integrally sound, skip the rest of the code - if (structureIntegrity && barricadeIntegrity) return; - - var globalCluster = m_BaseClusterDirectory.GetOrCreateGlobalCluster(); - - IsBeingDestroyed = true; - // If the structure is still integral, check the barricades and fix any non-integral parts. - if (structureIntegrity) - { - // Get all the barricades that are too far from the cluster in a copied list. - foreach (var b in Buildables.OfType().Where(k => !IsWithinRange(k)).ToList()) - { - // Find the next best cluster that this element is within - var bestCluster = m_BaseClusterDirectory.FindBestCluster(b); - - // If something is found, check that its not the same cluster we are already in. - if (bestCluster != null) - { - if (bestCluster != this) - { - // If its a different cluster, remove it from the current cluster and add it to the new one. - RemoveBuildable(b); - bestCluster.AddBuildable(b); - } - - continue; - } - - // If no best cluster is found, check if we have a global cluster. If we do, add the barricade to it. If we don't, create a new global cluster. - RemoveBuildable(b); - globalCluster.AddBuildable(b); - } - - IsBeingDestroyed = false; - return; - } - - // First, get a list of all buildables to cluster, including global cluster. - var builds = Buildables.Concat(globalCluster.Buildables).ToList(); - globalCluster.Reset(); - var clusterRegened = m_BaseClusterDirectory.ClusterElements(builds) - .OrderByDescending(k => k.Buildables.Count).ToList(); - - // Dispose correctly of the cluster we are not going to add here. - var discarded = clusterRegened.FirstOrDefault(); - m_BaseClusterDirectory.Return(discarded); - - // Select all the clusters, except for the largest one. - foreach (var c in clusterRegened.Skip(1).ToList()) - { - // Remove any of the elements on the new cluster from the old one. - RemoveBuildables(c.Buildables.ToList()); - - // Add the new cluster to the directory. - m_BaseClusterDirectory.RegisterCluster(c); - } - - // Finally, if there's no structure buildables left in this cluster, call to remove it. - if (!Buildables.OfType().Any()) - m_BaseClusterDirectory.Return(this); - - IsBeingDestroyed = false; - } - - internal void StealFromGlobal(BaseCluster? globalCluster) - { - if (globalCluster?.IsGlobalCluster != true) - return; - - foreach (var build in globalCluster.Buildables.Where(IsWithinRange)) - { - AddBuildable(build); - globalCluster.RemoveBuildable(build); - } - } -} \ No newline at end of file diff --git a/API/BaseClusters/BaseClusterDirectory.cs b/API/BaseClusters/BaseClusterDirectory.cs deleted file mode 100644 index e97edc9..0000000 --- a/API/BaseClusters/BaseClusterDirectory.cs +++ /dev/null @@ -1,679 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Delegates; -using Pustalorc.Plugins.BaseClustering.API.Patches; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Pustalorc.Plugins.BaseClustering.Config; -using SDG.Unturned; -using Steamworks; -using UnityEngine; -using Random = UnityEngine.Random; - -namespace Pustalorc.Plugins.BaseClustering.API.BaseClusters; - -/// -/// A directory that keeps track of all s. -/// -public sealed class BaseClusterDirectory -{ - /// - /// This event is raised when has finished executing. - /// - [UsedImplicitly] - public event VoidDelegate? OnClustersGenerated; - - /// - /// This event is raised whenever a new cluster is added. - /// - [UsedImplicitly] - public event ClusterChange? OnClusterAdded; - - /// - /// This event is raised whenever a cluster is removed. - /// - [UsedImplicitly] - public event ClusterChange? OnClusterRemoved; - - private readonly BaseClusteringPlugin m_Plugin; - private readonly BaseClusteringPluginConfiguration m_PluginConfiguration; - private readonly BuildableDirectory m_BuildableDirectory; - private readonly ConcurrentBag m_ClusterPool; - private readonly List m_Clusters; - private string m_SaveFilePath; - - private BaseCluster? m_GlobalCluster; - private int m_InstanceIds; - - /// - /// Gets a copied of all the clusters tracked. - /// - /// - /// This copied collection includes the global cluster from . - /// - public IReadOnlyCollection Clusters => - new ReadOnlyCollection(m_Clusters.Concat(new[] { GetOrCreateGlobalCluster() }).ToList()); - - /// - /// Creates a new instance of the BaseCluster Directory. - /// - /// The instance of the plugin. - /// The configuration of the plugin. - /// The buildable directory, which should've been initialized beforehand. - public BaseClusterDirectory(BaseClusteringPlugin plugin, BaseClusteringPluginConfiguration pluginConfiguration, - BuildableDirectory buildableDirectory) - { - m_Plugin = plugin; - m_PluginConfiguration = pluginConfiguration; - m_BuildableDirectory = buildableDirectory; - m_ClusterPool = new ConcurrentBag(); - m_Clusters = new List(); - m_SaveFilePath = ServerSavedata.directory + "/" + Provider.serverID + "/Level/" + - (Level.info?.name ?? "Washington") + "/Bases.dat"; - - PatchBuildableTransforms.OnBuildableTransformed += BuildableTransformed; - buildableDirectory.OnBuildablesAdded += BuildablesSpawned; - buildableDirectory.OnBuildablesRemoved += BuildablesDestroyed; - SaveManager.onPostSave += Save; - } - - internal void LevelLoaded() - { - m_SaveFilePath = ServerSavedata.directory + "/" + Provider.serverID + "/Level/" + - Level.info.name + "/Bases.dat"; - GenerateAndLoadAllClusters(); - - while (m_ClusterPool.Count < 25) - m_ClusterPool.Add(new BaseCluster(m_PluginConfiguration, this, m_InstanceIds++)); - } - - internal void Unload() - { - PatchBuildableTransforms.OnBuildableTransformed -= BuildableTransformed; - m_BuildableDirectory.OnBuildablesAdded -= BuildablesSpawned; - m_BuildableDirectory.OnBuildablesRemoved -= BuildablesDestroyed; - SaveManager.onPostSave -= Save; - Save(); - } - - internal void GenerateAndLoadAllClusters(bool loadSaveFile = true) - { - var stopwatch = Stopwatch.StartNew(); - - var allBuildables = BuildableDirectory.GetBuildables(includePlants: true).ToList(); - Logging.Write(m_Plugin, - $"Loaded {allBuildables.Count} buildables from the map. Took {stopwatch.ElapsedMilliseconds}ms", - ConsoleColor.Cyan); - - foreach (var c in m_Clusters) - Return(c); - - var successfulLoad = false; - if (loadSaveFile && LevelSavedata.fileExists("/Bases.dat")) - successfulLoad = LoadClusters(allBuildables); - - if (!successfulLoad) - { - Logging.Write(m_Plugin, - "Generating new clusters. This can take a LONG time. How long will depend on the following factors (but not limited to): CPU usage, CPU cores/threads, Buildables in the map. This generation only needs to be ran once from raw."); - m_Clusters.AddRange(ClusterElements(allBuildables, true)); - } - - stopwatch.Stop(); - Logging.Write(m_Plugin, - $"Clusters Loaded: {Clusters.Count}. Took {stopwatch.ElapsedMilliseconds}ms.", - ConsoleColor.Cyan); - - OnClustersGenerated?.Invoke(); - } - - /// - /// Saves the current data in . - /// - public void Save() - { - m_BuildableDirectory.WaitDestroyHandle(); - var river = new RiverExpanded(m_SaveFilePath); - river.WriteInt32(m_BuildableDirectory.Buildables.Count); - var clusters = Clusters; - river.WriteInt32(clusters.Count); - foreach (var cluster in clusters) - { - river.WriteInt32(cluster.InstanceId); - river.WriteBoolean(cluster.IsGlobalCluster); - river.WriteInt32(cluster.Buildables.Count); - foreach (var build in cluster.Buildables) - { - river.WriteUInt32(build.InstanceId); - river.WriteBoolean(build is StructureBuildable); - } - } - - river.CloseRiver(); - m_BuildableDirectory.RestartBackgroundWorker(); - } - - private bool LoadClusters(IEnumerable allBuildables) - { - var bases = new List(); - - foreach (var c in m_Clusters) - Return(c); - - try - { - var timer = Stopwatch.StartNew(); - var river = new RiverExpanded(m_SaveFilePath); - var allBuilds = allBuildables.ToList(); - var structures = allBuilds.OfType().ToDictionary(k => k.InstanceId); - var barricades = allBuilds.OfType().ToDictionary(k => k.InstanceId); - - var buildableCount = river.ReadInt32(); - - if (allBuilds.Count != buildableCount) - { - Logging.Write(m_Plugin, - "Warning! Buildable count doesn't match saved count! Buildable save data was most likely modified or lost during server downtime. Clusters will be now rebuilt.", - ConsoleColor.Yellow); - return false; - } - - var clusterCount = river.ReadInt32(); - var logRate = Math.Floor(clusterCount * 0.085); - - Logging.Write(m_Plugin, - $"Loading saved clusters... 0% [0/{clusterCount}] {timer.ElapsedMilliseconds}ms", - ConsoleColor.Cyan); - - for (var i = 0; i < clusterCount; i++) - { - var builds = new List(); - // Restore of instanceId is needed to maintain something unique to each cluster across restarts. - var instanceId = river.ReadInt32(); - var global = river.ReadBoolean(); - - var buildCount = river.ReadInt32(); - for (var o = 0; o < buildCount; o++) - { - var buildInstanceId = river.ReadUInt32(); - var isStructure = river.ReadBoolean(); - var build = isStructure - ? (Buildable)structures[buildInstanceId] - : barricades[buildInstanceId]; - - if (build == null) - { - Logging.Write(m_Plugin, - $"Warning! Buildable with InstanceId {buildInstanceId} [isStructure: {isStructure}] not found! Save data was most likely modified or lost during server downtime. Clusters will be now rebuilt.", - ConsoleColor.Yellow); - river.CloseRiver(); - return false; - } - - builds.Add(build); - } - - if (global) - { - if (m_GlobalCluster != null) - { - m_GlobalCluster.AddBuildables(builds); - } - else - { - var cluster = CreateCluster(instanceId, true); - cluster.AddBuildables(builds); - m_GlobalCluster = cluster; - } - } - else - { - var cluster = GetOrCreatePooledCluster(); - cluster.AddBuildables(builds); - bases.Add(cluster); - } - - if ((i + 1) % logRate == 0) - Logging.Write(m_Plugin, - $"Loading saved clusters... {Math.Ceiling((i + 1) / (double)clusterCount * 100)}% [{i + 1}/{clusterCount}] {timer.ElapsedMilliseconds}ms", - ConsoleColor.Cyan); - } - - m_Clusters.AddRange(bases); - - if (Clusters.Count > 0) - m_InstanceIds = Clusters.Max(k => k.InstanceId) + 1; - - for (var i = 0; i < m_InstanceIds; i++) - { - if (Clusters.Any(k => k.InstanceId == i) || m_ClusterPool.Any(k => k.InstanceId == i)) - continue; - - m_ClusterPool.Add(CreateCluster(i)); - } - - timer.Stop(); - return true; - } - catch (Exception ex) - { - Logging.Write(m_Plugin, - $"Warning! An exception was thrown when attempting to load the save file. Assuming the data is corrupted. Clusters will be now rebuilt. Exception: {ex}", - ConsoleColor.Yellow); - - foreach (var b in bases) - Return(b); - - return false; - } - } - - /// - /// Gets a from the pool. - ///
- /// If a isn't available from the pool, a new instance will be created and provided. - ///
- /// An instance of type . - public BaseCluster GetOrCreatePooledCluster() - { - return m_ClusterPool.TryTake(out var baseCluster) - ? baseCluster - : CreateCluster(m_InstanceIds++); - } - - private BaseCluster CreateCluster(int instanceId, bool globalCluster = false) - { - return new BaseCluster(m_PluginConfiguration, this, instanceId, globalCluster); - } - - /// - /// Returns and resets a to the pool. - /// - /// The to reset and return to the pool. - public void Return(BaseCluster? baseCluster) - { - if (baseCluster == null) - return; - - baseCluster.Reset(); - m_Clusters.Remove(baseCluster); - - if (baseCluster.IsGlobalCluster) - return; - - m_ClusterPool.Add(baseCluster); - OnClusterRemoved?.Invoke(baseCluster); - } - - /// - /// Gets the global . - ///
- /// If there's no global available, a new instance will be created and provided. - ///
- /// An instance of . - public BaseCluster GetOrCreateGlobalCluster() - { - return m_GlobalCluster ??= CreateCluster(m_InstanceIds++, true); - } - - /// - /// Generates a new with all the clusters generated from the inputs. - /// - /// The to cluster. - /// Should progress be logged to console whilst it clusters. - /// An with all the generated clusters - public IEnumerable ClusterElements(IEnumerable buildables, bool needLogging = false) - { - // Start a new stopwatch. This will be used to log how long the program is taking with each step. - var stopwatch = Stopwatch.StartNew(); - // Initialize an empty sample output for this method. - var output = new List(); - // Set constants of squared distance. This will be used on distance checks. - var maxStructureDistance = Mathf.Pow(m_PluginConfiguration.MaxDistanceBetweenStructures, 2); - var maxBarricadeDistance = Mathf.Pow(m_PluginConfiguration.MaxDistanceToConsiderPartOfBase, 2); - // Set a couple variables that are used for logging. - var currentMultiplier = 0; - var currentCount = 0; - - // Get all the buildables to cluster. Anything planted should NOT be clustered. - var buildablesToCluster = buildables.Where(k => !k.IsPlanted).ToList(); - // Get the count of buildables to cluster. This will be used for logging. - var totalBuildablesToCluster = buildablesToCluster.Count; - var logRate = Math.Floor(totalBuildablesToCluster * 0.085); - - // Get all the structures to cluster from all the buildables that are being clustered. - var structuresToCluster = buildablesToCluster.OfType().ToList(); - // Get all the barricades to cluster from all the buildables that are being clustered. - var barricadesToCluster = buildablesToCluster.OfType().ToList(); - - // A cluster is made by having at least one Structure. If we run out of structures to cluster, then the rest will be clustered in the global cluster. - while (structuresToCluster.Count > 0) - { - // Create a variable to store all the structures of the cluster. - var structuresOfCluster = new List(); - // Create a variable to store all the buildables of the cluster. - var buildablesOfCluster = new List(); - - // Pick a random structure (floor, pillar, wall, etc.) - var targetStructure = structuresToCluster[Random.Range(0, structuresToCluster.Count)]; - // Remove the picked structure from the toCluster list. - structuresToCluster.Remove(targetStructure); - // Add the picked structure to the final buildables of cluster list. - structuresOfCluster.Add(targetStructure); - - // Loop through buildablesOfCluster. Each element should only be checked against all others once. - for (var i = 0; i < structuresOfCluster.Count; i++) - { - // Get the element we are currently checking. - var s = structuresOfCluster[i]; - - // Check which of all the structures in the world we can add here. - var toAdd = structuresToCluster - .Where(k => (k.Position - s.Position).sqrMagnitude <= maxStructureDistance).ToList(); - // Add all those structures to the cluster. - structuresOfCluster.AddRange(toAdd); - // Remove all those structures from the main list. - structuresToCluster.RemoveAll(toAdd.Contains); - } - - // Barricades are simpler to cluster than structures. Barricades are only considered part of the cluster if there's a structure within range. - var barricadesToAdd = barricadesToCluster.Where(next => - structuresOfCluster.Exists(k => - (next.Position - k.Position).sqrMagnitude <= maxBarricadeDistance)) - .ToList(); - // Add all the barricades that are within range of one of the structures of this cluster. - buildablesOfCluster.AddRange(barricadesToAdd); - // Finally, remove all the barricades from the main list that we added to the cluster. - barricadesToCluster.RemoveAll(barricadesToAdd.Contains); - - // Combine all the buildables into one list. - buildablesOfCluster.AddRange(structuresOfCluster); - // Get or create a pooled cluster so we can define the cluster. - var cluster = GetOrCreatePooledCluster(); - // Add all the combined buildables to this cluster. - cluster.AddBuildables(buildablesOfCluster); - // Add this cluster to the output list. - output.Add(cluster); - - // Finally, check if we need logging, and if we are ready to log it. - currentCount += cluster.Buildables.Count; - if (!needLogging || !(currentCount / logRate > currentMultiplier)) continue; - - currentMultiplier++; - Logging.Write(m_Plugin, - $"Generating new clusters... {Math.Ceiling(currentCount / (double)totalBuildablesToCluster * 100)}% [{currentCount}/{totalBuildablesToCluster}] {stopwatch.ElapsedMilliseconds}ms", - ConsoleColor.Cyan); - } - - // Once all the structures have been clustered, check if we have any remaining barricades that have not been clustered. - var remainingBarricadeCount = barricadesToCluster.Count; - if (remainingBarricadeCount > 0) - { - // If we do have barricades that have not been clustered, get or create a global cluster. - var globalCluster = GetOrCreateGlobalCluster(); - // And add all those barricades to that global cluster. - globalCluster.AddBuildables(barricadesToCluster); - } - - // Finally, we should make sure we are logging the 100% message with this check, should logging actually be needed. - - // This invert is dumb, as we still need to return output. All we are doing is adding a visually earlier return, which makes 0 sense to do. - // ReSharper disable once InvertIf - if (needLogging) - { - var finalBuildCount = output.Sum(k => k.Buildables.Count) + remainingBarricadeCount; - Logging.Write(m_Plugin, - $"Generating new clusters... {Math.Ceiling(finalBuildCount / (double)totalBuildablesToCluster * 100)}% [{finalBuildCount}/{totalBuildablesToCluster}] {stopwatch.ElapsedMilliseconds}ms", - ConsoleColor.Cyan); - } - - return output; - } - - /// - /// Registers a new to . - /// - /// The to register. - public void RegisterCluster(BaseCluster cluster) - { - m_Clusters.Add(cluster); - OnClusterAdded?.Invoke(cluster); - } - - /// - /// Finds the best cluster for a specific buildable to be placed in. - /// - /// The buildable to find the best clusters for. - /// - /// if no best cluster is available. - ///
- /// An instance of if a best cluster is available. - ///
- public BaseCluster? FindBestCluster(Buildable target) - { - return FindBestClusters(target).FirstOrDefault(); - } - - /// - /// Finds the best clusters for a specific buildable to be placed in. - /// - /// The buildable to find the best clusters for. - /// - /// An with the best s for the buildable. - /// If no best clusters are found, will be empty. - /// - public IEnumerable FindBestClusters(Buildable target) - { - return Clusters.Where(k => k.IsWithinRange(target)) - .OrderBy(k => (k.AverageCenterPosition - target.Position).sqrMagnitude); - } - - /// - /// Finds the best cluster within range of a specific position - /// - /// The position to check within range. - /// - /// if no best cluster is available. - ///
- /// An instance of if a best cluster is available. - ///
- [UsedImplicitly] - public BaseCluster? FindBestCluster(Vector3 target) - { - return FindBestClusters(target).FirstOrDefault(); - } - - /// - /// Finds the best clusters within range of a specific position. - /// - /// The position to check within range. - /// - /// An with the best s for the buildable. - /// If no best clusters are found, will be empty. - /// - public IEnumerable FindBestClusters(Vector3 target) - { - return Clusters.Where(k => k.IsWithinRange(target)) - .OrderBy(k => (k.AverageCenterPosition - target).sqrMagnitude); - } - - private void BuildablesDestroyed(IEnumerable buildables) - { - var builds = buildables.ToList(); - - foreach (var cluster in Clusters.ToList()) - { - if (builds.Count == 0) - return; - - cluster.RemoveBuildables(builds); - } - } - - private void BuildableTransformed(Buildable buildable) - { - var builds = new[] { buildable }; - BuildablesDestroyed(builds); - BuildablesSpawned(builds); - } - - private void BuildablesSpawned(IEnumerable buildables) - { - var gCluster = GetOrCreateGlobalCluster(); - - foreach (var buildable in buildables) - { - if (buildable.IsPlanted) return; - - // On spawning, check if its a barricade - if (buildable is BarricadeBuildable) - { - // Find the best cluster for this barricade. - var bestCluster = FindBestCluster(buildable); - - // If we find a best cluster, add it on it. - if (bestCluster != null) - { - bestCluster.AddBuildable(buildable); - return; - } - - // If we don't, add it to the global cluster. - gCluster.AddBuildable(buildable); - return; - } - - // Otherwise, if its a structure, find all the clusters where it'd make a good target, and exclude any global clusters from the result. - var bestClusters = FindBestClusters(buildable).ToList(); - - switch (bestClusters.Count) - { - // If there's no results, create a new non-global cluster for this new base. - case 0: - var cluster = GetOrCreatePooledCluster(); - cluster.AddBuildable(buildable); - RegisterCluster(cluster); - cluster.StealFromGlobal(gCluster); - return; - // If there's exactly 1 cluster found, simply add it to that cluster. - case 1: - cluster = bestClusters.First(); - cluster.AddBuildable(buildable); - cluster.StealFromGlobal(gCluster); - return; - - // However, if there's more than 1 cluster, select every single buildable from all found clusters. - default: - var allBuilds = bestClusters.SelectMany(k => k.Buildables).ToList(); - - // Make sure to include the buildable we spawned in that set. - allBuilds.Add(buildable); - - // For all the found best clusters, we can now un-register them, as they are no longer needed. - foreach (var c in bestClusters) - Return(c); - - // And ask the clustering tool to generate new clusters, and populate the global cluster. - var newClusters = ClusterElements(allBuilds); - - // New clusters can be safely added now. - foreach (var c in newClusters) - { - RegisterCluster(c); - c.StealFromGlobal(c); - } - - return; - } - } - } - - /// - /// Retrieves all clusters that have the specified as the most common owner. - /// - /// The player to use for the search as the most common owner. - /// - /// An holding all the clusters that this player is deemed "most common owner" of. - /// - [UsedImplicitly] - public IEnumerable GetMostOwnedClusters(CSteamID player) - { - return GetClustersWithFilter(k => k.CommonOwner == player.m_SteamID); - } - - /// - /// Retrieves all clusters that satisfy the custom filter. - /// - /// An anonymous function that takes BaseCluster as parameter and returns bool. - /// - /// An that satisfies the filter. - /// - public IEnumerable GetClustersWithFilter(Func filter) - { - return Clusters.Where(filter); - } - - /// - /// Gets the cluster that contains the element with the provided model. - /// - /// The model of the buildable within a cluster. - /// - /// if no cluster is found. - ///
- /// An instance of if a cluster exists. - ///
- [UsedImplicitly] - public BaseCluster? GetClusterWithElement(Transform model) - { - return Clusters.FirstOrDefault(k => k.Buildables.Any(l => l.Model == model)); - } - - /// - /// Gets the cluster that contains the element with the provided position. - /// - /// The instanceId of the buildable within a cluster. - /// If the instanceId belongs to a structure or a barricade. - /// - /// if no cluster is found. - ///
- /// An instance of if a cluster exists. - ///
- [UsedImplicitly] - public BaseCluster? GetClusterWithElement(uint instanceId, bool isStructure) - { - return Clusters.FirstOrDefault(k => - { - var builds = k.Buildables.AsEnumerable(); - - if (builds == null) - return false; - - if (isStructure) - builds = builds.OfType(); - else - builds = builds.OfType(); - - return builds.Any(l => l.InstanceId == instanceId); - }); - } - - /// - /// Gets the cluster that contains the element with the provided buildable instance. - /// - /// The buildable within a cluster. - /// - /// if no cluster is found. - ///
- /// An instance of if a cluster exists. - ///
- [UsedImplicitly] - public BaseCluster? GetClusterWithElement(Buildable buildable) - { - return Clusters.FirstOrDefault(k => k.Buildables.Contains(buildable)); - } -} \ No newline at end of file diff --git a/API/Buildables/BarricadeBuildable.cs b/API/Buildables/BarricadeBuildable.cs deleted file mode 100644 index efed746..0000000 --- a/API/Buildables/BarricadeBuildable.cs +++ /dev/null @@ -1,96 +0,0 @@ -using SDG.Unturned; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.Buildables; - -/// -public sealed class BarricadeBuildable : Buildable -{ - /// - /// Server-Side barricade data. - /// - public BarricadeData BarricadeData { get; } - - /// - /// The drop/model of the barricade. - /// - public BarricadeDrop BarricadeDrop { get; } - - /// - /// Creates a new instance of with the specified drop. - /// - /// The drop to add. - public BarricadeBuildable(BarricadeDrop drop) - { - BarricadeDrop = drop; - BarricadeData = drop.GetServersideData(); - } - - /// - public override ushort AssetId => Asset.id; - - /// - public override ushort Health => BarricadeData.barricade.health; - - /// - public override byte[]? State => BarricadeData.barricade.state; - - /// - public override ulong Owner => BarricadeData.owner; - - /// - public override ulong Group => BarricadeData.group; - - /// - public override byte AngleX => BarricadeData.angle_x; - - /// - public override byte AngleY => BarricadeData.angle_y; - - /// - public override byte AngleZ => BarricadeData.angle_z; - - /// - public override Vector3 Position => BarricadeData.point; - - /// - public override Transform Model => BarricadeDrop.model; - - /// - public override Interactable? Interactable => BarricadeDrop.interactable; - - /// - public override uint InstanceId => BarricadeData.instanceID; - - /// - public override Asset Asset => BarricadeDrop.asset; - - /// - public override bool IsPlanted => BarricadeDrop.model != null && - BarricadeDrop.model.parent != null && - BarricadeDrop.model.parent.CompareTag("Vehicle"); - - /// - public override void UnsafeDestroy() - { - ThreadUtil.assertIsGameThread(); - if (!BarricadeManager.tryGetRegion(Model, out var x, out var y, out var plant, out _)) - return; - - BarricadeManager.destroyBarricade(BarricadeDrop, x, y, plant); - } - - /// - public override void UnsafeDamage(ushort damage) - { - ThreadUtil.assertIsGameThread(); - BarricadeManager.damage(Model, damage, 1, false, damageOrigin: EDamageOrigin.Unknown); - } - - /// - public override void UnsafeHeal(ushort amount) - { - ThreadUtil.assertIsGameThread(); - BarricadeManager.repair(Model, amount, 1); - } -} \ No newline at end of file diff --git a/API/Buildables/Buildable.cs b/API/Buildables/Buildable.cs deleted file mode 100644 index ccf3ff4..0000000 --- a/API/Buildables/Buildable.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Threading; -using JetBrains.Annotations; -using Rocket.Core.Utils; -using SDG.Unturned; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.Buildables; - -/// -/// An abstraction of both barricades and structures from unturned. -/// -/// -/// It specifically abstracts the following classes into one: -///
-/// -/// -///
-/// It also provides extra methods to interact with a buildable. -///
-public abstract class Buildable -{ - // Barricade.cs / Structure.cs - - /// - /// The Id of the Asset of this buildable. - /// - public abstract ushort AssetId { get; } - - /// - /// The amount of health this buildable has. - /// - [UsedImplicitly] - public abstract ushort Health { get; } - - /// - /// The state of the buildable. - /// - /// - /// This is because does not have any State information. - /// - [UsedImplicitly] - public abstract byte[]? State { get; } - - - // BarricadeData.cs / StructureData.cs - - /// - /// The owner of this buildable. - /// - public abstract ulong Owner { get; } - - /// - /// The group set to this buildable. - /// - public abstract ulong Group { get; } - - /// - /// The angle of rotation on the X axis of this buildable. - /// - [UsedImplicitly] - public abstract byte AngleX { get; } - - /// - /// The angle of rotation on the Y axis of this buildable. - /// - [UsedImplicitly] - public abstract byte AngleY { get; } - - /// - /// The angle of rotation on the Z axis of this buildable. - /// - [UsedImplicitly] - public abstract byte AngleZ { get; } - - /// - /// The position as a of this buildable. - /// - public abstract Vector3 Position { get; } - - - // BarricadeDrop.cs / StructureDrop.cs - - /// - /// The model () of this buildable. - /// - public abstract Transform Model { get; } - - /// - /// The instance of this buildable. - /// - /// - /// This is because does not have any Interactable information. - /// - public abstract Interactable? Interactable { get; } - - - // Multiple Files - - /// - /// The unique instance Id of this buildable, set by unturned. - /// - public abstract uint InstanceId { get; } - - /// - /// The of this buildable. - /// - public abstract Asset Asset { get; } - - - // Custom - - /// - /// This determines if the buildable is planted (on a vehicle). - /// - /// - /// This will always return if its a . - /// - public abstract bool IsPlanted { get; } - - /// - /// Destroys this buildable without checking if we are on the main thread. - /// - /// - /// If this method is ran on a separate thread and there's another read/write operation happening at the same time that we are destroying, the game will crash. - /// - [UsedImplicitly] - public abstract void UnsafeDestroy(); - - /// - /// Safely destroys this buildable no matter which thread it is called from. - /// - public void SafeDestroy() - { - if (!Thread.CurrentThread.IsGameThread()) - { - TaskDispatcher.QueueOnMainThread(UnsafeDestroy); - return; - } - - UnsafeDestroy(); - } - - /// - /// Damages this buildable without checking if we are on the main thread. - /// - /// The amount of damage to apply to the buildable. - /// - /// If this method is ran on a separate thread and there's another read/write operation happening at the same time that we are damaging (and possibly destroying), the game will crash. - ///
- /// is in raw damage, not a percentage of max health. - ///
- [UsedImplicitly] - public abstract void UnsafeDamage(ushort damage); - - /// - /// Safely damages this buildable no matter which thread it is called from. - /// - /// The amount of damage to apply to the buildable. - /// - /// is in raw damage, not a percentage of max health. - /// - [UsedImplicitly] - public void SafeDamage(ushort damage) - { - if (!Thread.CurrentThread.IsGameThread()) - { - TaskDispatcher.QueueOnMainThread(() => UnsafeDamage(damage)); - return; - } - - UnsafeDamage(damage); - } - - /// - /// Heals this buildable without checking if we are on the main thread. - /// - /// The amount of healing to apply to the buildable. - /// - /// If this method is ran on a separate thread and there's another read/write operation happening at the same time that we are healing, there's a high chance that the game will crash. - ///
- /// is in raw healing, not a percentage of max health. - ///
- [UsedImplicitly] - public abstract void UnsafeHeal(ushort amount); - - /// - /// Safely heals this buildable no matter which thread it is called from. - /// - /// The amount of healing to apply to the buildable. - /// - /// is in raw healing, not a percentage of max health. - /// - [UsedImplicitly] - public void SafeHeal(ushort amount) - { - if (!Thread.CurrentThread.IsGameThread()) - { - TaskDispatcher.QueueOnMainThread(() => UnsafeHeal(amount)); - return; - } - - UnsafeHeal(amount); - } - - /// - public override string ToString() - { - return $"Buildable [{AssetId}:{InstanceId}] located at {Position}"; - } -} \ No newline at end of file diff --git a/API/Buildables/BuildableDirectory.cs b/API/Buildables/BuildableDirectory.cs deleted file mode 100644 index 4ac9acf..0000000 --- a/API/Buildables/BuildableDirectory.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Delegates; -using Pustalorc.Plugins.BaseClustering.API.Patches; -using Pustalorc.Plugins.BaseClustering.Config; -using SDG.Unturned; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.Buildables; - -/// -/// A directory that keeps track of all s. -/// -public sealed class BuildableDirectory -{ - /// - /// An internal singleton accessor. To be used only by . - /// - private static BuildableDirectory? _instance; - - /// - /// This event is raised every time a buildable is added. - /// - public event BuildablesChanged? OnBuildablesAdded; - - /// - /// This event is raised every time buildables are removed (in bulk). - /// - public event BuildablesChanged? OnBuildablesRemoved; - - private readonly Dictionary m_BarricadeBuildables; - private readonly Dictionary m_StructureBuildables; - - private readonly ConcurrentQueue m_DeferredRemove; - private readonly ConcurrentQueue m_DeferredAdd; - - private readonly AutoResetEvent m_BackgroundWorkerEnd; - private readonly int m_BackgroundWorkerSleepTime; - - private BackgroundWorker m_BackgroundWorker; - - /// - /// Gets a copied of all the buildables tracked. - /// - public IReadOnlyCollection Buildables => - new ReadOnlyCollection(m_BarricadeBuildables.Values - .Concat(m_StructureBuildables.Values).ToList()); - - /// - /// Creates a new instance of the buildable directory. - /// - /// The plugin's configuration to utilize here. - public BuildableDirectory(BaseClusteringPluginConfiguration configuration) - { - m_BarricadeBuildables = new Dictionary(configuration.BuildableCapacity); - m_StructureBuildables = new Dictionary(configuration.BuildableCapacity); - m_DeferredRemove = new ConcurrentQueue(); - m_DeferredAdd = new ConcurrentQueue(); - m_BackgroundWorkerEnd = new AutoResetEvent(false); - m_BackgroundWorkerSleepTime = configuration.BackgroundWorkerSleepTime; - _instance = this; - - StructureManager.onStructureSpawned += StructureSpawned; - BarricadeManager.onBarricadeSpawned += BarricadeSpawned; - PatchBuildablesDestroy.OnBuildableDestroyed += BuildableDestroyed; - - m_BackgroundWorker = new BackgroundWorker { WorkerSupportsCancellation = true }; - RestartBackgroundWorker(); - } - - internal void LevelLoaded() - { - var builds = GetBuildables(useGeneratedBuilds: false); - - foreach (var element in builds) - switch (element) - { - case BarricadeBuildable b: - m_BarricadeBuildables.Add(element.InstanceId, b); - break; - case StructureBuildable s: - m_StructureBuildables.Add(element.InstanceId, s); - break; - } - } - - internal void Unload() - { - StructureManager.onStructureSpawned -= StructureSpawned; - BarricadeManager.onBarricadeSpawned -= BarricadeSpawned; - PatchBuildablesDestroy.OnBuildableDestroyed -= BuildableDestroyed; - } - - private void HandleDeferred(object sender, DoWorkEventArgs e) - { - while (!m_BackgroundWorker.CancellationPending) - { - InternalHandleDeferred(); - Thread.Sleep(m_BackgroundWorkerSleepTime); - } - - m_BackgroundWorkerEnd.Set(); - } - - private void InternalHandleDeferred() - { - var deferredAdd = new List(); - while (m_DeferredAdd.TryDequeue(out var element)) - deferredAdd.Add(element); - - var deferredRemove = new List(); - while (m_DeferredRemove.TryDequeue(out var element)) - deferredRemove.Add(element); - - OnBuildablesAdded?.Invoke(deferredAdd); - OnBuildablesRemoved?.Invoke(deferredRemove); - } - - internal void WaitDestroyHandle() - { - if (m_BackgroundWorker.IsBusy) - { - m_BackgroundWorker.CancelAsync(); - m_BackgroundWorkerEnd.WaitOne(); - } - - InternalHandleDeferred(); - } - - internal void RestartBackgroundWorker() - { - m_BackgroundWorker = new BackgroundWorker { WorkerSupportsCancellation = true }; - m_BackgroundWorker.DoWork += HandleDeferred; - m_BackgroundWorker.RunWorkerAsync(); - } - - private void BuildableDestroyed(uint instanceId, bool isStructure) - { - Buildable? build; - bool removed; - - switch (isStructure) - { - case true when m_StructureBuildables.TryGetValue(instanceId, out var s): - build = s; - removed = m_StructureBuildables.Remove(instanceId); - break; - case false when m_BarricadeBuildables.TryGetValue(instanceId, out var b): - build = b; - removed = m_BarricadeBuildables.Remove(instanceId); - break; - default: - return; - } - - if (!removed || build == null) - return; - - m_DeferredRemove.Enqueue(build); - } - - private void StructureSpawned(StructureRegion region, StructureDrop drop) - { - var build = new StructureBuildable(drop); - m_StructureBuildables.Add(build.InstanceId, build); - m_DeferredAdd.Enqueue(build); - } - - private void BarricadeSpawned(BarricadeRegion region, BarricadeDrop drop) - { - var build = new BarricadeBuildable(drop); - m_BarricadeBuildables.Add(build.InstanceId, build); - m_DeferredAdd.Enqueue(build); - } - - /// - /// Gets all of the s from the map or from the already generated cache. - /// - /// The owner with which to filter the result. - /// The group with which to filter the result. - /// If planted (on vehicle) barricades should be included. - /// If the should be used instead of generating a new from the map. - /// An - /// - /// If or are equal to 0, then there will be no filtering done respective to whichever is 0. - /// - public static IEnumerable GetBuildables(ulong owner = 0, ulong group = 0, bool includePlants = false, - bool useGeneratedBuilds = true) - { - IEnumerable result; - - if (useGeneratedBuilds && _instance != null) - { - result = _instance.Buildables; - if (!includePlants) - result = result.Where(k => !k.IsPlanted); - } - else - { - var barricadeRegions = BarricadeManager.regions.Cast().ToList(); - - if (includePlants) - barricadeRegions.AddRange(BarricadeManager.vehicleRegions); - - var structureRegions = StructureManager.regions.Cast().ToList(); - - var barricadeDrops = barricadeRegions.SelectMany(brd => brd.drops).ToList(); - var structureDrops = structureRegions.SelectMany(str => str.drops).ToList(); - - result = barricadeDrops.Select(k => new BarricadeBuildable(k)) - .Concat(structureDrops.Select(k => new StructureBuildable(k))); - } - - return (owner switch - { - 0 when group == 0 => result, - 0 => result.Where(k => k.Group == group), - _ => group == 0 - ? result.Where(k => k.Owner == owner) - : result.Where(k => k.Owner == owner || k.Group == group) - }).ToList(); - } - - /// - /// Gets a specific buildable based on a . - /// - /// The of the buildable to find. - /// - /// if the buildable was not found. - ///
- /// An instance of if the buildable was found. - ///
- public static Buildable? GetBuildable(Transform buildable) - { - return GetBuildables(includePlants: true).FirstOrDefault(k => k.Model == buildable); - } - - /// - /// Gets a specific buildable based on their instanceId and if they are a structure or not. - /// - /// The instance id of the buildable to find. - /// If the buildable we are trying to find is a structure or a barricade. - /// - /// if the buildable was not found. - ///
- /// An instance of if the buildable was found. - ///
- [UsedImplicitly] - public static Buildable? GetBuildable(uint instanceId, bool isStructure) - { - var buildables = GetBuildables(includePlants: true); - - if (isStructure) - buildables = buildables.OfType(); - else - buildables = buildables.OfType(); - - return buildables.FirstOrDefault(k => k.InstanceId == instanceId); - } -} \ No newline at end of file diff --git a/API/Buildables/StructureBuildable.cs b/API/Buildables/StructureBuildable.cs deleted file mode 100644 index 0251b59..0000000 --- a/API/Buildables/StructureBuildable.cs +++ /dev/null @@ -1,94 +0,0 @@ -using SDG.Unturned; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.Buildables; - -/// -public sealed class StructureBuildable : Buildable -{ - /// - /// Server-Side structure data. - /// - public StructureData StructureData { get; } - - /// - /// The drop/model of the structure. - /// - public StructureDrop StructureDrop { get; } - - /// - /// Creates a new instance of with the specified data and drop. - /// - /// The drop to add. - public StructureBuildable(StructureDrop drop) - { - StructureDrop = drop; - StructureData = drop.GetServersideData(); - } - - /// - public override ushort AssetId => Asset.id; - - /// - public override ushort Health => StructureData.structure.health; - - /// - public override byte[]? State => null; - - /// - public override ulong Owner => StructureData.owner; - - /// - public override ulong Group => StructureData.group; - - /// - public override byte AngleX => StructureData.angle_x; - - /// - public override byte AngleY => StructureData.angle_y; - - /// - public override byte AngleZ => StructureData.angle_z; - - /// - public override Vector3 Position => StructureData.point; - - /// - public override Transform Model => StructureDrop.model; - - /// - public override Interactable? Interactable => null; - - /// - public override uint InstanceId => StructureData.instanceID; - - /// - public override Asset Asset => StructureData.structure.asset; - - /// - public override bool IsPlanted => false; - - /// - public override void UnsafeDestroy() - { - ThreadUtil.assertIsGameThread(); - if (!StructureManager.tryGetRegion(Model, out var x, out var y, out _)) - return; - - StructureManager.destroyStructure(StructureDrop, x, y, Vector3.zero); - } - - /// - public override void UnsafeDamage(ushort damage) - { - ThreadUtil.assertIsGameThread(); - StructureManager.damage(Model, Vector3.zero, damage, 1, false, damageOrigin: EDamageOrigin.Unknown); - } - - /// - public override void UnsafeHeal(ushort amount) - { - ThreadUtil.assertIsGameThread(); - StructureManager.repair(Model, amount, 1); - } -} \ No newline at end of file diff --git a/API/Delegates/BuildableChange.cs b/API/Delegates/BuildableChange.cs deleted file mode 100644 index 2a4d1ef..0000000 --- a/API/Delegates/BuildableChange.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Pustalorc.Plugins.BaseClustering.API.Buildables; - -namespace Pustalorc.Plugins.BaseClustering.API.Delegates; - -/// -/// A delegate that handles any notification about a buildable changing (being added, removed). -/// -/// The affected . -/// -/// This delegate is to not be used and fired if multiple buildables changed. -///
-/// If multiple of them changed, please use . -///
-public delegate void BuildableChange(Buildable buildable); \ No newline at end of file diff --git a/API/Delegates/BuildableDeleted.cs b/API/Delegates/BuildableDeleted.cs deleted file mode 100644 index 7a4da26..0000000 --- a/API/Delegates/BuildableDeleted.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Pustalorc.Plugins.BaseClustering.API.Delegates; - -/// -/// A delegate that handles deletion of buildables from nelson's code. -/// -/// The instance Id of the buildable. -/// If the buildable was a structure or not. -public delegate void BuildableDeleted(uint instanceId, bool isStructure); \ No newline at end of file diff --git a/API/Delegates/BuildablesChanged.cs b/API/Delegates/BuildablesChanged.cs deleted file mode 100644 index 1a91f35..0000000 --- a/API/Delegates/BuildablesChanged.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using Pustalorc.Plugins.BaseClustering.API.Buildables; - -namespace Pustalorc.Plugins.BaseClustering.API.Delegates; - -/// -/// A delegate that handles a notification about multiple s changing (being added or removed). -/// -/// The affected s. -public delegate void BuildablesChanged(IEnumerable buildables); \ No newline at end of file diff --git a/API/Delegates/ClusterChange.cs b/API/Delegates/ClusterChange.cs deleted file mode 100644 index 1af653d..0000000 --- a/API/Delegates/ClusterChange.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Pustalorc.Plugins.BaseClustering.API.BaseClusters; - -namespace Pustalorc.Plugins.BaseClustering.API.Delegates; - -/// -/// A delegate that handles any notification about a cluster changing (being added, removed, reset, etc). -/// -/// The affected . -public delegate void ClusterChange(BaseCluster cluster); \ No newline at end of file diff --git a/API/Delegates/VoidDelegate.cs b/API/Delegates/VoidDelegate.cs deleted file mode 100644 index 136402e..0000000 --- a/API/Delegates/VoidDelegate.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Pustalorc.Plugins.BaseClustering.API.Delegates; - -/// -/// A delegate with no return type () and no parameters. -/// -public delegate void VoidDelegate(); \ No newline at end of file diff --git a/API/Patches/PatchBuildableTransforms.cs b/API/Patches/PatchBuildableTransforms.cs deleted file mode 100644 index f18290c..0000000 --- a/API/Patches/PatchBuildableTransforms.cs +++ /dev/null @@ -1,47 +0,0 @@ -using HarmonyLib; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Delegates; -using SDG.Unturned; - -// ReSharper disable InconsistentNaming -// Patches have inconsistent naming due to harmony rules. - -namespace Pustalorc.Plugins.BaseClustering.API.Patches; - -/// -/// A patch for barricades and structures being transformed. -/// -public static class PatchBuildableTransforms -{ - /// - /// Event is fired whenever a barricade or structure is transformed. - /// - public static event BuildableChange? OnBuildableTransformed; - - [HarmonyPatch] - internal static class InternalPatches - { - [HarmonyPatch(typeof(BarricadeDrop), "ReceiveTransform")] - [HarmonyPostfix] - [UsedImplicitly] - internal static void ReceiveTransformBarricade(BarricadeDrop __instance) - { - var buildable = BuildableDirectory.GetBuildable(__instance.instanceID, false) ?? - new BarricadeBuildable(__instance); - - OnBuildableTransformed?.Invoke(buildable); - } - - [HarmonyPatch(typeof(StructureDrop), "ReceiveTransform")] - [HarmonyPostfix] - [UsedImplicitly] - internal static void ReceiveTransformStructure(StructureDrop __instance) - { - var buildable = BuildableDirectory.GetBuildable(__instance.instanceID, true) ?? - new StructureBuildable(__instance); - - OnBuildableTransformed?.Invoke(buildable); - } - } -} \ No newline at end of file diff --git a/API/Patches/PatchBuildablesDestroy.cs b/API/Patches/PatchBuildablesDestroy.cs deleted file mode 100644 index 81625a3..0000000 --- a/API/Patches/PatchBuildablesDestroy.cs +++ /dev/null @@ -1,42 +0,0 @@ -using HarmonyLib; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Delegates; -using SDG.Unturned; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.Patches; - -/// -/// A patch for barricades and structures being destroyed. -/// -public static class PatchBuildablesDestroy -{ - /// - /// Event is fired whenever a barricade or structure is destroyed. - /// - public static event BuildableDeleted? OnBuildableDestroyed; - - [HarmonyPatch] - internal static class InternalPatches - { - [HarmonyPatch(typeof(BarricadeManager), "destroyBarricade", typeof(BarricadeDrop), typeof(byte), - typeof(byte), typeof(ushort))] - [HarmonyPrefix] - [UsedImplicitly] - internal static void DestroyBarricade(BarricadeDrop barricade) - { - ThreadUtil.assertIsGameThread(); - OnBuildableDestroyed?.Invoke(barricade.instanceID, false); - } - - [HarmonyPatch(typeof(StructureManager), "destroyStructure", typeof(StructureDrop), typeof(byte), - typeof(byte), typeof(Vector3))] - [HarmonyPrefix] - [UsedImplicitly] - internal static void DestroyStructure(StructureDrop structure) - { - ThreadUtil.assertIsGameThread(); - OnBuildableDestroyed?.Invoke(structure.instanceID, true); - } - } -} \ No newline at end of file diff --git a/API/Utilities/Extensions.cs b/API/Utilities/Extensions.cs deleted file mode 100644 index ca0a800..0000000 --- a/API/Utilities/Extensions.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Rocket.API; -using Rocket.Unturned.Player; -using SDG.Unturned; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.Utilities; - -/// -/// A class with extensions that the plugin utilizes. -/// -[PublicAPI] -public static class Extensions -{ - /// - /// Calculates the average center of an . - /// - /// The to get the average center from. - /// - /// A that is the average center of . - /// - /// - /// If is null, then this exception is thrown, as should never be null. - /// - public static Vector3 AverageCenter(this IEnumerable source) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - - var list = source.ToList(); - - var sum = Vector3.zero; - - checked - { - sum = list.Aggregate(sum, (current, element) => current + element); - } - - if (list.Count > 0) return sum / list.Count; - - return Vector3.zero; - } - - /// - /// Calculates the average center of an . - /// - /// The to get the average center from. - /// The way that the should be selected to convert it to an . - /// A type that can select a . - /// - /// A that is the average center of after applying a . - /// - /// - /// This method calls , which only takes an . - ///
- /// Therefore any input for this average center should support a selector to a . - ///
- public static Vector3 AverageCenter(this IEnumerable source, Func selector) - { - return source.Select(selector).AverageCenter(); - } - - /// - /// Checks if any element from is equal to the . - /// - /// The that should be searched. - /// The that we should find. - /// - /// If is found in , this will be a number greater than -1 but smaller than args.Count - ///
- /// If isn't found in , this will be -1. - /// - /// - /// if is > -1. - ///
- /// if is == -1. - ///
- public static bool CheckArgsIncludeString(this IEnumerable args, string include, out int index) - { - index = args.ToList().FindIndex(k => k.Equals(include, StringComparison.OrdinalIgnoreCase)); - return index > -1; - } - - /// - /// Gets all the of s that an element of . - /// - /// The that should be searched. - /// - /// If any element of can be an (s), this will be a number greater than -1 but smaller than args.Count - ///
- /// If no elements of can be an (s), this will be -1. - /// - /// - /// An empty if no element in can be an . - ///
- /// A with all the s from one of the entries in . - ///
- public static List GetMultipleItemAssets(this IEnumerable args, out int index) - { - var argsL = args.ToList(); - var assets = Assets.find(EAssetType.ITEM).Cast() - .Where(k => k is { itemName: not null, name: not null }).OrderBy(k => k.itemName.Length).ToList(); - - for (index = 0; index < argsL.Count; index++) - { - var itemAssets = assets.Where(k => - argsL[0].Equals(k.id.ToString(), StringComparison.OrdinalIgnoreCase) || - argsL[0].Split(' ').All(l => k.itemName.ToLower().Contains(l)) || - argsL[0].Split(' ').All(l => k.name.ToLower().Contains(l))).ToList(); - - if (itemAssets.Count <= 0) - continue; - - return itemAssets; - } - - index = -1; - return new List(); - } - - /// - /// Gets a from an element in . - /// - /// The that should be searched. - /// - /// If any element of is a valid , this will be a number greater than -1 but smaller than args.Count - ///
- /// If no elements of is a valid , this will be -1. - /// - /// - /// A from one of the entries in . - /// - public static float GetFloat(this IEnumerable args, out int index) - { - var output = float.NegativeInfinity; - index = args.ToList().FindIndex(k => float.TryParse(k, out output)); - return output; - } - - /// - /// Gets a from an element in . - /// - /// The that should be searched. - /// - /// If any element of is a valid , this will be a number greater than -1 but smaller than args.Count - ///
- /// If no elements of is a valid , this will be -1. - /// - /// - /// if none of the entries in can be an . - ///
- /// A from one of the entries in . - ///
- public static IRocketPlayer? GetIRocketPlayer(this IEnumerable args, out int index) - { - IRocketPlayer? output = null; - index = args.ToList().FindIndex(k => - { - output = UnturnedPlayer.FromName(k); - if (output == null && ulong.TryParse(k, out var id) && id > 76561197960265728) - output = new RocketPlayer(id.ToString()); - - return output != null; - }); - return output; - } - - /// - /// Compares a to see if it is a , as default comparison doesn't correctly check it. - /// - /// The to compare to negative infinity. - /// - /// if has any component. - ///
- /// if has no component. - ///
- public static bool IsNegativeInfinity(this Vector3 vector) - { - return float.IsNegativeInfinity(vector.x) || float.IsNegativeInfinity(vector.y) || - float.IsNegativeInfinity(vector.z); - } -} \ No newline at end of file diff --git a/API/Utilities/Logging.cs b/API/Utilities/Logging.cs deleted file mode 100644 index d8e9614..0000000 --- a/API/Utilities/Logging.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using Rocket.Core.Logging; -using Rocket.Core.Plugins; - -namespace Pustalorc.Plugins.BaseClustering.API.Utilities; - -/// -/// Custom class for logging. To be used only by this plugin. -/// -internal static class Logging -{ - /// - /// Logs a normal level message. - /// - /// - /// The apparent source that sent this message. - ///
- /// If it is of type then the version will be retrieved from the file to log alongside its name. - /// The actual message to be printed out. - /// The to use in console. - /// If the verbose message should be logged in rocketmod's log file. - /// If the message is to be modified when logging to rocketmod's log file, this value will be used if it is not null. - /// If the color that rocket perceives was used is to be modified when logging to rocketmod's log file, this value will be used if it is not null. - public static void Write(object source, object message, ConsoleColor consoleColor = ConsoleColor.Green, - bool logInRocket = true, object? rocketMessage = null, ConsoleColor? rocketColor = null) - { - var sourceIdent = source.ToString(); - - if (source is RocketPlugin pl) - { - var pluginVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(pl.Assembly.Location) - .ProductVersion; - sourceIdent = $"{pl.Name} v{pluginVersion}"; - } - - Console.ForegroundColor = consoleColor; - Console.WriteLine($"[{sourceIdent}]: {message}"); - - if (logInRocket) - Logger.ExternalLog(rocketMessage ?? message, rocketColor ?? consoleColor); - - Console.ResetColor(); - } - - /// - /// Logs that a plugin was loaded. - /// - /// The instance of the plugin that was loaded. - public static void PluginLoaded(RocketPlugin plugin) - { - var pluginVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(plugin.Assembly.Location) - .ProductVersion; - var pluginIdentity = $"{plugin.Name} v{pluginVersion}"; - Write(pluginIdentity, $"{pluginIdentity}, by Pustalorc, has been loaded."); - } - - /// - /// Logs that a plugin was unloaded. - /// - /// The instance of the plugin that was unloaded. - public static void PluginUnloaded(RocketPlugin plugin) - { - var pluginVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(plugin.Assembly.Location) - .ProductVersion; - var pluginIdentity = $"{plugin.Name} v{pluginVersion}"; - Write(pluginIdentity, $"{pluginIdentity}, by Pustalorc, has been unloaded."); - } -} \ No newline at end of file diff --git a/API/WreckingActions/WreckAction.cs b/API/WreckingActions/WreckAction.cs deleted file mode 100644 index 6fb8a77..0000000 --- a/API/WreckingActions/WreckAction.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Rocket.API; -using SDG.Unturned; -using System.Collections.Generic; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using UnityEngine; - -namespace Pustalorc.Plugins.BaseClustering.API.WreckingActions; - -/// -/// A wreck action for a collection of s. -///
-/// Actions determine how the plugin will do a final search when performing a confirmed wreck. -///
-public readonly struct WreckAction -{ - /// - /// The player we might possibly be targeting that owns specific clusters. - /// - public IRocketPlayer? TargetPlayer { get; } - - /// - /// The center position of the wreck action. - ///
- /// If no position is wanted, this value is to be set to . - ///
- public Vector3 Center { get; } - - /// - /// A list of all s that will be targeted. - /// - public List ItemAssets { get; } - - /// - /// The name of the user input for item asset search, or the name of the only item asset used. - /// - public string ItemAssetName { get; } - - /// - /// The radius based on the Center specified in this object. - /// - public float Radius { get; } - - /// - /// If the action should care about things on vehicles. - /// - public bool IncludeVehicles { get; } - - /// - /// If the action should filter only for barricades. - /// - public bool FilterForBarricades { get; } - - /// - /// If the action should filter only for structures. - /// - public bool FilterForStructures { get; } - - /// - /// Creates a new instance of the class. - /// - /// If the action should care about things on vehicles. - /// If the action should filter only for barricades. - /// If the action should filter only for structures. - /// The player we might possibly be targeting that owns specific clusters. - /// - /// The center position of the wreck action. - ///
- /// If no position is wanted, this value is to be set to . - /// - /// A list of all s that will be targeted. - /// The radius based on the Center specified in this object. - /// The name of the user input for item asset search, or the name of the only item asset used. - public WreckAction(bool plants, bool barricades, bool structs, IRocketPlayer? target, Vector3 center, - List assets, float radius, string itemAssetName) - { - IncludeVehicles = plants; - FilterForBarricades = barricades; - FilterForStructures = structs; - TargetPlayer = target; - Center = center; - ItemAssets = assets; - Radius = radius; - ItemAssetName = itemAssetName; - } -} \ No newline at end of file diff --git a/BaseClustering.API/BaseClustering.API.csproj b/BaseClustering.API/BaseClustering.API.csproj new file mode 100644 index 0000000..6ba317f --- /dev/null +++ b/BaseClustering.API/BaseClustering.API.csproj @@ -0,0 +1,38 @@ + + + net481;netstandard20 + 12 + enable + Pustalorc.Libraries.BaseClustering.API + BaseClustering.API + Pustalorc.BaseClustering.API + BaseClustering.API + Pustalorc + Copyright © Pustalorc 2020-2024 + 3.0.0 + 3.0.1 + 3.0.1 + bin\$(Configuration)\ + true + BaseClustering + Pustalorc + Library to cluster/group Buildables, which allows to define what a player built base is. + https://github.com/Pustalorc/BaseClustering/ + LGPL-3.0-or-later + Github + Pustalorc.BaseClustering.API + Push project to nuget + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Events/Added/BaseClusterBuildablesAddedEvent.cs b/BaseClustering.API/BaseClusters/Clusters/Events/Added/BaseClusterBuildablesAddedEvent.cs new file mode 100644 index 0000000..09da990 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Events/Added/BaseClusterBuildablesAddedEvent.cs @@ -0,0 +1,12 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.RocketModServices.Events.Implementations.Generics; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Added; + +/// +/// +/// An event that is raised when one or more new buildables are added to a cluster. +/// +[PublicAPI] +public class BaseClusterBuildablesAddedEvent : Event; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Events/Added/BaseClusterBuildablesAddedEventArguments.cs b/BaseClustering.API/BaseClusters/Clusters/Events/Added/BaseClusterBuildablesAddedEventArguments.cs new file mode 100644 index 0000000..9b11a44 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Events/Added/BaseClusterBuildablesAddedEventArguments.cs @@ -0,0 +1,36 @@ +extern alias JetBrainsAnnotations; +using System.Collections.Generic; +using System.Linq; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Added; + +/// +/// A struct for the arguments that are given to the callbacks. +/// +[PublicAPI] +public struct BaseClusterBuildablesAddedEventArguments +{ + /// + /// The cluster that was affected by this change. + /// + public IBaseCluster Cluster { get; } + + /// + /// The buildables that are added to the cluster. + /// + public List Buildables { get; } + + /// + /// Constructs the event's params struct + /// + /// The cluster that was affected by this change. + /// The buildables that are added to the cluster. + public BaseClusterBuildablesAddedEventArguments(IBaseCluster cluster, IEnumerable buildables) + { + Cluster = cluster; + Buildables = buildables.ToList(); + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Events/Removed/BaseClusterBuildablesRemovedEvent.cs b/BaseClustering.API/BaseClusters/Clusters/Events/Removed/BaseClusterBuildablesRemovedEvent.cs new file mode 100644 index 0000000..fe3e28f --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Events/Removed/BaseClusterBuildablesRemovedEvent.cs @@ -0,0 +1,12 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.RocketModServices.Events.Implementations.Generics; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Removed; + +/// +/// +/// An event that is raised when one or more buildables are removed from a cluster. +/// +[PublicAPI] +public class BaseClusterBuildablesRemovedEvent : Event; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Events/Removed/BaseClusterBuildablesRemovedEventArguments.cs b/BaseClustering.API/BaseClusters/Clusters/Events/Removed/BaseClusterBuildablesRemovedEventArguments.cs new file mode 100644 index 0000000..c0ab804 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Events/Removed/BaseClusterBuildablesRemovedEventArguments.cs @@ -0,0 +1,36 @@ +extern alias JetBrainsAnnotations; +using System.Collections.Generic; +using System.Linq; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Removed; + +/// +/// A struct for the arguments that are given to the callbacks. +/// +[PublicAPI] +public struct BaseClusterBuildablesRemovedEventArguments +{ + /// + /// The cluster that was affected by this change. + /// + public IBaseCluster Cluster { get; } + + /// + /// The buildables that are removed from the cluster. + /// + public List Buildables { get; } + + /// + /// Constructs the event's params struct + /// + /// The cluster that was affected by this change. + /// The buildables that are removed from the cluster. + public BaseClusterBuildablesRemovedEventArguments(IBaseCluster cluster, IEnumerable buildables) + { + Cluster = cluster; + Buildables = buildables.ToList(); + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Events/Reset/BaseClusterResetEvent.cs b/BaseClustering.API/BaseClusters/Clusters/Events/Reset/BaseClusterResetEvent.cs new file mode 100644 index 0000000..216d4e4 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Events/Reset/BaseClusterResetEvent.cs @@ -0,0 +1,12 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.RocketModServices.Events.Implementations.Generics; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Reset; + +/// +/// +/// An event that is raised when a cluster is reset. +/// +[PublicAPI] +public class BaseClusterResetEvent : Event; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Events/Reset/BaseClusterResetEventArguments.cs b/BaseClustering.API/BaseClusters/Clusters/Events/Reset/BaseClusterResetEventArguments.cs new file mode 100644 index 0000000..012dc59 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Events/Reset/BaseClusterResetEventArguments.cs @@ -0,0 +1,26 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Reset; + +/// +/// A struct for the arguments that are given to the callbacks. +/// +[PublicAPI] +public struct BaseClusterResetEventArguments +{ + /// + /// The cluster that was reset. + /// + public IBaseCluster Cluster { get; } + + /// + /// Constructs the event's params struct + /// + /// The cluster that was reset. + public BaseClusterResetEventArguments(IBaseCluster cluster) + { + Cluster = cluster; + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Implementation/DefaultBaseCluster.cs b/BaseClustering.API/BaseClusters/Clusters/Implementation/DefaultBaseCluster.cs new file mode 100644 index 0000000..2233cba --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Implementation/DefaultBaseCluster.cs @@ -0,0 +1,335 @@ +extern alias JetBrainsAnnotations; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Added; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Removed; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Reset; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.Utilities; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Implementations; +using Pustalorc.Libraries.RocketModServices.Events.Bus; +using Pustalorc.Libraries.RocketModServices.Services; +using SDG.Unturned; +using UnityEngine; +using Random = UnityEngine.Random; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Implementation; + +/// +/// This class provides a basic default Base Cluster. +/// +/// +[PublicAPI] +public class DefaultBaseCluster : IBaseCluster +{ + /// + public uint InstanceId { get; } + + /// + public virtual ulong Owner => BuildableList.GroupBy(static k => k.Owner).OrderByDescending(static k => k.Count()) + .Select(static g => g.Key).FirstOrDefault(); + + /// + public virtual ulong Group => BuildableList.GroupBy(static k => k.Group).OrderByDescending(static k => k.Count()) + .Select(static g => g.Key).FirstOrDefault(); + + /// + public virtual Vector3 Center => BuildableList.OfType().AverageCenter(static k => k.Position); + + /// + public virtual IReadOnlyList Buildables => new ReadOnlyCollection(BuildableList); + + /// + /// A modifiable list of all the buildables within this Base Cluster. + /// + protected List BuildableList { get; } + + /// + /// The rules this cluster will uphold. + /// + protected IClusterRules ClusterRules { get; } + + /// + /// Defines if this cluster is being destroyed, and therefore integrity check operations shouldn't be handled. + /// + protected bool IsBeingDestroyed { get; set; } + + /// + /// Constructs a default base cluster. + /// + /// The rules the cluster will uphold. + /// This cluster's Instance ID. Do not assign at random, let the Directory handle it. + public DefaultBaseCluster(IClusterRules clusterRules, uint instanceId) + { + ClusterRules = clusterRules; + BuildableList = []; + InstanceId = instanceId; + } + + /// + public virtual bool IsWithinRange(Buildable buildable) + { + var structures = Buildables.OfType(); + var distanceCheck = buildable is StructureBuildable + ? Mathf.Pow(ClusterRules.MaxDistanceBetweenStructures, 2) + : Mathf.Pow(ClusterRules.MaxDistanceToConsiderPartOfBase, 2); + return structures.Any(k => (k.Position - buildable.Position).sqrMagnitude <= distanceCheck); + } + + /// + public virtual bool IsWithinRange(Vector3 vector) + { + var distanceCheck = Mathf.Pow(ClusterRules.MaxDistanceToConsiderPartOfBase, 2); + + return Buildables.OfType().Any(k => (k.Position - vector).sqrMagnitude <= distanceCheck); + } + + /// + public virtual void Reset() + { + BuildableList.Clear(); + EventBus.Publish(new BaseClusterResetEventArguments(this)); + } + + /// + public virtual void AddBuildable(Buildable buildable) + { + if (BuildableList.Contains(buildable)) + return; + + BuildableList.Add(buildable); + var stolenBuildables = + StealFromCluster(RocketModService.GetService().GetOrCreateGlobalCluster()); + EventBus.Publish( + new BaseClusterBuildablesAddedEventArguments(this, stolenBuildables.Concat([buildable]))); + } + + /// + public virtual void RemoveBuildable(Buildable buildable) + { + var removedSomething = BuildableList.Remove(buildable); + + if (!removedSomething) + return; + + EventBus.Publish( + new BaseClusterBuildablesRemovedEventArguments(this, [buildable])); + + if (IsBeingDestroyed) + return; + + VerifyAndCorrectIntegrity(); + } + + /// + public virtual void AddBuildables(IEnumerable buildables) + { + AddBuildablesInternal(buildables); + } + + /// + public virtual void RemoveBuildables(IEnumerable buildables) + { + var consolidatedBuildables = buildables.ToList(); + var removed = new List(); + + foreach (var build in BuildableList.Where(consolidatedBuildables.Remove).ToList()) + { + BuildableList.Remove(build); + removed.Add(build); + } + + if (removed.Count > 0) + EventBus.Publish( + new BaseClusterBuildablesRemovedEventArguments(this, removed)); + + if (removed.Count > 0 && !IsBeingDestroyed) + VerifyAndCorrectIntegrity(); + } + + /// + public virtual List StealFromCluster(IBaseCluster? cluster) + { + if (cluster == null) + return []; + + var buildablesInRange = cluster.Buildables.Where(IsWithinRange).ToList(); + AddBuildables(buildablesInRange); + cluster.RemoveBuildables(buildablesInRange); + + return buildablesInRange; + } + + /// + public virtual void Destroy(bool shouldDropItems = true) + { + IsBeingDestroyed = true; + + foreach (var buildable in Buildables.ToList()) + { + var store = (InteractableStorage?)buildable.Interactable; + if (store != null) + store.despawnWhenDestroyed = !shouldDropItems; + + buildable.SafeDestroy(); + } + + RocketModService.GetService().Unregister(this); + } + + /// + /// Adds multiple new s to this cluster. + /// + /// + /// An with all the new s that will + /// be added to this cluster. + /// + /// + /// If set to the will not + /// be raised. + /// + protected virtual void AddBuildablesInternal(IEnumerable buildables, bool raiseEvent = true) + { + var consolidatedBuildables = buildables.Where(buildable => !BuildableList.Contains(buildable)).ToList(); + BuildableList.AddRange(consolidatedBuildables); + if (raiseEvent && consolidatedBuildables.Count > 0) + EventBus.Publish( + new BaseClusterBuildablesAddedEventArguments(this, consolidatedBuildables)); + } + + + /// + /// Verifies that the cluster has structure integrity (all Structures are within range of another Structure) + /// + /// + /// if all Structures are within range of another Structure + ///
+ /// otherwise. + ///
+ protected virtual bool VerifyStructureIntegrity() + { + var allStructures = Buildables.OfType().ToList(); + + if (allStructures.Count <= 0) + return false; + + var maxStructureDistance = Mathf.Pow(ClusterRules.MaxDistanceBetweenStructures, 2); + var succeeded = new List(); + + var random = allStructures[Random.Range(0, allStructures.Count)]; + succeeded.Add(random); + allStructures.Remove(random); + + for (var i = 0; i < succeeded.Count; i++) + { + var element = succeeded[i]; + + var result = allStructures.Where(k => (element.Position - k.Position).sqrMagnitude <= maxStructureDistance) + .ToList(); + succeeded.AddRange(result); + allStructures.RemoveAll(result.Contains); + } + + return allStructures.Count == 0; + } + + /// + /// Verifies that the cluster has barricade integrity (all Barricades are within range of a Structure) + /// + /// + /// if all barricades are within range of a Structure + ///
+ /// otherwise. + ///
+ protected virtual bool VerifyBarricadeIntegrity() + { + var structures = Buildables.OfType().ToList(); + + if (structures.Count <= 0) + return false; + + var maxBuildableDistance = Mathf.Pow(ClusterRules.MaxDistanceToConsiderPartOfBase, 2); + + return Buildables.OfType().All(br => + structures.Exists(k => (br.Position - k.Position).sqrMagnitude <= maxBuildableDistance)); + } + + /// + /// This will verify the base integrity (that all the elements are still within range of configured limits) and if not, + /// it will correct that. + /// + protected virtual void VerifyAndCorrectIntegrity() + { + var directory = RocketModService.GetService(); + var structureIntegrity = VerifyStructureIntegrity(); + var barricadeIntegrity = VerifyBarricadeIntegrity(); + + // If the base is still integrally sound, skip the rest of the code + if (structureIntegrity && barricadeIntegrity) return; + + var globalCluster = directory.GetOrCreateGlobalCluster(); + + IsBeingDestroyed = true; + // If the structure is still integral, check the barricades and fix any non-integral parts. + if (structureIntegrity) + { + // Get all the barricades that are too far from the cluster in a copied list. + foreach (var b in Buildables.OfType().Where(k => !IsWithinRange(k)).ToList()) + { + // Find the next best cluster that this element is within + var bestCluster = directory.FindBestCluster(b); + + // If something is found, check that it's not the same cluster we are already in. + if (bestCluster != null) + { + if (bestCluster != this) + { + // If it's a different cluster, remove it from the current cluster and add it to the new one. + RemoveBuildable(b); + bestCluster.AddBuildable(b); + } + + continue; + } + + // If no best cluster is found, check if we have a global cluster. If we do, add the barricade to it. If we don't, create a new global cluster. + RemoveBuildable(b); + globalCluster.AddBuildable(b); + } + + IsBeingDestroyed = false; + return; + } + + // First, get a list of all buildables to cluster, including global cluster. + var builds = Buildables.Concat(globalCluster.Buildables).ToList(); + globalCluster.Reset(); + var clusterRegened = directory.ClusterElements(builds) + .OrderByDescending(static k => k.Buildables.Count).ToList(); + + // Dispose correctly of the cluster we are not going to add here. + var discarded = clusterRegened.FirstOrDefault(); + directory.Unregister(discarded); + + // Select all the clusters, except for the largest one. + foreach (var c in clusterRegened.Skip(1).ToList()) + { + // Remove any of the elements on the new cluster from the old one. + RemoveBuildables(c.Buildables.ToList()); + + // Add the new cluster to the directory. + directory.Register(c); + } + + // Finally, if there's no structure buildables left in this cluster, call to remove it. + if (!Buildables.OfType().Any()) + directory.Unregister(this); + + IsBeingDestroyed = false; + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Implementation/GlobalBaseCluster.cs b/BaseClustering.API/BaseClusters/Clusters/Implementation/GlobalBaseCluster.cs new file mode 100644 index 0000000..459560d --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Implementation/GlobalBaseCluster.cs @@ -0,0 +1,88 @@ +extern alias JetBrainsAnnotations; +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Implementations; +using UnityEngine; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Implementation; + +/// +[PublicAPI] +public class GlobalBaseCluster : DefaultBaseCluster +{ + /// + public GlobalBaseCluster(IClusterRules clusterRules, uint instanceId) : base(clusterRules, instanceId) + { + } + + /// + public override void AddBuildable(Buildable build) + { + if (build is StructureBuildable) + throw new NotSupportedException("StructureBuildables are not supported by global clusters."); + + base.AddBuildable(build); + } + + /// + protected override void AddBuildablesInternal(IEnumerable buildables, bool raiseEvent = true) + { + base.AddBuildablesInternal(buildables.Where(static buildable => buildable is not StructureBuildable), + raiseEvent); + } + + /// + public override void Destroy(bool shouldDropItems = true) + { + base.Destroy(shouldDropItems); + IsBeingDestroyed = false; + } + + /// + /// + /// A global cluster does not have integrity. It holds all objects within range that are not in a separate cluster. + /// + protected override void VerifyAndCorrectIntegrity() + { + } + + /// + /// + /// A global cluster does not have integrity. It holds all objects within range that are not in a separate cluster. + /// + protected override bool VerifyBarricadeIntegrity() + { + return true; + } + + /// + /// + /// A global cluster does not have integrity. It holds all objects within range that are not in a separate cluster. + /// + protected override bool VerifyStructureIntegrity() + { + return true; + } + + /// + /// + /// A global cluster does not have integrity. It holds all objects within range that are not in a separate cluster. + /// + public override bool IsWithinRange(Buildable buildable) + { + return false; + } + + /// + /// + /// A global cluster does not have integrity. It holds all objects within range that are not in a separate cluster. + /// + public override bool IsWithinRange(Vector3 vector) + { + return false; + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Clusters/Interfaces/IBaseCluster.cs b/BaseClustering.API/BaseClusters/Clusters/Interfaces/IBaseCluster.cs new file mode 100644 index 0000000..57fbcf7 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Clusters/Interfaces/IBaseCluster.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Events.Reset; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; +using UnityEngine; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; + +/// +/// An interface definition of what a Base Cluster should hold and provide. +/// +[PublicAPI] +public interface IBaseCluster +{ + /// + /// An Instance ID to uniquely identify this cluster from others. + /// + public uint InstanceId { get; } + + /// + /// Provides the SteamId of the player that has placed the most elements for this base. + /// + public ulong Owner { get; } + + /// + /// Provides the SteamId of the group that has placed the most elements for this base. + /// + public ulong Group { get; } + + /// + /// The center point of this cluster. + /// + public Vector3 Center { get; } + + /// + /// A read only list of all the buildables in this cluster. + /// + public IReadOnlyList Buildables { get; } + + /// + /// Destroys the cluster and all its buildables. + /// + /// + /// If any buildables that can store items should drop their items on destruction, or if they + /// should delete the items. + /// + public void Destroy(bool shouldDropItems = true); + + /// + /// Resets a cluster to its default state (no buildables). + /// This method should raise . + /// + public void Reset(); + + /// + /// Checks if a is within range of this cluster. + /// + /// The to check. + /// + /// if the is within range. + ///
+ /// if the is outside range. + ///
+ public bool IsWithinRange(Buildable buildable); + + /// + /// Checks if a falls within range of this cluster. + /// + /// The to check. + /// + /// if the is within range. + ///
+ /// if the is outside range. + ///
+ /// + /// Unlike , this method should only check with + /// . It should not check with + /// . + /// + public bool IsWithinRange(Vector3 vector); + + /// + /// Adds a new to this cluster. + /// + /// The that will be added to the cluster. + public void AddBuildable(Buildable buildable); + + /// + /// Removes a from this cluster. + /// + /// The that will be removed from the cluster. + public void RemoveBuildable(Buildable buildable); + + /// + /// Adds multiple new s to this cluster. + /// + /// + /// An with all the new s that will + /// be added to this cluster. + /// + public void AddBuildables(IEnumerable buildables); + + /// + /// Removes multiple s from this cluster. + /// + /// + /// An with all the s that will be + /// removed from this cluster. + /// + public void RemoveBuildables(IEnumerable buildables); + + /// + /// Makes this cluster steal any in-range buildables from the opposing cluster. + /// + /// The target cluster to steal buildables from. + /// + /// A with all of the s that were stolen from the target + /// cluster. + /// + public List StealFromCluster(IBaseCluster? cluster); +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Configuration/Implementations/DefaultClusterRules.cs b/BaseClustering.API/BaseClusters/Configuration/Implementations/DefaultClusterRules.cs new file mode 100644 index 0000000..f611a5a --- /dev/null +++ b/BaseClustering.API/BaseClusters/Configuration/Implementations/DefaultClusterRules.cs @@ -0,0 +1,21 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Implementations; + +/// +/// +/// The default cluster rules. To be used when no other +/// is +/// available. +/// +[PublicAPI] +public class DefaultClusterRules : IClusterRules +{ + /// + public float MaxDistanceBetweenStructures { get; set; } = 6.1f; + + /// + public float MaxDistanceToConsiderPartOfBase { get; set; } = 10f; +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Configuration/Interfaces/IClusterRules.cs b/BaseClustering.API/BaseClusters/Configuration/Interfaces/IClusterRules.cs new file mode 100644 index 0000000..1081536 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Configuration/Interfaces/IClusterRules.cs @@ -0,0 +1,31 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using UnityEngine; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; + +/// +/// The required rules that clusters will follow. +/// +[PublicAPI] +public interface IClusterRules +{ + /// + /// The maximum distance between 2 structure objects for them to be considered part of a + /// . + ///
+ /// Distance is in meters by unity's measurements. Squaring is needed if using + /// for speed. + ///
+ public float MaxDistanceBetweenStructures { get; set; } + + /// + /// The maximum distance between a structure object and a position for it to be considered part of, or inside of a + /// . + ///
+ /// Distance is in meters by unity's measurements. Squaring is needed if using + /// for speed. + ///
+ public float MaxDistanceToConsiderPartOfBase { get; set; } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Constants/LoggingConstants.cs b/BaseClustering.API/BaseClusters/Directory/Constants/LoggingConstants.cs new file mode 100644 index 0000000..93edffb --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Constants/LoggingConstants.cs @@ -0,0 +1,28 @@ +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Constants; + +internal static class LoggingConstants +{ + public const string BuildablesLoaded = "Loaded {0} buildables from the map. Took {1}ms"; + + public const string ClustersGeneratingWarning = + "Generating new clusters. This can take a LONG time. How long will depend on the following factors (but not limited to): CPU usage, CPU cores/threads, Buildables in the map. This generation only needs to be ran once from raw."; + + public const string ClustersLoaded = "Clusters Loaded: {0}. Took {1}ms."; + + public const string BuildableCountMismatch = + "Warning! Buildable count doesn't match saved count! Buildable save data was most likely modified or lost during server downtime. Clusters will be now rebuilt."; + + public const string BuildableCountMismatchDebug = + "Buildables according to IBuildableDirectory: {0}. Buildables according to save file: {1}."; + + public const string ClusterLoadProgress = "Loading saved clusters... {0}% [{1}/{2}] {3}ms"; + public const string ClusterSaveProgress = "Saving clusters... {0}% [{1}/{2}] {3}ms"; + + public const string BuildNotFoundDuringLoad = + "Warning! Buildable with InstanceId {0} [isStructure: {1}] not found! Save data was most likely modified or lost during server downtime. Clusters will be now rebuilt."; + + public const string ClusterLoadException = + "Warning! An exception was thrown when attempting to load the save file. Assuming the data is corrupted. Clusters will be now rebuilt. Exception: {0}"; + + public const string ClusterGenerationProgress = "Generating new clusters... {0}% [{1}/{2}] {3}ms"; +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Constants/SaveConstants.cs b/BaseClustering.API/BaseClusters/Directory/Constants/SaveConstants.cs new file mode 100644 index 0000000..3abfc2d --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Constants/SaveConstants.cs @@ -0,0 +1,8 @@ +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Constants; + +internal static class SaveConstants +{ + public const string SaveFileName = "Bases.dat"; + + public const string LevelFolderName = "Level"; +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterAddedEvent/BaseClusterAddedEvent.cs b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterAddedEvent/BaseClusterAddedEvent.cs new file mode 100644 index 0000000..2382764 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterAddedEvent/BaseClusterAddedEvent.cs @@ -0,0 +1,12 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.RocketModServices.Events.Implementations.Generics; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterAddedEvent; + +/// +/// +/// An event that is raised when a new Base Cluster is generated and added. +/// +[PublicAPI] +public class BaseClusterAddedEvent : Event; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterAddedEvent/BaseClusterAddedEventArguments.cs b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterAddedEvent/BaseClusterAddedEventArguments.cs new file mode 100644 index 0000000..650ad99 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterAddedEvent/BaseClusterAddedEventArguments.cs @@ -0,0 +1,23 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterAddedEvent; + +/// +/// +[PublicAPI] +public struct BaseClusterAddedEventArguments +{ + /// + /// + public IBaseCluster Cluster { get; } + + /// + /// + /// + public BaseClusterAddedEventArguments(IBaseCluster cluster) + { + Cluster = cluster; + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterGeneratedEvent/BaseClustersGeneratedEvent.cs b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterGeneratedEvent/BaseClustersGeneratedEvent.cs new file mode 100644 index 0000000..1f2f111 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterGeneratedEvent/BaseClustersGeneratedEvent.cs @@ -0,0 +1,13 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.RocketModServices.Events.Implementations; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterGeneratedEvent; + +/// +/// +/// The event that is fired when all s are generated. +/// +[PublicAPI] +public class BaseClustersGeneratedEvent : Event; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterRemovedEvent/BaseClusterRemovedEvent.cs b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterRemovedEvent/BaseClusterRemovedEvent.cs new file mode 100644 index 0000000..f8821fc --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterRemovedEvent/BaseClusterRemovedEvent.cs @@ -0,0 +1,15 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.RocketModServices.Events.Implementations.Generics; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterRemovedEvent; + +/// +/// +/// The event that is fired when a is removed/returned from a +/// +/// +[PublicAPI] +public class BaseClusterRemovedEvent : Event; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterRemovedEvent/BaseClusterRemovedEventArguments.cs b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterRemovedEvent/BaseClusterRemovedEventArguments.cs new file mode 100644 index 0000000..ffd2b93 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Events/BaseClusterRemovedEvent/BaseClusterRemovedEventArguments.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterRemovedEvent; + +/// +/// The arguments for the +/// +/// The affected cluster for the event. +[PublicAPI] +public struct BaseClusterRemovedEventArguments(IBaseCluster cluster) +{ + /// + /// The affected cluster for this event. + /// + public IBaseCluster Cluster { get; } = cluster; +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Exceptions/BaseClusterDirectoryNotLoaded.cs b/BaseClustering.API/BaseClusters/Directory/Exceptions/BaseClusterDirectoryNotLoaded.cs new file mode 100644 index 0000000..e5a2ca5 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Exceptions/BaseClusterDirectoryNotLoaded.cs @@ -0,0 +1,5 @@ +using System; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Exceptions; + +internal sealed class BaseClusterDirectoryNotLoaded : Exception; \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Implementations/DefaultBaseClusterDirectory.cs b/BaseClustering.API/BaseClusters/Directory/Implementations/DefaultBaseClusterDirectory.cs new file mode 100644 index 0000000..e333821 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Implementations/DefaultBaseClusterDirectory.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Implementation; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Implementations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Constants; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterAddedEvent; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterGeneratedEvent; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Events.BaseClusterRemovedEvent; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Utilities; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.BuildableChangeDelayer.Events.Destroy; +using Pustalorc.Libraries.BuildableAbstractions.API.BuildableChangeDelayer.Events.Spawn; +using Pustalorc.Libraries.BuildableAbstractions.API.BuildableChangeDelayer.Events.Transform; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Implementations; +using Pustalorc.Libraries.BuildableAbstractions.API.Directory.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Directory.Utils; +using Pustalorc.Libraries.Logging.API.Loggers.Configuration; +using Pustalorc.Libraries.Logging.API.Pipes.Configuration; +using Pustalorc.Libraries.Logging.LogLevels; +using Pustalorc.Libraries.Logging.Manager; +using Pustalorc.Libraries.Logging.Pipes.Configuration; +using Pustalorc.Libraries.RocketModServices.Events.Bus; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Libraries.RocketModServices.Services.Interfaces; +using SDG.Unturned; +using UnityEngine; +using Random = UnityEngine.Random; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Implementations; + +/// +/// The default directory that keeps track of all s. +/// +/// +[PublicAPI] +public class DefaultBaseClusterDirectory : IBaseClusterDirectory, IService +{ + /// + public IReadOnlyCollection Clusters => new ReadOnlyCollection(LoadedClusters); + + private List LoadedClusters { get; set; } + private IBaseCluster? GlobalCluster { get; set; } + private IClusterRules ClusterRules { get; set; } + private SaveFileLoader SaveFileLoader { get; } + + /// + /// Instantiates the default base cluster directory with default configuration. + /// + public DefaultBaseClusterDirectory() + { + LoadedClusters = new List(); + GlobalCluster = null; + ClusterRules = new DefaultClusterRules(); + SaveFileLoader = new SaveFileLoader(Path.Combine(ServerSavedata.directory, Provider.serverID, + SaveConstants.LevelFolderName, Level.info.name, SaveConstants.SaveFileName)); + ServiceHelper.GetServiceOrUseDefault(); + LogManager.UpdateConfiguration(new LogConfiguration()); + } + + /// + public void ChangeClusterRules(IClusterRules clusterRules) + { + if (ClusterRules == clusterRules) + return; + + RocketModService.TryGetService()?.ChangeClusterRules(clusterRules); + ClusterRules = clusterRules; + } + + /// + public void Register(IBaseCluster? cluster) + { + if (cluster == null) + return; + + LoadedClusters.Add(cluster); + EventBus.Publish(new BaseClusterAddedEventArguments(cluster)); + } + + /// + public void Unregister(IBaseCluster? cluster) + { + if (cluster == null) + return; + + var clusterPool = RocketModService.TryGetService(); + + if (clusterPool == null) + return; + + var removedSomething = LoadedClusters.Remove(cluster); + var returnedSomething = clusterPool.Return(cluster); + + if (removedSomething && returnedSomething) + EventBus.Publish(new BaseClusterRemovedEventArguments(cluster)); + } + + /// + public IBaseCluster GetOrCreateGlobalCluster() + { + return GlobalCluster ??= RocketModService.GetService().GetOrCreatePooledCluster(true); + } + + /// + public IBaseCluster? FindBestCluster(Buildable target) + { + return FindBestClusters(target).FirstOrDefault(); + } + + /// + public IEnumerable FindBestClusters(Buildable target) + { + return Clusters.Where(k => k.IsWithinRange(target)) + .OrderBy(k => (k.Center - target.Position).sqrMagnitude); + } + + /// + public IBaseCluster? FindBestCluster(Vector3 target) + { + return FindBestClusters(target).FirstOrDefault(); + } + + /// + public IEnumerable FindBestClusters(Vector3 target) + { + return Clusters.Where(k => k.IsWithinRange(target)) + .OrderBy(k => (k.Center - target).sqrMagnitude); + } + + // This method has been heavily documented as to not lose what the fuck its meant to do/is doing. + // It's a pain to read anyway, and definitely could be improved. Feel free to make this better... Please. + /// + public List ClusterElements(IEnumerable buildables, bool shouldLogProgress = false) + { + // Start a new stopwatch. This will be used to log how long the program is taking with each step. + var stopwatch = Stopwatch.StartNew(); + + // Initialize an empty list for the output of this method. + var output = new List(); + + // Retrieve the cluster pool service + var baseClusterPool = RocketModService.GetService(); + + // Set constants of squared distance. This will be used on distance checks. + // Faster than taking the square root of a number. + var maxStructureDistance = Mathf.Pow(ClusterRules.MaxDistanceBetweenStructures, 2); + var maxBarricadeDistance = Mathf.Pow(ClusterRules.MaxDistanceToConsiderPartOfBase, 2); + + // Set a couple variables that are used for logging. + var currentMultiplier = 0; + var currentCount = 0; + + // Get all the buildables to cluster. Anything planted (ie: it's on a vehicle) should NOT be clustered. + var buildablesToCluster = buildables.Where(static k => !k.IsPlanted).ToList(); + + // Get the count of buildables to cluster. This will be used for logging. + var totalBuildablesToCluster = buildablesToCluster.Count; + var logRate = Math.Floor(totalBuildablesToCluster * 0.085); + + // Get all the structures to cluster from all the buildables that are being clustered. + var structuresToCluster = buildablesToCluster.OfType().ToList(); + + // Get all the barricades to cluster from all the buildables that are being clustered. + var barricadesToCluster = buildablesToCluster.OfType().ToList(); + + // A cluster is made by having at least one Structure. If we run out of structures to cluster, then the rest will be clustered in the global cluster. + while (structuresToCluster.Count > 0) + { + // Create a variable to store all the structures of the cluster. + var structuresOfCluster = new List(); + // Create a variable to store all the buildables of the cluster. + var buildablesOfCluster = new List(); + + // Pick a random structure (floor, pillar, wall, etc.) + var targetStructure = structuresToCluster[Random.Range(0, structuresToCluster.Count)]; + // Remove the picked structure from the toCluster list. + structuresToCluster.Remove(targetStructure); + // Add the picked structure to the final buildables of cluster list. + structuresOfCluster.Add(targetStructure); + + // Loop through buildablesOfCluster. Each element should only be checked against all others once. + for (var i = 0; i < structuresOfCluster.Count; i++) + { + // Get the element we are currently checking. + var s = structuresOfCluster[i]; + + // Check which of all the structures in the world we can add here. + var toAdd = structuresToCluster + .Where(k => (k.Position - s.Position).sqrMagnitude <= maxStructureDistance).ToList(); + // Add all those structures to the cluster. + structuresOfCluster.AddRange(toAdd); + // Remove all those structures from the main list. + structuresToCluster.RemoveAll(toAdd.Contains); + } + + // Barricades are simpler to cluster than structures. Barricades are only considered part of the cluster if there's a structure within range. + var barricadesToAdd = barricadesToCluster.Where(next => + structuresOfCluster.Exists(k => + (next.Position - k.Position).sqrMagnitude <= maxBarricadeDistance)) + .ToList(); + // Add all the barricades that are within range of one of the structures of this cluster. + buildablesOfCluster.AddRange(barricadesToAdd); + // Finally, remove all the barricades from the main list that we added to the cluster. + barricadesToCluster.RemoveAll(barricadesToAdd.Contains); + + // Combine all the buildables into one list. + buildablesOfCluster.AddRange(structuresOfCluster); + // Get or create a pooled cluster so we can define the cluster. + var cluster = baseClusterPool.GetOrCreatePooledCluster(); + // Add all the combined buildables to this cluster. + cluster.AddBuildables(buildablesOfCluster); + // Add this cluster to the output list. + output.Add(cluster); + + // Finally, check if we need logging, and if we are ready to log it. + currentCount += cluster.Buildables.Count; + if (!shouldLogProgress || !(currentCount / logRate > currentMultiplier)) continue; + + currentMultiplier++; + var currentProgress = Math.Ceiling(currentCount / (double)totalBuildablesToCluster * 100); + LogManager.Information(string.Format(LoggingConstants.ClusterGenerationProgress, currentProgress, + currentCount, totalBuildablesToCluster, stopwatch.ElapsedMilliseconds)); + } + + // Once all the structures have been clustered, check if we have any remaining barricades that have not been clustered. + var remainingBarricadeCount = barricadesToCluster.Count; + if (remainingBarricadeCount > 0) + { + // If we do have barricades that have not been clustered, get or create a global cluster. + var globalCluster = GetOrCreateGlobalCluster(); + // And add all those barricades to that global cluster. + globalCluster.AddBuildables(barricadesToCluster); + } + + // Finally, we should make sure we are logging the 100% message with this check, should logging actually be needed. + + // This invert is dumb, as we still need to return output. All we are doing is adding a visually earlier return, which makes 0 sense to do. + // ReSharper disable once InvertIf + if (shouldLogProgress) + { + var finalBuildCount = output.Sum(static k => k.Buildables.Count) + remainingBarricadeCount; + var currentProgress = Math.Ceiling(finalBuildCount / (double)totalBuildablesToCluster * 100); + LogManager.Information(string.Format(LoggingConstants.ClusterGenerationProgress, currentProgress, + finalBuildCount, totalBuildablesToCluster, stopwatch.ElapsedMilliseconds)); + } + + return output; + } + + /// + public void RegenerateClusters() + { + GenerateAndLoadAllClusters(false); + } + + /// + public void Load() + { + if (Level.isLoaded) + LevelLoaded(0); + else + Level.onPostLevelLoaded += LevelLoaded; + } + + /// + public void Unload() + { + SaveManager.onPostSave -= Save; + EventBus.Unsubscribe((object)BuildablesTransformed); + EventBus.Unsubscribe((object)BuildablesDestroyed); + EventBus.Unsubscribe((object)BuildablesSpawned); + + Save(); + + foreach (var cluster in LoadedClusters.ToList()) + Unregister(cluster); + } + + /// + /// The method that executes once the full level has been loaded. + /// + protected virtual void LevelLoaded(int id) + { + EventBus.Subscribe((object)BuildablesSpawned); + EventBus.Subscribe((object)BuildablesDestroyed); + EventBus.Subscribe((object)BuildablesTransformed); + SaveManager.onPostSave += Save; + + GenerateAndLoadAllClusters(); + RocketModService.GetService().FillPool(); + } + + private void GenerateAndLoadAllClusters(bool loadSaveFile = true) + { + var stopwatch = Stopwatch.StartNew(); + + var buildableDirectory = RocketModService.GetService(); + LogManager.Information(string.Format(LoggingConstants.BuildablesLoaded, buildableDirectory.BuildableCount, + stopwatch.ElapsedMilliseconds)); + + var successfulLoad = false; + if (loadSaveFile && LevelSavedata.fileExists($"/{SaveConstants.SaveFileName}")) + successfulLoad = LoadClusters(buildableDirectory); + + if (!successfulLoad) + { + LogManager.Warning(LoggingConstants.ClustersGeneratingWarning); + LoadedClusters.AddRange(ClusterElements(buildableDirectory.GetBuildables(), true)); + } + + stopwatch.Stop(); + LogManager.Information(string.Format(LoggingConstants.ClustersLoaded, Clusters.Count, + stopwatch.ElapsedMilliseconds)); + + EventBus.Publish(); + } + + private void Save() + { + SaveFileLoader.SaveClusters(LoadedClusters); + } + + private bool LoadClusters(IBuildableDirectory buildableDirectory) + { + foreach (var cluster in LoadedClusters.ToList()) + Unregister(cluster); + + var stopwatch = Stopwatch.StartNew(); + + try + { + if (!SaveFileLoader.LoadClusters(stopwatch)) + return false; + + LoadedClusters.AddRange(SaveFileLoader.LoadedClusters); + GlobalCluster = LoadedClusters.OfType().FirstOrDefault(); + + if (Clusters.Count > 0) + { + var nextInstanceId = Clusters.Max(static k => k.InstanceId) + 1; + RocketModService.GetService().SetNextInstanceId(nextInstanceId); + } + + stopwatch.Stop(); + return true; + } + catch (Exception exception) + { + LogManager.Warning(string.Format(LoggingConstants.ClusterLoadException, exception)); + + foreach (var cluster in SaveFileLoader.LoadedClusters) + Unregister(cluster); + + return false; + } + } + + private void BuildablesSpawned(DelayedBuildablesSpawnedEventArguments arguments) + { + BuildablesSpawnedInternal(arguments.Buildables); + } + + private void BuildablesSpawnedInternal(List buildables) + { + var baseClusterPool = RocketModService.GetService(); + var globalCluster = GetOrCreateGlobalCluster(); + + foreach (var buildable in buildables) + { + // Planted buildables (on vehicles) will not currently be clustered, as they can move around the map without triggering events. + if (buildable.IsPlanted) return; + + // On spawning, check if it's a barricade + if (buildable is BarricadeBuildable) + { + // Find the best cluster for this barricade. + var bestCluster = FindBestCluster(buildable); + + // If we find a best cluster, add it to it. + if (bestCluster != null) + { + bestCluster.AddBuildable(buildable); + return; + } + + // If we don't, add it to the global cluster. + globalCluster.AddBuildable(buildable); + return; + } + + // Otherwise, if it's a structure, find all the clusters where it'd make a good target, and exclude any global clusters from the result. + var bestClusters = FindBestClusters(buildable).ToList(); + + switch (bestClusters.Count) + { + // If there's no results, create a new non-global cluster for this new base. + case 0: + var cluster = baseClusterPool.GetOrCreatePooledCluster(); + cluster.AddBuildable(buildable); + Register(cluster); + cluster.StealFromCluster(globalCluster); + return; + // If there's exactly 1 cluster found, simply add it to that cluster. + case 1: + cluster = bestClusters.First(); + cluster.AddBuildable(buildable); + cluster.StealFromCluster(globalCluster); + return; + + // However, if there's more than 1 cluster, select every single buildable from all found clusters. + default: + var allBuilds = bestClusters.SelectMany(static k => k.Buildables).ToList(); + + // Make sure to include the buildable we spawned in that set. + allBuilds.Add(buildable); + + // For all the found best clusters, we can now un-register them, as they are no longer needed. + foreach (var baseCluster in bestClusters) + Unregister(baseCluster); + + // And ask the clustering tool to generate new clusters, and populate the global cluster. + var newClusters = ClusterElements(allBuilds); + + // New clusters can be safely added now. + foreach (var baseCluster in newClusters) + { + Register(baseCluster); + baseCluster.StealFromCluster(baseCluster); + } + + return; + } + } + } + + private void BuildablesDestroyed(DelayedBuildablesDestroyedEventArguments arguments) + { + BuildablesDestroyedInternal(arguments.Buildables); + } + + internal void BuildablesDestroyedInternal(List buildables) + { + foreach (var cluster in Clusters.ToList()) + { + if (buildables.Count == 0) + return; + + cluster.RemoveBuildables(buildables); + } + } + + private void BuildablesTransformed(DelayedBuildablesTransformedEventArguments arguments) + { + var buildables = arguments.Buildables; + BuildablesDestroyedInternal(buildables); + BuildablesSpawnedInternal(buildables); + } + + private class LogConfiguration : ILoggerConfiguration + { + public List PipeSettings => + [ + new ConsolePipeConfiguration(), + new FilePipeConfiguration() + ]; + + private sealed class ConsolePipeConfiguration : DefaultConsolePipeConfiguration + { + public override byte MaxLogLevel => LogLevel.Debug.Level; + } + + private sealed class FilePipeConfiguration : DefaultFilePipeConfiguration + { + public override byte MaxLogLevel => LogLevel.Debug.Level; + } + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Interfaces/IBaseClusterDirectory.cs b/BaseClustering.API/BaseClusters/Directory/Interfaces/IBaseClusterDirectory.cs new file mode 100644 index 0000000..19ca9bf --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Interfaces/IBaseClusterDirectory.cs @@ -0,0 +1,102 @@ +extern alias JetBrainsAnnotations; +using System.Collections.Generic; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; +using UnityEngine; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; + +/// +/// A base cluster directory, which keeps track and handles all the s in the game. +/// +[PublicAPI] +public interface IBaseClusterDirectory +{ + /// + /// A of all the clusters that this directory is currently keeping + /// track of. + /// + public IReadOnlyCollection Clusters { get; } + + /// + /// Changes the stored cluster rules for new s to the specified one. + /// + /// The new cluster rules to use for all new s. + /// + /// This method should aim to also change 's Cluster Rules. + /// However, it might not, so if you are using this method, please target 's method too. + /// + public void ChangeClusterRules(IClusterRules clusterRules); + + /// + /// Registers a new cluster to this directory. + /// + /// The cluster that will be registered + public void Register(IBaseCluster? cluster); + + /// + /// Removes a cluster from this directory, and returns it to the . + /// + /// The cluster that will be removed + public void Unregister(IBaseCluster? cluster); + + /// + /// Gets or creates a global cluster for the game. + /// + /// An instance of a class that inherits from . + public IBaseCluster GetOrCreateGlobalCluster(); + + /// + /// Finds the best cluster for the to join. + /// + /// The that needs to find a cluster + /// An instance of a class that inherits from , or null if no cluster was found. + public IBaseCluster? FindBestCluster(Buildable buildable); + + /// + /// Finds the best clusters for the to join. + /// + /// The that will be used to find the best clusters + /// + /// An with the best s for the buildable. + /// If no best clusters are found, will be empty. + /// + public IEnumerable FindBestClusters(Buildable buildable); + + /// + /// Finds the best cluster within range of a specific position. + /// + /// The position to check within range. + /// + /// if no best cluster is available. + ///
+ /// An instance of if a best cluster is available. + ///
+ public IBaseCluster? FindBestCluster(Vector3 position); + + /// + /// Finds the best clusters within range of a specific position. + /// + /// The position to check within range. + /// + /// An with the best s for the buildable. + /// If no best clusters are found, will be empty. + /// + public IEnumerable FindBestClusters(Vector3 position); + + /// + /// Clusters the specified with the current registered ruleset. + /// + /// The s to cluster. + /// If the progress of this clustering should be logged. + /// A new with all the generated clusters. + public List ClusterElements(IEnumerable buildables, bool shouldLogProgress = false); + + /// + /// Requests that the regenerates all the clusters. + /// + public void RegenerateClusters(); +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Directory/Utilities/SaveFileLoader.cs b/BaseClustering.API/BaseClusters/Directory/Utilities/SaveFileLoader.cs new file mode 100644 index 0000000..de9e230 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Directory/Utilities/SaveFileLoader.cs @@ -0,0 +1,286 @@ +extern alias JetBrainsAnnotations; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Implementation; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Constants; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.FileSystem; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Abstraction; +using Pustalorc.Libraries.BuildableAbstractions.API.Buildables.Implementations; +using Pustalorc.Libraries.BuildableAbstractions.API.Directory.Interfaces; +using Pustalorc.Libraries.BuildableAbstractions.API.Directory.Utils; +using Pustalorc.Libraries.Logging.Manager; +using Pustalorc.Libraries.RocketModServices.Services; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Utilities; + +/// +/// A utility for Base Cluster Directories to load and save data. +/// +[PublicAPI] +public class SaveFileLoader +{ + /// + /// The version for the root data of the save file. + /// + protected virtual ushort SaveFileVersion { get; } + + /// + /// The version for the data of clusters in the save file. + /// + protected virtual ushort ClusterDataVersion { get; } + + /// + /// The version for the data of buildables in the save file. + /// + protected virtual ushort BuildableDataVersion { get; } + + /// + /// The path where the save file is located at. + /// + protected string SaveFilePath { get; } + + /// + /// A list of all the clusters that were loaded by this utility + /// + public List LoadedClusters { get; } + + /// + /// Constructs the utility with the bare minimum required. + /// + /// The path where the save file is located at. + public SaveFileLoader(string saveFilePath) + { + SaveFileVersion = 1; + ClusterDataVersion = 1; + BuildableDataVersion = 1; + SaveFilePath = saveFilePath; + LoadedClusters = new List(); + ServiceHelper.GetServiceOrUseDefault(); + } + + /// + /// Saves the specified clusters to the save file. + /// + /// The clusters to save to a save file. + /// A stopwatch for the logs to know how much time has passed since the start of the operation. + public virtual void SaveClusters(List clusters, Stopwatch? stopwatch = null) + { + stopwatch ??= new Stopwatch(); + if (!stopwatch.IsRunning) + { + if (stopwatch.ElapsedMilliseconds > 0) + stopwatch.Reset(); + + stopwatch.Start(); + } + + var buildableDirectory = RocketModService.GetService(); + var river = new RiverExpandedUtility(SaveFilePath); + var clusterCount = clusters.Count; + var logRate = Math.Floor(clusterCount * 0.085); + + river.WriteUInt16(SaveFileVersion); + river.WriteInt32(buildableDirectory.BuildableCount); + river.WriteInt32(clusterCount); + + LogManager.Information(string.Format(LoggingConstants.ClusterSaveProgress, 0, 0, clusterCount, + stopwatch.ElapsedMilliseconds)); + for (var i = 0; i < clusterCount; i++) + { + var position = i + 1; + SaveBuildablesForCluster(river, clusters[i]); + + if (position % logRate != 0) + continue; + + var percentageCompleted = Math.Ceiling(position / (double)clusterCount * 100); + var logMessage = string.Format(LoggingConstants.ClusterSaveProgress, percentageCompleted, position, + clusterCount, stopwatch.ElapsedMilliseconds); + LogManager.Information(logMessage); + } + + LogManager.Information(string.Format(LoggingConstants.ClusterSaveProgress, 100, clusterCount, clusterCount, + stopwatch.ElapsedMilliseconds)); + + river.CloseRiver(); + stopwatch.Stop(); + } + + /// + /// Saves an and all its s. + /// + /// The river utility that is writing this save file. + /// The to save. + protected virtual void SaveBuildablesForCluster(RiverExpandedUtility river, IBaseCluster cluster) + { + river.WriteUInt16(ClusterDataVersion); + river.WriteUInt32(cluster.InstanceId); + river.WriteBoolean(cluster is GlobalBaseCluster); + river.WriteInt32(cluster.Buildables.Count); + + foreach (var buildable in cluster.Buildables) + SaveBuildable(river, buildable); + } + + /// + /// Saves a single . + /// + /// The river utility that is writing this save file. + /// The to save. + protected virtual void SaveBuildable(RiverExpandedUtility river, Buildable buildable) + { + river.WriteUInt16(BuildableDataVersion); + river.WriteUInt32(buildable.InstanceId); + river.WriteBoolean(buildable is StructureBuildable); + } + + /// + /// Loads all the clusters from the save file. + /// + /// A stopwatch for the logs to know how much time has passed since the start of the operation. + /// + /// if a buildable count mismatch happened, and therefore clusters should be rebuilt. + ///
+ /// otherwise. + ///
+ public virtual bool LoadClusters(Stopwatch? stopwatch = null) + { + stopwatch ??= new Stopwatch(); + if (!stopwatch.IsRunning) + { + if (stopwatch.ElapsedMilliseconds > 0) + stopwatch.Reset(); + + stopwatch.Start(); + } + + LoadedClusters.Clear(); + var buildableDirectory = RocketModService.GetService(); + var baseClusterPool = RocketModService.GetService(); + var river = new RiverExpandedUtility(SaveFilePath); + + // ReSharper disable once UnusedVariable + // To be used in the future. + var saveFileVersion = river.ReadUInt16(); + + var buildableCount = river.ReadInt32(); + + if (buildableDirectory.BuildableCount != buildableCount) + { + LogManager.Debug(string.Format(LoggingConstants.BuildableCountMismatchDebug, + buildableDirectory.BuildableCount, buildableCount)); + LogManager.Warning(LoggingConstants.BuildableCountMismatch); + return false; + } + + var clusterCount = river.ReadInt32(); + var logRate = Math.Floor(clusterCount * 0.085); + + LogManager.Information(string.Format(LoggingConstants.ClusterLoadProgress, 0, 0, clusterCount, + stopwatch.ElapsedMilliseconds)); + + IBaseCluster? globalCluster = null; + for (var i = 0; i < clusterCount; i++) + { + var position = i + 1; + var success = LoadBuildablesForCluster(river, buildableDirectory, baseClusterPool, ref globalCluster); + + if (!success) + return false; + + if (position % logRate != 0) + continue; + + var percentageCompleted = Math.Ceiling(position / (double)clusterCount * 100); + var logMessage = string.Format(LoggingConstants.ClusterLoadProgress, percentageCompleted, position, + clusterCount, stopwatch.ElapsedMilliseconds); + LogManager.Information(logMessage); + } + + LogManager.Information(string.Format(LoggingConstants.ClusterLoadProgress, 100, clusterCount, clusterCount, + stopwatch.ElapsedMilliseconds)); + + return true; + } + + /// + /// Loads all the s and all the data for a . + /// + /// The river utility that is reading this save file. + /// The current that is being used for this process. + /// + /// The current to generate new s + /// with. + /// + /// The current global cluster. + /// + /// if a buildable count mismatch happened, and therefore clusters should be rebuilt. + ///
+ /// otherwise. + ///
+ protected virtual bool LoadBuildablesForCluster(RiverExpandedUtility river, IBuildableDirectory buildableDirectory, + IBaseClusterPool baseClusterPool, ref IBaseCluster? globalCluster) + { + var buildables = new List(); + // ReSharper disable once UnusedVariable + // To be used in the future. + var clusterDataVersion = river.ReadUInt16(); + var instanceId = river.ReadUInt32(); + var isGlobalCluster = river.ReadBoolean(); + + var buildCount = river.ReadInt32(); + for (var o = 0; o < buildCount; o++) + { + var build = LoadBuildable(river, buildableDirectory); + if (build == null) + return false; + + buildables.Add(build); + } + + if (isGlobalCluster && globalCluster != null) + { + globalCluster.AddBuildables(buildables); + return true; + } + + var cluster = baseClusterPool.CreateCluster(instanceId, isGlobalCluster); + cluster.AddBuildables(buildables); + LoadedClusters.Add(cluster); + + if (isGlobalCluster) + globalCluster = cluster; + + return true; + } + + /// + /// Loads a single . + /// + /// The river utility that is reading this save file. + /// The current that is being used for this process. + /// A if found, otherwise. + protected virtual Buildable? LoadBuildable(RiverExpandedUtility river, IBuildableDirectory buildableDirectory) + { + // ReSharper disable once UnusedVariable + // To be used in the future. + var buildableDataVersion = river.ReadUInt16(); + var buildInstanceId = river.ReadUInt32(); + var isStructure = river.ReadBoolean(); + var build = isStructure + ? (Buildable?)buildableDirectory.GetBuildable(buildInstanceId) + : buildableDirectory.GetBuildable(buildInstanceId); + + if (build != null) return build; + + var logMessage = string.Format(LoggingConstants.BuildNotFoundDuringLoad, buildInstanceId, isStructure); + LogManager.Warning(logMessage); + river.CloseRiver(); + + return build; + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Pool/Implementations/DefaultBaseClusterPool.cs b/BaseClustering.API/BaseClusters/Pool/Implementations/DefaultBaseClusterPool.cs new file mode 100644 index 0000000..0d1f3d0 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Pool/Implementations/DefaultBaseClusterPool.cs @@ -0,0 +1,88 @@ +extern alias JetBrainsAnnotations; +using System.Collections.Concurrent; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Implementation; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Implementations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Interfaces; +using Pustalorc.Libraries.RocketModServices.Services; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Implementations; + +/// +[PublicAPI] +public class DefaultBaseClusterPool : IBaseClusterPool +{ + /// + public uint NextInstanceId { get; private set; } + + private ConcurrentBag ClusterPool { get; } + private IClusterRules ClusterRules { get; set; } + + /// + /// Instantiates the default base cluster pool. + /// + public DefaultBaseClusterPool() + { + ClusterPool = new ConcurrentBag(); + ClusterRules = new DefaultClusterRules(); + NextInstanceId = 0; + } + + /// + public void ChangeClusterRules(IClusterRules clusterRules) + { + if (ClusterRules == clusterRules) + return; + + RocketModService.TryGetService()?.ChangeClusterRules(clusterRules); + ClusterRules = clusterRules; + } + + /// + public void FillPool(int limit = 25) + { + while (ClusterPool.Count < limit) + ClusterPool.Add(new DefaultBaseCluster(ClusterRules, NextInstanceId++)); + } + + /// + public bool Return(IBaseCluster? cluster) + { + if (cluster == null) + return false; + + cluster.Reset(); + + if (cluster is GlobalBaseCluster) + return false; + + ClusterPool.Add(cluster); + return true; + } + + /// + public void SetNextInstanceId(uint nextInstanceId) + { + NextInstanceId = nextInstanceId; + } + + /// + public IBaseCluster GetOrCreatePooledCluster(bool isGlobalCluster = false) + { + if (isGlobalCluster) + return CreateCluster(NextInstanceId++, true); + + return ClusterPool.TryTake(out var baseCluster) ? baseCluster : CreateCluster(NextInstanceId++); + } + + /// + public IBaseCluster CreateCluster(uint instanceId, bool globalCluster = false) + { + return globalCluster + ? new GlobalBaseCluster(ClusterRules, instanceId) + : new DefaultBaseCluster(ClusterRules, instanceId); + } +} \ No newline at end of file diff --git a/BaseClustering.API/BaseClusters/Pool/Interfaces/IBaseClusterPool.cs b/BaseClustering.API/BaseClusters/Pool/Interfaces/IBaseClusterPool.cs new file mode 100644 index 0000000..2951313 --- /dev/null +++ b/BaseClustering.API/BaseClusters/Pool/Interfaces/IBaseClusterPool.cs @@ -0,0 +1,77 @@ +extern alias JetBrainsAnnotations; +using JetBrainsAnnotations::JetBrains.Annotations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; + +namespace Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Interfaces; + +/// +/// An interface for the class managing a pool of s. +/// +[PublicAPI] +public interface IBaseClusterPool +{ + /// + /// The next instance id that this pool will assign, should a new be created. + /// + public uint NextInstanceId { get; } + + /// + /// Changes the stored cluster rules for new s to the specified one. + /// + /// The new cluster rules to use for all new s. + /// + /// This method might not change 's Cluster Rules. + /// If you are using this method, please target 's method too. + /// + public void ChangeClusterRules(IClusterRules clusterRules); + + /// + /// Fills the pool up to the specified limit + /// + public void FillPool(int limit = 25); + + /// + /// Returns and resets an to the pool. + /// + /// The to reset and return to the pool. + public bool Return(IBaseCluster? cluster); + + /// + /// Sets the next instance id for the next . + /// + /// The next instance id + /// + /// When creating your own pool, this method should prevent the nextInstanceId from being smaller than the + /// currently stored nextInstanceId. + /// + public void SetNextInstanceId(uint nextInstanceId); + + /// + /// Gets a from the pool. + ///
+ /// If a isn't available from the pool, a new instance will be created and provided. + ///
+ /// + /// If the generated should be set to be a global cluster. + /// + /// An instance of type . + /// + /// If is set to true, + /// a new will always be generated. + /// + public IBaseCluster GetOrCreatePooledCluster(bool isGlobalCluster = false); + + /// + /// Creates a new . + /// + /// The instance id to give to the generated . + /// If the cluster should be created as a global . + /// A new instance of . + /// + /// This method is exposed to skip the pool and assign a specific instanceId to the . + /// If you use this, you might end up with s with duplicate ids. + /// + public IBaseCluster CreateCluster(uint instanceId, bool globalCluster = false); +} \ No newline at end of file diff --git a/API/Utilities/RiverExpanded.cs b/BaseClustering.API/FileSystem/RiverExpandedUtility.cs similarity index 50% rename from API/Utilities/RiverExpanded.cs rename to BaseClustering.API/FileSystem/RiverExpandedUtility.cs index c9c5441..734e02d 100644 --- a/API/Utilities/RiverExpanded.cs +++ b/BaseClustering.API/FileSystem/RiverExpandedUtility.cs @@ -1,218 +1,252 @@ -using System; +extern alias JetBrainsAnnotations; +using System; using System.IO; using System.Text; -using JetBrains.Annotations; +using JetBrainsAnnotations::JetBrains.Annotations; using SDG.Unturned; using Steamworks; using UnityEngine; -namespace Pustalorc.Plugins.BaseClustering.API.Utilities; +namespace Pustalorc.Libraries.BaseClustering.API.FileSystem; /// -/// A modified class of that implements more types and isn't sealed, so other plugins can inherit and expand it with more. +/// A modified class of that implements more types and isn't sealed, so other plugins can inherit +/// and expand it with more. /// -[UsedImplicitly] -public class RiverExpanded +[PublicAPI] +public class RiverExpandedUtility { /// - /// Reads a from the stream. + /// The buffer to which all reads are performed to. /// - /// A . - [UsedImplicitly] + public byte[] Buffer { get; protected set; } = new byte[Block.BUFFER_SIZE]; + + /// + /// The number of bytes currently pending to be written and flushed. + /// + public int Water { get; protected set; } + + /// + /// The path to the file to read/write from/to. + /// + public string Path { get; } + + /// + /// The FileStream dealing with the file. + /// + public FileStream Stream { get; protected set; } + + /// + /// Creates a new instance of . + /// + /// The path of the file to which this object will read and write to. + /// If the path should be combined with . + public RiverExpandedUtility(string newPath, bool usePath = true) + { + Path = newPath; + if (usePath) + Path = ReadWrite.PATH + Path; + + var dir = System.IO.Path.GetDirectoryName(Path); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + Stream = new FileStream(Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + Water = 0; + } + + /// + /// Reads a from the stream. + /// + /// A . public double ReadDouble() { - Stream.Read(Buffer, 0, 8); + const int size = 8; + + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToDouble(Buffer, 0); } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteDouble(double value) { + const int size = 8; + var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 8); - Water += 8; + + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public string ReadString() { var count = Stream.ReadByte(); - Stream.Read(Buffer, 0, count); + _ = Stream.Read(Buffer, 0, count); return Encoding.UTF8.GetString(Buffer, 0, count); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public bool ReadBoolean() { return Stream.ReadByte() != 0; } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public byte ReadByte() { return (byte)Stream.ReadByte(); } /// - /// Reads multiple s from the stream. + /// Reads multiple s from the stream. /// - /// An of s. - [UsedImplicitly] + /// An of s. public byte[] ReadBytes() { var array = new byte[ReadUInt16()]; - Stream.Read(array, 0, array.Length); + _ = Stream.Read(array, 0, array.Length); return array; } /// - /// Reads an from the stream. + /// Reads an from the stream. /// - /// An . - [UsedImplicitly] + /// An . public short ReadInt16() { - Stream.Read(Buffer, 0, 2); + const int size = 2; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToInt16(Buffer, 0); } /// - /// Reads an from the stream. + /// Reads an from the stream. /// - /// An . - [UsedImplicitly] + /// An . public ushort ReadUInt16() { - Stream.Read(Buffer, 0, 2); + const int size = 2; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToUInt16(Buffer, 0); } /// - /// Reads an from the stream. + /// Reads an from the stream. /// - /// An . - [UsedImplicitly] + /// An . public int ReadInt32() { - Stream.Read(Buffer, 0, 4); + const int size = 4; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToInt32(Buffer, 0); } /// - /// Reads an from the stream. + /// Reads an from the stream. /// - /// An . - [UsedImplicitly] + /// An . public uint ReadUInt32() { - Stream.Read(Buffer, 0, 4); + const int size = 4; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToUInt32(Buffer, 0); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public float ReadSingle() { - Stream.Read(Buffer, 0, 4); + const int size = 4; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToSingle(Buffer, 0); } /// - /// Reads an from the stream. + /// Reads an from the stream. /// - /// An . - [UsedImplicitly] + /// An . public long ReadInt64() { - Stream.Read(Buffer, 0, 8); + const int size = 8; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToInt64(Buffer, 0); } /// - /// Reads an from the stream. + /// Reads an from the stream. /// - /// An . - [UsedImplicitly] + /// An . public ulong ReadUInt64() { - Stream.Read(Buffer, 0, 8); + const int size = 8; + _ = Stream.Read(Buffer, 0, size); return BitConverter.ToUInt64(Buffer, 0); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public CSteamID ReadSteamID() { return new CSteamID(ReadUInt64()); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public Vector3 ReadSingleVector3() { return new Vector3(ReadSingle(), ReadSingle(), ReadSingle()); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public Quaternion ReadSingleQuaternion() { return Quaternion.Euler(ReadSingle(), ReadSingle(), ReadSingle()); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public Color ReadColor() { return new Color(ReadByte() / 255f, ReadByte() / 255f, ReadByte() / 255f); } /// - /// Reads a from the stream. + /// Reads a from the stream. /// - /// A . - [UsedImplicitly] + /// A . public DateTime ReadDateTime() { return DateTime.FromBinary(ReadInt64()); } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteString(string value) { var bytes = Encoding.UTF8.GetBytes(value); @@ -223,10 +257,9 @@ public void WriteString(string value) } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteBoolean(bool value) { Stream.WriteByte((byte)(value ? 1 : 0)); @@ -234,10 +267,9 @@ public void WriteBoolean(bool value) } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteByte(byte value) { Stream.WriteByte(value); @@ -245,10 +277,9 @@ public void WriteByte(byte value) } /// - /// Writes an of s to the stream. + /// Writes an of s to the stream. /// - /// The of s to write. - [UsedImplicitly] + /// The of s to write. public void WriteBytes(byte[] values) { var num = (ushort)values.Length; @@ -258,104 +289,102 @@ public void WriteBytes(byte[] values) } /// - /// Writes an to the stream. + /// Writes an to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteInt16(short value) { + const int size = 2; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 2); - Water += 2; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes an to the stream. + /// Writes an to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteUInt16(ushort value) { + const int size = 2; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 2); - Water += 2; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes an to the stream. + /// Writes an to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteInt32(int value) { + const int size = 4; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 4); - Water += 4; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes an to the stream. + /// Writes an to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteUInt32(uint value) { + const int size = 4; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 4); - Water += 4; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteSingle(float value) { + const int size = 4; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 4); - Water += 4; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes an to the stream. + /// Writes an to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteInt64(long value) { + const int size = 8; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 8); - Water += 8; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes an to the stream. + /// Writes an to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteUInt64(ulong value) { + const int size = 8; var bytes = BitConverter.GetBytes(value); - Stream.Write(bytes, 0, 8); - Water += 8; + Stream.Write(bytes, 0, size); + Water += size; } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteSteamID(CSteamID steamId) { WriteUInt64(steamId.m_SteamID); } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteSingleVector3(Vector3 value) { WriteSingle(value.x); @@ -364,10 +393,9 @@ public void WriteSingleVector3(Vector3 value) } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteSingleQuaternion(Quaternion value) { var eulerAngles = value.eulerAngles; @@ -377,10 +405,9 @@ public void WriteSingleQuaternion(Quaternion value) } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteColor(Color value) { WriteByte((byte)(value.r * 255f)); @@ -389,19 +416,17 @@ public void WriteColor(Color value) } /// - /// Writes a to the stream. + /// Writes a to the stream. /// - /// The value to write. - [UsedImplicitly] + /// The value to write. public void WriteDateTime(DateTime value) { WriteInt64(value.ToBinary()); } /// - /// Closes and disposes of the stream. + /// Closes and disposes of the stream. /// - [UsedImplicitly] public void CloseRiver() { if (Water > 0) @@ -413,55 +438,12 @@ public void CloseRiver() } /// - /// Creates a new instance of . - /// - /// The path of the file to which this object will read and write to. - /// If the path should be combined with . - public RiverExpanded(string newPath, bool usePath = true) - { - Path = newPath; - if (usePath) - Path = ReadWrite.PATH + Path; - - var dir = System.IO.Path.GetDirectoryName(Path); - if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - Stream = new FileStream(Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); - Water = 0; - } - - /// - /// Reads s but does not interpret them in any way, essentially skipping them. + /// Reads s but does not interpret them in any way, essentially skipping + /// them. /// - /// The number of s that you wish to skip ahead. - [UsedImplicitly] + /// The number of s that you wish to skip ahead. public void Skip(int count) { - Stream.Read(Buffer, 0, count); + _ = Stream.Read(Buffer, 0, count); } - - /// - /// The buffer to which all reads are performed to. - /// - [UsedImplicitly] - public byte[] Buffer { get; protected set; } = new byte[Block.BUFFER_SIZE]; - - /// - /// The number of bytes currently pending to be written and flushed. - /// - [UsedImplicitly] - public int Water { get; protected set; } - - /// - /// The path to the file to read/write from/to. - /// - [UsedImplicitly] - public string Path { get; } - - /// - /// The FileStream dealing with the file. - /// - [UsedImplicitly] - public FileStream Stream { get; protected set; } } \ No newline at end of file diff --git a/BaseClustering.API/Utilities/AverageCenterExtensions.cs b/BaseClustering.API/Utilities/AverageCenterExtensions.cs new file mode 100644 index 0000000..4058237 --- /dev/null +++ b/BaseClustering.API/Utilities/AverageCenterExtensions.cs @@ -0,0 +1,67 @@ +extern alias JetBrainsAnnotations; +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrainsAnnotations::JetBrains.Annotations; +using UnityEngine; + +namespace Pustalorc.Libraries.BaseClustering.API.Utilities; + +/// +/// A class with extensions to calculate the average center. +/// +[PublicAPI] +public static class AverageCenterExtensions +{ + /// + /// Calculates the average center of an . + /// + /// The to get the average center from. + /// + /// A that is the average center of . + /// + /// + /// If is null, then this exception is thrown, as should never be + /// null. + /// + public static Vector3 AverageCenter(this IEnumerable source) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + + var list = source.ToList(); + + var sum = Vector3.zero; + + checked + { + sum = list.Aggregate(sum, static (current, element) => current + element); + } + + if (list.Count > 0) return sum / list.Count; + + return Vector3.zero; + } + + /// + /// Calculates the average center of an . + /// + /// The to get the average center from. + /// + /// The way that the should be selected to convert it to an + /// . + /// + /// A type that can select a . + /// + /// A that is the average center of after applying a + /// . + /// + /// + /// This method calls , which only takes an . + ///
+ /// Therefore, any input for this average center should support a selector to a . + ///
+ public static Vector3 AverageCenter(this IEnumerable source, Func selector) + { + return source.Select(selector).AverageCenter(); + } +} \ No newline at end of file diff --git a/BaseClustering.csproj b/BaseClustering.csproj deleted file mode 100644 index bcb2b98..0000000 --- a/BaseClustering.csproj +++ /dev/null @@ -1,71 +0,0 @@ - - - Pustalorc.Plugins.BaseClustering - 0BaseClustering - net481 - 11 - enable - BaseClustering - Pustalorc - BaseClustering - Copyright © Pustalorc 2020-2023 - 2.1.3 - 2.1.3 - bin\$(Configuration)\ - true - 2.1.3 - BaseClustering - Pustalorc - Unturned Plugin to cluster/group Buildables & Structures, which allows to define what a player base is. - https://github.com/Pustalorc/BaseClustering/ - LGPL-3.0-or-later - Github - Pustalorc.BaseClustering - Push project to nuget - true - - - full - - - portable - bin\$(Configuration)\BaseClustering.xml - - - - - - - libs\Assembly-CSharp.dll - False - - - libs\com.rlabrecque.steamworks.net.dll - False - - - libs\Rocket.API.dll - False - - - libs\Rocket.Core.dll - False - - - libs\Rocket.Unturned.dll - False - - - libs\UnityEngine.dll - False - - - libs\UnityEngine.CoreModule.dll - False - - - libs\UnityEngine.PhysicsModule.dll - False - - - \ No newline at end of file diff --git a/BaseClustering.sln b/BaseClustering.sln index 4193018..caf6b2f 100644 --- a/BaseClustering.sln +++ b/BaseClustering.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseClustering", "BaseClustering.csproj", "{D64124CB-7B6E-4334-AB68-857848E03858}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseClustering", "BaseClustering\BaseClustering.csproj", "{D64124CB-7B6E-4334-AB68-857848E03858}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseClustering.API", "BaseClustering.API\BaseClustering.API.csproj", "{16F0259A-3E22-429A-BC76-311CBDF39F3E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +14,9 @@ Global {D64124CB-7B6E-4334-AB68-857848E03858}.Debug|Any CPU.Build.0 = Debug|Any CPU {D64124CB-7B6E-4334-AB68-857848E03858}.Release|Any CPU.ActiveCfg = Release|Any CPU {D64124CB-7B6E-4334-AB68-857848E03858}.Release|Any CPU.Build.0 = Release|Any CPU + {16F0259A-3E22-429A-BC76-311CBDF39F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16F0259A-3E22-429A-BC76-311CBDF39F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16F0259A-3E22-429A-BC76-311CBDF39F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16F0259A-3E22-429A-BC76-311CBDF39F3E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/BaseClustering/BaseClustering.csproj b/BaseClustering/BaseClustering.csproj new file mode 100644 index 0000000..895f8a6 --- /dev/null +++ b/BaseClustering/BaseClustering.csproj @@ -0,0 +1,44 @@ + + + net481 + enable + BaseClustering + BaseClustering + BaseClustering + Pustalorc.Plugins.BaseClustering + 12 + Pustalorc + Pustalorc + Copyright © Pustalorc 2020-2024 + 3.0.0 + 3.0.1 + bin\$(Configuration)\ + true + + + + + + + + + + + + ..\libs\Rocket.API.dll + False + + + ..\libs\Rocket.Core.dll + False + + + ..\libs\Rocket.Unturned.dll + False + + + + + + + \ No newline at end of file diff --git a/BaseClustering/BaseClusteringPlugin.cs b/BaseClustering/BaseClusteringPlugin.cs new file mode 100644 index 0000000..9ec300d --- /dev/null +++ b/BaseClustering/BaseClusteringPlugin.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Implementations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Implementations; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Pool.Interfaces; +using Pustalorc.Libraries.Logging.Manager; +using Pustalorc.Libraries.RocketModCommandsExtended.Abstractions; +using Pustalorc.Libraries.RocketModCommandsExtended.Extensions; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Plugins.BaseClustering.Commands.Actions; +using Pustalorc.Plugins.BaseClustering.Commands.Information; +using Pustalorc.Plugins.BaseClustering.Commands.Wreck; +using Pustalorc.Plugins.BaseClustering.Config; +using Pustalorc.Plugins.BaseClustering.Constants; +using Rocket.Core.Plugins; +using SDG.Unturned; + +namespace Pustalorc.Plugins.BaseClustering; + +/// +public sealed class BaseClusteringPlugin : RocketPlugin +{ + private List Commands { get; } + + /// + public BaseClusteringPlugin() + { + var translations = this.GetCurrentTranslationsForCommands(); + + Commands = + [ + new ClustersRegenCommand(translations), + new TeleportToClusterCommand(translations), + new FindClustersCommand(translations), + new TopClustersCommand(translations), + new WreckClustersCommand(translations) + ]; + + Commands.LoadAndRegisterCommands(this); + } + + /// + protected override void Load() + { + LogManager.UpdateConfiguration(Configuration.Instance); + + if (Level.isLoaded) + OnLevelLoaded(0); + else + Level.onPrePreLevelLoaded += OnLevelLoaded; + + Provider.onCommenceShutdown += SaveManager.save; + Commands.ReloadCommands(this); + + LogManager.Information(LoggingConstants.PluginLoaded); + } + + /// + protected override void Unload() + { + Provider.onCommenceShutdown -= SaveManager.save; + Level.onPrePreLevelLoaded -= OnLevelLoaded; + + RocketModService.UnregisterService(); + RocketModService.UnregisterService(); + + LogManager.Information(LoggingConstants.PluginUnloaded); + } + + private void OnLevelLoaded(int level) + { + LogManager.Debug(LoggingConstants.LevelLoaded); + if (RocketModService.TryGetService() == null) + { + LogManager.Debug(LoggingConstants.NoIBaseClusterPoolService); + RocketModService.RegisterService(new DefaultBaseClusterPool()); + LogManager.Information(LoggingConstants.DefaultBaseClusterPoolRegistered); + } + + LogManager.Debug(LoggingConstants.UpdateIBaseClusterPoolConfig); + RocketModService.GetService().ChangeClusterRules(Configuration.Instance); + + LogManager.Debug(LoggingConstants.CheckIBaseClusterDirectoryService); + if (RocketModService.TryGetService() == null) + { + LogManager.Debug(LoggingConstants.NoIBaseClusterDirectoryService); + RocketModService.RegisterService(new DefaultBaseClusterDirectory()); + LogManager.Information(LoggingConstants.DefaultBaseClusterDirectoryRegistered); + } + + LogManager.Debug(LoggingConstants.UpdateIBaseClusterDirectoryConfig); + RocketModService.GetService().ChangeClusterRules(Configuration.Instance); + + LogManager.Information(LoggingConstants.LoadingFinished); + } +} \ No newline at end of file diff --git a/BaseClustering/Commands/Actions/ClustersRegenCommand.cs b/BaseClustering/Commands/Actions/ClustersRegenCommand.cs new file mode 100644 index 0000000..58750ad --- /dev/null +++ b/BaseClustering/Commands/Actions/ClustersRegenCommand.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.RocketModCommandsExtended.Abstractions; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Plugins.BaseClustering.Commands.Constants; +using Rocket.API; + +namespace Pustalorc.Plugins.BaseClustering.Commands.Actions; + +internal sealed class ClustersRegenCommand(Dictionary translations) + : RocketCommandWithTranslations(false, translations) +{ + public override AllowedCaller AllowedCaller => AllowedCaller.Both; + + public override string Name => "clustersRegen"; + + public override string Help => "Regenerates all clusters from scratch."; + + public override string Syntax => ""; + + public override Dictionary DefaultTranslations => new() + { + { TranslationKeys.CommandExceptionKey, CommandTranslationConstants.CommandExceptionValue }, + { CommandTranslationConstants.ClustersRegenWarningKey, CommandTranslationConstants.ClustersRegenWarningValue } + }; + + public override Task ExecuteAsync(IRocketPlayer caller, string[] command) + { + var clusterDirectory = RocketModService.GetService(); + SendTranslatedMessage(caller, CommandTranslationConstants.ClustersRegenWarningKey); + clusterDirectory.RegenerateClusters(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/BaseClustering/Commands/Actions/TeleportToClusterCommand.cs b/BaseClustering/Commands/Actions/TeleportToClusterCommand.cs new file mode 100644 index 0000000..c9a622c --- /dev/null +++ b/BaseClustering/Commands/Actions/TeleportToClusterCommand.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.RocketModCommandsExtended.Abstractions; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Plugins.BaseClustering.Commands.Constants; +using Pustalorc.Plugins.BaseClustering.Commands.Extensions; +using Rocket.API; +using Rocket.Unturned.Player; +using UnityEngine; + +namespace Pustalorc.Plugins.BaseClustering.Commands.Actions; + +internal sealed class TeleportToClusterCommand(Dictionary translations) + : RocketCommandWithTranslations(true, translations) +{ + public override AllowedCaller AllowedCaller => AllowedCaller.Player; + + public override string Name => "teleportToCluster"; + + public override string Help => "Teleports you to a random cluster on the map based on filters."; + + public override string Syntax => "[player]"; + + public override List Aliases => ["tpc"]; + + public override Dictionary DefaultTranslations => new() + { + { TranslationKeys.CommandExceptionKey, CommandTranslationConstants.CommandExceptionValue }, + { CommandTranslationConstants.NotAvailableKey, CommandTranslationConstants.NotAvailableValue }, + { + CommandTranslationConstants.CannotTeleportNoClustersKey, + CommandTranslationConstants.CannotTeleportNoClustersValue + } + }; + + public override Task ExecuteAsync(IRocketPlayer caller, string[] command) + { + if (caller is not UnturnedPlayer player) + return Task.CompletedTask; + + var clusterDirectory = RocketModService.GetService(); + var args = command.ToList(); + var targetName = Translate(CommandTranslationConstants.NotAvailableKey); + + var target = args.GetIRocketPlayer(out var index); + if (index > -1) + args.RemoveAt(index); + + var clusters = clusterDirectory.Clusters.AsEnumerable(); + + if (target != null && ulong.TryParse(target.Id, out var targetId)) + { + targetName = target.DisplayName; + clusters = clusters.Where(cluster => cluster.Owner == targetId); + } + + var clusterList = clusters.Where(static k => k.Center != Vector3.zero).ToList(); + if (!clusterList.Any()) + { + SendTranslatedMessage(caller, CommandTranslationConstants.CannotTeleportNoClustersKey, targetName); + return Task.CompletedTask; + } + + var cluster = clusterList[Random.Range(0, clusterList.Count - 1)]; + + if (cluster == null) + { + SendTranslatedMessage(caller, CommandTranslationConstants.CannotTeleportNoClustersKey, targetName); + return Task.CompletedTask; + } + + var offset = new Vector3(0, 4, 0); + + while (!player.Player.stance.wouldHaveHeightClearanceAtPosition(cluster.Center + offset, + 0.5f)) + offset.y++; + + player.Teleport(cluster.Center + offset, player.Rotation); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/BaseClustering/Commands/Constants/CommandTranslationConstants.cs b/BaseClustering/Commands/Constants/CommandTranslationConstants.cs new file mode 100644 index 0000000..0641f49 --- /dev/null +++ b/BaseClustering/Commands/Constants/CommandTranslationConstants.cs @@ -0,0 +1,51 @@ +namespace Pustalorc.Plugins.BaseClustering.Commands.Constants; + +internal static class CommandTranslationConstants +{ + public const string CommandExceptionValue = + "An issue occurred during command execution of /{0} {1}. Error message: {2}. Stack trace: {3}"; + + public const string ClustersRegenWarningKey = "clusters_regen_warning"; + + public const string ClustersRegenWarningValue = + "WARNING! This operation can take a long amount of time! The more buildables in the map the longer it will take! Please see console for when this operation is completed."; + + public const string NotAvailableKey = "not_available"; + public const string NotAvailableValue = "N/A"; + public const string CannotTeleportNoClustersKey = "cannot_teleport_no_clusters"; + + public const string CannotTeleportNoClustersValue = + "Cannot teleport anywhere, no clusters found with the following filters. Player: {0}"; + + public const string NotEnoughArgumentsKey = "not_enough_args"; + public const string NotEnoughArgumentsValue = "You need more arguments to use this command."; + public const string CannotBeExecutedFromConsoleKey = "cannot_be_executed_from_console"; + + public const string CannotBeExecutedFromConsoleValue = + "That command cannot be executed from console with those arguments."; + + public const string ClusterCountKey = "cluster_count"; + + public const string ClusterCountValue = + "There are a total of {0} clusters. Specific Item: {1}, Radius: {2}, Player: {3}"; + + public const string TopClusterFormatKey = "top_cluster_format"; + public const string TopClusterFormatValue = "At number {0}, {1} with {2} clusters!"; + public const string ActionCancelledKey = "action_cancelled"; + public const string ActionCancelledValue = "The wreck action was cancelled."; + public const string NoActionQueuedKey = "no_action_queued"; + public const string NoActionQueuedValue = "There is no wreck action queued."; + public const string CannotWreckNoClustersKey = "cannot_wreck_no_clusters"; + public const string CannotWreckNoClustersValue = "There are no clusters selected, so nothing can be wrecked."; + public const string WreckedClustersKey = "wrecked_clusters"; + public const string WreckedClustersValue = "Wrecked {0} clusters. Specific Item: {1}, Radius: {2}, Player: {3}"; + public const string WreckedClustersActionQueuedKey = "wreck_clusters_action_queued"; + + public const string WreckedClustersActionQueuedValue = + "Queued a wreck clusters action for {3} clusters. Confirm with /wc confirm. Player: {0}, Specific Item: {1}, Radius: {2}."; + + public const string WreckedClustersActionQueuedNewKey = "wreck_clusters_action_queued_new"; + + public const string WreckedClustersActionQueuedNewValue = + "Discarded previous queued action and queued a new wreck clusters action for {3} clusters. Confirm with /wc confirm. Player: {0}, Specific Item: {1}, Radius: {2}."; +} \ No newline at end of file diff --git a/BaseClustering/Commands/Extensions/CommandExtensions.cs b/BaseClustering/Commands/Extensions/CommandExtensions.cs new file mode 100644 index 0000000..1b341cb --- /dev/null +++ b/BaseClustering/Commands/Extensions/CommandExtensions.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Rocket.API; +using Rocket.Unturned.Player; +using SDG.Unturned; +using UnityEngine; + +namespace Pustalorc.Plugins.BaseClustering.Commands.Extensions; + +internal static class CommandExtensions +{ + /// + /// Checks if any element from is equal to the + /// . + /// + /// The that should be searched. + /// The that we should find. + /// + /// If is found in , this will be a number greater than -1 but + /// smaller than args.Count + ///
+ /// If isn't found in , this will be -1. + /// + /// + /// if is > -1. + ///
+ /// if is == -1. + ///
+ public static bool CheckArgsIncludeString(this IEnumerable args, string include, out int index) + { + index = args.ToList().FindIndex(k => k.Equals(include, StringComparison.OrdinalIgnoreCase)); + return index > -1; + } + + /// + /// Gets all the of s that an element of . + /// + /// The that should be searched. + /// + /// If any element of can be an (s), this will be a number greater + /// than -1 but smaller than args.Count + ///
+ /// If no elements of can be an (s), this will be -1. + /// + /// + /// An empty if no element in can be an + /// . + ///
+ /// A with all the s from one of the entries in + /// . + ///
+ public static List GetMultipleItemAssets(this IEnumerable args, out int index) + { + var argsL = args.ToList(); +#pragma warning disable CS0618 // Type or member is obsolete +// Hey Nelson? How about NO. You return me a List. Stop asking me to give you a list. + var assets = Assets.find(EAssetType.ITEM).Cast() +#pragma warning restore CS0618 // Type or member is obsolete + .Where(static k => k is { itemName: not null, name: not null }).OrderBy(static k => k.itemName.Length) + .ToList(); + + for (index = 0; index < argsL.Count; index++) + { + var itemAssets = assets.Where(k => + argsL[0].Equals(k.id.ToString(), StringComparison.OrdinalIgnoreCase) || + argsL[0].Split(' ').All(l => k.itemName.ToLower().Contains(l)) || + argsL[0].Split(' ').All(l => k.name.ToLower().Contains(l))).ToList(); + + if (itemAssets.Count <= 0) + continue; + + return itemAssets; + } + + index = -1; + return new List(); + } + + /// + /// Gets a from an element in . + /// + /// The that should be searched. + /// + /// If any element of is a valid , this will be a number greater than -1 + /// but smaller than args.Count + ///
+ /// If no elements of is a valid , this will be -1. + /// + /// + /// A from one of the entries in . + /// + public static float GetFloat(this IEnumerable args, out int index) + { + var output = float.NegativeInfinity; + index = args.ToList().FindIndex(k => float.TryParse(k, out output)); + return output; + } + + /// + /// Gets a from an element in . + /// + /// The that should be searched. + /// + /// If any element of is a valid , this will be a number greater + /// than -1 but smaller than args.Count + ///
+ /// If no elements of is a valid , this will be -1. + /// + /// + /// if none of the entries in can be an . + ///
+ /// A from one of the entries in . + ///
+ public static IRocketPlayer? GetIRocketPlayer(this IEnumerable args, out int index) + { + IRocketPlayer? output = null; + index = args.ToList().FindIndex(k => + { + output = UnturnedPlayer.FromName(k); + if (output == null && ulong.TryParse(k, out var id) && id > 76561197960265728) + output = new RocketPlayer(id.ToString()); + + return output != null; + }); + return output; + } + + /// + /// Compares a to see if it is a , as default comparison + /// doesn't correctly check it. + /// + /// The to compare to negative infinity. + /// + /// if has any component. + ///
+ /// if has no component. + ///
+ public static bool IsNegativeInfinity(this Vector3 vector) + { + return float.IsNegativeInfinity(vector.x) || float.IsNegativeInfinity(vector.y) || + float.IsNegativeInfinity(vector.z); + } +} \ No newline at end of file diff --git a/BaseClustering/Commands/Information/FindClustersCommand.cs b/BaseClustering/Commands/Information/FindClustersCommand.cs new file mode 100644 index 0000000..e6c8727 --- /dev/null +++ b/BaseClustering/Commands/Information/FindClustersCommand.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.RocketModCommandsExtended.Abstractions; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Plugins.BaseClustering.Commands.Constants; +using Pustalorc.Plugins.BaseClustering.Commands.Extensions; +using Rocket.API; +using Rocket.Unturned.Player; +using UnityEngine; + +namespace Pustalorc.Plugins.BaseClustering.Commands.Information; + +internal sealed class FindClustersCommand(Dictionary translations) + : RocketCommandWithTranslations(true, translations) +{ + public override AllowedCaller AllowedCaller => AllowedCaller.Both; + public override string Name => "findClusters"; + public override string Help => "Finds clusters around the map"; + public override string Syntax => " [id] [radius] | [id] [radius]"; + public override List Aliases => ["fc"]; + + public override Dictionary DefaultTranslations => new() + { + { TranslationKeys.CommandExceptionKey, CommandTranslationConstants.CommandExceptionValue }, + { CommandTranslationConstants.NotAvailableKey, CommandTranslationConstants.NotAvailableValue }, + { + CommandTranslationConstants.CannotBeExecutedFromConsoleKey, + CommandTranslationConstants.CannotBeExecutedFromConsoleValue + }, + { CommandTranslationConstants.ClusterCountKey, CommandTranslationConstants.ClusterCountValue } + }; + + public override Task ExecuteAsync(IRocketPlayer caller, string[] command) + { + var clusterDirectory = RocketModService.GetService(); + var args = command.ToList(); + var notAvailable = Translate(CommandTranslationConstants.NotAvailableKey); + + var target = args.GetIRocketPlayer(out var index); + if (index > -1) + args.RemoveAt(index); + + var itemAssetInput = notAvailable; + var itemAssets = args.GetMultipleItemAssets(out index); + var assetCount = itemAssets.Count; + if (index > -1) + { + itemAssetInput = args[index]; + args.RemoveAt(index); + } + + var radius = args.GetFloat(out index); + if (index > -1) + args.RemoveAt(index); + + var clusters = clusterDirectory.Clusters.AsEnumerable(); + var targetName = notAvailable; + + if (target != null && ulong.TryParse(target.Id, out var targetId)) + { + targetName = target.DisplayName; + clusters = clusters.Where(cluster => cluster.Owner == targetId); + } + + if (assetCount > 0) + clusters = clusters.Where(k => k.Buildables.Any(l => itemAssets.Exists(z => l.AssetId == z.id))); + + var radiusText = notAvailable; + + if (!float.IsNegativeInfinity(radius)) + { + if (caller is not UnturnedPlayer cPlayer) + { + SendTranslatedMessage(caller, CommandTranslationConstants.CannotBeExecutedFromConsoleKey); + return Task.CompletedTask; + } + + radiusText = radius.ToString(CultureInfo.CurrentCulture); + clusters = clusters.Where(k => + k.Buildables.Any(l => (l.Position - cPlayer.Position).sqrMagnitude <= Mathf.Pow(radius, 2))); + } + + var itemAssetName = assetCount switch + { + 1 => itemAssets.First().itemName, + > 1 => itemAssetInput, + _ => notAvailable + }; + + SendTranslatedMessage(caller, CommandTranslationConstants.ClusterCountKey, clusters.Count(), itemAssetName, + radiusText, targetName); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/BaseClustering/Commands/Information/TopClustersCommand.cs b/BaseClustering/Commands/Information/TopClustersCommand.cs new file mode 100644 index 0000000..4efc4d7 --- /dev/null +++ b/BaseClustering/Commands/Information/TopClustersCommand.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.RocketModCommandsExtended.Abstractions; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Plugins.BaseClustering.Commands.Constants; +using Rocket.API; + +namespace Pustalorc.Plugins.BaseClustering.Commands.Information; + +internal sealed class TopClustersCommand(Dictionary translations) + : RocketCommandWithTranslations(true, translations) +{ + public override AllowedCaller AllowedCaller => AllowedCaller.Both; + + public override string Name => "topClusters"; + + public override string Help => "Displays the top 5 clusters in the game."; + + public override string Syntax => ""; + + public override List Aliases => ["topC"]; + + public override Dictionary DefaultTranslations => new() + { + { TranslationKeys.CommandExceptionKey, CommandTranslationConstants.CommandExceptionValue }, + { CommandTranslationConstants.TopClusterFormatKey, CommandTranslationConstants.TopClusterFormatValue } + }; + + public override Task ExecuteAsync(IRocketPlayer caller, string[] command) + { + var clusterDirectory = RocketModService.GetService(); + var clusters = clusterDirectory.Clusters; + + var topClusters = clusters.GroupBy(static k => k.Owner).OrderByDescending(static k => k.Count()).Take(5) + .ToList(); + + for (var i = 0; i < topClusters.Count; i++) + { + var builder = topClusters.ElementAt(i); + + SendTranslatedMessage(caller, CommandTranslationConstants.TopClusterFormatKey, i + 1, builder.Key, + builder.Count()); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/API/WreckingActions/WreckClustersAction.cs b/BaseClustering/Commands/Wreck/WreckClustersAction.cs similarity index 50% rename from API/WreckingActions/WreckClustersAction.cs rename to BaseClustering/Commands/Wreck/WreckClustersAction.cs index 55350af..b35e8a2 100644 --- a/API/WreckingActions/WreckClustersAction.cs +++ b/BaseClustering/Commands/Wreck/WreckClustersAction.cs @@ -1,55 +1,55 @@ -using Rocket.API; +using System.Collections.Generic; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Clusters.Interfaces; +using Rocket.API; using SDG.Unturned; -using System.Collections.Generic; -using Pustalorc.Plugins.BaseClustering.API.BaseClusters; using UnityEngine; -namespace Pustalorc.Plugins.BaseClustering.API.WreckingActions; +namespace Pustalorc.Plugins.BaseClustering.Commands.Wreck; /// -/// A wreck action for a collection of s. -///
-/// Actions determine how the plugin will do a final search when performing a confirmed wreck. +/// A wreck action for a collection of s. +///
+/// Actions determine how the plugin will do a final search when performing a confirmed wreck. ///
-public readonly struct WreckClustersAction +internal readonly struct WreckClustersAction { /// - /// The player we might possibly be targeting that owns specific clusters. + /// The player we might possibly be targeting that owns specific clusters. /// public IRocketPlayer? TargetPlayer { get; } /// - /// The center position of the wreck action. - ///
- /// If no position is wanted, this value is to be set to . + /// The center position of the wreck action. + ///
+ /// If no position is wanted, this value is to be set to . ///
public Vector3 Center { get; } /// - /// A list of all s that will be targeted. + /// A list of all s that will be targeted. /// public List ItemAssets { get; } /// - /// The radius based on the Center specified in this object. + /// The radius based on the Center specified in this object. /// public float Radius { get; } /// - /// The name of the user input for item asset search, or the name of the only item asset used. + /// The name of the user input for item asset search, or the name of the only item asset used. /// public string ItemAssetName { get; } /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The player we might possibly be targeting that owns specific clusters. /// - /// The center position of the wreck action. - ///
- /// If no position is wanted, this value is to be set to . + /// The center position of the wreck action. + ///
+ /// If no position is wanted, this value is to be set to . /// - /// A list of all s that will be targeted. + /// A list of all s that will be targeted. /// The radius based on the Center specified in this object. /// The name of the user input for item asset search, or the name of the only item asset used. public WreckClustersAction(IRocketPlayer? target, Vector3 center, List assets, float radius, diff --git a/BaseClustering/Commands/Wreck/WreckClustersCommand.cs b/BaseClustering/Commands/Wreck/WreckClustersCommand.cs new file mode 100644 index 0000000..5879f3e --- /dev/null +++ b/BaseClustering/Commands/Wreck/WreckClustersCommand.cs @@ -0,0 +1,217 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Directory.Interfaces; +using Pustalorc.Libraries.RocketModCommandsExtended.Abstractions; +using Pustalorc.Libraries.RocketModServices.Services; +using Pustalorc.Plugins.BaseClustering.Commands.Constants; +using Pustalorc.Plugins.BaseClustering.Commands.Extensions; +using Rocket.API; +using Rocket.Unturned.Player; +using UnityEngine; + +namespace Pustalorc.Plugins.BaseClustering.Commands.Wreck; + +internal sealed class WreckClustersCommand(Dictionary translations) + : RocketCommandWithTranslations(true, translations) +{ + public override AllowedCaller AllowedCaller => AllowedCaller.Both; + + public override string Name => "wreckClusters"; + + public override string Help => "Destroys clusters from the map."; + + public override string Syntax => "confirm | abort | [player] [item] [radius]"; + + public override List Aliases => ["wc"]; + + public override Dictionary DefaultTranslations => new() + { + { TranslationKeys.CommandExceptionKey, CommandTranslationConstants.CommandExceptionValue }, + { CommandTranslationConstants.NotEnoughArgumentsKey, CommandTranslationConstants.NotEnoughArgumentsValue }, + { CommandTranslationConstants.NotAvailableKey, CommandTranslationConstants.NotAvailableValue }, + { + CommandTranslationConstants.CannotBeExecutedFromConsoleKey, + CommandTranslationConstants.CannotBeExecutedFromConsoleValue + }, + { CommandTranslationConstants.ActionCancelledKey, CommandTranslationConstants.ActionCancelledValue }, + { CommandTranslationConstants.NoActionQueuedKey, CommandTranslationConstants.NoActionQueuedValue }, + { + CommandTranslationConstants.CannotWreckNoClustersKey, CommandTranslationConstants.CannotWreckNoClustersValue + }, + { CommandTranslationConstants.WreckedClustersKey, CommandTranslationConstants.WreckedClustersValue }, + { + CommandTranslationConstants.WreckedClustersActionQueuedKey, + CommandTranslationConstants.WreckedClustersActionQueuedValue + }, + { + CommandTranslationConstants.WreckedClustersActionQueuedNewKey, + CommandTranslationConstants.WreckedClustersActionQueuedNewValue + } + }; + + private Dictionary WreckActions { get; } = new(); + + public override async Task ExecuteAsync(IRocketPlayer caller, string[] command) + { + var clusterDirectory = RocketModService.GetService(); + var args = command.ToList(); + + if (args.Count == 0) + { + SendTranslatedMessage(caller, CommandTranslationConstants.NotEnoughArgumentsKey); + return; + } + + if (args.CheckArgsIncludeString("abort", out var index)) + { + await Cancel(caller); + return; + } + + if (args.CheckArgsIncludeString("confirm", out index)) + { + await Confirm(caller); + return; + } + + var target = args.GetIRocketPlayer(out index); + if (index > -1) + args.RemoveAt(index); + + var notAvailable = Translate(CommandTranslationConstants.NotAvailableKey); + var itemAssetInput = notAvailable; + var itemAssets = args.GetMultipleItemAssets(out index); + var assetCount = itemAssets.Count; + if (index > -1) + { + itemAssetInput = args[index]; + args.RemoveAt(index); + } + + var radius = args.GetFloat(out index); + if (index > -1) + args.RemoveAt(index); + + var clusters = clusterDirectory.Clusters.AsEnumerable(); + + if (target != null && ulong.TryParse(target.Id, out var tId)) + clusters = clusters.Where(cluster => cluster.Owner == tId); + + if (assetCount > 0) + clusters = clusters.Where(k => k.Buildables.Any(l => itemAssets.Exists(z => l.AssetId == z.id))); + + var center = Vector3.negativeInfinity; + + if (!float.IsNegativeInfinity(radius)) + { + if (caller is not UnturnedPlayer cPlayer) + { + SendTranslatedMessage(caller, CommandTranslationConstants.CannotBeExecutedFromConsoleKey); + return; + } + + center = cPlayer.Position; + clusters = clusters.Where(k => + k.Buildables.Any(l => (l.Position - center).sqrMagnitude <= Mathf.Pow(radius, 2))); + } + + var count = clusters.Count(); + + if (count <= 0) + { + SendTranslatedMessage(caller, CommandTranslationConstants.CannotWreckNoClustersKey); + return; + } + + var itemAssetName = assetCount switch + { + 1 => itemAssets.First().itemName, + > 1 => itemAssetInput, + _ => notAvailable + }; + + var callerId = caller.Id; + var notAvailableTranslation = Translate(CommandTranslationConstants.NotAvailableKey); + if (WreckActions.TryGetValue(callerId, out _)) + { + WreckActions[callerId] = new WreckClustersAction(target, center, itemAssets, radius, itemAssetInput); + SendTranslatedMessage(caller, CommandTranslationConstants.WreckedClustersActionQueuedNewKey, + target?.DisplayName ?? notAvailableTranslation, itemAssetName, + !float.IsNegativeInfinity(radius) + ? radius.ToString(CultureInfo.CurrentCulture) + : notAvailableTranslation, count); + } + else + { + WreckActions.Add(callerId, new WreckClustersAction(target, center, itemAssets, radius, itemAssetInput)); + SendTranslatedMessage(caller, CommandTranslationConstants.WreckedClustersActionQueuedKey, + target?.DisplayName ?? notAvailableTranslation, itemAssetName, + !float.IsNegativeInfinity(radius) + ? radius.ToString(CultureInfo.CurrentCulture) + : notAvailableTranslation, count); + } + } + + private Task Cancel(IRocketPlayer caller) + { + if (WreckActions.Remove(caller.Id)) + { + SendTranslatedMessage(caller, CommandTranslationConstants.ActionCancelledKey); + return Task.CompletedTask; + } + + SendTranslatedMessage(caller, CommandTranslationConstants.NoActionQueuedKey); + return Task.CompletedTask; + } + + private Task Confirm(IRocketPlayer caller) + { + var callerId = caller.Id; + if (!WreckActions.TryGetValue(callerId, out var action)) + { + SendTranslatedMessage(caller, CommandTranslationConstants.NoActionQueuedKey); + return Task.CompletedTask; + } + + WreckActions.Remove(callerId); + var clusterDirectory = RocketModService.GetService(); + var notAvailable = Translate(CommandTranslationConstants.NotAvailableKey); + var targetName = notAvailable; + var radiusText = notAvailable; + + var clusters = clusterDirectory.Clusters.AsEnumerable(); + + if (action.TargetPlayer != null && ulong.TryParse(action.TargetPlayer.Id, out var targetId)) + { + targetName = action.TargetPlayer.DisplayName; + clusters = clusters.Where(cluster => cluster.Owner == targetId); + } + + if (!float.IsNegativeInfinity(action.Radius)) + radiusText = action.Radius.ToString(CultureInfo.CurrentCulture); + + if (action.ItemAssets.Count > 0) + clusters = clusters.Where(k => k.Buildables.Any(l => action.ItemAssets.Exists(z => l.AssetId == z.id))); + + if (!action.Center.IsNegativeInfinity()) + clusters = clusters.Where(k => + k.Buildables.Any(l => + (l.Position - action.Center).sqrMagnitude <= Mathf.Pow(action.Radius, 2))); + + var clusterList = clusters.ToList(); + if (!clusterList.Any()) + { + SendTranslatedMessage(caller, CommandTranslationConstants.CannotWreckNoClustersKey); + return Task.CompletedTask; + } + + foreach (var cluster in clusterList) + cluster.Destroy(); + + SendTranslatedMessage(caller, CommandTranslationConstants.WreckedClustersKey, clusterList.Count, + action.ItemAssetName, radiusText, targetName); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/BaseClustering/Config/BaseClusteringPluginConfiguration.cs b/BaseClustering/Config/BaseClusteringPluginConfiguration.cs new file mode 100644 index 0000000..80023fb --- /dev/null +++ b/BaseClustering/Config/BaseClusteringPluginConfiguration.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; +using Pustalorc.Libraries.BaseClustering.API.BaseClusters.Configuration.Interfaces; +using Pustalorc.Libraries.Logging.API.Loggers.Configuration; +using Pustalorc.Libraries.Logging.API.Pipes.Configuration; +using Rocket.API; + +namespace Pustalorc.Plugins.BaseClustering.Config; + +/// +/// +/// Configuration for the plugin when it comes to how it should operate and handle things. +/// +[Serializable] +public sealed class BaseClusteringPluginConfiguration : IRocketPluginConfiguration, IClusterRules, ILoggerConfiguration +{ + /// + public float MaxDistanceBetweenStructures { get; set; } + + /// + public float MaxDistanceToConsiderPartOfBase { get; set; } + + /// + [XmlIgnore] + public List PipeSettings => + [ + ConsoleLogSettings, + FileLogSettings + ]; + + /// + /// The log settings for the ConsolePipe + /// + public ConsolePipeConfiguration ConsoleLogSettings { get; set; } = new(); + + + /// + /// The log settings for the FilePipe + /// + public FilePipeConfiguration FileLogSettings { get; set; } = new(); + + /// + /// Loads the default values for the config. + /// + public void LoadDefaults() + { + MaxDistanceBetweenStructures = 6.1f; + MaxDistanceToConsiderPartOfBase = 10f; + ConsoleLogSettings = new ConsolePipeConfiguration(); + FileLogSettings = new FilePipeConfiguration(); + } +} \ No newline at end of file diff --git a/BaseClustering/Config/ConsolePipeConfiguration.cs b/BaseClustering/Config/ConsolePipeConfiguration.cs new file mode 100644 index 0000000..8e8111c --- /dev/null +++ b/BaseClustering/Config/ConsolePipeConfiguration.cs @@ -0,0 +1,25 @@ +using System; +using Pustalorc.Libraries.Logging.API.Pipes.Configuration; +using Pustalorc.Libraries.Logging.LogLevels; +using Pustalorc.Libraries.Logging.Pipes.Configuration; + +namespace Pustalorc.Plugins.BaseClustering.Config; + +/// +[Serializable] +public class ConsolePipeConfiguration : IPipeConfiguration +{ + /// + public byte MaxLogLevel { get; set; } = LogLevel.Debug.Level; + + /// + public string PipeName => Default.PipeName; + + /// + public string MessageFormat => Default.MessageFormat; + + /// + public byte MinLogLevel => Default.MinLogLevel; + + private DefaultConsolePipeConfiguration Default { get; } = new(); +} \ No newline at end of file diff --git a/BaseClustering/Config/FilePipeConfiguration.cs b/BaseClustering/Config/FilePipeConfiguration.cs new file mode 100644 index 0000000..bc3f2e5 --- /dev/null +++ b/BaseClustering/Config/FilePipeConfiguration.cs @@ -0,0 +1,28 @@ +using System; +using Pustalorc.Libraries.Logging.API.Pipes.Configuration; +using Pustalorc.Libraries.Logging.LogLevels; +using Pustalorc.Libraries.Logging.Pipes.Configuration; + +namespace Pustalorc.Plugins.BaseClustering.Config; + +/// +[Serializable] +public class FilePipeConfiguration : IFilePipeConfiguration +{ + /// + public byte MaxLogLevel { get; set; } = LogLevel.Debug.Level; + + /// + public string PipeName => Default.PipeName; + + /// + public string MessageFormat => Default.MessageFormat; + + /// + public byte MinLogLevel => Default.MinLogLevel; + + /// + public string FileNameFormat => Default.FileNameFormat; + + private DefaultFilePipeConfiguration Default { get; } = new(); +} \ No newline at end of file diff --git a/BaseClustering/Constants/LoggingConstants.cs b/BaseClustering/Constants/LoggingConstants.cs new file mode 100644 index 0000000..16a1078 --- /dev/null +++ b/BaseClustering/Constants/LoggingConstants.cs @@ -0,0 +1,34 @@ +namespace Pustalorc.Plugins.BaseClustering.Constants; + +internal static class LoggingConstants +{ + public const string PluginLoaded = + "Plugin has a deferred load. Please wait for the level to load and then confirm that the plugin loaded. Created by Pustalorc."; + + public const string PluginUnloaded = "Plugin has been unloaded. Created by Pustalorc."; + + public const string LevelLoaded = "Level has loaded, checking for an existing 'IBaseClusterPool' service..."; + + public const string NoIBaseClusterPoolService = + "No existing 'IBaseClusterPool' service found. Registering 'DefaultBaseClusterPool'..."; + + public const string DefaultBaseClusterPoolRegistered = + "Service 'DefaultBaseClusterPool' registered in 'IBaseClusterPool' successfully!"; + + public const string UpdateIBaseClusterPoolConfig = + "Updating registered 'IBaseClusterPool' service with the latest configuration..."; + + public const string CheckIBaseClusterDirectoryService = + "Checking for an existing 'IBaseClusterDirectory' service..."; + + public const string NoIBaseClusterDirectoryService = + "No existing 'IBaseClusterDirectory' service found. Registering and loading 'DefaultBaseClusterDirectory'..."; + + public const string DefaultBaseClusterDirectoryRegistered = + "Service 'DefaultBaseClusterDirectory' registered in 'IBaseClusterDirectory' and loaded successfully!"; + + public const string UpdateIBaseClusterDirectoryConfig = + "Updating registered 'IBaseClusterDirectory' service with the latest configuration..."; + + public const string LoadingFinished = "Plugin finished loading."; +} \ No newline at end of file diff --git a/BaseClusteringPlugin.cs b/BaseClusteringPlugin.cs deleted file mode 100644 index 0a68ec8..0000000 --- a/BaseClusteringPlugin.cs +++ /dev/null @@ -1,186 +0,0 @@ -using HarmonyLib; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.BaseClusters; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Delegates; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Pustalorc.Plugins.BaseClustering.Config; -using Rocket.API.Collections; -using Rocket.Core.Plugins; -using SDG.Unturned; - -namespace Pustalorc.Plugins.BaseClustering; - -/// -/// Main class for the Base Clustering Plugin. Handles instances of both and . -/// -public sealed class BaseClusteringPlugin : RocketPlugin -{ - /// - /// A singleton accessor for the plugin. - /// - public static BaseClusteringPlugin? Instance { get; private set; } - - /// - /// This event is only raised when the plugin has fully loaded. - ///
- /// To be exact, the plugin instantiates everything first on and then has the instances correctly initialize once the level has loaded completely. - ///
- [UsedImplicitly] - public static event VoidDelegate? OnPluginFullyLoaded; - - /// - /// Harmony instance that the plugin utilizes. - /// - private static Harmony? _harmony; - - /// - /// The main instance of type . - /// - public BuildableDirectory? BuildableDirectory { get; private set; } - - /// - /// The main instance of type . - /// - public BaseClusterDirectory? BaseClusterDirectory { get; private set; } - - /// - /// Gets the default translations that the plugin uses. - /// - public override TranslationList DefaultTranslations => new() - { - { - "command_fail_clustering_disabled", - "This command is disabled as the base clustering feature is disabled." - }, - { - "clusters_regen_warning", - "WARNING! This operation can take a long amount of time! The more buildables in the map the longer it will take! Please see console for when this operation is completed." - }, - { "not_available", "N/A" }, - { "cannot_be_executed_from_console", "That command cannot be executed from console with those arguments." }, - { - "build_count", - "There are a total of {0} builds. Specific Item: {1}, Radius: {2}, Player: {3}, Planted Barricades Included: {4}, Filter by Barricades: {5}, Filter by Structures: {6}" - }, - { "cluster_count", "There are a total of {0} clusters. Specific Item: {1}, Radius: {2}, Player: {3}" }, - { - "not_looking_buildable", "You are not looking at a structure/barricade, so you cannot get any info." - }, - { - "cannot_teleport_no_builds", - "Cannot teleport anywhere, no buildables found with the following filters. Specific Item: {0}, Player: {1}, Planted Barricades Included: {2}, Filter by Barricades: {3}, Filter by Structures: {4}" - }, - { - "cannot_teleport_builds_too_close", - "Cannot teleport anywhere, all buildables with the specified filters are too close. Specific Item: {0}, Player: {1}, Planted Barricades Included: {2}, Filter by Barricades: {3}, Filter by Structures: {4}" - }, - { - "cannot_teleport_no_clusters", - "Cannot teleport anywhere, no clusters found with the following filters. Player: {0}" - }, - { "top_builder_format", "At number {0}, {1} with {2} buildables!" }, - { "top_cluster_format", "At number {0}, {1} with {2} clusters!" }, - { "not_enough_args", "You need more arguments to use this command." }, - { "action_cancelled", "The wreck action was cancelled." }, - { "no_action_queued", "There is no wreck action queued." }, - { "cannot_wreck_no_clusters", "There are no clusters selected, so nothing can be wrecked." }, - { - "wrecked_clusters", - "Wrecked {0} clusters. Specific Item: {1}, Radius: {2}, Player: {3}" - }, - { - "wreck_clusters_action_queued", - "Queued a wreck clusters action for {3} clusters. Confirm with /wc confirm. Player: {0}, Specific Item: {1}, Radius: {2}." - }, - { - "wreck_clusters_action_queued_new", - "Discarded previous queued action and queued a new wreck clusters action for {3} clusters. Confirm with /wc confirm. Player: {0}, Specific Item: {1}, Radius: {2}." - }, - { "cannot_wreck_no_builds", "There are no buildables selected, so nothing can be wrecked." }, - { - "wrecked", - "Wrecked {0} buildables. Specific Item: {1}, Radius: {2}, Player: {3}, Planted Barricades Included: {4}, Filter by Barricades: {5}, Filter by Structures: {6}" - }, - { - "wreck_action_queued", - "Queued a wreck action for {6} buildables. Confirm with /w confirm. Specific Item: {0}, Radius: {1}, Player: {2}, Planted Barricades Included: {3}, Filter by Barricades: {4}, Filter by Structures: {5}" - }, - { - "wreck_action_queued_new", - "Discarded previous queued action and queued a new wreck action for {6} buildables. Confirm with /w confirm. Specific Item: {0}, Radius: {1}, Player: {2}, Planted Barricades Included: {3}, Filter by Barricades: {4}, Filter by Structures: {5}" - }, - { - "no_vehicle_found", - "Couldn't find a vehicle in the direction you're looking, or you are too far away from one. Maximum distance is 10 units." - }, - { - "vehicle_dead", - "The vehicle you are looking at is destroyed and cannot be wrecked. Please look at a vehicle that isn't destroyed." - }, - { - "vehicle_no_plant", - "The vehicle appears to have no assigned barricades to it, please make sure that it has barricades before asking to wreck them." - }, - { "vehicle_wreck", "Wrecked buildables from {0} [{1}]. Instance ID: {2}, Owner: {3}" } - }; - - /// - /// Loads and initializes the plugin. - /// - protected override void Load() - { - if (_harmony == null) - { - _harmony = new Harmony("com.pustalorc.baseClustering"); - _harmony.PatchAll(); - } - - BuildableDirectory = new BuildableDirectory(Configuration.Instance); - - if (Configuration.Instance.EnableClustering) - BaseClusterDirectory = new BaseClusterDirectory(this, Configuration.Instance, BuildableDirectory); - - if (Level.isLoaded) - OnLevelLoaded(0); - else - Level.onLevelLoaded += OnLevelLoaded; - - Provider.onCommenceShutdown += SaveManager.save; - - Instance = this; - Logging.PluginLoaded(this); - } - - /// - /// Unloads and de-initializes the plugin. - /// - protected override void Unload() - { - Instance = null; - - Provider.onCommenceShutdown -= SaveManager.save; - Level.onLevelLoaded -= OnLevelLoaded; - - if (BaseClusterDirectory != null) - { - BaseClusterDirectory.Unload(); - BaseClusterDirectory = null; - } - - if (BuildableDirectory != null) - { - BuildableDirectory.Unload(); - BuildableDirectory = null; - } - - Logging.PluginUnloaded(this); - } - - private void OnLevelLoaded(int level) - { - BuildableDirectory?.LevelLoaded(); - BaseClusterDirectory?.LevelLoaded(); - OnPluginFullyLoaded?.Invoke(); - } -} \ No newline at end of file diff --git a/Commands/ClustersRegenCommand.cs b/Commands/ClustersRegenCommand.cs deleted file mode 100644 index 03bcc32..0000000 --- a/Commands/ClustersRegenCommand.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Rocket.API; -using Rocket.Unturned.Chat; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class ClustersRegenCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Both; - - public string Name => "clustersregen"; - - public string Help => "Regenerates all clusters from scratch."; - - public string Syntax => ""; - - public List Aliases => new(); - - public List Permissions => new() { "clustersregen" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - var clusterDirectory = pluginInstance.BaseClusterDirectory; - if (clusterDirectory == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("command_fail_clustering_disabled")); - return; - } - - UnturnedChat.Say(caller, pluginInstance.Translate("clusters_regen_warning")); - clusterDirectory.GenerateAndLoadAllClusters(false); - } -} \ No newline at end of file diff --git a/Commands/FindBuildsCommand.cs b/Commands/FindBuildsCommand.cs deleted file mode 100644 index 40d8aa2..0000000 --- a/Commands/FindBuildsCommand.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using SDG.Unturned; -using UnityEngine; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class FindBuildsCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Both; - - public string Name => "findbuilds"; - - public string Help => "Finds buildables around the map"; - - public string Syntax => - "b [radius] | s [radius] | [id] [radius] | v [id] [radius] | [player] [id] [radius] | [player] b [radius] | [player] s [radius] | [player] v [id] [radius]"; - - public List Aliases => new() { "fb" }; - - public List Permissions => new() { "findbuilds" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - var args = command.ToList(); - - var barricades = args.CheckArgsIncludeString("b", out var index); - if (index > -1) - args.RemoveAt(index); - - var structs = args.CheckArgsIncludeString("s", out index); - if (index > -1) - args.RemoveAt(index); - - var plants = args.CheckArgsIncludeString("v", out index); - if (index > -1) - args.RemoveAt(index); - - var target = args.GetIRocketPlayer(out index); - if (index > -1) - args.RemoveAt(index); - - var itemAssetInput = pluginInstance.Translate("not_available"); - var itemAssets = args.GetMultipleItemAssets(out index); - var assetCount = itemAssets.Count; - if (index > -1) - { - itemAssetInput = args[index]; - args.RemoveAt(index); - } - - var radius = args.GetFloat(out index); - if (index > -1) - args.RemoveAt(index); - - var builds = BuildableDirectory.GetBuildables(includePlants: plants); - - if (target != null) builds = builds.Where(k => k.Owner.ToString().Equals(target.Id)); - - if (barricades) builds = builds.Where(k => k.Asset is ItemBarricadeAsset); - else if (structs) builds = builds.Where(k => k.Asset is ItemStructureAsset); - - if (assetCount > 0) builds = builds.Where(k => itemAssets.Exists(l => k.AssetId == l.id)); - - if (!float.IsNegativeInfinity(radius)) - { - if (caller is not UnturnedPlayer cPlayer) - { - UnturnedChat.Say(caller, pluginInstance.Translate("cannot_be_executed_from_console")); - return; - } - - builds = builds.Where(k => (k.Position - cPlayer.Position).sqrMagnitude <= Mathf.Pow(radius, 2)); - } - - var itemAssetName = pluginInstance.Translate("not_available"); - - switch (assetCount) - { - case 1: - itemAssetName = itemAssets.First().itemName; - break; - case > 1: - itemAssetName = itemAssetInput; - break; - } - - UnturnedChat.Say(caller, - pluginInstance.Translate("build_count", builds.Count(), itemAssetName, - !float.IsNegativeInfinity(radius) - ? radius.ToString(CultureInfo.CurrentCulture) - : pluginInstance.Translate("not_available"), - target != null ? target.DisplayName : pluginInstance.Translate("not_available"), plants, barricades, - structs)); - } -} \ No newline at end of file diff --git a/Commands/FindClustersCommand.cs b/Commands/FindClustersCommand.cs deleted file mode 100644 index 921f4a1..0000000 --- a/Commands/FindClustersCommand.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using UnityEngine; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class FindClustersCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Both; - public string Name => "findclusters"; - public string Help => "Finds clusters around the map"; - public string Syntax => " [id] [radius] | [id] [radius]"; - public List Aliases => new() { "fc" }; - public List Permissions => new() { "findclusters" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - var clusterDirectory = pluginInstance.BaseClusterDirectory; - if (clusterDirectory == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("command_fail_clustering_disabled")); - return; - } - - var args = command.ToList(); - - var target = args.GetIRocketPlayer(out var index); - if (index > -1) - args.RemoveAt(index); - - var itemAssetInput = pluginInstance.Translate("not_available"); - var itemAssets = args.GetMultipleItemAssets(out index); - var assetCount = itemAssets.Count; - if (index > -1) - { - itemAssetInput = args[index]; - args.RemoveAt(index); - } - - var radius = args.GetFloat(out index); - if (index > -1) - args.RemoveAt(index); - - var clusters = target == null - ? clusterDirectory.Clusters - : clusterDirectory.GetClustersWithFilter(k => - k.Buildables.Any(l => l.Owner.ToString().Equals(target.Id))); - - if (assetCount > 0) - clusters = clusters.Where(k => k.Buildables.Any(l => itemAssets.Exists(z => l.AssetId == z.id))); - - if (!float.IsNegativeInfinity(radius)) - { - if (caller is not UnturnedPlayer cPlayer) - { - UnturnedChat.Say(caller, - pluginInstance.Translate("cannot_be_executed_from_console")); - return; - } - - clusters = clusters.Where(k => - k.Buildables.Any(l => (l.Position - cPlayer.Position).sqrMagnitude <= Mathf.Pow(radius, 2))); - } - - var itemAssetName = pluginInstance.Translate("not_available"); - - switch (assetCount) - { - case 1: - itemAssetName = itemAssets.First().itemName; - break; - case > 1: - itemAssetName = itemAssetInput; - break; - } - - UnturnedChat.Say(caller, - pluginInstance.Translate("cluster_count", clusters.Count(), itemAssetName, - !float.IsNegativeInfinity(radius) - ? radius.ToString(CultureInfo.CurrentCulture) - : pluginInstance.Translate("not_available"), - target != null ? target.DisplayName : pluginInstance.Translate("not_available"))); - } -} \ No newline at end of file diff --git a/Commands/RemoveBuildableCommand.cs b/Commands/RemoveBuildableCommand.cs deleted file mode 100644 index 7cddd5e..0000000 --- a/Commands/RemoveBuildableCommand.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using SDG.Unturned; -using UnityEngine; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class RemoveBuildableCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Player; - - public string Name => "removebuildable"; - - public string Help => "Removes the buildable you are staring at"; - - public string Syntax => ""; - - public List Aliases => new(); - - public List Permissions => new() { "removebuildable" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - if (caller is not UnturnedPlayer player) return; - - if (!Physics.Raycast(new Ray(player.Player.look.aim.position, player.Player.look.aim.forward), out var hit, - player.Player.look.perspective == EPlayerPerspective.THIRD ? 6 : 4, - RayMasks.BARRICADE_INTERACT | RayMasks.BARRICADE | RayMasks.STRUCTURE | - RayMasks.STRUCTURE_INTERACT) || - hit.transform == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("not_looking_buildable")); - return; - } - - var buildable = BuildableDirectory.GetBuildable(hit.transform); - - if (buildable == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("not_looking_buildable")); - return; - } - - buildable.SafeDestroy(); - } -} \ No newline at end of file diff --git a/Commands/TeleportToBuildCommand.cs b/Commands/TeleportToBuildCommand.cs deleted file mode 100644 index eedb0dc..0000000 --- a/Commands/TeleportToBuildCommand.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using SDG.Unturned; -using UnityEngine; -using Random = UnityEngine.Random; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class TeleportToBuildCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Player; - - public string Name => "teleporttobuild"; - - public string Help => "Teleports you to a random buildable on the map based on filters."; - - public string Syntax => "b [player] | s [player] | v [player] | [player] [id]"; - - public List Aliases => new() { "tpb" }; - - public List Permissions => new() { "teleporttobuild" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - if (caller is not UnturnedPlayer player) return; - - var args = command.ToList(); - - var barricades = args.CheckArgsIncludeString("b", out var index); - if (index > -1) - args.RemoveAt(index); - - var structs = args.CheckArgsIncludeString("s", out index); - if (index > -1) - args.RemoveAt(index); - - var plants = args.CheckArgsIncludeString("v", out index); - if (index > -1) - args.RemoveAt(index); - - var target = args.GetIRocketPlayer(out index); - if (index > -1) - args.RemoveAt(index); - - var itemAssetInput = pluginInstance.Translate("not_available"); - var itemAssets = args.GetMultipleItemAssets(out index); - var assetCount = itemAssets.Count; - if (index > -1) - { - itemAssetInput = args[index]; - args.RemoveAt(index); - } - - var builds = BuildableDirectory.GetBuildables(includePlants: plants); - - builds = target != null - ? builds.Where(k => k.Owner.ToString().Equals(target.Id)) - : builds.Where(k => (k.Position - player.Position).sqrMagnitude > 400); - - if (barricades) builds = builds.Where(k => k.Asset is ItemBarricadeAsset); - else if (structs) builds = builds.Where(k => k.Asset is ItemStructureAsset); - - if (assetCount > 0) builds = builds.Where(k => itemAssets.Exists(l => l.id == k.AssetId)); - - var itemAssetName = pluginInstance.Translate("not_available"); - - switch (assetCount) - { - case 1: - itemAssetName = itemAssets.First().itemName; - break; - case > 1: - itemAssetName = itemAssetInput; - break; - } - - var buildsL = builds.ToList(); - if (!buildsL.Any()) - { - UnturnedChat.Say(caller, - pluginInstance.Translate("cannot_teleport_no_builds", itemAssetName, - target != null ? target.DisplayName : pluginInstance.Translate("not_available"), plants, - barricades, structs)); - return; - } - - var build = buildsL[Random.Range(0, buildsL.Count - 1)]; - - if (build != null) - { - var offset = new Vector3(0, plants ? 4 : 2, 0); - - while (!player.Player.stance.wouldHaveHeightClearanceAtPosition(build.Position + offset, 0.5f)) - offset.y++; - - player.Teleport(build.Position + offset, player.Rotation); - } - else - { - UnturnedChat.Say(caller, - pluginInstance.Translate("cannot_teleport_builds_too_close", itemAssetName, - target != null ? target.DisplayName : pluginInstance.Translate("not_available"), plants, - barricades, structs)); - } - } -} \ No newline at end of file diff --git a/Commands/TeleportToClusterCommand.cs b/Commands/TeleportToClusterCommand.cs deleted file mode 100644 index eb7b170..0000000 --- a/Commands/TeleportToClusterCommand.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using UnityEngine; -using Random = UnityEngine.Random; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class TeleportToClusterCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Player; - - public string Name => "teleporttocluster"; - - public string Help => "Teleports you to a random cluster on the map based on filters."; - - public string Syntax => "[player]"; - - public List Aliases => new() { "tpc" }; - - public List Permissions => new() { "teleporttocluster" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - var clusterDirectory = pluginInstance.BaseClusterDirectory; - if (clusterDirectory == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("command_fail_clustering_disabled")); - return; - } - - if (caller is not UnturnedPlayer player) return; - - var args = command.ToList(); - - var target = args.GetIRocketPlayer(out var index); - if (index > -1) - args.RemoveAt(index); - - var clusters = target != null - ? clusterDirectory.GetClustersWithFilter(k => - k.CommonOwner.ToString().Equals(target.Id)) - : clusterDirectory.Clusters; - - var clustersL = clusters.Where(k => k.AverageCenterPosition != Vector3.zero).ToList(); - if (!clustersL.Any()) - { - UnturnedChat.Say(caller, - pluginInstance.Translate("cannot_teleport_no_clusters", - target != null - ? target.DisplayName - : pluginInstance.Translate("not_available"))); - return; - } - - var cluster = clustersL[Random.Range(0, clustersL.Count - 1)]; - - if (cluster != null) - { - var offset = new Vector3(0, 4, 0); - - while (!player.Player.stance.wouldHaveHeightClearanceAtPosition(cluster.AverageCenterPosition + offset, - 0.5f)) - offset.y++; - - player.Teleport(cluster.AverageCenterPosition + offset, player.Rotation); - } - else - { - UnturnedChat.Say(caller, - pluginInstance.Translate("cannot_teleport_no_clusters", - target != null - ? target.DisplayName - : pluginInstance.Translate("not_available"))); - } - } -} \ No newline at end of file diff --git a/Commands/TopBuildersCommand.cs b/Commands/TopBuildersCommand.cs deleted file mode 100644 index 4193739..0000000 --- a/Commands/TopBuildersCommand.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Rocket.API; -using Rocket.Unturned.Chat; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class TopBuildersCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Both; - - public string Name => "topbuilders"; - - public string Help => "Displays the top 5 builders in the game."; - - public string Syntax => "v"; - - public List Aliases => new() { "topb" }; - - public List Permissions => new() { "topbuilders" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var args = command.ToList(); - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - - var plants = args.CheckArgsIncludeString("v", out var index); - if (index > -1) - args.RemoveAt(index); - - var builds = BuildableDirectory.GetBuildables(includePlants: plants); - - var topBuilders = builds.GroupBy(k => k.Owner).OrderByDescending(k => k.Count()).Take(5).ToList(); - - for (var i = 0; i < topBuilders.Count; i++) - { - var builder = topBuilders.ElementAt(i); - - UnturnedChat.Say(caller, - pluginInstance.Translate("top_builder_format", i + 1, builder.Key, builder.Count())); - } - } -} \ No newline at end of file diff --git a/Commands/TopClustersCommand.cs b/Commands/TopClustersCommand.cs deleted file mode 100644 index 100a726..0000000 --- a/Commands/TopClustersCommand.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Rocket.API; -using Rocket.Unturned.Chat; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class TopClustersCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Both; - - public string Name => "topclusters"; - - public string Help => "Displays the top 5 clusters in the game."; - - public string Syntax => ""; - - public List Aliases => new() { "topc" }; - - public List Permissions => new() { "topclusters" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - var clusterDirectory = pluginInstance.BaseClusterDirectory; - if (clusterDirectory == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("command_fail_clustering_disabled")); - return; - } - - var clusters = clusterDirectory.Clusters; - - var topClusters = clusters.GroupBy(k => k.CommonOwner).OrderByDescending(k => k.Count()).Take(5).ToList(); - - for (var i = 0; i < topClusters.Count; i++) - { - var builder = topClusters.ElementAt(i); - - UnturnedChat.Say(caller, - pluginInstance.Translate("top_cluster_format", i + 1, builder.Key, builder.Count())); - } - } -} \ No newline at end of file diff --git a/Commands/WreckClustersCommand.cs b/Commands/WreckClustersCommand.cs deleted file mode 100644 index ebc703d..0000000 --- a/Commands/WreckClustersCommand.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Pustalorc.Plugins.BaseClustering.API.WreckingActions; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using UnityEngine; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class WreckClustersCommand : IRocketCommand -{ - private readonly Dictionary m_WreckActions = new(); - - public AllowedCaller AllowedCaller => AllowedCaller.Both; - - public string Name => "wreckclusters"; - - public string Help => "Destroys clusters from the map."; - - public string Syntax => "confirm | abort | [player] [item] [radius]"; - - public List Aliases => new() { "wc" }; - - public List Permissions => new() { "wreckclusters" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var pluginInstance = BaseClusteringPlugin.Instance; - - if (pluginInstance == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - var clusterDirectory = pluginInstance.BaseClusterDirectory; - if (clusterDirectory == null) - { - UnturnedChat.Say(caller, pluginInstance.Translate("command_fail_clustering_disabled")); - return; - } - - var cId = caller.Id; - var args = command.ToList(); - - if (args.Count == 0) - { - UnturnedChat.Say(caller, pluginInstance.Translate("not_enough_args")); - return; - } - - var abort = args.CheckArgsIncludeString("abort", out var index); - if (index > -1) - args.RemoveAt(index); - - var confirm = args.CheckArgsIncludeString("confirm", out index); - if (index > -1) - args.RemoveAt(index); - - var target = args.GetIRocketPlayer(out index); - if (index > -1) - args.RemoveAt(index); - - var itemAssetInput = pluginInstance.Translate("not_available"); - var itemAssets = args.GetMultipleItemAssets(out index); - var assetCount = itemAssets.Count; - if (index > -1) - { - itemAssetInput = args[index]; - args.RemoveAt(index); - } - - var radius = args.GetFloat(out index); - if (index > -1) - args.RemoveAt(index); - - if (abort) - { - if (m_WreckActions.TryGetValue(cId, out _)) - { - m_WreckActions.Remove(cId); - UnturnedChat.Say(caller, pluginInstance.Translate("action_cancelled")); - return; - } - - UnturnedChat.Say(caller, pluginInstance.Translate("no_action_queued")); - return; - } - - if (confirm) - { - if (!m_WreckActions.TryGetValue(cId, out var action)) - { - UnturnedChat.Say(caller, pluginInstance.Translate("no_action_queued")); - return; - } - - m_WreckActions.Remove(cId); - - var remove = action.TargetPlayer != null - ? clusterDirectory.GetClustersWithFilter(k => - k.CommonOwner.ToString().Equals(action.TargetPlayer.Id)) - : clusterDirectory.Clusters; - - if (action.ItemAssets.Count > 0) - remove = remove.Where(k => k.Buildables.Any(l => action.ItemAssets.Exists(z => l.AssetId == z.id))); - - if (!action.Center.IsNegativeInfinity()) - remove = remove.Where(k => - k.Buildables.Any(l => - (l.Position - action.Center).sqrMagnitude <= Mathf.Pow(action.Radius, 2))); - - var baseClusters = remove.ToList(); - if (!baseClusters.Any()) - { - UnturnedChat.Say(caller, pluginInstance.Translate("cannot_wreck_no_clusters")); - return; - } - - foreach (var cluster in baseClusters) - cluster.Destroy(); - - UnturnedChat.Say(caller, - pluginInstance.Translate("wrecked_clusters", baseClusters.Count, action.ItemAssetName, - !float.IsNegativeInfinity(action.Radius) - ? action.Radius.ToString(CultureInfo.CurrentCulture) - : pluginInstance.Translate("not_available"), - action.TargetPlayer != null - ? action.TargetPlayer.DisplayName - : pluginInstance.Translate("not_available"))); - return; - } - - var clusters = target != null - ? clusterDirectory.GetClustersWithFilter(k => - k.CommonOwner.ToString().Equals(target.Id)) - : clusterDirectory.Clusters; - - if (assetCount > 0) - clusters = clusters.Where(k => k.Buildables.Any(l => itemAssets.Exists(z => l.AssetId == z.id))); - - var center = Vector3.negativeInfinity; - - if (!float.IsNegativeInfinity(radius)) - { - if (caller is not UnturnedPlayer cPlayer) - { - UnturnedChat.Say(caller, - pluginInstance.Translate("cannot_be_executed_from_console")); - return; - } - - center = cPlayer.Position; - clusters = clusters.Where(k => - k.Buildables.Any(l => (l.Position - center).sqrMagnitude <= Mathf.Pow(radius, 2))); - } - - var count = clusters.Count(); - - if (count <= 0) - { - UnturnedChat.Say(caller, pluginInstance.Translate("cannot_wreck_no_clusters")); - return; - } - - var itemAssetName = pluginInstance.Translate("not_available"); - - switch (assetCount) - { - case 1: - itemAssetName = itemAssets.First().itemName; - break; - case > 1: - itemAssetName = itemAssetInput; - break; - } - - if (m_WreckActions.TryGetValue(cId, out _)) - { - m_WreckActions[cId] = new WreckClustersAction(target, center, itemAssets, radius, itemAssetInput); - UnturnedChat.Say(caller, - pluginInstance.Translate("wreck_clusters_action_queued_new", - target?.DisplayName ?? pluginInstance.Translate("not_available"), itemAssetName, - !float.IsNegativeInfinity(radius) - ? radius.ToString(CultureInfo.CurrentCulture) - : pluginInstance.Translate("not_available"), count)); - } - else - { - m_WreckActions.Add(cId, new WreckClustersAction(target, center, itemAssets, radius, itemAssetInput)); - UnturnedChat.Say(caller, - pluginInstance.Translate("wreck_clusters_action_queued", - target?.DisplayName ?? pluginInstance.Translate("not_available"), itemAssetName, - !float.IsNegativeInfinity(radius) - ? radius.ToString(CultureInfo.CurrentCulture) - : pluginInstance.Translate("not_available"), count)); - } - } -} \ No newline at end of file diff --git a/Commands/WreckCommand.cs b/Commands/WreckCommand.cs deleted file mode 100644 index f9958f6..0000000 --- a/Commands/WreckCommand.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Pustalorc.Plugins.BaseClustering.API.Utilities; -using Pustalorc.Plugins.BaseClustering.API.WreckingActions; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using SDG.Unturned; -using UnityEngine; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class WreckCommand : IRocketCommand -{ - private readonly Dictionary m_WreckActions = new(); - - public AllowedCaller AllowedCaller => AllowedCaller.Both; - - public string Name => "wreck"; - - public string Help => "Destroys buildables from the map."; - - public string Syntax => - "confirm | abort | b [radius] | s [radius] | [radius] | v [item] [radius] | [item] [radius]"; - - public List Aliases => new() { "w" }; - - public List Permissions => new() { "wreck" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var cId = caller.Id; - var args = command.ToList(); - var baseClusteringPlugin = BaseClusteringPlugin.Instance; - - if (baseClusteringPlugin == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - if (args.Count == 0) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("not_enough_args")); - return; - } - - var abort = args.CheckArgsIncludeString("abort", out var index); - if (index > -1) - args.RemoveAt(index); - - var confirm = args.CheckArgsIncludeString("confirm", out index); - if (index > -1) - args.RemoveAt(index); - - var plants = args.CheckArgsIncludeString("v", out index); - if (index > -1) - args.RemoveAt(index); - - var barricades = args.CheckArgsIncludeString("b", out index); - if (index > -1) - args.RemoveAt(index); - - var structs = args.CheckArgsIncludeString("s", out index); - if (index > -1) - args.RemoveAt(index); - - var target = args.GetIRocketPlayer(out index); - if (index > -1) - args.RemoveAt(index); - - var itemAssetInput = baseClusteringPlugin.Translate("not_available"); - var itemAssets = args.GetMultipleItemAssets(out index); - var assetCount = itemAssets.Count; - if (index > -1) - { - itemAssetInput = args[index]; - args.RemoveAt(index); - } - - var radius = args.GetFloat(out index); - if (index > -1) - args.RemoveAt(index); - - if (abort) - { - if (m_WreckActions.TryGetValue(cId, out _)) - { - m_WreckActions.Remove(cId); - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("action_cancelled")); - return; - } - - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("no_action_queued")); - return; - } - - if (confirm) - { - if (!m_WreckActions.TryGetValue(cId, out var action)) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("no_action_queued")); - return; - } - - m_WreckActions.Remove(cId); - - var remove = BuildableDirectory.GetBuildables(includePlants: action.IncludeVehicles); - - if (action.TargetPlayer != null) - remove = remove.Where(k => k.Owner.ToString().Equals(action.TargetPlayer.Id)); - - if (action.FilterForBarricades) remove = remove.Where(k => k.Asset is ItemBarricadeAsset); - else if (action.FilterForStructures) remove = remove.Where(k => k.Asset is ItemStructureAsset); - - if (action.ItemAssets.Count > 0) - remove = remove.Where(k => action.ItemAssets.Exists(l => k.AssetId == l.id)); - - if (!action.Center.IsNegativeInfinity()) - remove = remove.Where(k => - (k.Position - action.Center).sqrMagnitude <= Mathf.Pow(action.Radius, 2)); - - var buildables = remove.ToList(); - if (!buildables.Any()) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("cannot_wreck_no_builds")); - return; - } - - foreach (var build in buildables) - build.SafeDestroy(); - - UnturnedChat.Say(caller, - baseClusteringPlugin.Translate("wrecked", buildables.Count, action.ItemAssetName, - !float.IsNegativeInfinity(action.Radius) - ? action.Radius.ToString(CultureInfo.CurrentCulture) - : baseClusteringPlugin.Translate("not_available"), - action.TargetPlayer != null - ? action.TargetPlayer.DisplayName - : baseClusteringPlugin.Translate("not_available"), action.IncludeVehicles, - action.FilterForBarricades, action.FilterForStructures)); - return; - } - - var builds = BuildableDirectory.GetBuildables(includePlants: plants); - - if (target != null) builds = builds.Where(k => k.Owner.ToString().Equals(target.Id)); - - if (barricades) builds = builds.Where(k => k.Asset is ItemBarricadeAsset); - else if (structs) builds = builds.Where(k => k.Asset is ItemStructureAsset); - - if (assetCount > 0) builds = builds.Where(k => itemAssets.Exists(l => k.AssetId == l.id)); - - var center = Vector3.negativeInfinity; - - if (!float.IsNegativeInfinity(radius)) - { - if (caller is not UnturnedPlayer cPlayer) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("cannot_be_executed_from_console")); - return; - } - - center = cPlayer.Position; - builds = builds.Where(k => (k.Position - center).sqrMagnitude <= Mathf.Pow(radius, 2)); - } - - var itemAssetName = baseClusteringPlugin.Translate("not_available"); - - switch (assetCount) - { - case 1: - itemAssetName = itemAssets.First().itemName; - break; - case > 1: - itemAssetName = itemAssetInput; - break; - } - - var count = builds.Count(); - if (count <= 0) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("cannot_wreck_no_builds")); - return; - } - - if (m_WreckActions.TryGetValue(cId, out _)) - { - m_WreckActions[cId] = new WreckAction(plants, barricades, structs, target, center, itemAssets, radius, - itemAssetName); - UnturnedChat.Say(caller, - baseClusteringPlugin.Translate("wreck_action_queued_new", itemAssetName, - baseClusteringPlugin.Translate("not_available"), - !float.IsNegativeInfinity(radius) - ? radius.ToString(CultureInfo.CurrentCulture) - : baseClusteringPlugin.Translate("not_available"), - target != null ? target.DisplayName : baseClusteringPlugin.Translate("not_available"), - plants, barricades, structs, count)); - } - else - { - m_WreckActions.Add(cId, - new WreckAction(plants, barricades, structs, target, center, itemAssets, radius, itemAssetName)); - UnturnedChat.Say(caller, - baseClusteringPlugin.Translate("wreck_action_queued", itemAssetName, - !float.IsNegativeInfinity(radius) - ? radius.ToString(CultureInfo.CurrentCulture) - : baseClusteringPlugin.Translate("not_available"), - target != null ? target.DisplayName : baseClusteringPlugin.Translate("not_available"), - plants, barricades, structs, count)); - } - } -} \ No newline at end of file diff --git a/Commands/WreckVehicleCommand.cs b/Commands/WreckVehicleCommand.cs deleted file mode 100644 index 93511df..0000000 --- a/Commands/WreckVehicleCommand.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Rocket.API; -using Rocket.Unturned.Chat; -using Rocket.Unturned.Player; -using SDG.Unturned; -using UnityEngine; - -#pragma warning disable 1591 - -namespace Pustalorc.Plugins.BaseClustering.Commands; - -[UsedImplicitly] -public sealed class WreckVehicleCommand : IRocketCommand -{ - public AllowedCaller AllowedCaller => AllowedCaller.Player; - - public string Name => "wreckvehicle"; - - public string Help => "Wrecks all the buildables on the vehicle that you are looking at."; - - public string Syntax => ""; - - public List Aliases => new() { "wv" }; - - public List Permissions => new() { "wreckvehicle" }; - - public void Execute(IRocketPlayer caller, string[] command) - { - var player = (UnturnedPlayer)caller; - var raycastInfo = - DamageTool.raycast(new Ray(player.Player.look.aim.position, player.Player.look.aim.forward), 10f, - RayMasks.VEHICLE); - var baseClusteringPlugin = BaseClusteringPlugin.Instance; - - if (baseClusteringPlugin == null) - throw new NullReferenceException("BaseClusteringPlugin.Instance is null. Cannot execute command."); - - if (raycastInfo.vehicle == null) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("no_vehicle_found")); - return; - } - - if (raycastInfo.vehicle.isDead) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("vehicle_dead")); - return; - } - - if (!BarricadeManager.tryGetPlant(raycastInfo.transform, out var x, out var y, out var plant, - out var region)) - { - UnturnedChat.Say(caller, baseClusteringPlugin.Translate("vehicle_no_plant")); - return; - } - - for (var i = region.drops.Count - 1; i > 0; i--) - BarricadeManager.destroyBarricade(region.drops[i], x, y, plant); - - UnturnedChat.Say(caller, - baseClusteringPlugin.Translate("vehicle_wreck", - raycastInfo.vehicle.asset.vehicleName ?? raycastInfo.vehicle.asset.name, - raycastInfo.vehicle.id, raycastInfo.vehicle.instanceID, - raycastInfo.vehicle.lockedOwner.ToString())); - } -} \ No newline at end of file diff --git a/Config/BaseClusteringPluginConfiguration.cs b/Config/BaseClusteringPluginConfiguration.cs deleted file mode 100644 index b5ef5e6..0000000 --- a/Config/BaseClusteringPluginConfiguration.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel; -using JetBrains.Annotations; -using Pustalorc.Plugins.BaseClustering.API.BaseClusters; -using Pustalorc.Plugins.BaseClustering.API.Buildables; -using Rocket.API; - -namespace Pustalorc.Plugins.BaseClustering.Config; - -/// -/// Configuration for the plugin when it comes to how it should operate and handle things. -/// -[UsedImplicitly] -public sealed class BaseClusteringPluginConfiguration : IRocketPluginConfiguration -{ - /// - /// If the should be instantiated and loaded or not. - /// - public bool EnableClustering { get; set; } - - /// - /// The maximum distance between 2 structure objects for them to be considered part of a . - ///
- /// Distance is in meters by unity's measurements. Squaring is needed if using for speed. - ///
- public float MaxDistanceBetweenStructures { get; set; } - - /// - /// The maximum distance between a structure object and a position for it to be considered part of, or inside of a . - ///
- /// Distance is in meters by unity's measurements. Squaring is needed if using for speed. - ///
- public float MaxDistanceToConsiderPartOfBase { get; set; } - - /// - /// The default buildable capacity when initializing . - /// - public int BuildableCapacity { get; set; } - - /// - /// The amount of time the will sleep after completing one cycle of deferred adds and removes. - /// - public int BackgroundWorkerSleepTime { get; set; } - - /// - /// Loads the default values for the config. - /// - public void LoadDefaults() - { - EnableClustering = true; - MaxDistanceBetweenStructures = 6.1f; - MaxDistanceToConsiderPartOfBase = 10f; - BuildableCapacity = 60000; - BackgroundWorkerSleepTime = 125; - } -} \ No newline at end of file diff --git a/README.md b/README.md index e2869c5..9b2d772 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BaseClustering -Unturned Plugin to cluster Buildables & Structures - +Unturned Plugin to define bases in unturned, by clustering/grouping buildables and structures. +This plugin utilizes [BuildableAbstractions](https://github.com/Pustalorc/BuildableAbstractions/) Download is available on github [releases](https://github.com/Pustalorc/BaseClustering/releases/) --- @@ -9,45 +9,30 @@ Download is available on github [releases](https://github.com/Pustalorc/BaseClus ## Commands `/clustersregen` - Regenerates ALL clusters. Useful if a new barricade was missed, or one of them is reported at the -wrong position, or some other issue with clusters has occurred. - -`/findbuilds [b | s | v] [player] [item] [radius]` - Finds and returns the count of buildables with the filters. - -`/findclusters [player] [item] [radius]` - Finds and returns the count of clusters with the filters. - -`/removebuildable` - Removes the buildable that the player is currently looking at. - -`/teleporttobuild [b | s | v] [player] [item]` - Teleports to a random buildable that satisfies the filters. - -`/teleporttocluster [player]` - Teleports to a random cluster that satisfies the filters. - -`/topbuilders [v]` - Lists the top 5 players that have the most amount of buildables on the map. - -`/topclusters` - Lists the top 5 players that have the most amount of common ownerships on clusters. - +wrong position, or some other issue with clusters has occurred. +`/findbuilds [b | s | v] [player] [item] [radius]` - Finds and returns the count of buildables with the filters. +`/findclusters [player] [item] [radius]` - Finds and returns the count of clusters with the filters. +`/removebuildable` - Removes the buildable that the player is currently looking at. +`/teleporttobuild [b | s | v] [player] [item]` - Teleports to a random buildable that satisfies the filters. +`/teleporttocluster [player]` - Teleports to a random cluster that satisfies the filters. +`/topbuilders [v]` - Lists the top 5 players that have the most amount of buildables on the map. +`/topclusters` - Lists the top 5 players that have the most amount of common ownerships on clusters. `/wreckclusters [player] [item] [radius]` and `/wreckclusters [abort | confirm]` - Wrecks all of the clusters that -satisfy the filters. Requires confirmation before fully wrecking them. - +satisfy the filters. Requires confirmation before fully wrecking them. `/wreck [b | s | v] [player] [item] [radius]` and `/wreck [abort | confirm]` - Wrecks all of the buildables that satisfy -the filters. Requires confirmation before fully wrecking them. - +the filters. Requires confirmation before fully wrecking them. `/wreckvehicle` - Wrecks all of the buildables without confirmation on the vehicle that you are facing. ### Explanation of arguments: -Arguments can be on any order, so doing: `/wreck b pusta birch 5.0` should be the same as `/wreck birch b 5.0 pusta` - +Arguments can be on any order, so doing: `/wreck b pusta birch 5.0` should be the same as `/wreck birch b 5.0 pusta` `[b | s | v]` specifies filters for all of the buildables. It can specify to filter JUST for barricades (`b`), JUST for -structures (`s`), or INCLUDE buildables on vehicles (`v`). - -`[player]` self-explanatory. Accepts Steam64ID as well as the name. - +structures (`s`), or INCLUDE buildables on vehicles (`v`). +`[player]` self-explanatory. Accepts Steam64ID as well as the name. `[item]` self-explanatory. Accepts item names (including just typing `birch`) as well as item IDs. Using item names will -select all results with that name, so be careful if you only write one letter! - +select all results with that name, so be careful if you only write one letter! `[radius]` self-explanatory. Note that typing `5` will be considered an item if you do not specify an item by name or ID -before it! To prevent this, type `.0` at the end of the number, that will force it to be detected as a radius. - +before it! To prevent this, type `.0` at the end of the number, that will force it to be detected as a radius. `[abort | confirm]` - self-explanatory, aborts or confirms previous action --- diff --git a/libs/Assembly-CSharp.dll b/libs/Assembly-CSharp.dll deleted file mode 100644 index 5e8d2ad..0000000 Binary files a/libs/Assembly-CSharp.dll and /dev/null differ diff --git a/libs/SDG.NetTransport.dll b/libs/SDG.NetTransport.dll deleted file mode 100644 index 9c3b06c..0000000 Binary files a/libs/SDG.NetTransport.dll and /dev/null differ diff --git a/libs/UnityEngine.CoreModule.dll b/libs/UnityEngine.CoreModule.dll deleted file mode 100644 index ca04242..0000000 Binary files a/libs/UnityEngine.CoreModule.dll and /dev/null differ diff --git a/libs/UnityEngine.PhysicsModule.dll b/libs/UnityEngine.PhysicsModule.dll deleted file mode 100644 index 2e358d2..0000000 Binary files a/libs/UnityEngine.PhysicsModule.dll and /dev/null differ diff --git a/libs/UnityEngine.dll b/libs/UnityEngine.dll deleted file mode 100644 index ca014f5..0000000 Binary files a/libs/UnityEngine.dll and /dev/null differ diff --git a/libs/com.rlabrecque.steamworks.net.dll b/libs/com.rlabrecque.steamworks.net.dll deleted file mode 100644 index 942650b..0000000 Binary files a/libs/com.rlabrecque.steamworks.net.dll and /dev/null differ