using Content.Server.Speech; using Content.Shared.Actions; using Content.Shared.Administration.Logs; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Clothing; using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement.Components; using Content.Shared.Implants; using Content.Shared.Inventory; using Content.Shared.Lock; using Content.Shared.Popups; using Content.Shared.Speech; using Content.Shared.VoiceMask; using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Prototypes; namespace Content.Server.VoiceMask; public sealed partial class VoiceMaskSystem : EntitySystem { [Dependency] private SharedUserInterfaceSystem _uiSystem = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; [Dependency] private IConfigurationManager _cfgManager = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private IPrototypeManager _proto = default!; [Dependency] private SharedActionsSystem _actions = default!; [Dependency] private LockSystem _lock = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private IdentitySystem _identity = default!; /// /// The name of the client-side type that represents the user interface window. /// Used for innate voice masks, which need to be able to create their own UIs. /// private const string UiGeneratedName = "VoiceMaskBoundUserInterface"; // CCVar. private int _maxNameLength; public override void Initialize() { base.Initialize(); // These events should fire in the order Innate -> Implant -> Inventory // Transform speaker name events SubscribeLocalEvent>(OnTransformSpeakerNameInventory); SubscribeLocalEvent>(OnTransformSpeakerNameImplant); SubscribeLocalEvent(OnInnateTransformSpeakerName); // See identity attempt events SubscribeLocalEvent>(OnSeeIdentityAttemptEvent); SubscribeLocalEvent(OnInnateSeeIdentityAttemptEvent); // Transform speech events SubscribeLocalEvent>(OnTransformSpeechInventory, before: [typeof(AccentSystem)]); SubscribeLocalEvent>(OnTransformSpeechImplant, before: [typeof(AccentSystem)]); SubscribeLocalEvent(OnTransformSpeech, before: [typeof(AccentSystem)]); // Voice mask transform things SubscribeLocalEvent>((ent, ref ev) => OnVoiceMaskToggledEvent(ent, ref ev.Args)); SubscribeLocalEvent>((ent, ref ev) => OnVoiceMaskToggledEvent(ent, ref ev.Args)); SubscribeLocalEvent(OnVoiceMaskToggledEvent); // Other events SubscribeLocalEvent(OnImplantImplantedEvent); SubscribeLocalEvent(OnImplantRemovedEventEvent); SubscribeLocalEvent(OnLockToggled); SubscribeLocalEvent(OnChangeName); SubscribeLocalEvent(OnChangeVerb); SubscribeLocalEvent(OnToggle); SubscribeLocalEvent(OnAccentToggle); SubscribeLocalEvent(OnEquip); SubscribeLocalEvent(OpenUI); SubscribeLocalEvent(OnMapInit); Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); InitializeTTS(); // Corvax-TTS } private void OnMapInit(Entity ent, ref MapInitEvent args) { if (!ent.Comp.IsInnate) return; // all masks should be inactive on creation ent.Comp.Active = false; _actions.AddAction(ent, ent.Comp.Action); _uiSystem.SetUi((ent, null), VoiceMaskUIKey.Key, new InterfaceData(UiGeneratedName)); _identity.QueueIdentityUpdate(ent.Owner); } /// /// Toggles this mask off it it isn't the mask turned on /// private void OnVoiceMaskToggledEvent(Entity ent, ref VoiceMaskToggledEvent args) { // we only toggle when the other mask turns on if (!args.Active) return; // we don't want the entity turned on to be turned off, and there isn't any work to do if it already inactive if (ent.Owner == args.Mask || !ent.Comp.Active) return; // turn it off ent.Comp.Active = false; // update the UpdateUI(ent); _identity.QueueIdentityUpdate(args.Source); } /// /// Hides accent if the voice mask is on and the option to block accents is on /// private void TransformSpeech(Entity entity, TransformSpeechEvent args) { if (entity.Comp.AccentHide && entity.Comp.Active) args.Cancel(); } private void OnTransformSpeech(Entity entity, ref TransformSpeechEvent args) { TransformSpeech(entity, args); } private void OnTransformSpeechInventory(Entity entity, ref InventoryRelayedEvent args) { TransformSpeech(entity, args.Args); } private void OnTransformSpeechImplant(Entity entity, ref ImplantRelayEvent args) { TransformSpeech(entity, args.Args); } private void OnInnateTransformSpeakerName(Entity ent, ref TransformSpeakerNameEvent args) { TransformVoice(ent, args); } private void OnTransformSpeakerNameInventory(Entity entity, ref InventoryRelayedEvent args) { TransformVoice(entity, args.Args); } private void OnTransformSpeakerNameImplant(Entity entity, ref ImplantRelayEvent args) { TransformVoice(entity, args.Args); } private void OnInnateSeeIdentityAttemptEvent(Entity entity, ref SeeIdentityAttemptEvent args) { if (!entity.Comp.OverrideIdentity || !entity.Comp.Active || !entity.Comp.IsInnate) return; args.NameOverride = GetCurrentVoiceName(entity); } private void OnSeeIdentityAttemptEvent(Entity entity, ref ImplantRelayEvent args) { if (!entity.Comp.OverrideIdentity || !entity.Comp.Active) return; args.Args.NameOverride = GetCurrentVoiceName(entity); } private void OnImplantImplantedEvent(Entity entity, ref ImplantImplantedEvent ev) { entity.Comp.Active = false; _identity.QueueIdentityUpdate(ev.Implanted); } private void OnImplantRemovedEventEvent(Entity entity, ref ImplantRemovedEvent ev) { _identity.QueueIdentityUpdate(ev.Implanted); } private void OnLockToggled(Entity ent, ref LockToggledEvent args) { if (args.Locked) _actions.RemoveAction(ent.Comp.ActionEntity); else if (_container.TryGetContainingContainer(ent.Owner, out var container)) _actions.AddAction(container.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action, ent); } #region User inputs from UI private void OnChangeVerb(Entity entity, ref VoiceMaskChangeVerbMessage msg) { if (msg.Verb is { } id && !_proto.HasIndex(id)) return; entity.Comp.VoiceMaskSpeechVerb = msg.Verb; // verb is only important to metagamers so no need to log as opposed to name _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-success"), entity, msg.Actor); UpdateUI(entity); } private void OnChangeName(Entity entity, ref VoiceMaskChangeNameMessage message) { if (message.Name.Length > _maxNameLength || message.Name.Length <= 0) { _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-failure"), entity, message.Actor, PopupType.SmallCaution); return; } var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name); if (entity.Comp.IsInnate) RaiseLocalEvent(entity.Owner, ref nameUpdatedEvent); else RaiseLocalEvent(message.Actor, ref nameUpdatedEvent); entity.Comp.VoiceMaskName = message.Name; _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}"); _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-success"), entity, message.Actor); UpdateUI(entity); } private void OnToggle(Entity entity, ref VoiceMaskToggleMessage args) { _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-toggle"), entity, args.Actor); entity.Comp.Active = !entity.Comp.Active; var ev = new VoiceMaskToggledEvent(entity.Owner, args.Actor, entity.Comp.Active); RaiseLocalEvent(entity.Owner, ev); // Update identity because of possible name override _identity.QueueIdentityUpdate(args.Actor); UpdateUI(entity); } private void OnAccentToggle(Entity entity, ref VoiceMaskAccentToggleMessage args) { _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-accent-toggle"), entity, args.Actor); entity.Comp.AccentHide = !entity.Comp.AccentHide; UpdateUI(entity); } #endregion #region UI private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEquippedEvent args) { if (_lock.IsLocked(uid)) return; component.Active = false; _actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid); } private void OpenUI(VoiceMaskSetNameEvent ev) { var maskEntity = ev.Action.Comp.Container; if (!TryComp(maskEntity, out var voiceMaskComp)) return; if (!_uiSystem.HasUi(maskEntity.Value, VoiceMaskUIKey.Key)) return; _uiSystem.OpenUi(maskEntity.Value, VoiceMaskUIKey.Key, ev.Performer); UpdateUI((maskEntity.Value, voiceMaskComp)); } private void UpdateUI(Entity entity) { if (_uiSystem.HasUi(entity, VoiceMaskUIKey.Key)) _uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide, entity.Comp.TitleText, entity.Comp.VoiceId)); //entity.Comp.VoiceId Corvax-TTS } #endregion #region Helper functions private string GetCurrentVoiceName(Entity entity) { return entity.Comp.VoiceMaskName ?? Loc.GetString("voice-mask-default-name-override"); } private void TransformVoice(Entity entity, TransformSpeakerNameEvent args) { if (!entity.Comp.Active) return; args.VoiceName = GetCurrentVoiceName(entity); args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.SpeechVerb; } #endregion }