Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the Language System #459

Merged
merged 19 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
45c1bf0
Discover polymorphism
Mnemotechnician Jun 13, 2024
3df0c63
Apply polymorphism + fix some obfuscation phrases
Mnemotechnician Jun 13, 2024
f0af64c
Oops
Mnemotechnician Jun 14, 2024
86eaa73
Cleaned up imports
Mnemotechnician Jun 14, 2024
6770f30
Refactored and localized commands; made them more user-friendly.
Mnemotechnician Jun 14, 2024
e4f5917
Added HandheldTranslatorComponent.SetLanguageOnInteract for convenience
Mnemotechnician Jun 14, 2024
ad56d53
Refactor everything and pray it works:
Mnemotechnician Jun 15, 2024
0e63714
Update yaml prototypes accordingly
Mnemotechnician Jun 15, 2024
44b625f
Move LanguagePrototype.Replacement to ObfuscationMethod and remove Ob…
Mnemotechnician Jun 15, 2024
2f05074
Do not log missing LanguageSpeakerComponents
Mnemotechnician Jun 18, 2024
5a2dc13
Rewrite translator implants to use the existing implant infrastructure
Mnemotechnician Jun 18, 2024
b3545d0
Update the language menu when choosing a new language
Mnemotechnician Jun 18, 2024
734e764
Fix handheld translators adjusting your current language
Mnemotechnician Jun 18, 2024
31d82fb
I fixes
Mnemotechnician Jun 18, 2024
fe43060
Merge branch 'master' into refactor/languages
Mnemotechnician Jun 21, 2024
19667eb
Merge branch 'master' into refactor/languages
Mnemotechnician Jun 23, 2024
6adc50d
Merge branch 'Simple-Station:master' into refactor/languages
Mnemotechnician Jun 27, 2024
d9d74de
Minor improvements
Mnemotechnician Jun 27, 2024
43e4dfb
Fix markup in chat messages. Oops...
Mnemotechnician Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions Content.Client/Language/LanguageMenuWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
using Content.Client.Language.Systems;
using Content.Shared.Language;
using Content.Shared.Language.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
using Robust.Shared.Utility;
using Serilog;
using static Content.Shared.Language.Systems.SharedLanguageSystem;

namespace Content.Client.Language;

Expand Down Expand Up @@ -121,8 +115,11 @@ private void AddLanguageEntry(string language)
private void OnLanguageChosen(string id)
{
var proto = _clientLanguageSystem.GetLanguagePrototype(id);
if (proto != null)
_clientLanguageSystem.RequestSetLanguage(proto);
if (proto == null)
return;

_clientLanguageSystem.RequestSetLanguage(proto);
UpdateState(id, _clientLanguageSystem.SpokenLanguages);
}


Expand Down
1 change: 0 additions & 1 deletion Content.Client/Language/Systems/LanguageSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Content.Shared.Language.Events;
using Content.Shared.Language.Systems;
using Robust.Client;
using Robust.Shared.Console;

namespace Content.Client.Language.Systems;

Expand Down
8 changes: 0 additions & 8 deletions Content.Client/Language/Systems/TranslatorImplanterSystem.cs

This file was deleted.

6 changes: 3 additions & 3 deletions Content.Server/Chat/Systems/ChatSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ private void SendEntityWhisper(
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.

var canUnderstandLanguage = _language.CanUnderstand(listener, language);
var canUnderstandLanguage = _language.CanUnderstand(listener, language.ID);
// How the entity perceives the message depends on whether it can understand its language
var perceivedMessage = canUnderstandLanguage ? message : languageObfuscatedMessage;

Expand Down Expand Up @@ -717,7 +717,7 @@ private void SendInVoiceRange(ChatChannel channel, string name, string message,


// If the channel does not support languages, or the entity can understand the message, send the original message, otherwise send the obfuscated version
if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language))
if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language.ID))
{
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author);
}
Expand Down Expand Up @@ -846,7 +846,7 @@ public string WrapPublicMessage(EntityUid source, string name, string message)
("verb", verbName),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("message", FormattedMessage.EscapeText(message)));
("message", message));
}

/// <summary>
Expand Down
23 changes: 13 additions & 10 deletions Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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;
using Content.Shared.Language.Systems;
using Content.Shared.Mind.Components;
using Robust.Shared.Prototypes;
using Content.Server.Psionics; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic.
using Content.Shared.Humanoid; //Delta-V - Banning humanoids from becoming ghost roles.
using Content.Server.Psionics;
using Content.Shared.Body.Part; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic.
using Content.Shared.Humanoid;
using Content.Shared.Language.Components; //Delta-V - Banning humanoids from becoming ghost roles.
using Content.Shared.Language.Events;

