Skip to content

Commit

Permalink
Language Refactor 3 (DeltaV-Station#937)
Browse files Browse the repository at this point in the history
# 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<LanguagePrototype> type instead of raw
strings (god, I hated those so much)
- The server-side language system now accepts Entity<T?> 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

<!--
TODO MEDIA

<details><summary><h1>Media</h1></summary>
<p>

![Example Media Embed](https://example.com/thisimageisntreal.png)

</p>
</details>

-->

# Changelog
No cl

---------

Co-authored-by: VMSolidus <[email protected]>
  • Loading branch information
Mnemotechnician and VMSolidus authored Oct 9, 2024
1 parent 14d2280 commit 8c5faf3
Show file tree
Hide file tree
Showing 24 changed files with 247 additions and 325 deletions.
56 changes: 28 additions & 28 deletions Content.Client/Language/LanguageMenuWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -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<EntryState> _entries = new();


public LanguageMenuWindow()
{
RobustXamlLoader.Load(this);
_clientLanguageSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<LanguageSystem>();

_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<string> spokenLanguages)
public void UpdateState(ProtoId<LanguagePrototype> currentLanguage, List<ProtoId<LanguagePrototype>> spokenLanguages)
{
var langName = Loc.GetString($"language-{currentLanguage}-name");
CurrentLanguageLabel.Text = Loc.GetString("language-menu-current-language", ("language", langName));
Expand All @@ -58,15 +61,15 @@ public void UpdateState(string currentLanguage, List<string> 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<LanguagePrototype> 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 };

Expand All @@ -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);
Expand Down Expand Up @@ -125,21 +128,18 @@ private void AddLanguageEntry(string language)
_entries.Add(state);
}


private void OnLanguageChosen(string id)
private void OnLanguageChosen(ProtoId<LanguagePrototype> 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<LanguagePrototype> Language;
public Button? Button;
}
}
78 changes: 29 additions & 49 deletions Content.Client/Language/Systems/LanguageSystem.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Client-side language system.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public sealed class LanguageSystem : SharedLanguageSystem
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;

/// <summary>
/// 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.
/// </summary>
public string CurrentLanguage { get; private set; } = default!;
/// <summary>
/// The list of languages the currently possessed entity can speak.
/// </summary>
public List<string> SpokenLanguages { get; private set; } = new();
/// <summary>
/// The list of languages the currently possessed entity can understand.
/// </summary>
public List<string> UnderstoodLanguages { get; private set; } = new();

public event EventHandler<LanguagesUpdatedMessage>? OnLanguagesChanged;
public event Action? OnLanguagesChanged;

public override void Initialize()
{
base.Initialize();

SubscribeNetworkEvent<LanguagesUpdatedMessage>(OnLanguagesUpdated);
_client.RunLevelChanged += OnRunLevelChanged;
_playerManager.LocalPlayerAttached += NotifyUpdate;
SubscribeLocalEvent<LanguageSpeakerComponent, ComponentHandleState>(OnHandleState);
}

private void OnLanguagesUpdated(LanguagesUpdatedMessage message)
private void OnHandleState(Entity<LanguageSpeakerComponent> 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);
}

/// <summary>
/// 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.
/// </summary>
public void RequestStateUpdate()
public LanguageSpeakerComponent? GetLocalSpeaker()
{
RaiseNetworkEvent(new RequestLanguagesMessage());
return CompOrNull<LanguageSpeakerComponent>(_playerManager.LocalEntity);
}

public void RequestSetLanguage(LanguagePrototype language)
public void RequestSetLanguage(ProtoId<LanguagePrototype> 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();
}
}
1 change: 0 additions & 1 deletion Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Content.Server/Cloning/CloningSystem.Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
5 changes: 3 additions & 2 deletions Content.Server/Language/Commands/AdminLanguageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -62,13 +63,13 @@ public EntityUid RemoveLanguage(
}

[CommandImplementation("lsspoken")]
public IEnumerable<string> ListSpoken([PipedArgument] EntityUid input)
public IEnumerable<ProtoId<LanguagePrototype>> ListSpoken([PipedArgument] EntityUid input)
{
return Languages.GetSpokenLanguages(input);
}

[CommandImplementation("lsunderstood")]
public IEnumerable<string> ListUnderstood([PipedArgument] EntityUid input)
public IEnumerable<ProtoId<LanguagePrototype>> ListUnderstood([PipedArgument] EntityUid input)
{
return Languages.GetUnderstoodLanguages(input);
}
Expand Down
7 changes: 4 additions & 3 deletions Content.Server/Language/Commands/AdminTranslatorCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,23 +108,23 @@ public EntityUid RemoveRequiredLanguage(
}

[CommandImplementation("lsspoken")]
public IEnumerable<string> ListSpoken([PipedArgument] EntityUid input)
public IEnumerable<ProtoId<LanguagePrototype>> ListSpoken([PipedArgument] EntityUid input)
{
if (!TryGetTranslatorComp(input, out var translator))
return [];
return translator.SpokenLanguages;
}

[CommandImplementation("lsunderstood")]
public IEnumerable<string> ListUnderstood([PipedArgument] EntityUid input)
public IEnumerable<ProtoId<LanguagePrototype>> ListUnderstood([PipedArgument] EntityUid input)
{
if (!TryGetTranslatorComp(input, out var translator))
return [];
return translator.UnderstoodLanguages;
}

[CommandImplementation("lsrequired")]
public IEnumerable<string> ListRequired([PipedArgument] EntityUid input)
public IEnumerable<ProtoId<LanguagePrototype>> ListRequired([PipedArgument] EntityUid input)
{
if (!TryGetTranslatorComp(input, out var translator))
return [];
Expand Down
23 changes: 23 additions & 0 deletions Content.Server/Language/LanguageKnowledgeComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Content.Shared.Language;
using Robust.Shared.Prototypes;

namespace Content.Server.Language;

/// <summary>
/// Stores data about entities' intrinsic language knowledge.
/// </summary>
[RegisterComponent]
public sealed partial class LanguageKnowledgeComponent : Component
{
/// <summary>
/// List of languages this entity can speak without any external tools.
/// </summary>
[DataField("speaks", required: true)]
public List<ProtoId<LanguagePrototype>> SpokenLanguages = new();

/// <summary>
/// List of languages this entity can understand without any external tools.
/// </summary>
[DataField("understands", required: true)]
public List<ProtoId<LanguagePrototype>> UnderstoodLanguages = new();
}
Loading

0 comments on commit 8c5faf3

Please sign in to comment.