using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Configuration; using Oxide.Core.Libraries; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using Oxide.Plugins.MonumentAddonsExtensions; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using UnityEngine; using VLB; using static IOEntity; using static WireTool; using CustomSpawnCallback = System.Func; using CustomKillCallback = System.Action; using CustomUpdateCallback = System.Action; using CustomAddDisplayInfoCallback = System.Action; using CustomSetDataCallback = System.Action; using System.Text.RegularExpressions; namespace Oxide.Plugins { [Info("Monument Addons", "WhiteThunder", "0.15.0")] [Description("Allows adding entities, spawn points and more to monuments.")] internal class MonumentAddons : CovalencePlugin { #region Fields [PluginReference] private Plugin CopyPaste, CustomVendingSetup, EntityScaleManager, MonumentFinder, SignArtist; private MonumentAddons _plugin; private Configuration _config; private StoredData _data; private ProfileStateData _profileStateData; private const float MaxRaycastDistance = 100; private const float TerrainProximityTolerance = 0.001f; private const float MaxFindDistanceSquared = 4; private const float ShowVanillaDuration = 60; private const string PermissionAdmin = "monumentaddons.admin"; private const string WireToolPlugEffect = "assets/prefabs/tools/wire/effects/plugeffect.prefab"; private const string CargoShipShortName = "cargoshiptest"; private const string DefaultProfileName = "Default"; private const string DefaultUrlPattern = "https://github.com/WheteThunger/MonumentAddons/blob/master/Profiles/{0}.json?raw=true"; private static readonly int HitLayers = Rust.Layers.Solid | Rust.Layers.Mask.Water; private static readonly Dictionary JsonRequestHeaders = new Dictionary { { "Content-Type", "application/json" } }; private readonly ProfileStore _profileStore = new ProfileStore(); private readonly OriginalProfileStore _originalProfileStore = new OriginalProfileStore(); private readonly ProfileManager _profileManager; private readonly CoroutineManager _coroutineManager = new CoroutineManager(); private readonly MonumentEntityTracker _entityTracker = new MonumentEntityTracker(); private readonly AdapterListenerManager _adapterListenerManager; private readonly ControllerFactory _controllerFactory; private readonly CustomAddonManager _customAddonManager; private readonly UniqueNameRegistry _uniqueNameRegistry = new UniqueNameRegistry(); private readonly AdapterDisplayManager _adapterDisplayManager; private readonly MonumentHelper _monumentHelper; private readonly WireToolManager _wireToolManager; private readonly IOManager _ioManager = new IOManager(); private readonly UndoManager _undoManager = new UndoManager(); private readonly Color[] _distinctColors = { Color.HSVToRGB(0, 1, 1), Color.HSVToRGB(0.1f, 1, 1), Color.HSVToRGB(0.2f, 1, 1), Color.HSVToRGB(0.35f, 1, 1), Color.HSVToRGB(0.55f, 1, 1), Color.HSVToRGB(0.8f, 1, 1), new Color(1, 1, 1), }; private readonly object False = false; private ItemDefinition _waterDefinition; private ProtectionProperties _immortalProtection; private ActionDebounced _saveProfileStateDebounced; private Coroutine _startupCoroutine; private bool _serverInitialized; private bool _isLoaded = true; public MonumentAddons() { _profileManager = new ProfileManager(this, _originalProfileStore, _profileStore); _adapterDisplayManager = new AdapterDisplayManager(this, _uniqueNameRegistry); _adapterListenerManager = new AdapterListenerManager(this); _customAddonManager = new CustomAddonManager(this); _controllerFactory = new ControllerFactory(this); _monumentHelper = new MonumentHelper(this); _wireToolManager = new WireToolManager(this, _profileStore, _entityTracker); _saveProfileStateDebounced = new ActionDebounced(timer, 1, () => { if (!_isLoaded) return; _profileStateData.Save(); }); } #endregion #region Hooks private void Init() { _plugin = this; _data = StoredData.Load(_profileStore); _profileStateData = ProfileStateData.Load(_data); _config.Init(); // Ensure the profile folder is created to avoid errors. _profileStore.EnsureDefaultProfile(); permission.RegisterPermission(PermissionAdmin, this); Unsubscribe(nameof(OnEntitySpawned)); _adapterListenerManager.Init(); } private void OnServerInitialized() { _waterDefinition = ItemManager.FindItemDefinition("water"); _immortalProtection = ScriptableObject.CreateInstance(); _immortalProtection.name = "MonumentAddonsProtection"; _immortalProtection.Add(1); _uniqueNameRegistry.OnServerInitialized(); _adapterListenerManager.OnServerInitialized(); _monumentHelper.OnServerInitialized(); _ioManager.OnServerInitialized(); var entitiesToKill = _profileStateData.CleanDisabledProfileState(); if (entitiesToKill.Count > 0) { CoroutineManager.StartGlobalCoroutine(KillEntitiesRoutine(entitiesToKill)); } if (CheckDependencies()) { StartupRoutine(); } _serverInitialized = true; } private void Unload() { _coroutineManager.Destroy(); _profileManager.UnloadAllProfiles(); _saveProfileStateDebounced.Flush(); _wireToolManager.Unload(); UnityEngine.Object.Destroy(_immortalProtection); _isLoaded = false; } private void OnNewSave() { _profileStateData.Reset(); } private void OnPluginLoaded(Plugin plugin) { // Check whether initialized to detect only late (re)loads. // Note: We are not dynamically subscribing to OnPluginLoaded since that interferes with [PluginReference] for some reason. if (_serverInitialized && plugin == MonumentFinder) { StartupRoutine(); } } private void OnPluginUnloaded(Plugin plugin) { if (plugin.Name == Name) return; _customAddonManager.UnregisterAllForPlugin(plugin); } private void OnEntitySpawned(CargoShip cargoShip) { var cargoShipMonument = new DynamicMonument(cargoShip, isMobile: true); _coroutineManager.StartCoroutine(_profileManager.PartialLoadForLateMonumentRoutine(cargoShipMonument)); } // This hook is exposed by plugin: Remover Tool (RemoverTool). private object canRemove(BasePlayer player, BaseEntity entity) { if (_entityTracker.IsMonumentEntity(entity)) return False; return null; } private object CanChangeGrade(BasePlayer player, BuildingBlock block, BuildingGrade.Enum grade) { if (_entityTracker.IsMonumentEntity(block) && !HasAdminPermission(player)) return False; return null; } private object CanUpdateSign(BasePlayer player, ISignage signage) { if (_entityTracker.IsMonumentEntity(signage as BaseEntity) && !HasAdminPermission(player)) { ChatMessage(player, LangEntry.ErrorNoPermission); return False; } return null; } private void OnSignUpdated(ISignage signage, BasePlayer player) { if (!_entityTracker.IsMonumentEntity(signage as BaseEntity)) return; var component = MonumentEntityComponent.GetForEntity(signage.NetworkID); if (component == null) return; var adapter = component.Adapter as SignAdapter; if (adapter == null) return; var controller = adapter.Controller as SignController; if (controller == null) return; controller.UpdateSign(signage.GetTextureCRCs()); } // This hook is exposed by plugin: Sign Arist (SignArtist). private void OnImagePost(BasePlayer player, string url, bool raw, ISignage signage, uint textureIndex = 0) { SignController controller; if (!_entityTracker.IsMonumentEntity(signage as BaseEntity, out controller)) return; if (controller.EntityData.SignArtistImages == null) { controller.EntityData.SignArtistImages = new SignArtistImage[signage.TextureCount]; } else if (controller.EntityData.SignArtistImages.Length < signage.TextureCount) { Array.Resize(ref controller.EntityData.SignArtistImages, signage.TextureCount); } controller.EntityData.SignArtistImages[textureIndex] = new SignArtistImage { Url = url, Raw = raw, }; _profileStore.Save(controller.Profile); } private object OnSprayRemove(SprayCanSpray spray, BasePlayer player) { if (_entityTracker.IsMonumentEntity(spray)) return False; return null; } private void OnEntityScaled(BaseEntity entity, float scale) { EntityController controller; if (!_entityTracker.IsMonumentEntity(entity, out controller) || controller.EntityData.Scale == scale) return; controller.EntityData.Scale = scale; controller.StartHandleChangesRoutine(); _profileStore.Save(controller.Profile); } // This hook is exposed by plugin: Telekinesis. private BaseEntity OnTelekinesisFindFailed(BasePlayer player) { if (!HasAdminPermission(player)) return null; return FindAdapter(player).Adapter?.Entity; } // This hook is exposed by plugin: Telekinesis. private object CanStartTelekinesis(BasePlayer player, BaseEntity moveEntity) { if (_entityTracker.IsMonumentEntity(moveEntity) && !HasAdminPermission(player)) return False; return null; } // This hook is exposed by plugin: Telekinesis. private void OnTelekinesisStarted(BasePlayer player, BaseEntity moveEntity, BaseEntity rotateEntity) { if (_entityTracker.IsMonumentEntity(moveEntity)) { _adapterDisplayManager.ShowAllRepeatedly(player); } if (GetSpawnPointAdapter(moveEntity) != null) { _adapterDisplayManager.ShowAllRepeatedly(player); var spawnedVehicleComponent = moveEntity.GetComponent(); if (spawnedVehicleComponent != null) { spawnedVehicleComponent.CancelInvoke(spawnedVehicleComponent.CheckPositionTracked); } } } // This hook is exposed by plugin: Telekinesis. private void OnTelekinesisStopped(BasePlayer player, BaseEntity moveEntity, BaseEntity rotateEntity) { EntityAdapter adapter; EntityController controller; int adapterCount; string profileName; var spawnPointAdapter = GetSpawnPointAdapter(moveEntity); if (spawnPointAdapter != null) { var moveEntityTransform = moveEntity.transform; var moveEntityPosition = moveEntityTransform.position; if (!spawnPointAdapter.Monument.IsInBounds(moveEntityPosition)) return; var localPosition = spawnPointAdapter.Monument.InverseTransformPoint(moveEntityPosition); spawnPointAdapter.SpawnPointData.Position = localPosition; spawnPointAdapter.SpawnPointData.RotationAngles = (Quaternion.Inverse(spawnPointAdapter.Monument.Rotation) * moveEntityTransform.rotation).eulerAngles; spawnPointAdapter.SpawnPointData.SnapToTerrain = IsOnTerrain(moveEntityPosition); _profileStore.Save(spawnPointAdapter.Profile); var spawnGroupController = spawnPointAdapter.Controller as SpawnGroupController; spawnGroupController.UpdateSpawnGroups(); adapterCount = spawnGroupController.Adapters.Count; profileName = spawnPointAdapter.Profile.Name; } else if (_entityTracker.IsMonumentEntity(moveEntity, out adapter, out controller)) { if (!adapter.TrySaveAndApplyChanges()) return; adapterCount = controller.Adapters.Count; profileName = controller.Profile.Name; } else return; if (player != null) { _adapterDisplayManager.ShowAllRepeatedly(player); ChatMessage(player, LangEntry.SaveSuccess, adapterCount, profileName); } } // This hook is exposed by plugin: Custom Vending Setup (CustomVendingSetup). private Dictionary OnCustomVendingSetupDataProvider(NPCVendingMachine vendingMachine) { EntityController controller; if (!_entityTracker.IsMonumentEntity(vendingMachine, out controller)) return null; var vendingProfile = controller.EntityData.VendingProfile; if (vendingProfile == null) { // Check if there's one to be migrated. var migratedVendingProfile = MigrateVendingProfile(vendingMachine); if (migratedVendingProfile != null) { controller.EntityData.VendingProfile = migratedVendingProfile; _profileStore.Save(controller.Profile); LogWarning($"Successfully migrated vending machine settings from CustomVendingSetup to MonumentAddons profile '{controller.Profile.Name}'."); } } return controller.GetVendingDataProvider(); } #endregion #region Dependencies private bool CheckDependencies() { if (MonumentFinder == null) { LogError("MonumentFinder is not loaded, get it at https://umod.org."); return false; } return true; } private float GetEntityScale(BaseEntity entity) { if (EntityScaleManager == null) return 1; return Convert.ToSingle(EntityScaleManager?.Call("API_GetScale", entity)); } private bool TryScaleEntity(BaseEntity entity, float scale) { var result = EntityScaleManager?.Call("API_ScaleEntity", entity, scale); return result is bool && (bool)result; } private void SkinSign(ISignage signage, SignArtistImage[] signArtistImages) { if (SignArtist == null) return; var apiName = signage is Signage ? "API_SkinSign" : signage is PhotoFrame ? "API_SkinPhotoFrame" : signage is CarvablePumpkin ? "API_SkinPumpkin" : null; if (apiName == null) { LogError($"Unrecognized sign type: {signage.GetType()}"); return; } for (uint textureIndex = 0; textureIndex < signArtistImages.Length; textureIndex++) { var imageInfo = signArtistImages[textureIndex]; if (imageInfo == null) continue; SignArtist.Call(apiName, null, signage, imageInfo.Url, imageInfo.Raw, textureIndex); } } private JObject MigrateVendingProfile(NPCVendingMachine vendingMachine) { return CustomVendingSetup?.Call("API_MigrateVendingProfile", vendingMachine) as JObject; } private void RefreshVendingProfile(NPCVendingMachine vendingMachine) { CustomVendingSetup?.Call("API_RefreshDataProvider", vendingMachine); } private static class PasteUtils { private static readonly string[] CopyPasteArgs = { "stability", "false", "checkplaced", "false", }; private static VersionNumber _requiredVersion = new VersionNumber(4, 1, 32); public static bool IsCopyPasteCompatible(Plugin copyPaste) { return copyPaste != null && copyPaste.Version >= _requiredVersion; } public static bool DoesPasteExist(string filename) { return Interface.Oxide.DataFileSystem.ExistsDatafile("copypaste/" + filename); } public static Action PasteWithCancelCallback(Plugin copyPaste, PasteData pasteData, Vector3 position, float yRotation, Action onEntityPasted, Action onPasteCompleted) { if (copyPaste == null) return null; var result = copyPaste.Call("TryPasteFromVector3Cancellable", position, yRotation, pasteData.Filename, CopyPasteArgs, onPasteCompleted, onEntityPasted); if (!(result is ValueTuple)) { LogError($"CopyPaste returned an unexpected response for paste \"{pasteData.Filename}\": {result}. Is CopyPaste up-to-date?"); return null; } var pasteResult = (ValueTuple)result; if (!true.Equals(pasteResult.Item1)) { LogError($"CopyPaste returned an unexpected response for paste \"{pasteData.Filename}\": {pasteResult.Item1}."); return null; } return pasteResult.Item2; } } #endregion #region Commands private enum PuzzleOption { PlayersBlockReset, PlayerDetectionRadius, SecondsBetweenResets, } private enum SpawnGroupOption { Name, MaxPopulation, RespawnDelayMin, RespawnDelayMax, SpawnPerTickMin, SpawnPerTickMax, InitialSpawn, PreventDuplicates, PauseScheduleWhileFull, RespawnWhenNearestPuzzleResets, } private enum SpawnPointOption { Exclusive, SnapToGround, CheckSpace, RandomRotation, RandomRadius, PlayerDetectionRadius, } [Command("maspawn")] private void CommandSpawn(IPlayer player, string cmd, string[] args) { ProfileController profileController; string prefabName; Vector3 position; BaseMonument monument; CustomAddonDefinition addonDefinition; ulong skinId; BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player) || !VerifyMonumentFinderLoaded(player) || !VerifyProfileSelected(player, out profileController) || !VerifyValidEntityPrefabOrDeployable(player, args, out prefabName, out addonDefinition, out skinId) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument)) return; Vector3 localPosition; Vector3 localRotationAngles; bool isOnTerrain; DetermineLocalTransformData(position, basePlayer, monument, out localPosition, out localRotationAngles, out isOnTerrain); BaseData addonData; if (prefabName != null) { // Found a valid prefab name. var shortPrefabName = GetShortName(prefabName); if (shortPrefabName == "big_wheel") { localRotationAngles.y -= 90; localRotationAngles.z = 270; } else if (shortPrefabName == "boatspawner") { if (position.y == TerrainMeta.WaterMap.GetHeight(position)) { // Set the boatspawner to -1.5 like the vanilla ones. localPosition.y -= 1.5f; } } else if (shortPrefabName == "spray.decal") { localRotationAngles.x += 270; localRotationAngles.y -= 90; } var entityData = new EntityData { Id = Guid.NewGuid(), Skin = skinId, PrefabName = prefabName, Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain, }; if (shortPrefabName.StartsWith("generator.static")) { entityData.Puzzle = new PuzzleData { PlayersBlockReset = true, PlayerDetectionRadius = 30, SecondsBetweenResets = 1800, }; } addonData = entityData; } else { // Found a custom addon definition. addonData = new CustomAddonData { Id = Guid.NewGuid(), AddonName = addonDefinition.AddonName, Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain, }; } var matchingMonuments = GetMonumentsByAliasOrShortName(monument.AliasOrShortName); profileController.Profile.AddData(monument.AliasOrShortName, addonData); _profileStore.Save(profileController.Profile); profileController.SpawnNewData(addonData, matchingMonuments); ReplyToPlayer(player, LangEntry.PrefabSuccess, matchingMonuments.Count, profileController.Profile.Name, monument.AliasOrShortName); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } [Command("maprefab")] private void CommandPrefab(IPlayer player, string cmd, string[] args) { ProfileController profileController; string prefabName; Vector3 position; BaseMonument monument; BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player) || !VerifyMonumentFinderLoaded(player) || !VerifyProfileSelected(player, out profileController) || !VerifyValidModderPrefab(player, args, out prefabName) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument)) return; if (FindBaseEntityForPrefab(prefabName) != null) { ReplyToPlayer(player, LangEntry.PrefabErrorIsEntity, prefabName); return; } Vector3 localPosition; Vector3 localRotationAngles; bool isOnTerrain; DetermineLocalTransformData(position, basePlayer, monument, out localPosition, out localRotationAngles, out isOnTerrain); var prefabData = new PrefabData { Id = Guid.NewGuid(), PrefabName = prefabName, Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain, }; var matchingMonuments = GetMonumentsByAliasOrShortName(monument.AliasOrShortName); profileController.Profile.AddData(monument.AliasOrShortName, prefabData); _profileStore.Save(profileController.Profile); profileController.SpawnNewData(prefabData, matchingMonuments); ReplyToPlayer(player, LangEntry.SpawnSuccess, matchingMonuments.Count, profileController.Profile.Name, monument.AliasOrShortName); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } [Command("masave")] private void CommandSave(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; EntityAdapter adapter; EntityController controller; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; if (!adapter.TrySaveAndApplyChanges()) { ReplyToPlayer(player, LangEntry.SaveNothingToDo); return; } ReplyToPlayer(player, LangEntry.SaveSuccess, controller.Adapters.Count, controller.Profile.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } [Command("makill")] private void CommandKill(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; BaseController controller; TransformAdapter adapter; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; // Capture adapter count before killing the controller. var numAdapters = controller.Adapters.Count; string monumentAliasOrShortName; controller.Profile.RemoveData(adapter.Data, out monumentAliasOrShortName); var profile = controller.Profile; _profileStore.Save(profile); var profileController = controller.ProfileController; var killRoutine = controller.Kill(adapter.Data); if (killRoutine != null) { profileController.StartCallbackRoutine(killRoutine, profileController.SetupIO); } var spawnGroupData = controller.Data as SpawnGroupData; var spawnPointData = adapter.Data as SpawnPointData; if (spawnGroupData != null && spawnPointData != null) { _undoManager.AddUndo(basePlayer, new UndoKillSpawnPoint(this, profileController, monumentAliasOrShortName, spawnGroupData, spawnPointData)); } else { _undoManager.AddUndo(basePlayer, new UndoKill(this, profileController, monumentAliasOrShortName, controller.Data)); } ReplyToPlayer(player, LangEntry.KillSuccess, GetAddonName(player, adapter.Data), numAdapters, profile.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } [Command("maundo")] private void CommandUndo(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (!_undoManager.TryUndo(basePlayer)) { ReplyToPlayer(player, LangEntry.UndoNotFound); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } } [Command("masetid")] private void CommandSetId(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (args.Length < 1 || !ComputerStation.IsValidIdentifier(args[0])) { ReplyToPlayer(player, LangEntry.CCTVSetIdSyntax, cmd); return; } CCTVController controller; if (!VerifyLookingAtAdapter(player, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; if (controller.EntityData.CCTV == null) { controller.EntityData.CCTV = new CCTVInfo(); } var hadIdentifier = !string.IsNullOrEmpty(controller.EntityData.CCTV.RCIdentifier); controller.EntityData.CCTV.RCIdentifier = args[0]; _profileStore.Save(controller.Profile); controller.StartHandleChangesRoutine(); ReplyToPlayer(player, LangEntry.CCTVSetIdSuccess, args[0], controller.Adapters.Count, controller.Profile.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: hadIdentifier); } [Command("masetdir")] private void CommandSetDirection(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; CCTVEntityAdapter adapter; CCTVController controller; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; var cctv = adapter.Entity as CCTV_RC; var direction = Vector3Ex.Direction(basePlayer.eyes.position, cctv.transform.position); direction = cctv.transform.InverseTransformDirection(direction); var lookAngles = BaseMountable.ConvertVector(Quaternion.LookRotation(direction).eulerAngles); if (controller.EntityData.CCTV == null) { controller.EntityData.CCTV = new CCTVInfo(); } controller.EntityData.CCTV.Pitch = lookAngles.x; controller.EntityData.CCTV.Yaw = lookAngles.y; _profileStore.Save(controller.Profile); controller.StartHandleChangesRoutine(); ReplyToPlayer(player, LangEntry.CCTVSetDirectionSuccess, controller.Adapters.Count, controller.Profile.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } [Command("maskull")] private void CommandSkull(IPlayer player, string cmd, string[] args) { if (player.IsServer || !VerifyHasPermission(player)) return; EntityController controller; EntityAdapter adapter; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; var skullTrophy = adapter.Entity as SkullTrophy; if (skullTrophy == null) { ReplyToPlayer(player, LangEntry.ErrorNoSuitableAddonFound); return; } if (args.Length == 0) { ReplyToPlayer(player, LangEntry.SkullNameSyntax, cmd); return; } var skullName = args[0]; var updatedSkullName = controller.EntityData.SkullName != skullName; controller.EntityData.SkullName = skullName; _profileStore.Save(controller.Profile); controller.StartHandleChangesRoutine(); ReplyToPlayer(player, LangEntry.SkullNameSetSuccess, skullName, controller.Adapters.Count, controller.Profile.Name); var basePlayer = player.Object as BasePlayer; _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: !updatedSkullName); } [Command("maskin")] private void CommandSkin(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; EntityAdapter adapter; EntityController controller; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; if (args.Length == 0) { ReplyToPlayer(player, LangEntry.SkinGet, adapter.Entity.skinID, cmd); return; } ulong skinId; if (!ulong.TryParse(args[0], out skinId)) { ReplyToPlayer(player, LangEntry.SkinSetSyntax, cmd); return; } string alternativeShortName; if (IsRedirectSkin(skinId, out alternativeShortName)) { ReplyToPlayer(player, LangEntry.SkinErrorRedirect, skinId, alternativeShortName); return; } var updatedExistingSkin = (controller.EntityData.Skin == 0) != (skinId == 0); controller.EntityData.Skin = skinId; _profileStore.Save(controller.Profile); controller.StartHandleChangesRoutine(); ReplyToPlayer(player, LangEntry.SkinSetSuccess, skinId, controller.Adapters.Count, controller.Profile.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: !updatedExistingSkin); } private void AddProfileDescription(StringBuilder sb, IPlayer player, ProfileController profileController) { foreach (var summaryEntry in GetProfileSummary(player, profileController.Profile)) { sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileDescribeItem, summaryEntry.AddonType, summaryEntry.AddonName, summaryEntry.Count, summaryEntry.MonumentName)); } } [Command("macardlevel")] private void CommandLevel(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; EntityAdapter adapter; EntityController controller; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; var cardReader = adapter.Entity as CardReader; if ((object)cardReader == null) { ReplyToPlayer(player, LangEntry.ErrorNoSuitableAddonFound); return; } int accessLevel; if (args.Length < 1 || !int.TryParse(args[0], result: out accessLevel) || accessLevel < 1 || accessLevel > 3) { ReplyToPlayer(player, LangEntry.CardReaderSetLevelSyntax, cmd); return; } if (cardReader.accessLevel != accessLevel) { adapter.EntityData.CardReaderLevel = (ushort)accessLevel; _profileStore.Save(controller.Profile); controller.StartHandleChangesRoutine(); } ReplyToPlayer(player, LangEntry.CardReaderSetLevelSuccess, adapter.EntityData.CardReaderLevel); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } [Command("mapuzzle")] private void CommandPuzzle(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (args.Length == 0) { SubCommandPuzzleHelp(player, cmd); return; } switch (args[0].ToLower()) { case "reset": { EntityAdapter adapter; EntityController controller; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound)) return; var ioEntity = adapter.Entity as IOEntity; var puzzleReset = ioEntity != null ? FindConnectedPuzzleReset(ioEntity) : null; if (puzzleReset == null) { ReplyToPlayer(player, LangEntry.PuzzleNotConnected, _uniqueNameRegistry.GetUniqueShortName(adapter.Entity.PrefabName)); return; } puzzleReset.DoReset(); puzzleReset.ResetTimer(); ReplyToPlayer(player, LangEntry.PuzzleResetSuccess); break; } case "add": case "remove": { var isAdd = args[0] == "add"; if (args.Length < 2) { ReplyToPlayer(player, isAdd ? LangEntry.PuzzleAddSpawnGroupSyntax : LangEntry.PuzzleRemoveSpawnGroupSyntax, cmd); return; } EntityAdapter adapter; EntityController controller; PuzzleReset puzzleReset; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound) || !VerifyEntityComponent(player, adapter.Entity, out puzzleReset, LangEntry.PuzzleNotPresent)) return; SpawnGroupController spawnGroupController; if (!VerifySpawnGroupFound(player, args[1], adapter.Monument, out spawnGroupController)) return; var spawnGroupData = spawnGroupController.SpawnGroupData; var spawnGroupId = spawnGroupData.Id; var puzzleData = controller.EntityData.EnsurePuzzleData(puzzleReset); if (!isAdd) { puzzleData.RemoveSpawnGroupId(spawnGroupId); } else if (!puzzleData.HasSpawnGroupId(spawnGroupId)) { puzzleData.AddSpawnGroupId(spawnGroupId); } controller.StartHandleChangesRoutine(); _profileStore.Save(controller.Profile); ReplyToPlayer(player, isAdd ? LangEntry.PuzzleAddSpawnGroupSuccess : LangEntry.PuzzleRemoveSpawnGroupSuccess, spawnGroupData.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: false); break; } case "set": { if (args.Length < 3) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ErrorSetSyntaxGeneric, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleSetHelpMaxPlayersBlockReset)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleSetHelpPlayerDetectionRadius)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleSetHelpSecondsBetweenResets)); player.Reply(sb.ToString()); return; } PuzzleOption puzzleOption; if (!VerifyValidEnumValue(player, args[1], out puzzleOption)) return; EntityAdapter adapter; EntityController controller; PuzzleReset puzzleReset; if (!VerifyLookingAtAdapter(player, out adapter, out controller, LangEntry.ErrorNoSuitableAddonFound) || !VerifyEntityComponent(player, adapter.Entity, out puzzleReset, LangEntry.PuzzleNotPresent)) return; var puzzleData = controller.EntityData.EnsurePuzzleData(puzzleReset); object setValue = args[2]; var showImmediate = true; switch (puzzleOption) { case PuzzleOption.PlayersBlockReset: { bool playerBlockReset; if (!VerifyValidBool(player, args[2], out playerBlockReset, LangEntry.ErrorSetSyntax, cmd, PuzzleOption.PlayersBlockReset)) return; puzzleData.PlayersBlockReset = playerBlockReset; setValue = playerBlockReset; showImmediate = false; break; } case PuzzleOption.PlayerDetectionRadius: { float playerDetectionRadius; if (!VerifyValidFloat(player, args[2], out playerDetectionRadius, LangEntry.ErrorSetSyntax, cmd, PuzzleOption.PlayerDetectionRadius)) return; puzzleData.PlayersBlockReset = true; puzzleData.PlayerDetectionRadius = playerDetectionRadius; setValue = playerDetectionRadius; break; } case PuzzleOption.SecondsBetweenResets: { float secondsBetweenResets; if (!VerifyValidFloat(player, args[2], out secondsBetweenResets, LangEntry.ErrorSetSyntax, cmd, PuzzleOption.SecondsBetweenResets)) return; puzzleData.SecondsBetweenResets = secondsBetweenResets; setValue = secondsBetweenResets; break; } } controller.StartHandleChangesRoutine(); _profileStore.Save(controller.Profile); ReplyToPlayer(player, LangEntry.PuzzleSetSuccess, puzzleOption, setValue); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: showImmediate); break; } default: { SubCommandPuzzleHelp(player, cmd); break; } } } private void SubCommandPuzzleHelp(IPlayer player, string cmd) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleHelpHeader, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleHelpReset, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleHelpSet, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleHelpAdd, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.PuzzleHelpRemove, cmd)); player.Reply(sb.ToString()); } [Command("maprofile")] private void CommandProfile(IPlayer player, string cmd, string[] args) { if (!_serverInitialized) return; if (!player.IsServer && !VerifyHasPermission(player)) return; if (args.Length == 0) { SubCommandProfileHelp(player); return; } var basePlayer = player.Object as BasePlayer; switch (args[0].ToLower()) { case "list": { var profileList = ProfileInfo.GetList(_data, _profileManager); if (profileList.Count == 0) { ReplyToPlayer(player, LangEntry.ProfileListEmpty); return; } var playerProfileName = player.IsServer ? null : _data.GetSelectedProfileName(player.Id); profileList = profileList .OrderByDescending(profile => profile.Enabled && profile.Name == playerProfileName) .ThenByDescending(profile => profile.Enabled) .ThenBy(profile => profile.Name) .ToList(); var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileListHeader)); foreach (var profile in profileList) { var messageName = profile.Enabled && profile.Name == playerProfileName ? LangEntry.ProfileListItemSelected : profile.Enabled ? LangEntry.ProfileListItemEnabled : LangEntry.ProfileListItemDisabled; sb.AppendLine(GetMessage(player.Id, messageName, profile.Name, GetAuthorSuffix(player, profile.Profile?.Author))); } player.Reply(sb.ToString()); break; } case "describe": { ProfileController controller; if (!VerifyProfile(player, args, out controller, LangEntry.ProfileDescribeSyntax)) return; if (controller.Profile.IsEmpty()) { ReplyToPlayer(player, LangEntry.ProfileEmpty, controller.Profile.Name); return; } var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileDescribeHeader, controller.Profile.Name)); AddProfileDescription(sb, player, controller); player.Reply(sb.ToString()); if (!player.IsServer) { _adapterDisplayManager.SetPlayerProfile(basePlayer, controller); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } break; } case "select": { if (player.IsServer) return; if (args.Length <= 1) { ReplyToPlayer(player, LangEntry.ProfileSelectSyntax); return; } ProfileController controller; if (!VerifyProfile(player, args, out controller, LangEntry.ProfileSelectSyntax)) return; var profile = controller.Profile; var profileName = profile.Name; _data.SetProfileSelected(player.Id, profileName); var wasEnabled = controller.IsEnabled; if (wasEnabled) { // Only save if the profile is not enabled, since enabling it will already save the main data file. _data.Save(); } else { Profile newProfileData; if (!VerifyCanLoadProfile(player, profileName, out newProfileData)) return; controller.Enable(newProfileData); } ReplyToPlayer(player, wasEnabled ? LangEntry.ProfileSelectSuccess : LangEntry.ProfileSelectEnableSuccess, profileName); _adapterDisplayManager.SetPlayerProfile(basePlayer, controller); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); break; } case "create": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.ProfileCreateSyntax); return; } var newName = DynamicConfigFile.SanitizeName(args[1]); if (string.IsNullOrWhiteSpace(newName)) { ReplyToPlayer(player, LangEntry.ProfileCreateSyntax); return; } if (!VerifyProfileNameAvailable(player, newName)) return; var controller = _profileManager.CreateProfile(newName, basePlayer?.displayName); if (!player.IsServer) { _data.SetProfileSelected(player.Id, newName); } _data.SetProfileEnabled(newName); ReplyToPlayer(player, LangEntry.ProfileCreateSuccess, controller.Profile.Name); break; } case "rename": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.ProfileRenameSyntax); return; } ProfileController controller; if (args.Length == 2) { controller = player.IsServer ? null : _profileManager.GetPlayerProfileController(player.Id); if (controller == null) { ReplyToPlayer(player, LangEntry.ProfileRenameSyntax); return; } } else if (!VerifyProfileExists(player, args[1], out controller)) return; string newName = DynamicConfigFile.SanitizeName(args.Length == 2 ? args[1] : args[2]); if (string.IsNullOrWhiteSpace(newName)) { ReplyToPlayer(player, LangEntry.ProfileRenameSyntax); return; } if (!VerifyProfileNameAvailable(player, newName)) return; // Cache the actual old name in case it was case-insensitive matched. var actualOldName = controller.Profile.Name; controller.Rename(newName); ReplyToPlayer(player, LangEntry.ProfileRenameSuccess, actualOldName, controller.Profile.Name); if (!player.IsServer) { _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } break; } case "reload": { ProfileController controller; if (!VerifyProfile(player, args, out controller, LangEntry.ProfileReloadSyntax)) return; if (!controller.IsEnabled) { ReplyToPlayer(player, LangEntry.ProfileNotEnabled, controller.Profile.Name); return; } Profile newProfileData; if (!VerifyCanLoadProfile(player, controller.Profile.Name, out newProfileData)) return; controller.Reload(newProfileData); ReplyToPlayer(player, LangEntry.ProfileReloadSuccess, controller.Profile.Name); if (!player.IsServer) { _adapterDisplayManager.SetPlayerProfile(basePlayer, controller); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } break; } case "enable": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.ProfileEnableSyntax); return; } ProfileController controller; Profile newProfileData; if (!VerifyProfileExists(player, args[1], out controller)) return; var profileName = controller.Profile.Name; if (controller.IsEnabled) { ReplyToPlayer(player, LangEntry.ProfileAlreadyEnabled, profileName); return; } if (!VerifyCanLoadProfile(player, controller.Profile.Name, out newProfileData)) return; controller.Enable(newProfileData); ReplyToPlayer(player, LangEntry.ProfileEnableSuccess, profileName); if (!player.IsServer) { _adapterDisplayManager.SetPlayerProfile(basePlayer, controller); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } break; } case "disable": { ProfileController controller; if (!VerifyProfile(player, args, out controller, LangEntry.ProfileDisableSyntax)) return; var profileName = controller.Profile.Name; if (!controller.IsEnabled) { ReplyToPlayer(player, LangEntry.ProfileAlreadyDisabled, profileName); return; } _profileManager.DisableProfile(controller); ReplyToPlayer(player, LangEntry.ProfileDisableSuccess, profileName); break; } case "clear": { if (args.Length <= 1) { ReplyToPlayer(player, LangEntry.ProfileClearSyntax); return; } ProfileController controller; if (!VerifyProfile(player, args, out controller, LangEntry.ProfileClearSyntax)) return; if (!controller.Profile.IsEmpty()) { controller.Clear(); } ReplyToPlayer(player, LangEntry.ProfileClearSuccess, controller.Profile.Name); break; } case "delete": { if (args.Length <= 1) { ReplyToPlayer(player, LangEntry.ProfileDeleteSyntax); return; } ProfileController controller; if (!VerifyProfile(player, args, out controller, LangEntry.ProfileDeleteSyntax)) return; var profileName = controller.Profile.Name; if (controller.IsEnabled && !controller.Profile.IsEmpty()) { ReplyToPlayer(player, LangEntry.ProfileDeleteBlocked, profileName); return; } _profileManager.DeleteProfile(controller); ReplyToPlayer(player, LangEntry.ProfileDeleteSuccess, profileName); break; } case "moveto": { BaseController addonController; if (!VerifyLookingAtAdapter(player, out addonController, LangEntry.ErrorNoSuitableAddonFound)) return; ProfileController newProfileController; if (!VerifyProfile(player, args, out newProfileController, LangEntry.ProfileMoveToSyntax)) return; var oldProfileController = addonController.ProfileController; var newProfile = newProfileController.Profile; var oldProfile = addonController.Profile; var data = addonController.Data; var addonName = GetAddonName(player, data); if (newProfileController == oldProfileController) { ReplyToPlayer(player, LangEntry.ProfileMoveToAlreadyPresent, addonName, oldProfile.Name); return; } string monumentAliasOrShortName; if (!oldProfile.RemoveData(data, out monumentAliasOrShortName)) { LogError($"Unexpected error: {data.GetType()} {data.Id} was not found in profile {oldProfile.Name}"); return; } _profileStore.Save(oldProfile); var killRoutine = addonController.Kill(); if (killRoutine != null) { oldProfileController.StartCallbackRoutine(killRoutine, oldProfileController.SetupIO); } newProfile.AddData(monumentAliasOrShortName, data); _profileStore.Save(newProfile); newProfileController.SpawnNewData(data, GetMonumentsByAliasOrShortName(monumentAliasOrShortName)); ReplyToPlayer(player, LangEntry.ProfileMoveToSuccess, addonName, oldProfile.Name, newProfile.Name); if (!player.IsServer) { _adapterDisplayManager.SetPlayerProfile(basePlayer, newProfileController); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } break; } case "install": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.ProfileInstallSyntax); return; } SharedCommandInstallProfile(player, args.Skip(1).ToArray()); break; } default: { SubCommandProfileHelp(player); break; } } } private void SubCommandProfileHelp(IPlayer player) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpHeader)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpList)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpDescribe)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpEnable)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpDisable)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpReload)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpSelect)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpCreate)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpRename)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpClear)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpDelete)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpMoveTo)); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileHelpInstall)); player.Reply(sb.ToString()); } [Command("mainstall")] private void CommandInstallProfile(IPlayer player, string cmd, string[] args) { if (!_serverInitialized) return; if (!player.IsServer && !VerifyHasPermission(player)) return; if (args.Length < 1) { ReplyToPlayer(player, LangEntry.ProfileInstallShorthandSyntax); return; } SharedCommandInstallProfile(player, args); } private void SharedCommandInstallProfile(IPlayer player, string[] args) { var url = args[0]; Uri parsedUri; if (!Uri.TryCreate(url, UriKind.Absolute, out parsedUri)) { var fallbackUrl = string.Format(DefaultUrlPattern, url); if (Uri.TryCreate(fallbackUrl, UriKind.Absolute, out parsedUri)) { url = fallbackUrl; } else { ReplyToPlayer(player, LangEntry.ProfileUrlInvalid, url); return; } } DownloadProfile( player, url, successCallback: profile => { profile.Name = DynamicConfigFile.SanitizeName(profile.Name); if (string.IsNullOrWhiteSpace(profile.Name)) { var urlDerivedProfileName = DynamicConfigFile.SanitizeName(parsedUri.Segments.LastOrDefault().Replace(".json", "")); if (string.IsNullOrEmpty(urlDerivedProfileName)) { LogError($"Unable to determine profile name from url: \"{url}\". Please ask the URL owner to supply a \"Name\" in the file."); ReplyToPlayer(player, LangEntry.ProfileInstallError, url); return; } profile.Name = urlDerivedProfileName; } if (OriginalProfileStore.IsOriginalProfile(profile.Name)) { LogError($"Profile \"{profile.Name}\" should not end with \"{OriginalProfileStore.OriginalSuffix}\"."); ReplyToPlayer(player, LangEntry.ProfileInstallError, url); return; } var profileController = _profileManager.GetProfileController(profile.Name); if (profileController != null && !profileController.Profile.IsEmpty()) { ReplyToPlayer(player, LangEntry.ProfileAlreadyExistsNotEmpty, profile.Name); return; } _profileStore.Save(profile); _originalProfileStore.Save(profile); if (profileController == null) { profileController = _profileManager.GetProfileController(profile.Name); } if (profileController == null) { LogError($"Profile \"{profile.Name}\" could not be found on disk after download from url: \"{url}\""); ReplyToPlayer(player, LangEntry.ProfileInstallError, url); return; } if (profileController.IsEnabled) { profileController.Reload(profile); } else { profileController.Enable(profile); } var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ProfileInstallSuccess, profile.Name, GetAuthorSuffix(player, profile.Author))); AddProfileDescription(sb, player, profileController); player.Reply(sb.ToString()); if (!player.IsServer) { var basePlayer = player.Object as BasePlayer; _adapterDisplayManager.SetPlayerProfile(basePlayer, profileController); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } }, errorCallback: player.Reply ); } [Command("mashow")] private void CommandShow(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; int duration = AdapterDisplayManager.DefaultDisplayDuration; string profileName = null; foreach (var arg in args) { int argIntValue; if (int.TryParse(arg, out argIntValue)) { duration = argIntValue; continue; } if (profileName == null) { profileName = arg; } } ProfileController profileController = null; if (profileName != null) { profileController = _profileManager.GetProfileController(profileName); if (profileController == null) { ReplyToPlayer(player, LangEntry.ProfileNotFound, profileName); return; } } _adapterDisplayManager.SetPlayerProfile(basePlayer, profileController); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, duration); ReplyToPlayer(player, LangEntry.ShowSuccess, FormatTime(duration)); } [Command("maspawngroup", "masg")] private void CommandSpawnGroup(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (args.Length < 1) { SubCommandSpawnGroupHelp(player, cmd); return; } switch (args[0].ToLower()) { case "create": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.SpawnGroupCreateSyntax, cmd); return; } var spawnGroupName = args[1]; ProfileController profileController; Vector3 position; BaseMonument monument; if (!VerifyMonumentFinderLoaded(player) || !VerifyProfileSelected(player, out profileController) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument) || !VerifySpawnGroupNameAvailable(player, profileController.Profile, monument, spawnGroupName)) return; Vector3 localPosition; Vector3 localRotationAngles; bool isOnTerrain; DetermineLocalTransformData(position, basePlayer, monument, out localPosition, out localRotationAngles, out isOnTerrain); var spawnGroupData = new SpawnGroupData { Id = Guid.NewGuid(), Name = spawnGroupName, SpawnPoints = new List { new SpawnPointData { Id = Guid.NewGuid(), Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain, Exclusive = true, SnapToGround = true, }, }, }; var matchingMonuments = GetMonumentsByAliasOrShortName(monument.AliasOrShortName); profileController.Profile.AddData(monument.AliasOrShortName, spawnGroupData); _profileStore.Save(profileController.Profile); profileController.SpawnNewData(spawnGroupData, matchingMonuments); ReplyToPlayer(player, LangEntry.SpawnGroupCreateSucces, spawnGroupName); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); break; } case "set": { if (args.Length < 3) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ErrorSetSyntaxGeneric, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpName)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpMaxPopulation)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpRespawnDelayMin)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpRespawnDelayMax)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpSpawnPerTickMin)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpSpawnPerTickMax)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpInitialSpawn)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpPreventDuplicates)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpPauseScheduleWhileFull)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupSetHelpRespawnWhenNearestPuzzleResets)); player.Reply(sb.ToString()); return; } SpawnGroupOption spawnGroupOption; if (!VerifyValidEnumValue(player, args[1], out spawnGroupOption)) return; SpawnPointAdapter spawnPointAdapter; SpawnGroupController spawnGroupController; if (!VerifyLookingAtAdapter(player, out spawnPointAdapter, out spawnGroupController, LangEntry.ErrorNoSpawnPointFound)) return; var spawnGroupData = spawnGroupController.SpawnGroupData; object setValue = args[2]; var showImmediate = true; switch (spawnGroupOption) { case SpawnGroupOption.Name: { if (!VerifySpawnGroupNameAvailable(player, spawnGroupController.Profile, spawnPointAdapter.Monument, args[2], spawnGroupController)) return; spawnGroupData.Name = args[2]; break; } case SpawnGroupOption.MaxPopulation: { int maxPopulation; if (!VerifyValidInt(player, args[2], out maxPopulation, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.MaxPopulation)) return; spawnGroupData.MaxPopulation = maxPopulation; break; } case SpawnGroupOption.RespawnDelayMin: { float respawnDelayMin; if (!VerifyValidFloat(player, args[2], out respawnDelayMin, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.RespawnDelayMin)) return; showImmediate = respawnDelayMin == 0 || spawnGroupData.RespawnDelayMax != 0; spawnGroupData.RespawnDelayMin = respawnDelayMin; spawnGroupData.RespawnDelayMax = Math.Max(respawnDelayMin, spawnGroupData.RespawnDelayMax); setValue = respawnDelayMin; break; } case SpawnGroupOption.RespawnDelayMax: { float respawnDelayMax; if (!VerifyValidFloat(player, args[2], out respawnDelayMax, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.RespawnDelayMax)) return; showImmediate = (respawnDelayMax == 0) == (spawnGroupData.RespawnDelayMax == 0); spawnGroupData.RespawnDelayMax = respawnDelayMax; spawnGroupData.RespawnDelayMin = Math.Min(spawnGroupData.RespawnDelayMin, respawnDelayMax); setValue = respawnDelayMax; break; } case SpawnGroupOption.SpawnPerTickMin: { int spawnPerTickMin; if (!VerifyValidInt(player, args[2], out spawnPerTickMin, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.SpawnPerTickMin)) return; spawnGroupData.SpawnPerTickMin = spawnPerTickMin; spawnGroupData.SpawnPerTickMax = Math.Max(spawnPerTickMin, spawnGroupData.SpawnPerTickMax); setValue = spawnPerTickMin; break; } case SpawnGroupOption.SpawnPerTickMax: { int spawnPerTickMax; if (!VerifyValidInt(player, args[2], out spawnPerTickMax, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.SpawnPerTickMax)) return; spawnGroupData.SpawnPerTickMax = spawnPerTickMax; spawnGroupData.SpawnPerTickMin = Math.Min(spawnGroupData.SpawnPerTickMin, spawnPerTickMax); setValue = spawnPerTickMax; break; } case SpawnGroupOption.InitialSpawn: { bool initialSpawn; if (!VerifyValidBool(player, args[2], out initialSpawn, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.PreventDuplicates)) return; spawnGroupData.InitialSpawn = initialSpawn; setValue = initialSpawn; showImmediate = false; break; } case SpawnGroupOption.PreventDuplicates: { bool preventDuplicates; if (!VerifyValidBool(player, args[2], out preventDuplicates, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.PreventDuplicates)) return; spawnGroupData.PreventDuplicates = preventDuplicates; setValue = preventDuplicates; showImmediate = false; break; } case SpawnGroupOption.PauseScheduleWhileFull: { bool pauseScheduleWhileFull; if (!VerifyValidBool(player, args[2], out pauseScheduleWhileFull, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.PauseScheduleWhileFull)) return; spawnGroupData.PauseScheduleWhileFull = pauseScheduleWhileFull; setValue = pauseScheduleWhileFull; showImmediate = false; break; } case SpawnGroupOption.RespawnWhenNearestPuzzleResets: { bool respawnWhenNearestPuzzleResets; if (!VerifyValidBool(player, args[2], out respawnWhenNearestPuzzleResets, LangEntry.ErrorSetSyntax, cmd, SpawnGroupOption.RespawnWhenNearestPuzzleResets)) return; spawnGroupData.RespawnWhenNearestPuzzleResets = respawnWhenNearestPuzzleResets; setValue = respawnWhenNearestPuzzleResets; showImmediate = false; break; } } spawnGroupController.UpdateSpawnGroups(); _profileStore.Save(spawnGroupController.Profile); ReplyToPlayer(player, LangEntry.SpawnGroupSetSuccess, spawnGroupData.Name, spawnGroupOption, setValue); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: showImmediate); break; } case "add": { var weight = 100; if (args.Length < 2 || args.Length >= 3 && !int.TryParse(args[2], out weight)) { ReplyToPlayer(player, LangEntry.SpawnGroupAddSyntax, cmd); return; } string prefabPath; if (!VerifyValidEntityPrefab(player, args[1], out prefabPath)) return; SpawnGroupController spawnGroupController; if (!VerifyLookingAtAdapter(player, out spawnGroupController, LangEntry.ErrorNoSpawnPointFound)) return; var updatedExistingEntry = false; var spawnGroupData = spawnGroupController.SpawnGroupData; var prefabData = spawnGroupData.Prefabs.FirstOrDefault(entry => entry.PrefabName == prefabPath); if (prefabData != null) { prefabData.Weight = weight; updatedExistingEntry = true; } else { prefabData = new WeightedPrefabData { PrefabName = prefabPath, Weight = weight, }; spawnGroupData.Prefabs.Add(prefabData); } spawnGroupController.UpdateSpawnGroups(); _profileStore.Save(spawnGroupController.Profile); ReplyToPlayer(player, LangEntry.SpawnGroupAddSuccess, _uniqueNameRegistry.GetUniqueShortName(prefabData.PrefabName), weight, spawnGroupData.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: updatedExistingEntry); break; } case "remove": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.SpawnGroupRemoveSyntax, cmd); return; } SpawnGroupController spawnGroupController; if (!VerifyLookingAtAdapter(player, out spawnGroupController, LangEntry.ErrorNoSpawnPointFound)) return; string desiredPrefab = args[1]; var spawnGroupData = spawnGroupController.SpawnGroupData; var matchingPrefabs = spawnGroupData.FindPrefabMatches(desiredPrefab, _uniqueNameRegistry); if (matchingPrefabs.Count == 0) { ReplyToPlayer(player, LangEntry.SpawnGroupRemoveNoMatch, spawnGroupData.Name, desiredPrefab); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); return; } if (matchingPrefabs.Count > 1) { ReplyToPlayer(player, LangEntry.SpawnGroupRemoveMultipleMatches, spawnGroupData.Name, desiredPrefab); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); return; } var prefabMatch = matchingPrefabs[0]; spawnGroupData.Prefabs.Remove(prefabMatch); spawnGroupController.StartKillSpawnedInstancesRoutine(prefabMatch.PrefabName); spawnGroupController.UpdateSpawnGroups(); _profileStore.Save(spawnGroupController.Profile); ReplyToPlayer(player, LangEntry.SpawnGroupRemoveSuccess, _uniqueNameRegistry.GetUniqueShortName(prefabMatch.PrefabName), spawnGroupData.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate: false); break; } case "spawn": case "tick": { SpawnGroupController spawnGroupController; if (!VerifyLookingAtAdapter(player, out spawnGroupController, LangEntry.ErrorNoSpawnPointFound)) return; spawnGroupController.StartSpawnRoutine(); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); break; } case "respawn": { SpawnGroupController spawnGroupController; if (!VerifyLookingAtAdapter(player, out spawnGroupController, LangEntry.ErrorNoSpawnPointFound)) return; spawnGroupController.StartRespawnRoutine(); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); break; } default: { SubCommandSpawnGroupHelp(player, cmd); break; } } } private void SubCommandSpawnGroupHelp(IPlayer player, string cmd) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpHeader, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpCreate, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpSet, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpAdd, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpRemove, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpSpawn, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnGroupHelpRespawn, cmd)); player.Reply(sb.ToString()); } [Command("maspawnpoint", "masp")] private void CommandSpawnPoint(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (args.Length < 1) { SubCommandSpawnPointHelp(player, cmd); return; } switch (args[0].ToLower()) { case "create": { if (args.Length < 2) { ReplyToPlayer(player, LangEntry.SpawnPointCreateSyntax, cmd); return; } Vector3 position; BaseMonument monument; if (!VerifyMonumentFinderLoaded(player) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument)) return; SpawnGroupController spawnGroupController; if (!VerifySpawnGroupFound(player, args[1], monument, out spawnGroupController)) return; Vector3 localPosition; Vector3 localRotationAngles; bool isOnTerrain; DetermineLocalTransformData(position, basePlayer, monument, out localPosition, out localRotationAngles, out isOnTerrain); var spawnPointData = new SpawnPointData { Id = Guid.NewGuid(), Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain, Exclusive = true, SnapToGround = true, }; spawnGroupController.SpawnGroupData.SpawnPoints.Add(spawnPointData); _profileStore.Save(spawnGroupController.Profile); spawnGroupController.CreateSpawnPoint(spawnPointData); ReplyToPlayer(player, LangEntry.SpawnPointCreateSuccess, spawnGroupController.SpawnGroupData.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); break; } case "set": { if (args.Length < 3) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.ErrorSetSyntaxGeneric, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointSetHelpExclusive)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointSetHelpSnapToGround)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointSetHelpCheckSpace)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointSetHelpRandomRotation)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointSetHelpRandomRadius)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointSetHelpPlayerDetectionRadius)); player.Reply(sb.ToString()); return; } SpawnPointOption spawnPointOption; if (!VerifyValidEnumValue(player, args[1], out spawnPointOption)) return; SpawnPointAdapter spawnPointAdapter; SpawnGroupController spawnGroupController; if (!VerifyLookingAtAdapter(player, out spawnPointAdapter, out spawnGroupController, LangEntry.ErrorNoSpawnPointFound)) return; var spawnPointData = spawnPointAdapter.SpawnPointData; object setValue = args[2]; switch (spawnPointOption) { case SpawnPointOption.Exclusive: { bool exclusive; if (!VerifyValidBool(player, args[2], out exclusive, LangEntry.SpawnGroupSetSuccess, LangEntry.ErrorSetSyntax, cmd, SpawnPointOption.Exclusive)) return; spawnPointData.Exclusive = exclusive; setValue = exclusive; break; } case SpawnPointOption.SnapToGround: { bool snapToGround; if (!VerifyValidBool(player, args[2], out snapToGround, LangEntry.ErrorSetSyntax, cmd, SpawnPointOption.SnapToGround)) return; spawnPointData.SnapToGround = snapToGround; setValue = snapToGround; break; } case SpawnPointOption.CheckSpace: { bool checkSpace; if (!VerifyValidBool(player, args[2], out checkSpace, LangEntry.ErrorSetSyntax, cmd, SpawnPointOption.CheckSpace)) return; spawnPointData.CheckSpace = checkSpace; setValue = checkSpace; break; } case SpawnPointOption.RandomRotation: { bool randomRotation; if (!VerifyValidBool(player, args[2], out randomRotation, LangEntry.ErrorSetSyntax, cmd, SpawnPointOption.RandomRotation)) return; spawnPointData.RandomRotation = randomRotation; setValue = randomRotation; break; } case SpawnPointOption.RandomRadius: { float radius; if (!VerifyValidFloat(player, args[2], out radius, LangEntry.ErrorSetSyntax, cmd, SpawnPointOption.RandomRadius)) return; spawnPointData.RandomRadius = radius; setValue = radius; break; } case SpawnPointOption.PlayerDetectionRadius: { float radius; if (!VerifyValidFloat(player, args[2], out radius, LangEntry.ErrorSetSyntax, cmd, SpawnPointOption.PlayerDetectionRadius)) return; spawnPointData.PlayerDetectionRadius = radius; setValue = radius; break; } } _profileStore.Save(spawnGroupController.Profile); ReplyToPlayer(player, LangEntry.SpawnPointSetSuccess, spawnPointOption, setValue); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); break; } default: { SubCommandSpawnPointHelp(player, cmd); break; } } } private void SubCommandSpawnPointHelp(IPlayer player, string cmd) { var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointHelpHeader, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointHelpCreate, cmd)); sb.AppendLine(GetMessage(player.Id, LangEntry.SpawnPointHelpSet, cmd)); player.Reply(sb.ToString()); } [Command("mapaste")] private void CommandPaste(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (args.Length < 1) { ReplyToPlayer(player, LangEntry.PasteSyntax); return; } if (!PasteUtils.IsCopyPasteCompatible(CopyPaste)) { ReplyToPlayer(player, LangEntry.PasteNotCompatible); return; } ProfileController profileController; Vector3 position; BaseMonument monument; if (!VerifyMonumentFinderLoaded(player) || !VerifyProfileSelected(player, out profileController) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument)) return; var pasteName = args[0]; if (!PasteUtils.DoesPasteExist(pasteName)) { ReplyToPlayer(player, LangEntry.PasteNotFound, pasteName); return; } Vector3 localPosition; Vector3 localRotationAngles; bool isOnTerrain; DetermineLocalTransformData(position, basePlayer, monument, out localPosition, out localRotationAngles, out isOnTerrain, flipRotation: false); var pasteData = new PasteData { Id = Guid.NewGuid(), Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain, Filename = pasteName, }; var matchingMonuments = GetMonumentsByAliasOrShortName(monument.AliasOrShortName); profileController.Profile.AddData(monument.AliasOrShortName, pasteData); _profileStore.Save(profileController.Profile); profileController.SpawnNewData(pasteData, matchingMonuments); ReplyToPlayer(player, LangEntry.PasteSuccess, pasteName, monument.AliasOrShortName, matchingMonuments.Count, profileController.Profile.Name); _adapterDisplayManager.ShowAllRepeatedly(basePlayer); } private void AddSpawnGroupInfo(IPlayer player, StringBuilder sb, SpawnGroup spawnGroup, int spawnPointCount) { sb.AppendLine($"{GetMessage(player.Id, LangEntry.ShowHeaderVanillaSpawnGroup, spawnGroup.name)}"); sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelSpawnPoints, spawnPointCount)); if ((int)spawnGroup.Tier != -1) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelTiers, string.Join(", ", GetTierList(spawnGroup.Tier)))); } if (spawnGroup.wantsInitialSpawn) { if (spawnGroup.temporary) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelSpawnWhenParentSpawns)); } else if (spawnGroup.forceInitialSpawn) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelSpawnOnServerStart)); } else { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelSpawnOnMapWipe)); } } if (spawnGroup.preventDuplicates && spawnGroup.prefabs.Count > 1) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelPreventDuplicates)); } sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelPopulation, spawnGroup.currentPopulation, spawnGroup.maxPopulation)); sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelRespawnPerTick, spawnGroup.numToSpawnPerTickMin, spawnGroup.numToSpawnPerTickMax)); if (!spawnGroup.temporary) { if (!float.IsPositiveInfinity(spawnGroup.respawnDelayMin)) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelRespawnDelay, FormatTime(spawnGroup.respawnDelayMin), FormatTime(spawnGroup.respawnDelayMax))); } var nextSpawnTime = GetTimeToNextSpawn(spawnGroup); if (!float.IsPositiveInfinity(nextSpawnTime) && SingletonComponent.Instance.SpawnGroups.Contains(spawnGroup)) { sb.AppendLine(GetMessage( player.Id, LangEntry.ShowLabelNextSpawn, nextSpawnTime <= 0 ? GetMessage(player.Id, LangEntry.ShowLabelNextSpawnQueued) : FormatTime(Mathf.CeilToInt(nextSpawnTime)) )); } } if (spawnGroup.prefabs.Count > 0) { var totalWeight = 0; foreach (var prefab in spawnGroup.prefabs) { totalWeight += prefab.weight; } sb.AppendLine(_plugin.GetMessage(player.Id, LangEntry.ShowLabelEntities)); foreach (var prefabEntry in spawnGroup.prefabs) { var relativeChance = (float)prefabEntry.weight / totalWeight; sb.AppendLine(_plugin.GetMessage(player.Id, LangEntry.ShowLabelEntityDetail, _uniqueNameRegistry.GetUniqueShortName(prefabEntry.prefab.resourcePath), prefabEntry.weight, relativeChance)); } } else { sb.AppendLine(_plugin.GetMessage(player.Id, LangEntry.ShowLabelNoEntities)); } } [Command("mashowvanilla")] private void CommandShowVanillaSpawns(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; Transform parentObject = null; RaycastHit hit; if (TryRaycast(basePlayer, out hit)) { parentObject = hit.GetEntity()?.transform; } if (parentObject == null) { Vector3 position; BaseMonument monument; if (!VerifyMonumentFinderLoaded(player) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument)) return; parentObject = monument.Object.transform; } var spawnerList = parentObject.GetComponentsInChildren(); if (spawnerList.Length == 0) { var grandParent = parentObject.transform.parent; if (grandParent != null && grandParent != parentObject.transform.root) { parentObject = grandParent; spawnerList = parentObject.GetComponentsInChildren(); } } if (spawnerList.Length == 0) { ReplyToPlayer(player, LangEntry.ShowVanillaNoSpawnPoints, parentObject.name); return; } var _selectedColorIndex = 0; var sb = new StringBuilder(); var playerPosition = basePlayer.transform.position; foreach (var spawner in spawnerList) { var spawnGroup = spawner as SpawnGroup; if (spawnGroup != null) { var spawnPointList = spawnGroup.spawnPoints; if (spawnPointList == null || spawnPointList.Length == 0) { spawnPointList = spawnGroup.GetComponentsInChildren(); } var color = _distinctColors[_selectedColorIndex++]; if (_selectedColorIndex >= _distinctColors.Length) { _selectedColorIndex = 0; } var tierMask = (int)spawnGroup.Tier; if (spawnPointList.Length == 0) { AddSpawnGroupInfo(player, sb, spawnGroup, spawnPointList.Length); var spawnGroupPosition = spawnGroup.transform.position; Ddraw.Sphere(basePlayer, spawnGroupPosition, 0.5f, color, ShowVanillaDuration); Ddraw.Text(basePlayer, spawnGroupPosition + new Vector3(0, tierMask > 0 ? Mathf.Log(tierMask, 2) : 0, 0), sb.ToString(), color, ShowVanillaDuration); sb.Clear(); continue; } BaseSpawnPoint closestSpawnPoint = null; var closestDistanceSquared = float.MaxValue; foreach (var spawnPoint in spawnPointList) { var distanceSquared = (playerPosition - spawnPoint.transform.position).sqrMagnitude; if (distanceSquared < closestDistanceSquared) { closestSpawnPoint = spawnPoint; closestDistanceSquared = distanceSquared; } } var closestSpawnPointPosition = closestSpawnPoint.transform.position; foreach (var spawnPoint in spawnPointList) { sb.AppendLine($"{GetMessage(player.Id, LangEntry.ShowHeaderVanillaSpawnPoint, spawnGroup.name)}"); var booleanProperties = new List(); var genericSpawnPoint = spawnPoint as GenericSpawnPoint; if (genericSpawnPoint != null) { booleanProperties.Add(_plugin.GetMessage(player.Id, LangEntry.ShowLabelSpawnPointExclusive)); if (genericSpawnPoint.randomRot) { booleanProperties.Add(_plugin.GetMessage(player.Id, LangEntry.ShowLabelSpawnPointRandomRotation)); } if (genericSpawnPoint.dropToGround) { booleanProperties.Add(_plugin.GetMessage(player.Id, LangEntry.ShowLabelSpawnPointSnapToGround)); } } var spaceCheckingSpawnPoint = spawnPoint as SpaceCheckingSpawnPoint; if (spaceCheckingSpawnPoint != null) { booleanProperties.Add(_plugin.GetMessage(player.Id, LangEntry.ShowLabelSpawnPointCheckSpace)); } if (booleanProperties.Count > 0) { sb.AppendLine(_plugin.GetMessage(player.Id, LangEntry.ShowLabelFlags, string.Join(" | ", booleanProperties))); } var radialSpawnPoint = spawnPoint as RadialSpawnPoint; if (radialSpawnPoint != null) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelSpawnPointRandomRadius, radialSpawnPoint.radius)); } if (spawnPoint == closestSpawnPoint) { sb.AppendLine(AdapterDisplayManager.Divider); AddSpawnGroupInfo(player, sb, spawnGroup, spawnPointList.Length); } var spawnPointTransform = spawnPoint.transform; var spawnPointPosition = spawnPointTransform.position; Ddraw.ArrowThrough(basePlayer, spawnPointPosition + AdapterDisplayManager.ArrowVerticalOffeset, spawnPointTransform.rotation, 1, 0.15f, color, ShowVanillaDuration); Ddraw.Sphere(basePlayer, spawnPointPosition, 0.5f, color, ShowVanillaDuration); Ddraw.Text(basePlayer, spawnPointPosition + new Vector3(0, tierMask > 0 ? Mathf.Log(tierMask, 2) : 0, 0), sb.ToString(), color, ShowVanillaDuration); if (spawnPoint != closestSpawnPoint) { Ddraw.Arrow(basePlayer, closestSpawnPointPosition + AdapterDisplayManager.ArrowVerticalOffeset, spawnPointPosition + AdapterDisplayManager.ArrowVerticalOffeset, 0.25f, color, ShowVanillaDuration); } sb.Clear(); } continue; } var individualSpawner = spawner as IndividualSpawner; if (individualSpawner != null) { var color = _distinctColors[_selectedColorIndex++]; if (_selectedColorIndex >= _distinctColors.Length) { _selectedColorIndex = 0; } sb.AppendLine($"{GetMessage(player.Id, LangEntry.ShowHeaderVanillaIndividualSpawnPoint, individualSpawner.name)}"); sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelFlags, $"{GetMessage(player.Id, LangEntry.ShowLabelSpawnPointExclusive)} | {GetMessage(player.Id, LangEntry.ShowLabelSpawnPointCheckSpace)}")); if (individualSpawner.oneTimeSpawner) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelSpawnOnMapWipe)); } else { if (!float.IsPositiveInfinity(individualSpawner.respawnDelayMin)) { sb.AppendLine(GetMessage(player.Id, LangEntry.ShowLabelRespawnDelay, FormatTime(individualSpawner.respawnDelayMin), FormatTime(individualSpawner.respawnDelayMax))); } var nextSpawnTime = GetTimeToNextSpawn(individualSpawner); if (!individualSpawner.IsSpawned && !float.IsPositiveInfinity(nextSpawnTime)) { sb.AppendLine(GetMessage( player.Id, LangEntry.ShowLabelNextSpawn, nextSpawnTime <= 0 ? GetMessage(player.Id, LangEntry.ShowLabelNextSpawnQueued) : FormatTime(Mathf.CeilToInt(nextSpawnTime)) )); } } sb.AppendLine(GetMessage(player.Id, LangEntry.ShowHeaderEntity, _uniqueNameRegistry.GetUniqueShortName(individualSpawner.entityPrefab.resourcePath))); var spawnerTransform = individualSpawner.transform; var spawnPointPosition = spawnerTransform.position; Ddraw.ArrowThrough(basePlayer, spawnPointPosition + AdapterDisplayManager.ArrowVerticalOffeset, spawnerTransform.rotation, 1f, 0.15f, color, ShowVanillaDuration); Ddraw.Sphere(basePlayer, spawnPointPosition, 0.5f, color, ShowVanillaDuration); Ddraw.Text(basePlayer, spawnPointPosition, sb.ToString(), color, ShowVanillaDuration); sb.Clear(); continue; } } } [Command("magenerate")] private void CommandGenerateSpawnPointProfile(IPlayer player, string cmd, string[] args) { Vector3 position; BaseMonument monument; BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player) || !VerifyMonumentFinderLoaded(player) || !VerifyHitPosition(player, out position) || !VerifyAtMonument(player, position, out monument)) return; var parentObject = monument.Object.transform; var spawnerList = parentObject.GetComponentsInChildren(); if (spawnerList.Length == 0) { var grandParent = parentObject.transform.parent; if (grandParent != null && grandParent != parentObject.transform.root) { parentObject = grandParent; spawnerList = parentObject.GetComponentsInChildren(); } } if (spawnerList.Length == 0) { ReplyToPlayer(player, LangEntry.ShowVanillaNoSpawnPoints, parentObject.name); return; } var monumentTierMask = GetMonumentTierMask(monument.Position); var monumentTierList = GetTierList(monumentTierMask); var spawnGroupsSpecifyTier = false; var spawnGroupDataList = new List(); foreach (var spawner in spawnerList) { var spawnGroup = spawner as SpawnGroup; if (spawnGroup != null) { if (spawnGroup.spawnPoints.Length == 0) continue; if ((int)spawnGroup.Tier != -1) { spawnGroupsSpecifyTier = true; if ((spawnGroup.Tier & monumentTierMask) == 0) { // Don't add spawn groups with different tiers. This can be improved later. continue; } } var spawnGroupData = new SpawnGroupData { Id = Guid.NewGuid(), Name = spawnGroup.name, MaxPopulation = spawnGroup.maxPopulation, RespawnDelayMin = spawnGroup.respawnDelayMin, RespawnDelayMax = spawnGroup.respawnDelayMax, SpawnPerTickMin = spawnGroup.numToSpawnPerTickMin, SpawnPerTickMax = spawnGroup.numToSpawnPerTickMax, PreventDuplicates = spawnGroup.preventDuplicates, }; spawnGroupDataList.Add(spawnGroupData); foreach (var prefabEntry in spawnGroup.prefabs) { spawnGroupData.Prefabs.Add(new WeightedPrefabData { PrefabName = prefabEntry.prefab.resourcePath, Weight = prefabEntry.weight, }); } foreach (var spawnPoint in spawnGroup.spawnPoints) { var spawnPointData = new SpawnPointData { Id = Guid.NewGuid(), Position = monument.InverseTransformPoint(spawnPoint.transform.position), RotationAngles = (Quaternion.Inverse(monument.Rotation) * spawnPoint.transform.rotation).eulerAngles, }; var genericSpawnPoint = spawnPoint as GenericSpawnPoint; if (genericSpawnPoint != null) { spawnPointData.Exclusive = true; spawnPointData.RandomRotation = genericSpawnPoint.randomRot; spawnPointData.SnapToGround = genericSpawnPoint.dropToGround; } var radialSpawnPoint = spawnPoint as RadialSpawnPoint; if (radialSpawnPoint != null) { spawnPointData.RandomRotation = true; spawnPointData.RandomRadius = radialSpawnPoint.radius; } if (spawnPoint is SpaceCheckingSpawnPoint) { spawnPointData.CheckSpace = true; } spawnGroupData.SpawnPoints.Add(spawnPointData); } continue; } } var tierSuffix = spawnGroupsSpecifyTier && monumentTierList.Count > 0 ? $"_{string.Join("_", monumentTierList)}" : string.Empty; var profileName = $"{monument.AliasOrShortName}{tierSuffix}_vanilla_generated"; var profile = _profileStore.Create(profileName, basePlayer.displayName); foreach (var data in spawnGroupDataList) { profile.AddData(monument.AliasOrShortName, data); } _profileStore.Save(profile); ReplyToPlayer(player, LangEntry.GenerateSuccess, profileName); } [Command("mawire")] private void CommandWire(IPlayer player, string cmd, string[] args) { BasePlayer basePlayer; if (!VerifyPlayer(player, out basePlayer) || !VerifyHasPermission(player)) return; if (_wireToolManager.HasPlayer(basePlayer) && args.Length == 0) { _wireToolManager.StopSession(basePlayer); return; } WireColour? wireColor; if (args.Length > 0) { WireColour parsedWireColor; if (StringUtils.EqualsCaseInsensitive(args[0], "invisible")) { wireColor = null; } else if (Enum.TryParse(args[0], ignoreCase: true, result: out parsedWireColor)) { wireColor = parsedWireColor; } else { ReplyToPlayer(player, LangEntry.WireToolInvalidColor, args[0]); return; } } else { wireColor = WireColour.Default; } var activeItemShortName = basePlayer.GetActiveItem()?.info.shortname; if (activeItemShortName != "wiretool" && activeItemShortName != "hosetool") { ReplyToPlayer(player, LangEntry.WireToolNotEquipped); return; } _wireToolManager.StartOrUpdateSession(basePlayer, wireColor); ChatMessage(basePlayer, LangEntry.WireToolActivated, wireColor?.ToString() ?? GetMessage(player.Id, LangEntry.WireToolInvisible)); } [Command("mahelp")] private void CommandHelp(IPlayer player, string cmd, string[] args) { if (!VerifyHasPermission(player)) return; var sb = new StringBuilder(); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpHeader)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSpawn)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpPrefab)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpKill)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpUndo)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSave)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSkin)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSetId)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSetDir)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSkull)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpCardReaderLevel)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpPuzzle)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSpawnGroup)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpSpawnPoint)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpPaste)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpShow)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpShowVanilla)); sb.AppendLine(GetMessage(player.Id, LangEntry.HelpProfile)); player.Reply(sb.ToString()); } #endregion #region API private Dictionary API_RegisterCustomAddon(Plugin plugin, string addonName, Dictionary addonSpec) { LogWarning($"API_RegisterCustomAddon is experimental and may be changed or removed in future updates."); var addonDefinition = CustomAddonDefinition.FromDictionary(addonName, plugin, addonSpec); if (addonDefinition.Spawn == null) { LogError($"Unable to register custom addon \"{addonName}\" for plugin {plugin.Name} due to missing Spawn method."); return null; } if (addonDefinition.Kill == null) { LogError($"Unable to register custom addon \"{addonName}\" for plugin {plugin.Name} due to missing Kill method."); return null; } Plugin otherPlugin; if (_customAddonManager.IsRegistered(addonName, out otherPlugin)) { LogError($"Unable to register custom addon \"{addonName}\" for plugin {plugin.Name} because it's already been registered by plugin {otherPlugin.Name}."); return null; } _customAddonManager.RegisterAddon(addonDefinition); return addonDefinition.ToApiResult(_profileStore); } #endregion #region Utilities #region Helper Methods - Command Checks private bool VerifyPlayer(IPlayer player, out BasePlayer basePlayer) { if (player.IsServer) { basePlayer = null; return false; } basePlayer = player.Object as BasePlayer; return true; } private bool VerifyHasPermission(IPlayer player, string perm = PermissionAdmin) { if (player.HasPermission(perm)) return true; ReplyToPlayer(player, LangEntry.ErrorNoPermission); return false; } private bool VerifyValidInt(IPlayer player, string arg, out int value, LangEntry errorLangEntry, params object[] args) { if (int.TryParse(arg, out value)) return true; ReplyToPlayer(player, errorLangEntry, args); return false; } private bool VerifyValidFloat(IPlayer player, string arg, out float value, LangEntry errorLangEntry, params object[] args) { if (float.TryParse(arg, out value)) return true; ReplyToPlayer(player, errorLangEntry, args); return false; } private bool VerifyValidBool(IPlayer player, string arg, out bool value, LangEntry errorLangEntry, params object[] args) { if (BooleanParser.TryParse(arg, out value)) return true; ReplyToPlayer(player, errorLangEntry, args); return false; } private bool VerifyValidEnumValue(IPlayer player, string arg, out T enumValue) where T : struct { if (TryParseEnum(arg, out enumValue)) return true; ReplyToPlayer(player, LangEntry.ErrorSetUnknownOption, arg); return false; } private bool VerifyMonumentFinderLoaded(IPlayer player) { if (MonumentFinder != null) return true; ReplyToPlayer(player, LangEntry.ErrorMonumentFinderNotLoaded); return false; } private bool VerifyProfileSelected(IPlayer player, out ProfileController profileController) { profileController = _profileManager.GetPlayerProfileControllerOrDefault(player.Id); if (profileController != null) return true; ReplyToPlayer(player, LangEntry.SpawnErrorNoProfileSelected); return false; } private bool VerifyHitPosition(IPlayer player, out Vector3 position) { if (TryGetHitPosition(player.Object as BasePlayer, out position)) return true; ReplyToPlayer(player, LangEntry.ErrorNoSurface); return false; } private bool VerifyAtMonument(IPlayer player, Vector3 position, out BaseMonument closestMonument) { closestMonument = GetClosestMonument(player.Object as BasePlayer, position); if (closestMonument == null) { ReplyToPlayer(player, LangEntry.ErrorNoMonuments); return false; } if (!closestMonument.IsInBounds(position)) { var closestPoint = closestMonument.ClosestPointOnBounds(position); var distance = (position - closestPoint).magnitude; ReplyToPlayer(player, LangEntry.ErrorNotAtMonument, closestMonument.AliasOrShortName, distance.ToString("f1")); return false; } return true; } private bool VerifyValidModderPrefab(IPlayer player, string[] args, out string prefabPath) { prefabPath = null; var prefabArg = args.FirstOrDefault(); if (string.IsNullOrWhiteSpace(prefabArg) || IsKeyBindArg(prefabArg)) { ReplyToPlayer(player, LangEntry.PrefabErrorSyntax, prefabArg); return false; } var prefabMatches = SearchUtils.FindModderPrefabMatches(prefabArg); if (prefabMatches.Count == 1) { prefabPath = prefabMatches.First().ToLower(); return true; } if (prefabMatches.Count == 0) { ReplyToPlayer(player, LangEntry.PrefabErrorNotFound, prefabArg); return false; } // Multiple matches were found, so print them all to the player. var replyMessage = GetMessage(player.Id, LangEntry.SpawnErrorMultipleMatches); foreach (var matchingPrefabPath in prefabMatches) { replyMessage += $"\n{matchingPrefabPath}"; } player.Reply(replyMessage); return false; } private bool VerifyValidEntityPrefab(IPlayer player, string prefabArg, out string prefabPath) { prefabPath = null; var prefabMatches = SearchUtils.FindEntityPrefabMatches(prefabArg, _uniqueNameRegistry); if (prefabMatches.Count == 1) { prefabPath = prefabMatches.First().ToLower(); return true; } if (prefabMatches.Count == 0) { ReplyToPlayer(player, LangEntry.SpawnErrorEntityNotFound, prefabArg); return false; } // Multiple matches were found, so print them all to the player. var replyMessage = GetMessage(player.Id, LangEntry.SpawnErrorMultipleMatches); foreach (var matchingPrefabPath in prefabMatches) { replyMessage += $"\n{_uniqueNameRegistry.GetUniqueShortName(matchingPrefabPath)}"; } player.Reply(replyMessage); return false; } private bool VerifyValidEntityPrefabOrCustomAddon(IPlayer player, string prefabArg, out string prefabPath, out CustomAddonDefinition addonDefinition) { prefabPath = null; addonDefinition = null; var prefabMatches = SearchUtils.FindEntityPrefabMatches(prefabArg, _uniqueNameRegistry); var customAddonMatches = SearchUtils.FindCustomAddonMatches(prefabArg, _customAddonManager.GetAllAddons()); var matchCount = prefabMatches.Count + customAddonMatches.Count; if (matchCount == 0) { ReplyToPlayer(player, LangEntry.SpawnErrorEntityOrAddonNotFound, prefabArg); return false; } if (matchCount == 1) { if (prefabMatches.Count == 1) { prefabPath = prefabMatches.First().ToLower(); } else { addonDefinition = customAddonMatches.First(); } return true; } // Multiple matches were found, so print them all to the player. var replyMessage = GetMessage(player.Id, LangEntry.SpawnErrorMultipleMatches); foreach (var matchingPrefabPath in prefabMatches) { replyMessage += $"\n{_uniqueNameRegistry.GetUniqueShortName(matchingPrefabPath)}"; } foreach (var matchingAddonDefinition in customAddonMatches) { replyMessage += $"\n{matchingAddonDefinition.AddonName}"; } player.Reply(replyMessage); return false; } private bool VerifyValidEntityPrefabOrDeployable(IPlayer player, string[] args, out string prefabPath, out CustomAddonDefinition addonDefinition, out ulong skinId) { var prefabArg = args.FirstOrDefault(); skinId = 0; if (!string.IsNullOrWhiteSpace(prefabArg) && !IsKeyBindArg(prefabArg)) return VerifyValidEntityPrefabOrCustomAddon(player, prefabArg, out prefabPath, out addonDefinition); addonDefinition = null; var basePlayer = player.Object as BasePlayer; var deployablePrefab = DeterminePrefabFromPlayerActiveDeployable(basePlayer, out skinId); if (!string.IsNullOrEmpty(deployablePrefab)) { prefabPath = deployablePrefab; return true; } prefabPath = null; ReplyToPlayer(player, LangEntry.SpawnErrorSyntax); return false; } private bool VerifyLookingAtAdapter(IPlayer player, out AdapterFindResult findResult, LangEntry errorLangEntry) where TAdapter : TransformAdapter where TController : BaseController { var basePlayer = player.Object as BasePlayer; RaycastHit hit; var hitResult = FindHitAdapter(basePlayer, out hit); if (hitResult.Controller != null) { // Found a suitable entity via direct hit. findResult = hitResult; return true; } var nearbyResult = FindClosestNearbyAdapter(hit.point); if (nearbyResult.Controller != null) { // Found a suitable nearby entity. findResult = nearbyResult; return true; } if (hitResult.Entity != null && hitResult.Component == null) { // Found an entity via direct hit, but it does not belong to Monument Addons. ReplyToPlayer(player, LangEntry.ErrorEntityNotEligible); } else { // Maybe found an entity, but it did not match the adapter/controller type. ReplyToPlayer(player, errorLangEntry); } findResult = default(AdapterFindResult); return false; } private bool VerifyLookingAtAdapter(IPlayer player, out TAdapter adapter, out TController controller, LangEntry errorLangEntry) where TAdapter : TransformAdapter where TController : BaseController { AdapterFindResult findResult; var result = VerifyLookingAtAdapter(player, out findResult, errorLangEntry); adapter = findResult.Adapter; controller = findResult.Controller; return result; } // Convenient method that does not require an adapter type. private bool VerifyLookingAtAdapter(IPlayer player, out TController controller, LangEntry errorLangEntry) where TController : BaseController { AdapterFindResult findResult; var result = VerifyLookingAtAdapter(player, out findResult, errorLangEntry); controller = findResult.Controller; return result; } private bool VerifySpawnGroupFound(IPlayer player, string partialGroupName, BaseMonument monument, out SpawnGroupController spawnGroupController) { var matches = FindSpawnGroups(partialGroupName, monument.AliasOrShortName, partialMatch: true).ToList(); spawnGroupController = matches.FirstOrDefault(); if (matches.Count == 1) return true; if (matches.Count == 0) { ReplyToPlayer(player, LangEntry.SpawnGroupNotFound, partialGroupName); return false; } var playerProfileController = _profileManager.GetPlayerProfileControllerOrDefault(player.Id); // Multiple matches found, try to narrow it down. for (var i = matches.Count - 1; i >= 0; i--) { var match = matches[i]; if (match.ProfileController != playerProfileController) { // Remove any controllers that don't match the player's selected profile. matches.Remove(match); } } if (matches.Count == 1) { spawnGroupController = matches[0]; return true; } ReplyToPlayer(player, LangEntry.SpawnGroupMultipeMatches, partialGroupName); return false; } private bool VerifyProfileNameAvailable(IPlayer player, string profileName) { if (!_profileManager.ProfileExists(profileName)) return true; ReplyToPlayer(player, LangEntry.ProfileAlreadyExists, profileName); return false; } private bool VerifyProfileExists(IPlayer player, string profileName, out ProfileController controller) { try { controller = _profileManager.GetProfileController(profileName); if (controller != null) return true; } catch (JsonReaderException ex) { controller = null; player.Reply("{0}", string.Empty, ex.Message); return false; } ReplyToPlayer(player, LangEntry.ProfileNotFound, profileName); return false; } private bool VerifyProfile(IPlayer player, string[] args, out ProfileController controller, LangEntry syntaxLangEntry) { if (args.Length <= 1) { controller = player.IsServer ? null : _profileManager.GetPlayerProfileControllerOrDefault(player.Id); if (controller != null) return true; ReplyToPlayer(player, syntaxLangEntry); return false; } return VerifyProfileExists(player, args[1], out controller); } private bool VerifySpawnGroupNameAvailable(IPlayer player, Profile profile, BaseMonument monument, string spawnGroupName, SpawnGroupController spawnGroupController = null) { var matches = FindSpawnGroups(spawnGroupName, monument.AliasOrShortName, profile).ToList(); if (matches.Count == 0) return true; // Allow renaming a spawn group with different case. if (spawnGroupController != null && matches.Count == 1 && matches[0] == spawnGroupController) return true; ReplyToPlayer(player, LangEntry.SpawnGroupCreateNameInUse, spawnGroupName, monument.AliasOrShortName, profile.Name); return false; } private bool VerifyEntityComponent(IPlayer player, BaseEntity entity, out T component, LangEntry errorLangEntry) where T : UnityEngine.Component { if (entity.gameObject.TryGetComponent(out component)) return true; ReplyToPlayer(player, errorLangEntry); return false; } private bool VerifyCanLoadProfile(IPlayer player, string profileName, out Profile profile) { string errorMessage; if (!_profileStore.TryLoad(profileName, out profile, out errorMessage)) { player.Reply("{0}", string.Empty, errorMessage); } return true; } #endregion #region Helper Methods - Finding Adapters private struct AdapterFindResult where TAdapter : TransformAdapter where TController : BaseController { public BaseEntity Entity; public MonumentEntityComponent Component; public TAdapter Adapter; public TController Controller; public AdapterFindResult(BaseEntity entity) { Entity = entity; Component = MonumentEntityComponent.GetForEntity(entity); Adapter = Component?.Adapter as TAdapter; Controller = Adapter?.Controller as TController; } public AdapterFindResult(TAdapter adapter, TController controller) { Entity = null; Component = null; Adapter = adapter; Controller = controller; } } private AdapterFindResult FindHitAdapter(BasePlayer basePlayer, out RaycastHit hit) where TAdapter : TransformAdapter where TController : BaseController { if (!TryRaycast(basePlayer, out hit)) return default(AdapterFindResult); var entity = hit.GetEntity(); if (entity == null) return default(AdapterFindResult); var spawnPointAdapter = GetSpawnPointAdapter(entity); if (spawnPointAdapter != null) { // First check if the caller wants a SpawnPointAdapter, else try to give a SpawnGroupAdapter. var spawnAdapter = spawnPointAdapter as TAdapter ?? spawnPointAdapter.SpawnGroupAdapter as TAdapter; if (spawnAdapter != null) return new AdapterFindResult(spawnAdapter, spawnAdapter.Controller as TController); } return new AdapterFindResult(entity); } private AdapterFindResult FindClosestNearbyAdapter(Vector3 position) where TAdapter : TransformAdapter where TController : BaseController { TAdapter closestNearbyAdapter = null; TController associatedController = null; var closestDistanceSquared = float.MaxValue; foreach (var adapter in _profileManager.GetEnabledAdapters()) { var controllerOfType = adapter.Controller as TController; if (controllerOfType == null) continue; var adapterDistanceSquared = (adapter.Position - position).sqrMagnitude; if (adapterDistanceSquared <= MaxFindDistanceSquared && adapterDistanceSquared < closestDistanceSquared) { closestNearbyAdapter = adapter; associatedController = controllerOfType; closestDistanceSquared = adapterDistanceSquared; } } return closestNearbyAdapter != null ? new AdapterFindResult(closestNearbyAdapter, associatedController) : default(AdapterFindResult); } private AdapterFindResult FindAdapter(BasePlayer basePlayer) where TAdapter : TransformAdapter where TController : BaseController { RaycastHit hit; var hitResult = FindHitAdapter(basePlayer, out hit); if (hitResult.Controller != null) return hitResult; return FindClosestNearbyAdapter(hit.point); } // Convenient method that does not require a controller type. private AdapterFindResult FindAdapter(BasePlayer basePlayer) where TAdapter : TransformAdapter { return FindAdapter(basePlayer); } private IEnumerable FindSpawnGroups(string partialGroupName, string monumentAliasOrShortName, Profile profile = null, bool partialMatch = false) { foreach (var spawnGroupController in _profileManager.GetEnabledControllers()) { if (profile != null && spawnGroupController.Profile != profile) continue; var spawnGroupName = spawnGroupController.SpawnGroupData.Name; if (partialMatch) { if (spawnGroupName.IndexOf(partialGroupName, StringComparison.InvariantCultureIgnoreCase) == -1) continue; } else if (!spawnGroupName.Equals(partialGroupName, StringComparison.InvariantCultureIgnoreCase)) continue; // Can only select a spawn group for the same monument. // This a slightly hacky way to check this, since data and controllers aren't directly aware of monuments. if (spawnGroupController.Adapters.FirstOrDefault()?.Monument.AliasOrShortName != monumentAliasOrShortName) continue; yield return spawnGroupController; } } private PuzzleReset FindConnectedPuzzleReset(IOSlot[] slotList, HashSet visited) { foreach (var slot in slotList) { var otherEntity = slot.connectedTo.Get(); if (otherEntity == null) continue; var puzzleReset = FindConnectedPuzzleReset(otherEntity, visited); if (puzzleReset != null) return puzzleReset; } return null; } private PuzzleReset FindConnectedPuzzleReset(IOEntity ioEntity, HashSet visited = null) { var puzzleReset = ioEntity.GetComponent(); if (puzzleReset != null) return puzzleReset; if (visited == null) { visited = new HashSet(); } if (!visited.Add(ioEntity)) return null; return FindConnectedPuzzleReset(ioEntity.inputs, visited) ?? FindConnectedPuzzleReset(ioEntity.outputs, visited); } private SpawnPointAdapter GetSpawnPointAdapter(BaseEntity entity) { var spawnPointInstance = entity.GetComponent(); if (spawnPointInstance == null) return null; var spawnPoint = spawnPointInstance.parentSpawnPoint as CustomSpawnPoint; if (spawnPoint == null) return null; return spawnPoint.Adapter; } #endregion #region Helper Methods public static void LogInfo(string message) => Interface.Oxide.LogInfo($"[Monument Addons] {message}"); public static void LogError(string message) => Interface.Oxide.LogError($"[Monument Addons] {message}"); public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Monument Addons] {message}"); private static bool IsKeyBindArg(string arg) { return arg == "True"; } private static bool TryRaycast(BasePlayer player, out RaycastHit hit, float maxDistance = MaxRaycastDistance) { return Physics.Raycast(player.eyes.HeadRay(), out hit, maxDistance, HitLayers, QueryTriggerInteraction.Ignore); } private static bool TrySphereCast(BasePlayer player, out RaycastHit hit, int layerMask, float radius, float maxDistance = MaxRaycastDistance) { return Physics.SphereCast(player.eyes.HeadRay(), radius, out hit, maxDistance, layerMask, QueryTriggerInteraction.Ignore); } private static bool TryGetHitPosition(BasePlayer player, out Vector3 position) { RaycastHit hit; if (TryRaycast(player, out hit)) { position = hit.point; return true; } position = Vector3.zero; return false; } public static T GetLookEntitySphereCast(BasePlayer player, int layerMask, float radius, float maxDistance = MaxRaycastDistance) where T : BaseEntity { RaycastHit hit; return TrySphereCast(player, out hit, layerMask, radius, maxDistance) ? hit.GetEntity() as T : null; } private static void SendEffect(BasePlayer player, string effectPrefab) { var effect = new Effect(effectPrefab, player, 0, Vector3.zero, Vector3.forward); EffectNetwork.Send(effect, player.net.connection); } private static bool IsOnTerrain(Vector3 position) => Math.Abs(position.y - TerrainMeta.HeightMap.GetHeight(position)) <= TerrainProximityTolerance; private static string GetShortName(string prefabName) { var slashIndex = prefabName.LastIndexOf("/", StringComparison.Ordinal); var baseName = (slashIndex == -1) ? prefabName : prefabName.Substring(slashIndex + 1); return baseName.Replace(".prefab", string.Empty); } private static void DetermineLocalTransformData(Vector3 position, BasePlayer basePlayer, BaseMonument monument, out Vector3 localPosition, out Vector3 localRotationAngles, out bool isOnTerrain, bool flipRotation = true) { localPosition = monument.InverseTransformPoint(position); var localRotationAngle = basePlayer.HasParent() ? basePlayer.viewAngles.y : basePlayer.viewAngles.y - monument.Rotation.eulerAngles.y; if (flipRotation) { localRotationAngle += 180; } localRotationAngles = new Vector3(0, (localRotationAngle + 360) % 360, 0); isOnTerrain = IsOnTerrain(position); } private static void DestroyProblemComponents(BaseEntity entity) { UnityEngine.Object.DestroyImmediate(entity.GetComponent()); UnityEngine.Object.DestroyImmediate(entity.GetComponent()); } private static bool OnCargoShip(BasePlayer player, Vector3 position, out BaseMonument cargoShipMonument) { cargoShipMonument = null; var cargoShip = player.GetParentEntity() as CargoShip; if (cargoShip == null) return false; cargoShipMonument = new DynamicMonument(cargoShip, isMobile: true); if (!cargoShipMonument.IsInBounds(position)) return false; return true; } private static bool IsRedirectSkin(ulong skinId, out string alternativeShortName) { alternativeShortName = null; if (skinId > int.MaxValue) return false; var skinIdInt = Convert.ToInt32(skinId); foreach (var skin in ItemSkinDirectory.Instance.skins) { var itemSkin = skin.invItem as ItemSkin; if (itemSkin == null || itemSkin.id != skinIdInt) continue; var redirect = itemSkin.Redirect; if (redirect == null) return false; var modDeployable = redirect.GetComponent(); if (modDeployable != null) { alternativeShortName = GetShortName(modDeployable.entityPrefab.resourcePath); } return true; } return false; } private static BaseEntity FindBaseEntityForPrefab(string prefabName) { var prefab = GameManager.server.FindPrefab(prefabName); if (prefab == null) return null; return prefab.GetComponent(); } private static string FormatTime(double seconds) => TimeSpan.FromSeconds(seconds).ToString("g"); private static void BroadcastEntityTransformChange(BaseEntity entity) { if (entity is StabilityEntity) { entity.TerminateOnClient(BaseNetworkable.DestroyMode.None); entity.SendNetworkUpdateImmediate(); return; } var wasSyncPosition = entity.syncPosition; entity.syncPosition = true; entity.TransformChanged(); entity.syncPosition = wasSyncPosition; entity.transform.hasChanged = false; } private static void EnableSavingRecursive(BaseEntity entity, bool enableSaving) { entity.EnableSaving(enableSaving); foreach (var child in entity.children) EnableSavingRecursive(child, enableSaving); } private static IEnumerator WaitWhileWithTimeout(Func predicate, float timeoutSeconds) { var timeoutAt = UnityEngine.Time.time + timeoutSeconds; while (predicate() && UnityEngine.Time.time < timeoutAt) { yield return null; } } private static bool TryParseEnum(string arg, out T enumValue) where T : struct { foreach (var value in Enum.GetValues(typeof(T))) { if (value.ToString().IndexOf(arg, StringComparison.InvariantCultureIgnoreCase) >= 0) { enumValue = (T)value; return true; } } enumValue = default(T); return false; } private static float GetTimeToNextSpawn(SpawnGroup spawnGroup) { var events = spawnGroup.spawnClock.events; if (events.Count == 0 || float.IsNaN(events.First().time)) return float.PositiveInfinity; return events.First().time - UnityEngine.Time.time; } private static float GetTimeToNextSpawn(IndividualSpawner spawner) { if (spawner.nextSpawnTime == -1) return float.PositiveInfinity; return spawner.nextSpawnTime - UnityEngine.Time.time; } private static List GetTierList(MonumentTier tier) { var tierList = new List(); if ((tier & MonumentTier.Tier0) != 0) { tierList.Add(MonumentTier.Tier0); } if ((tier & MonumentTier.Tier1) != 0) { tierList.Add(MonumentTier.Tier1); } if ((tier & MonumentTier.Tier2) != 0) { tierList.Add(MonumentTier.Tier2); } return tierList; } private static MonumentTier GetMonumentTierMask(Vector3 position) { var topologyMask = TerrainMeta.TopologyMap.GetTopology(position); var mask = (MonumentTier)0; if ((TerrainTopology.TIER0 & topologyMask) != 0) { mask |= MonumentTier.Tier0; } if ((TerrainTopology.TIER1 & topologyMask) != 0) { mask |= MonumentTier.Tier1; } if ((TerrainTopology.TIER2 & topologyMask) != 0) { mask |= MonumentTier.Tier2; } return mask; } private static BaseEntity FindValidEntity(ulong entityId) { var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(entityId)) as BaseEntity; return entity != null && !entity.IsDestroyed ? entity : null; } private static IEnumerator KillEntitiesRoutine(ICollection entityList) { foreach (var entity in entityList) { entity.Kill(); yield return null; } } private bool HasAdminPermission(string userId) => permission.UserHasPermission(userId, PermissionAdmin); private bool HasAdminPermission(BasePlayer player) => HasAdminPermission(player.UserIDString); private BaseMonument GetClosestMonument(BasePlayer player, Vector3 position) { BaseMonument cargoShipMonument; if (OnCargoShip(player, position, out cargoShipMonument)) return cargoShipMonument; return _monumentHelper.GetClosestMonumentAdapter(position); } private List GetMonumentsByAliasOrShortName(string aliasOrShortName) { if (aliasOrShortName == CargoShipShortName) { var cargoShipList = new List(); foreach (var entity in BaseNetworkable.serverEntities) { var cargoShip = entity as CargoShip; if (cargoShip != null) cargoShipList.Add(new DynamicMonument(cargoShip, isMobile: true)); } return cargoShipList.Count > 0 ? cargoShipList : null; } var monuments = _monumentHelper.FindMonumentsByAlias(aliasOrShortName); if (monuments.Count > 0) return monuments; return _monumentHelper.FindMonumentsByShortName(aliasOrShortName); } private IEnumerator SpawnAllProfilesRoutine() { // Delay slightly to allow Monument Finder to finish loading. yield return null; yield return _profileManager.LoadAllProfilesRoutine(); if (_config.EnableDynamicMonuments) { // We don't want to be subscribed to OnEntitySpawned(CargoShip) until the coroutine is done. // Otherwise, a cargo ship could spawn while the coroutine is running and could get double entities. Subscribe(nameof(OnEntitySpawned)); } } private void StartupRoutine() { // Don't spawn entities if that's already been done. if (_startupCoroutine != null) return; _startupCoroutine = _coroutineManager.StartCoroutine(SpawnAllProfilesRoutine()); } private void DownloadProfile(IPlayer player, string url, Action successCallback, Action errorCallback) { webrequest.Enqueue( url: url, body: null, callback: (statusCode, responseBody) => { if (_plugin == null) { // Ignore the response because the plugin was unloaded. return; } if (statusCode != 200) { errorCallback(GetMessage(player.Id, LangEntry.ProfileDownloadError, url, statusCode)); return; } Profile profile; try { profile = JsonConvert.DeserializeObject(responseBody); } catch (Exception ex) { errorCallback(GetMessage(player.Id, LangEntry.ProfileParseError, url, ex.Message)); return; } ProfileDataMigration.MigrateToLatest(profile); profile.Url = url; successCallback(profile); }, owner: this, method: RequestMethod.GET, headers: JsonRequestHeaders, timeout: 5000 ); } private string DeterminePrefabFromPlayerActiveDeployable(BasePlayer basePlayer, out ulong skinId) { skinId = 0; var activeItem = basePlayer.GetActiveItem(); if (activeItem == null) return null; skinId = activeItem.skin; string overridePrefabPath; if (_config.DeployableOverrides.TryGetValue(activeItem.info.shortname, out overridePrefabPath)) return overridePrefabPath; var itemModDeployable = activeItem.info.GetComponent(); if (itemModDeployable == null) return null; return itemModDeployable.entityPrefab.resourcePath; } #endregion #region Helper Classes private static class StringUtils { public static bool EqualsCaseInsensitive(string a, string b) => string.Compare(a, b, StringComparison.OrdinalIgnoreCase) == 0; public static bool Contains(string haystack, string needle) => haystack.Contains(needle, CompareOptions.IgnoreCase); } private static class SearchUtils { public static List FindModderPrefabMatches(string partialName) { return FindMatches( GameManifest.Current.pooledStrings.Select(pooledString => pooledString.str) .Where(str => str.StartsWith("assets/bundled/prefabs/modding", StringComparison.OrdinalIgnoreCase)), prefabPath => StringUtils.Contains(prefabPath, partialName), prefabPath => StringUtils.EqualsCaseInsensitive(prefabPath, partialName) ); } public static List FindEntityPrefabMatches(string partialName, UniqueNameRegistry uniqueNameRegistry) { return FindMatches( GameManifest.Current.entities, prefabPath => StringUtils.Contains(prefabPath, partialName) && FindBaseEntityForPrefab(prefabPath) != null, prefabPath => StringUtils.EqualsCaseInsensitive(prefabPath, partialName), prefabPath => StringUtils.Contains(uniqueNameRegistry.GetUniqueShortName(prefabPath), partialName), prefabPath => StringUtils.EqualsCaseInsensitive(uniqueNameRegistry.GetUniqueShortName(prefabPath), partialName) ); } public static List FindCustomAddonMatches(string partialName, IEnumerable customAddons) { return FindMatches( customAddons, addonDefinition => StringUtils.Contains(addonDefinition.AddonName, partialName), addonDefinition => StringUtils.EqualsCaseInsensitive(addonDefinition.AddonName, partialName) ); } public static List FindMatches(IEnumerable sourceList, params Func[] predicateList) { List results = null; foreach (var predicate in predicateList) { if (results == null) { results = sourceList.Where(predicate).ToList(); continue; } var newResults = results.Where(predicate).ToList(); if (newResults.Count == 0) { // No matches found after filtering, so ignore the results and then try a different filter. continue; } if (newResults.Count == 1) { // Only a single match after filtering, so return new results. return newResults; } // Multiple matches found, so proceed with further filtering. results = newResults; } return results; } } private static class Ddraw { public static void Sphere(BasePlayer player, Vector3 origin, float radius, Color color, float duration) => player.SendConsoleCommand("ddraw.sphere", duration, color, origin, radius); public static void Line(BasePlayer player, Vector3 origin, Vector3 target, Color color, float duration) => player.SendConsoleCommand("ddraw.line", duration, color, origin, target); public static void Arrow(BasePlayer player, Vector3 origin, Vector3 target, float headSize, Color color, float duration) => player.SendConsoleCommand("ddraw.arrow", duration, color, origin, target, headSize); public static void ArrowThrough(BasePlayer player, Vector3 center, Quaternion rotation, float length, float headSize, Color color, float duration) { var start = center - rotation * new Vector3(0, 0, length / 2); var end = center + rotation * new Vector3(0, 0, length / 2); Arrow(player, start, end, headSize, color, duration); } public static void Text(BasePlayer player, Vector3 origin, string text, Color color, float duration) => player.SendConsoleCommand("ddraw.text", duration, color, origin, text); } private static class EntityUtils { public static T GetNearbyEntity(BaseEntity originEntity, float maxDistance, int layerMask = -1, string filterShortPrefabName = null) where T : BaseEntity { var entityList = new List(); Vis.Entities(originEntity.transform.position, maxDistance, entityList, layerMask, QueryTriggerInteraction.Ignore); foreach (var entity in entityList) { if (filterShortPrefabName == null || entity.ShortPrefabName == filterShortPrefabName) return entity; } return null; } public static T GetClosestNearbyEntity(Vector3 position, float maxDistance, int layerMask = -1, Func predicate = null) where T : BaseEntity { var entityList = Facepunch.Pool.GetList(); Vis.Entities(position, maxDistance, entityList, layerMask, QueryTriggerInteraction.Ignore); try { return GetClosestComponent(position, entityList, predicate); } finally { Facepunch.Pool.FreeList(ref entityList); } } public static T GetClosestNearbyComponent(Vector3 position, float maxDistance, int layerMask = -1, Func predicate = null) where T : UnityEngine.Component { var componentList = Facepunch.Pool.GetList(); Vis.Components(position, maxDistance, componentList, layerMask, QueryTriggerInteraction.Ignore); try { return GetClosestComponent(position, componentList, predicate); } finally { Facepunch.Pool.FreeList(ref componentList); } } public static void ConnectNearbyVehicleSpawner(VehicleVendor vehicleVendor) { if (vehicleVendor.GetVehicleSpawner() != null) return; var vehicleSpawner = vehicleVendor.ShortPrefabName == "bandit_conversationalist" ? GetNearbyEntity(vehicleVendor, 40, Rust.Layers.Mask.Deployed, "airwolfspawner") : vehicleVendor.ShortPrefabName == "boat_shopkeeper" ? GetNearbyEntity(vehicleVendor, 20, Rust.Layers.Mask.Deployed, "boatspawner") : null; if (vehicleSpawner == null) return; vehicleVendor.spawnerRef.Set(vehicleSpawner); } public static void ConnectNearbyVehicleVendor(VehicleSpawner vehicleSpawner) { var vehicleVendor = vehicleSpawner.ShortPrefabName == "airwolfspawner" ? GetNearbyEntity(vehicleSpawner, 40, Rust.Layers.Mask.Player_Server, "bandit_conversationalist") : vehicleSpawner.ShortPrefabName == "boatspawner" ? GetNearbyEntity(vehicleSpawner, 20, Rust.Layers.Mask.Player_Server, "boat_shopkeeper") : null; if (vehicleVendor == null) return; vehicleVendor.spawnerRef.Set(vehicleSpawner); } public static void ConnectNearbyDoor(DoorManipulator doorManipulator) { // The door manipulator normally checks on layer 21, but use layerMask -1 to allow finding arctic garage doors. var door = EntityUtils.GetClosestNearbyEntity(doorManipulator.transform.position, 3); if (door == null || door.IsDestroyed) return; doorManipulator.SetTargetDoor(door); door.SetFlag(BaseEntity.Flags.Locked, true); } private static T GetClosestComponent(Vector3 position, List componentList, Func predicate = null) where T : UnityEngine.Component { T closestComponent = null; float closestDistanceSquared = float.MaxValue; foreach (var component in componentList) { if (predicate?.Invoke(component) == false) continue; var distance = (component.transform.position - position).sqrMagnitude; if (distance < closestDistanceSquared) { closestDistanceSquared = distance; closestComponent = component; } } return closestComponent; } } private static class EntitySetupUtils { public static bool ShouldBeImmortal(BaseEntity entity) { var samSite = entity as SamSite; if (samSite != null && samSite.staticRespawn) return false; return true; } public static void PreSpawnShared(BaseEntity entity) { var combatEntity = entity as BaseCombatEntity; if (combatEntity != null) { combatEntity.pickup.enabled = false; } var stabilityEntity = entity as StabilityEntity; if (stabilityEntity != null) { stabilityEntity.grounded = true; } DestroyProblemComponents(entity); } public static void PostSpawnShared(MonumentAddons plugin, BaseEntity entity, bool enableSaving) { // Enable/Disable saving after spawn to account for children that spawn late (e.g., Lift). EnableSavingRecursive(entity, enableSaving); var combatEntity = entity as BaseCombatEntity; if (combatEntity != null) { if (ShouldBeImmortal(entity)) { var basePlayer = entity as BasePlayer; if (basePlayer != null) { // Don't share common protection properties with BasePlayer instances since they get destroyed on kill. combatEntity.baseProtection.Clear(); combatEntity.baseProtection.Add(1); } else { // Must set after spawn for building blocks. combatEntity.baseProtection = plugin._immortalProtection; } } } var decayEntity = entity as DecayEntity; if (decayEntity != null) { decayEntity.decay = null; var buildingBlock = entity as BuildingBlock; if (buildingBlock != null) { // Must be done after spawn for some reason. if (buildingBlock.HasFlag(BuildingBlock.BlockFlags.CanRotate) || buildingBlock.HasFlag(BuildingBlock.BlockFlags.CanDemolish)) { buildingBlock.SetFlag(BuildingBlock.BlockFlags.CanRotate, false, recursive: false, networkupdate: false); buildingBlock.SetFlag(BuildingBlock.BlockFlags.CanDemolish, false, recursive: false, networkupdate: false); buildingBlock.CancelInvoke(buildingBlock.StopBeingRotatable); buildingBlock.CancelInvoke(buildingBlock.StopBeingDemolishable); buildingBlock.SendNetworkUpdate_Flags(); } } } } } private static class BooleanParser { private static string[] _booleanYesValues = new string[] { "true", "yes", "on", "1" }; private static string[] _booleanNoValues = new string[] { "false", "no", "off", "0" }; public static bool TryParse(string arg, out bool value) { if (_booleanYesValues.Contains(arg, StringComparer.InvariantCultureIgnoreCase)) { value = true; return true; } if (_booleanNoValues.Contains(arg, StringComparer.InvariantCultureIgnoreCase)) { value = false; return true; } value = false; return false; } } private class UniqueNameRegistry { private Dictionary _uniqueNameByPrefabPath = new Dictionary(StringComparer.InvariantCultureIgnoreCase); public void OnServerInitialized() => BuildIndex(); public string GetUniqueShortName(string prefabPath) { string uniqueName; if (!_uniqueNameByPrefabPath.TryGetValue(prefabPath, out uniqueName)) { // Unique names are only stored initially if different from short name. // To avoid frequent heap allocations, also cache other short names that are accessed. uniqueName = GetShortName(prefabPath); _uniqueNameByPrefabPath[prefabPath] = uniqueName.ToLower(); } return uniqueName; } private string[] GetSegments(string prefabPath) => prefabPath.Split('/'); private string GetPartialPath(string[] segments, int numSegments) { numSegments = Math.Min(numSegments, segments.Length); var arraySegment = new ArraySegment(segments, segments.Length - numSegments, numSegments); return string.Join("/", arraySegment); } private void BuildIndex() { var remainingPrefabPaths = GameManifest.Current.entities.ToList(); var numSegmentsFromEnd = 1; var iterations = 0; var maxIterations = 1; while (remainingPrefabPaths.Count > 0 && iterations++ < maxIterations) { var countByPartialPath = new Dictionary(); foreach (var prefabPath in remainingPrefabPaths) { var segments = GetSegments(prefabPath); maxIterations = Math.Max(maxIterations, segments.Length); var partialPath = GetPartialPath(segments, numSegmentsFromEnd); int segmentCount; if (!countByPartialPath.TryGetValue(partialPath, out segmentCount)) { segmentCount = 0; } countByPartialPath[partialPath] = segmentCount + 1; } for (var i = remainingPrefabPaths.Count - 1; i >= 0; i--) { var prefabPath = remainingPrefabPaths[i]; var partialPath = GetPartialPath(GetSegments(prefabPath), numSegmentsFromEnd); if (countByPartialPath[partialPath] == 1) { // Only cache the unique name if different than short name. if (numSegmentsFromEnd > 1) { _uniqueNameByPrefabPath[prefabPath] = partialPath.ToLower().Replace(".prefab", string.Empty); } remainingPrefabPaths.RemoveAt(i); } } numSegmentsFromEnd++; } } } private class EmptyMonoBehavior : MonoBehaviour {} private class CoroutineManager { public static Coroutine StartGlobalCoroutine(IEnumerator enumerator) => ServerMgr.Instance?.StartCoroutine(enumerator); private static IEnumerator CallbackRoutine(Coroutine dependency, Action action) { yield return dependency; action(); } // Object for tracking all coroutines for spawning or updating entities. // This allows easily stopping all those coroutines by simply destroying the game object. private MonoBehaviour _coroutineComponent; public Coroutine StartCoroutine(IEnumerator enumerator) { if (_coroutineComponent == null) { _coroutineComponent = new GameObject().AddComponent(); } return _coroutineComponent.StartCoroutine(enumerator); } public Coroutine StartCallbackRoutine(Coroutine coroutine, Action callback) { return StartCoroutine(CallbackRoutine(coroutine, callback)); } public void StopAll() { if (_coroutineComponent == null) return; _coroutineComponent.StopAllCoroutines(); } public void Destroy() { if (_coroutineComponent == null) return; UnityEngine.Object.DestroyImmediate(_coroutineComponent?.gameObject); } } private class WireToolManager { private const float DrawDuration = 3; private const float DrawSlotRadius = 0.04f; private const float PreviewDotRadius = 0.01f; private const float InputCooldown = 0.25f; private const float HoldToClearDuration = 0.5f; private const float DrawIntervalDuration = 0.01f; private const float DrawIntervalWithBuffer = DrawIntervalDuration + 0.05f; private const float MinAngleDot = 0.999f; private class WireSession { public BasePlayer Player { get; } public WireColour? WireColor; public IOType WireType; public EntityAdapter Adapter; public IOSlot StartSlot; public int StartSlotIndex; public bool IsSource; public float LastInput; public float SecondaryPressedTime = float.MaxValue; public float LastDrawTime; public List Points = new List(); public WireSession(BasePlayer player) { Player = player; } public void Reset(bool refreshPoints = false) { Points.Clear(); if (refreshPoints) { RefreshPoints(); } Adapter = null; SecondaryPressedTime = float.MaxValue; } public void StartConnection(EntityAdapter adapter, IOSlot startSlot, int slotIndex, bool isSource) { WireType = startSlot.type; Adapter = adapter; StartSlot = startSlot; StartSlotIndex = slotIndex; IsSource = isSource; startSlot.wireColour = WireColor ?? WireColour.Default; } public void AddPoint(Vector3 position) { Points.Add(position); RefreshPoints(); } public void RemoveLast() { if (Points.Count > 0) { Points.RemoveAt(Points.Count - 1); RefreshPoints(); } else { Reset(refreshPoints: true); } } public bool IsOnInputCooldown() { return LastInput + InputCooldown > UnityEngine.Time.time; } public bool IsOnDrawCooldown() { return LastDrawTime + DrawIntervalDuration > UnityEngine.Time.time; } public void RefreshPoints() { var ioEntity = Adapter?.Entity as IOEntity; if (ioEntity == null) return; var slot = IsSource ? ioEntity.outputs[StartSlotIndex] : ioEntity.inputs[StartSlotIndex]; if (slot.type == IOType.Kinetic) { // Don't attempt to render wires for kinetic slots since that will kick the player. return; } slot.linePoints = new Vector3[Points.Count + 1]; if (IsSource) { slot.linePoints[0] = slot.handlePosition; for (var i = 0; i < Points.Count; i++) { slot.linePoints[i + 1] = ioEntity.transform.InverseTransformPoint(Points[i]); } } else { // TODO: Implement a temporary entity to show a line from destination input } ioEntity.SendNetworkUpdate(); } } private MonumentAddons _plugin; private ProfileStore _profileStore; private MonumentEntityTracker _entityTracker; private List _playerSessions = new List(); private Timer _timer; public WireToolManager(MonumentAddons plugin, ProfileStore profileStore, MonumentEntityTracker entityTracker) { _plugin = plugin; _profileStore = profileStore; _entityTracker = entityTracker; } public bool HasPlayer(BasePlayer player) { return GetPlayerSession(player) != null; } public void StartOrUpdateSession(BasePlayer player, WireColour? wireColor) { int sessionIndex; var session = GetPlayerSession(player, out sessionIndex); if (session == null) { session = new WireSession(player); _playerSessions.Add(session); } session.WireColor = wireColor; if (_timer == null || _timer.Destroyed) { _timer = _plugin.timer.Every(0, ProcessPlayers); } } public void StopSession(BasePlayer player) { int sessionIndex; var session = GetPlayerSession(player, out sessionIndex); if (session == null) return; DestroySession(session, sessionIndex); } public void Unload() { for (var i = _playerSessions.Count - 1; i >= 0; i--) { DestroySession(_playerSessions[i], i); } } private WireSession GetPlayerSession(BasePlayer player, out int index) { for (var i = 0; i < _playerSessions.Count; i++) { var session = _playerSessions[i]; if (session.Player == player) { index = i; return session; } } index = 0; return null; } private WireSession GetPlayerSession(BasePlayer player) { int index; return GetPlayerSession(player, out index); } private void DestroySession(WireSession session, int index) { session.Reset(refreshPoints: true); _playerSessions.RemoveAt(index); if (_playerSessions.Count == 0 && _timer != null) { _timer.Destroy(); _timer = null; } _plugin.ChatMessage(session.Player, LangEntry.WireToolDeactivated); } private EntityAdapter GetLookIOAdapter(BasePlayer player, out IOEntity ioEntity) { ioEntity = GetLookEntitySphereCast(player, Rust.Layers.Solid, 0.1f, 6); if (ioEntity == null) return null; EntityAdapter adapter; EntityController controller; if (!_entityTracker.IsMonumentEntity(ioEntity, out adapter, out controller)) { _plugin.ChatMessage(player, LangEntry.ErrorEntityNotEligible); return null; } if (adapter == null) { _plugin.ChatMessage(player, LangEntry.ErrorNoSuitableAddonFound); return null; } return adapter; } private IOSlot GetClosestIOSlot(IOEntity ioEntity, Ray ray, float minDot, out int index, out float highestDot, bool wantsSourceSlot, bool? wantsOccupiedSlots = false) { IOSlot closestSlot = null; index = 0; highestDot = -1f; var transform = ioEntity.transform; var slotList = wantsSourceSlot ? ioEntity.outputs : ioEntity.inputs; for (var slotIndex = 0; slotIndex < slotList.Length; slotIndex++) { var slot = slotList[slotIndex]; if (wantsOccupiedSlots.HasValue && wantsOccupiedSlots.Value == (slot.connectedTo.Get() == null)) continue; var slotPosition = transform.TransformPoint(slot.handlePosition); var dot = Vector3.Dot(ray.direction, (slotPosition - ray.origin).normalized); if (dot > minDot && dot > highestDot) { closestSlot = slot; index = slotIndex; highestDot = dot; } } return closestSlot; } private IOSlot GetClosestIOSlot(IOEntity ioEntity, Ray ray, float minDot, out int index, out bool isSourceSlot, bool? wantsOccupiedSlots = null) { index = 0; isSourceSlot = false; int sourceSlotIndex; float sourceDot; var sourceSlot = GetClosestIOSlot(ioEntity, ray, minDot, out sourceSlotIndex, out sourceDot, wantsSourceSlot: true, wantsOccupiedSlots: wantsOccupiedSlots); int destinationSlotIndex; float destinationDot; var destinationSlot = GetClosestIOSlot(ioEntity, ray, minDot, out destinationSlotIndex, out destinationDot, wantsSourceSlot: false, wantsOccupiedSlots: wantsOccupiedSlots); if (sourceSlot == null && destinationSlot == null) return null; if (sourceSlot != null && destinationSlot != null) { if (sourceDot >= destinationDot) { isSourceSlot = true; index = sourceSlotIndex; return sourceSlot; } index = destinationSlotIndex; return destinationSlot; } if (sourceSlot != null) { isSourceSlot = true; index = sourceSlotIndex; return sourceSlot; } index = destinationSlotIndex; return destinationSlot; } private bool CanPlayerUseTool(BasePlayer player) { if (player == null || player.IsDestroyed || !player.IsConnected || player.IsDead()) return false; var activeItemShortName = player.GetActiveItem()?.info.shortname; if (activeItemShortName == null) return false; return activeItemShortName == "wiretool" || activeItemShortName == "hosetool"; } private Color DetermineSlotColor(IOSlot slot) { if (slot.connectedTo.Get() != null) return Color.red; if (slot.type == IOType.Fluidic) return Color.cyan; if (slot.type == IOType.Kinetic) return new Color(1, 0.5f, 0); return Color.yellow; } private void ShowSlots(BasePlayer player, IOEntity ioEntity, bool showSourceSlots) { var transform = ioEntity.transform; var slotList = showSourceSlots ? ioEntity.outputs : ioEntity.inputs; foreach (var slot in slotList) { var color = DetermineSlotColor(slot); var position = transform.TransformPoint(slot.handlePosition); Ddraw.Sphere(player, position, DrawSlotRadius, color, DrawDuration); Ddraw.Text(player, position, showSourceSlots ? "OUT" : "IN", color, DrawDuration); } } private void DrawSessionState(WireSession session) { if (session.Adapter == null || session.IsOnDrawCooldown()) return; var player = session.Player; session.LastDrawTime = UnityEngine.Time.time; var ioEntity = session.Adapter.Entity as IOEntity; var startPosition = ioEntity.transform.TransformPoint(session.StartSlot.handlePosition); Ddraw.Sphere(player, startPosition, DrawSlotRadius, Color.green, DrawIntervalWithBuffer); Vector3 hitPosition; if (TryGetHitPosition(player, out hitPosition)) { var lastPoint = session.Points.Count == 0 ? session.StartSlot.handlePosition : session.StartSlot.linePoints.LastOrDefault(); Ddraw.Sphere(player, hitPosition, PreviewDotRadius, Color.green, DrawIntervalWithBuffer); Ddraw.Line(player, ioEntity.transform.TransformPoint(lastPoint), hitPosition, Color.green, DrawIntervalWithBuffer); } } private void MaybeStartWire(WireSession session, EntityAdapter adapter) { var player = session.Player; var ioEntity = adapter.Entity as IOEntity; bool isSourceSlot; int slotIndex; var slot = GetClosestIOSlot(ioEntity, player.eyes.HeadRay(), MinAngleDot, out slotIndex, out isSourceSlot, wantsOccupiedSlots: false); if (slot == null) { ShowSlots(player, ioEntity, showSourceSlots: true); ShowSlots(player, ioEntity, showSourceSlots: false); return; } if (slot.connectedTo.Get() != null) { Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.red, DrawDuration); return; } session.StartConnection(adapter, slot, slotIndex, isSource: isSourceSlot); Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.green, DrawDuration); SendEffect(player, WireToolPlugEffect); } private void MaybeEndWire(WireSession session, EntityAdapter adapter) { var player = session.Player; var ioEntity = adapter.Entity as IOEntity; var headRay = player.eyes.HeadRay(); int slotIndex; float distanceSquared; var slot = GetClosestIOSlot(ioEntity, headRay, MinAngleDot, out slotIndex, out distanceSquared, wantsSourceSlot: !session.IsSource); if (slot == null) { slot = GetClosestIOSlot(ioEntity, headRay, MinAngleDot, out slotIndex, out distanceSquared, wantsSourceSlot: session.IsSource); if (slot != null) { Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.red, DrawDuration); } ShowSlots(player, ioEntity, showSourceSlots: !session.IsSource); return; } if (slot.connectedTo.Get() != null) { Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.red, DrawDuration); return; } var adapterProfile = adapter.Controller.Profile; var sessionProfile = session.Adapter.Controller.Profile; if (adapterProfile != sessionProfile) { Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.red, DrawDuration); _plugin.ChatMessage(player, LangEntry.WireToolProfileMismatch, sessionProfile, adapterProfile.Name); return; } if (!adapter.Monument.IsSameAs(session.Adapter.Monument)) { Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.red, DrawDuration); _plugin.ChatMessage(player, LangEntry.WireToolMonumentMismatch); return; } if (slot.type != session.WireType) { Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.red, DrawDuration); _plugin.ChatMessage(player, LangEntry.WireToolTypeMismatch, session.WireType, slot.type); return; } var sourceAdapter = session.IsSource ? session.Adapter : adapter; var destinationAdapter = session.IsSource ? adapter : session.Adapter; var points = session.Points.Select(adapter.Monument.InverseTransformPoint); if (!session.IsSource) { points = points.Reverse(); } var connectionData = new IOConnectionData { ConnectedToId = destinationAdapter.EntityData.Id, Slot = session.IsSource ? session.StartSlotIndex : slotIndex, ConnectedToSlot = session.IsSource ? slotIndex : session.StartSlotIndex, Points = points.ToArray(), ShowWire = session.WireColor.HasValue, Color = session.WireColor ?? WireColour.Default, }; sourceAdapter.EntityData.AddIOConnection(connectionData); _profileStore.Save(sourceAdapter.Controller.Profile); (sourceAdapter.Controller as EntityController).StartHandleChangesRoutine(); session.Reset(); Ddraw.Sphere(player, adapter.Transform.TransformPoint(slot.handlePosition), DrawSlotRadius, Color.green, DrawDuration); SendEffect(player, WireToolPlugEffect); } private void MaybeClearWire(WireSession session) { var player = session.Player; session.SecondaryPressedTime = float.MaxValue; IOEntity ioEntity; var adapter = GetLookIOAdapter(player, out ioEntity); if (adapter == null) return; int slotIndex; bool isSourceSlot; var slot = GetClosestIOSlot(ioEntity, player.eyes.HeadRay(), MinAngleDot, out slotIndex, out isSourceSlot, wantsOccupiedSlots: true); if (slot == null) return; EntityAdapter sourceAdapter; EntityAdapter destinationAdapter; int sourceSlotIndex; if (isSourceSlot) { sourceAdapter = adapter; sourceSlotIndex = slotIndex; var destinationEntity = slot.connectedTo.Get(); EntityController destinationController; if (!_entityTracker.IsMonumentEntity(destinationEntity, out destinationAdapter, out destinationController)) return; } else { destinationAdapter = adapter; var sourceEntity = slot.connectedTo.Get(); EntityController sourceController; if (!_entityTracker.IsMonumentEntity(sourceEntity, out sourceAdapter, out sourceController)) return; sourceSlotIndex = slot.connectedToSlot; } sourceAdapter.EntityData.RemoveIOConnection(sourceSlotIndex); _profileStore.Save(sourceAdapter.Controller.Profile); var handleChangesRoutine = (sourceAdapter.Controller as EntityController).StartHandleChangesRoutine(); destinationAdapter.ProfileController.StartCallbackRoutine(handleChangesRoutine, () => (destinationAdapter.Controller as EntityController).StartHandleChangesRoutine()); } private void ProcessPlayers() { for (var i = _playerSessions.Count - 1; i >= 0; i--) { var session = _playerSessions[i]; var player = session.Player; if (!CanPlayerUseTool(player)) { DestroySession(session, i); continue; } if (session.Adapter != null && (session.Adapter == null || session.Adapter.IsDestroyed)) { session.Reset(); } var secondaryJustPressed = player.serverInput.WasJustPressed(BUTTON.FIRE_SECONDARY); var secondaryJustReleased = player.serverInput.WasJustReleased(BUTTON.FIRE_SECONDARY); var now = UnityEngine.Time.time; var secondaryPressedDuration = 0f; if (secondaryJustPressed) { session.SecondaryPressedTime = now; } else if (secondaryJustReleased) { session.SecondaryPressedTime = float.MaxValue; } else if (session.SecondaryPressedTime != float.MaxValue) { secondaryPressedDuration = now - session.SecondaryPressedTime; } DrawSessionState(session); if (session.IsOnInputCooldown()) continue; if (player.serverInput.WasJustPressed(BUTTON.FIRE_PRIMARY)) { session.LastInput = now; IOEntity ioEntity; var adapter = GetLookIOAdapter(player, out ioEntity); if (adapter != null) { if (session.Adapter == null) { MaybeStartWire(session, adapter); continue; } MaybeEndWire(session, adapter); continue; } if (session.Adapter != null) { if (session.WireType == IOType.Kinetic) { // Placing wires with kinetic slots will kick the player. continue; } Vector3 position; if (session.WireColor.HasValue && TryGetHitPosition(player, out position)) { session.AddPoint(position); SendEffect(player, WireToolPlugEffect); } } continue; } if (session.Adapter != null && secondaryJustPressed) { // Remove the most recently placed wire. session.LastInput = now; session.RemoveLast(); } if (session.Adapter == null && session.SecondaryPressedTime != float.MaxValue && secondaryPressedDuration >= HoldToClearDuration) { MaybeClearWire(session); } } if (_playerSessions.Count == 0 && _timer != null && _timer.Destroyed) { _timer.Destroy(); _timer = null; } } } private abstract class DictionaryKeyConverter : JsonConverter { public virtual string KeyToString(TKey key) => key.ToString(); public abstract TKey KeyFromString(string key); public override bool CanConvert(Type objectType) { return objectType == typeof(Dictionary); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var obj = serializer.Deserialize(reader) as JObject; var dict = existingValue as Dictionary; if (dict == null) return null; foreach (var entry in obj) { dict[KeyFromString(entry.Key)] = entry.Value.ToObject(); } return dict; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var dict = value as Dictionary; if (dict == null) { writer.WriteStartObject(); writer.WriteEndObject(); return; } writer.WriteStartObject(); foreach (var entry in dict) { writer.WritePropertyName(KeyToString(entry.Key)); serializer.Serialize(writer, entry.Value); } writer.WriteEndObject(); } } private class ActionDebounced { private PluginTimers _pluginTimers; private float _seconds; private Action _action; private Timer _timer; public ActionDebounced(PluginTimers pluginTimers, float seconds, Action action) { _pluginTimers = pluginTimers; _seconds = seconds; _action = action; } public void Schedule() { if (_timer == null || _timer.Destroyed) { _timer = _pluginTimers.Once(_seconds, _action); return; } // Restart the existing timer. _timer.Reset(); } public void Flush() { if (_timer == null || _timer.Destroyed) return; _timer.Destroy(); _action(); } } private interface IDeepCollection { bool HasItems(); } private static bool HasDeepItems(Dictionary dict) where TValue : IDeepCollection { if (dict.Count == 0) return false; foreach (var value in dict.Values) { if (value.HasItems()) return true; } return false; } private class MonumentHelper { private MonumentAddons _plugin; private Plugin _monumentFinder => _plugin.MonumentFinder; private Dictionary _entranceToMonument = new Dictionary(); public MonumentHelper(MonumentAddons plugin) { _plugin = plugin; } public void OnServerInitialized() { foreach (var monumentInfo in TerrainMeta.Path.Monuments) { if (monumentInfo.DungeonEntrance != null) { _entranceToMonument[monumentInfo.DungeonEntrance] = monumentInfo; } } } public List FindMonumentsByAlias(string alias) => WrapFindMonumentResults(_monumentFinder.Call("API_FindByAlias", alias) as List>); public List FindMonumentsByShortName(string shortName) => WrapFindMonumentResults(_monumentFinder.Call("API_FindByShortName", shortName) as List>); public MonumentAdapter GetClosestMonumentAdapter(Vector3 position) { var dictResult = _monumentFinder.Call("API_GetClosest", position) as Dictionary; if (dictResult == null) return null; return new MonumentAdapter(dictResult); } public bool IsMonumentUnique(string shortName) { var monuments = FindMonumentsByShortName(shortName); return monuments == null || monuments.Count <= 1; } private List WrapFindMonumentResults(List> dictList) { if (dictList == null) return null; var monumentList = new List(); foreach (var dict in dictList) { monumentList.Add(new MonumentAdapter(dict)); } return monumentList; } public MonumentInfo GetMonumentFromTunnel(DungeonGridCell dungeonGridCell) { var entrance = TerrainMeta.Path.FindClosest(TerrainMeta.Path.DungeonGridEntrances, dungeonGridCell.transform.position); if (entrance == null) return null; return GetMonumentFromEntrance(entrance); } private MonumentInfo GetMonumentFromEntrance(DungeonGridInfo dungeonGridInfo) { MonumentInfo monumentInfo; return _entranceToMonument.TryGetValue(dungeonGridInfo, out monumentInfo) ? monumentInfo : null; } } private static class PhoneUtils { public const string Tunnel = "FTL"; public const string UnderwaterLab = "Underwater Lab"; public const string CargoShip = "Cargo Ship"; private static readonly Regex SplitCamelCaseRegex = new Regex("([a-z])([A-Z])", RegexOptions.Compiled); private static readonly string[] PolymorphicMonumentVariants = { "fishing_village_b", "fishing_village_c", "harbor_1", "harbor_2", "power_sub_big_1", "power_sub_big_2", "power_sub_small_1", "power_sub_small_2", "water_well_a", "water_well_b", "water_well_c", "water_well_d", "water_well_e", "entrance_bunker_a", "entrance_bunker_b", "entrance_bunker_c", "entrance_bunker_d", }; public static void NameTelephone(Telephone telephone, BaseMonument monument, Vector3 position, MonumentHelper monumentHelper) { string phoneName = null; var monumentInfo = monument.Object as MonumentInfo; if (monumentInfo != null && !string.IsNullOrEmpty(monumentInfo.displayPhrase.translated)) { phoneName = monumentInfo.displayPhrase.translated; if (ShouldAppendCoordinate(monument.ShortName, monumentHelper)) { phoneName += $" {PhoneController.PositionToGridCoord(position)}"; } } var dungeonGridCell = monument.Object as DungeonGridCell; if (dungeonGridCell != null && !string.IsNullOrEmpty(monument.Alias)) { phoneName = GetFTLPhoneName(monument.Alias, dungeonGridCell, monument, position, monumentHelper); } var dungeonBaseLink = monument.Object as DungeonBaseLink; if (dungeonBaseLink != null) { phoneName = GetUnderwaterLabPhoneName(dungeonBaseLink, position); } var dynamicMonument = monument as DynamicMonument; if (dynamicMonument != null) { phoneName = GetDynamicMonumentPhoneName(dynamicMonument, telephone); } telephone.Controller.PhoneName = !string.IsNullOrEmpty(phoneName) ? phoneName : $"{telephone.GetDisplayName()} {PhoneController.PositionToGridCoord(position)}"; TelephoneManager.RegisterTelephone(telephone.Controller); } private static string GetFTLCorridorPhoneName(string tunnelName, Vector3 position) { return $"{Tunnel} {tunnelName} {PhoneController.PositionToGridCoord(position)}"; } private static string GetFTLPhoneName(string tunnelAlias, DungeonGridCell dungeonGridCell, BaseMonument monument, Vector3 position, MonumentHelper monumentHelper) { var tunnelName = SplitCamelCase(tunnelAlias); var phoneName = GetFTLCorridorPhoneName(tunnelName, position); if (monument.AliasOrShortName == "TrainStation") { var attachedMonument = monumentHelper.GetMonumentFromTunnel(dungeonGridCell); if (attachedMonument != null && !attachedMonument.name.Contains("tunnel-entrance/entrance_bunker")) { phoneName = GetFTLTrainStationPhoneName(attachedMonument, tunnelName, position, monumentHelper); } } return phoneName; } private static string GetFTLTrainStationPhoneName(MonumentInfo attachedMonument, string tunnelName, Vector3 position, MonumentHelper monumentHelper) { var phoneName = string.IsNullOrEmpty(attachedMonument.displayPhrase.translated) ? $"{Tunnel} {tunnelName}" : $"{Tunnel} {attachedMonument.displayPhrase.translated} {tunnelName}"; var shortname = GetShortName(attachedMonument.name); if (ShouldAppendCoordinate(shortname, monumentHelper)) { phoneName += $" {PhoneController.PositionToGridCoord(position)}"; } return phoneName; } private static string GetUnderwaterLabPhoneName(DungeonBaseLink link, Vector3 position) { var floors = link.Dungeon.Floors; var gridCoordinate = PhoneController.PositionToGridCoord(position); for (int i = 0; i < floors.Count; i++) { if (floors[i].Links.Contains(link)) { var roomLevel = $"L{1 + i}"; var roomType = link.Type.ToString(); var roomNumber = 1 + floors[i].Links.IndexOf(link); var roomName = $"{roomLevel} {roomType} {roomNumber}"; return $"{UnderwaterLab} {gridCoordinate} {roomName}"; } } return $"{UnderwaterLab} {gridCoordinate}"; } private static string GetDynamicMonumentPhoneName(DynamicMonument monument, Telephone phone) { if (monument.RootEntity is CargoShip) return $"{CargoShip} {monument.EntityId}"; return $"{phone.GetDisplayName()} {monument.EntityId}"; } private static bool ShouldAppendCoordinate(string monumentShortName, MonumentHelper monumentHelper) { if (PolymorphicMonumentVariants.Contains(monumentShortName)) return true; return !monumentHelper.IsMonumentUnique(monumentShortName); } private static string SplitCamelCase(string camelCase) { return SplitCamelCaseRegex.Replace(camelCase, "$1 $2"); } } #endregion #endregion #region Monuments private abstract class BaseMonument { public MonoBehaviour Object { get; } public virtual string PrefabName => Object.name; public virtual string ShortName => GetShortName(PrefabName); public virtual string Alias => null; public virtual string AliasOrShortName => Alias ?? ShortName; public virtual Vector3 Position => Object.transform.position; public virtual Quaternion Rotation => Object.transform.rotation; public virtual bool IsValid => Object != null; protected BaseMonument(MonoBehaviour behavior) { Object = behavior; } public virtual Vector3 TransformPoint(Vector3 localPosition) => Object.transform.TransformPoint(localPosition); public virtual Vector3 InverseTransformPoint(Vector3 worldPosition) => Object.transform.InverseTransformPoint(worldPosition); public abstract Vector3 ClosestPointOnBounds(Vector3 position); public abstract bool IsInBounds(Vector3 position); public virtual bool IsSameAs(BaseMonument other) { return PrefabName == other.PrefabName && Position == other.Position; } } private class MonumentAdapter : BaseMonument { public override string PrefabName => (string)_monumentInfo["PrefabName"]; public override string ShortName => (string)_monumentInfo["ShortName"]; public override string Alias => (string)_monumentInfo["Alias"]; public override Vector3 Position => (Vector3)_monumentInfo["Position"]; public override Quaternion Rotation => (Quaternion)_monumentInfo["Rotation"]; private Dictionary _monumentInfo; public MonumentAdapter(Dictionary monumentInfo) : base((MonoBehaviour)monumentInfo["Object"]) { _monumentInfo = monumentInfo; } public override Vector3 TransformPoint(Vector3 localPosition) => ((Func)_monumentInfo["TransformPoint"]).Invoke(localPosition); public override Vector3 InverseTransformPoint(Vector3 worldPosition) => ((Func)_monumentInfo["InverseTransformPoint"]).Invoke(worldPosition); public override Vector3 ClosestPointOnBounds(Vector3 position) => ((Func)_monumentInfo["ClosestPointOnBounds"]).Invoke(position); public override bool IsInBounds(Vector3 position) => ((Func)_monumentInfo["IsInBounds"]).Invoke(position); } private class DynamicMonument : BaseMonument { public BaseEntity RootEntity { get; } public bool IsMobile { get; } public override bool IsValid => base.IsValid && !RootEntity.IsDestroyed; public NetworkableId EntityId { get; } protected OBB BoundingBox => RootEntity.WorldSpaceBounds(); public DynamicMonument(BaseEntity entity, bool isMobile) : base(entity) { RootEntity = entity; EntityId = entity.net?.ID ?? new NetworkableId(); IsMobile = isMobile; } public override Vector3 ClosestPointOnBounds(Vector3 position) => BoundingBox.ClosestPoint(position); public override bool IsInBounds(Vector3 position) => BoundingBox.Contains(position); public override bool IsSameAs(BaseMonument other) { return RootEntity == (other as DynamicMonument)?.RootEntity; } } #endregion #region Undo Manager private abstract class BaseUndo { private const float ExpireAfterSeconds = 300; public virtual bool IsValid => !IsExpired && ProfileExists; protected readonly MonumentAddons _plugin; protected readonly ProfileController _profileController; protected readonly string _monumentAliasOrShortName; private readonly float _undoTime; protected Profile Profile => _profileController.Profile; private bool IsExpired => _undoTime + ExpireAfterSeconds < UnityEngine.Time.realtimeSinceStartup; private bool ProfileExists => _plugin._profileStore.Exists(Profile.Name); protected BaseUndo(MonumentAddons plugin, ProfileController profileController, string monumentAliasOrShortName) { _plugin = plugin; _undoTime = UnityEngine.Time.realtimeSinceStartup; _profileController = profileController; _monumentAliasOrShortName = monumentAliasOrShortName; } public abstract void Undo(BasePlayer player); } private class UndoKill : BaseUndo { protected readonly BaseData _data; public UndoKill(MonumentAddons plugin, ProfileController profileController, string monumentAliasOrShortName, BaseData data) : base(plugin, profileController, monumentAliasOrShortName) { _data = data; } public override void Undo(BasePlayer player) { Profile.AddData(_monumentAliasOrShortName, _data); _plugin._profileStore.Save(Profile); if (_profileController.IsEnabled) { var matchingMonuments = _plugin.GetMonumentsByAliasOrShortName(_monumentAliasOrShortName); if (matchingMonuments?.Count > 0) { _profileController.SpawnNewData(_data, matchingMonuments); } } var iPlayer = player.IPlayer; _plugin.ReplyToPlayer(iPlayer, LangEntry.UndoKillSuccess, _plugin.GetAddonName(iPlayer, _data), _monumentAliasOrShortName, Profile.Name); } } private class UndoKillSpawnPoint : UndoKill { private readonly SpawnGroupData _spawnGroupData; private readonly SpawnPointData _spawnPointData; public UndoKillSpawnPoint(MonumentAddons plugin, ProfileController profileController, string monumentAliasOrShortName, SpawnGroupData spawnGroupData, SpawnPointData spawnPointData) : base(plugin, profileController, monumentAliasOrShortName, spawnGroupData) { _spawnGroupData = spawnGroupData; _spawnPointData = spawnPointData; } public override void Undo(BasePlayer player) { if (!Profile.HasSpawnGroup(_monumentAliasOrShortName, _spawnGroupData.Id)) { base.Undo(player); return; } _spawnGroupData.SpawnPoints.Add(_spawnPointData); _plugin._profileStore.Save(Profile); if (_profileController.IsEnabled) { (_profileController.FindControllerById(_spawnGroupData.Id) as SpawnGroupController)?.CreateSpawnPoint(_spawnPointData); } var iPlayer = player.IPlayer; _plugin.ReplyToPlayer(iPlayer, LangEntry.UndoKillSuccess, _plugin.GetAddonName(iPlayer, _spawnPointData), _monumentAliasOrShortName, Profile.Name); } } private class UndoManager { private static void CleanStack(Stack stack) { BaseUndo undoAction; while (stack.TryPeek(out undoAction) && !undoAction.IsValid) { stack.Pop(); } } private readonly Dictionary> _undoStackByPlayer = new Dictionary>(); public bool TryUndo(BasePlayer player) { var undoStack = GetUndoStack(player); if (undoStack == null) return false; BaseUndo undoAction; if (!undoStack.TryPop(out undoAction)) return false; undoAction.Undo(player); return true; } public void AddUndo(BasePlayer player, BaseUndo undoAction) { EnsureUndoStack(player).Push(undoAction); } private Stack EnsureUndoStack(BasePlayer player) { var undoStack = GetUndoStack(player); if (undoStack == null) { undoStack = new Stack(); _undoStackByPlayer[player.userID] = undoStack; } return undoStack; } private Stack GetUndoStack(BasePlayer player) { Stack stack; if (!_undoStackByPlayer.TryGetValue(player.userID, out stack)) return null; CleanStack(stack); return stack; } } #endregion #region Adapters/Controllers #region Entity Component private class MonumentEntityComponent : FacepunchBehaviour { public static void AddToEntity(MonumentEntityTracker entityTracker, BaseEntity entity, IEntityAdapter adapter, BaseMonument monument) { entity.GetOrAddComponent().Init(entityTracker, adapter, monument); var parentSphere = entity.GetParentEntity() as SphereEntity; if (parentSphere != null) { AddToEntity(entityTracker, parentSphere, adapter, monument); } } public static void RemoveFromEntity(BaseEntity entity) { DestroyImmediate(entity.GetComponent()); var parentSphere = entity.GetParentEntity() as SphereEntity; if (parentSphere != null) { RemoveFromEntity(parentSphere); } } public static MonumentEntityComponent GetForEntity(BaseEntity entity) => entity.GetComponent(); public static MonumentEntityComponent GetForEntity(NetworkableId id) => BaseNetworkable.serverEntities.Find(id)?.GetComponent(); public IEntityAdapter Adapter; private MonumentEntityTracker _entityTracker; private BaseEntity _entity; private void Awake() { _entity = GetComponent(); } public void Init(MonumentEntityTracker entityTracker, IEntityAdapter adapter, BaseMonument monument) { _entityTracker = entityTracker; _entityTracker.RegisterEntity(_entity); Adapter = adapter; } private void OnDestroy() { _entityTracker.UnregisterEntity(_entity); Adapter.OnEntityKilled(_entity); } } private class MonumentEntityTracker { private HashSet _trackedEntities = new HashSet(); public bool IsMonumentEntity(BaseEntity entity) { return entity != null && !entity.IsDestroyed && _trackedEntities.Contains(entity); } public bool IsMonumentEntity(BaseEntity entity, out TAdapter adapter, out TController controller) where TAdapter : EntityAdapter where TController : EntityController { adapter = null; controller = null; if (!IsMonumentEntity(entity)) return false; var component = MonumentEntityComponent.GetForEntity(entity); if (component == null) return false; adapter = component.Adapter as TAdapter; controller = adapter?.Controller as TController; return controller != null; } public bool IsMonumentEntity(BaseEntity entity, out TController controller) where TController : EntityController { EntityAdapter adapter; return IsMonumentEntity(entity, out adapter, out controller); } public void RegisterEntity(BaseEntity entity) => _trackedEntities.Add(entity); public void UnregisterEntity(BaseEntity entity) => _trackedEntities.Remove(entity); } #endregion #region Adapter/Controller - Base private interface IEntityAdapter { void OnEntityKilled(BaseEntity entity); } // Represents a single entity, spawn group, or spawn point at a single monument. private abstract class BaseAdapter { public BaseData Data { get; } public BaseController Controller { get; } public BaseMonument Monument { get; } public MonumentAddons Plugin => Controller.Plugin; public ProfileController ProfileController => Controller.ProfileController; public Profile Profile => Controller.Profile; protected Configuration _config => Plugin._config; protected ProfileStateData _profileStateData => Plugin._profileStateData; protected IOManager _ioManager => Plugin._ioManager; // Subclasses can override this to wait more than one frame for spawn/kill operations. public IEnumerator WaitInstruction { get; protected set; } protected BaseAdapter(BaseData data, BaseController controller, BaseMonument monument) { Data = data; Controller = controller; Monument = monument; } // Creates all GameObjects/Component that make up the addon. public abstract void Spawn(); // Destroys all GameObjects/Components that make up the addon. public abstract void Kill(); public virtual void Unregister() {} // Called when the addon is scheduled to be killed or unregistered. public virtual void PreUnload() {} } // Represents a single entity or spawn point at a single monument. private abstract class TransformAdapter : BaseAdapter { public BaseTransformData TransformData { get; } public abstract Vector3 Position { get; } public abstract Quaternion Rotation { get; } public Vector3 LocalPosition => Monument.InverseTransformPoint(Position); public Quaternion LocalRotation => Quaternion.Inverse(Monument.Rotation) * Rotation; public Vector3 IntendedPosition { get { var intendedPosition = Monument.TransformPoint(TransformData.Position); if (TransformData.SnapToTerrain) intendedPosition.y = TerrainMeta.HeightMap.GetHeight(intendedPosition); return intendedPosition; } } public Quaternion IntendedRotation => Monument.Rotation * Quaternion.Euler(TransformData.RotationAngles); public virtual bool IsAtIntendedPosition => Position == IntendedPosition && Rotation == IntendedRotation; protected TransformAdapter(BaseTransformData transformData, BaseController controller, BaseMonument monument) : base(transformData, controller, monument) { TransformData = transformData; } } // Represents an entity or spawn point across one or more identical monuments. private abstract class BaseController { public ProfileController ProfileController { get; } public BaseData Data { get; } public List Adapters { get; } = new List(); public MonumentAddons Plugin => ProfileController.Plugin; public Profile Profile => ProfileController.Profile; private bool _enabled = true; protected BaseController(ProfileController profileController, BaseData data) { ProfileController = profileController; Data = data; } public abstract BaseAdapter CreateAdapter(BaseMonument monument); public virtual void OnAdapterSpawned(BaseAdapter adapter) {} public virtual void OnAdapterKilled(BaseAdapter adapter) { Adapters.Remove(adapter); if (Adapters.Count == 0) { ProfileController.OnControllerKilled(this); } } public virtual BaseAdapter SpawnAtMonument(BaseMonument monument) { var adapter = CreateAdapter(monument); Adapters.Add(adapter); adapter.Spawn(); OnAdapterSpawned(adapter); return adapter; } public virtual IEnumerator SpawnAtMonumentsRoutine(IEnumerable monumentList) { foreach (var monument in monumentList) { if (!_enabled) yield break; Plugin.TrackStart(); var adapter = SpawnAtMonument(monument); Plugin.TrackEnd(); yield return adapter.WaitInstruction; } } // Subclasses can override this if they need to kill child data (e.g., spawn point data). public virtual Coroutine Kill(BaseData data) { PreUnload(); Coroutine coroutine = null; if (Adapters.Count > 0) { coroutine = CoroutineManager.StartGlobalCoroutine(KillRoutine()); } ProfileController.OnControllerKilled(this); return coroutine; } public Coroutine Kill() { return Kill(Data); } public void PreUnload() { // Stop the controller from spawning more adapters. _enabled = false; for (var i = Adapters.Count - 1; i >= 0; i--) { Adapters[i].PreUnload(); } } public IEnumerator KillRoutine() { for (var i = Adapters.Count - 1; i >= 0; i--) { Plugin.TrackStart(); var adapter = Adapters[i]; adapter.Kill(); Plugin.TrackEnd(); yield return adapter.WaitInstruction; } } public void Unregister() { for (var i = Adapters.Count - 1; i >= 0; i--) { Plugin.TrackStart(); Adapters[i].Unregister(); Plugin.TrackEnd(); } } public BaseAdapter FindAdapterForMonument(BaseMonument monument) { foreach (var adapter in Adapters) { if (adapter.Monument.IsSameAs(monument)) return adapter; } return null; } } #endregion #region Adapter/Controller - Prefab private class PrefabAdapter : TransformAdapter { public GameObject GameObject { get; private set; } public PrefabData PrefabData { get; } public Transform Transform { get; private set; } public override Vector3 Position => Transform.position; public override Quaternion Rotation => Transform.rotation; public PrefabAdapter(BaseController controller, PrefabData prefabData, BaseMonument monument) : base(prefabData, controller, monument) { PrefabData = prefabData; } public override void Spawn() { GameObject = GameManager.server.CreatePrefab(PrefabData.PrefabName, IntendedPosition, IntendedRotation); Transform = GameObject.transform; } public override void Kill() { UnityEngine.Object.Destroy(GameObject); } } private class PrefabController : BaseController { public PrefabData PrefabData { get; } public PrefabController(ProfileController profileController, PrefabData prefabData) : base(profileController, prefabData) { PrefabData = prefabData; } public override BaseAdapter CreateAdapter(BaseMonument monument) => new PrefabAdapter(this, PrefabData, monument); } #endregion #region Entity Adapter/Controller private class EntityAdapter : TransformAdapter, IEntityAdapter { private class IOEntityOverrideInfo { public Dictionary Inputs = new Dictionary(); public Dictionary Outputs = new Dictionary(); } private static readonly Dictionary IOOverridesByEntity = new Dictionary { ["assets/prefabs/io/electric/switches/doormanipulator.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(0, 0.9f, 0), [1] = new Vector3(0, 0.85f, 0), }, }, ["assets/prefabs/io/electric/switches/simpleswitch/simpleswitch.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(0, 0.78f, 0.03f), }, Outputs = new Dictionary { [0] = new Vector3(0, 1.22f, 0.03f), }, }, ["assets/prefabs/io/electric/switches/timerswitch.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(0, 0.795f, 0.03f), [1] = new Vector3(-0.15f, 1.04f, 0.03f), }, Outputs = new Dictionary { [0] = new Vector3(0, 1.295f, 0.025f), }, }, ["assets/prefabs/io/electric/switches/orswitch.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(-0.035f, 0.82f, -0.125f), [1] = new Vector3(-0.035f, 0.82f, -0.175f), }, Outputs = new Dictionary { [0] = new Vector3(-0.035f, 1.3f, -0.15f), [1] = new Vector3(-0.035f, 1.3f, -0.15f), [2] = new Vector3(-0.035f, 1.3f, -0.15f), [3] = new Vector3(-0.035f, 1.3f, -0.15f), [4] = new Vector3(-0.035f, 1.3f, -0.15f), [5] = new Vector3(-0.035f, 1.3f, -0.15f), [6] = new Vector3(-0.035f, 1.3f, -0.15f), [7] = new Vector3(-0.035f, 1.3f, -0.15f), }, }, ["assets/prefabs/io/electric/switches/cardreader.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(-0.014f, 1.18f, 0.03f), }, Outputs = new Dictionary { [0] = new Vector3(-0.014f, 1.55f, 0.03f), }, }, ["assets/prefabs/io/electric/switches/pressbutton/pressbutton.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(0, 1.1f, 0.025f), }, Outputs = new Dictionary { [0] = new Vector3(0, 1.38f, 0.025f), }, }, ["assets/prefabs/io/kinetic/wheelswitch.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(-0.1f, 0, 0), }, Outputs = new Dictionary { [0] = new Vector3(0.1f, 0, 0), }, }, ["assets/content/structures/interactive_garage_door/sliding_blast_door.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(0.1f, 0, 0), }, Outputs = new Dictionary { [0] = new Vector3(-0.1f, 0, 0), }, }, ["assets/prefabs/io/electric/switches/fusebox/fusebox.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(0, 1.06f, 0.06f), }, Outputs = new Dictionary { [0] = new Vector3(0, 1.565f, 0.06f), [1] = new Vector3(0, 1.565f, 0.06f), [2] = new Vector3(0, 1.565f, 0.06f), [3] = new Vector3(0, 1.565f, 0.06f), [4] = new Vector3(0, 1.565f, 0.06f), [5] = new Vector3(0, 1.565f, 0.06f), [6] = new Vector3(0, 1.565f, 0.06f), [7] = new Vector3(0, 1.565f, 0.06f), }, }, ["assets/prefabs/io/electric/generators/generator.static.prefab"] = new IOEntityOverrideInfo { Outputs = new Dictionary { [0] = new Vector3(0, 0.75f, -0.5f), [1] = new Vector3(0, 0.75f, -0.3f), [2] = new Vector3(0, 0.75f, -0.1f), [3] = new Vector3(0, 0.75f, 0.1f), [4] = new Vector3(0, 0.75f, 0.3f), [5] = new Vector3(0, 0.75f, 0.5f), }, }, ["assets/prefabs/io/electric/switches/splitter.prefab"] = new IOEntityOverrideInfo { Inputs = new Dictionary { [0] = new Vector3(-0.03f, 1.44f, 0f), }, Outputs = new Dictionary { [0] = new Vector3(-0.03f, 0.8f,0.108f), [1] = new Vector3(-0.03f, 0.8f, 0), [2] = new Vector3(-0.03f, 0.8f, -0.112f), }, }, }; public BaseEntity Entity { get; private set; } public EntityData EntityData { get; } public virtual bool IsDestroyed => Entity == null || Entity.IsDestroyed; public override Vector3 Position => Transform.position; public override Quaternion Rotation => Transform.rotation; public Transform Transform { get; private set; } private PuzzleResetHandler _puzzleResetHandler; private BuildingGrade.Enum IntendedBuildingGrade { get { var buildingBlock = Entity as BuildingBlock; var desiredGrade = EntityData.BuildingBlock?.Grade; return desiredGrade != null && desiredGrade != BuildingGrade.Enum.None ? desiredGrade.Value : buildingBlock.blockDefinition.defaultGrade.gradeBase.type; } } public EntityAdapter(BaseController controller, EntityData entityData, BaseMonument monument) : base(entityData, controller, monument) { EntityData = entityData; } public override void Spawn() { var existingEntity = _profileStateData.FindEntity(Profile.Name, Monument, Data.Id); if (existingEntity != null) { if (existingEntity.PrefabName != EntityData.PrefabName) { existingEntity.Kill(); } else { Entity = existingEntity; Transform = Entity.transform; PreEntitySpawn(); PostEntitySpawn(); HandleChanges(); MonumentEntityComponent.AddToEntity(Plugin._entityTracker, Entity, this, Monument); var vendingMachine = Entity as NPCVendingMachine; if (vendingMachine != null) { Plugin.RefreshVendingProfile(vendingMachine); } var mountable = Entity as BaseMountable; if (mountable != null) { var dynamicMonument = Monument as DynamicMonument; if (dynamicMonument != null && dynamicMonument.IsMobile) { mountable.isMobile = true; if (!BaseMountable.FixedUpdateMountables.Contains(mountable)) { BaseMountable.FixedUpdateMountables.Add(mountable); } } } if (!_config.EnableEntitySaving) { // If saving is no longer enabled, remove the entity from the data file. // This prevents a bug where a subsequent reload would discover the entity before it is destroyed. _profileStateData.RemoveEntity(Profile.Name, Monument, Data.Id); Plugin._saveProfileStateDebounced.Schedule(); } return; } } Entity = CreateEntity(IntendedPosition, IntendedRotation); Transform = Entity.transform; PreEntitySpawn(); Entity.Spawn(); PostEntitySpawn(); if (_config.EnableEntitySaving && Entity != existingEntity) { _profileStateData.AddEntity(Profile.Name, Monument, Data.Id, Entity.net.ID); Plugin._saveProfileStateDebounced.Schedule(); } } public override void Kill() { if (IsDestroyed) return; PreEntityKill(); Entity.Kill(); } public virtual void OnEntityKilled(BaseEntity entity) { Plugin.TrackStart(); // Only consider the adapter destroyed if the main entity was destroyed. // For example, the scaled sphere parent may be killed if resized to default scale. if (entity == Entity) { if (entity == null || entity.IsDestroyed) { if (_profileStateData.RemoveEntity(Profile.Name, Monument, Data.Id)) { Plugin._saveProfileStateDebounced.Schedule(); } } Controller.OnAdapterKilled(this); } Plugin.TrackEnd(); } public void UpdateScale() { if (Plugin.TryScaleEntity(Entity, EntityData.Scale)) { var parentSphere = Entity.GetParentEntity() as SphereEntity; if (parentSphere == null) return; if (Plugin._entityTracker.IsMonumentEntity(parentSphere)) return; MonumentEntityComponent.AddToEntity(Plugin._entityTracker, parentSphere, this, Monument); } } public void UpdateSkin() { if (Entity.skinID == EntityData.Skin || EntityData.Skin == 0 && Entity is NPCVendingMachine) return; Entity.skinID = EntityData.Skin; if (Entity.IsFullySpawned()) { Entity.SendNetworkUpdate(); } } public bool TrySaveAndApplyChanges() { var hasChanged = false; if (!IsAtIntendedPosition) { EntityData.Position = LocalPosition; EntityData.RotationAngles = LocalRotation.eulerAngles; EntityData.SnapToTerrain = IsOnTerrain(Position); hasChanged = true; } var buildingBlock = Entity as BuildingBlock; if (buildingBlock != null && buildingBlock.grade != IntendedBuildingGrade) { if (EntityData.BuildingBlock == null) { EntityData.BuildingBlock = new BuildingBlockInfo(); } EntityData.BuildingBlock.Grade = buildingBlock.grade; hasChanged = true; } if (hasChanged) { var singleEntityController = Controller as EntityController; singleEntityController.StartHandleChangesRoutine(); Plugin._profileStore.Save(singleEntityController.Profile); } return hasChanged; } public virtual void HandleChanges() { UpdatePosition(); UpdateSkin(); UpdateScale(); UpdateBuildingGrade(); UpdateSkullName(); UpdatePuzzle(); UpdateCardReaderLevel(); UpdateIOConnections(); MaybeProvidePower(); } public void UpdateSkullName() { var skullName = EntityData.SkullName; if (skullName == null) return; var skullTrophy = Entity as SkullTrophy; if (skullTrophy == null) return; if (skullTrophy.inventory == null) return; if (skullTrophy.inventory.itemList.Count == 1) { var item = skullTrophy.inventory.itemList[0]; item.RemoveFromContainer(); item.Remove(); } var skull = ItemManager.CreateByPartialName("skull.human"); skull.name = HumanBodyResourceDispenser.CreateSkullName(skullName); if (!skull.MoveToContainer(skullTrophy.inventory)) skull.Remove(); // Setting flag here so vanilla functionality is preserved for trophies without name set skullTrophy.SetFlag(BaseEntity.Flags.Busy, true); } public void UpdateIOConnections() { var ioEntityData = EntityData.IOEntityData; if (ioEntityData == null) return; var ioEntity = Entity as IOEntity; if (ioEntity == null) return; var hasChanged = false; for (var outputSlotIndex = 0; outputSlotIndex < ioEntity.outputs.Length; outputSlotIndex++) { var sourceSlot = ioEntity.outputs[outputSlotIndex]; var destinationEntity = sourceSlot.connectedTo.Get(); var destinationSlot = destinationEntity?.inputs.ElementAtOrDefault(sourceSlot.connectedToSlot); var connectionData = ioEntityData.FindConnection(outputSlotIndex); if (connectionData != null) { var intendedDestinationEntity = Controller.ProfileController.FindEntity(connectionData.ConnectedToId, Monument); var intendedDestinationSlot = intendedDestinationEntity?.inputs.ElementAtOrDefault(connectionData.ConnectedToSlot); if (destinationSlot != intendedDestinationSlot) { // Existing destination entity or slot is incorrect. if (destinationSlot != null) { ClearIOSlot(destinationEntity, destinationSlot); } destinationEntity = intendedDestinationEntity; destinationSlot = intendedDestinationSlot; sourceSlot.Clear(); hasChanged = true; } if (destinationEntity == null) { // The destination entity cannot be found. Maybe it was destroyed. continue; } if (destinationSlot == null) { // The destination slot cannot be found. Maybe the index was out of range (bad data). continue; } SetupIOSlot(sourceSlot, destinationEntity, connectionData.ConnectedToSlot, connectionData.Color); if (connectionData.ShowWire) { if (sourceSlot.type != IOType.Kinetic && destinationSlot.type != IOType.Kinetic) { var numPoints = connectionData.Points?.Length ?? 0; sourceSlot.linePoints = new Vector3[numPoints + 2]; sourceSlot.linePoints[0] = sourceSlot.handlePosition; sourceSlot.linePoints[numPoints + 1] = Transform.InverseTransformPoint(destinationEntity.transform.TransformPoint(destinationSlot.handlePosition)); for (var pointIndex = 0; pointIndex < numPoints; pointIndex++) { sourceSlot.linePoints[pointIndex + 1] = Transform.InverseTransformPoint(Monument.TransformPoint(connectionData.Points[pointIndex])); } } } else { sourceSlot.linePoints = Array.Empty(); } SetupIOSlot(destinationSlot, ioEntity, connectionData.Slot, connectionData.Color); destinationEntity.SendNetworkUpdate(); continue; } if (destinationSlot != null) { // No connection data saved, so clear existing connection. ClearIOSlot(destinationEntity, destinationSlot); sourceSlot.Clear(); hasChanged = true; } } if (hasChanged) { ioEntity.MarkDirtyForceUpdateOutputs(); ioEntity.SendNetworkUpdate(); ioEntity.SendChangedToRoot(forceUpdate: true); } } public void MaybeProvidePower() { var ioEntity = Entity as IOEntity; if ((object)ioEntity == null) return; _ioManager.MaybeProvidePower(ioEntity); } public override void PreUnload() { if (_puzzleResetHandler != null) { _puzzleResetHandler.Destroy(); } var targetDoor = (Entity as DoorManipulator)?.targetDoor; if (targetDoor != null) { targetDoor.SetFlag(BaseEntity.Flags.Locked, false); } } public void HandlePuzzleReset() { var spawnGroupIdList = EntityData.Puzzle?.SpawnGroupIds; if (spawnGroupIdList == null) return; foreach (var spawnGroupId in spawnGroupIdList) { (ProfileController.FindAdapter(spawnGroupId, Monument) as SpawnGroupAdapter)?.SpawnGroup.OnPuzzleReset(); } } protected virtual void PreEntitySpawn() { UpdateSkin(); UpdateCardReaderLevel(); EntitySetupUtils.PreSpawnShared(Entity); var buildingBlock = Entity as BuildingBlock; if (buildingBlock != null) { buildingBlock.blockDefinition = PrefabAttribute.server.Find(buildingBlock.prefabID); if (buildingBlock.blockDefinition != null) { buildingBlock.SetGrade(IntendedBuildingGrade); var maxHealth = buildingBlock.currentGrade.maxHealth; buildingBlock.InitializeHealth(maxHealth, maxHealth); buildingBlock.ResetLifeStateOnSpawn = false; } } var ioEntity = Entity as IOEntity; if (ioEntity != null) { UpdateIOEntitySlotPositions(ioEntity); } } protected virtual void PostEntitySpawn() { EntitySetupUtils.PostSpawnShared(Plugin, Entity, _config.EnableEntitySaving); UpdatePuzzle(); // NPCVendingMachine needs its skin updated after spawn because vanilla sets it to 861142659. UpdateSkin(); var computerStation = Entity as ComputerStation; if (computerStation != null && computerStation.isStatic) { var computerStation2 = computerStation; computerStation.CancelInvoke(computerStation.GatherStaticCameras); computerStation.Invoke(() => { Plugin.TrackStart(); GatherStaticCameras(computerStation2); Plugin.TrackEnd(); }, 1); } var paddlingPool = Entity as PaddlingPool; if (paddlingPool != null) { paddlingPool.inventory.AddItem(Plugin._waterDefinition, paddlingPool.inventory.maxStackSize); // Disallow adding or removing water. paddlingPool.SetFlag(BaseEntity.Flags.Busy, true); } var vehicleSpawner = Entity as VehicleSpawner; if (vehicleSpawner != null) { var vehicleSpawner2 = vehicleSpawner; vehicleSpawner.Invoke(() => { Plugin.TrackStart(); EntityUtils.ConnectNearbyVehicleVendor(vehicleSpawner2); Plugin.TrackEnd(); }, 1); } var vehicleVendor = Entity as VehicleVendor; if (vehicleVendor != null) { // Use a slightly longer delay than the vendor check since this can short-circuit as an optimization. var vehicleVendor2 = vehicleVendor; vehicleVendor.Invoke(() => { Plugin.TrackStart(); EntityUtils.ConnectNearbyVehicleSpawner(vehicleVendor2); Plugin.TrackEnd(); }, 2); } var candle = Entity as Candle; if (candle != null) { candle.SetFlag(BaseEntity.Flags.On, true); candle.CancelInvoke(candle.Burn); // Disallow extinguishing. candle.SetFlag(BaseEntity.Flags.Busy, true); } var fogMachine = Entity as FogMachine; if (fogMachine != null) { var fogMachine2 = fogMachine; fogMachine.SetFlag(BaseEntity.Flags.On, true); fogMachine.InvokeRepeating(() => { fogMachine2.SetFlag(FogMachine.Emitting, true); fogMachine2.Invoke(fogMachine2.EnableFogField, 1f); fogMachine2.Invoke(fogMachine2.DisableNozzle, fogMachine2.nozzleBlastDuration); fogMachine2.Invoke(fogMachine2.FinishFogging, fogMachine2.fogLength); }, UnityEngine.Random.Range(0f, 5f), fogMachine.fogLength - 1); // Disallow interaction. fogMachine.SetFlag(BaseEntity.Flags.Busy, true); } var oven = Entity as BaseOven; if (oven != null) { // Lanterns if (oven is BaseFuelLightSource) { oven.SetFlag(BaseEntity.Flags.On, true); oven.SetFlag(BaseEntity.Flags.Busy, true); } // jackolantern.angry or jackolantern.happy else if (oven.prefabID == 1889323056 || oven.prefabID == 630866573) { oven.SetFlag(BaseEntity.Flags.On, true); oven.SetFlag(BaseEntity.Flags.Busy, true); } } var spooker = Entity as SpookySpeaker; if (spooker != null) { spooker.SetFlag(BaseEntity.Flags.On, true); spooker.InvokeRandomized( spooker.SendPlaySound, spooker.soundSpacing, spooker.soundSpacing, spooker.soundSpacingRand); spooker.SetFlag(BaseEntity.Flags.Busy, true); } var doorManipulator = Entity as DoorManipulator; if (doorManipulator != null) { if (doorManipulator.targetDoor != null) { doorManipulator.targetDoor.SetFlag(BaseEntity.Flags.Locked, true); } else { var doorManipulator2 = doorManipulator; doorManipulator.Invoke(() => { Plugin.TrackStart(); EntityUtils.ConnectNearbyDoor(doorManipulator2); Plugin.TrackEnd(); }, 1); } } var spray = Entity as SprayCanSpray; if (spray != null) { spray.CancelInvoke(spray.RainCheck); #if !CARBON spray.splashThreshold = int.MaxValue; #endif } var telephone = Entity as Telephone; if (telephone != null && telephone.prefabID == 1009655496) { PhoneUtils.NameTelephone(telephone, Monument, Position, Plugin._monumentHelper); } var microphoneStand = Entity as MicrophoneStand; if ((object)microphoneStand != null) { var microphoneStand2 = microphoneStand; microphoneStand.Invoke(() => { Plugin.TrackStart(); microphoneStand2.PostMapEntitySpawn(); Plugin.TrackEnd(); }, 1); } var storageContainer = Entity as StorageContainer; if ((object)storageContainer != null) { storageContainer.isLockable = false; storageContainer.isMonitorable = false; } var christmasTree = Entity as ChristmasTree; if ((object)christmasTree != null) { foreach (var itemShortName in _config.XmasTreeDecorations) { var item = ItemManager.CreateByName(itemShortName); if (item == null) continue; if (!item.MoveToContainer(christmasTree.inventory)) { item.Remove(); } } christmasTree.inventory.SetLocked(true); } if (EntityData.Scale != 1 || Entity.GetParentEntity() is SphereEntity) { UpdateScale(); } var skullTrophy = Entity as SkullTrophy; if (skullTrophy != null) { UpdateSkullName(); } } protected virtual void PreEntityKill() {} public override void Unregister() { if (IsDestroyed) return; // Not safe to unregister the entity if the profile no longer declares it. if (!Profile.HasEntity(Monument.AliasOrShortName, EntityData)) return; // Not safe to unregister the entity if it's not tracked in the profile state. if (!_profileStateData.HasEntity(Profile.Name, Monument, Data.Id, Entity.net.ID)) return; MonumentEntityComponent.RemoveFromEntity(Entity); } protected BaseEntity CreateEntity(Vector3 position, Quaternion rotation) { var entity = GameManager.server.CreateEntity(EntityData.PrefabName, position, rotation); if (entity == null) return null; EnableSavingRecursive(entity, enableSaving: _config.EnableEntitySaving); var dynamicMonument = Monument as DynamicMonument; if (dynamicMonument != null) { entity.SetParent(dynamicMonument.RootEntity, worldPositionStays: true); if (dynamicMonument.IsMobile) { var mountable = entity as BaseMountable; if (mountable != null) { // Setting isMobile prior to spawn will automatically update the position of mounted players. mountable.isMobile = true; } } } MonumentEntityComponent.AddToEntity(Plugin._entityTracker, entity, this, Monument); return entity; } private void UpdatePosition() { if (IsAtIntendedPosition) return; var entityToMove = GetEntityToMove(); var entityToRotate = Entity; entityToMove.transform.position = IntendedPosition; entityToRotate.transform.rotation = IntendedRotation; BroadcastEntityTransformChange(entityToMove); if (entityToRotate != entityToMove) { BroadcastEntityTransformChange(entityToRotate); } } private List GetNearbyStaticCameras() { var dynamicMonument = Monument as DynamicMonument; if (dynamicMonument != null && dynamicMonument.RootEntity == Entity.GetParentEntity()) { var cargoCameraList = new List(); foreach (var child in dynamicMonument.RootEntity.children) { var cctv = child as CCTV_RC; if (cctv != null && cctv.isStatic) { cargoCameraList.Add(cctv); } } return cargoCameraList; } var entityList = new List(); Vis.Entities(Entity.transform.position, 100, entityList, Rust.Layers.Mask.Deployed, QueryTriggerInteraction.Ignore); if (entityList.Count == 0) return null; var cameraList = new List(); foreach (var entity in entityList) { var cctv = entity as CCTV_RC; if (cctv != null && !cctv.IsDestroyed && cctv.isStatic) { cameraList.Add(cctv); } } return cameraList; } private void GatherStaticCameras(ComputerStation computerStation) { var cameraList = GetNearbyStaticCameras(); if (cameraList == null) return; foreach (var cctv in cameraList) { computerStation.ForceAddBookmark(cctv.rcIdentifier); } } private BaseEntity GetEntityToMove() { if (EntityData.Scale != 1 && Plugin.GetEntityScale(Entity) != 1) { var parentSphere = Entity.GetParentEntity() as SphereEntity; if (parentSphere != null) return parentSphere; } return Entity; } private void UpdateBuildingGrade() { var buildingBlock = Entity as BuildingBlock; if (buildingBlock == null) return; var intendedBuildingGrade = IntendedBuildingGrade; if (buildingBlock.grade == intendedBuildingGrade) return; buildingBlock.SetGrade(intendedBuildingGrade); buildingBlock.SetHealthToMax(); buildingBlock.SendNetworkUpdate(); buildingBlock.baseProtection = Plugin._immortalProtection; } private void UpdatePuzzle() { if (EntityData.Puzzle == null) return; var puzzleReset = (Entity as IOEntity)?.GetComponent(); if (puzzleReset == null) return; puzzleReset.playersBlockReset = EntityData.Puzzle.PlayersBlockReset; puzzleReset.playerDetectionRadius = EntityData.Puzzle.PlayerDetectionRadius; puzzleReset.timeBetweenResets = EntityData.Puzzle.SecondsBetweenResets; if (EntityData.Puzzle.SpawnGroupIds?.Count > 0) { if (_puzzleResetHandler == null) { _puzzleResetHandler = PuzzleResetHandler.Create(this, puzzleReset); } } } private void UpdateCardReaderLevel() { var cardReader = Entity as CardReader; if ((object)cardReader == null) return; var accessLevel = EntityData.CardReaderLevel; if (EntityData.CardReaderLevel == 0 || accessLevel == cardReader.accessLevel) return; cardReader.accessLevel = accessLevel; cardReader.SetFlag(cardReader.AccessLevel1, accessLevel == 1); cardReader.SetFlag(cardReader.AccessLevel2, accessLevel == 2); cardReader.SetFlag(cardReader.AccessLevel3, accessLevel == 3); } private void UpdateIOEntitySlotPositions(IOEntity ioEntity) { IOEntityOverrideInfo overrideInfo; if (!IOOverridesByEntity.TryGetValue(ioEntity.PrefabName, out overrideInfo)) return; for (var i = 0; i < ioEntity.inputs.Length; i++) { Vector3 overridePosition; if (overrideInfo.Inputs.TryGetValue(i, out overridePosition)) { ioEntity.inputs[i].handlePosition = overridePosition; } } for (var i = 0; i < ioEntity.outputs.Length; i++) { Vector3 overridePosition; if (overrideInfo.Outputs.TryGetValue(i, out overridePosition)) { ioEntity.outputs[i].handlePosition = overridePosition; } } } private void SetupIOSlot(IOSlot slot, IOEntity otherEntity, int otherSlotIndex, WireColour color) { slot.connectedTo.Set(otherEntity); slot.connectedToSlot = otherSlotIndex; slot.wireColour = color; slot.connectedTo.Init(); } private void ClearIOSlot(IOEntity ioEntity, IOSlot slot) { slot.Clear(); ioEntity.MarkDirtyForceUpdateOutputs(); ioEntity.SendNetworkUpdate(); } } private class EntityController : BaseController { public EntityData EntityData { get; } private Dictionary _vendingDataProvider; public EntityController(ProfileController profileController, EntityData entityData) : base(profileController, entityData) { EntityData = entityData; } public override BaseAdapter CreateAdapter(BaseMonument monument) => new EntityAdapter(this, EntityData, monument); public override void OnAdapterSpawned(BaseAdapter adapter) { base.OnAdapterSpawned(adapter); Plugin._adapterListenerManager.OnAdapterSpawned(adapter); } public override void OnAdapterKilled(BaseAdapter adapter) { base.OnAdapterKilled(adapter); Plugin._adapterListenerManager.OnAdapterKilled(adapter); } public Coroutine StartHandleChangesRoutine() { return ProfileController.StartCoroutine(HandleChangesRoutine()); } public Dictionary GetVendingDataProvider() { return _vendingDataProvider ?? (_vendingDataProvider = new Dictionary { ["GetData"] = new Func(() => EntityData.VendingProfile as JObject), ["SaveData"] = new Action(vendingProfile => { if (!Plugin._isLoaded) return; EntityData.VendingProfile = vendingProfile; Plugin._profileStore.Save(Profile); }), }); } private IEnumerator HandleChangesRoutine() { foreach (var adapter in Adapters.ToList()) { var singleAdapter = adapter as EntityAdapter; if (singleAdapter.IsDestroyed) continue; singleAdapter.HandleChanges(); yield return null; } } } #endregion #region Entity Adapter/Controller - Signs private class SignAdapter : EntityAdapter { public SignAdapter(BaseController controller, EntityData entityData, BaseMonument monument) : base(controller, entityData, monument) {} public override void PreUnload() { if (!Entity.IsDestroyed && !Entity.enableSaving) { // Delete sign files immediately, since the entities may not be explicitly killed on server shutdown. DeleteSignFiles(); } base.PreUnload(); } public uint[] GetTextureIds() => (Entity as ISignage)?.GetTextureCRCs(); public void SetTextureIds(uint[] textureIds) { var sign = Entity as ISignage; if (textureIds == null || textureIds.Equals(sign.GetTextureCRCs())) return; sign.SetTextureCRCs(textureIds); } public void SkinSign() { if (EntityData.SignArtistImages == null) return; Plugin.SkinSign(Entity as ISignage, EntityData.SignArtistImages); } protected override void PreEntitySpawn() { base.PreEntitySpawn(); (Entity as Signage)?.EnsureInitialized(); var carvablePumpkin = Entity as CarvablePumpkin; if (carvablePumpkin != null) { carvablePumpkin.EnsureInitialized(); carvablePumpkin.SetFlag(BaseEntity.Flags.On, true); } Entity.SetFlag(BaseEntity.Flags.Locked, true); } protected override void PreEntityKill() { // Sign files are removed by vanilla only during Die(), so we have to explicitly delete them. DeleteSignFiles(); } private void DeleteSignFiles() { FileStorage.server.RemoveAllByEntity(Entity.net.ID); var signage = Entity as Signage; if (signage != null) { if (signage.textureIDs != null) { Array.Clear(signage.textureIDs, 0, signage.textureIDs.Length); } return; } var photoFrame = Entity as PhotoFrame; if (photoFrame != null) { photoFrame._overlayTextureCrc = 0u; return; } var carvablePumpkin = Entity as CarvablePumpkin; if (carvablePumpkin != null) { if (carvablePumpkin.textureIDs != null) { Array.Clear(carvablePumpkin.textureIDs, 0, carvablePumpkin.textureIDs.Length); } return; } } } private class SignController : EntityController { public SignController(ProfileController profileController, EntityData data) : base(profileController, data) {} // Sign artist will only be called for the primary adapter. // Texture ids are copied to the others. protected SignAdapter _primaryAdapter; public override BaseAdapter CreateAdapter(BaseMonument monument) => new SignAdapter(this, EntityData, monument); public override void OnAdapterSpawned(BaseAdapter adapter) { base.OnAdapterSpawned(adapter); var signEntityAdapter = adapter as SignAdapter; if (_primaryAdapter != null) { var textureIds = _primaryAdapter.GetTextureIds(); if (textureIds != null) { signEntityAdapter.SetTextureIds(textureIds); } } else { _primaryAdapter = signEntityAdapter; _primaryAdapter.SkinSign(); } } public override void OnAdapterKilled(BaseAdapter adapter) { base.OnAdapterKilled(adapter); if (adapter == _primaryAdapter) { _primaryAdapter = Adapters.FirstOrDefault() as SignAdapter; } } public void UpdateSign(uint[] textureIds) { foreach (var adapter in Adapters) { (adapter as SignAdapter).SetTextureIds(textureIds); } } } #endregion #region Entity Adapter/Controller - CCTV private class CCTVEntityAdapter : EntityAdapter { private int _idSuffix; private string _cachedIdentifier; private string _savedIdentifier => EntityData.CCTV?.RCIdentifier; public CCTVEntityAdapter(BaseController controller, EntityData entityData, BaseMonument monument, int idSuffix) : base(controller, entityData, monument) { _idSuffix = idSuffix; } // Ensure the RC identifiers are freed up as soon as possible to avoid conflicts when reloading. public override void PreUnload() { SetIdentifier(string.Empty); base.PreUnload(); } protected override void PreEntitySpawn() { base.PreEntitySpawn(); UpdateIdentifier(); UpdateDirection(); } protected override void PostEntitySpawn() { base.PostEntitySpawn(); if (_cachedIdentifier != null) { var computerStationList = GetNearbyStaticComputerStations(); if (computerStationList != null) { foreach (var computerStation in computerStationList) { computerStation.ForceAddBookmark(_cachedIdentifier); } } } } public override void OnEntityKilled(BaseEntity entity) { base.OnEntityKilled(entity); Plugin.TrackStart(); if (_cachedIdentifier != null) { var computerStationList = GetNearbyStaticComputerStations(); if (computerStationList != null) { foreach (var computerStation in computerStationList) computerStation.controlBookmarks.Remove(_cachedIdentifier); } } Plugin.TrackEnd(); } public override void HandleChanges() { base.HandleChanges(); UpdateIdentifier(); UpdateDirection(); } public void UpdateIdentifier() { if (_savedIdentifier == null) { SetIdentifier(string.Empty); return; } var newIdentifier = $"{_savedIdentifier}{_idSuffix}"; if (newIdentifier == _cachedIdentifier) return; if (RemoteControlEntity.IDInUse(newIdentifier)) { LogWarning($"CCTV ID in use: {newIdentifier}"); return; } SetIdentifier(newIdentifier); if (Entity.IsFullySpawned()) { Entity.SendNetworkUpdate(); var computerStationList = GetNearbyStaticComputerStations(); if (computerStationList != null) { foreach (var computerStation in computerStationList) { if (_cachedIdentifier != null) computerStation.controlBookmarks.Remove(_cachedIdentifier); computerStation.ForceAddBookmark(newIdentifier); } } } _cachedIdentifier = newIdentifier; } public void UpdateDirection() { var cctvInfo = EntityData.CCTV; if (cctvInfo == null) return; var cctv = Entity as CCTV_RC; cctv.pitchAmount = cctvInfo.Pitch; cctv.yawAmount = cctvInfo.Yaw; cctv.pitchAmount = Mathf.Clamp(cctv.pitchAmount, cctv.pitchClamp.x, cctv.pitchClamp.y); cctv.yawAmount = Mathf.Clamp(cctv.yawAmount, cctv.yawClamp.x, cctv.yawClamp.y); cctv.pitch.transform.localRotation = Quaternion.Euler(cctv.pitchAmount, 0f, 0f); cctv.yaw.transform.localRotation = Quaternion.Euler(0f, cctv.yawAmount, 0f); if (Entity.IsFullySpawned()) { Entity.SendNetworkUpdate(); } } public string GetIdentifier() => (Entity as CCTV_RC).rcIdentifier; private void SetIdentifier(string id) => (Entity as CCTV_RC).rcIdentifier = id; private List GetNearbyStaticComputerStations() { var dynamicMonument = Monument as DynamicMonument; if (dynamicMonument != null && dynamicMonument.RootEntity == Entity.GetParentEntity()) { var cargoComputerStationList = new List(); foreach (var child in dynamicMonument.RootEntity.children) { var computerStation = child as ComputerStation; if (computerStation != null && computerStation.isStatic) cargoComputerStationList.Add(computerStation); } return cargoComputerStationList; } var entityList = new List(); Vis.Entities(Entity.transform.position, 100, entityList, Rust.Layers.Mask.Deployed, QueryTriggerInteraction.Ignore); if (entityList.Count == 0) return null; var computerStationList = new List(); foreach (var entity in entityList) { var computerStation = entity as ComputerStation; if (computerStation != null && !computerStation.IsDestroyed && computerStation.isStatic) computerStationList.Add(computerStation); } return computerStationList; } } private class CCTVController : EntityController { private int _nextId = 1; public CCTVController(ProfileController profileController, EntityData data) : base(profileController, data) {} public override BaseAdapter CreateAdapter(BaseMonument monument) => new CCTVEntityAdapter(this, EntityData, monument, _nextId++); } #endregion #region Entity Listeners private abstract class AdapterListenerBase { public virtual void Init() {} public virtual void OnServerInitialized() {} public abstract bool InterestedInAdapter(BaseAdapter adapter); public abstract void OnAdapterSpawned(BaseAdapter adapter); public abstract void OnAdapterKilled(BaseAdapter adapter); protected static bool IsEntityAdapter(BaseAdapter adapter) { var entityData = adapter.Data as EntityData; if (entityData == null) return false; return FindBaseEntityForPrefab(entityData.PrefabName) is T; } } private abstract class DynamicHookListener : AdapterListenerBase { private MonumentAddons _plugin; protected string[] _dynamicHookNames; private HashSet _adapters = new HashSet(); protected DynamicHookListener(MonumentAddons plugin) { _plugin = plugin; } public override void Init() { UnsubscribeHooks(); } public override void OnAdapterSpawned(BaseAdapter adapter) { _adapters.Add(adapter); if (_adapters.Count == 1) { SubscribeHooks(); } } public override void OnAdapterKilled(BaseAdapter adapter) { _adapters.Remove(adapter); if (_adapters.Count == 0) { UnsubscribeHooks(); } } private void SubscribeHooks() { if (_dynamicHookNames == null || !_plugin._isLoaded) return; foreach (var hookName in _dynamicHookNames) { _plugin.Subscribe(hookName); } } private void UnsubscribeHooks() { if (_dynamicHookNames == null || !_plugin._isLoaded) return; foreach (var hookName in _dynamicHookNames) { _plugin.Unsubscribe(hookName); } } } private class SignEntityListener : DynamicHookListener { public SignEntityListener(MonumentAddons plugin) : base(plugin) { _dynamicHookNames = new[] { nameof(CanUpdateSign), nameof(OnSignUpdated), nameof(OnImagePost), }; } public override bool InterestedInAdapter(BaseAdapter adapter) { return IsEntityAdapter(adapter); } } private class BuildingBlockEntityListener : DynamicHookListener { public BuildingBlockEntityListener(MonumentAddons plugin) : base(plugin) { _dynamicHookNames = new[] { nameof(CanChangeGrade), }; } public override bool InterestedInAdapter(BaseAdapter adapter) { return IsEntityAdapter(adapter); } } private class SprayDecalListener : DynamicHookListener { public SprayDecalListener(MonumentAddons plugin) : base(plugin) { _dynamicHookNames = new[] { nameof(OnSprayRemove), }; } public override bool InterestedInAdapter(BaseAdapter adapter) { return IsEntityAdapter(adapter); } } private class AdapterListenerManager { private AdapterListenerBase[] _listeners; public AdapterListenerManager(MonumentAddons plugin) { _listeners = new AdapterListenerBase[] { new SignEntityListener(plugin), new BuildingBlockEntityListener(plugin), new SprayDecalListener(plugin), }; } public void Init() { foreach (var listener in _listeners) { listener.Init(); } } public void OnServerInitialized() { foreach (var listener in _listeners) { listener.OnServerInitialized(); } } public void OnAdapterSpawned(BaseAdapter entityAdapter) { foreach (var listener in _listeners) { if (listener.InterestedInAdapter(entityAdapter)) { listener.OnAdapterSpawned(entityAdapter); } } } public void OnAdapterKilled(BaseAdapter entityAdapter) { foreach (var listener in _listeners) { if (listener.InterestedInAdapter(entityAdapter)) { listener.OnAdapterKilled(entityAdapter); } } } } #endregion #region SpawnGroup Adapter/Controller private class PuzzleResetHandler : FacepunchBehaviour { public static PuzzleResetHandler Create(EntityAdapter adapter, PuzzleReset puzzleReset) { var gameObject = new GameObject(); gameObject.transform.SetParent(puzzleReset.transform); var component = gameObject.AddComponent(); component._adapter = adapter; puzzleReset.resetObjects = new[] { gameObject }; return component; } private EntityAdapter _adapter; // Called by Rust via Unity SendMessage. private void OnPuzzleReset() { _adapter.HandlePuzzleReset(); } public void Destroy() => Destroy(gameObject); } private class SpawnedVehicleComponent : FacepunchBehaviour { public static void AddToVehicle(MonumentAddons plugin, GameObject gameObject) { var newComponent = gameObject.AddComponent(); newComponent._plugin = plugin; } private const float MaxDistanceSquared = 1; private MonumentAddons _plugin; private Vector3 _originalPosition; private Transform _transform; public void Awake() { _transform = transform; _originalPosition = _transform.position; InvokeRandomized(CheckPositionTracked, 10, 10, 1); } public void CheckPositionTracked() { _plugin.TrackStart(); CheckPosition(); _plugin.TrackEnd(); } private void CheckPosition() { if ((_transform.position - _originalPosition).sqrMagnitude < MaxDistanceSquared) return; // Vehicle has moved from its spawn point, so unregister it and re-enable saving. var vehicle = GetComponent(); if (vehicle != null && !vehicle.IsDestroyed) { EnableSavingRecursive(vehicle, true); var workcart = vehicle as TrainEngine; if (workcart != null && !workcart.IsInvoking(workcart.DecayTick)) { workcart.InvokeRandomized(workcart.DecayTick, UnityEngine.Random.Range(20f, 40f), workcart.decayTickSpacing, workcart.decayTickSpacing * 0.1f); } } Destroy(GetComponent()); Destroy(this); } } private class CustomSpawnPoint : BaseSpawnPoint { private const int TrainCarLayerMask = Rust.Layers.Mask.AI | Rust.Layers.Mask.Vehicle_World | Rust.Layers.Mask.Player_Server | Rust.Layers.Mask.Construction; // When CheckSpace is enabled, override the layer mask for certain entity prefabs. private static readonly Dictionary CustomBoundsCheckMask = new Dictionary { ["assets/content/vehicles/trains/locomotive/locomotive.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/sedan_a/sedanrail.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/workcart/workcart.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/workcart/workcart_aboveground.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/workcart/workcart_aboveground2.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/wagons/trainwagona.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/wagons/trainwagonb.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/wagons/trainwagonc.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/wagons/trainwagonunloadablefuel.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/wagons/trainwagonunloadableloot.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/wagons/trainwagonunloadable.entity.prefab"] = TrainCarLayerMask, ["assets/content/vehicles/trains/caboose/traincaboose.entity.prefab"] = TrainCarLayerMask, }; public SpawnPointAdapter Adapter { get; private set; } private SpawnPointData _spawnPointData; private Transform _transform; private BaseEntity _parentEntity; private List _instances = new List(); public void Init(SpawnPointAdapter adapter, SpawnPointData spawnPointData) { Adapter = adapter; _spawnPointData = spawnPointData; } public void PreUnload() { KillSpawnedInstances(); gameObject.SetActive(false); } private void Awake() { _transform = transform; _parentEntity = _transform.parent?.ToBaseEntity(); } public override void GetLocation(out Vector3 position, out Quaternion rotation) { position = _transform.position; rotation = _transform.rotation; if (_spawnPointData.RandomRadius > 0) { Vector2 vector = UnityEngine.Random.insideUnitCircle * _spawnPointData.RandomRadius; position += new Vector3(vector.x, 0f, vector.y); } if (_spawnPointData.RandomRotation) { rotation *= Quaternion.Euler(0f, UnityEngine.Random.Range(0f, 360f), 0f); } if (_spawnPointData.SnapToGround) { DropToGround(ref position, ref rotation); } } public override void ObjectSpawned(SpawnPointInstance instance) { _instances.Add(instance); var entity = instance.GetComponent(); if (!entity.HasParent() && _parentEntity != null && !_parentEntity.IsDestroyed) { entity.SetParent(_parentEntity, worldPositionStays: true); } if (IsVehicle(entity)) { SpawnedVehicleComponent.AddToVehicle(Adapter.Plugin, instance.gameObject); entity.Invoke(() => DisableVehicleDecay(entity), 5); } var hackableCrate = entity as HackableLockedCrate; if ((object)hackableCrate != null && hackableCrate.shouldDecay) { hackableCrate.shouldDecay = false; hackableCrate.CancelInvoke(hackableCrate.DelayedDestroy); } } public override void ObjectRetired(SpawnPointInstance instance) { _instances.Remove(instance); Adapter.SpawnGroupAdapter.SpawnGroup.HandleObjectRetired(); } public override bool IsAvailableTo(GameObjectRef prefabRef) { if (!base.IsAvailableTo(prefabRef)) return false; if (_spawnPointData.Exclusive && _instances.Count > 0) return false; if (_spawnPointData.CheckSpace) { int customBoundsCheckMask; if (CustomBoundsCheckMask.TryGetValue(prefabRef.resourcePath, out customBoundsCheckMask)) return SpawnHandler.CheckBounds(prefabRef.Get(), _transform.position, _transform.rotation, Vector3.one, customBoundsCheckMask); return SingletonComponent.Instance.CheckBounds(prefabRef.Get(), _transform.position, _transform.rotation, Vector3.one); } return true; } public override bool HasPlayersIntersecting() { var detectionRadius = _spawnPointData.PlayerDetectionRadius > 0 ? _spawnPointData.PlayerDetectionRadius : _spawnPointData.RandomRadius > 0 ? _spawnPointData.RandomRadius + 1 : 0; return detectionRadius > 0 ? BaseNetworkable.HasCloseConnections(transform.position, detectionRadius) : base.HasPlayersIntersecting(); } public void OnDestroy() { KillSpawnedInstances(); Adapter.OnSpawnPointKilled(); } public void KillSpawnedInstances(string prefabName = null) { for (var i = _instances.Count - 1; i >= 0; i--) { var entity = _instances[i].GetComponent(); if ((prefabName == null || entity.PrefabName == prefabName) && entity != null && !entity.IsDestroyed) { entity.Kill(); } } } private bool IsVehicle(BaseEntity entity) { return entity is HotAirBalloon || entity is BaseVehicle; } private void DisableVehicleDecay(BaseEntity vehicle) { var kayak = vehicle as Kayak; if (kayak != null) { kayak.timeSinceLastUsed = float.MinValue; return; } var boat = vehicle as MotorRowboat; if (boat != null) { boat.timeSinceLastUsedFuel = float.MinValue; return; } var snowmobile = vehicle as Snowmobile; if (snowmobile != null) { snowmobile.timeSinceLastUsed = float.MinValue; return; } var sub = vehicle as BaseSubmarine; if (sub != null) { sub.timeSinceLastUsed = float.MinValue; return; } var hab = vehicle as HotAirBalloon; if (hab != null) { hab.lastBlastTime = float.MaxValue; return; } var heli = vehicle as PlayerHelicopter; if (heli != null) { heli.lastEngineOnTime = float.MaxValue; return; } var car = vehicle as ModularCar; if (car != null) { car.lastEngineOnTime = float.MaxValue; return; } var horse = vehicle as RidableHorse; if (horse != null) { horse.lastInputTime = float.MaxValue; return; } var workcart = vehicle as TrainEngine; if (workcart != null) { workcart.CancelInvoke(workcart.DecayTick); return; } } public void MoveSpawnedInstances() { for (var i = _instances.Count - 1; i >= 0; i--) { var entity = _instances[i].GetComponent(); if (entity != null && !entity.IsDestroyed) { Vector3 position; Quaternion rotation; Adapter.SpawnPoint.GetLocation(out position, out rotation); if (position != entity.transform.position || rotation != entity.transform.rotation) { if (IsVehicle(entity)) { var spawnedVehicleComponent = entity.GetComponent(); if (spawnedVehicleComponent != null) { spawnedVehicleComponent.CancelInvoke(spawnedVehicleComponent.CheckPositionTracked); } Adapter.Plugin.NextTick(() => spawnedVehicleComponent.Awake()); } entity.transform.SetPositionAndRotation(position, rotation); BroadcastEntityTransformChange(entity); } } } } } private class CustomSpawnGroup : SpawnGroup { private static AIInformationZone FindVirtualInfoZone(Vector3 position) { foreach (var zone in AIInformationZone.zones) { if (zone.Virtual && zone.PointInside(position)) return zone; } return null; } public SpawnGroupAdapter SpawnGroupAdapter { get; private set; } private AIInformationZone _cachedInfoZone; private bool _didLookForInfoZone; private SpawnGroupData SpawnGroupData => SpawnGroupAdapter.SpawnGroupData; public void Init(SpawnGroupAdapter spawnGroupAdapter) { SpawnGroupAdapter = spawnGroupAdapter; } public void UpdateSpawnClock() { if (!WantsTimedSpawn()) { spawnClock.events.Clear(); return; } if (spawnClock.events.Count == 0) { spawnClock.Add(GetSpawnDelta(), GetSpawnVariance(), Spawn); } else { var clockEvent = spawnClock.events[0]; var timeUntilSpawn = clockEvent.time - UnityEngine.Time.time; if (timeUntilSpawn > SpawnGroupData.RespawnDelayMax) { clockEvent.time = UnityEngine.Time.time + SpawnGroupData.RespawnDelayMax; spawnClock.events[0] = clockEvent; } } } public void HandleObjectRetired() { // Add one to current population because it was just decremented. if (SpawnGroupData.PauseScheduleWhileFull && currentPopulation + 1 >= maxPopulation) { ResetSpawnClock(); } } // Called by Rust via Unity SendMessage, when associating with a vanilla puzzle reset. public void OnPuzzleReset() { Clear(); DelayedSpawn(); } protected override void PostSpawnProcess(BaseEntity entity, BaseSpawnPoint spawnPoint) { base.PostSpawnProcess(entity, spawnPoint); EnableSavingRecursive(entity, false); var npcPlayer = entity as NPCPlayer; if (npcPlayer != null) { var virtualInfoZone = GetVirtualInfoZone(); if (virtualInfoZone != null) { npcPlayer.VirtualInfoZone = virtualInfoZone; } var humanNpc = npcPlayer as global::HumanNPC; if (humanNpc != null) { virtualInfoZone?.RegisterSleepableEntity(humanNpc.Brain); var agent = npcPlayer.NavAgent; agent.agentTypeID = -1372625422; agent.areaMask = 1; agent.autoTraverseOffMeshLink = true; agent.autoRepath = true; var brain = humanNpc.Brain; humanNpc.Invoke(() => { var navigator = brain.Navigator; if (navigator == null) return; navigator.DefaultArea = "Walkable"; navigator.Init(humanNpc, agent); navigator.PlaceOnNavMesh(); }, 0); } } } private AIInformationZone GetVirtualInfoZone() { if (!_didLookForInfoZone) { _cachedInfoZone = FindVirtualInfoZone(transform.position); _didLookForInfoZone = true; } return _cachedInfoZone; } private void ResetSpawnClock() { if (spawnClock.events.Count == 0) return; var clockEvent = spawnClock.events[0]; clockEvent.delta = GetSpawnDelta(); var variance = GetSpawnVariance(); clockEvent.variance = variance; clockEvent.time = UnityEngine.Time.time + clockEvent.delta + UnityEngine.Random.Range(-variance, variance); spawnClock.events[0] = clockEvent; } private void OnDestroy() { SingletonComponent.Instance.SpawnGroups.Remove(this); SpawnGroupAdapter.OnSpawnGroupKilled(); } } private class SpawnPointAdapter : TransformAdapter { public SpawnPointData SpawnPointData { get; } public SpawnGroupAdapter SpawnGroupAdapter { get; } public CustomSpawnPoint SpawnPoint { get; private set; } public override Vector3 Position => _transform.position; public override Quaternion Rotation => _transform.rotation; private Transform _transform; public SpawnPointAdapter(SpawnPointData spawnPointData, SpawnGroupAdapter spawnGroupAdapter, BaseController controller, BaseMonument monument) : base(spawnPointData, controller, monument) { SpawnPointData = spawnPointData; SpawnGroupAdapter = spawnGroupAdapter; } public override void Spawn() { var gameObject = new GameObject(); _transform = gameObject.transform; _transform.SetPositionAndRotation(IntendedPosition, IntendedRotation); var dynamicMonument = Monument as DynamicMonument; if (dynamicMonument != null) { _transform.SetParent(dynamicMonument.RootEntity.transform, worldPositionStays: true); } SpawnPoint = gameObject.AddComponent(); SpawnPoint.Init(this, SpawnPointData); } public override void Kill() { UnityEngine.Object.Destroy(SpawnPoint?.gameObject); } public override void PreUnload() { SpawnPoint.PreUnload(); } public void OnSpawnPointKilled() { SpawnGroupAdapter.OnSpawnPointAdapterKilled(this); } public void KillSpawnedInstances(string prefabName) { SpawnPoint.KillSpawnedInstances(prefabName); } public void UpdatePosition() { if (!IsAtIntendedPosition) { _transform.SetPositionAndRotation(IntendedPosition, IntendedRotation); SpawnPoint.MoveSpawnedInstances(); } } } private class SpawnGroupAdapter : BaseAdapter { public SpawnGroupData SpawnGroupData { get; } public List SpawnPointAdapters { get; } = new List(); public CustomSpawnGroup SpawnGroup { get; private set; } public PuzzleReset AssociatedPuzzleReset { get; private set; } public SpawnGroupAdapter(SpawnGroupData spawnGroupData, BaseController controller, BaseMonument monument) : base(spawnGroupData, controller, monument) { SpawnGroupData = spawnGroupData; } public override void Spawn() { var spawnGroupGameObject = new GameObject(); spawnGroupGameObject.transform.SetPositionAndRotation(Monument.Position, Monument.Rotation); spawnGroupGameObject.SetActive(false); // Configure the spawn group and create spawn points before enabling the group. // This allows the vanilla Awake() method to perform initial spawn and schedule spawns. SpawnGroup = spawnGroupGameObject.AddComponent(); SpawnGroup.Init(this); if (SpawnGroup.prefabs == null) { SpawnGroup.prefabs = new List(); } UpdateProperties(); UpdatePrefabEntries(); // This will call Awake() on the CustomSpawnGroup component. spawnGroupGameObject.SetActive(true); foreach (var spawnPointData in SpawnGroupData.SpawnPoints) { CreateSpawnPoint(spawnPointData); } UpdateSpawnPointReferences(); UpdatePuzzleResetAssociation(); if (SpawnGroupData.InitialSpawn) { SpawnGroup.Spawn(); } } public override void Kill() { UnityEngine.Object.Destroy(SpawnGroup?.gameObject); } public override void PreUnload() { foreach (var adapter in SpawnPointAdapters) { adapter.PreUnload(); } } public void OnSpawnPointAdapterKilled(SpawnPointAdapter spawnPointAdapter) { SpawnPointAdapters.Remove(spawnPointAdapter); if (SpawnGroup != null) { UpdateSpawnPointReferences(); } if (SpawnPointAdapters.Count == 0) { Controller.OnAdapterKilled(this); } } public void OnSpawnGroupKilled() { if (AssociatedPuzzleReset != null) { UnregisterWithPuzzleReset(AssociatedPuzzleReset); } foreach (var spawnPointAdapter in SpawnPointAdapters.ToList()) { spawnPointAdapter.Kill(); } } private Vector3 GetMidpoint() { var min = Vector3.positiveInfinity; var max = Vector3.negativeInfinity; foreach (var spawnPointAdapter in SpawnPointAdapters) { var position = spawnPointAdapter.Position; min = Vector3.Min(min, position); max = Vector3.Max(max, position); } return (min + max) / 2f; } private PuzzleReset FindClosestVanillaPuzzleReset(float maxDistance = 60) { return EntityUtils.GetClosestNearbyComponent(GetMidpoint(), maxDistance, Rust.Layers.Mask.World, puzzleReset => { var entity = puzzleReset.GetComponent(); return entity != null && !Plugin._entityTracker.IsMonumentEntity(entity); }); } private void RegisterWithPuzzleReset(PuzzleReset puzzleReset) { if (IsRegisteredWithPuzzleReset(puzzleReset)) return; if (puzzleReset.resetObjects == null) { puzzleReset.resetObjects = new[] { SpawnGroup.gameObject }; return; } var originalLength = puzzleReset.resetObjects.Length; Array.Resize(ref puzzleReset.resetObjects, originalLength + 1); puzzleReset.resetObjects[originalLength] = SpawnGroup.gameObject; } private void UnregisterWithPuzzleReset(PuzzleReset puzzleReset) { if (!IsRegisteredWithPuzzleReset(puzzleReset)) return; puzzleReset.resetObjects = puzzleReset.resetObjects.Where(obj => obj != SpawnGroup.gameObject).ToArray(); } private bool IsRegisteredWithPuzzleReset(PuzzleReset puzzleReset) { return puzzleReset.resetObjects?.Contains(SpawnGroup.gameObject) == true; } private void UpdateProperties() { SpawnGroup.preventDuplicates = SpawnGroupData.PreventDuplicates; SpawnGroup.maxPopulation = SpawnGroupData.MaxPopulation; SpawnGroup.numToSpawnPerTickMin = SpawnGroupData.SpawnPerTickMin; SpawnGroup.numToSpawnPerTickMax = SpawnGroupData.SpawnPerTickMax; var respawnDelayMin = SpawnGroupData.RespawnDelayMin; var respawnDelayMax = Mathf.Max(respawnDelayMin, SpawnGroupData.RespawnDelayMax > 0 ? SpawnGroupData.RespawnDelayMax : float.PositiveInfinity); var respawnDelayMinChanged = SpawnGroup.respawnDelayMin != respawnDelayMin; var respawnDelayMaxChanged = SpawnGroup.respawnDelayMax != respawnDelayMax; SpawnGroup.respawnDelayMin = respawnDelayMin; SpawnGroup.respawnDelayMax = respawnDelayMax; if (SpawnGroup.gameObject.activeSelf && (respawnDelayMinChanged || respawnDelayMaxChanged)) { SpawnGroup.UpdateSpawnClock(); } } private void UpdatePrefabEntries() { if (SpawnGroup.prefabs.Count == SpawnGroupData.Prefabs.Count) { for (var i = 0; i < SpawnGroup.prefabs.Count; i++) { SpawnGroup.prefabs[i].weight = SpawnGroupData.Prefabs[i].Weight; } return; } SpawnGroup.prefabs.Clear(); foreach (var prefabEntry in SpawnGroupData.Prefabs) { string guid; if (!GameManifest.pathToGuid.TryGetValue(prefabEntry.PrefabName, out guid)) continue; SpawnGroup.prefabs.Add(new SpawnGroup.SpawnEntry { prefab = new GameObjectRef { guid = guid }, weight = prefabEntry.Weight, }); } } private void UpdateSpawnPointReferences() { if (!SpawnGroup.gameObject.activeSelf || SpawnGroup.spawnPoints.Length == SpawnPointAdapters.Count) return; SpawnGroup.spawnPoints = new BaseSpawnPoint[SpawnPointAdapters.Count]; for (var i = 0; i < SpawnPointAdapters.Count; i++) { SpawnGroup.spawnPoints[i] = SpawnPointAdapters[i].SpawnPoint; } } private void UpdateSpawnPointPositions() { foreach (var adapter in SpawnPointAdapters) { if (!adapter.IsAtIntendedPosition) { adapter.UpdatePosition(); } } } private void UpdatePuzzleResetAssociation() { if (!SpawnGroup.gameObject.activeSelf) return; if (SpawnGroupData.RespawnWhenNearestPuzzleResets) { var closestPuzzleReset = FindClosestVanillaPuzzleReset(); // Re-evaluate current association since the midpoint and other circumstances can change. if (AssociatedPuzzleReset != null && AssociatedPuzzleReset != closestPuzzleReset) { UnregisterWithPuzzleReset(AssociatedPuzzleReset); } AssociatedPuzzleReset = closestPuzzleReset; if (closestPuzzleReset != null) { RegisterWithPuzzleReset(closestPuzzleReset); } } else if (AssociatedPuzzleReset != null) { UnregisterWithPuzzleReset(AssociatedPuzzleReset); } } public void UpdateSpawnGroup() { UpdateProperties(); UpdatePrefabEntries(); UpdateSpawnPointReferences(); UpdateSpawnPointPositions(); UpdatePuzzleResetAssociation(); } public void SpawnTick() { SpawnGroup.Spawn(); } public void KillSpawnedInstances(string prefabName) { foreach (var spawnPointAdapter in SpawnPointAdapters) { spawnPointAdapter.KillSpawnedInstances(prefabName); } } public void CreateSpawnPoint(SpawnPointData spawnPointData) { var spawnPointAdapter = new SpawnPointAdapter(spawnPointData, this, Controller, Monument); SpawnPointAdapters.Add(spawnPointAdapter); spawnPointAdapter.Spawn(); if (SpawnGroup.gameObject.activeSelf) { UpdateSpawnPointReferences(); } } public void KillSpawnPoint(SpawnPointData spawnPointData) { FindSpawnPoint(spawnPointData)?.Kill(); } private SpawnPointAdapter FindSpawnPoint(SpawnPointData spawnPointData) { foreach (var spawnPointAdapter in SpawnPointAdapters) { if (spawnPointAdapter.SpawnPointData == spawnPointData) return spawnPointAdapter; } return null; } } private class SpawnGroupController : BaseController { public SpawnGroupData SpawnGroupData { get; } public IEnumerable SpawnGroupAdapters { get; } public SpawnGroupController(ProfileController profileController, SpawnGroupData spawnGroupData) : base(profileController, spawnGroupData) { SpawnGroupData = spawnGroupData; SpawnGroupAdapters = Adapters.Cast(); } public override BaseAdapter CreateAdapter(BaseMonument monument) => new SpawnGroupAdapter(SpawnGroupData, this, monument); public override Coroutine Kill(BaseData data) { if (data == Data) return base.Kill(data); var spawnPointData = data as SpawnPointData; if (spawnPointData != null) { KillSpawnPoint(spawnPointData); return null; } LogError($"{nameof(SpawnGroupController)}.{nameof(Kill)} not implemented for type {data.GetType()}. Killing {nameof(SpawnGroupController)}."); return null; } public void CreateSpawnPoint(SpawnPointData spawnPointData) { foreach (var spawnGroupAdapter in SpawnGroupAdapters) spawnGroupAdapter.CreateSpawnPoint(spawnPointData); } public void KillSpawnPoint(SpawnPointData spawnPointData) { foreach (var spawnGroupAdapter in SpawnGroupAdapters) spawnGroupAdapter.KillSpawnPoint(spawnPointData); } public void UpdateSpawnGroups() { foreach (var spawnGroupAdapter in SpawnGroupAdapters) spawnGroupAdapter.UpdateSpawnGroup(); } public void StartSpawnRoutine() => ProfileController.StartCoroutine(SpawnTickRoutine()); public void StartKillSpawnedInstancesRoutine(string prefabName) => ProfileController.StartCoroutine(KillSpawnedInstancesRoutine(prefabName)); public void StartRespawnRoutine() => ProfileController.StartCoroutine(RespawnRoutine()); private IEnumerator SpawnTickRoutine() { foreach (var spawnGroupAdapter in SpawnGroupAdapters.ToList()) { spawnGroupAdapter.SpawnTick(); yield return null; } } private IEnumerator KillSpawnedInstancesRoutine(string prefabName = null) { foreach (var spawnGroupAdapter in SpawnGroupAdapters.ToList()) { spawnGroupAdapter.KillSpawnedInstances(prefabName); yield return null; } } private IEnumerator RespawnRoutine() { yield return KillSpawnedInstancesRoutine(); yield return SpawnTickRoutine(); } } #endregion #region Paste Adapter/Controller private class PasteAdapter : TransformAdapter, IEntityAdapter { private const float CopyPasteMagicRotationNumber = 57.2958f; public PasteData PasteData { get; } public override Vector3 Position => _position; public override Quaternion Rotation => _rotation; private Vector3 _position; private Quaternion _rotation; private bool _isWorking; private Action _cancelPaste; private List _pastedEntities = new List(); public PasteAdapter(PasteData pasteData, BaseController controller, BaseMonument monument) : base(pasteData, controller, monument) { PasteData = pasteData; } public override void Spawn() { // Simply cache the position and rotation since they are not expected to change. // Parenting pastes to dynamic monuments is not currently supported. _position = IntendedPosition; _rotation = IntendedRotation; _cancelPaste = PasteUtils.PasteWithCancelCallback(Plugin.CopyPaste, PasteData, _position, _rotation.eulerAngles.y / CopyPasteMagicRotationNumber, OnEntityPasted, OnPasteComplete); if (_cancelPaste != null) { _isWorking = true; WaitInstruction = new WaitWhile(() => _isWorking); } } public override void Kill() { _cancelPaste?.Invoke(); _isWorking = true; CoroutineManager.StartGlobalCoroutine(KillRoutine()); WaitInstruction = WaitWhileWithTimeout(() => _isWorking, 5); } public void OnEntityKilled(BaseEntity entity) { _pastedEntities.Remove(entity); if (_pastedEntities.Count == 0) { // Cancel the paste in case it is still in-progress, to avoid an orphaned adapter. _cancelPaste?.Invoke(); Controller.OnAdapterKilled(this); } } private IEnumerator KillRoutine() { var pastedEntities = _pastedEntities.ToList(); // Remove the entities in reverse order. Hopefully this makes the top of the building get removed first. for (var i = pastedEntities.Count - 1; i >= 0; i--) { var entity = pastedEntities[i]; if (entity != null && !entity.IsDestroyed) { Plugin.TrackStart(); entity.Kill(); Plugin.TrackEnd(); yield return null; } } _isWorking = false; } private void OnEntityPasted(BaseEntity entity) { EntitySetupUtils.PreSpawnShared(entity); EntitySetupUtils.PostSpawnShared(Plugin, entity, enableSaving: false); MonumentEntityComponent.AddToEntity(Plugin._entityTracker, entity, this, Monument); _pastedEntities.Add(entity); } private void OnPasteComplete() { _isWorking = false; } } private class PasteController : BaseController { public PasteData PasteData { get; } public PasteController(ProfileController profileController, PasteData pasteData) : base(profileController, pasteData) { PasteData = pasteData; } public override BaseAdapter CreateAdapter(BaseMonument monument) => new PasteAdapter(PasteData, this, monument); public override IEnumerator SpawnAtMonumentsRoutine(IEnumerable monumentList) { if (!PasteUtils.IsCopyPasteCompatible(Plugin.CopyPaste)) { LogError($"Unable to paste \"{PasteData.Filename}\" for profile \"{Profile.Name}\" because CopyPaste is not loaded or its version is incompatible."); yield break; } if (!PasteUtils.DoesPasteExist(PasteData.Filename)) { LogError($"Unable to paste \"{PasteData.Filename}\" for profile \"{Profile.Name}\" because the file does not exist."); yield break; } yield return base.SpawnAtMonumentsRoutine(monumentList); } } #endregion #region Custom Addon Adapter/Controller private class CustomAddonDefinition { public static CustomAddonDefinition FromDictionary(string addonName, Plugin plugin, Dictionary addonSpec) { var addonDefinition = new CustomAddonDefinition { AddonName = addonName, OwnerPlugin = plugin, }; object spawnCallback, killCallback, updateCallback, addDataCallback; if (addonSpec.TryGetValue("Spawn", out spawnCallback)) { addonDefinition.Spawn = spawnCallback as CustomSpawnCallback; } if (addonSpec.TryGetValue("Kill", out killCallback)) { addonDefinition.Kill = killCallback as CustomKillCallback; } if (addonSpec.TryGetValue("Update", out updateCallback)) { addonDefinition.Update = updateCallback as CustomUpdateCallback; } if (addonSpec.TryGetValue("AddDisplayInfo", out addDataCallback)) { addonDefinition.AddDisplayInfo = addDataCallback as CustomAddDisplayInfoCallback; } return addonDefinition; } public string AddonName; public Plugin OwnerPlugin; public CustomSpawnCallback Spawn; public CustomKillCallback Kill; public CustomUpdateCallback Update; public CustomAddDisplayInfoCallback AddDisplayInfo; public List AdapterUsers = new List(); public Dictionary ToApiResult(ProfileStore profileStore) { return new Dictionary { ["SetData"] = new CustomSetDataCallback( (component, data) => { if (Update == null) { LogError($"Unable to set data for custom addon \"{AddonName}\" due to missing Update method."); return; } var matchingAdapter = AdapterUsers.FirstOrDefault(adapter => adapter.Component == component); if (matchingAdapter == null) { LogError($"Unable to set data for custom addon \"{AddonName}\" because it has no spawned instances."); return; } var controller = matchingAdapter.Controller as CustomAddonController; controller.CustomAddonData.SetData(data); profileStore.Save(controller.Profile); foreach (var adapter in controller.Adapters) { Update((adapter as CustomAddonAdapter).Component, controller.CustomAddonData.GetSerializedData()); } } ), }; } } private class CustomAddonManager { private MonumentAddons _plugin; private Dictionary _customAddonsByName = new Dictionary(); private Dictionary> _customAddonsByPlugin = new Dictionary>(); public IEnumerable GetAllAddons() => _customAddonsByName.Values; public CustomAddonManager(MonumentAddons plugin) { _plugin = plugin; } public bool IsRegistered(string addonName, out Plugin otherPlugin) { otherPlugin = null; CustomAddonDefinition existingAddon; if (_customAddonsByName.TryGetValue(addonName, out existingAddon)) { otherPlugin = existingAddon.OwnerPlugin; return true; } return false; } public void RegisterAddon(CustomAddonDefinition addonDefinition) { _customAddonsByName[addonDefinition.AddonName] = addonDefinition; var addonsForPlugin = GetAddonsForPlugin(addonDefinition.OwnerPlugin); if (addonsForPlugin == null) { addonsForPlugin = new List(); _customAddonsByPlugin[addonDefinition.OwnerPlugin.Name] = addonsForPlugin; } addonsForPlugin.Add(addonDefinition); if (_plugin._serverInitialized) { foreach (var profileController in _plugin._profileManager.GetEnabledProfileControllers()) { foreach (var monumentEntry in profileController.Profile.MonumentDataMap) { var monumentName = monumentEntry.Key; var monumentData = monumentEntry.Value; foreach (var customAddonData in monumentData.CustomAddons) { if (customAddonData.AddonName == addonDefinition.AddonName) { profileController.SpawnNewData(customAddonData, _plugin.GetMonumentsByAliasOrShortName(monumentName)); } } } } } } public void UnregisterAllForPlugin(Plugin plugin) { if (_customAddonsByName.Count == 0) return; var addonsForPlugin = GetAddonsForPlugin(plugin); if (addonsForPlugin == null) return; var controllerList = new HashSet(); foreach (var addonDefinition in addonsForPlugin) { foreach (var adapter in addonDefinition.AdapterUsers) { controllerList.Add(adapter.Controller as CustomAddonController); } _customAddonsByName.Remove(addonDefinition.AddonName); } foreach (var controller in controllerList) { controller.PreUnload(); } CoroutineManager.StartGlobalCoroutine(DestroyControllersRoutine(controllerList)); _customAddonsByPlugin.Remove(plugin.Name); } public CustomAddonDefinition GetAddon(string addonName) { CustomAddonDefinition addonDefinition; return _customAddonsByName.TryGetValue(addonName, out addonDefinition) ? addonDefinition : null; } private List GetAddonsForPlugin(Plugin plugin) { List addonsForPlugin; return _customAddonsByPlugin.TryGetValue(plugin.Name, out addonsForPlugin) ? addonsForPlugin : null; } private IEnumerator DestroyControllersRoutine(ICollection controllerList) { foreach (var controller in controllerList) { yield return controller.KillRoutine(); } } } private class CustomAddonAdapter : TransformAdapter, IEntityAdapter { private class CustomAddonComponent : MonoBehaviour { public CustomAddonAdapter Adapter; private void OnDestroy() => Adapter.OnAddonDestroyed(); } public CustomAddonData CustomAddonData { get; } public CustomAddonDefinition AddonDefinition { get; } public UnityEngine.Component Component { get; private set; } public override Vector3 Position => Component.transform.position; public override Quaternion Rotation => Component.transform.rotation; private bool _wasKilled; public CustomAddonAdapter(CustomAddonData customAddonData, BaseController controller, BaseMonument monument, CustomAddonDefinition addonDefinition) : base(customAddonData, controller, monument) { CustomAddonData = customAddonData; AddonDefinition = addonDefinition; } public override void Spawn() { Component = AddonDefinition.Spawn(IntendedPosition, IntendedRotation, CustomAddonData.GetSerializedData()); AddonDefinition.AdapterUsers.Add(this); var entity = Component as BaseEntity; if (entity != null) { MonumentEntityComponent.AddToEntity(Plugin._entityTracker, entity, this, Monument); } else { Component.gameObject.AddComponent().Adapter = this; } } public override void Kill() { if (_wasKilled) return; _wasKilled = true; AddonDefinition.Kill(Component); } public void OnEntityKilled(BaseEntity entity) { // In case it's a multi-part addon, call Kill() to ensure the whole addon is removed. Kill(); AddonDefinition.AdapterUsers.Remove(this); Controller.OnAdapterKilled(this); } public void OnAddonDestroyed() { // In case it's a multi-part addon, call Kill() to ensure the whole addon is removed. Kill(); AddonDefinition.AdapterUsers.Remove(this); Controller.OnAdapterKilled(this); } } private class CustomAddonController : BaseController { public CustomAddonData CustomAddonData { get; } private CustomAddonDefinition _addonDefinition; public CustomAddonController(ProfileController profileController, CustomAddonData customAddonData, CustomAddonDefinition addonDefinition) : base(profileController, customAddonData) { CustomAddonData = customAddonData; _addonDefinition = addonDefinition; } public override BaseAdapter CreateAdapter(BaseMonument monument) => new CustomAddonAdapter(CustomAddonData, this, monument, _addonDefinition); } #endregion #region Controller Factories private class EntityControllerFactory { public virtual bool AppliesToEntity(BaseEntity entity) => true; public virtual EntityController CreateController(ProfileController controller, EntityData entityData) => new EntityController(controller, entityData); } private class SignControllerFactory : EntityControllerFactory { public override bool AppliesToEntity(BaseEntity entity) => entity is ISignage; public override EntityController CreateController(ProfileController controller, EntityData entityData) => new SignController(controller, entityData); } private class CCTVControllerFactory : EntityControllerFactory { public override bool AppliesToEntity(BaseEntity entity) => entity is CCTV_RC; public override EntityController CreateController(ProfileController controller, EntityData entityData) => new CCTVController(controller, entityData); } private class ControllerFactory { private MonumentAddons _plugin; public ControllerFactory(MonumentAddons plugin) { _plugin = plugin; } private EntityControllerFactory[] _entityFactories = { // The first that matches will be used. new CCTVControllerFactory(), new SignControllerFactory(), new EntityControllerFactory(), }; public BaseController CreateController(ProfileController profileController, BaseData data) { var spawnGroupData = data as SpawnGroupData; if (spawnGroupData != null) return new SpawnGroupController(profileController, spawnGroupData); var pasteData = data as PasteData; if (pasteData != null) return new PasteController(profileController, pasteData); var customAddonData = data as CustomAddonData; if (customAddonData != null) { var addonDefinition = _plugin._customAddonManager.GetAddon(customAddonData.AddonName); return addonDefinition != null ? new CustomAddonController(profileController, customAddonData, addonDefinition) : null; } var entityData = data as EntityData; if (entityData != null) return ResolveEntityFactory(entityData)?.CreateController(profileController, entityData); var prefabData = data as PrefabData; if (prefabData != null) return new PrefabController(profileController, prefabData); return null; } private EntityControllerFactory ResolveEntityFactory(EntityData entityData) { var baseEntity = FindBaseEntityForPrefab(entityData.PrefabName); if (baseEntity == null) return null; foreach (var controllerFactory in _entityFactories) { if (controllerFactory.AppliesToEntity(baseEntity)) return controllerFactory; } return null; } } #endregion #endregion #region IO Manager private class IOManager { private const int FreePowerAmount = 1000; private static bool HasInput(IOEntity ioEntity, IOType ioType) { foreach (var input in ioEntity.inputs) { if (input.type == ioType) return true; } return false; } private static bool HasConnectedInput(IOEntity ioEntity, int inputSlot) { return inputSlot < ioEntity.inputs.Length && ioEntity.inputs[inputSlot].connectedTo.Get() != null; } private readonly int[] _defaultInputSlots = { 0 }; private readonly Type[] _dontPowerPrefabsOfType = { typeof(ANDSwitch), typeof(CustomDoorManipulator), typeof(DoorManipulator), typeof(ORSwitch), typeof(RFBroadcaster), typeof(TeslaCoil), typeof(XORSwitch), // Has inputs to move the lift but does not consume power. typeof(Elevator), // Has inputs to toggle on/off but does not consume power. typeof(FuelGenerator), // Has audio input only. typeof(AudioVisualisationEntityLight), typeof(ConnectedSpeaker), // Has no power input. typeof(FogMachine), typeof(SnowMachine), typeof(StrobeLight), }; private readonly Dictionary _inputSlotsByPrefabName = new Dictionary { ["assets/prefabs/deployable/playerioents/gates/combiner/electrical.combiner.deployed.prefab"] = new[] { 0, 1 }, ["assets/prefabs/deployable/playerioents/fluidswitch/fluidswitch.prefab"] = new[] { 2 }, ["assets/prefabs/deployable/playerioents/industrialconveyor/industrialconveyor.deployed.prefab"] = new[] { 1 }, ["assets/prefabs/deployable/playerioents/industrialcrafter/industrialcrafter.deployed.prefab"] = new[] { 1 }, ["assets/prefabs/deployable/playerioents/poweredwaterpurifier/poweredwaterpurifier.deployed.prefab"] = new[] { 1 }, }; private readonly Dictionary _inputSlotsByPrefabId = new Dictionary(); private List _dontPowerPrefabIds = new List(); public void OnServerInitialized() { foreach (var prefabPath in GameManifest.Current.entities) { var ioEntity = GameManager.server.FindPrefab(prefabPath)?.GetComponent(); if (ioEntity == null || !HasInput(ioEntity, IOType.Electric)) continue; if (_dontPowerPrefabsOfType.Contains(ioEntity.GetType())) { _dontPowerPrefabIds.Add(ioEntity.prefabID); } } foreach (var entry in _inputSlotsByPrefabName) { var ioEntity = GameManager.server.FindPrefab(entry.Key)?.GetComponent(); if (ioEntity == null) continue; _inputSlotsByPrefabId[ioEntity.prefabID] = entry.Value; } } public bool MaybeProvidePower(IOEntity ioEntity) { if (_dontPowerPrefabIds.Contains(ioEntity.prefabID)) return false; var providedPower = false; var inputSlotList = DeterminePowerInputSlots(ioEntity); foreach (var inputSlot in inputSlotList) { if (inputSlot >= ioEntity.inputs.Length || HasConnectedInput(ioEntity, inputSlot)) continue; if (ioEntity.inputs[inputSlot].type != IOType.Electric) continue; ioEntity.UpdateFromInput(FreePowerAmount, inputSlot); providedPower = true; } return providedPower; } private int[] DeterminePowerInputSlots(IOEntity ioEntity) { int[] inputSlots; return _inputSlotsByPrefabId.TryGetValue(ioEntity.prefabID, out inputSlots) ? inputSlots : _defaultInputSlots; } } #endregion #region Adapter Display Manager private class AdapterDisplayManager { private MonumentAddons _plugin; private UniqueNameRegistry _uniqueNameRegistry; private Configuration _config => _plugin._config; public const int DefaultDisplayDuration = 60; public const int HeaderSize = 25; public static readonly string Divider = $"------------------------------"; public static readonly Vector3 ArrowVerticalOffeset = new Vector3(0, 0.5f, 0); private const int DisplayIntervalDuration = 2; private class PlayerInfo { public Timer Timer; public ProfileController ProfileController; } private float DisplayDistanceSquared => Mathf.Pow(_config.DebugDisplayDistance, 2); private StringBuilder _sb = new StringBuilder(200); private Dictionary _playerInfo = new Dictionary(); public AdapterDisplayManager(MonumentAddons plugin, UniqueNameRegistry uniqueNameRegistry) { _plugin = plugin; _uniqueNameRegistry = uniqueNameRegistry; } public void SetPlayerProfile(BasePlayer player, ProfileController profileController) { GetOrCreatePlayerInfo(player).ProfileController = profileController; } public void ShowAllRepeatedly(BasePlayer player, int duration = -1, bool immediate = true) { var playerInfo = GetOrCreatePlayerInfo(player); if (immediate || playerInfo.Timer == null || playerInfo.Timer.Destroyed) { ShowNearbyAdapters(player, player.transform.position, playerInfo); } if (playerInfo.Timer != null && !playerInfo.Timer.Destroyed) { if (duration == 0) { playerInfo.Timer.Destroy(); } else { var remainingTime = playerInfo.Timer.Repetitions * DisplayIntervalDuration; var newDuration = duration > 0 ? duration : Math.Max(remainingTime, DefaultDisplayDuration); var newRepetitions = Math.Max(newDuration / DisplayIntervalDuration, 1); playerInfo.Timer.Reset(delay: -1, repetitions: newRepetitions); } return; } if (duration == -1) { duration = DefaultDisplayDuration; } // Ensure repetitions is not 0 since that would result in infintire repetitions. var repetitions = Math.Max(duration / DisplayIntervalDuration, 1); playerInfo.Timer = _plugin.timer.Repeat(DisplayIntervalDuration - 0.2f, repetitions, () => { if (player == null || player.IsDestroyed || !player.IsConnected) { playerInfo.Timer.Destroy(); _playerInfo.Remove(player.userID); return; } ShowNearbyAdapters(player, player.transform.position, playerInfo); }); } private Color DetermineColor(BaseAdapter adapter, PlayerInfo playerInfo, ProfileController profileController) { if (playerInfo.ProfileController != null && playerInfo.ProfileController != profileController) return Color.grey; if (adapter is SpawnPointAdapter) return new Color(1, 0.5f, 0); if (adapter is PasteAdapter) return Color.cyan; if (adapter is CustomAddonAdapter) return Color.green; return Color.magenta; } private void AddCommonInfo(BasePlayer player, ProfileController profileController, BaseController controller, BaseAdapter adapter) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelProfile, profileController.Profile.Name)); var monumentTierList = GetTierList(GetMonumentTierMask(adapter.Monument.Position)); _sb.AppendLine(monumentTierList.Count > 0 ? _plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelMonumentWithTier, adapter.Monument.AliasOrShortName, controller.Adapters.Count, string.Join(", ", monumentTierList)) : _plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelMonument, adapter.Monument.AliasOrShortName, controller.Adapters.Count)); } private void ShowPuzzleInfo(BasePlayer player, EntityAdapter entityAdapter, PuzzleReset puzzleReset, Vector3 playerPosition, PlayerInfo playerInfo) { _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderPuzzle)}"); _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPuzzlePlayersBlockReset, puzzleReset.playersBlockReset)); if (puzzleReset.playersBlockReset) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPlayerDetectionRadius, puzzleReset.playerDetectionRadius)); if (PuzzleReset.AnyPlayersWithinDistance(puzzleReset.playerDetectionOrigin, puzzleReset.playerDetectionRadius)) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPlayerDetectedInRadius)); } } _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPuzzleTimeBetweenResets, FormatTime(puzzleReset.timeBetweenResets))); var resetTimeElapsedField = typeof(PuzzleReset).GetField("resetTimeElapsed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (resetTimeElapsedField != null) { var resetTimeElapsed = (float)resetTimeElapsedField.GetValue(puzzleReset); var timeRemaining = puzzleReset.GetResetSpacing() - resetTimeElapsed; var nextResetMessage = timeRemaining > 0 ? FormatTime(timeRemaining) : _plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPuzzleNextResetOverdue); _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPuzzleNextReset, nextResetMessage)); } if (entityAdapter != null) { var profileController = entityAdapter.ProfileController; var spawnGroupIdList = entityAdapter.EntityData.Puzzle?.SpawnGroupIds; if (spawnGroupIdList != null) { List spawnGroupNameList = null; foreach (var spawnGroupId in spawnGroupIdList) { var spawnGroupAdapter = profileController.FindAdapter(spawnGroupId, entityAdapter.Monument) as SpawnGroupAdapter; if (spawnGroupAdapter == null) continue; if (spawnGroupNameList == null) { spawnGroupNameList = Facepunch.Pool.GetList(); } spawnGroupNameList.Add(spawnGroupAdapter.SpawnGroupData.Name); float closestDistanceSquared; var spawnPointAdapter = FindClosestSpawnPointAdapter(spawnGroupAdapter, playerPosition, out closestDistanceSquared); if (spawnPointAdapter != null) { Ddraw.Arrow(player, entityAdapter.Position + ArrowVerticalOffeset, spawnPointAdapter.Position + ArrowVerticalOffeset, 0.25f, DetermineColor(spawnPointAdapter, playerInfo, profileController), DisplayIntervalDuration); } } if (spawnGroupNameList != null) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPuzzleSpawnGroups, string.Join(", ", spawnGroupNameList))); Facepunch.Pool.FreeList(ref spawnGroupNameList); } } } } private void ShowEntityInfo(BasePlayer player, EntityAdapter adapter, Vector3 playerPosition, PlayerInfo playerInfo) { var entityData = adapter.EntityData; var controller = adapter.Controller; var profileController = controller.ProfileController; var color = DetermineColor(adapter, playerInfo, profileController); var uniqueEntityName = _uniqueNameRegistry.GetUniqueShortName(entityData.PrefabName); _sb.Clear(); _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderEntity, uniqueEntityName)}"); AddCommonInfo(player, profileController, controller, adapter); if (entityData.Skin != 0) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSkin, entityData.Skin)); } if (entityData.Scale != 1) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelScale, entityData.Scale)); } var vehicleVendor = adapter.Entity as VehicleVendor; if (vehicleVendor != null) { var vehicleSpawner = vehicleVendor.GetVehicleSpawner(); if (vehicleSpawner != null) { Ddraw.Arrow(player, adapter.Position + new Vector3(0, 1.5f, 0), vehicleSpawner.transform.position, 0.25f, color, DisplayIntervalDuration); } } var doorManipulator = adapter.Entity as DoorManipulator; if (doorManipulator != null && doorManipulator.targetDoor != null) { Ddraw.Arrow(player, adapter.Position, doorManipulator.targetDoor.transform.position, 0.2f, color, DisplayIntervalDuration); } var cctvIdentifier = entityData.CCTV?.RCIdentifier; if (cctvIdentifier != null) { var identifier = (adapter as CCTVEntityAdapter)?.GetIdentifier(); if (identifier != null) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelRCIdentifier, identifier)); } } var puzzleReset = (adapter.Entity as IOEntity)?.GetComponent(); if (puzzleReset != null) { _sb.AppendLine(Divider); ShowPuzzleInfo(player, adapter, puzzleReset, playerPosition, playerInfo); } Ddraw.Text(player, adapter.Position, _sb.ToString(), color, DisplayIntervalDuration); } private void ShowPrefabInfo(BasePlayer player, PrefabAdapter adapter, Vector3 playerPosition, PlayerInfo playerInfo) { var prefabData = adapter.PrefabData; var controller = adapter.Controller; var profileController = controller.ProfileController; var color = DetermineColor(adapter, playerInfo, profileController); var uniqueEntityName = _uniqueNameRegistry.GetUniqueShortName(prefabData.PrefabName); _sb.Clear(); _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderPrefab, uniqueEntityName)}"); AddCommonInfo(player, profileController, controller, adapter); Ddraw.Text(player, adapter.Position, _sb.ToString(), color, DisplayIntervalDuration); } private void ShowSpawnPointInfo(BasePlayer player, SpawnPointAdapter adapter, SpawnGroupAdapter spawnGroupAdapter, PlayerInfo playerInfo, bool showGroupInfo) { var spawnPointData = adapter.SpawnPointData; var controller = adapter.Controller; var profileController = controller.ProfileController; var color = DetermineColor(adapter, playerInfo, profileController); var spawnGroupData = spawnGroupAdapter.SpawnGroupData; _sb.Clear(); _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderSpawnPoint, spawnGroupData.Name)}"); AddCommonInfo(player, profileController, controller, adapter); var booleanProperties = new List(); if (spawnPointData.Exclusive) { booleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSpawnPointExclusive)); } if (spawnPointData.RandomRotation) { booleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSpawnPointRandomRotation)); } if (spawnPointData.SnapToGround) { booleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSpawnPointSnapToGround)); } if (spawnPointData.CheckSpace) { booleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSpawnPointCheckSpace)); } if (booleanProperties.Count > 0) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelFlags, string.Join(" | ", booleanProperties))); } if (spawnPointData.RandomRadius > 0) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSpawnPointRandomRadius, spawnPointData.RandomRadius)); } if (spawnPointData.PlayerDetectionRadius > 0) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPlayerDetectionRadius, spawnPointData.PlayerDetectionRadius)); } if (adapter.SpawnPoint.HasPlayersIntersecting()) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPlayerDetectedInRadius)); } if (showGroupInfo) { _sb.AppendLine(Divider); _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderSpawnGroup, spawnGroupData.Name)}"); _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelSpawnPoints, spawnGroupData.SpawnPoints.Count)); var groupBooleanProperties = new List(); if (spawnGroupData.InitialSpawn) { groupBooleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelInitialSpawn)); } if (spawnGroupData.PreventDuplicates) { groupBooleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPreventDuplicates)); } if (spawnGroupData.PauseScheduleWhileFull) { groupBooleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPauseScheduleWhileFull)); } if (spawnGroupData.RespawnWhenNearestPuzzleResets) { groupBooleanProperties.Add(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelRespawnWhenNearestPuzzleResets) + (spawnGroupAdapter.AssociatedPuzzleReset == null ? " (!)" : "")); } if (groupBooleanProperties.Count > 0) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelFlags, string.Join(" | ", groupBooleanProperties))); } _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPopulation, spawnGroupAdapter.SpawnGroup.currentPopulation, spawnGroupData.MaxPopulation)); _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelRespawnPerTick, spawnGroupData.SpawnPerTickMin, spawnGroupData.SpawnPerTickMax)); var spawnGroup = spawnGroupAdapter.SpawnGroup; if (spawnGroup.WantsTimedSpawn()) { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelRespawnDelay, FormatTime(spawnGroup.respawnDelayMin), FormatTime(spawnGroup.respawnDelayMax))); var nextSpawnTime = GetTimeToNextSpawn(spawnGroup); if (!float.IsPositiveInfinity(nextSpawnTime)) { var nextSpawnMessage = spawnGroupData.PauseScheduleWhileFull && spawnGroup.currentPopulation >= spawnGroup.maxPopulation ? _plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelNextSpawnPaused) : nextSpawnTime <= 0 ? _plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelNextSpawnQueued) : FormatTime(Mathf.CeilToInt(nextSpawnTime)); _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelNextSpawn, nextSpawnMessage)); } } if (spawnGroupData.Prefabs.Count > 0) { var totalWeight = spawnGroupData.TotalWeight; _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelEntities)); foreach (var prefabEntry in spawnGroupData.Prefabs) { var relativeChance = (float)prefabEntry.Weight / totalWeight; _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelEntityDetail, _uniqueNameRegistry.GetUniqueShortName(prefabEntry.PrefabName), prefabEntry.Weight, relativeChance)); } } else { _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelNoEntities)); } foreach (var otherAdapter in spawnGroupAdapter.SpawnPointAdapters) { Ddraw.Arrow(player, otherAdapter.Position + ArrowVerticalOffeset, adapter.Position + ArrowVerticalOffeset, 0.25f, color, DisplayIntervalDuration); } } Ddraw.ArrowThrough(player, adapter.Position + ArrowVerticalOffeset, adapter.Rotation, 1f, 0.15f, color, DisplayIntervalDuration); Ddraw.Sphere(player, adapter.Position, 0.5f, color, DisplayIntervalDuration); Ddraw.Text(player, adapter.Position, _sb.ToString(), color, DisplayIntervalDuration); if (spawnGroupData.RespawnWhenNearestPuzzleResets) { _sb.Clear(); var puzzleReset = spawnGroupAdapter.AssociatedPuzzleReset; if (puzzleReset != null) { ShowPuzzleInfo(player, null, spawnGroupAdapter.AssociatedPuzzleReset, player.transform.position, playerInfo); var position = puzzleReset.transform.position; Ddraw.Arrow(player, position + ArrowVerticalOffeset, adapter.Position + ArrowVerticalOffeset, 0.25f, DetermineColor(adapter, playerInfo, profileController), DisplayIntervalDuration); Ddraw.Text(player, position, _sb.ToString(), color, DisplayIntervalDuration); } } } private void ShowPasteInfo(BasePlayer player, PasteAdapter adapter, PlayerInfo playerInfo) { var pasteData = adapter.PasteData; var controller = adapter.Controller; var profileController = controller.ProfileController; var color = DetermineColor(adapter, playerInfo, profileController); _sb.Clear(); _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderPaste, pasteData.Filename)}"); AddCommonInfo(player, profileController, controller, adapter); Ddraw.Text(player, adapter.Position, _sb.ToString(), color, DisplayIntervalDuration); } private void ShowCustomAddonInfo(BasePlayer player, CustomAddonAdapter adapter, PlayerInfo playerInfo) { var customAddonData = adapter.CustomAddonData; var controller = adapter.Controller; var profileController = controller.ProfileController; var color = DetermineColor(adapter, playerInfo, profileController); var addonDefinition = adapter.AddonDefinition; _sb.Clear(); _sb.AppendLine($"{_plugin.GetMessage(player.UserIDString, LangEntry.ShowHeaderCustom, customAddonData.AddonName)}"); _sb.AppendLine(_plugin.GetMessage(player.UserIDString, LangEntry.ShowLabelPlugin, addonDefinition.OwnerPlugin.Name)); AddCommonInfo(player, profileController, controller, adapter); addonDefinition.AddDisplayInfo?.Invoke(adapter.Component, customAddonData.GetSerializedData(), _sb); Ddraw.Text(player, adapter.Position, _sb.ToString(), color, DisplayIntervalDuration); } private SpawnPointAdapter FindClosestSpawnPointAdapter(SpawnGroupAdapter spawnGroupAdapter, Vector3 origin, out float closestDistanceSquared) { SpawnPointAdapter closestSpawnPointAdapter = null; closestDistanceSquared = float.MaxValue; foreach (var spawnPointAdapter in spawnGroupAdapter.SpawnPointAdapters) { var adapterDistanceSquared = (spawnPointAdapter.Position - origin).sqrMagnitude; if (adapterDistanceSquared < closestDistanceSquared) { closestSpawnPointAdapter = spawnPointAdapter; closestDistanceSquared = adapterDistanceSquared; } } return closestSpawnPointAdapter; } private void ShowNearbyAdapters(BasePlayer player, Vector3 playerPosition, PlayerInfo playerInfo) { var isAdmin = player.IsAdmin; if (!isAdmin) { player.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, true); player.SendNetworkUpdateImmediate(); } foreach (var adapter in _plugin._profileManager.GetEnabledAdapters()) { var entityAdapter = adapter as EntityAdapter; if (entityAdapter != null) { if ((playerPosition - entityAdapter.Position).sqrMagnitude <= DisplayDistanceSquared) { ShowEntityInfo(player, entityAdapter, playerPosition, playerInfo); } continue; } var prefabAdapter = adapter as PrefabAdapter; if (prefabAdapter != null) { if ((playerPosition - prefabAdapter.Position).sqrMagnitude <= DisplayDistanceSquared) { ShowPrefabInfo(player, prefabAdapter, playerPosition, playerInfo); } continue; } var spawnGroupAdapter = adapter as SpawnGroupAdapter; if (spawnGroupAdapter != null) { float closestDistanceSquared; var closestSpawnPointAdapter = FindClosestSpawnPointAdapter(spawnGroupAdapter, playerPosition, out closestDistanceSquared); if (closestDistanceSquared <= DisplayDistanceSquared) { foreach (var spawnPointAdapter in spawnGroupAdapter.SpawnPointAdapters) { ShowSpawnPointInfo(player, spawnPointAdapter, spawnGroupAdapter, playerInfo, showGroupInfo: spawnPointAdapter == closestSpawnPointAdapter); } } continue; } var pasteAdapter = adapter as PasteAdapter; if (pasteAdapter != null) { if ((playerPosition - pasteAdapter.Position).sqrMagnitude <= DisplayDistanceSquared) { ShowPasteInfo(player, pasteAdapter, playerInfo); } continue; } var customAddonAdapter = adapter as CustomAddonAdapter; if (customAddonAdapter != null) { if ((playerPosition - customAddonAdapter.Position).sqrMagnitude <= DisplayDistanceSquared) { ShowCustomAddonInfo(player, customAddonAdapter, playerInfo); } continue; } } if (!isAdmin) { player.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, false); player.SendNetworkUpdateImmediate(); } } private PlayerInfo GetOrCreatePlayerInfo(BasePlayer player) { PlayerInfo playerInfo; if (!_playerInfo.TryGetValue(player.userID, out playerInfo)) { playerInfo = new PlayerInfo(); _playerInfo[player.userID] = playerInfo; } return playerInfo; } } #endregion #region Profile Management private enum ProfileStatus { Loading, Loaded, Unloading, Unloaded } private struct SpawnQueueItem { public BaseData Data; public BaseMonument Monument; public ICollection MonumentList; public SpawnQueueItem(BaseData data, ICollection monumentList) { Data = data; Monument = null; MonumentList = monumentList; } public SpawnQueueItem(BaseData data, BaseMonument monument) { Data = data; Monument = monument; MonumentList = null; } } private class ProfileController { public MonumentAddons Plugin { get; } public Profile Profile { get; private set; } public ProfileStatus ProfileStatus { get; private set; } = ProfileStatus.Unloaded; public WaitUntil WaitUntilLoaded; public WaitUntil WaitUntilUnloaded; private Configuration _config => Plugin._config; private StoredData _pluginData => Plugin._data; private ProfileStateData _profileStateData => Plugin._profileStateData; private CoroutineManager _coroutineManager = new CoroutineManager(); private Dictionary _controllersByData = new Dictionary(); private Queue _spawnQueue = new Queue(); public bool IsEnabled => _pluginData.IsProfileEnabled(Profile.Name); public ProfileController(MonumentAddons plugin, Profile profile, bool startLoaded = false) { Plugin = plugin; Profile = profile; WaitUntilLoaded = new WaitUntil(() => ProfileStatus == ProfileStatus.Loaded); WaitUntilUnloaded = new WaitUntil(() => ProfileStatus == ProfileStatus.Unloaded); if (startLoaded) { ProfileStatus = ProfileStatus.Loaded; } } public void OnControllerKilled(BaseController controller) => _controllersByData.Remove(controller.Data); public Coroutine StartCoroutine(IEnumerator enumerator) => _coroutineManager.StartCoroutine(enumerator); public Coroutine StartCallbackRoutine(Coroutine coroutine, Action callback) => _coroutineManager.StartCallbackRoutine(coroutine, callback); public IEnumerable GetControllers() where T : BaseController { return _controllersByData.Values.OfType(); } public IEnumerable GetAdapters() where T : BaseAdapter { foreach (var controller in _controllersByData.Values) { foreach (var adapter in controller.Adapters) { var adapterOfType = adapter as T; if (adapterOfType != null) { yield return adapterOfType; continue; } var spawnGroupAdapter = adapter as SpawnGroupAdapter; if (spawnGroupAdapter != null) { foreach (var childAdapter in spawnGroupAdapter.SpawnPointAdapters.OfType()) { yield return childAdapter; } } } } } public void Load(ProfileCounts profileCounts = null) { if (ProfileStatus == ProfileStatus.Loading || ProfileStatus == ProfileStatus.Loaded) return; CleanOrphanedEntities(); EnqueueAll(profileCounts); if (_spawnQueue.Count == 0) { ProfileStatus = ProfileStatus.Loaded; } } public void PreUnload() { _coroutineManager.Destroy(); foreach (var controller in _controllersByData.Values.ToList()) { controller.PreUnload(); } } public void Unregister() { foreach (var controller in _controllersByData.Values.ToList()) { controller.Unregister(); } } public void Unload(IEnumerator cleanupRoutine = null) { if (ProfileStatus == ProfileStatus.Unloading || ProfileStatus == ProfileStatus.Unloaded) return; ProfileStatus = ProfileStatus.Unloading; CoroutineManager.StartGlobalCoroutine(UnloadRoutine(cleanupRoutine)); } public void Reload(Profile newProfileData) { Interrupt(); PreUnload(); if (_config.EnableEntitySaving) { Unregister(); } StartCoroutine(ReloadRoutine(newProfileData)); } public IEnumerator PartialLoadForLateMonument(ICollection dataList, BaseMonument monument) { foreach (var data in dataList) { Enqueue(new SpawnQueueItem(data, monument)); } yield return WaitUntilLoaded; } public void SpawnNewData(BaseData data, ICollection monumentList) { if (ProfileStatus == ProfileStatus.Unloading || ProfileStatus == ProfileStatus.Unloaded) return; Enqueue(new SpawnQueueItem(data, monumentList)); } public void Rename(string newName) { _pluginData.RenameProfileReferences(Profile.Name, newName); Plugin._originalProfileStore.MoveTo(Profile, newName); Plugin._profileStore.MoveTo(Profile, newName); } public void Enable(Profile newProfileData) { if (IsEnabled) return; Profile = newProfileData; _pluginData.SetProfileEnabled(Profile.Name); Load(); } public void Disable() { Interrupt(); PreUnload(); IEnumerator cleanupRoutine = null; if (_config.EnableEntitySaving) { Unregister(); var entitiesToKill = _profileStateData.FindAndRemoveValidEntities(Profile.Name); if (entitiesToKill != null && entitiesToKill.Count > 0) { Plugin._saveProfileStateDebounced.Schedule(); cleanupRoutine = KillEntitiesRoutine(entitiesToKill); } } Unload(cleanupRoutine); } public void Clear() { if (!IsEnabled) { Profile.MonumentDataMap.Clear(); Plugin._profileStore.Save(Profile); return; } Interrupt(); StartCoroutine(ClearRoutine()); } public BaseController FindControllerById(Guid guid) { foreach (var entry in _controllersByData) { if (entry.Key.Id == guid) return entry.Value; } return null; } public BaseAdapter FindAdapter(Guid guid, BaseMonument monument) { return FindControllerById(guid)?.FindAdapterForMonument(monument); } public T FindEntity(Guid guid, BaseMonument monument) where T : BaseEntity { return _config.EnableEntitySaving ? _profileStateData.FindEntity(Profile.Name, monument, guid) as T : (FindAdapter(guid, monument) as EntityAdapter)?.Entity as T; } public void SetupIO() { // Setup connections first. foreach (var entry in _controllersByData) { var data = entry.Key; var controller = entry.Value; var entityData = data as EntityData; if (entityData?.IOEntityData == null) continue; var entityController = controller as EntityController; if (entityController == null) continue; foreach (var adapter in entityController.Adapters) { (adapter as EntityAdapter)?.UpdateIOConnections(); } } // Provide free power to unconnected entities. foreach (var entry in _controllersByData) { var singleEntityController = entry.Value as EntityController; if (singleEntityController == null) continue; foreach (var adapter in singleEntityController.Adapters) { (adapter as EntityAdapter)?.MaybeProvidePower(); } } } private void Interrupt() { _coroutineManager.StopAll(); _spawnQueue.Clear(); } private BaseController GetController(BaseData data) { BaseController controller; return _controllersByData.TryGetValue(data, out controller) ? controller : null; } private BaseController EnsureController(BaseData data) { var controller = GetController(data); if (controller == null) { controller = Plugin._controllerFactory.CreateController(this, data); if (controller != null) { _controllersByData[data] = controller; } } return controller; } private void Enqueue(SpawnQueueItem queueItem) { _spawnQueue.Enqueue(queueItem); // If there are more items in the queue, we can assume there's already a coroutine processing them. if (_spawnQueue.Count == 1) { ProfileStatus = ProfileStatus.Loading; StartCoroutine(ProcessSpawnQueue()); } } private void EnqueueAll(ProfileCounts profileCounts) { foreach (var entry in Profile.MonumentDataMap) { var monumentData = entry.Value; if (monumentData.NumSpawnables == 0) continue; var monumentAliasOrShortName = entry.Key; var matchingMonuments = Plugin.GetMonumentsByAliasOrShortName(monumentAliasOrShortName); if (matchingMonuments == null) continue; if (profileCounts != null) { profileCounts.EntityCount += matchingMonuments.Count * monumentData.Entities.Count; profileCounts.SpawnPointCount += matchingMonuments.Count * monumentData.NumSpawnPoints; profileCounts.PasteCount += matchingMonuments.Count * monumentData.Pastes.Count; profileCounts.PrefabCount = matchingMonuments.Count * monumentData.Prefabs.Count; } foreach (var data in monumentData.GetSpawnablesLazy()) { Enqueue(new SpawnQueueItem(data, matchingMonuments)); } } } private IEnumerator ProcessSpawnQueue() { // Wait one frame to ensure the queue has time to be populated. yield return null; SpawnQueueItem queueItem; while (_spawnQueue.TryDequeue(out queueItem)) { Plugin.TrackStart(); var controller = EnsureController(queueItem.Data); Plugin.TrackEnd(); if (controller == null) { // The controller factory may not have been implemented for this data type, // or the custom addon owner plugin may not be loaded. continue; } if (queueItem.Monument != null) { // Check for null in case the monument is dynamic and was destroyed (e.g., cargo ship). if (queueItem.Monument.IsValid) { controller.SpawnAtMonument(queueItem.Monument); yield return null; } } else { yield return controller.SpawnAtMonumentsRoutine(queueItem.MonumentList); } } ProfileStatus = ProfileStatus.Loaded; SetupIO(); } private IEnumerator UnloadRoutine(IEnumerator cleanupRoutine) { foreach (var controller in _controllersByData.Values.ToList()) { yield return controller.KillRoutine(); } if (cleanupRoutine != null) yield return cleanupRoutine; ProfileStatus = ProfileStatus.Unloaded; } private IEnumerator ReloadRoutine(Profile newProfileData) { Unload(); yield return WaitUntilUnloaded; Profile = newProfileData; Load(); yield return WaitUntilLoaded; } private IEnumerator ClearRoutine() { Unload(); yield return WaitUntilUnloaded; Profile.MonumentDataMap.Clear(); Plugin._profileStore.Save(Profile); ProfileStatus = ProfileStatus.Loaded; } private IEnumerator CleanEntitiesRoutine(ProfileState profileState, List entitiesToKill) { yield return KillEntitiesRoutine(entitiesToKill); if (profileState.CleanStaleEntityRecords() > 0) { Plugin._saveProfileStateDebounced.Schedule(); } } private void CleanOrphanedEntities() { var profileState = _profileStateData.GetProfileState(Profile.Name); if (profileState == null) return; List entitiesToKill = null; foreach (var entityEntry in profileState.FindValidEntities()) { if (!Profile.HasEntity(entityEntry.MonumentAliasOrShortName, entityEntry.Guid)) { if (entitiesToKill == null) { entitiesToKill = new List(); } entitiesToKill.Add(entityEntry.Entity); } } if (entitiesToKill != null && entitiesToKill.Count > 0) { CoroutineManager.StartGlobalCoroutine(CleanEntitiesRoutine(profileState, entitiesToKill)); } else if (profileState.CleanStaleEntityRecords() > 0) { Plugin._saveProfileStateDebounced.Schedule(); } } } private class ProfileCounts { public int EntityCount; public int PrefabCount; public int SpawnPointCount; public int PasteCount; } private class ProfileInfo { public static List GetList(StoredData pluginData, ProfileManager profileManager) { var profileNameList = ProfileStore.GetProfileNames(); var profileInfoList = new List(profileNameList.Length); foreach (var profileName in profileNameList) { if (OriginalProfileStore.IsOriginalProfile(profileName)) continue; profileInfoList.Add(new ProfileInfo { Name = profileName, Enabled = pluginData.EnabledProfiles.Contains(profileName), Profile = profileManager.GetCachedProfileController(profileName)?.Profile }); } return profileInfoList; } public string Name; public bool Enabled; public Profile Profile; } private class ProfileManager { private readonly MonumentAddons _plugin; private OriginalProfileStore _originalProfileStore; private readonly ProfileStore _profileStore; private List _profileControllers = new List(); private Configuration _config => _plugin._config; private StoredData _pluginData => _plugin._data; public ProfileManager(MonumentAddons plugin, OriginalProfileStore originalProfileStore, ProfileStore profileStore) { _plugin = plugin; _originalProfileStore = originalProfileStore; _profileStore = profileStore; } public IEnumerator LoadAllProfilesRoutine() { foreach (var profileName in _pluginData.EnabledProfiles.ToList()) { ProfileController controller; try { controller = GetProfileController(profileName); } catch (Exception ex) { _pluginData.SetProfileDisabled(profileName); LogError($"Disabled profile {profileName} due to error: {ex.Message}"); continue; } if (controller == null) { _pluginData.SetProfileDisabled(profileName); LogWarning($"Disabled profile {profileName} because its data file was not found."); continue; } var profileCounts = new ProfileCounts(); controller.Load(profileCounts); yield return controller.WaitUntilLoaded; var profile = controller.Profile; var byAuthor = !string.IsNullOrWhiteSpace(profile.Author) ? $" by {profile.Author}" : string.Empty; var spawnablesSummaryList = new List(); if (profileCounts.EntityCount > 0) { spawnablesSummaryList.Add($"{profileCounts.EntityCount} entities"); } if (profileCounts.SpawnPointCount > 0) { spawnablesSummaryList.Add($"{profileCounts.SpawnPointCount} spawn points"); } if (profileCounts.PasteCount > 0) { spawnablesSummaryList.Add($"{profileCounts.PasteCount} pastes"); } var spawnablesSummary = spawnablesSummaryList.Count > 0 ? string.Join(", ", spawnablesSummaryList) : "No addons spawned"; LogInfo($"Loaded profile {profile.Name}{byAuthor} ({spawnablesSummary})."); } } public void UnloadAllProfiles() { foreach (var profileController in _profileControllers) { profileController.PreUnload(); if (_config.EnableEntitySaving) { profileController.Unregister(); } } CoroutineManager.StartGlobalCoroutine(UnloadAllProfilesRoutine()); } public IEnumerator PartialLoadForLateMonumentRoutine(BaseMonument monument) { foreach (var controller in _profileControllers) { if (!controller.IsEnabled) continue; MonumentData monumentData; if (!controller.Profile.MonumentDataMap.TryGetValue(monument.AliasOrShortName, out monumentData)) continue; if (monumentData.NumSpawnables == 0) continue; yield return controller.PartialLoadForLateMonument(monumentData.GetSpawnables(), monument); } } public ProfileController GetCachedProfileController(string profileName) { var profileNameLower = profileName.ToLower(); foreach (var cachedController in _profileControllers) { if (cachedController.Profile.Name.ToLower() == profileNameLower) return cachedController; } return null; } public ProfileController GetProfileController(string profileName) { var profileController = GetCachedProfileController(profileName); if (profileController != null) return profileController; var profile = _profileStore.LoadIfExists(profileName); if (profile != null) { var controller = new ProfileController(_plugin, profile); _profileControllers.Add(controller); return controller; } return null; } public ProfileController GetPlayerProfileController(string userId) { string profileName; return _pluginData.SelectedProfiles.TryGetValue(userId, out profileName) ? GetProfileController(profileName) : null; } public ProfileController GetPlayerProfileControllerOrDefault(string userId) { var controller = GetPlayerProfileController(userId); if (controller != null) return controller; controller = GetProfileController(DefaultProfileName); return controller != null && controller.IsEnabled ? controller : null; } public bool ProfileExists(string profileName) { var profileNameLower = profileName.ToLower(); foreach (var cachedController in _profileControllers) { if (cachedController.Profile.Name.ToLower() == profileNameLower) return true; } return _profileStore.Exists(profileName); } public ProfileController CreateProfile(string profileName, string authorName) { var profile = _profileStore.Create(profileName, authorName); var controller = new ProfileController(_plugin, profile, startLoaded: true); _profileControllers.Add(controller); return controller; } public void DisableProfile(ProfileController profileController) { _pluginData.SetProfileDisabled(profileController.Profile.Name); profileController.Disable(); } public void DeleteProfile(ProfileController profileController) { if (profileController.IsEnabled) { DisableProfile(profileController); } _profileControllers.Remove(profileController); _originalProfileStore.Delete(profileController.Profile.Name); _profileStore.Delete(profileController.Profile.Name); } public IEnumerable GetEnabledProfileControllers() { foreach (var profileControler in _profileControllers) { if (profileControler.IsEnabled) yield return profileControler; } } public IEnumerable GetEnabledControllers() where T : BaseController { foreach (var profileController in GetEnabledProfileControllers()) { foreach (var controller in profileController.GetControllers()) { yield return controller; } } } public IEnumerable GetEnabledAdapters() where T : BaseAdapter { foreach (var profileControler in GetEnabledProfileControllers()) { foreach (var adapter in profileControler.GetAdapters()) { yield return adapter; } } } private IEnumerator UnloadAllProfilesRoutine() { foreach (var controller in _profileControllers) { controller.Unload(); yield return controller.WaitUntilUnloaded; } } } #endregion #region Data #region Base Data private abstract class BaseData { [JsonProperty("Id", Order = -10)] public Guid Id; } private abstract class BaseTransformData : BaseData { [JsonProperty("SnapToTerrain", Order = -4, DefaultValueHandling = DefaultValueHandling.Ignore)] public bool SnapToTerrain; [JsonProperty("Position", Order = -3)] public Vector3 Position; // Kept for backwards compatibility. [JsonProperty("RotationAngle", DefaultValueHandling = DefaultValueHandling.Ignore)] public float DeprecatedRotationAngle { set { RotationAngles = new Vector3(0, value, 0); } } [JsonProperty("RotationAngles", Order = -2, DefaultValueHandling = DefaultValueHandling.Ignore)] public Vector3 RotationAngles; [JsonProperty("OnTerrain")] public bool DepredcatedOnTerrain { set { SnapToTerrain = value; } } } #endregion #region Prefab Data private class PrefabData : BaseTransformData { [JsonProperty("PrefabName", Order = -5)] public string PrefabName; } #endregion #region Entity Data private class BuildingBlockInfo { [JsonProperty("Grade")] [JsonConverter(typeof(StringEnumConverter))] [DefaultValue(BuildingGrade.Enum.None)] public BuildingGrade.Enum Grade = BuildingGrade.Enum.None; } private class CCTVInfo { [JsonProperty("RCIdentifier", DefaultValueHandling = DefaultValueHandling.Ignore)] public string RCIdentifier; [JsonProperty("Pitch", DefaultValueHandling = DefaultValueHandling.Ignore)] public float Pitch; [JsonProperty("Yaw", DefaultValueHandling = DefaultValueHandling.Ignore)] public float Yaw; } private class SignArtistImage { [JsonProperty("Url")] public string Url; [JsonProperty("Raw", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool Raw; } private class IOConnectionData { [JsonProperty("ConnectedToId")] public Guid ConnectedToId; [JsonProperty("Slot")] public int Slot; [JsonProperty("ConnectedToSlot")] public int ConnectedToSlot; [JsonProperty("ShowWire")] public bool ShowWire = true; [JsonProperty("Color", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonConverter(typeof(StringEnumConverter))] public WireColour Color; [JsonProperty("Points")] public Vector3[] Points; } private class IOEntityData { [JsonProperty("Outputs")] public List Outputs = new List(); public IOConnectionData FindConnection(int slot) { foreach (var connectionData in Outputs) { if (connectionData.Slot == slot) return connectionData; } return null; } } private class PuzzleData { [JsonProperty("PlayersBlockReset")] public bool PlayersBlockReset; [JsonProperty("PlayerDetectionRadius")] public float PlayerDetectionRadius; [JsonProperty("SecondsBetweenResets")] public float SecondsBetweenResets; [JsonProperty("SpawnGroupIds", DefaultValueHandling = DefaultValueHandling.Ignore)] public List SpawnGroupIds; public bool ShouldSerializeSpawnGroupIds() => SpawnGroupIds?.Count > 0; public bool HasSpawnGroupId(Guid spawnGroupId) { return SpawnGroupIds?.Contains(spawnGroupId) ?? false; } public void AddSpawnGroupId(Guid spawnGroupId) { if (SpawnGroupIds == null) { SpawnGroupIds = new List(); } SpawnGroupIds.Add(spawnGroupId); } public void RemoveSpawnGroupId(Guid spawnGroupId) { SpawnGroupIds?.Remove(spawnGroupId); } } private class EntityData : BaseTransformData { [JsonProperty("PrefabName", Order = -5)] public string PrefabName; [JsonProperty("Skin", DefaultValueHandling = DefaultValueHandling.Ignore)] public ulong Skin; [JsonProperty("Puzzle", DefaultValueHandling = DefaultValueHandling.Ignore)] public PuzzleData Puzzle; [JsonProperty("Scale", DefaultValueHandling = DefaultValueHandling.Ignore)] [DefaultValue(1f)] public float Scale = 1; [JsonProperty("CardReaderLevel", DefaultValueHandling = DefaultValueHandling.Ignore)] public ushort CardReaderLevel; [JsonProperty("BuildingBlock", DefaultValueHandling = DefaultValueHandling.Ignore)] public BuildingBlockInfo BuildingBlock; [JsonProperty("CCTV", DefaultValueHandling = DefaultValueHandling.Ignore)] public CCTVInfo CCTV; [JsonProperty("SignArtistImages", DefaultValueHandling = DefaultValueHandling.Ignore)] public SignArtistImage[] SignArtistImages; [JsonProperty("VendingProfile", DefaultValueHandling = DefaultValueHandling.Ignore)] public object VendingProfile; [JsonProperty("IOEntity", DefaultValueHandling = DefaultValueHandling.Ignore)] public IOEntityData IOEntityData; [JsonProperty("SkullName", DefaultValueHandling = DefaultValueHandling.Ignore)] public string SkullName; public void RemoveIOConnection(int slot) { if (IOEntityData == null) return; for (var i = IOEntityData.Outputs.Count - 1; i >= 0; i--) { if (IOEntityData.Outputs[i].Slot == slot) { IOEntityData.Outputs.RemoveAt(i); break; } } } public void AddIOConnection(IOConnectionData connectionData) { if (IOEntityData == null) { IOEntityData = new IOEntityData(); } RemoveIOConnection(connectionData.Slot); IOEntityData.Outputs.Add(connectionData); } public PuzzleData EnsurePuzzleData(PuzzleReset puzzleReset) { return Puzzle ?? (Puzzle = new PuzzleData { PlayersBlockReset = puzzleReset.playersBlockReset, PlayerDetectionRadius = puzzleReset.playerDetectionRadius, SecondsBetweenResets = puzzleReset.timeBetweenResets, }); } } #endregion #region Spawn Group Data private class SpawnPointData : BaseTransformData { [JsonProperty("Exclusive", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool Exclusive; [JsonProperty("SnapToGround", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool SnapToGround; [JsonProperty("DropToGround")] public bool DeprecatedDropToGround { set { SnapToGround = value; } } [JsonProperty("CheckSpace", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool CheckSpace; [JsonProperty("RandomRotation", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool RandomRotation; [JsonProperty("RandomRadius", DefaultValueHandling = DefaultValueHandling.Ignore)] public float RandomRadius; [JsonProperty("PlayerDetectionRadius", DefaultValueHandling = DefaultValueHandling.Ignore)] public float PlayerDetectionRadius; } private class WeightedPrefabData { [JsonProperty("PrefabName")] public string PrefabName; [JsonProperty("Weight")] public int Weight = 1; } private class SpawnGroupData : BaseData { [JsonProperty("Name")] public string Name; [JsonProperty("MaxPopulation")] public int MaxPopulation = 1; [JsonProperty("SpawnPerTickMin")] public int SpawnPerTickMin = 1; [JsonProperty("SpawnPerTickMax")] public int SpawnPerTickMax = 2; [JsonProperty("RespawnDelayMin")] public float RespawnDelayMin = 1500; [JsonProperty("RespawnDelayMax")] public float RespawnDelayMax = 2100; // Default to true for backwards compatibility. [JsonProperty("InitialSpawn")] public bool InitialSpawn = true; [JsonProperty("PreventDuplicates", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool PreventDuplicates; [JsonProperty("PauseScheduleWhileFull", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool PauseScheduleWhileFull; [JsonProperty("RespawnWhenNearestPuzzleResets", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool RespawnWhenNearestPuzzleResets; [JsonProperty("Prefabs")] public List Prefabs = new List(); [JsonProperty("SpawnPoints")] public List SpawnPoints = new List(); [JsonIgnore] public int TotalWeight { get { var total = 0; foreach (var prefabEntry in Prefabs) { total += prefabEntry.Weight; } return total; } } public List FindPrefabMatches(string partialName, UniqueNameRegistry uniqueNameRegistry) { return SearchUtils.FindMatches( Prefabs, prefabData => StringUtils.Contains(prefabData.PrefabName, partialName), prefabData => StringUtils.EqualsCaseInsensitive(prefabData.PrefabName, partialName), prefabData => StringUtils.Contains(uniqueNameRegistry.GetUniqueShortName(prefabData.PrefabName), partialName), prefabData => StringUtils.EqualsCaseInsensitive(uniqueNameRegistry.GetUniqueShortName(prefabData.PrefabName), partialName) ); } } #endregion #region Paste Data private class PasteData : BaseTransformData { [JsonProperty("Filename")] public string Filename; } #endregion #region Custom Addon Data private class CustomAddonData : BaseTransformData { [JsonProperty("AddonName")] public string AddonName; [JsonProperty("PluginData", DefaultValueHandling = DefaultValueHandling.Ignore)] private object PluginData; public JObject GetSerializedData() { return PluginData as JObject; } public void SetData(object data) { PluginData = data as JObject ?? JObject.FromObject(data); } } #endregion #region Profile Data private class ProfileSummaryEntry { public string MonumentName; public string AddonType; public string AddonName; public int Count; } private List GetProfileSummary(IPlayer player, Profile profile) { var summary = new List(); var addonTypeEntity = GetMessage(player.Id, LangEntry.AddonTypeEntity); var addonTypePrefab = GetMessage(player.Id, LangEntry.AddonTypePrefab); var addonTypePaste = GetMessage(player.Id, LangEntry.AddonTypePaste); var addonTypeSpawnPoint = GetMessage(player.Id, LangEntry.AddonTypeSpawnPoint); var addonTypeCustom = GetMessage(player.Id, LangEntry.AddonTypeCustom); foreach (var monumentEntry in profile.MonumentDataMap) { var monumentName = monumentEntry.Key; var monumentData = monumentEntry.Value; var entryMap = new Dictionary(); foreach (var entityData in monumentData.Entities) { var entityUniqueName = _uniqueNameRegistry.GetUniqueShortName(entityData.PrefabName); ProfileSummaryEntry summaryEntry; if (!entryMap.TryGetValue(entityUniqueName, out summaryEntry)) { summaryEntry = new ProfileSummaryEntry { MonumentName = monumentName, AddonType = addonTypeEntity, AddonName = entityUniqueName, }; entryMap[entityUniqueName] = summaryEntry; } summaryEntry.Count++; } foreach (var prefabData in monumentData.Prefabs) { var uniqueName = _uniqueNameRegistry.GetUniqueShortName(prefabData.PrefabName); ProfileSummaryEntry summaryEntry; if (!entryMap.TryGetValue(uniqueName, out summaryEntry)) { summaryEntry = new ProfileSummaryEntry { MonumentName = monumentName, AddonType = addonTypePrefab, AddonName = uniqueName, }; entryMap[uniqueName] = summaryEntry; } summaryEntry.Count++; } foreach (var spawnGroupData in monumentData.SpawnGroups) { if (spawnGroupData.SpawnPoints.Count == 0) continue; // Add directly to the summary since different spawn groups could have the same name. summary.Add(new ProfileSummaryEntry { MonumentName = monumentName, AddonType = addonTypeSpawnPoint, AddonName = spawnGroupData.Name, Count = spawnGroupData.SpawnPoints.Count, }); } foreach (var pasteData in monumentData.Pastes) { ProfileSummaryEntry summaryEntry; if (!entryMap.TryGetValue(pasteData.Filename, out summaryEntry)) { summaryEntry = new ProfileSummaryEntry { MonumentName = monumentName, AddonType = addonTypePaste, AddonName = pasteData.Filename, }; entryMap[pasteData.Filename] = summaryEntry; } summaryEntry.Count++; } foreach (var customAddonData in monumentData.CustomAddons) { ProfileSummaryEntry summaryEntry; if (!entryMap.TryGetValue(customAddonData.AddonName, out summaryEntry)) { summaryEntry = new ProfileSummaryEntry { MonumentName = monumentName, AddonType = addonTypeCustom, AddonName = customAddonData.AddonName, }; entryMap[customAddonData.AddonName] = summaryEntry; } summaryEntry.Count++; } summary.AddRange(entryMap.Values); } return summary; } private class MonumentData { [JsonProperty("Entities")] public List Entities = new List(); public bool ShouldSerializeEntities() => Entities.Count > 0; [JsonProperty("Prefabs")] public List Prefabs = new List(); public bool ShouldSerializePrefabs() => Prefabs.Count > 0; [JsonProperty("SpawnGroups")] public List SpawnGroups = new List(); public bool ShouldSerializeSpawnGroups() => SpawnGroups.Count > 0; [JsonProperty("Pastes")] public List Pastes = new List(); public bool ShouldSerializePastes() => Pastes.Count > 0; [JsonProperty("CustomAddons")] public List CustomAddons = new List(); public bool ShouldSerializeCustomAddons() => CustomAddons.Count > 0; [JsonIgnore] public int NumSpawnables => Entities.Count + Prefabs.Count + SpawnGroups.Count + Pastes.Count + CustomAddons.Count; [JsonIgnore] public int NumSpawnPoints { get { var count = 0; foreach (var spawnGroup in SpawnGroups) { count += spawnGroup.SpawnPoints.Count; } return count; } } public IEnumerable GetSpawnablesLazy() { foreach (var entityData in Entities) yield return entityData; foreach (var prefabData in Prefabs) yield return prefabData; foreach (var spawnGroupData in SpawnGroups) yield return spawnGroupData; foreach (var pasteData in Pastes) yield return pasteData; foreach (var customAddonData in CustomAddons) yield return customAddonData; } public ICollection GetSpawnables() { var list = new List(NumSpawnables); foreach (var spawnable in GetSpawnablesLazy()) { list.Add(spawnable); } return list; } public bool HasEntity(Guid guid) { foreach (var entityData in Entities) { if (entityData.Id == guid) return true; } return false; } public bool HasSpawnGroup(Guid guid) { foreach (var spawnGroupData in SpawnGroups) { if (spawnGroupData.Id == guid) return true; } return false; } public void AddData(BaseData data) { var entityData = data as EntityData; if (entityData != null) { Entities.Add(entityData); return; } var prefabData = data as PrefabData; if (prefabData != null) { Prefabs.Add(prefabData); return; } var spawnGroupData = data as SpawnGroupData; if (spawnGroupData != null) { SpawnGroups.Add(spawnGroupData); return; } var pasteData = data as PasteData; if (pasteData != null) { Pastes.Add(pasteData); return; } var customAddonData = data as CustomAddonData; if (customAddonData != null) { CustomAddons.Add(customAddonData); return; } LogError($"AddData not implemented for type: {data.GetType()}"); } public bool RemoveData(BaseData data) { var entityData = data as EntityData; if (entityData != null) return Entities.Remove(entityData); var prefabData = data as PrefabData; if (prefabData != null) return Prefabs.Remove(prefabData); var spawnGroupData = data as SpawnGroupData; if (spawnGroupData != null) return SpawnGroups.Remove(spawnGroupData); var spawnPointData = data as SpawnPointData; if (spawnPointData != null) { foreach (var parentSpawnGroupData in SpawnGroups) { var index = parentSpawnGroupData.SpawnPoints.IndexOf(spawnPointData); if (index == -1) continue; // If removing the spawn group, don't remove the spawn point, so it's easier to undo. if (parentSpawnGroupData.SpawnPoints.Count == 1) { SpawnGroups.Remove(parentSpawnGroupData); CleanSpawnGroupReferences(parentSpawnGroupData.Id); } else { parentSpawnGroupData.SpawnPoints.RemoveAt(index); } return true; } return false; } var pasteData = data as PasteData; if (pasteData != null) return Pastes.Remove(pasteData); var customAddonData = data as CustomAddonData; if (customAddonData != null) return CustomAddons.Remove(customAddonData); LogError($"RemoveData not implemented for type: {data.GetType()}"); return false; } private void CleanSpawnGroupReferences(Guid id) { foreach (var entityData in Entities) { entityData.Puzzle?.SpawnGroupIds?.Remove(id); } } } private static class ProfileDataMigration where T : Profile { private static readonly Dictionary _prefabCorrections = new Dictionary { ["assets/content/vehicles/locomotive/locomotive.entity.prefab"] = "assets/content/vehicles/trains/locomotive/locomotive.entity.prefab", ["assets/content/vehicles/workcart/workcart.entity.prefab"] = "assets/content/vehicles/trains/workcart/workcart.entity.prefab", ["assets/content/vehicles/workcart/workcart_aboveground.entity.prefab"] = "assets/content/vehicles/trains/workcart/workcart_aboveground.entity.prefab", ["assets/content/vehicles/workcart/workcart_aboveground2.entity.prefab"] = "assets/content/vehicles/trains/workcart/workcart_aboveground2.entity.prefab", ["assets/content/vehicles/train/trainwagona.entity.prefab"] = "assets/content/vehicles/trains/wagons/trainwagona.entity.prefab", ["assets/content/vehicles/train/trainwagonb.entity.prefab"] = "assets/content/vehicles/trains/wagons/trainwagonb.entity.prefab", ["assets/content/vehicles/train/trainwagonc.entity.prefab"] = "assets/content/vehicles/trains/wagons/trainwagonc.entity.prefab", ["assets/content/vehicles/train/trainwagonunloadablefuel.entity.prefab"] = "assets/content/vehicles/trains/wagons/trainwagonunloadablefuel.entity.prefab", ["assets/content/vehicles/train/trainwagonunloadableloot.entity.prefab"] = "assets/content/vehicles/trains/wagons/trainwagonunloadableloot.entity.prefab", ["assets/content/vehicles/train/trainwagonunloadable.entity.prefab"] = "assets/content/vehicles/trains/wagons/trainwagonunloadable.entity.prefab", }; private static string GetPrefabCorrectionIfExists(string prefabName) { string correctedPrefabName; return _prefabCorrections.TryGetValue(prefabName, out correctedPrefabName) ? correctedPrefabName : prefabName; } public static bool MigrateToLatest(T data) { // Using single | to avoid short-circuiting. return MigrateV0ToV1(data) | MigrateV1ToV2(data) | MigrateIncorrectPrefabs(data); } public static bool MigrateV0ToV1(T data) { if (data.SchemaVersion != 0) return false; data.SchemaVersion++; var contentChanged = false; if (data.DeprecatedMonumentMap != null) { foreach (var entityDataList in data.DeprecatedMonumentMap.Values) { if (entityDataList == null) continue; foreach (var entityData in entityDataList) { if (GetShortName(entityData.PrefabName) == "big_wheel" && entityData.RotationAngles.x != 90) { // The plugin used to coerce the x component to 90. entityData.RotationAngles.x = 90; contentChanged = true; } } } } return contentChanged; } public static bool MigrateV1ToV2(T data) { if (data.SchemaVersion != 1) return false; data.SchemaVersion++; var contentChanged = false; if (data.DeprecatedMonumentMap != null) { foreach (var entry in data.DeprecatedMonumentMap) { var entityDataList = entry.Value; if (entityDataList == null || entityDataList.Count == 0) continue; data.MonumentDataMap[entry.Key] = new MonumentData { Entities = entityDataList, }; contentChanged = true; } data.DeprecatedMonumentMap = null; } return contentChanged; } public static bool MigrateIncorrectPrefabs(T data) { var contentChanged = false; foreach (var monumentData in data.MonumentDataMap.Values) { foreach (var entityData in monumentData.Entities) { var correctedPrefabName = GetPrefabCorrectionIfExists(entityData.PrefabName); if (correctedPrefabName != entityData.PrefabName) { entityData.PrefabName = correctedPrefabName; contentChanged = true; } } foreach (var spawnGroupData in monumentData.SpawnGroups) { foreach (var prefabData in spawnGroupData.Prefabs) { var correctedPrefabName = GetPrefabCorrectionIfExists(prefabData.PrefabName); if (correctedPrefabName != prefabData.PrefabName) { prefabData.PrefabName = correctedPrefabName; contentChanged = true; } } } } return contentChanged; } } private class FileStore where T : class, new() { protected string _directoryPath; public FileStore(string directoryPath) { _directoryPath = directoryPath + "/"; } public virtual bool Exists(string filename) { return Interface.Oxide.DataFileSystem.ExistsDatafile(GetFilepath(filename)); } public virtual T Load(string filename) { return Interface.Oxide.DataFileSystem.ReadObject(GetFilepath(filename)) ?? new T(); } public T LoadIfExists(string filename) { return Exists(filename) ? Load(filename) : default(T); } public void Save(string filename, T data) { Interface.Oxide.DataFileSystem.WriteObject(GetFilepath(filename), data); } public void Delete(string filename) { Interface.Oxide.DataFileSystem.DeleteDataFile(GetFilepath(filename)); } protected virtual string GetFilepath(string filename) { return $"{_directoryPath}{filename}"; } } private class OriginalProfileStore : FileStore { public const string OriginalSuffix = "_original"; public static bool IsOriginalProfile(string profileName) { return profileName.EndsWith(OriginalSuffix); } public OriginalProfileStore() : base(nameof(MonumentAddons)) {} protected override string GetFilepath(string profileName) { return base.GetFilepath(profileName + OriginalSuffix); } public void Save(Profile profile) { base.Save(profile.Name, profile); } public void MoveTo(Profile profile, string newName) { var original = LoadIfExists(profile.Name); if (original == null) return; var oldName = original.Name; original.Name = newName; Save(original); Delete(oldName); } } private class ProfileStore : FileStore { public static string[] GetProfileNames() { var filenameList = Interface.Oxide.DataFileSystem.GetFiles(nameof(MonumentAddons)); for (var i = 0; i < filenameList.Length; i++) { var filename = filenameList[i]; var start = filename.LastIndexOf(System.IO.Path.DirectorySeparatorChar) + 1; var end = filename.LastIndexOf(".", StringComparison.Ordinal); filenameList[i] = filename.Substring(start, end - start); } return filenameList; } public ProfileStore() : base(nameof(MonumentAddons)) {} public override bool Exists(string profileName) { return !OriginalProfileStore.IsOriginalProfile(profileName) && base.Exists(profileName); } public override Profile Load(string profileName) { if (OriginalProfileStore.IsOriginalProfile(profileName)) return null; var profile = base.Load(profileName); profile.Name = GetCaseSensitiveFileName(profileName); var migrated = ProfileDataMigration.MigrateToLatest(profile); if (migrated) { LogWarning($"Profile {profile.Name} has been automatically migrated."); } // Backfill ids if missing. foreach (var monumentData in profile.MonumentDataMap.Values) { foreach (var entityData in monumentData.Entities) { if (entityData.Id == Guid.Empty) { entityData.Id = Guid.NewGuid(); } } } if (migrated) { Save(profile.Name, profile); } return profile; } public bool TryLoad(string profileName, out Profile profile, out string errorMessage) { try { profile = Load(profileName); errorMessage = null; return true; } catch (JsonReaderException ex) { profile = null; errorMessage = ex.Message; return false; } } public void Save(Profile profile) { base.Save(profile.Name, profile); } public Profile Create(string profileName, string authorName) { var profile = new Profile { Name = profileName, Author = authorName, }; ProfileDataMigration.MigrateToLatest(profile); Save(profile); return profile; } public void MoveTo(Profile profile, string newName) { var oldName = profile.Name; profile.Name = newName; Save(profile); Delete(oldName); } public void EnsureDefaultProfile() { Load(DefaultProfileName); } private string GetCaseSensitiveFileName(string profileName) { foreach (var name in GetProfileNames()) { if (StringUtils.EqualsCaseInsensitive(name, profileName)) return name; } return profileName; } } private class Profile { [JsonProperty("Name")] public string Name; [JsonProperty("Author", DefaultValueHandling = DefaultValueHandling.Ignore)] public string Author; [JsonProperty("SchemaVersion", DefaultValueHandling = DefaultValueHandling.Ignore)] public float SchemaVersion; [JsonProperty("Url", DefaultValueHandling = DefaultValueHandling.Ignore)] public string Url; [JsonProperty("Monuments", DefaultValueHandling = DefaultValueHandling.Ignore)] public Dictionary> DeprecatedMonumentMap; [JsonProperty("MonumentData")] public Dictionary MonumentDataMap = new Dictionary(); public bool IsEmpty() { if (MonumentDataMap == null || MonumentDataMap.IsEmpty()) return true; foreach (var monumentData in MonumentDataMap.Values) { if (monumentData.NumSpawnables > 0) return false; } return true; } public bool HasEntity(string monumentAliasOrShortName, Guid guid) { return MonumentDataMap.GetOrDefault(monumentAliasOrShortName)?.HasEntity(guid) ?? false; } public bool HasEntity(string monumentAliasOrShortName, EntityData entityData) { return MonumentDataMap.GetOrDefault(monumentAliasOrShortName)?.Entities.Contains(entityData) ?? false; } public bool HasSpawnGroup(string monumentAliasOrShortName, Guid guid) { return MonumentDataMap.GetOrDefault(monumentAliasOrShortName)?.HasSpawnGroup(guid) ?? false; } public void AddData(string monumentAliasOrShortName, BaseData data) { EnsureMonumentData(monumentAliasOrShortName).AddData(data); } public bool RemoveData(BaseData data, out string monumentAliasOrShortName) { foreach (var entry in MonumentDataMap) { if (entry.Value.RemoveData(data)) { monumentAliasOrShortName = entry.Key; return true; } } monumentAliasOrShortName = null; return false; } private MonumentData EnsureMonumentData(string monumentAliasOrShortName) { MonumentData monumentData; if (!MonumentDataMap.TryGetValue(monumentAliasOrShortName, out monumentData)) { monumentData = new MonumentData(); MonumentDataMap[monumentAliasOrShortName] = monumentData; } return monumentData; } } #endregion #region Data File Utils private static class DataFileUtils { public static bool Exists(string filepath) { return Interface.Oxide.DataFileSystem.ExistsDatafile(filepath); } public static T Load(string filepath) where T : class, new() { return Interface.Oxide.DataFileSystem.ReadObject(filepath) ?? new T(); } public static T LoadIfExists(string filepath) where T : class, new() { return Exists(filepath) ? Load(filepath) : null; } public static T LoadOrNew(string filepath) where T : class, new() { return LoadIfExists(filepath) ?? new T(); } public static void Save(string filepath, T data) { Interface.Oxide.DataFileSystem.WriteObject(filepath, data); } } private class BaseDataFile { private string _filepath; public BaseDataFile(string filepath) { _filepath = filepath; } public void Save() { DataFileUtils.Save(_filepath, this); } } #endregion #region Profile State private struct MonumentEntityEntry { public string MonumentAliasOrShortName; public Guid Guid; public BaseEntity Entity; public MonumentEntityEntry(string monumentAliasOrShortName, Guid guid, BaseEntity entity) { MonumentAliasOrShortName = monumentAliasOrShortName; Guid = guid; Entity = entity; } } private class MonumentState : IDeepCollection { [JsonProperty("Entities")] public Dictionary Entities = new Dictionary(); public bool HasItems() => Entities.Count > 0; public bool HasEntity(Guid guid, NetworkableId entityId) { return Entities.GetOrDefault(guid) == entityId.Value; } public BaseEntity FindEntity(Guid guid) { ulong entityId; if (!Entities.TryGetValue(guid, out entityId)) return null; var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(entityId)) as BaseEntity; if (entity == null || entity.IsDestroyed) return null; return entity; } public void AddEntity(Guid guid, NetworkableId entityId) { Entities[guid] = entityId.Value; } public bool RemoveEntity(Guid guid) { return Entities.Remove(guid); } public IEnumerable> FindValidEntities() { if (Entities.Count == 0) yield break; foreach (var entry in Entities) { var entity = FindValidEntity(entry.Value); if (entity == null) continue; yield return new ValueTuple(entry.Key, entity); } } public int CleanStaleEntityRecords() { if (Entities.Count == 0) return 0; var cleanedCount = 0; foreach (var entry in Entities.ToList()) { if (FindValidEntity(entry.Value) == null) { Entities.Remove(entry.Key); cleanedCount++; } } return cleanedCount; } } private class MonumentStateMapConverter : DictionaryKeyConverter { public override string KeyToString(Vector3 v) => $"{v.x:g9},{v.y:g9},{v.z:g9}"; public override Vector3 KeyFromString(string key) { var parts = key.Split(','); return new Vector3(float.Parse(parts[0]), float.Parse(parts[1]), float.Parse(parts[2])); } } private class Vector3EqualityComparer : IEqualityComparer { public bool Equals(Vector3 a, Vector3 b) { return a == b; } public int GetHashCode(Vector3 vector) { return vector.GetHashCode(); } } private class MonumentStateMap : IDeepCollection { [JsonProperty("ByLocation")] [JsonConverter(typeof(MonumentStateMapConverter))] private Dictionary ByLocation = new Dictionary(new Vector3EqualityComparer()); public bool ShouldSerializeByLocation() => HasDeepItems(ByLocation); [JsonProperty("ByEntity")] private Dictionary ByEntity = new Dictionary(); public bool ShouldSerializeByEntity() => HasDeepItems(ByEntity); public bool HasItems() => HasDeepItems(ByLocation) || HasDeepItems(ByEntity); public IEnumerable> FindValidEntities() { foreach (var monumentState in ByLocation.Values) { foreach (var entityEntry in monumentState.FindValidEntities()) { yield return entityEntry; } } foreach (var monumentEntry in ByEntity) { var monumentEntityId = monumentEntry.Key; if (FindValidEntity(monumentEntityId) == null) continue; foreach (var entityEntry in monumentEntry.Value.FindValidEntities()) { yield return entityEntry; } } } public int CleanStaleEntityRecords() { var cleanedCount = 0; if (ByLocation.Count > 0) { foreach (var monumentState in ByLocation.Values) { cleanedCount += monumentState.CleanStaleEntityRecords(); } } if (ByEntity.Count > 0) { foreach (var monumentEntry in ByEntity.ToList()) { if (FindValidEntity(monumentEntry.Key) == null) { ByEntity.Remove(monumentEntry.Key); continue; } cleanedCount += monumentEntry.Value.CleanStaleEntityRecords(); } } return cleanedCount; } public MonumentState GetMonumentState(BaseMonument monument) { var dynamicMonument = monument as DynamicMonument; if (dynamicMonument != null) return ByEntity.GetOrDefault(dynamicMonument.EntityId.Value); return ByLocation.GetOrDefault(monument.Position); } public MonumentState GetOrCreateMonumentState(BaseMonument monument) { var dynamicMonument = monument as DynamicMonument; if (dynamicMonument != null) return ByEntity.GetOrCreate(dynamicMonument.EntityId.Value); return ByLocation.GetOrCreate(monument.Position); } } private class ProfileState : Dictionary, IDeepCollection { public bool HasItems() => HasDeepItems(this); public int CleanStaleEntityRecords() { var cleanedCount = 0; foreach (var monumentStateMap in Values) { cleanedCount += monumentStateMap.CleanStaleEntityRecords(); } return cleanedCount; } public IEnumerable FindValidEntities() { if (Count == 0) yield break; foreach (var entry in this) { var monumentAliasOrShortName = entry.Key; var monumentStateMap = entry.Value; if (!monumentStateMap.HasItems()) continue; foreach (var entityEntry in monumentStateMap.FindValidEntities()) { yield return new MonumentEntityEntry(monumentAliasOrShortName, entityEntry.Item1, entityEntry.Item2); } } } } private class ProfileStateMap : Dictionary, IDeepCollection { public ProfileStateMap() : base(StringComparer.InvariantCultureIgnoreCase) {} public bool HasItems() => HasDeepItems(this); } private class ProfileStateData : BaseDataFile { private static string Filepath => $"{nameof(MonumentAddons)}_State"; public static ProfileStateData Load(StoredData pluginData) { var data = DataFileUtils.LoadOrNew(Filepath); data._pluginData = pluginData; return data; } private StoredData _pluginData; public ProfileStateData() : base(Filepath) {} [JsonProperty("ProfileState", DefaultValueHandling = DefaultValueHandling.Ignore)] private ProfileStateMap ProfileStateMap = new ProfileStateMap(); public bool ShouldSerializeProfileStateMap() => ProfileStateMap.HasItems(); public ProfileState GetProfileState(string profileName) { return ProfileStateMap.GetOrDefault(profileName); } public bool HasEntity(string profileName, BaseMonument monument, Guid guid, NetworkableId entityId) { return GetProfileState(profileName) ?.GetOrDefault(monument.AliasOrShortName) ?.GetMonumentState(monument) ?.HasEntity(guid, entityId) ?? false; } public BaseEntity FindEntity(string profileName, BaseMonument monument, Guid guid) { return GetProfileState(profileName) ?.GetOrDefault(monument.AliasOrShortName) ?.GetMonumentState(monument) ?.FindEntity(guid); } public void AddEntity(string profileName, BaseMonument monument, Guid guid, NetworkableId entityId) { ProfileStateMap.GetOrCreate(profileName) .GetOrCreate(monument.AliasOrShortName) .GetOrCreateMonumentState(monument) .AddEntity(guid, entityId); } public bool RemoveEntity(string profileName, BaseMonument monument, Guid guid) { return GetProfileState(profileName) ?.GetOrDefault(monument.AliasOrShortName) ?.GetMonumentState(monument) ?.RemoveEntity(guid) ?? false; } public List FindAndRemoveValidEntities(string profileName) { var profileState = ProfileStateMap.GetOrDefault(profileName); if (profileState == null) return null; var entityList = new List(); foreach (var entityEntry in profileState.FindValidEntities()) { entityList.Add(entityEntry.Entity); } ProfileStateMap.Remove(profileName); return entityList; } public List CleanDisabledProfileState() { var entitiesToKill = new List(); if (ProfileStateMap.Count == 0) return entitiesToKill; var cleanedCount = 0; foreach (var entry in ProfileStateMap.ToList()) { var profileName = entry.Key; var monumentStateMap = entry.Value; if (_pluginData.IsProfileEnabled(profileName)) continue; // Delete entities previously spawned for profiles which are now disabled. // This addresses the use case where the plugin is unloaded with entity persistence enabled, // then the data file is manually edited to disable a profile. foreach (var entityEntry in monumentStateMap.FindValidEntities()) { entitiesToKill.Add(entityEntry.Entity); } ProfileStateMap.Remove(profileName); cleanedCount++; } if (cleanedCount > 0) { Save(); } return entitiesToKill; } public void Reset() { if (!ProfileStateMap.HasItems()) return; ProfileStateMap.Clear(); Save(); } } #endregion #region Plugin Data private static class StoredDataMigration { private static readonly Dictionary MigrateMonumentNames = new Dictionary { ["TRAIN_STATION"] = "TrainStation", ["BARRICADE_TUNNEL"] = "BarricadeTunnel", ["LOOT_TUNNEL"] = "LootTunnel", ["3_WAY_INTERSECTION"] = "Intersection", ["4_WAY_INTERSECTION"] = "LargeIntersection", }; public static bool MigrateToLatest(ProfileStore profileStore, StoredData data) { // Using single | to avoid short-circuiting. return MigrateV0ToV1(data) | MigrateV1ToV2(profileStore, data); } public static bool MigrateV0ToV1(StoredData data) { if (data.DataFileVersion != 0) return false; data.DataFileVersion++; var contentChanged = false; if (data.DeprecatedMonumentMap != null) { foreach (var monumentEntry in data.DeprecatedMonumentMap.ToList()) { var alias = monumentEntry.Key; var entityList = monumentEntry.Value; string newAlias; if (MigrateMonumentNames.TryGetValue(alias, out newAlias)) { data.DeprecatedMonumentMap[newAlias] = entityList; data.DeprecatedMonumentMap.Remove(alias); alias = newAlias; } foreach (var entityData in entityList) { if (alias == "LootTunnel" || alias == "BarricadeTunnel") { // Migrate from the original rotations to the rotations used by MonumentFinder. entityData.DeprecatedRotationAngle = (entityData.RotationAngles.y + 180) % 360; entityData.Position = Quaternion.Euler(0, 180, 0) * entityData.Position; } // Migrate from the backwards rotations to the correct ones. var newAngle = (720 - entityData.RotationAngles.y) % 360; entityData.DeprecatedRotationAngle = newAngle; contentChanged = true; } } } return contentChanged; } public static bool MigrateV1ToV2(ProfileStore profileStore, StoredData data) { if (data.DataFileVersion != 1) return false; data.DataFileVersion++; var profile = new Profile { Name = DefaultProfileName, }; if (data.DeprecatedMonumentMap != null) { profile.DeprecatedMonumentMap = data.DeprecatedMonumentMap; } profileStore.Save(profile); data.DeprecatedMonumentMap = null; data.EnabledProfiles.Add(DefaultProfileName); return true; } } private class StoredData { public static StoredData Load(ProfileStore profileStore) { var data = Interface.Oxide.DataFileSystem.ReadObject(nameof(MonumentAddons)) ?? new StoredData(); var originalDataFileVersion = data.DataFileVersion; if (StoredDataMigration.MigrateToLatest(profileStore, data)) { LogWarning("Data file has been automatically migrated."); } if (data.DataFileVersion != originalDataFileVersion) { data.Save(); } return data; } [JsonProperty("DataFileVersion", DefaultValueHandling = DefaultValueHandling.Ignore)] public float DataFileVersion; [JsonProperty("EnabledProfiles")] public HashSet EnabledProfiles = new HashSet(); [JsonProperty("SelectedProfiles")] public Dictionary SelectedProfiles = new Dictionary(); [JsonProperty("Monuments", DefaultValueHandling = DefaultValueHandling.Ignore)] public Dictionary> DeprecatedMonumentMap; public void Save() => Interface.Oxide.DataFileSystem.WriteObject(nameof(MonumentAddons), this); public bool IsProfileEnabled(string profileName) => EnabledProfiles.Contains(profileName); public void SetProfileEnabled(string profileName) { EnabledProfiles.Add(profileName); Save(); } public void SetProfileDisabled(string profileName) { if (!EnabledProfiles.Remove(profileName)) return; foreach (var entry in SelectedProfiles.ToList()) { if (entry.Value == profileName) { SelectedProfiles.Remove(entry.Key); } } Save(); } public void RenameProfileReferences(string oldName, string newName) { foreach (var entry in SelectedProfiles.ToList()) { if (entry.Value == oldName) { SelectedProfiles[entry.Key] = newName; } } if (EnabledProfiles.Remove(oldName)) { EnabledProfiles.Add(newName); } Save(); } public string GetSelectedProfileName(string userId) { string profileName; if (SelectedProfiles.TryGetValue(userId, out profileName)) return profileName; if (EnabledProfiles.Contains(DefaultProfileName)) return DefaultProfileName; return null; } public void SetProfileSelected(string userId, string profileName) { SelectedProfiles[userId] = profileName; } } #endregion #endregion #region Configuration private class Configuration : BaseConfiguration { [JsonProperty("Debug", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool Debug = false; [JsonProperty("EnableDynamicMonuments")] [DefaultValue(true)] private bool DeprecatedEnableDynamicMonuments { set { EnableDynamicMonuments = value; } } [JsonProperty("Enable dynamic monuments", DefaultValueHandling = DefaultValueHandling.Ignore)] [DefaultValue(true)] public bool EnableDynamicMonuments = true; [JsonProperty("DebugDisplayDistance")] private float DeprecatedDebugDisplayDistance { set { DebugDisplayDistance = value; } } [JsonProperty("Debug display distance")] public float DebugDisplayDistance = 150; [JsonProperty("PersistEntitiesAfterUnload")] private bool DeprecatedEnableEntitySaving { set { EnableEntitySaving = value; } } [JsonProperty("Persist entities while the plugin is unloaded")] public bool EnableEntitySaving; [JsonProperty("DeployableOverrides")] public Dictionary DeprecatedDeployableOverrides { set { DeployableOverrides = value; } } [JsonProperty("Deployable overrides")] public Dictionary DeployableOverrides = new Dictionary { ["arcade.machine.chippy"] = "assets/bundled/prefabs/static/chippyarcademachine.static.prefab", ["autoturret"] = "assets/content/props/sentry_scientists/sentry.bandit.static.prefab", ["bbq"] = "assets/bundled/prefabs/static/bbq.static.prefab", ["boombox"] = "assets/prefabs/voiceaudio/boombox/boombox.static.prefab", ["box.repair.bench"] = "assets/bundled/prefabs/static/repairbench_static.prefab", ["cctv.camera"] = "assets/prefabs/deployable/cctvcamera/cctv.static.prefab", ["chair"] = "assets/bundled/prefabs/static/chair.static.prefab", ["computerstation"] = "assets/prefabs/deployable/computerstation/computerstation.static.prefab", ["connected.speaker"] = "assets/prefabs/voiceaudio/hornspeaker/connectedspeaker.deployed.static.prefab", ["hobobarrel"] = "assets/bundled/prefabs/static/hobobarrel_static.prefab", ["microphonestand"] = "assets/prefabs/voiceaudio/microphonestand/microphonestand.deployed.static.prefab", ["modularcarlift"] = "assets/bundled/prefabs/static/modularcarlift.static.prefab", ["research.table"] = "assets/bundled/prefabs/static/researchtable_static.prefab", ["samsite"] = "assets/prefabs/npc/sam_site_turret/sam_static.prefab", ["small.oil.refinery"] = "assets/bundled/prefabs/static/small_refinery_static.prefab", ["telephone"] = "assets/bundled/prefabs/autospawn/phonebooth/phonebooth.static.prefab", ["vending.machine"] = "assets/prefabs/deployable/vendingmachine/npcvendingmachine.prefab", ["wall.frame.shopfront.metal"] = "assets/bundled/prefabs/static/wall.frame.shopfront.metal.static.prefab", ["workbench1"] = "assets/bundled/prefabs/static/workbench1.static.prefab", ["workbench2"] = "assets/bundled/prefabs/static/workbench2.static.prefab", }; [JsonProperty("Xmas tree decorations (item shortnames)")] public string[] XmasTreeDecorations = { "xmas.decoration.baubels", "xmas.decoration.candycanes", "xmas.decoration.gingerbreadmen", "xmas.decoration.lights", "xmas.decoration.pinecone", "xmas.decoration.star", "xmas.decoration.tinsel", }; public void Init() { if (XmasTreeDecorations != null) { foreach (var itemShortName in XmasTreeDecorations) { var itemDefinition = ItemManager.FindItemDefinition(itemShortName); if (itemDefinition == null) { LogError(($"Invalid item short name in config: {itemShortName}")); continue; } if (itemDefinition.GetComponent() == null) { LogError(($"Item is not an Xmas tree decoration: {itemShortName}")); continue; } } } } } #region Configuration Helpers private class BaseConfiguration { public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary; } private static class JsonHelper { public static object Deserialize(string json) => ToObject(JToken.Parse(json)); private static object ToObject(JToken token) { switch (token.Type) { case JTokenType.Object: return token.Children() .ToDictionary(prop => prop.Name, prop => ToObject(prop.Value)); case JTokenType.Array: return token.Select(ToObject).ToList(); default: return ((JValue)token).Value; } } } private bool MaybeUpdateConfig(BaseConfiguration config) { var currentWithDefaults = config.ToDictionary(); var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value); return MaybeUpdateConfigDict(currentWithDefaults, currentRaw); } private bool MaybeUpdateConfigDict(Dictionary currentWithDefaults, Dictionary currentRaw) { bool changed = false; foreach (var key in currentWithDefaults.Keys) { object currentRawValue; if (currentRaw.TryGetValue(key, out currentRawValue)) { var defaultDictValue = currentWithDefaults[key] as Dictionary; var currentDictValue = currentRawValue as Dictionary; if (defaultDictValue != null) { if (currentDictValue == null) { currentRaw[key] = currentWithDefaults[key]; changed = true; } else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue)) { changed = true; } } } else { currentRaw[key] = currentWithDefaults[key]; changed = true; } } return changed; } protected override void LoadDefaultConfig() => _config = new Configuration(); protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { throw new JsonException(); } if (MaybeUpdateConfig(_config)) { LogWarning("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch (Exception e) { LogError(e.Message); LogWarning($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() { Log($"Configuration changes saved to {Name}.json"); Config.WriteObject(_config, true); } #endregion #endregion #region Localization private class LangEntry { public static List AllLangEntries = new List(); public static readonly LangEntry ErrorNoPermission = new LangEntry("Error.NoPermission", "You don't have permission to do that."); public static readonly LangEntry ErrorMonumentFinderNotLoaded = new LangEntry("Error.MonumentFinderNotLoaded", "Error: Monument Finder is not loaded."); public static readonly LangEntry ErrorNoMonuments = new LangEntry("Error.NoMonuments", "Error: No monuments found."); public static readonly LangEntry ErrorNotAtMonument = new LangEntry("Error.NotAtMonument", "Error: Not at a monument. Nearest is {0} with distance {1}"); public static readonly LangEntry ErrorNoSuitableAddonFound = new LangEntry("Error.NoSuitableAddonFound", "Error: No suitable addon found."); public static readonly LangEntry ErrorEntityNotEligible = new LangEntry("Error.EntityNotEligible", "Error: That entity is not managed by Monument Addons."); public static readonly LangEntry ErrorNoSpawnPointFound = new LangEntry("Error.NoSpawnPointFound", "Error: No spawn point found."); public static readonly LangEntry ErrorSetSyntaxGeneric = new LangEntry("Error.Set.Syntax.Generic", "Syntax: {0} set