using System; using ACE.Common; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Managers; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { public class Door : WorldObject, Lock { private static readonly Motion motionOpen = new Motion(MotionStance.NonCombat, MotionCommand.On); private static readonly Motion motionClosed = new Motion(MotionStance.NonCombat, MotionCommand.Off); /// /// A new biota be created taking all of its values from weenie. /// public Door(Weenie weenie, ObjectGuid guid) : base(weenie, guid) { SetEphemeralValues(); } /// /// Restore a WorldObject from the database. /// public Door(Biota biota) : base(biota) { SetEphemeralValues(); } private void SetEphemeralValues() { ObjectDescriptionFlags |= ObjectDescriptionFlag.Door; if (!DefaultOpen) { CurrentMotionState = motionClosed; IsOpen = false; //Ethereal = false; } else { CurrentMotionState = motionOpen; IsOpen = true; Ethereal = true; } ResetInterval = ResetInterval ?? 30.0f; LockCode = LockCode ?? ""; // Account for possible missing property from recreated weenies if (IsLocked && !DefaultLocked) DefaultLocked = true; if (DefaultLocked) IsLocked = true; else IsLocked = false; ActivationResponse |= ActivationResponse.Use; } public string LockCode { get => GetProperty(PropertyString.LockCode); set { if (value == null) RemoveProperty(PropertyString.LockCode); else SetProperty(PropertyString.LockCode, value); } } public override void OnTalk(WorldObject activator) { if (activator is Player player) { var behind = player != null && player.GetRelativeDir(this).HasFlag(Quadrant.Back); if (IsOpen && !behind) // not sure if retail made this distinction, but for the doors tested, it seemed more logical given the text shown player.Session.Network.EnqueueSend(new GameMessageSystemChat(ActivationTalk, ChatMessageType.Broadcast)); } } public override void ActOnUse(WorldObject worldObject) { if (IsBusy) return; var player = worldObject as Player; var behind = player != null && player.GetRelativeDir(this).HasFlag(Quadrant.Back); if (!IsLocked || behind) { if (!IsOpen) Open(worldObject.Guid); else if (!(worldObject is Switch) && !(worldObject is PressurePlate)) Close(worldObject.Guid); // Create Door auto close timer var useTimestamp = UseTimestamp ?? 0; var autoCloseTimer = new ActionChain(); autoCloseTimer.AddDelaySeconds(ResetInterval ?? 0); autoCloseTimer.AddAction(this, () => Reset(useTimestamp)); autoCloseTimer.EnqueueChain(); } else { if (player != null) { var doorIsLocked = new GameEventCommunicationTransientString(player.Session, "The door is locked!"); player.Session.Network.EnqueueSend(doorIsLocked); EnqueueBroadcast(new GameMessageSound(Guid, Sound.OpenFailDueToLock, 1.0f)); } } } public void Open(ObjectGuid opener = new ObjectGuid()) { if (CurrentMotionState == motionOpen) return; EnqueueBroadcastMotion(motionOpen); CurrentMotionState = motionOpen; Ethereal = true; IsOpen = true; EnqueueBroadcastPhysicsState(); if (opener.Full > 0) UseTimestamp = Time.GetUnixTime(); IsBusy = true; var animTime = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, MotionStance.NonCombat, MotionCommand.Off, MotionCommand.On); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(animTime); actionChain.AddAction(this, () => IsBusy = false); actionChain.EnqueueChain(); } public double CloseTimestamp; public void Close(ObjectGuid closer = new ObjectGuid()) { if (CurrentMotionState == motionClosed) return; EnqueueBroadcastMotion(motionClosed); CurrentMotionState = motionClosed; IsOpen = false; var animTime = Physics.Animation.MotionTable.GetAnimationLength(MotionTableId, MotionStance.NonCombat, MotionCommand.On, MotionCommand.Off); //Console.WriteLine($"AnimTime: {animTime}"); IsBusy = true; CloseTimestamp = Time.GetUnixTime(); var closeTimestamp = CloseTimestamp; var actionChain = new ActionChain(); actionChain.AddDelaySeconds(animTime); actionChain.AddAction(this, () => { FinalizeClose(closeTimestamp); IsBusy = false; }); actionChain.EnqueueChain(); if (closer.Full > 0) UseTimestamp = Time.GetUnixTime(); } private void FinalizeClose(double closeTimestamp) { if (IsOpen || closeTimestamp != CloseTimestamp) return; // ethereal must be set to false for ethereal_check_for_collisions Ethereal = false; if (PropertyManager.GetBool("allow_door_hold").Item && PhysicsObj.ethereal_check_for_collisions()) { // the source of this bug is EtherealHook for the door // physics engine set_ethereal() -> ethereal_check_for_collisions() -> CheckEthereal state // if fix_door_holding == true, the player can still hold doors for other nearby players // who already know about the door / have not been far away from the door for > 25s // fix_door_holding == true only fixes 'long holding' //Console.WriteLine($"{Name} ({Guid}).FinalizeClose()"); Ethereal = true; var holdChain = new ActionChain(); holdChain.AddDelaySeconds(1.0f); // poll every second holdChain.AddAction(this, () => FinalizeClose(closeTimestamp)); holdChain.EnqueueChain(); return; } EnqueueBroadcastPhysicsState(); } private void Reset(double useTimestamp) { if (useTimestamp != UseTimestamp) return; if (!DefaultOpen) { Close(ObjectGuid.Invalid); if (DefaultLocked) { IsLocked = true; var updateProperty = new GameMessagePublicUpdatePropertyBool(this, PropertyBool.Locked, IsLocked); EnqueueBroadcast(updateProperty); } } else Open(ObjectGuid.Invalid); ResetTimestamp = Time.GetUnixTime(); } /// /// Used for unlocking a door via lockpick, so contains a skill check /// player.Skills[Skill.Lockpick].Current should be sent for the skill check /// public UnlockResults Unlock(uint unlockerGuid, uint playerLockpickSkillLvl, ref int difficulty) { return LockHelper.Unlock(this, playerLockpickSkillLvl, ref difficulty); } /// /// Used for unlocking a door via a key /// public UnlockResults Unlock(uint unlockerGuid, Key key, string keyCode = null) { return LockHelper.Unlock(this, key, keyCode); } public override void SetLinkProperties(WorldObject wo) { wo.ActivationTarget = Guid.Full; } public override void OnCollideObject(WorldObject target) { if (IsOpen) return; // currently the only AI options appear to be 0 or 1, // 1 meaning able to open doors? var creature = target as Creature; if (creature == null || creature.AiOptions == 0) return; ActOnUse(target); } } }