namespace Content.Server.Chemistry.ReagentEffects;
Expand All @@ -28,19 +32,18 @@ public override void Effect(ReagentEffectArgs args)
entityManager.RemoveComponent<ReplacementAccentComponent>(uid);
entityManager.RemoveComponent<MonkeyAccentComponent>(uid);

// Make sure the entity knows at least fallback (Galactic Common)
var speaker = entityManager.EnsureComponent<LanguageSpeakerComponent>(uid);
var knowledge = entityManager.EnsureComponent<LanguageKnowledgeComponent>(uid);
var fallback = SharedLanguageSystem.FallbackLanguagePrototype;

if (!speaker.UnderstoodLanguages.Contains(fallback))
speaker.UnderstoodLanguages.Add(fallback);
if (!knowledge.UnderstoodLanguages.Contains(fallback))
knowledge.UnderstoodLanguages.Add(fallback);

if (!speaker.SpokenLanguages.Contains(fallback))
{
speaker.CurrentLanguage = fallback;
speaker.SpokenLanguages.Add(fallback);
}
if (!knowledge.SpokenLanguages.Contains(fallback))
knowledge.SpokenLanguages.Add(fallback);

args.EntityManager.EventBus.RaiseLocalEvent(uid, new LanguagesUpdateEvent(), true);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<LanguageSystem>().UpdateEntityLanguages(uid, speaker);

// Stops from adding a ghost role to things like people who already have a mind
if (entityManager.TryGetComponent<MindContainerComponent>(uid, out var mindContainer) && mindContainer.HasMind)
Expand Down
27 changes: 23 additions & 4 deletions Content.Server/Language/Commands/ListLanguagesCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Linq;
using Content.Shared.Administration;
using Content.Shared.Language;
using Robust.Shared.Console;
using Robust.Shared.Enums;

Expand Down Expand Up @@ -30,10 +30,29 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
}

var languages = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<LanguageSystem>();
var currentLang = languages.GetLanguage(playerEntity).ID;

var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity);
shell.WriteLine(Loc.GetString("command-language-spoken"));
var spoken = languages.GetSpokenLanguages(playerEntity);
for (int i = 0; i < spoken.Count; i++)
{
var lang = spoken[i];
shell.WriteLine(lang == currentLang
? Loc.GetString("command-language-current-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))
: Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))));
}

shell.WriteLine("Spoken:\n" + string.Join("\n", spokenLangs));
shell.WriteLine("Understood:\n" + string.Join("\n", knownLangs));
shell.WriteLine(Loc.GetString("command-language-understood"));
var understood = languages.GetUnderstoodLanguages(playerEntity);
for (int i = 0; i < understood.Count; i++)
{
var lang = understood[i];
shell.WriteLine(Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))));
}
}

private string LanguageName(string id)
{
return Loc.GetString($"language-{id}-name");
}
}
6 changes: 2 additions & 4 deletions Content.Server/Language/Commands/SayLanguageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
if (args.Length < 2)
return;

var languageId = args[0];
var message = string.Join(" ", args, startIndex: 1, count: args.Length - 1).Trim();

if (string.IsNullOrEmpty(message))
Expand All @@ -41,10 +40,9 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
var languages = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<LanguageSystem>();
var chats = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();

var language = languages.GetLanguagePrototype(languageId);
if (language == null || !languages.CanSpeak(playerEntity, language.ID))
if (!SelectLanguageCommand.TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language))
{
shell.WriteError($"Language {languageId} is invalid or you cannot speak it!");
shell.WriteError(failReason);
return;
}

Expand Down
50 changes: 45 additions & 5 deletions Content.Server/Language/Commands/SelectLanguageCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Administration;
using Content.Shared.Language;
using Robust.Shared.Console;
using Robust.Shared.Enums;

Expand Down Expand Up @@ -32,17 +34,55 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
if (args.Length < 1)
return;

var languageId = args[0];

var languages = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<LanguageSystem>();

var language = languages.GetLanguagePrototype(languageId);
if (language == null || !languages.CanSpeak(playerEntity, language.ID))
if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language))
{
shell.WriteError($"Language {languageId} is invalid or you cannot speak it!");
shell.WriteError(failReason);
return;
}

languages.SetLanguage(playerEntity, language.ID);
}

