Files
wwdpublic/Content.Server/Language/LanguageSystem.cs
Mnemotechnican 9d5f3b65df Refactor the Language System (#459)
# Description
The language PR was merged early and OH GOD I ALREADY REGRET IT

This PR is intended to provide the missing refactors and address the
issues that were missed due to the early merge.

---

# TODO
- [X] Introduced a polymorphic obfuscation property to the
LanguagePrototype - now it supports more than just 2 hardcoded methods,
and each method can be configured per-language. Currently there are 3
obfuscation methods: replacement (same as replacement accent),
obuscation by syllables and obfuscation by phrases.
- [X] Refactored the existing obfuscation methods to not be a big
hardcoded mess.
- [X] Updated the existing languages accordingly: animalistic languages
are now less of an unreadable mess and include less syllables. Certain
languages like binary and snake seriously benefit from that.
- [X] Refactored the existing commands in response to the
never-addressed review (it got lost among hundreds of others)
- [X] Refactored the commands to be more user-friendly (you can now use
the number of the language in saylang and languageselect which can allow
using keybinds to switch between languages)
- [X] Moved a lot of obfuscation-related stuff from server to shared.
The actual obfuscation process, however, is still done on the server.
That may or may not be subject to change, too.
- [X] Refactored the entire process of resolution of entities'
languages. Instead of raising an event every time it's required to learn
what languages an entity knows, the lists of ALL languages available to
the entity (including via translators) is stored in
LanguageSpeakerComponent and only updated when necessary (e.g. when a
translator gets toggled). The list of languages the entity knows on its
own is now stored in LanguageKnowledgeComponent.
- [X] Made handheld translators automatically change your current
language when activated.
- [X] Rewrote the translator implanter system, now using the real
implants and implanters
- [ ] Rebalance science stuff (translators are incredibly expensive for
what they're worth)
- [ ] Uhhh stuff

---

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

N/A for now

</p>
</details>

---

# Changelog

🆑
- tweak: Translator implants are now proper implants that can be
removed.
- tweak: Animalistic languages should now look less messy.
- fix: Hopefully fixed language menu desync and other issues.

---------

Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
2024-07-05 19:49:47 -04:00

213 lines
7.5 KiB
C#

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;
namespace Content.Server.Language;
public sealed partial class LanguageSystem : SharedLanguageSystem
{
public override void Initialize()
{
base.Initialize();
InitializeNet();
SubscribeLocalEvent<LanguageSpeakerComponent, ComponentInit>(OnInitLanguageSpeaker);
}
#region public api
public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerComponent? component = null)
{
if (language == UniversalPrototype || HasComp<UniversalLanguageSpeakerComponent>(listener))
return true;
if (!Resolve(listener, ref component, logMissing: false))
return false;
return component.UnderstoodLanguages.Contains(language);
}
public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? component = null)
{
if (HasComp<UniversalLanguageSpeakerComponent>(speaker))
return true;
if (!Resolve(speaker, ref component, logMissing: false))
return false;
return component.SpokenLanguages.Contains(language);
}
/// <summary>
/// Returns the current language of the given entity, assumes Universal if it's not a language speaker.
/// </summary>
public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null)
{
if (HasComp<UniversalLanguageSpeakerComponent>(speaker) || !Resolve(speaker, ref component, logMissing: false))
return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment)
if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex<LanguagePrototype>(component.CurrentLanguage, out var proto))
return Universal;
return proto;
}
/// <summary>
/// Returns the list of languages this entity can speak.
/// </summary>
/// <remarks>Typically, checking <see cref="LanguageSpeakerComponent.SpokenLanguages"/> is sufficient.</remarks>
public List<string> GetSpokenLanguages(EntityUid uid)
{
if (HasComp<UniversalLanguageSpeakerComponent>(uid))
return [UniversalPrototype];
if (TryComp<LanguageSpeakerComponent>(uid, out var component))
return component.SpokenLanguages;
return [];
}
/// <summary>
/// Returns the list of languages this entity can understand.
/// </summary>
/// <remarks>Typically, checking <see cref="LanguageSpeakerComponent.UnderstoodLanguages"/> is sufficient.</remarks>
public List<string> GetUnderstoodLanguages(EntityUid uid)
{
if (HasComp<UniversalLanguageSpeakerComponent>(uid))
return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one.
if (TryComp<LanguageSpeakerComponent>(uid, out var component))
return component.UnderstoodLanguages;
return [];
}
public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null)
{
if (!CanSpeak(speaker, language) || (HasComp<UniversalLanguageSpeakerComponent>(speaker) && language != UniversalPrototype))
return;
if (!Resolve(speaker, ref component) || component.CurrentLanguage == language)
return;
component.CurrentLanguage = language;
RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true);
}
/// <summary>
/// Adds a new language to the respective lists of intrinsically known languages of the given entity.
/// </summary>
public void AddLanguage(
EntityUid uid,
string language,
bool addSpoken = true,
bool addUnderstood = true,
LanguageKnowledgeComponent? knowledge = null,
LanguageSpeakerComponent? speaker = null)
{
if (knowledge == null)
knowledge = EnsureComp<LanguageKnowledgeComponent>(uid);
if (addSpoken && !knowledge.SpokenLanguages.Contains(language))
knowledge.SpokenLanguages.Add(language);
if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language))
knowledge.UnderstoodLanguages.Add(language);
UpdateEntityLanguages(uid, speaker);
}
/// <summary>
/// Removes a language from the respective lists of intrinsically known languages of the given entity.
/// </summary>
public void RemoveLanguage(
EntityUid uid,
string language,
bool removeSpoken = true,
bool removeUnderstood = true,
LanguageKnowledgeComponent? knowledge = null,
LanguageSpeakerComponent? speaker = null)
{
if (knowledge == null)
knowledge = EnsureComp<LanguageKnowledgeComponent>(uid);
if (removeSpoken)
knowledge.SpokenLanguages.Remove(language);
if (removeUnderstood)
knowledge.UnderstoodLanguages.Remove(language);
UpdateEntityLanguages(uid, speaker);
}
/// <summary>
/// Ensures the given entity has a valid language as its current language.
/// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty.
/// </summary>
/// <returns>True if the current language was modified, false otherwise.</returns>
public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null)
{
if (!Resolve(entity, ref comp))
return false;
if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage))
{
comp.CurrentLanguage = comp.SpokenLanguages.FirstOrDefault(UniversalPrototype);
RaiseLocalEvent(entity, new LanguagesUpdateEvent());
return true;
}
return false;
}
/// <summary>
/// Immediately refreshes the cached lists of spoken and understood languages for the given entity.
/// </summary>
public void UpdateEntityLanguages(EntityUid entity, LanguageSpeakerComponent? languages = null)
{
if (!Resolve(entity, ref languages))
return;
var ev = new DetermineEntityLanguagesEvent();
// We add the intrinsically known languages first so other systems can manipulate them easily
if (TryComp<LanguageKnowledgeComponent>(entity, out var knowledge))
{
foreach (var spoken in knowledge.SpokenLanguages)
ev.SpokenLanguages.Add(spoken);
foreach (var understood in knowledge.UnderstoodLanguages)
ev.UnderstoodLanguages.Add(understood);
}
RaiseLocalEvent(entity, ref ev);
languages.SpokenLanguages.Clear();
languages.UnderstoodLanguages.Clear();
languages.SpokenLanguages.AddRange(ev.SpokenLanguages);
languages.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);
UpdateEntityLanguages(uid, component);
}
#endregion
}