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