// TODO: find a better place for this method
/// <summary>
/// Tries to parse the input argument as either a language ID or the position of the language in the list of languages
/// the entity can speak. Returns true if sucessful.
/// </summary>
public static bool TryParseLanguageArgument(
LanguageSystem languageSystem,
EntityUid speaker,
string input,
[NotNullWhen(false)] out string? failureReason,
[NotNullWhen(true)] out LanguagePrototype? language)
{
failureReason = null;
language = null;

if (int.TryParse(input, out var num))
{
// The argument is a number
var spoken = languageSystem.GetSpokenLanguages(speaker);
if (num > 0 && num - 1 < spoken.Count)
language = languageSystem.GetLanguagePrototype(spoken[num - 1]);

if (language != null) // the ability to speak it is implied
return true;

failureReason = Loc.GetString("command-language-invalid-number", ("total", spoken.Count));
return false;
}
else
{
// The argument is a language ID
language = languageSystem.GetLanguagePrototype(input);

if (language != null && languageSystem.CanSpeak(speaker, language.ID))
return true;

failureReason = Loc.GetString("command-language-invalid-language", ("id", input));
return false;
}
}
}
32 changes: 14 additions & 18 deletions Content.Server/Language/DetermineEntityLanguagesEvent.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
using Content.Shared.Language;

namespace Content.Server.Language;

/// <summary>
/// Raised in order to determine the language an entity speaks at the current moment,
/// as well as the list of all languages the entity may speak and understand.
/// Raised in order to determine the list of languages the entity can speak and understand at the given moment.
/// Typically raised on an entity after a language agent (e.g. a translator) has been added to or removed from them.
/// </summary>
public sealed class DetermineEntityLanguagesEvent : EntityEventArgs
[ByRefEvent]
public record struct DetermineEntityLanguagesEvent
{
/// <summary>
/// The default language of this entity. If empty, remain unchanged.
/// This field has no effect if the entity decides to speak in a concrete language.
/// </summary>
public string CurrentLanguage;
/// <summary>
/// The list of all languages the entity may speak. Must NOT be held as a reference!
/// The list of all languages the entity may speak.
/// By default, contains the languages this entity speaks intrinsically.
/// </summary>
public List<string> SpokenLanguages;
public HashSet<string> SpokenLanguages = new();

/// <summary>
/// The list of all languages the entity may understand. Must NOT be held as a reference!
/// The list of all languages the entity may understand.
/// By default, contains the languages this entity understands intrinsically.
/// </summary>
public List<string> UnderstoodLanguages;
public HashSet<string> UnderstoodLanguages = new();

public DetermineEntityLanguagesEvent(string currentLanguage, List<string> spokenLanguages, List<string> understoodLanguages)
{
CurrentLanguage = currentLanguage;
SpokenLanguages = spokenLanguages;
UnderstoodLanguages = understoodLanguages;
}
public DetermineEntityLanguagesEvent() {}
}
39 changes: 29 additions & 10 deletions Content.Server/Language/LanguageSystem.Networking.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,48 @@
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;

/// <summary>
/// LanguageSystem Networking
/// This is used to update client state when mind change entity.
/// </summary>

public sealed partial class LanguageSystem
{
[Dependency] private readonly MindSystem _mind = default!;


public void InitializeNet()
{
SubscribeNetworkEvent<LanguagesSetMessage>(OnClientSetLanguage);
SubscribeNetworkEvent<RequestLanguagesMessage>((_, session) => SendLanguageStateToClient(session.SenderSession));

SubscribeLocalEvent<LanguageSpeakerComponent, LanguagesUpdateEvent>((uid, comp, _) => SendLanguageStateToClient(uid, comp));

// Refresh the client's state when its mind hops to a different entity
SubscribeLocalEvent<MindContainerComponent, MindAddedMessage>((uid, _, _) => SendLanguageStateToClient(uid));
SubscribeLocalEvent<MindComponent, MindGotRemovedEvent>((_, _, args) =>
{
if (args.Mind.Comp.Session != null)
SendLanguageStateToClient(args.Mind.Comp.Session);
});

SubscribeLocalEvent<LanguageSpeakerComponent, LanguagesUpdateEvent>((uid, comp, _) => SendLanguageStateToClient(uid, comp));
SubscribeNetworkEvent<RequestLanguagesMessage>((_, session) => SendLanguageStateToClient(session.SenderSession));
}


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
Expand All @@ -50,10 +61,18 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo
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 langs = GetLanguages(uid, component);
var message = new LanguagesUpdatedMessage(langs.CurrentLanguage, langs.SpokenLanguages, langs.UnderstoodLanguages);
var isUniversal = HasComp<UniversalLanguageSpeakerComponent>(uid);
if (!isUniversal)
Resolve(uid, ref component, logMissing: false);

// I really don't want to call 3 getter methods here, so we'll just have this slightly hardcoded solution
var message = isUniversal || component == null
? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype])
: new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages);

RaiseNetworkEvent(message, session);
}
}
Loading
Loading