using Content.Shared.Actions; using Content.Shared.Buckle.Components; using Content.Shared.Damage; using Content.Shared.Damage.ForceSay; using Content.Shared.Examine; using Content.Shared.Eye.Blinding.Systems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Pointing; using Content.Shared.Popups; using Content.Shared.Slippery; using Content.Shared.Sound; using Content.Shared.Sound.Components; using Content.Shared.Speech; using Content.Shared.StatusEffect; using Content.Shared.Stunnable; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Bed.Sleep; public sealed partial class SleepingSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; [Dependency] private readonly BlindableSystem _blindableSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedEmitSoundSystem _emitSound = default!; [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; public static readonly EntProtoId SleepActionId = "ActionSleep"; public static readonly EntProtoId WakeActionId = "ActionWake"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBedSleepAction); SubscribeLocalEvent(OnSleepStateChanged); SubscribeLocalEvent(OnWakeAction); SubscribeLocalEvent(OnSleepAction); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnSpeakAttempt); SubscribeLocalEvent(OnSeeAttempt); SubscribeLocalEvent(OnPointAttempt); SubscribeLocalEvent(OnSlip); SubscribeLocalEvent(OnConsciousAttempt); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent>(AddWakeVerb); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnUnbuckleAttempt); } private void OnUnbuckleAttempt(Entity ent, ref UnbuckleAttemptEvent args) { // TODO is this necessary? // Shouldn't the interaction have already been blocked by a general interaction check? if (ent.Owner == args.User) args.Cancelled = true; } private void OnBedSleepAction(Entity ent, ref SleepActionEvent args) { TrySleeping(args.Performer); } private void OnWakeAction(Entity ent, ref WakeActionEvent args) { if (TryWakeWithCooldown(ent.Owner)) args.Handled = true; } private void OnSleepAction(Entity ent, ref SleepActionEvent args) { TrySleeping((ent, ent.Comp)); } /// /// when sleeping component is added or removed, we do some stuff with other components. /// private void OnSleepStateChanged(Entity ent, ref SleepStateChangedEvent args) { if (args.FellAsleep) { // Expiring status effects would remove the components needed for sleeping _statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "Stun"); _statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "KnockedDown"); EnsureComp(ent); EnsureComp(ent); if (TryComp(ent, out var sleepSound)) { var emitSound = EnsureComp(ent); if (HasComp(ent)) { emitSound.Sound = sleepSound.Snore; } emitSound.MinInterval = sleepSound.Interval; emitSound.MaxInterval = sleepSound.MaxInterval; emitSound.PopUp = sleepSound.PopUp; Dirty(ent.Owner, emitSound); } return; } RemComp(ent); RemComp(ent); RemComp(ent); } private void OnMapInit(Entity ent, ref MapInitEvent args) { var ev = new SleepStateChangedEvent(true); RaiseLocalEvent(ent, ref ev); _blindableSystem.UpdateIsBlind(ent.Owner); _actionsSystem.AddAction(ent, ref ent.Comp.WakeAction, WakeActionId, ent); // TODO remove hardcoded time. _actionsSystem.SetCooldown(ent.Comp.WakeAction, _gameTiming.CurTime, _gameTiming.CurTime + TimeSpan.FromSeconds(2f)); } private void OnSpeakAttempt(Entity ent, ref SpeakAttemptEvent args) { // TODO reduce duplication of this behavior with MobStateSystem somehow if (HasComp(ent)) { RemCompDeferred(ent); return; } args.Cancel(); } private void OnSeeAttempt(Entity ent, ref CanSeeAttemptEvent args) { if (ent.Comp.LifeStage <= ComponentLifeStage.Running) args.Cancel(); } private void OnPointAttempt(Entity ent, ref PointAttemptEvent args) { args.Cancel(); } private void OnSlip(Entity ent, ref SlipAttemptEvent args) { args.NoSlip = true; } private void OnConsciousAttempt(Entity ent, ref ConsciousAttemptEvent args) { args.Cancelled = true; } private void OnExamined(Entity ent, ref ExaminedEvent args) { if (args.IsInDetailsRange) { args.PushMarkup(Loc.GetString("sleep-examined", ("target", Identity.Entity(ent, EntityManager)))); } } private void AddWakeVerb(Entity ent, ref GetVerbsEvent args) { if (!args.CanInteract || !args.CanAccess) return; var target = args.Target; var user = args.User; AlternativeVerb verb = new() { Act = () => { TryWakeWithCooldown((ent, ent.Comp), user: user); }, Text = Loc.GetString("action-name-wake"), Priority = 2 }; args.Verbs.Add(verb); } /// /// When you click on a sleeping person with an empty hand, try to wake them. /// private void OnInteractHand(Entity ent, ref InteractHandEvent args) { args.Handled = true; TryWakeWithCooldown((ent, ent.Comp), args.User); } /// /// Wake up on taking an instance of damage at least the value of WakeThreshold. /// private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) { if (!args.DamageIncreased || args.DamageDelta == null) return; if (args.DamageDelta.GetTotal() >= ent.Comp.WakeThreshold) TryWaking((ent, ent.Comp)); } /// /// In crit, we wake up if we are not being forced to sleep. /// And, you can't sleep when dead... /// private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) { if (args.NewMobState == MobState.Dead) { RemComp(ent); RemComp(ent); return; } if (TryComp(ent, out var spam)) _emitSound.SetEnabled((ent, spam), args.NewMobState == MobState.Alive); } private void OnInit(Entity ent, ref ComponentInit args) { TrySleeping(ent.Owner); } private void Wake(Entity ent) { RemComp(ent); _actionsSystem.RemoveAction(ent, ent.Comp.WakeAction); var ev = new SleepStateChangedEvent(false); RaiseLocalEvent(ent, ref ev); _blindableSystem.UpdateIsBlind(ent.Owner); } /// /// Try sleeping. Only mobs can sleep. /// public bool TrySleeping(Entity ent) { if (!Resolve(ent, ref ent.Comp, logMissing: false)) return false; var tryingToSleepEvent = new TryingToSleepEvent(ent); RaiseLocalEvent(ent, ref tryingToSleepEvent); if (tryingToSleepEvent.Cancelled) return false; EnsureComp(ent); return true; } /// /// Tries to wake up , with a cooldown between attempts to prevent spam. /// public bool TryWakeWithCooldown(Entity ent, EntityUid? user = null) { if (!Resolve(ent, ref ent.Comp, false)) return false; var curTime = _gameTiming.CurTime; if (curTime < ent.Comp.CooldownEnd) return false; ent.Comp.CooldownEnd = curTime + ent.Comp.Cooldown; Dirty(ent, ent.Comp); return TryWaking(ent, user: user); } /// /// Try to wake up . /// public bool TryWaking(Entity ent, bool force = false, EntityUid? user = null) { if (!Resolve(ent, ref ent.Comp, false)) return false; if (!force && HasComp(ent)) { if (user != null) { _audio.PlayPredicted(ent.Comp.WakeAttemptSound, ent, user); _popupSystem.PopupClient(Loc.GetString("wake-other-failure", ("target", Identity.Entity(ent, EntityManager))), ent, user, PopupType.SmallCaution); } return false; } if (user != null) { _audio.PlayPredicted(ent.Comp.WakeAttemptSound, ent, user); _popupSystem.PopupClient(Loc.GetString("wake-other-success", ("target", Identity.Entity(ent, EntityManager))), ent, user); } Wake((ent, ent.Comp)); return true; } } public sealed partial class SleepActionEvent : InstantActionEvent; public sealed partial class WakeActionEvent : InstantActionEvent; /// /// Raised on an entity when they fall asleep or wake up. /// [ByRefEvent] public record struct SleepStateChangedEvent(bool FellAsleep);