using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using Oxide.Core; using Rust.AI; using UnityEngine; using VLB; using static SamSite; using HumanNpc = global::HumanNPC; namespace Oxide.Plugins { [Info("Targetable Drones", "WhiteThunder", "1.2.6")] [Description("Allows RC drones to be targeted by Auto Turrets and SAM Sites.")] internal class TargetableDrones : CovalencePlugin { #region Fields [PluginReference] private readonly Plugin Clans, Friends, DroneScaleManager, IQGuardianDrone; private const string PermissionUntargetable = "targetabledrones.untargetable"; private const BaseEntity.Flags UntargetableFlag = BaseEntity.Flags.Protected; private Configuration _config; private readonly object False = false; private float? SqrScanRadius; #endregion #region Hooks private void Init() { permission.RegisterPermission(PermissionUntargetable, this); Unsubscribe(nameof(OnEntitySpawned)); if (!_config.EnableSamTargeting) { Unsubscribe(nameof(OnSamSiteTargetScan)); Unsubscribe(nameof(OnSamSiteTarget)); } if (!_config.EnableTurretTargeting) { Unsubscribe(nameof(OnEntityEnter)); Unsubscribe(nameof(OnTurretTarget)); Unsubscribe(nameof(OnDroneScaled)); } } private void OnServerInitialized() { if (_config.OnServerInitialized() && !_config.UsingDefaults) { SaveConfig(); } foreach (var entity in BaseNetworkable.serverEntities) { var drone = entity as Drone; if (drone == null || !IsDroneEligible(drone)) continue; OnEntitySpawned(drone); } Subscribe(nameof(OnEntitySpawned)); if (!_config.EnableTurretTargeting && !_config.NPCTargetingSettings.DamageMultiplierEnabled) { Unsubscribe(nameof(OnEntityTakeDamage)); } } private void Unload() { foreach (var entity in BaseNetworkable.serverEntities) { var drone = entity as Drone; if (drone == null || !IsDroneEligible(drone)) continue; if (_config.EnableTurretTargeting) { TurretTargetComponent.RemoveFromDrone(this, drone); } if (_config.EnableSamTargeting) { SAMTargetComponent.RemoveFromDrone(drone); } if (_config.NPCTargetingSettings.Enabled) { NPCTargetComponent.RemoveFromDrone(drone); } } // Just in case since this is static. SAMTargetComponent.DroneComponents.Clear(); } private void OnEntitySpawned(Drone drone) { if (!IsDroneEligible(drone)) return; if (_config.EnableTurretTargeting) { TurretTargetComponent.AddToDroneIfMissing(this, drone); } if (_config.EnableSamTargeting) { SAMTargetComponent.AddToDroneIfMissing(this, drone); } if (_config.NPCTargetingSettings.Enabled) { NPCTargetComponent.AddToDrone(this, drone); } } // Avoid unwanted trigger interactions. private object OnEntityEnter(TriggerBase trigger, Drone drone) { if (trigger is PlayerDetectionTrigger) { // Only allow interaction with Laser Detectors. // This avoids NREs with HBHF sensors or anything unknown. if (trigger.GetComponentInParent() is LaserDetector) return null; return False; } if (trigger is TargetTrigger) { // Only allow interaction with Auto Turrets. // This avoids NREs with flame turrets, shotgun traps, tesla coils, or anything unknown. if (trigger.GetComponentInParent() is AutoTurret) return null; return False; } return null; } private static ulong GetDroneControllerOrOwnerId(Drone drone) { var controllerSteamId = drone.ControllingViewerId?.SteamId ?? 0; if (controllerSteamId != 0) return controllerSteamId; var droneTurret = GetDroneTurret(drone); if ((object)droneTurret != null) { controllerSteamId = droneTurret.ControllingViewerId?.SteamId ?? 0; if (controllerSteamId != 0) return controllerSteamId; var turretOwnerId = droneTurret.OwnerID; if (turretOwnerId != 0) return turretOwnerId; } return drone.OwnerID; } private object OnTurretTarget(AutoTurret turret, Drone drone) { if (drone == null || drone.IsDestroyed) return null; if (!ShouldTurretTargetDrone(turret, drone)) return False; return null; } private void OnSamSiteTargetScan(SamSite samSite, List targetList) { if (SAMTargetComponent.DroneComponents.Count == 0) return; var samSitePosition = samSite.transform.position; SqrScanRadius ??= Mathf.Pow(targetTypeVehicle.scanRadius, 2); foreach (var droneComponent in SAMTargetComponent.DroneComponents) { if (droneComponent.Drone.HasFlag(UntargetableFlag)) continue; // Distance checking is way more efficient than collider checking, even with hundreds of drones. if ((samSitePosition - droneComponent.Position).sqrMagnitude <= SqrScanRadius.Value) { targetList.Add(droneComponent); } } } private object OnSamSiteTarget(SamSite samSite, SAMTargetComponent droneComponent) { var drone = droneComponent.Drone; if (drone == null || drone.IsDestroyed) return null; if (!ShouldSamSiteTargetDrone(samSite, drone) || ExposedHooks.OnSamSiteTarget(samSite, drone) is false) return False; return null; } private void OnEntityTakeDamage(Drone drone, HitInfo hitInfo) { if (hitInfo == null) return; // Multiply damage taken by NPCs. var humanNpc = hitInfo.Initiator as HumanNpc; if ((object)humanNpc != null) { if (!_config.NPCTargetingSettings.DamageMultiplierEnabled || !_config.NPCTargetingSettings.IsAllowed(humanNpc)) return; hitInfo.damageTypes.ScaleAll(_config.NPCTargetingSettings.DamageMultiplier); return; } // Make drone turrets retaliate against other turrets. var turret = hitInfo.Initiator as AutoTurret; if ((object)turret != null) { if (turret == null || turret.IsDestroyed) return; // Ignore if this drone does not have a turret since it can't retaliate. var droneTurret = GetDroneTurret(drone); if (droneTurret == null) return; // Ignore if the turret damaged its owner drone. if (droneTurret == turret) return; // Ignore if the turret is not online or has an existing visible target. if (!droneTurret.IsOnline() || droneTurret.HasTarget() && droneTurret.targetVisible) return; var attackerDrone = GetParentDrone(turret); if (attackerDrone != null) { // If the attacker turret is on a drone, target that drone. droneTurret.SetTarget(attackerDrone); return; } // Shoot back at the turret. droneTurret.SetTarget(turret); return; } } private void OnDroneScaled(Drone drone, BaseEntity rootEntity, float scale, float previousScale) { if (scale == 1 || rootEntity.IsDestroyed) return; TurretTargetComponent.AddToRootEntityIfMissing(drone, rootEntity); } #endregion #region Exposed Hooks private static class ExposedHooks { public static object OnSamSiteTarget(SamSite samSite, Drone drone) { return Interface.CallHook("OnSamSiteTarget", samSite, drone); } } #endregion #region Helper Methods public static void LogInfo(string message) => Interface.Oxide.LogInfo($"[Targetable Drones] {message}"); public static void LogWarning(string message) => Interface.Oxide.LogWarning($"[Targetable Drones] {message}"); public static void LogError(string message) => Interface.Oxide.LogError($"[Targetable Drones] {message}"); private static bool SameTeam(ulong userId, ulong otherUserId) { return RelationshipManager.ServerInstance.FindPlayersTeam(userId)?.members.Contains(otherUserId) ?? false; } private static bool IsDroneEligible(Drone drone) { return drone is not DeliveryDrone; } private static Drone GetParentDrone(BaseEntity entity) { var sphereEntity = entity.GetParentEntity() as SphereEntity; return sphereEntity != null ? sphereEntity.GetParentEntity() as Drone : null; } private static AutoTurret GetDroneTurret(Drone drone) { return drone.GetSlot(BaseEntity.Slot.UpperModifier) as AutoTurret; } private static void RemoveFromAutoTurretTriggers(BaseEntity entity) { if (entity.triggers == null || entity.triggers.Count == 0) return; foreach (var trigger in entity.triggers.ToArray()) { if (trigger is not TargetTrigger) continue; var autoTurret = trigger.gameObject.ToBaseEntity() as AutoTurret; if (autoTurret != null && autoTurret.targetTrigger == trigger) { trigger.RemoveEntity(entity); } } } private bool IsTargetableIQGuardianDrone(Drone drone) { return IQGuardianDrone?.Call("IsValidDrone", drone) is true; } private bool IsDroneBeingControlled(Drone drone) { return !drone.IsBeingControlled && IsTargetableIQGuardianDrone(drone); } private bool IsPlayerTargetExempt(ulong userId) { return userId != 0 && permission.UserHasPermission(userId.ToString(), PermissionUntargetable); } private bool IsTargetExempt(Drone drone) { return drone.HasFlag(UntargetableFlag) || drone.isGrounded || drone.InSafeZone() || IsPlayerTargetExempt(GetDroneControllerOrOwnerId(drone)); } private BaseEntity GetRootEntity(Drone drone) { return DroneScaleManager?.Call("API_GetRootEntity", drone) as BaseEntity; } private bool HasFriend(ulong userId, ulong otherUserId) { return Friends?.Call("HasFriend", userId, otherUserId) is true; } private bool SameClan(ulong userId, ulong otherUserId) { return Clans?.Call("IsClanMember", userId.ToString(), otherUserId.ToString()) is true; } private bool AreAllies(ulong userId, ulong otherUserId) { return Clans?.Call("IsAllyPlayer", userId.ToString(), otherUserId.ToString()) is true; } private bool ShouldTurretTargetDrone(AutoTurret turret, Drone drone) { // Drones are not inherently hostile. // TODO: Revisit this for player-deployed sentry turrets if (turret is NPCAutoTurret) return false; if (IsTargetExempt(drone)) return false; // Don't allow a drone turret to target its parent drone. if (GetParentDrone(turret) == drone) return false; // Skip auth/team/friends/clan logic for drones that have no apparent controller/owner (likely NPC drones). var droneControllerOrOwnerId = GetDroneControllerOrOwnerId(drone); if (droneControllerOrOwnerId == 0) return true; // Direct authorization trumps anything else. if (turret.IsAuthed(droneControllerOrOwnerId)) return false; // In case the owner lost authorization, don't share with team/friends/clan. if (turret.OwnerID == 0 || !turret.IsAuthed(turret.OwnerID)) return true; if (turret.OwnerID == droneControllerOrOwnerId || _config.DefaultSharingSettings.Team && SameTeam(turret.OwnerID, droneControllerOrOwnerId) || _config.DefaultSharingSettings.Friends && HasFriend(turret.OwnerID, droneControllerOrOwnerId) || _config.DefaultSharingSettings.Clan && SameClan(turret.OwnerID, droneControllerOrOwnerId) || _config.DefaultSharingSettings.Allies && AreAllies(turret.OwnerID, droneControllerOrOwnerId)) return false; return true; } private bool ShouldSamSiteTargetDrone(SamSite samSite, Drone drone) { if (samSite.staticRespawn || samSite.OwnerID == 0) return true; var droneOwnerId = GetDroneControllerOrOwnerId(drone); if (droneOwnerId == 0) return true; if (samSite.OwnerID == droneOwnerId || _config.DefaultSharingSettings.Team && SameTeam(samSite.OwnerID, droneOwnerId) || _config.DefaultSharingSettings.Friends && HasFriend(samSite.OwnerID, droneOwnerId) || _config.DefaultSharingSettings.Clan && SameClan(samSite.OwnerID, droneOwnerId) || _config.DefaultSharingSettings.Allies && AreAllies(samSite.OwnerID, droneOwnerId)) return false; return true; } #endregion #region Custom Targeting private class NPCTargetTriggerComponent : TriggerBase { public static NPCTargetTriggerComponent AddToDrone(TargetableDrones plugin, Drone drone, GameObject host) { var component = host.AddComponent(); component._plugin = plugin; component._drone = drone; component.interestLayers = Rust.Layers.Mask.Player_Server; return component; } private const int LayerMask = Rust.Layers.Mask.Default | Rust.Layers.Mask.Vehicle_Detailed | Rust.Layers.Mask.World | Rust.Layers.Mask.Construction | Rust.Layers.Mask.Terrain | Rust.Layers.Mask.Vehicle_Large | Rust.Layers.Mask.Tree; private TargetableDrones _plugin; private Drone _drone; private Action _checkTriggerContents; private NPCTargetTriggerComponent() { _checkTriggerContents = CheckTriggerContents; } public void CheckTriggerContents() { if (!HasAnyEntityContents) { CancelInvoke(_checkTriggerContents); return; } foreach (var entity in entityContents) { var humanNpc = entity as HumanNpc; if (humanNpc == null) continue; var memory = GetMemory(entity, out var senses); if (memory == null) continue; if (IsTargetableBy(humanNpc)) { AddToMemory(memory, senses); } else { RemoveFromMemory(memory); } } } public override GameObject InterestedInObject(GameObject obj) { obj = base.InterestedInObject(obj); if (obj == null) return null; var humanNpc = obj.ToBaseEntity() as HumanNpc; if (humanNpc == null || GetMemory(humanNpc) == null) return null; if (!_plugin._config.NPCTargetingSettings.IsAllowed(humanNpc)) return null; return humanNpc.gameObject; } public override void OnEntityEnter(BaseEntity entity) { base.OnEntityEnter(entity); if (!HasAnyEntityContents || !entityContents.Contains(entity)) return; if (!IsInvoking(_checkTriggerContents)) { InvokeRepeating(_checkTriggerContents, UnityEngine.Random.Range(0, 1), 1); } } public override void OnEntityLeave(BaseEntity entity) { base.OnEntityLeave(entity); var memory = GetMemory(entity); if (memory != null) { RemoveFromMemory(memory); } } private SimpleAIMemory GetMemory(BaseEntity entity, out AIBrainSenses senses) { var brain = (entity as HumanNpc)?.Brain; senses = brain?.Senses; return senses?.Memory; } private SimpleAIMemory GetMemory(BaseEntity entity) { return GetMemory(entity, out _); } private bool IsTargetableBy(HumanNpc humanNpc) { if (!_plugin.IsDroneBeingControlled(_drone)) return false; var eyesPosition = humanNpc.isMounted ? humanNpc.eyes.worldMountedPosition : humanNpc.IsDucked() ? humanNpc.eyes.worldCrouchedPosition : !humanNpc.IsCrawling() ? humanNpc.eyes.worldStandingPosition : humanNpc.eyes.worldCrawlingPosition; var layerMask = LayerMask; if (humanNpc.AdditionalLosBlockingLayer != 0) { layerMask |= 1 << humanNpc.AdditionalLosBlockingLayer; } return humanNpc.IsVisibleSpecificLayers(_drone.CenterPoint(), eyesPosition, layerMask); } private bool AddToMemory(SimpleAIMemory memory, AIBrainSenses senses) { if (_drone.ControllingViewerId.HasValue && _plugin.IsPlayerTargetExempt(_drone.ControllingViewerId.Value.SteamId)) return false; if (!memory.LOS.Add(_drone)) return false; senses.LastThreatTimestamp = Time.realtimeSinceStartup; memory.All.Add(new SimpleAIMemory.SeenInfo { Entity = _drone, Position = _drone.transform.position, Timestamp = Time.realtimeSinceStartup, }); memory.Players.Add(_drone); memory.Targets.Add(_drone); memory.Threats.Add(_drone); return true; } private bool RemoveFromMemory(SimpleAIMemory memory) { if (!memory.LOS.Remove(_drone)) return false; for (var i = 0; i < memory.All.Count; i++) { var seenInfo = memory.All[i]; if (seenInfo.Entity == _drone) { memory.All.RemoveAt(i); break; } } memory.Players.Remove(_drone); memory.Targets.Remove(_drone); memory.Threats.Remove(_drone); return true; } private void OnDestroy() { if (HasAnyEntityContents) { foreach (var entity in entityContents) { var memory = GetMemory(entity); if (memory == null) continue; RemoveFromMemory(memory); } } } } private class NPCTargetComponent : FacepunchBehaviour { public static void AddToDrone(TargetableDrones plugin, Drone drone) { var component = drone.gameObject.AddComponent(); var child = drone.gameObject.CreateChild(); child.layer = (int)Rust.Layer.Trigger; // HACK: Prevent the drone's sweep test from using incorporating the child collider. child.AddComponent().isKinematic = true; var collider = child.AddComponent(); collider.isTrigger = true; collider.radius = plugin._config.NPCTargetingSettings.MaxRange; component._trigger = NPCTargetTriggerComponent.AddToDrone(plugin, drone, child); } public static void RemoveFromDrone(Drone drone) { DestroyImmediate(drone.gameObject.GetComponent()); } private NPCTargetTriggerComponent _trigger; private void OnDestroy() { if (_trigger != null) { Destroy(_trigger.gameObject); } } } private class TurretTargetComponent : EntityComponent { public static void AddToRootEntityIfMissing(Drone drone, BaseEntity rootEntity) { rootEntity.GetOrAddComponent().InitForDrone(drone); } public static void AddToDroneIfMissing(TargetableDrones plugin, Drone drone) { // Must be added to the drone itself since the root entity (SphereEntity) is not a BaseCombatEntity. drone.GetOrAddComponent().InitForDrone(drone); // Add to the root entity to ensure consistency with side effect of landing on cargo ship. var rootEntity = plugin.GetRootEntity(drone); if (rootEntity != null) { AddToRootEntityIfMissing(drone, rootEntity); } } private static void RemoveFromEntity(BaseEntity entity) { var turretComponent = entity.GetComponent(); if (turretComponent != null) { DestroyImmediate(turretComponent); RemoveFromAutoTurretTriggers(entity); } } public static void RemoveFromDrone(TargetableDrones plugin, Drone drone) { RemoveFromEntity(drone); var rootEntity = plugin.GetRootEntity(drone); if (rootEntity != null) { RemoveFromEntity(rootEntity); } } private Drone _ownerDrone; private GameObject _child; private TurretTargetComponent InitForDrone(Drone drone) { _ownerDrone = drone; AddChildLayerForAutoTurrets(); return this; } private void AddChildLayerForAutoTurrets() { _child = gameObject.CreateChild(); _child.layer = (int)Rust.Layer.Player_Server; var triggerCollider = _child.gameObject.AddComponent(); triggerCollider.size = _ownerDrone.bounds.extents; triggerCollider.isTrigger = true; } private void OnDestroy() { if (_child != null) { Destroy(_child); } } } private class SAMTargetComponent : FacepunchBehaviour, ISamSiteTarget { public static HashSet DroneComponents = new(); public static void AddToDroneIfMissing(TargetableDrones plugin, Drone drone) { var component = drone.GetOrAddComponent(); component._plugin = plugin; } public static void RemoveFromDrone(Drone drone) { var samComponent = drone.GetComponent(); if (samComponent != null) { DestroyImmediate(samComponent); } } public Drone Drone { get; private set; } private TargetableDrones _plugin; private Transform _transform; private void Awake() { Drone = GetComponent(); _transform = transform; DroneComponents.Add(this); } public Vector3 Position => _transform.position; public SamTargetType SAMTargetType => targetTypeVehicle; public bool isClient => false; public bool IsValidSAMTarget(bool isStaticSamSite) { if (_plugin.IsTargetExempt(Drone)) return false; return isStaticSamSite ? _plugin._config.EnableStaticSAMTargeting : _plugin._config.EnablePlayerSAMTargeting; } public Vector3 CenterPoint() => Drone.CenterPoint(); public Vector3 GetWorldVelocity() => Drone.body.velocity; public bool IsVisible(Vector3 position, float distance) => Drone.IsVisible(position, distance); private void OnDestroy() => DroneComponents.Remove(this); } #endregion #region Configuration private class CaseInsensitiveDictionary : Dictionary { public CaseInsensitiveDictionary() : base(StringComparer.OrdinalIgnoreCase) {} public CaseInsensitiveDictionary(IEnumerable> collection) : base(collection, StringComparer.OrdinalIgnoreCase) {} } [JsonObject(MemberSerialization.OptIn)] private class NPCTargetingSettings { [JsonProperty("MaxRange")] public float MaxRange = 45; [JsonProperty("DamageMultiplier")] public float DamageMultiplier = 4f; [JsonProperty("EnabledByNpcPrefab")] private CaseInsensitiveDictionary EnabledByNpcPrefabName = new(); private Dictionary EnabledByNpcPrefabId = new(); public bool Enabled { get; private set; } public bool DamageMultiplierEnabled => Enabled && DamageMultiplier != 1; public bool IsAllowed(BaseEntity entity) { return EnabledByNpcPrefabId.TryGetValue(entity.prefabID, out var canTarget) && canTarget; } public bool OnServerInitialized() { var changed = AddMissingNpcPrefabs(); foreach (var (prefabPath, value) in EnabledByNpcPrefabName) { var humanNpc = GameManager.server.FindPrefab(prefabPath)?.GetComponent(); if (humanNpc == null) { LogWarning($"Invalid HumanNPC prefab in config: {prefabPath}"); continue; } EnabledByNpcPrefabId[humanNpc.prefabID] = value; Enabled = true; } return changed; } private bool AddMissingNpcPrefabs() { var changed = false; foreach (var prefabPath in GameManifest.Current.entities) { var humanNpc = GameManager.server.FindPrefab(prefabPath)?.GetComponent(); if (humanNpc == null) continue; if (humanNpc.GetComponent()?.HostileTargetsOnly ?? false) continue; if (!EnabledByNpcPrefabName.ContainsKey(prefabPath)) { EnabledByNpcPrefabName[prefabPath.ToLower()] = false; changed = true; } } if (changed) { EnabledByNpcPrefabName = new CaseInsensitiveDictionary(EnabledByNpcPrefabName.OrderBy(entry => entry.Key)); } return changed; } } [JsonObject(MemberSerialization.OptIn)] private class SharingSettings { [JsonProperty("Team")] public bool Team = false; [JsonProperty("Friends")] public bool Friends = false; [JsonProperty("Clan")] public bool Clan = false; [JsonProperty("Allies")] public bool Allies = false; } [JsonObject(MemberSerialization.OptIn)] private class Configuration : BaseConfiguration { [JsonProperty("EnableTurretTargeting")] public bool EnableTurretTargeting = true; [JsonProperty("EnablePlayerSAMTargeting")] public bool EnablePlayerSAMTargeting = true; [JsonProperty("EnableStaticSAMTargeting")] public bool EnableStaticSAMTargeting = true; [JsonProperty("EnableSAMTargeting")] public bool DeprecatedEnableSAMTargeting { set { EnablePlayerSAMTargeting = value; EnableStaticSAMTargeting = value; } } [JsonProperty("NPCTargeting")] public NPCTargetingSettings NPCTargetingSettings = new(); [JsonProperty("DefaultSharingSettings")] public SharingSettings DefaultSharingSettings = new(); [JsonIgnore] public bool EnableSamTargeting => EnablePlayerSAMTargeting || EnableStaticSAMTargeting; public bool OnServerInitialized() { return NPCTargetingSettings.OnServerInitialized(); } } private Configuration GetDefaultConfig() => new(); #endregion #region Configuration Helpers [JsonObject(MemberSerialization.OptIn)] private class BaseConfiguration { public bool UsingDefaults; 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) { var changed = false; foreach (var key in currentWithDefaults.Keys) { if (currentRaw.TryGetValue(key, out var currentRawValue)) { var currentDictValue = currentRawValue as Dictionary; if (currentWithDefaults[key] is Dictionary defaultDictValue) { 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 = GetDefaultConfig(); 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(); if (_config != null) { _config.UsingDefaults = true; } } } protected override void SaveConfig() { Log($"Configuration changes saved to {Name}.json"); Config.WriteObject(_config, true); } #endregion } }