using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Item.ItemToggle.Components; using Content.Shared.Popups; using Content.Shared.Temperature; using Content.Shared.Toggleable; using Content.Shared.Verbs; using Content.Shared.Wieldable; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; namespace Content.Shared.Item.ItemToggle; /// /// Handles generic item toggles, like a welder turning on and off, or an e-sword. /// /// /// If you need extended functionality (e.g. requiring power) then add a new component and use events. /// public sealed class ItemToggleSystem : EntitySystem { [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; private EntityQuery _query; public override void Initialize() { base.Initialize(); _query = GetEntityQuery(); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(TurnOffOnUnwielded); SubscribeLocalEvent(TurnOnOnWielded); SubscribeLocalEvent(OnUseInHand); SubscribeLocalEvent>(OnActivateVerb); SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnIsHotEvent); SubscribeLocalEvent(UpdateActiveSound); } private void OnStartup(Entity ent, ref ComponentStartup args) { UpdateVisuals(ent); } private void OnMapInit(Entity ent, ref MapInitEvent args) { if (!ent.Comp.Activated) return; var ev = new ItemToggledEvent(Predicted: ent.Comp.Predictable, Activated: ent.Comp.Activated, User: null); RaiseLocalEvent(ent, ref ev); } private void OnUseInHand(Entity ent, ref UseInHandEvent args) { if (args.Handled || !ent.Comp.OnUse) return; args.Handled = true; Toggle((ent, ent.Comp), args.User, predicted: ent.Comp.Predictable); } private void OnActivateVerb(Entity ent, ref GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract) return; var user = args.User; args.Verbs.Add(new ActivationVerb() { Text = !ent.Comp.Activated ? Loc.GetString("item-toggle-activate") : Loc.GetString("item-toggle-deactivate"), Act = () => { Toggle((ent.Owner, ent.Comp), user, predicted: ent.Comp.Predictable); } }); } private void OnActivate(Entity ent, ref ActivateInWorldEvent args) { if (args.Handled || !ent.Comp.OnActivate) return; args.Handled = true; Toggle((ent.Owner, ent.Comp), args.User, predicted: ent.Comp.Predictable); } /// /// Used when an item is attempted to be toggled. /// Sets its state to the opposite of what it is. /// /// Same as public bool Toggle(Entity ent, EntityUid? user = null, bool predicted = true) { if (!_query.Resolve(ent, ref ent.Comp, false)) return false; return TrySetActive(ent, !ent.Comp.Activated, user, predicted); } /// /// Tries to set the activated bool from a value. /// /// false if the attempt fails for any reason public bool TrySetActive(Entity ent, bool active, EntityUid? user = null, bool predicted = true) { if (active) return TryActivate(ent, user, predicted: predicted); else return TryDeactivate(ent, user, predicted: predicted); } /// /// Used when an item is attempting to be activated. It returns false if the attempt fails any reason, interrupting the activation. /// public bool TryActivate(Entity ent, EntityUid? user = null, bool predicted = true) { if (!_query.Resolve(ent, ref ent.Comp, false)) return false; var uid = ent.Owner; var comp = ent.Comp; if (comp.Activated) return true; if (!comp.Predictable && _netManager.IsClient) return true; var attempt = new ItemToggleActivateAttemptEvent(user); RaiseLocalEvent(uid, ref attempt); if (!comp.Predictable) predicted = false; if (attempt.Cancelled) { if (predicted) _audio.PlayPredicted(comp.SoundFailToActivate, uid, user); else _audio.PlayPvs(comp.SoundFailToActivate, uid); if (attempt.Popup != null && user != null) { if (predicted) _popup.PopupClient(attempt.Popup, uid, user.Value); else _popup.PopupEntity(attempt.Popup, uid, user.Value); } return false; } Activate((uid, comp), predicted, user); return true; } /// /// Used when an item is attempting to be deactivated. It returns false if the attempt fails any reason, interrupting the deactivation. /// public bool TryDeactivate(Entity ent, EntityUid? user = null, bool predicted = true) { if (!_query.Resolve(ent, ref ent.Comp, false)) return false; var uid = ent.Owner; var comp = ent.Comp; if (!comp.Activated) return true; if (!comp.Predictable && _netManager.IsClient) return true; var attempt = new ItemToggleDeactivateAttemptEvent(user); RaiseLocalEvent(uid, ref attempt); if (attempt.Cancelled) return false; if (!comp.Predictable) predicted = false; Deactivate((uid, comp), predicted, user); return true; } private void Activate(Entity ent, bool predicted, EntityUid? user = null) { var (uid, comp) = ent; var soundToPlay = comp.SoundActivate; if (predicted) _audio.PlayPredicted(soundToPlay, uid, user); else _audio.PlayPvs(soundToPlay, uid); comp.Activated = true; UpdateVisuals((uid, comp)); Dirty(uid, comp); var toggleUsed = new ItemToggledEvent(predicted, Activated: true, user); RaiseLocalEvent(uid, ref toggleUsed); } /// /// Used to make the actual changes to the item's components on deactivation. /// private void Deactivate(Entity ent, bool predicted, EntityUid? user = null) { var (uid, comp) = ent; var soundToPlay = comp.SoundDeactivate; if (predicted) _audio.PlayPredicted(soundToPlay, uid, user); else _audio.PlayPvs(soundToPlay, uid); comp.Activated = false; UpdateVisuals((uid, comp)); Dirty(uid, comp); var toggleUsed = new ItemToggledEvent(predicted, Activated: false, user); RaiseLocalEvent(uid, ref toggleUsed); } private void UpdateVisuals(Entity ent) { if (TryComp(ent, out AppearanceComponent? appearance)) { _appearance.SetData(ent, ToggleVisuals.Toggled, ent.Comp.Activated, appearance); } } /// /// Used for items that require to be wielded in both hands to activate. For instance the dual energy sword will turn off if not wielded. /// private void TurnOffOnUnwielded(Entity ent, ref ItemUnwieldedEvent args) { TryDeactivate((ent, ent.Comp), args.User); } /// /// Wieldable items will automatically turn on when wielded. /// private void TurnOnOnWielded(Entity ent, ref ItemWieldedEvent args) { // FIXME: for some reason both client and server play sound TryActivate((ent, ent.Comp)); } public bool IsActivated(Entity ent) { if (!_query.Resolve(ent, ref ent.Comp, false)) return true; // assume always activated if no component return ent.Comp.Activated; } /// /// Used to make the item hot when activated. /// private void OnIsHotEvent(Entity ent, ref IsHotEvent args) { args.IsHot |= IsActivated(ent.Owner); } /// /// Used to update the looping active sound linked to the entity. /// private void UpdateActiveSound(Entity ent, ref ItemToggledEvent args) { var (uid, comp) = ent; if (!args.Activated) { comp.PlayingStream = _audio.Stop(comp.PlayingStream); return; } if (comp.ActiveSound != null && comp.PlayingStream == null) { var loop = AudioParams.Default.WithLoop(true); var stream = args.Predicted ? _audio.PlayPredicted(comp.ActiveSound, uid, args.User, loop) : _audio.PlayPvs(comp.ActiveSound, uid, loop); if (stream?.Entity is {} entity) comp.PlayingStream = entity; } } }