using System; using System.Collections.Generic; using System.Linq; using Facepunch; using Newtonsoft.Json; using UnityEngine; namespace Oxide.Plugins { [Info("UnderwaterLabPuzzleReset", "Uzumi", "1.0.0")] [Description("When a tracked security door opens, starts a server timer, then resets its wired puzzle when the area is clear of players (optional proximity wait). Fully automatic.")] internal class UnderwaterLabPuzzleReset : RustPlugin { private const float PollIntervalSeconds = 0.75f; private PluginConfig _config; private readonly Dictionary _timersByDoorNetId = new Dictionary(); private readonly HashSet _watchedDoorNetIds = new HashSet(); private readonly Dictionary _doorWasOpen = new Dictionary(); private Timer _pollTimer; private class PluginConfig { [JsonProperty("Doors")] public List Doors = new List { "door.hinged.underwater_labs.security" }; [JsonProperty("ResetDelaySeconds")] public float ResetDelaySeconds = 3600f; [JsonProperty("IoSearchRadius")] public float IoSearchRadius = 18f; [JsonProperty("PlayerBlockRadius")] public float PlayerBlockRadius = 100f; [JsonProperty("PlayerBlockRecheckSeconds")] public float PlayerBlockRecheckSeconds = 300f; } protected override void LoadDefaultConfig() { _config = new PluginConfig(); SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new JsonException(); var corrected = false; if (_config.Doors == null || _config.Doors.Count == 0) { _config.Doors = new List { "door.hinged.underwater_labs.security" }; corrected = true; } if (_config.ResetDelaySeconds < 60f) { _config.ResetDelaySeconds = 60f; corrected = true; } if (_config.IoSearchRadius < 3f) { _config.IoSearchRadius = 18f; corrected = true; } if (_config.PlayerBlockRadius < 0f) { _config.PlayerBlockRadius = 0f; corrected = true; } if (_config.PlayerBlockRecheckSeconds < 30f) { _config.PlayerBlockRecheckSeconds = 30f; corrected = true; } if (_config.PlayerBlockRecheckSeconds > 7200f) { _config.PlayerBlockRecheckSeconds = 7200f; corrected = true; } if (corrected) SaveConfig(); } catch { PrintWarning("Config is invalid; using defaults."); LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_config); private void OnServerInitialized() { foreach (var net in BaseNetworkable.serverEntities) RegisterWatchDoor(net as Door); _pollTimer?.Destroy(); _pollTimer = timer.Every(PollIntervalSeconds, PollWatchedLabDoors); } private void Unload() { _pollTimer?.Destroy(); _pollTimer = null; foreach (var kv in _timersByDoorNetId) kv.Value?.Destroy(); _timersByDoorNetId.Clear(); _watchedDoorNetIds.Clear(); _doorWasOpen.Clear(); } private void OnEntitySpawned(BaseNetworkable entity) => RegisterWatchDoor(entity as Door); private void OnEntityKill(BaseNetworkable entity) { var door = entity as Door; if (door == null) return; var id = door.net.ID.Value; if (!_watchedDoorNetIds.Contains(id)) return; _watchedDoorNetIds.Remove(id); _doorWasOpen.Remove(id); if (_timersByDoorNetId.TryGetValue(id, out var t)) { t?.Destroy(); _timersByDoorNetId.Remove(id); } } private void RegisterWatchDoor(Door door) { if (door == null || !MatchesTrackedDoor(door)) return; var id = door.net.ID.Value; _watchedDoorNetIds.Add(id); if (!_doorWasOpen.ContainsKey(id)) _doorWasOpen[id] = door.IsOpen(); } private bool MatchesTrackedDoor(Door door) { if (door == null) return false; var shortName = door.ShortPrefabName; var full = door.PrefabName ?? string.Empty; foreach (var name in _config.Doors) { if (string.IsNullOrEmpty(name)) continue; if (!string.IsNullOrEmpty(shortName) && shortName.Equals(name, StringComparison.OrdinalIgnoreCase)) return true; if (full.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0) return true; } return false; } private void PollWatchedLabDoors() { foreach (var id in _watchedDoorNetIds.ToArray()) { var door = BaseNetworkable.serverEntities.Find(new NetworkableId(id)) as Door; if (door == null || door.IsDestroyed) { _watchedDoorNetIds.Remove(id); _doorWasOpen.Remove(id); continue; } var open = door.IsOpen(); _doorWasOpen.TryGetValue(id, out var wasOpen); if (open && !wasOpen) HandleTrackedDoorOpenedTransition(door); _doorWasOpen[id] = open; } } private void HandleTrackedDoorOpenedTransition(Door door) { var netId = door.net.ID.Value; var doorPos = door.transform.position; if (_timersByDoorNetId.TryGetValue(netId, out var existing)) { existing?.Destroy(); _timersByDoorNetId.Remove(netId); } var delay = _config.ResetDelaySeconds; _timersByDoorNetId[netId] = timer.Once(delay, () => { _timersByDoorNetId.Remove(netId); TryFinalizePuzzleResetOrRecheck(netId, doorPos); }); } private void TryFinalizePuzzleResetOrRecheck(ulong netId, Vector3 doorPos) { if (_config.PlayerBlockRadius > 0f && IsAnyHumanPlayerNear(doorPos, _config.PlayerBlockRadius)) { var recheck = _config.PlayerBlockRecheckSeconds; _timersByDoorNetId[netId] = timer.Once(recheck, () => { _timersByDoorNetId.Remove(netId); TryFinalizePuzzleResetOrRecheck(netId, doorPos); }); return; } ResetPuzzlesOnNetworkFromDoorPosition(doorPos); } private static bool IsAnyHumanPlayerNear(Vector3 position, float radiusMeters) { var sq = radiusMeters * radiusMeters; foreach (var p in BasePlayer.allPlayerList) { if (p == null || p.IsDestroyed || p.IsNpc || !p.userID.IsSteamId()) continue; if (p.IsDead()) continue; if (!p.IsConnected && !p.IsSleeping()) continue; if ((p.transform.position - position).sqrMagnitude <= sq) return true; } return false; } private int ResetPuzzlesOnNetworkFromDoorPosition(Vector3 doorPos) { var buffer = Pool.Get>(); try { Vis.Entities(doorPos, _config.IoSearchRadius, buffer, -1, QueryTriggerInteraction.Ignore); var visited = new HashSet(); var queue = new Queue(); foreach (var io in buffer) { if (io == null || io.IsDestroyed) continue; if (visited.Add(io)) queue.Enqueue(io); } if (queue.Count == 0) return 0; var found = new HashSet(); while (queue.Count > 0) { var e = queue.Dequeue(); var pr = e.GetComponent(); if ((UnityEngine.Object)pr != null) found.Add(pr); EnqueueIoNeighbors(e.inputs, queue, visited); EnqueueIoNeighbors(e.outputs, queue, visited); } var count = 0; foreach (var p in found) { if ((UnityEngine.Object)p == null) continue; p.DoReset(); p.ResetTimer(); count++; } return count; } finally { buffer.Clear(); Pool.FreeUnmanaged(ref buffer); } } private static void EnqueueIoNeighbors(IOEntity.IOSlot[] slots, Queue queue, HashSet visited) { if (slots == null) return; foreach (var slot in slots) { if (slot == null) continue; var next = slot.connectedTo.Get() as IOEntity; if (next == null || next.IsDestroyed) continue; if (visited.Add(next)) queue.Enqueue(next); } } } }