using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using ModernUO.Serialization; using Server.Targeting; using CalcMoves = Server.Movement.Movement; namespace Server.Items; [SerializationGenerator(0, false)] public abstract partial class BaseDoor : Item, ILockable, ITelekinesisable { private static readonly Point3D[] m_Offsets = { new(-1, 1, 0), new(1, 1, 0), new(-1, 0, 0), new(1, -1, 0), new(1, 1, 0), new(1, -1, 0), new(0, 0, 0), new(0, -1, 0), new(0, 0, 0), new(0, 0, 0), new(0, 0, 0), new(0, 0, 0) }; private Timer _timer; [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] private uint _keyValue; [SerializableField(2)] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _locked; [SerializableField(3)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _openedId; [SerializableField(4)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _closedId; [SerializableField(5)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _openedSound; [SerializableField(6)] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _closedSound; [SerializableField(7)] [SerializedCommandProperty(AccessLevel.GameMaster)] private Point3D _offset; [SerializableField(8)] [SerializedCommandProperty(AccessLevel.GameMaster)] private BaseDoor _link; public BaseDoor(int closedID, int openedID, int openedSound, int closedSound, Point3D offset) : base(closedID) { _openedId = openedID; _closedId = closedID; _openedSound = openedSound; _closedSound = closedSound; _offset = offset; _timer = new InternalTimer(this); Movable = false; } [SerializableField(1, fieldChanged: nameof(OnOpenChanged))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _open; private void OnOpenChanged(bool oldValue, bool newValue) { ItemID = _open ? _openedId : _closedId; if (_open) { Location = new Point3D(X + _offset.X, Y + _offset.Y, Z + _offset.Z); } else { Location = new Point3D(X - _offset.X, Y - _offset.Y, Z - _offset.Z); } Effects.PlaySound(this, _open ? OpenedSound : ClosedSound); if (_open) { _timer ??= new InternalTimer(this); _timer.Start(); } else { _timer.Stop(); _timer = null; } } public virtual bool UseChainedFunctionality => false; public void OnTelekinesis(Mobile from) { Effects.SendLocationParticles(EffectItem.Create(Location, Map, EffectItem.DefaultDuration), 0x376A, 9, 32, 5022); Effects.PlaySound(Location, Map, 0x1F5); Use(from); } public static void Configure() { CommandSystem.Register("Link", AccessLevel.GameMaster, Link_OnCommand); CommandSystem.Register("ChainLink", AccessLevel.GameMaster, ChainLink_OnCommand); } [Usage("Link"), Description("Links two targeted doors together.")] private static void Link_OnCommand(CommandEventArgs e) { e.Mobile.BeginTarget(-1, false, TargetFlags.None, Link_OnFirstTarget); e.Mobile.SendMessage("Target the first door to link."); } private static void Link_OnFirstTarget(Mobile from, object targeted) { if (targeted is not BaseDoor door) { from.BeginTarget(-1, false, TargetFlags.None, Link_OnFirstTarget); from.SendMessage("That is not a door. Try again."); } else { from.BeginTarget(-1, false, TargetFlags.None, Link_OnSecondTarget, door); from.SendMessage("Target the second door to link."); } } private static void Link_OnSecondTarget(Mobile from, object targeted, BaseDoor first) { if (targeted is not BaseDoor second) { from.BeginTarget(-1, false, TargetFlags.None, Link_OnSecondTarget, first); from.SendMessage("That is not a door. Try again."); } else { first.Link = second; second.Link = first; from.SendMessage("The doors have been linked."); } } [Usage("ChainLink"), Description("Chain-links two or more targeted doors together.")] private static void ChainLink_OnCommand(CommandEventArgs e) { e.Mobile.BeginTarget(-1, false, TargetFlags.None, ChainLink_OnTarget, new List()); e.Mobile.SendMessage("Target the first of a sequence of doors to link."); } private static void ChainLink_OnTarget(Mobile from, object targeted, List list) { if (targeted is not BaseDoor door) { from.BeginTarget(-1, false, TargetFlags.None, ChainLink_OnTarget, list); from.SendMessage("That is not a door. Try again."); } else { if (list.Count > 0 && list[0] == door) { if (list.Count >= 2) { for (var i = 0; i < list.Count; ++i) { list[i].Link = list[(i + 1) % list.Count]; } from.SendMessage("The chain of doors have been linked."); } else { from.BeginTarget(-1, false, TargetFlags.None, ChainLink_OnTarget, list); from.SendMessage("You have not yet targeted two unique doors. Target the second door to link."); } } else if (list.Contains(door)) { from.BeginTarget(-1, false, TargetFlags.None, ChainLink_OnTarget, list); from.SendMessage( "You have already targeted that door. Target another door, or retarget the first door to complete the chain." ); } else { list.Add(door); from.BeginTarget(-1, false, TargetFlags.None, ChainLink_OnTarget, list); if (list.Count == 1) { from.SendMessage("Target the second door to link."); } else { from.SendMessage("Target another door to link. To complete the chain, retarget the first door."); } } } } public static void OpenDoorMacroUsed(Mobile m) { if (m.Map == null || !m.CheckAlive()) { return; } var x = m.X; var y = m.Y; CalcMoves.Offset(m.Direction, ref x, ref y); foreach (var item in m.Map.GetItemsAt(x, y)) { if (item.Z + item.ItemData.Height > m.Z && m.Z + 16 > item.Z && item is BaseDoor && m.CanSee(item) && m.InLOS(item)) { m.SendLocalizedMessage(500024); // Opening door... item.OnDoubleClick(m); break; } } } public static Point3D GetOffset(DoorFacing facing) => m_Offsets[(int)facing]; public bool CanClose() { if (!_open) { return true; } var map = Map; if (map == null) { return false; } var p = new Point3D(X - Offset.X, Y - Offset.Y, Z - Offset.Z); return CheckFit(map, p, 16); } private static bool CheckFit(Map map, Point3D p, int height) { if (map == Map.Internal) { return false; } var x = p.X; var y = p.Y; var z = p.Z; foreach (var item in map.GetItemsAt(x, y)) { if (item.ItemID <= TileData.MaxItemValue && item is not (BaseMulti or BaseDoor)) { var id = item.ItemData; var surface = id.Surface; var impassable = id.Impassable; if ((surface || impassable) && item.Z + id.CalcHeight > z && z + height > item.Z) { return false; } } } foreach (var m in map.GetMobilesAt(x, y)) { // At the same location, not hidden, or is a player, alive, and within z-bounds - then cannot fit if (m.Location.X == x && m.Location.Y == y && (!m.Hidden || m.AccessLevel == AccessLevel.Player) && m.Alive && m.Z + 16 > z && z + height > m.Z) { return false; } } return true; } public ChainEnumerable GetChain() => new(this); public bool IsFreeToClose() { if (!UseChainedFunctionality) { return CanClose(); } foreach (var link in GetChain()) { if (!link.CanClose()) { return false; } } return true; } public virtual bool IsInside(Mobile from) => false; public virtual bool UseLocks() => true; public virtual void Use(Mobile from) { if (Locked && !_open && UseLocks()) { if (from.AccessLevel >= AccessLevel.GameMaster) { // That is locked, but you open it with your godly powers. from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502502); } else if (Key.ContainsKey(from.Backpack, KeyValue)) { // You quickly unlock, open, and relock the door from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 501282); } else if (IsInside(from)) { // That is locked, but is usable from the inside. from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 501280); } else { if (Hue == 0x44E && Map == Map.Malas) // doom door into healer room in doom { SendLocalizedMessageTo(from, 1060014); // Only the dead may pass. } else { from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502503); // That is locked. } return; } } if (_open && !IsFreeToClose()) { return; } if (_open) { OnClosed(from); } else { OnOpened(from); } if (UseChainedFunctionality) { var open = !_open; foreach (var link in GetChain()) { link.Open = open; } } else { Open = !_open; var link = Link; if (_open && link is { Deleted: false, Open: false }) { link.Open = true; } } } public virtual void OnOpened(Mobile from) { } public virtual void OnClosed(Mobile from) { } public override void OnDoubleClick(Mobile from) { if (from.AccessLevel == AccessLevel.Player && !from.InRange(GetWorldLocation(), 2)) { from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that. } else { Use(from); } } public override void OnDelete() { _timer?.Stop(); _timer = null; Link = null; } [AfterDeserialization] private void AfterDeserialization() { if (_open) { _timer = new InternalTimer(this); _timer.Start(); } } private class InternalTimer : Timer { private readonly BaseDoor _door; public InternalTimer(BaseDoor door) : base(TimeSpan.FromSeconds(20.0), TimeSpan.FromSeconds(10.0)) => _door = door; protected override void OnTick() { if (_door.Deleted) { Stop(); return; } if (_door.Open && _door.IsFreeToClose()) { _door.Open = false; } } } public ref struct ChainEnumerable { private readonly BaseDoor _door; public ChainEnumerable(BaseDoor door) => _door = door; [MethodImpl(MethodImplOptions.AggressiveInlining)] public ChainEnumerator GetEnumerator() => new(_door); } public ref struct ChainEnumerator { private BaseDoor _door; private BaseDoor _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ChainEnumerator(BaseDoor door) { _door = door; _current = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { bool valid; if (_current == null) { _current = _door; valid = _current != null; } else { _current = _current.Link; valid = _current?.Deleted == false && _current != _door; } if (!valid) { _door = null; _current = null; } return valid; } public BaseDoor Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _current; } } }