From 8c5faf34027b83d1435bb96df644a8a467a7740a Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:49:07 +0300 Subject: [PATCH] Language Refactor 3 (#937) # Description This significantly improves the quality of the language system by fixing the mistakes I've made almost a year ago while developing it. Mainly, this throws away the old half-broken way of networking in favor of the component state system provided by RT. Language speaker comp is now shared with SendOnlyToOwner = true, and its state is handled manually. In addition to that, this brings the following changes: - UniversalLanguageSpeaker and LanguageKnowledge are now server-side - DetermineLanguagesEvent and LanguagesUpdateEvent are now shared (so that future systems can be built in shared, if needed) - Everything now uses the ProtoId type instead of raw strings (god, I hated those so much) - The server-side language system now accepts Entity arguments instead of EntityUid + T - UniversalLanguageSpeaker is now based on DetermineEntityLanguagesEvent and gets an Enabled field, which allows to turn it off. This may have some use in the future. - Some minor cleanup # Changelog No cl --------- Co-authored-by: VMSolidus --- .../Language/LanguageMenuWindow.xaml.cs | 56 +++--- .../Language/Systems/LanguageSystem.cs | 78 +++----- .../Chemistry/ReagentEffects/MakeSentient.cs | 1 - .../Cloning/CloningSystem.Utility.cs | 2 +- .../Language/Commands/AdminLanguageCommand.cs | 5 +- .../Commands/AdminTranslatorCommand.cs | 7 +- .../Language/LanguageKnowledgeComponent.cs | 23 +++ .../Language/LanguageSystem.Networking.cs | 73 -------- Content.Server/Language/LanguageSystem.cs | 177 ++++++++++-------- .../Language/LanguagesUpdateEvent.cs | 8 - .../Language/TranslatorImplantSystem.cs | 1 + Content.Server/Language/TranslatorSystem.cs | 6 +- .../UniversalLanguageSpeakerComponent.cs | 5 +- .../Mind/Commands/MakeSentientCommand.cs | 2 +- .../Traits/Assorted/ForeignerTraitSystem.cs | 6 +- .../Components/LanguageKnowledgeComponent.cs | 24 --- .../Components/LanguageSpeakerComponent.cs | 28 ++- .../Translators/BaseTranslatorComponent.cs | 16 +- .../HandheldTranslatorComponent.cs | 2 +- .../Events}/DetermineEntityLanguagesEvent.cs | 8 +- .../Language/Events/LanguagesUpdateEvent.cs | 12 ++ .../Events/LanguagesUpdatedMessage.cs | 15 -- .../Events/RequestLanguagesMessage.cs | 10 - .../Language/Systems/SharedLanguageSystem.cs | 7 +- 24 files changed, 247 insertions(+), 325 deletions(-) create mode 100644 Content.Server/Language/LanguageKnowledgeComponent.cs delete mode 100644 Content.Server/Language/LanguageSystem.Networking.cs delete mode 100644 Content.Server/Language/LanguagesUpdateEvent.cs rename {Content.Shared/Language/Components => Content.Server/Language}/UniversalLanguageSpeakerComponent.cs (74%) delete mode 100644 Content.Shared/Language/Components/LanguageKnowledgeComponent.cs rename {Content.Server/Language => Content.Shared/Language/Events}/DetermineEntityLanguagesEvent.cs (77%) create mode 100644 Content.Shared/Language/Events/LanguagesUpdateEvent.cs delete mode 100644 Content.Shared/Language/Events/LanguagesUpdatedMessage.cs delete mode 100644 Content.Shared/Language/Events/RequestLanguagesMessage.cs diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index ed6ec6b3e2d..b72d79d6de2 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml.cs +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -1,48 +1,51 @@ using Content.Client.Language.Systems; +using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; namespace Content.Client.Language; [GenerateTypedNameReferences] -public sealed partial class LanguageMenuWindow : DefaultWindow, IEntityEventSubscriber +public sealed partial class LanguageMenuWindow : DefaultWindow { private readonly LanguageSystem _clientLanguageSystem; private readonly List _entries = new(); - public LanguageMenuWindow() { RobustXamlLoader.Load(this); _clientLanguageSystem = IoCManager.Resolve().GetEntitySystem(); - - _clientLanguageSystem.OnLanguagesChanged += OnUpdateState; + _clientLanguageSystem.OnLanguagesChanged += UpdateState; } protected override void Dispose(bool disposing) { base.Dispose(disposing); - _clientLanguageSystem.OnLanguagesChanged -= OnUpdateState; + + if (disposing) + _clientLanguageSystem.OnLanguagesChanged -= UpdateState; } protected override void Opened() { - // Refresh the window when it gets opened. - // This actually causes two refreshes: one immediately, and one after the server sends a state message. - UpdateState(_clientLanguageSystem.CurrentLanguage, _clientLanguageSystem.SpokenLanguages); - _clientLanguageSystem.RequestStateUpdate(); + UpdateState(); } - - private void OnUpdateState(object? sender, LanguagesUpdatedMessage args) + private void UpdateState() { - UpdateState(args.CurrentLanguage, args.Spoken); + var languageSpeaker = _clientLanguageSystem.GetLocalSpeaker(); + if (languageSpeaker == null) + return; + + UpdateState(languageSpeaker.CurrentLanguage, languageSpeaker.SpokenLanguages); } - public void UpdateState(string currentLanguage, List spokenLanguages) + public void UpdateState(ProtoId currentLanguage, List> spokenLanguages) { var langName = Loc.GetString($"language-{currentLanguage}-name"); CurrentLanguageLabel.Text = Loc.GetString("language-menu-current-language", ("language", langName)); @@ -58,15 +61,15 @@ public void UpdateState(string currentLanguage, List spokenLanguages) // Disable the button for the currently chosen language foreach (var entry in _entries) { - if (entry.button != null) - entry.button.Disabled = entry.language == currentLanguage; + if (entry.Button != null) + entry.Button.Disabled = entry.Language == currentLanguage; } } - private void AddLanguageEntry(string language) + private void AddLanguageEntry(ProtoId language) { var proto = _clientLanguageSystem.GetLanguagePrototype(language); - var state = new EntryState { language = language }; + var state = new EntryState { Language = language }; var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical }; @@ -87,7 +90,7 @@ private void AddLanguageEntry(string language) var button = new Button { Text = "Choose" }; button.OnPressed += _ => OnLanguageChosen(language); - state.button = button; + state.Button = button; header.AddChild(name); header.AddChild(button); @@ -125,21 +128,18 @@ private void AddLanguageEntry(string language) _entries.Add(state); } - - private void OnLanguageChosen(string id) + private void OnLanguageChosen(ProtoId id) { - var proto = _clientLanguageSystem.GetLanguagePrototype(id); - if (proto == null) - return; + _clientLanguageSystem.RequestSetLanguage(id); - _clientLanguageSystem.RequestSetLanguage(proto); - UpdateState(id, _clientLanguageSystem.SpokenLanguages); + // Predict the change + if (_clientLanguageSystem.GetLocalSpeaker()?.SpokenLanguages is {} languages) + UpdateState(id, languages); } - private struct EntryState { - public string language; - public Button? button; + public ProtoId Language; + public Button? Button; } } diff --git a/Content.Client/Language/Systems/LanguageSystem.cs b/Content.Client/Language/Systems/LanguageSystem.cs index cb6bb60512b..efdf1f7d7d0 100644 --- a/Content.Client/Language/Systems/LanguageSystem.cs +++ b/Content.Client/Language/Systems/LanguageSystem.cs @@ -1,81 +1,61 @@ using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Language.Systems; -using Robust.Client; +using Robust.Client.Player; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; namespace Content.Client.Language.Systems; -/// -/// Client-side language system. -/// -/// -/// Unlike the server, the client is not aware of other entities' languages; it's only notified about the entity that it posesses. -/// Due to that, this system stores such information in a static manner. -/// public sealed class LanguageSystem : SharedLanguageSystem { - [Dependency] private readonly IBaseClient _client = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; /// - /// The current language of the entity currently possessed by the player. + /// Invoked when the languages of the local player entity change, for use in UI. /// - public string CurrentLanguage { get; private set; } = default!; - /// - /// The list of languages the currently possessed entity can speak. - /// - public List SpokenLanguages { get; private set; } = new(); - /// - /// The list of languages the currently possessed entity can understand. - /// - public List UnderstoodLanguages { get; private set; } = new(); - - public event EventHandler? OnLanguagesChanged; + public event Action? OnLanguagesChanged; public override void Initialize() { - base.Initialize(); - - SubscribeNetworkEvent(OnLanguagesUpdated); - _client.RunLevelChanged += OnRunLevelChanged; + _playerManager.LocalPlayerAttached += NotifyUpdate; + SubscribeLocalEvent(OnHandleState); } - private void OnLanguagesUpdated(LanguagesUpdatedMessage message) + private void OnHandleState(Entity ent, ref ComponentHandleState args) { - // TODO this entire thing is horrible. If someone is willing to refactor this, LanguageSpeakerComponent should become shared with SendOnlyToOwner = true - // That way, this system will be able to use the existing networking infrastructure instead of relying on this makeshift... whatever this is. - CurrentLanguage = message.CurrentLanguage; - SpokenLanguages = message.Spoken; - UnderstoodLanguages = message.Understood; + if (args.Current is not LanguageSpeakerComponent.State state) + return; - OnLanguagesChanged?.Invoke(this, message); - } + ent.Comp.CurrentLanguage = state.CurrentLanguage; + ent.Comp.SpokenLanguages = state.SpokenLanguages; + ent.Comp.UnderstoodLanguages = state.UnderstoodLanguages; - private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs args) - { - // Request an update when entering a game - if (args.NewLevel == ClientRunLevel.InGame) - RequestStateUpdate(); + if (ent.Owner == _playerManager.LocalEntity) + NotifyUpdate(ent); } /// - /// Sends a network request to the server to update this system's state. - /// The server may ignore the said request if the player is not possessing an entity. + /// Returns the LanguageSpeakerComponent of the local player entity. + /// Will return null if the player does not have an entity, or if the client has not yet received the component state. /// - public void RequestStateUpdate() + public LanguageSpeakerComponent? GetLocalSpeaker() { - RaiseNetworkEvent(new RequestLanguagesMessage()); + return CompOrNull(_playerManager.LocalEntity); } - public void RequestSetLanguage(LanguagePrototype language) + public void RequestSetLanguage(ProtoId language) { - if (language.ID == CurrentLanguage) + if (GetLocalSpeaker()?.CurrentLanguage?.Equals(language) == true) return; - RaiseNetworkEvent(new LanguagesSetMessage(language.ID)); + RaiseNetworkEvent(new LanguagesSetMessage(language)); + } - // May cause some minor desync... - // So to reduce the probability of desync, we replicate the change locally too - if (SpokenLanguages.Contains(language.ID)) - CurrentLanguage = language.ID; + private void NotifyUpdate(EntityUid localPlayer) + { + RaiseLocalEvent(localPlayer, new LanguagesUpdateEvent(), broadcast: true); + OnLanguagesChanged?.Invoke(); } } diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index a84e21b997e..21bfe239232 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -1,7 +1,6 @@ using System.Linq; using Content.Server.Ghost.Roles.Components; using Content.Server.Language; -using Content.Server.Language.Events; using Content.Server.Speech.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Language; diff --git a/Content.Server/Cloning/CloningSystem.Utility.cs b/Content.Server/Cloning/CloningSystem.Utility.cs index 3c924d9dc2b..d98e105e631 100644 --- a/Content.Server/Cloning/CloningSystem.Utility.cs +++ b/Content.Server/Cloning/CloningSystem.Utility.cs @@ -21,9 +21,9 @@ using Content.Shared.Damage.ForceSay; using Content.Shared.Chat; using Content.Server.Body.Components; +using Content.Server.Language; using Content.Shared.Abilities.Psionics; using Content.Shared.Language.Components; -using Content.Shared.Language; using Content.Shared.Nutrition.Components; using Robust.Shared.Enums; diff --git a/Content.Server/Language/Commands/AdminLanguageCommand.cs b/Content.Server/Language/Commands/AdminLanguageCommand.cs index f02d9c7f401..2e7a0b193a1 100644 --- a/Content.Server/Language/Commands/AdminLanguageCommand.cs +++ b/Content.Server/Language/Commands/AdminLanguageCommand.cs @@ -3,6 +3,7 @@ using Content.Shared.Language; using Content.Shared.Language.Components; using Content.Shared.Language.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; @@ -62,13 +63,13 @@ public EntityUid RemoveLanguage( } [CommandImplementation("lsspoken")] - public IEnumerable ListSpoken([PipedArgument] EntityUid input) + public IEnumerable> ListSpoken([PipedArgument] EntityUid input) { return Languages.GetSpokenLanguages(input); } [CommandImplementation("lsunderstood")] - public IEnumerable ListUnderstood([PipedArgument] EntityUid input) + public IEnumerable> ListUnderstood([PipedArgument] EntityUid input) { return Languages.GetUnderstoodLanguages(input); } diff --git a/Content.Server/Language/Commands/AdminTranslatorCommand.cs b/Content.Server/Language/Commands/AdminTranslatorCommand.cs index 8a7984bc36b..fc8ee02e386 100644 --- a/Content.Server/Language/Commands/AdminTranslatorCommand.cs +++ b/Content.Server/Language/Commands/AdminTranslatorCommand.cs @@ -6,6 +6,7 @@ using Content.Shared.Language.Components.Translators; using Content.Shared.Language.Systems; using Robust.Server.Containers; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; @@ -107,7 +108,7 @@ public EntityUid RemoveRequiredLanguage( } [CommandImplementation("lsspoken")] - public IEnumerable ListSpoken([PipedArgument] EntityUid input) + public IEnumerable> ListSpoken([PipedArgument] EntityUid input) { if (!TryGetTranslatorComp(input, out var translator)) return []; @@ -115,7 +116,7 @@ public IEnumerable ListSpoken([PipedArgument] EntityUid input) } [CommandImplementation("lsunderstood")] - public IEnumerable ListUnderstood([PipedArgument] EntityUid input) + public IEnumerable> ListUnderstood([PipedArgument] EntityUid input) { if (!TryGetTranslatorComp(input, out var translator)) return []; @@ -123,7 +124,7 @@ public IEnumerable ListUnderstood([PipedArgument] EntityUid input) } [CommandImplementation("lsrequired")] - public IEnumerable ListRequired([PipedArgument] EntityUid input) + public IEnumerable> ListRequired([PipedArgument] EntityUid input) { if (!TryGetTranslatorComp(input, out var translator)) return []; diff --git a/Content.Server/Language/LanguageKnowledgeComponent.cs b/Content.Server/Language/LanguageKnowledgeComponent.cs new file mode 100644 index 00000000000..da8376f7625 --- /dev/null +++ b/Content.Server/Language/LanguageKnowledgeComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.Language; +using Robust.Shared.Prototypes; + +namespace Content.Server.Language; + +/// +/// Stores data about entities' intrinsic language knowledge. +/// +[RegisterComponent] +public sealed partial class LanguageKnowledgeComponent : Component +{ + /// + /// List of languages this entity can speak without any external tools. + /// + [DataField("speaks", required: true)] + public List> SpokenLanguages = new(); + + /// + /// List of languages this entity can understand without any external tools. + /// + [DataField("understands", required: true)] + public List> UnderstoodLanguages = new(); +} diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs deleted file mode 100644 index 5f7f2742734..00000000000 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Content.Server.Language.Events; -using Content.Server.Mind; -using Content.Shared.Language; -using Content.Shared.Language.Components; -using Content.Shared.Language.Events; -using Content.Shared.Mind; -using Content.Shared.Mind.Components; -using Robust.Shared.Player; - -namespace Content.Server.Language; - -public sealed partial class LanguageSystem -{ - [Dependency] private readonly MindSystem _mind = default!; - - - public void InitializeNet() - { - SubscribeNetworkEvent(OnClientSetLanguage); - SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); - - SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); - - // Refresh the client's state when its mind hops to a different entity - SubscribeLocalEvent((uid, _, _) => SendLanguageStateToClient(uid)); - SubscribeLocalEvent((_, _, args) => - { - if (args.Mind.Comp.Session != null) - SendLanguageStateToClient(args.Mind.Comp.Session); - }); - } - - - private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) - { - if (args.SenderSession.AttachedEntity is not { Valid: true } uid) - return; - - var language = GetLanguagePrototype(message.CurrentLanguage); - if (language == null || !CanSpeak(uid, language.ID)) - return; - - SetLanguage(uid, language.ID); - } - - private void SendLanguageStateToClient(EntityUid uid, LanguageSpeakerComponent? comp = null) - { - // Try to find a mind inside the entity and notify its session - if (!_mind.TryGetMind(uid, out _, out var mindComp) || mindComp.Session == null) - return; - - SendLanguageStateToClient(uid, mindComp.Session, comp); - } - - private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerComponent? comp = null) - { - // Try to find an entity associated with the session and resolve the languages from it - if (session.AttachedEntity is not { Valid: true } entity) - return; - - SendLanguageStateToClient(entity, session, comp); - } - - // TODO this is really stupid and can be avoided if we just make everything shared... - private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) - { - var message = !Resolve(uid, ref component, logMissing: false) - ? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]) - : new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); - - RaiseNetworkEvent(message, session); - } -} diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index a1c30997e2d..3ef4caa84d4 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -1,9 +1,10 @@ using System.Linq; -using Content.Server.Language.Events; using Content.Shared.Language; using Content.Shared.Language.Components; +using Content.Shared.Language.Events; using Content.Shared.Language.Systems; -using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; namespace Content.Server.Language; @@ -12,55 +13,85 @@ public sealed partial class LanguageSystem : SharedLanguageSystem public override void Initialize() { base.Initialize(); - InitializeNet(); - SubscribeLocalEvent(OnInitLanguageSpeaker); - SubscribeLocalEvent(OnUniversalInit); - SubscribeLocalEvent(OnUniversalShutdown); + SubscribeLocalEvent(OnInitLanguageSpeaker); + SubscribeLocalEvent(OnGetLanguageState); + SubscribeLocalEvent(OnDetermineUniversalLanguages); + SubscribeNetworkEvent(OnClientSetLanguage); + + SubscribeLocalEvent((uid, _, _) => UpdateEntityLanguages(uid)); + SubscribeLocalEvent((uid, _, _) => UpdateEntityLanguages(uid)); } - private void OnUniversalShutdown(EntityUid uid, UniversalLanguageSpeakerComponent component, ComponentShutdown args) + #region event handling + + private void OnInitLanguageSpeaker(Entity ent, ref MapInitEvent args) { - RemoveLanguage(uid, UniversalPrototype); + if (string.IsNullOrEmpty(ent.Comp.CurrentLanguage)) + ent.Comp.CurrentLanguage = ent.Comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + + UpdateEntityLanguages(ent!); } - private void OnUniversalInit(EntityUid uid, UniversalLanguageSpeakerComponent component, MapInitEvent args) + private void OnGetLanguageState(Entity entity, ref ComponentGetState args) { - AddLanguage(uid, UniversalPrototype); + args.State = new LanguageSpeakerComponent.State + { + CurrentLanguage = entity.Comp.CurrentLanguage, + SpokenLanguages = entity.Comp.SpokenLanguages, + UnderstoodLanguages = entity.Comp.UnderstoodLanguages + }; } - #region public api + private void OnDetermineUniversalLanguages(Entity entity, ref DetermineEntityLanguagesEvent ev) + { + // We only add it as a spoken language; CanUnderstand checks for ULSC itself. + if (entity.Comp.Enabled) + ev.SpokenLanguages.Add(UniversalPrototype); + } - public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerComponent? component = null) + + private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) { - if (language == UniversalPrototype || HasComp(listener)) - return true; + if (args.SenderSession.AttachedEntity is not { Valid: true } uid) + return; - if (!Resolve(listener, ref component, logMissing: false)) - return false; + var language = GetLanguagePrototype(message.CurrentLanguage); + if (language == null || !CanSpeak(uid, language.ID)) + return; - return component.UnderstoodLanguages.Contains(language); + SetLanguage(uid, language.ID); } - public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) + #endregion + + #region public api + + public bool CanUnderstand(Entity ent, ProtoId language) { - if (HasComp(speaker)) + if (language == UniversalPrototype || TryComp(ent, out var uni) && uni.Enabled) return true; - if (!Resolve(speaker, ref component, logMissing: false)) + return Resolve(ent, ref ent.Comp, logMissing: false) && ent.Comp.UnderstoodLanguages.Contains(language); + } + + public bool CanSpeak(Entity ent, ProtoId language) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) return false; - return component.SpokenLanguages.Contains(language); + return ent.Comp.SpokenLanguages.Contains(language); } /// /// Returns the current language of the given entity, assumes Universal if it's not a language speaker. /// - public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) + public LanguagePrototype GetLanguage(Entity ent) { - if (!Resolve(speaker, ref component, logMissing: false) - || string.IsNullOrEmpty(component.CurrentLanguage) - || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) + if (!Resolve(ent, ref ent.Comp, logMissing: false) + || string.IsNullOrEmpty(ent.Comp.CurrentLanguage) + || !_prototype.TryIndex(ent.Comp.CurrentLanguage, out var proto) + ) return Universal; return proto; @@ -69,36 +100,31 @@ public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent /// /// Returns the list of languages this entity can speak. /// - /// Typically, checking is sufficient. - public List GetSpokenLanguages(EntityUid uid) + /// This simply returns the value of . + public List> GetSpokenLanguages(EntityUid uid) { - if (!TryComp(uid, out var component)) - return []; - - return component.SpokenLanguages; + return TryComp(uid, out var component) ? component.SpokenLanguages : []; } /// /// Returns the list of languages this entity can understand. - /// - /// Typically, checking is sufficient. - public List GetUnderstoodLanguages(EntityUid uid) + /// This simply returns the value of . + public List> GetUnderstoodLanguages(EntityUid uid) { - if (!TryComp(uid, out var component)) - return []; - - return component.UnderstoodLanguages; + return TryComp(uid, out var component) ? component.UnderstoodLanguages : []; } - public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) + public void SetLanguage(Entity ent, ProtoId language) { - if (!CanSpeak(speaker, language) - || !Resolve(speaker, ref component) - || component.CurrentLanguage == language) + if (!CanSpeak(ent, language) + || !Resolve(ent, ref ent.Comp) + || ent.Comp.CurrentLanguage == language) return; - component.CurrentLanguage = language; - RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true); + ent.Comp.CurrentLanguage = language; + RaiseLocalEvent(ent, new LanguagesUpdateEvent(), true); + Dirty(ent); } /// @@ -106,12 +132,12 @@ public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerCompo /// public void AddLanguage( EntityUid uid, - string language, + ProtoId language, bool addSpoken = true, bool addUnderstood = true) { EnsureComp(uid, out var knowledge); - EnsureComp(uid); + EnsureComp(uid, out var speaker); if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) knowledge.SpokenLanguages.Add(language); @@ -119,28 +145,29 @@ public void AddLanguage( if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) knowledge.UnderstoodLanguages.Add(language); - UpdateEntityLanguages(uid); + UpdateEntityLanguages((uid, speaker)); } /// /// Removes a language from the respective lists of intrinsically known languages of the given entity. /// public void RemoveLanguage( - EntityUid uid, - string language, + Entity ent, + ProtoId language, bool removeSpoken = true, bool removeUnderstood = true) { - if (!TryComp(uid, out var knowledge)) + if (!Resolve(ent, ref ent.Comp, false)) return; if (removeSpoken) - knowledge.SpokenLanguages.Remove(language); + ent.Comp.SpokenLanguages.Remove(language); if (removeUnderstood) - knowledge.UnderstoodLanguages.Remove(language); + ent.Comp.UnderstoodLanguages.Remove(language); - UpdateEntityLanguages(uid); + // We don't ensure that the entity has a speaker comp. If it doesn't... Well, woe be the caller of this method. + UpdateEntityLanguages(ent.Owner); } /// @@ -148,15 +175,16 @@ public void RemoveLanguage( /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. /// /// True if the current language was modified, false otherwise. - public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + public bool EnsureValidLanguage(Entity ent) { - if (!Resolve(entity, ref comp)) + if (!Resolve(ent, ref ent.Comp, false)) return false; - if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage)) + if (!ent.Comp.SpokenLanguages.Contains(ent.Comp.CurrentLanguage)) { - comp.CurrentLanguage = comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); - RaiseLocalEvent(entity, new LanguagesUpdateEvent()); + ent.Comp.CurrentLanguage = ent.Comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + RaiseLocalEvent(ent, new LanguagesUpdateEvent()); + Dirty(ent); return true; } @@ -166,14 +194,14 @@ public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp /// /// Immediately refreshes the cached lists of spoken and understood languages for the given entity. /// - public void UpdateEntityLanguages(EntityUid entity) + public void UpdateEntityLanguages(Entity ent) { - if (!TryComp(entity, out var languages)) + if (!Resolve(ent, ref ent.Comp, false)) return; var ev = new DetermineEntityLanguagesEvent(); // We add the intrinsically known languages first so other systems can manipulate them easily - if (TryComp(entity, out var knowledge)) + if (TryComp(ent, out var knowledge)) { foreach (var spoken in knowledge.SpokenLanguages) ev.SpokenLanguages.Add(spoken); @@ -182,28 +210,19 @@ public void UpdateEntityLanguages(EntityUid entity) ev.UnderstoodLanguages.Add(understood); } - RaiseLocalEvent(entity, ref ev); + RaiseLocalEvent(ent, ref ev); - languages.SpokenLanguages.Clear(); - languages.UnderstoodLanguages.Clear(); + ent.Comp.SpokenLanguages.Clear(); + ent.Comp.UnderstoodLanguages.Clear(); - languages.SpokenLanguages.AddRange(ev.SpokenLanguages); - languages.UnderstoodLanguages.AddRange(ev.UnderstoodLanguages); + ent.Comp.SpokenLanguages.AddRange(ev.SpokenLanguages); + ent.Comp.UnderstoodLanguages.AddRange(ev.UnderstoodLanguages); - if (!EnsureValidLanguage(entity)) - RaiseLocalEvent(entity, new LanguagesUpdateEvent()); - } - - #endregion - - #region event handling - - private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) - { - if (string.IsNullOrEmpty(component.CurrentLanguage)) - component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); + // If EnsureValidLanguage returns true, it also raises a LanguagesUpdateEvent, so we try to avoid raising it twice in that case. + if (!EnsureValidLanguage(ent)) + RaiseLocalEvent(ent, new LanguagesUpdateEvent()); - UpdateEntityLanguages(uid); + Dirty(ent); } #endregion diff --git a/Content.Server/Language/LanguagesUpdateEvent.cs b/Content.Server/Language/LanguagesUpdateEvent.cs deleted file mode 100644 index 88ea09916bb..00000000000 --- a/Content.Server/Language/LanguagesUpdateEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Content.Server.Language.Events; - -/// -/// Raised on an entity when its list of languages changes. -/// -public sealed class LanguagesUpdateEvent : EntityEventArgs -{ -} diff --git a/Content.Server/Language/TranslatorImplantSystem.cs b/Content.Server/Language/TranslatorImplantSystem.cs index 4d58144481d..439389477c5 100644 --- a/Content.Server/Language/TranslatorImplantSystem.cs +++ b/Content.Server/Language/TranslatorImplantSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Implants.Components; using Content.Shared.Language; using Content.Shared.Language.Components; +using Content.Shared.Language.Events; using Robust.Shared.Containers; namespace Content.Server.Language; diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index c48b93a3930..5cb5c8cd2e9 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -8,7 +8,9 @@ using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; +using Content.Shared.Language.Events; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Language; @@ -112,7 +114,7 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen // Update the current language of the entity if necessary if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {}) - _language.SetLanguage(holder, firstNewLanguage, languageComp); + _language.SetLanguage((holder, languageComp), firstNewLanguage); } OnAppearanceChange(translator, translatorComp); @@ -152,7 +154,7 @@ private void CopyLanguages(BaseTranslatorComponent from, DetermineEntityLanguage /// /// Checks whether any OR all required languages are provided. Used for utility purposes. /// - public static bool CheckLanguagesMatch(ICollection required, ICollection provided, bool requireAll) + public static bool CheckLanguagesMatch(ICollection> required, ICollection> provided, bool requireAll) { if (required.Count == 0) return true; diff --git a/Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs b/Content.Server/Language/UniversalLanguageSpeakerComponent.cs similarity index 74% rename from Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs rename to Content.Server/Language/UniversalLanguageSpeakerComponent.cs index 6f5ad1178b8..ee43450335a 100644 --- a/Content.Shared/Language/Components/UniversalLanguageSpeakerComponent.cs +++ b/Content.Server/Language/UniversalLanguageSpeakerComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Shared.Language.Components; +namespace Content.Server.Language; // // Signifies that this entity can speak and understand any language. @@ -7,5 +7,6 @@ namespace Content.Shared.Language.Components; [RegisterComponent] public sealed partial class UniversalLanguageSpeakerComponent : Component { - + [DataField] + public bool Enabled = true; } diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index b58d782d9c5..450f0712a14 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -3,7 +3,7 @@ using Content.Shared.Administration; using Content.Shared.Emoting; using Content.Shared.Examine; -using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Content.Shared.Movement.Components; diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs index 2c7274a13d5..ce9fb51a031 100644 --- a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -38,7 +38,7 @@ private void OnSpawn(Entity entity, ref ComponentInit a } var alternateLanguage = knowledge.SpokenLanguages.Find(it => it != entity.Comp.BaseLanguage); - if (alternateLanguage == null) + if (alternateLanguage == default) { Log.Warning($"Entity {entity.Owner} does not have an alternative language to choose from (must have at least one non-GC for ForeignerTrait)!"); return; @@ -46,12 +46,12 @@ private void OnSpawn(Entity entity, ref ComponentInit a if (TryGiveTranslator(entity.Owner, entity.Comp.BaseTranslator, entity.Comp.BaseLanguage, alternateLanguage, out var translator)) { - _languages.RemoveLanguage(entity, entity.Comp.BaseLanguage, entity.Comp.CantSpeak, entity.Comp.CantUnderstand); + _languages.RemoveLanguage(entity.Owner, entity.Comp.BaseLanguage, entity.Comp.CantSpeak, entity.Comp.CantUnderstand); } } /// - /// Tries to create and give the entity a translator to translator that translates speech between the two specified languages. + /// Tries to create and give the entity a translator that translates speech between the two specified languages. /// public bool TryGiveTranslator( EntityUid uid, diff --git a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs deleted file mode 100644 index ddbdc742be4..00000000000 --- a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Language.Components; - -// TODO: move to server side, it's never synchronized! - -/// -/// Stores data about entities' intrinsic language knowledge. -/// -[RegisterComponent] -public sealed partial class LanguageKnowledgeComponent : Component -{ - /// - /// List of languages this entity can speak without any external tools. - /// - [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List SpokenLanguages = new(); - - /// - /// List of languages this entity can understand without any external tools. - /// - [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List UnderstoodLanguages = new(); -} diff --git a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs index e8ebccb3ddf..f026361cadf 100644 --- a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs +++ b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs @@ -1,7 +1,9 @@ -namespace Content.Shared.Language; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations; -// TODO: either move all language speaker-related components to server side, or make everything else shared. -// The current approach leads to confusion, as the server never informs the client of updates in these components. +namespace Content.Shared.Language.Components; /// /// Stores the current state of the languages the entity can speak and understand. @@ -10,23 +12,35 @@ namespace Content.Shared.Language; /// All fields of this component are populated during a DetermineEntityLanguagesEvent. /// They are not to be modified externally. /// -[RegisterComponent] +[RegisterComponent, NetworkedComponent] public sealed partial class LanguageSpeakerComponent : Component { + public override bool SendOnlyToOwner => true; + /// /// The current language the entity uses when speaking. /// Other listeners will hear the entity speak in this language. /// [DataField] - public string CurrentLanguage = ""; // The language system will override it on init + public string CurrentLanguage = ""; // The language system will override it on mapinit /// /// List of languages this entity can speak at the current moment. /// - public List SpokenLanguages = []; + [DataField] + public List> SpokenLanguages = []; /// /// List of languages this entity can understand at the current moment. /// - public List UnderstoodLanguages = []; + [DataField] + public List> UnderstoodLanguages = []; + + [Serializable, NetSerializable] + public sealed class State : ComponentState + { + public string CurrentLanguage = default!; + public List> SpokenLanguages = default!; + public List> UnderstoodLanguages = default!; + } } diff --git a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs index 072480031d5..8bd1f6be488 100644 --- a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs @@ -1,3 +1,4 @@ +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; namespace Content.Shared.Language.Components.Translators; @@ -7,21 +8,21 @@ public abstract partial class BaseTranslatorComponent : Component /// /// The list of additional languages this translator allows the wielder to speak. /// - [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List SpokenLanguages = new(); + [DataField("spoken")] + public List> SpokenLanguages = new(); /// /// The list of additional languages this translator allows the wielder to understand. /// - [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List UnderstoodLanguages = new(); + [DataField("understood")] + public List> UnderstoodLanguages = new(); /// /// The languages the wielding MUST know in order for this translator to have effect. /// The field [RequiresAllLanguages] indicates whether all of them are required, or just one. /// - [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List RequiredLanguages = new(); + [DataField("requires")] + public List> RequiredLanguages = new(); /// /// If true, the wielder must understand all languages in [RequiredLanguages] to speak [SpokenLanguages], @@ -30,9 +31,8 @@ public abstract partial class BaseTranslatorComponent : Component /// Otherwise, at least one language must be known (or the list must be empty). /// [DataField("requiresAll")] - [ViewVariables(VVAccess.ReadWrite)] public bool RequiresAllLanguages = false; - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] + [DataField("enabled")] public bool Enabled = true; } diff --git a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs index 7e3de0eca61..6b2f434fa70 100644 --- a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs @@ -4,7 +4,7 @@ namespace Content.Shared.Language.Components.Translators; /// A translator that must be held in a hand or a pocket of an entity in order ot have effect. /// [RegisterComponent] -public sealed partial class HandheldTranslatorComponent : Translators.BaseTranslatorComponent +public sealed partial class HandheldTranslatorComponent : BaseTranslatorComponent { /// /// Whether interacting with this translator toggles it on and off. diff --git a/Content.Server/Language/DetermineEntityLanguagesEvent.cs b/Content.Shared/Language/Events/DetermineEntityLanguagesEvent.cs similarity index 77% rename from Content.Server/Language/DetermineEntityLanguagesEvent.cs rename to Content.Shared/Language/Events/DetermineEntityLanguagesEvent.cs index 8d6b868d070..a01e5613f82 100644 --- a/Content.Server/Language/DetermineEntityLanguagesEvent.cs +++ b/Content.Shared/Language/Events/DetermineEntityLanguagesEvent.cs @@ -1,6 +1,6 @@ -using Content.Shared.Language; +using Robust.Shared.Prototypes; -namespace Content.Server.Language; +namespace Content.Shared.Language.Events; /// /// Raised in order to determine the list of languages the entity can speak and understand at the given moment. @@ -13,13 +13,13 @@ public record struct DetermineEntityLanguagesEvent /// The list of all languages the entity may speak. /// By default, contains the languages this entity speaks intrinsically. /// - public HashSet SpokenLanguages = new(); + public HashSet> SpokenLanguages = new(); /// /// The list of all languages the entity may understand. /// By default, contains the languages this entity understands intrinsically. /// - public HashSet UnderstoodLanguages = new(); + public HashSet> UnderstoodLanguages = new(); public DetermineEntityLanguagesEvent() {} } diff --git a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs b/Content.Shared/Language/Events/LanguagesUpdateEvent.cs new file mode 100644 index 00000000000..fa68bf5af6f --- /dev/null +++ b/Content.Shared/Language/Events/LanguagesUpdateEvent.cs @@ -0,0 +1,12 @@ +namespace Content.Shared.Language.Events; + +/// +/// Raised on an entity when its list of languages changes. +/// +/// +/// This is raised both on the server and on the client. +/// The client raises it broadcast after receiving a new language comp state from the server. +/// +public sealed class LanguagesUpdateEvent : EntityEventArgs +{ +} diff --git a/Content.Shared/Language/Events/LanguagesUpdatedMessage.cs b/Content.Shared/Language/Events/LanguagesUpdatedMessage.cs deleted file mode 100644 index 563f036df6d..00000000000 --- a/Content.Shared/Language/Events/LanguagesUpdatedMessage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Language.Events; - -/// -/// Sent to the client when its list of languages changes. -/// The client should in turn update its HUD and relevant systems. -/// -[Serializable, NetSerializable] -public sealed class LanguagesUpdatedMessage(string currentLanguage, List spoken, List understood) : EntityEventArgs -{ - public string CurrentLanguage = currentLanguage; - public List Spoken = spoken; - public List Understood = understood; -} diff --git a/Content.Shared/Language/Events/RequestLanguagesMessage.cs b/Content.Shared/Language/Events/RequestLanguagesMessage.cs deleted file mode 100644 index aead1f4cd1a..00000000000 --- a/Content.Shared/Language/Events/RequestLanguagesMessage.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Language.Events; - -/// -/// Sent from the client to the server when it needs to learn the list of languages its entity knows. -/// This event should always be followed by a , unless the client doesn't have an entity. -/// -[Serializable, NetSerializable] -public sealed class RequestLanguagesMessage : EntityEventArgs; diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index d9e882147c0..86a58dc2a7c 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -31,9 +31,9 @@ public override void Initialize() Universal = _prototype.Index("Universal"); } - public LanguagePrototype? GetLanguagePrototype(string id) + public LanguagePrototype? GetLanguagePrototype(ProtoId id) { - _prototype.TryIndex(id, out var proto); + _prototype.TryIndex(id, out var proto); return proto; } @@ -43,8 +43,7 @@ public override void Initialize() public string ObfuscateSpeech(string message, LanguagePrototype language) { var builder = new StringBuilder(); - var method = language.Obfuscation; - method.Obfuscate(builder, message, this); + language.Obfuscation.Obfuscate(builder, message, this); return builder.ToString(); }