C#:
using Facepunch;
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.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Text;
using UnityEngine;
using static IOEntity;
using static RecyclerConfig;
using static WireTool;
using Component = UnityEngine.Component;
using HumanNPCGlobal = global : : HumanNPC;
using SkullTrophyGlobal = global : : SkullTrophy;
using CustomInitializeCallback = System.Func<BasePlayer, string[], object>;
using CustomInitializeCallbackV2 = System.Func<BasePlayer, string[], System.ValueTuple<bool, object>>;
using CustomEditCallback = System.Func<BasePlayer, string[], UnityEngine.Component, Newtonsoft.Json.Linq.JObject, System.ValueTuple<bool, object>>;
using CustomSpawnCallback = System.Func<UnityEngine.Vector3, UnityEngine.Quaternion, Newtonsoft.Json.Linq.JObject, UnityEngine.Component>;
using CustomSpawnCallbackV2 = System.Func<System.Guid, UnityEngine.Component, UnityEngine.Vector3, UnityEngine.Quaternion, Newtonsoft.Json.Linq.JObject, UnityEngine.Component>;
using CustomCheckSpaceCallback = System.Func<UnityEngine.Vector3, UnityEngine.Quaternion, Newtonsoft.Json.Linq.JObject, bool>;
using CustomKillCallback = System.Action<UnityEngine.Component>;
using CustomUnloadCallback = System.Action<UnityEngine.Component>;
using CustomUpdateCallback = System.Action<UnityEngine.Component, Newtonsoft.Json.Linq.JObject>;
using CustomUpdateCallbackV2 = System.Func<UnityEngine.Component, Newtonsoft.Json.Linq.JObject, UnityEngine.Component>;
using CustomDisplayCallback = System.Action<UnityEngine.Component, Newtonsoft.Json.Linq.JObject, System.Text.StringBuilder>;
using CustomDisplayCallbackV2 = System.Action<UnityEngine.Component, Newtonsoft.Json.Linq.JObject, BasePlayer, System.Text.StringBuilder, float>;
using CustomSetDataCallback = System.Action<UnityEngine.Component, object>;
using Quaternion = UnityEngine.Quaternion;
using Tuple1 = System.ValueTuple<object>;
using Tuple2 = System.ValueTuple<object, object>;
using Tuple3 = System.ValueTuple<object, object, object>;
using Tuple4 = System.ValueTuple<object, object, object, object>;
using Vector2 = UnityEngine.Vector2;
using Vector3 = UnityEngine.Vector3;
namespace Oxide.Plugins {
[Info("Monument Addons", "WhiteThunder", "0.21.5")]
[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 Configuration _config;
private StoredData _data;
private ProfileStateData _profileStateData;
private SpawnedVehicleData _spawnedVehicleData;
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 CargoShipPrefab = "assets/content/vehicles/boats/cargoship/cargoshiptest.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 | Rust.Layers.Mask.Vehicle_World;
private static readonly Dictionary<string, string> JsonRequestHeaders = new Dictionary<string, string> {
{
"Content-Type", "application/json"
}
}
;
private static readonly Dictionary<string, string> OfficialProfileNames = new[] {
"BarnAirwolf", "CargoShipCCTV", "FishingVillageAirwolf", "MonumentCooking", "MonumentLifts", "MonumentsRecycler", "OilRigSharks", "OutpostAirwolf", "OutpostExtended", "SafeZoneRecyclers", "TrainStationCCTV",
}
.ToDictionary(name => name, name => name, StringComparer.OrdinalIgnoreCase);
private readonly HookCollection _dynamicMonumentHooks;
private readonly ProfileStore _profileStore = new ProfileStore();
private readonly OriginalProfileStore _originalProfileStore = new OriginalProfileStore();
private readonly ProfileManager _profileManager;
private readonly CoroutineManager _coroutineManager = new CoroutineManager();
private readonly AddonComponentTracker _componentTracker = new AddonComponentTracker();
private readonly AdapterListenerManager _adapterListenerManager;
private readonly ControllerFactory _controllerFactory;
private readonly CustomAddonManager _customAddonManager;
private readonly CustomMonumentManager _customMonumentManager;
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 SpawnGroupLimitManager _spawnGroupLimitManager;
private readonly ValueRotator<Color> _colorRotator = new(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 True = true;
private readonly object False = false;
private ItemDefinition _waterDefinition;
private ProtectionProperties _immortalProtection;
private ActionDebounced _saveProfileStateDebounced;
private ActionDebounced _saveSpawnedVehicleDataDebounced;
private StringBuilder _sb = new StringBuilder();
private HashSet<string> _deployablePrefabs = new();
private BaseEntity[] _entityBuffer = new BaseEntity[32];
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);
_customMonumentManager = new CustomMonumentManager(this);
_controllerFactory = new ControllerFactory(this);
_monumentHelper = new MonumentHelper(this);
_wireToolManager = new WireToolManager(this, _profileStore);
_spawnGroupLimitManager = new SpawnGroupLimitManager(this);
_saveProfileStateDebounced = new ActionDebounced(timer, 1, () => {
if (!_isLoaded) return;
_profileStateData.Save();
}
);
_saveSpawnedVehicleDataDebounced = new ActionDebounced(timer, 1, () => {
if (!_isLoaded) return;
_spawnedVehicleData.SaveIfDirty();
}
);
_dynamicMonumentHooks = new HookCollection(this, new[] {
nameof(OnEntitySpawned)
}
, () => _profileManager.HasAnyEnabledDynamicMonuments );
}
#endregion
#region Hooks
private void Init() {
_data = StoredData.Load(_profileStore);
_profileStateData = ProfileStateData.Load(_data);
_spawnedVehicleData = SpawnedVehicleData.Load();
_config.Init();
// Ensure the profile folder is created to avoid errors.
_profileStore.EnsureDefaultProfile();
permission.RegisterPermission(PermissionAdmin, this);
_dynamicMonumentHooks.Unsubscribe();
_adapterListenerManager.Init();
}
private void OnServerInitialized() {
_config.OnServerInitialized();
_waterDefinition = ItemManager.FindItemDefinition("water");
_immortalProtection = ScriptableObject.CreateInstance<ProtectionProperties>();
_immortalProtection.name = "MonumentAddonsProtection";
_immortalProtection.Add(1);
_uniqueNameRegistry.OnServerInitialized();
_adapterListenerManager.OnServerInitialized();
_monumentHelper.OnServerInitialized();
_ioManager.OnServerInitialized();
_spawnGroupLimitManager.OnServerInitialized();
var entitiesToKill = _profileStateData.CleanDisabledProfileState();
if (entitiesToKill.Count > 0) {
CoroutineManager.StartGlobalCoroutine(KillEntitiesRoutine(entitiesToKill));
}
if (CheckDependencies()) {
StartupRoutine();
}
_deployablePrefabs = DetermineAllDeployablePrefabs();
_profileManager.ProfileStatusChanged += (_, _, _) => _dynamicMonumentHooks.Refresh();
_serverInitialized = true;
}
private void Unload() {
_coroutineManager.Destroy();
_profileManager.UnloadAllProfiles();
_saveProfileStateDebounced.Flush();
_saveSpawnedVehicleDataDebounced.Flush();
_wireToolManager.Unload();
SpawnedVehicleComponent.DestroyAll();
UnityEngine.Object.Destroy(_immortalProtection);
_isLoaded = false;
}
private void OnNewSave() {
_profileStateData.Reset();
_spawnedVehicleData.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);
_customMonumentManager.UnregisterAllForPlugin(plugin);
}
private void OnEntitySpawned(BaseEntity entity) {
if (!IsDynamicMonument(entity)) return;
var entityForClosure = entity;
NextTick(() => {
if (entityForClosure == null || entityForClosure.IsDestroyed) return;
if (ExposedHooks.OnDynamicMonument(entityForClosure) is false) return;
var dynamicMonument = new DynamicMonument(entityForClosure);
_coroutineManager.StartCoroutine(_profileManager.PartialLoadForLateMonumentRoutine(dynamicMonument));
}
);
}
// This hook is exposed by plugin: Remover Tool (RemoverTool).
private object canRemove(BasePlayer player, BaseEntity entity) {
if (_componentTracker.IsAddonComponent(entity)) return False;
return null;
}
private object CanChangeGrade(BasePlayer player, BuildingBlock block, BuildingGrade.Enum grade) {
return ReturnFalseIfNoPermission(player, block);
}
private object CanUpdateSign(BasePlayer player, ISignage signage) {
return ReturnFalseIfNoPermission(player, signage as BaseEntity, notify : true);
}
private void OnSignUpdated(ISignage signage, BasePlayer player) {
if (!_componentTracker.IsAddonComponent(signage as BaseEntity, out SignController controller)) 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) {
if (!_componentTracker.IsAddonComponent(signage as BaseEntity, out SignController 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 (_componentTracker.IsAddonComponent(spray)) return False;
return null;
}
private object CanMannequinChangePose(Mannequin mannequin, BasePlayer player) {
return ReturnFalseIfNoPermission(player, mannequin, notify : true);
}
private object CanMannequinSwap(Mannequin mannequin, BasePlayer player) {
return ReturnFalseIfNoPermission(player, mannequin, notify : true);
}
// This hook is exposed by plugin: Entity Scale Manager (EntityScaleManager) v3.
private void OnEntityScaled(BaseEntity entity, Vector3 scale) {
if (!_componentTracker.IsAddonComponent(entity, out EntityAdapter adapter, out EntityController controller) || controller.EntityData.GetScale() == scale) return;
// Record any other pending updates.
adapter.TryRecordUpdates();
controller.EntityData.SetScale(scale);
controller.StartUpdateRoutine();
_profileStore.Save(controller.Profile);
}
// This hook is exposed by plugin: Entity Scale Manager (EntityScaleManager) v2.
private void OnEntityScaled(BaseEntity entity, float scale) {
OnEntityScaled(entity, Vector3.one * scale);
}
// This hook is exposed by plugin: Telekinesis.
private Component OnTelekinesisFindFailed(BasePlayer player) {
if (!HasAdminPermission(player)) return null;
return FindAdapter<TransformAdapter>(player).Adapter?.Component;
}
// This hook is exposed by plugin: Telekinesis.
private Tuple<Component, Component> OnTelekinesisStart(BasePlayer player, BaseEntity entity) {
if (!_componentTracker.IsAddonComponent(entity, out PasteAdapter adapter, out PasteController _)) return null;
return new Tuple<Component, Component>(adapter.Component, adapter.Component);
}
// This hook is exposed by plugin: Telekinesis.
private object CanStartTelekinesis(BasePlayer player, Component moveComponent, Component rotateComponent) {
if (IsTransformAddon(moveComponent, out _) && !HasAdminPermission(player)) return False;
return null;
}
// This hook is exposed by plugin: Telekinesis.
private void OnTelekinesisStarted(BasePlayer player, Component moveComponent, Component rotateComponent) {
if (!IsTransformAddon(moveComponent, out var adapter, out var controller) || controller is not IUpdateableController) return;
if (adapter.Component == moveComponent || adapter.Component == rotateComponent) {
_adapterDisplayManager.SetPlayerMovingAdapter(player, adapter);
}
_adapterDisplayManager.ShowAllRepeatedly(player);
if (moveComponent is BaseEntity moveEntity && IsSpawnPointEntity(moveEntity)) {
_adapterDisplayManager.ShowAllRepeatedly(player);
var spawnedVehicleComponent = moveComponent.GetComponent<SpawnedVehicleComponent>();
if (spawnedVehicleComponent != null) {
spawnedVehicleComponent.CancelInvoke(spawnedVehicleComponent.CheckPositionTracked);
}
}
}
// This hook is exposed by plugin: Telekinesis.
private void OnTelekinesisStopped(BasePlayer player, Component moveComponent, Component rotateComponent) {
if (!IsTransformAddon(moveComponent, out TransformAdapter transformAdapter, out BaseController controller) || controller is not IUpdateableController updateableController) return;
if (player != null) {
_adapterDisplayManager.SetPlayerMovingAdapter(player, null);
}
if (!transformAdapter.TryRecordUpdates(moveComponent.transform, rotateComponent.transform)) return;
if (moveComponent is CustomSpawnPoint spawnPoint) {
spawnPoint.MoveSpawnedInstances();
}
updateableController.StartUpdateRoutine();
_profileStore.Save(controller.Profile);
if (player != null) {
_adapterDisplayManager.ShowAllRepeatedly(player);
ChatMessage(player, LangEntry.SaveSuccess, controller.Adapters.Count, controller.Profile.Name);
}
}
// This hook is exposed by plugin: Custom Vending Setup (CustomVendingSetup).
private Dictionary<string, object> OnCustomVendingSetupDataProvider(NPCVendingMachine vendingMachine) {
if (!_componentTracker.IsAddonComponent(vendingMachine, out EntityController 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 Vector3 GetEntityScale(BaseEntity entity) {
if (EntityScaleManager == null) return Vector3.one;
var entityScale = EntityScaleManager.Call("API_GetScale", entity);
if (entityScale is float floatScale) return Vector3.one * floatScale;
if (entityScale is Vector3 vector3Scale) return vector3Scale;
return Vector3.one;
}
private bool IsEntityScaled(BaseEntity entity) {
return GetEntityScale(entity) != Vector3.one;
}
private bool TryScaleEntity(BaseEntity entity, Vector3 scale) {
if (EntityScaleManager == null) return false;
if (EntityScaleManager.Version.Major < 3) return EntityScaleManager.Call("API_ScaleEntity", entity, scale.x) is true;
return EntityScaleManager.Call("API_ScaleEntity", entity, scale) is true;
}
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<BaseEntity> onEntityPasted, Action onPasteCompleted) {
if (copyPaste == null) return null;
var args = CopyPasteArgs;
if (pasteData.Args is {
Length : > 0
}
) {
args = args.Concat(pasteData.Args).ToArray();
}
var result = copyPaste.Call("TryPasteFromVector3Cancellable", position, yRotation, pasteData.Filename, args, onPasteCompleted, onEntityPasted);
if (!(result is ValueTuple<object, Action>)) {
LogError($"CopyPaste returned an unexpected response for paste \"{pasteData.Filename}\": {result}. Is CopyPaste up-to-date?");
return null;
}
var pasteResult = (ValueTuple<object, Action>)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 API
[HookMethod(nameof(API_IsMonumentEntity))]
public object API_IsMonumentEntity(BaseEntity entity) {
return ObjectCache.Get(_componentTracker.IsAddonComponent(entity));
}
[HookMethod(nameof(API_GetMonumentEntityGuid))]
public object API_GetMonumentEntityGuid(BaseEntity entity) {
if (!_componentTracker.IsAddonComponent(entity)) return null;
var adapter = AddonComponent.GetForComponent(entity).Adapter;
if (adapter == null) return null;
return ObjectCache.Get(adapter.Data.Id);
}
[HookMethod(nameof(API_RegisterCustomAddon))]
public Dictionary<string, object> API_RegisterCustomAddon(Plugin plugin, string addonName, Dictionary<string, object> addonSpec) {
var addonDefinition = CustomAddonDefinition.FromDictionary(addonName, plugin, addonSpec);
if (!addonDefinition.Validate()) return null;
if (_customAddonManager.IsRegistered(addonName, out var otherPlugin)) {
if (otherPlugin.Name != plugin.Name) {
LogError($"Unable to register custom addon \"{addonName}\" for plugin {plugin.Name} because it's already been registered by plugin {otherPlugin.Name}.");
return null;
}
}
else {
_customAddonManager.RegisterAddon(addonDefinition);
}
return addonDefinition.ToApiResult(_profileStore);
}
[HookMethod(nameof(API_RegisterCustomMonument))]
public object API_RegisterCustomMonument(Plugin plugin, string monumentName, Component component, Bounds bounds) {
var objectType = component is BaseEntity ? "entity" : "object";
if (plugin == null) {
LogError($"A plugin has attempted to register an {objectType} as a custom monument, but the plugin did not identify itself.");
return False;
}
if (String.IsNullOrWhiteSpace(monumentName)) {
LogError($"Plugin {plugin.Name} tried to register an {objectType} as a custom monument, but did not provide a valid monument name.");
return False;
}
if (component == null || (component is BaseEntity {
IsDestroyed : true
}
)) {
LogError($"Plugin {plugin.Name} tried to register a null or destroyed {objectType} as a custom monument.");
return False;
}
if (bounds == default) {
LogWarning($"Plugin {plugin.Name} tried to register an {objectType} as a custom monument, but did not provide bounds. This was most likely a mistake by the developer of {plugin.Name} ({plugin.Author}).");
}
var existingMonument = _customMonumentManager.FindByComponent(component);
if (existingMonument != null) {
if (existingMonument.OwnerPlugin.Name != plugin.Name) {
LogError($"Plugin {plugin.Name} tried to register an {objectType} at {component.transform.position} as a custom monument with name '{monumentName}', but that {objectType} was already registered by plugin {existingMonument.OwnerPlugin.Name} with name '{existingMonument.UniqueName}'.");
return False;
}
else if (existingMonument.UniqueName != monumentName) {
LogError($"Plugin {plugin.Name} tried to register an {objectType} at {component.transform.position} as a custom monument with name '{monumentName}', but that {objectType} was already registered with name '{existingMonument.UniqueName}'.");
return False;
}
else if (existingMonument.Bounds != bounds) {
// Changing the bounds is permitted.
existingMonument.Bounds = bounds;
return True;
}
else {
LogWarning($"Plugin {plugin.Name} tried to double register a monument '{monumentName}'. This is OK but may be a mistake by the developer of {plugin.Name} ({plugin.Author}).");
return True;
}
}
var monument = component is BaseEntity entity ? new CustomEntityMonument(plugin, entity, monumentName, bounds) : new CustomMonument(plugin, component, monumentName, bounds);
_customMonumentManager.Register(monument);
CustomMonumentComponent.AddToMonument(_customMonumentManager, monument);
_coroutineManager.StartCoroutine(_profileManager.PartialLoadForLateMonumentRoutine(monument));
LogInfo($"Plugin {plugin.Name} successfully registered an {objectType} at {component.transform.position} as a custom monument with name '{monumentName}'.");
return True;
}
[HookMethod(nameof(API_UnregisterCustomMonument))]
public object API_UnregisterCustomMonument(Plugin plugin, Component component) {
var objectType = component is BaseEntity ? "entity" : "object";
if (component == null || (component is BaseEntity {
IsDestroyed : true
}
)) {
LogWarning($"Plugin {plugin.Name} tried to unregister a null or destroyed {objectType} as a custom monument. This is not necessary because {Name} automatically detects when custom monuments are destroyed and despawns associated addons. This is OK but may be a mistake by the developer of {plugin.Name} ({plugin.Author}).");
return True;
}
var existingMonument = _customMonumentManager.FindByComponent(component);
if (existingMonument == null) {
LogError($"Plugin {plugin.Name} tried to unregister an {objectType} at {component.transform.position} as a custom monument, but that {objectType} was not currently registered as a custom monument. Either the {objectType} was unregistered earlier, or the wrong {objectType} was provided. This was most likely a mistake by the developer of {plugin.Name} ({plugin.Author}).");
return True;
}
if (existingMonument.OwnerPlugin.Name != plugin.Name) {
LogError($"Plugin {plugin.Name} tried to unregister an {objectType} at {component.transform.position} as a custom monument, but that {objectType} was registered as a custom monument by plugin {existingMonument.OwnerPlugin.Name}, so this was not allowed.");
return False;
}
_customMonumentManager.Unregister(existingMonument);
LogInfo($"Plugin {plugin.Name} successfully unregistered an {objectType} at {component.transform.position} as a custom monument with name '{existingMonument.UniqueName}'.");
return True;
}
#endregion
#region Exposed Hooks
private static class ExposedHooks {
public static void OnMonumentAddonsInitialized() {
Interface.CallHook("OnMonumentAddonsInitialized");
}
public static void OnMonumentEntitySpawned(BaseEntity entity, Component monument, Guid guid) {
Interface.CallHook("OnMonumentEntitySpawned", entity, monument, ObjectCache.Get(guid));
}
public static void OnMonumentPrefabCreated(GameObject gameObject, Component monument, Guid guid) {
Interface.CallHook("OnMonumentPrefabCreated", gameObject, monument, ObjectCache.Get(guid));
}
public static object OnDynamicMonument(BaseEntity entity) {
return Interface.CallHook("OnDynamicMonument", entity);
}
}
#endregion
#region Commands
private enum PuzzleOption {
PlayersBlockReset, PlayerDetectionRadius, SecondsBetweenResets,
}
private enum SpawnGroupOption {
Name, Color, 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) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyMonumentFinderLoaded(player) || !VerifyProfileSelected(player, out var profileController) || !VerifyValidEntityPrefabOrDeployable(player, args, out var prefabName, out var addonDefinition, out var skinId) || !VerifyLookingAtMonumentPosition(player, out var position, out var monument)) return;
DetermineLocalTransformData(position, basePlayer, monument, out var localPosition, out var localRotationAngles, out var 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 (Mathf.Approximately(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 = _config.AddonDefaults.Puzzles.ApplyTo(new PuzzleData());
}
else if (shortPrefabName == "mannequin_deployed") {
entityData.MannequinData = MannequinData.FromPlayer(basePlayer);
}
addonData = entityData;
}
else {
// Found a custom addon definition.
if (!addonDefinition.TryInitialize(basePlayer, args.Skip(1).ToArray(), out var pluginData)) return;
addonData = new CustomAddonData {
Id = Guid.NewGuid(), AddonName = addonDefinition.AddonName, Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain,
}
.SetData(pluginData);
}
var matchingMonuments = GetMonumentsByIdentifier(monument.UniqueName);
profileController.Profile.AddData(monument.UniqueName, addonData);
_profileStore.Save(profileController.Profile);
profileController.SpawnNewData(addonData, matchingMonuments);
ReplyToPlayer(player, LangEntry.SpawnSuccess, matchingMonuments.Count, profileController.Profile.Name, monument.UniqueDisplayName);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer);
if (addonData is not CustomAddonData && ShouldRecommendSpawnPoints(prefabName)) {
ReplyToPlayer(player, LangEntry.WarningRecommendSpawnPoint);
}
}
[Command("maedit")]
private void CommandEdit(IPlayer player, string cmd, string[] args) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out CustomAddonAdapter adapter, out CustomAddonController controller, LangEntry.ErrorNoCustomAddonFound)) return;
if (args.Length == 0) {
ReplyToPlayer(player, LangEntry.EditSynax);
return;
}
var addonDefinition = adapter.AddonDefinition;
var addonName = addonDefinition.AddonName;
if (addonName != args[0]) {
ReplyToPlayer(player, LangEntry.EditErrorNoMatch, args[0]);
return;
}
if (!addonDefinition.SupportsEditing) {
ReplyToPlayer(player, LangEntry.EditErrorNotEditable, addonName);
return;
}
if (!addonDefinition.TryEdit(basePlayer, args.Skip(1).ToArray(), adapter.Component, adapter.CustomAddonData.GetSerializedData(), out var data)) return;
addonDefinition.SetData(_profileStore, controller, data);
ReplyToPlayer(player, LangEntry.EditSuccess, addonName);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer);
}
[Command("maprefab")]
private void CommandPrefab(IPlayer player, string cmd, string[] args) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyMonumentFinderLoaded(player) || !VerifyProfileSelected(player, out var profileController) || !VerifyValidModderPrefab(player, args, out var prefabName) || !VerifyLookingAtMonumentPosition(player, out var position, out var monument)) return;
if (FindPrefabBaseEntity(prefabName) != null) {
ReplyToPlayer(player, LangEntry.PrefabErrorIsEntity, prefabName);
return;
}
DetermineLocalTransformData(position, basePlayer, monument, out var localPosition, out var localRotationAngles, out var isOnTerrain);
var prefabData = new PrefabData {
Id = Guid.NewGuid(), PrefabName = prefabName, Position = localPosition, RotationAngles = localRotationAngles, SnapToTerrain = isOnTerrain,
}
;
var matchingMonuments = GetMonumentsByIdentifier(monument.UniqueName);
profileController.Profile.AddData(monument.UniqueName, prefabData);
_profileStore.Save(profileController.Profile);
profileController.SpawnNewData(prefabData, matchingMonuments);
ReplyToPlayer(player, LangEntry.PrefabSuccess, matchingMonuments.Count, profileController.Profile.Name, monument.UniqueDisplayName);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer);
}
[Command("masave")]
private void CommandSave(IPlayer player, string cmd, string[] args) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out EntityAdapter adapter, out EntityController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
if (!adapter.TryRecordUpdates()) {
ReplyToPlayer(player, LangEntry.SaveNothingToDo);
return;
}
controller.StartUpdateRoutine();
_profileStore.Save(controller.Profile);
ReplyToPlayer(player, LangEntry.SaveSuccess, controller.Adapters.Count, controller.Profile.Name);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer);
}
[Command("makill")]
private void CommandKill(IPlayer player, string cmd, string[] args) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out TransformAdapter adapter, out BaseController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
// Capture adapter count before killing the controller.
var numAdapters = controller.Adapters.Count;
controller.Profile.RemoveData(adapter.Data, out var monumentIdentifier);
_dynamicMonumentHooks.Refresh();
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);
}
if (controller.Data is SpawnGroupData spawnGroupData && adapter.Data is SpawnPointData spawnPointData) {
_undoManager.AddUndo(basePlayer, new UndoKillSpawnPoint(this, profileController, monumentIdentifier, spawnGroupData, spawnPointData));
}
else {
_undoManager.AddUndo(basePlayer, new UndoKill(this, profileController, monumentIdentifier, 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) {
if (!VerifyPlayer(player, out var 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) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player)) return;
if (args.Length < 1 || !ComputerStation.IsValidIdentifier(args[0])) {
ReplyToPlayer(player, LangEntry.CCTVSetIdSyntax, cmd);
return;
}
if (!VerifyLookingAtAdapter(player, out CCTVController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
controller.EntityData.CCTV ??= new CCTVInfo();
var hadIdentifier = !string.IsNullOrEmpty(controller.EntityData.CCTV.RCIdentifier);
controller.EntityData.CCTV.RCIdentifier = args[0];
_profileStore.Save(controller.Profile);
controller.StartUpdateRoutine();
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) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out CCTVEntityAdapter adapter, out CCTVController 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);
controller.EntityData.CCTV ??= new CCTVInfo();
controller.EntityData.CCTV.Pitch = lookAngles.x;
controller.EntityData.CCTV.Yaw = lookAngles.y;
_profileStore.Save(controller.Profile);
controller.StartUpdateRoutine();
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 (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out EntityAdapter adapter, out EntityController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
var skullTrophy = adapter.Entity as SkullTrophyGlobal;
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.StartUpdateRoutine();
ReplyToPlayer(player, LangEntry.SkullNameSetSuccess, skullName, controller.Adapters.Count, controller.Profile.Name);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate : !updatedSkullName);
}
[Command("matrophy")]
private void CommandTrophy(IPlayer player, string cmd, string[] args) {
T GetSubEntity<T>(Item item) where T : BaseEntity {
var entityId = item.instanceData?.subEntity ?? new NetworkableId(0);
if (entityId.Value == 0) return null;
return BaseNetworkable.serverEntities.Find(entityId) as T;
}
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out EntityAdapter adapter, out EntityController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
var huntingTrophy = adapter.Entity as HuntingTrophy;
if (huntingTrophy == null) {
ReplyToPlayer(player, LangEntry.ErrorNoSuitableAddonFound);
return;
}
var heldItem = basePlayer.GetActiveItem();
var headEntity = heldItem != null ? GetSubEntity<HeadEntity>(heldItem) : null;
if (headEntity == null) {
ReplyToPlayer(player, LangEntry.SetHeadNoHeadItem);
return;
}
if (!huntingTrophy.CanSubmitHead(headEntity)) {
ReplyToPlayer(player, LangEntry.SetHeadMismatch);
return;
}
controller.EntityData.HeadData = HeadData.FromHeadEntity(headEntity);
_profileStore.Save(controller.Profile);
controller.StartUpdateRoutine();
ReplyToPlayer(player, LangEntry.SetHeadSuccess, controller.Adapters.Count, controller.Profile.Name);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer);
}
[Command("maskin")]
private void CommandSkin(IPlayer player, string cmd, string[] args) {
if (!VerifyPlayer(player, out var basePlayer) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out EntityAdapter adapter, out EntityController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
if (args.Length == 0) {
ReplyToPlayer(player, LangEntry.SkinGet, adapter.Entity.skinID, cmd);
return;
}
if (!ulong.TryParse(args[0], out var skinId)) {
ReplyToPlayer(player, LangEntry.SkinSetSyntax, cmd);
return;
}
if (IsRedirectSkin(skinId, out var 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.StartUpdateRoutine();
ReplyToPlayer(player, LangEntry.SkinSetSuccess, skinId, controller.Adapters.Count, controller.Profile.Name);
_adapterDisplayManager.ShowAllRepeatedly(basePlayer, immediate : !updatedExistingSkin);
}
[Command("maflag")]
private void CommandFlag(IPlayer player, string cmd, string[] args) {
if (!VerifyPlayer(player, out _) || !VerifyHasPermission(player) || !VerifyLookingAtAdapter(player, out EntityAdapter adapter, out EntityController controller, LangEntry.ErrorNoSuitableAddonFound)) return;
if (args.Length == 0) {
var notAplicableMessage = GetMessage(player.Id, LangEntry.NotApplicable);
var currentFlags = adapter.Entity.flags == 0 ? notAplicableMessage : adapter.Entity.flags.ToString();
var enabledFlags = adapter.EntityData.EnabledFlags == 0 ? notAplicableMessage : ada
// ... [Code preview truncated due to length. Full source in attached file] ...