From f5e79010acb6765c4940c5fb7d14933c853255f3 Mon Sep 17 00:00:00 2001 From: fox Date: Sun, 7 Jul 2024 19:09:54 +0300 Subject: [PATCH 1/6] Make translators equippable in the neck slot --- .../Prototypes/Entities/Objects/Devices/translators.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index 664626ea4b4..331a58605df 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -23,6 +23,11 @@ False: { visible: false } - type: HandheldTranslator enabled: false + - type: Clothing # To allow equipping translators on the neck slot + slots: [neck, pocket] + equipDelay: 0.3 + unequipDelay: 0.3 + quickEquip: false # Would conflict - type: entity abstract: true From 3857823317002c04cc45e97d54ac4be4d02b011a Mon Sep 17 00:00:00 2001 From: fox Date: Sun, 7 Jul 2024 22:22:14 +0300 Subject: [PATCH 2/6] Improve translator examination text --- .../Language/Systems/SharedTranslatorSystem.cs | 18 ++++++++++++++---- Resources/Locale/en-US/language/translator.ftl | 9 +++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Content.Shared/Language/Systems/SharedTranslatorSystem.cs b/Content.Shared/Language/Systems/SharedTranslatorSystem.cs index 08a016efa9c..4a72de791f0 100644 --- a/Content.Shared/Language/Systems/SharedTranslatorSystem.cs +++ b/Content.Shared/Language/Systems/SharedTranslatorSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Examine; using Content.Shared.Toggleable; using Content.Shared.Language.Components.Translators; @@ -17,11 +18,20 @@ public override void Initialize() private void OnExamined(EntityUid uid, HandheldTranslatorComponent component, ExaminedEvent args) { - var state = Loc.GetString(component.Enabled - ? "translator-enabled" - : "translator-disabled"); + var understoodLanguageNames = component.UnderstoodLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + var spokenLanguageNames = component.SpokenLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + var requiredLanguageNames = component.RequiredLanguages + .Select(it => Loc.GetString($"language-{it}-name")); - args.PushMarkup(state); + args.PushMarkup(Loc.GetString("translator-examined-langs-understood", ("languages", string.Join(", ", understoodLanguageNames)))); + args.PushMarkup(Loc.GetString("translator-examined-langs-spoken", ("languages", string.Join(", ", spokenLanguageNames)))); + + args.PushMarkup(Loc.GetString(component.RequiresAllLanguages ? "translator-examined-requires-all" : "translator-examined-requires-any", + ("languages", string.Join(", ", requiredLanguageNames)))); + + args.PushMarkup(Loc.GetString(component.Enabled ? "translator-examined-enabled" : "translator-examined-disabled")); } protected void OnAppearanceChange(EntityUid translator, HandheldTranslatorComponent? comp = null) diff --git a/Resources/Locale/en-US/language/translator.ftl b/Resources/Locale/en-US/language/translator.ftl index b2a1e9b2b8c..8070d03be29 100644 --- a/Resources/Locale/en-US/language/translator.ftl +++ b/Resources/Locale/en-US/language/translator.ftl @@ -1,8 +1,13 @@ translator-component-shutoff = The {$translator} shuts off. translator-component-turnon = The {$translator} turns on. -translator-enabled = It appears to be active. -translator-disabled = It appears to be disabled. translator-implanter-refuse = The {$implanter} has no effect on {$target}. translator-implanter-success = The {$implanter} successfully injected {$target}. translator-implanter-ready = This implanter appears to be ready to use. translator-implanter-used = This implanter seems empty. + +translator-examined-langs-understood = It can translate from: [color=green]{$languages}[/color]. +translator-examined-langs-spoken = It can translate to: [color=green]{$languages}[/color]. +translator-examined-requires-any = It requires you to know at least one of these languages: [color=yellow]{$languages}[/color]. +translator-examined-requires-all = It requires you to know all of these languages: [color=yellow]{$languages}[/color]. +translator-examined-enabled = It appears to be [color=green]active[/color]. +translator-examined-disabled = It appears to be [color=red]turned off[/color]. From 0eedda572d40f73edca2377cffb5756951f3aa07 Mon Sep 17 00:00:00 2001 From: fox Date: Sun, 7 Jul 2024 22:26:31 +0300 Subject: [PATCH 3/6] implement foreigner traits --- Content.Server/Language/TranslatorSystem.cs | 2 + .../Assorted/ForeignerTraitComponent.cs | 36 ++++++ .../Traits/Assorted/ForeignerTraitSystem.cs | 107 ++++++++++++++++++ .../Components/LanguageKnowledgeComponent.cs | 2 + Resources/Locale/en-US/traits/traits.ftl | 10 ++ .../Entities/Objects/Devices/translators.yml | 13 ++- .../Prototypes/Traits/inconveniences.yml | 23 ++++ 7 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 Content.Server/Traits/Assorted/ForeignerTraitComponent.cs create mode 100644 Content.Server/Traits/Assorted/ForeignerTraitSystem.cs diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 5022e540960..9d7b7a7fc8e 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Language.Events; using Content.Server.Popups; using Content.Server.PowerCell; +using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Language; @@ -25,6 +26,7 @@ public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); diff --git a/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs new file mode 100644 index 00000000000..e2d74ba5d9b --- /dev/null +++ b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs @@ -0,0 +1,36 @@ +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server.Traits.Assorted; + +/// +/// When applied to a not-yet-spawned player entity, removes from the lists of their languages +/// and gives them a translator instead. +/// +[RegisterComponent] +public sealed partial class ForeignerTraitComponent : Component +{ + /// + /// The "base" language that is to be removed and substituted with a translator. + /// By default, equals to the fallback language, which is GalacticCommon. + /// + [DataField] + public ProtoId BaseLanguage = SharedLanguageSystem.FallbackLanguagePrototype; + + /// + /// Whether this trait prevents the entity from understanding the base language. + /// + public bool CantUnderstand = true; + + /// + /// Whether this trait prevents the entity from speaking the base language. + /// + public bool CantSpeak = true; + + /// + /// The base translator prototype to use when creating a translator for the entity. + /// + [DataField(required: true)] + public ProtoId BaseTranslator = default!; +} diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs new file mode 100644 index 00000000000..292bf6dd9d2 --- /dev/null +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -0,0 +1,107 @@ +using System.Linq; +using Content.Server.GameTicking; +using Content.Server.Hands.Systems; +using Content.Server.Language; +using Content.Server.Storage.EntitySystems; +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Content.Shared.Language.Components.Translators; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; + +namespace Content.Server.Traits.Assorted; + + +public sealed partial class ForeignerTraitSystem : EntitySystem +{ + [Dependency] private readonly EntityManager _entMan = default!; + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly LanguageSystem _languages = default!; + [Dependency] private readonly MetaDataSystem _meta = default!; + [Dependency] private readonly StorageSystem _storage = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnSpawn); // TraitSystem adds it after PlayerSpawnCompleteEvent so it's fine + } + + private void OnSpawn(Entity entity, ref ComponentInit args) + { + if (entity.Comp.CantUnderstand && !entity.Comp.CantSpeak) + Log.Warning($"Allowing entity {entity.Owner} to speak a language but not understand it leads to undefined behavior."); + + if (!TryComp(entity, out var knowledge)) + { + Log.Error($"Entity {entity.Owner} does not have a LanguageKnowledge but has a ForeignerTrait!"); + return; + } + + var alternateLanguage = knowledge.SpokenLanguages.Find(it => it != entity.Comp.BaseLanguage); + if (alternateLanguage == null) + { + Log.Error($"Entity {entity.Owner} does not have an alternative language to choose from (must have at least one non-GC for ForeignerTrait)!"); + return; + } + + 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, knowledge); + } + } + + /// + /// Tries to create and give the entity a translator to translator that translates speech between the two specified languages. + /// + public bool TryGiveTranslator( + EntityUid uid, + string baseTranslatorPrototype, + ProtoId translatorLanguage, + ProtoId entityLanguage, + out EntityUid result) + { + result = EntityUid.Invalid; + if (translatorLanguage == entityLanguage) + return false; + + var translator = _entMan.SpawnNextToOrDrop(baseTranslatorPrototype, uid); + result = translator; + + if (!TryComp(translator, out var handheld)) + { + handheld = AddComp(translator); + handheld.ToggleOnInteract = true; + handheld.SetLanguageOnInteract = true; + } + + // Allows to speak the specified language and requires entities language. + handheld.SpokenLanguages = [translatorLanguage]; + handheld.UnderstoodLanguages = [translatorLanguage]; + handheld.RequiredLanguages = [entityLanguage]; + + // Try to put it in entities hand + if (_hands.TryPickupAnyHand(uid, translator, false, false, false)) + return true; + + // Try to find a valid clothing slot on the entity and equip the translator there + if (TryComp(translator, out var clothing) + && clothing.Slots != SlotFlags.NONE + && _inventory.TryGetSlots(uid, out var slots) + && slots.Any(it => _inventory.TryEquip(uid, translator, it.Name, true, false))) + return true; + + // Try to put the translator into entities bag, if it has one + if (_inventory.TryGetSlotEntity(uid, "back", out var bag) + && TryComp(bag, out var storage) + && _storage.Insert(bag.Value, translator, out _, null, storage, false, false)) + return true; + + // If all of the above has failed, just drop it at the same location as the entity + // This should ideally never happen, but who knows. + Transform(translator).Coordinates = Transform(uid).Coordinates; + + return true; + } +} diff --git a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs index 0632f5d9cb2..ddbdc742be4 100644 --- a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs +++ b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs @@ -2,6 +2,8 @@ namespace Content.Shared.Language.Components; +// TODO: move to server side, it's never synchronized! + /// /// Stores data about entities' intrinsic language knowledge. /// diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 2f59d322282..f6e3e0b1fc1 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -42,3 +42,13 @@ trait-name-Thieving = Thieving trait-description-Thieving = You are deft with your hands, and talented at convincing people of their belongings. You can identify pocketed items, steal them quieter, and steal ~33% faster. + +trait-name-ForeignerLight = Foreigner (light) +trait-description-ForeignerLight = + You struggle to learn this station's primary language, and as such, cannot speak it. You can, however, comprehend what others say in that language. + To help you overcome this obstacle, you are equipped with a translator that helps you speak in this station's primary language. + +trait-name-Foreigner = Foreigner +trait-description-Foreigner = + For one reason or another you do not speak this station's primary language. + Instead, you have a translator issued to you that only you can use. diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index 331a58605df..b28541253d4 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -1,5 +1,5 @@ - type: entity - abstract: true + noSpawn: true id: TranslatorUnpowered parent: BaseItem name: translator @@ -30,7 +30,7 @@ quickEquip: false # Would conflict - type: entity - abstract: true + noSpawn: true id: Translator parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ] suffix: Powered @@ -39,7 +39,7 @@ drawRate: 1 - type: entity - abstract: true + noSpawn: true id: TranslatorEmpty parent: Translator suffix: Empty @@ -49,6 +49,13 @@ cell_slot: name: power-cell-slot-component-slot-name-default +- type: entity + noSpawn: true + id: TranslatorForeigner + parent: [ Translator, PowerCellSlotHighItem ] + name: foreigner's translator + description: A special-issue translator that helps foreigner's speak and understand this station's primary language. + - type: entity id: CanilunztTranslator diff --git a/Resources/Prototypes/Traits/inconveniences.yml b/Resources/Prototypes/Traits/inconveniences.yml index dcf53d9ab7f..8dc0264ffea 100644 --- a/Resources/Prototypes/Traits/inconveniences.yml +++ b/Resources/Prototypes/Traits/inconveniences.yml @@ -26,3 +26,26 @@ fourRandomProb: 0 threeRandomProb: 0 cutRandomProb: 0 + +- type: trait + id: ForeignerLight + category: Mental + points: 1 + requirements: + - !type:TraitGroupExclusionRequirement + prototypes: [ Foreigner ] + components: + - type: ForeignerTrait + cantUnderstand: false # Allows to understand + baseTranslator: TranslatorForeigner + +- type: trait + id: Foreigner + category: Mental + points: 2 + requirements: # TODO: Add a requirement to know at least 1 non-gc language + - !type:TraitGroupExclusionRequirement + prototypes: [ ForeignerLight ] + components: + - type: ForeignerTrait + baseTranslator: TranslatorForeigner From 0c82df81a6167fefec7657d5c9f41cc90928b5f6 Mon Sep 17 00:00:00 2001 From: fox Date: Sun, 7 Jul 2024 22:44:01 +0300 Subject: [PATCH 4/6] Cleanup --- Content.Server/Language/TranslatorSystem.cs | 5 ----- Content.Server/Traits/Assorted/ForeignerTraitSystem.cs | 1 - 2 files changed, 6 deletions(-) diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 9d7b7a7fc8e..adbfe2d681f 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -1,16 +1,12 @@ using System.Linq; -using Content.Server.Language.Events; using Content.Server.Popups; using Content.Server.PowerCell; -using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Language; -using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; -using Robust.Shared.Utility; namespace Content.Server.Language; @@ -26,7 +22,6 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs index 292bf6dd9d2..7e2f16c7105 100644 --- a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Server.GameTicking; using Content.Server.Hands.Systems; using Content.Server.Language; using Content.Server.Storage.EntitySystems; From 9852374228ff84fbfa97b6389b6f9a66be347fc8 Mon Sep 17 00:00:00 2001 From: fox Date: Sun, 7 Jul 2024 22:44:50 +0300 Subject: [PATCH 5/6] Unused dependency --- Content.Server/Traits/Assorted/ForeignerTraitSystem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs index 7e2f16c7105..08be9a9aed2 100644 --- a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -19,7 +19,6 @@ public sealed partial class ForeignerTraitSystem : EntitySystem [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly LanguageSystem _languages = default!; - [Dependency] private readonly MetaDataSystem _meta = default!; [Dependency] private readonly StorageSystem _storage = default!; public override void Initialize() From 8dc91c3eeba9c1f8f6741a26d2cfacde02604b11 Mon Sep 17 00:00:00 2001 From: fox Date: Mon, 8 Jul 2024 00:41:09 +0300 Subject: [PATCH 6/6] Warn instead of error --- Content.Server/Traits/Assorted/ForeignerTraitSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs index 08be9a9aed2..58e974227ce 100644 --- a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -33,14 +33,14 @@ private void OnSpawn(Entity entity, ref ComponentInit a if (!TryComp(entity, out var knowledge)) { - Log.Error($"Entity {entity.Owner} does not have a LanguageKnowledge but has a ForeignerTrait!"); + Log.Warning($"Entity {entity.Owner} does not have a LanguageKnowledge but has a ForeignerTrait!"); return; } var alternateLanguage = knowledge.SpokenLanguages.Find(it => it != entity.Comp.BaseLanguage); if (alternateLanguage == null) { - Log.Error($"Entity {entity.Owner} does not have an alternative language to choose from (must have at least one non-GC for ForeignerTrait)!"); + Log.Warning($"Entity {entity.Owner} does not have an alternative language to choose from (must have at least one non-GC for ForeignerTrait)!"); return; }