Skip to content
This repository has been archived by the owner on Nov 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #123 from FireNameFN/VulpLanguage
Browse files Browse the repository at this point in the history
Added vulp language system
  • Loading branch information
Vonsant authored May 5, 2024
2 parents f2f1a11 + 97dbb43 commit 6463712
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 19 deletions.
108 changes: 96 additions & 12 deletions Content.Server/Chat/Systems/ChatSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
using Content.Shared.Radio;
Expand Down Expand Up @@ -167,7 +168,9 @@ public void TrySendInGameICMessage(
ICommonSession? player = null,
string? nameOverride = null,
bool checkRadioPrefix = true,
bool ignoreActionBlocker = false
bool ignoreActionBlocker = false,
EntityUid? languageSource = null,
string? languageMessage = null
)
{
if (HasComp<GhostComponent>(source))
Expand Down Expand Up @@ -234,7 +237,7 @@ public void TrySendInGameICMessage(
{
if (TryProccessRadioMessage(source, message, out var modMessage, out var channel))
{
SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker);
SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker, languageSource: languageSource, languageMessage: languageMessage);
return;
}
}
Expand All @@ -243,10 +246,10 @@ public void TrySendInGameICMessage(
switch (desiredType)
{
case InGameICChatType.Speak:
SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker);
SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker, languageSource: languageSource, languageMessage: languageMessage);
break;
case InGameICChatType.Whisper:
SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker);
SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker, languageSource: languageSource, languageMessage: languageMessage);
break;
case InGameICChatType.Emote:
SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
Expand Down Expand Up @@ -372,13 +375,19 @@ private void SendEntitySpeak(
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool ignoreActionBlocker = false
bool ignoreActionBlocker = false,
EntityUid? languageSource = null,
string? languageMessage = null
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;

var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage));
var message = FormattedMessage.RemoveMarkup(originalMessage);

languageMessage ??= LanguageTransformSpeech(languageSource ?? source, message);

message = TransformSpeech(source, message);

if (message.Length == 0)
return;
Expand Down Expand Up @@ -410,7 +419,24 @@ private void SendEntitySpeak(
("fontSize", speech.FontSize),
("message", FormattedMessage.EscapeText(message)));

SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
var wrappedLanguageMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message",
("entityName", name),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("message", FormattedMessage.EscapeText(languageMessage)));

foreach (var (session, data) in GetRecipients(source, VoiceRange))
{
var entRange = MessageRangeCheck(session, data, range);

if (entRange == MessageRangeCheckResult.Disallowed)
continue;

_chatManager.ChatMessageToOne(ChatChannel.Local, message, CheckLanguageUnderstand(languageSource ?? source, session) ? wrappedMessage : wrappedLanguageMessage, source, entRange == MessageRangeCheckResult.HideChat, session.Channel);
}

_replay.RecordServerMessage(new ChatMessage(ChatChannel.Local, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));

var ev = new EntitySpokeEvent(source, message, null, null);
RaiseLocalEvent(source, ev, true);
Expand Down Expand Up @@ -445,18 +471,28 @@ private void SendEntityWhisper(
RadioChannelPrototype? channel,
string? nameOverride,
bool hideLog = false,
bool ignoreActionBlocker = false
bool ignoreActionBlocker = false,
EntityUid? languageSource = null,
string? languageMessage = null
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;

var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage));

var message = FormattedMessage.RemoveMarkup(originalMessage);

languageMessage ??= LanguageTransformSpeech(languageSource ?? source, message);

message = TransformSpeech(source, message);

if (message.Length == 0)
return;

var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);

var obfuscatedLanguageMessage = ObfuscateMessageReadability(languageMessage, 0.2f);

// get the entity's name by visual identity (if no override provided).
string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
// get the entity's name by voice (if no override provided).
Expand All @@ -482,6 +518,15 @@ private void SendEntityWhisper(
var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message",
("message", FormattedMessage.EscapeText(obfuscatedMessage)));

var wrappedLanguageMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(languageMessage)));

var wrappedLanguageobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedLanguageMessage)));

var wrappedLanguageUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message",
("message", FormattedMessage.EscapeText(obfuscatedLanguageMessage)));


foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange))
{
Expand All @@ -494,15 +539,17 @@ 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 understand = CheckLanguageUnderstand(languageSource ?? source, session);

if (data.Range <= WhisperClearRange)
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, understand ? wrappedMessage : wrappedLanguageMessage, source, false, session.Channel);
//If listener is too far, they only hear fragments of the message
//Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind
else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) //Shared.Physics.CollisionGroup.Opaque
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel);
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, understand ? wrappedobfuscatedMessage : wrappedLanguageobfuscatedMessage, source, false, session.Channel);
//If listener is too far and has no line of sight, they can't identify the whisperer's identity
else
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel);
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, understand ? wrappedUnknownMessage : wrappedLanguageUnknownMessage, source, false, session.Channel);
}

_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
Expand Down Expand Up @@ -736,6 +783,30 @@ public string TransformSpeech(EntityUid sender, string message)
return ev.Message;
}

public string LanguageTransformSpeech(EntityUid sender, string message)
{
var e = new LanguageTransformEvent(sender, message);
RaiseLocalEvent(e);

return e.Message;
}

public bool CheckLanguageUnderstand(EntityUid sender, ICommonSession listener)
{
if (!EntityManager.TryGetComponent<MindComponent>(listener.GetMind(), out var mind) || mind.CurrentEntity is null)
return false;

return CheckLanguageUnderstand(sender, mind.CurrentEntity.Value);
}

public bool CheckLanguageUnderstand(EntityUid sender, EntityUid listener)
{
var e = new CheckLanguageUnderstandEvent(sender, listener, false);
RaiseLocalEvent(e);

return e.Understand;
}

public bool CheckIgnoreSpeechBlocker(EntityUid sender, bool ignoreBlocker)
{
if (ignoreBlocker)
Expand Down Expand Up @@ -896,6 +967,19 @@ public TransformSpeechEvent(EntityUid sender, string message)
}
}

public sealed class LanguageTransformEvent(EntityUid sender, string message) : EntityEventArgs
{
public EntityUid Sender = sender;
public string Message = message;
}

public sealed class CheckLanguageUnderstandEvent(EntityUid sender, EntityUid listener, bool understand) : EntityEventArgs
{
public EntityUid Sender = sender;
public EntityUid Listener = listener;
public bool Understand = understand;
}

public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs
{
public EntityUid Sender;
Expand Down
140 changes: 140 additions & 0 deletions Content.Server/Corvax/VulpLanguage/VulpLanguageSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Content.Server.Chat.Systems;
using Content.Server.PowerCell;
using Content.Server.VulpLangauge;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Storage;

namespace Content.Server.Corvax.VulpLanguage;

public sealed partial class VulpLanguageSystem : EntitySystem
{
[GeneratedRegex(@"\b\w+\b|\W+")]
private static partial Regex GetWordRegex();

[GeneratedRegex(@"\w")]
private static partial Regex GetLetterRegex();

[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PowerCellSystem _power = default!;

private readonly Dictionary<string, string> _words = [];

private readonly string[] _syllables = ["рур", "йа", "цен", "раур", "бар", "кук", "тек", "кват", "ук", "ву", "вух", "тах", "тч", "счз", "ауч", "ист", "айн",
"енщ", "звичз", "тут", "мир", "во", "бис", "ес", "вор", "ник", "гро", "ллл", "енем", "зандт", "тзч", "ноч", "хел", "ишт", "фар", "ва", "барам", "ийренг",
"теч", "лач", "сам", "мак", "лич", "ген", "ор", "аг", "ецк", "гец", "стаг", "онн", "бин", "кет", "жарл", "вулф", "ейнеч", "црестхц", "азунайн", "гхзтх"];

private readonly SearchValues<char> _vowels = SearchValues.Create("ауоиэыяюеё");

private readonly Random _random = new();

public override void Initialize()
{
SubscribeLocalEvent<LanguageTransformEvent>(OnLanguageTransform);
SubscribeLocalEvent<CheckLanguageUnderstandEvent>(OnCheckLanguageUnderstand);
}

private void OnLanguageTransform(LanguageTransformEvent e)
{
if (!EntityManager.HasComponent<VulpLanguageSpeakerComponent>(e.Sender))
return;

if (TryGetTranslator(e.Sender, out var translator) && _power.TryUseCharge(translator.Value, 0.2f * e.Message.Length))
return;

var words = GetWordRegex().Split(e.Message);

StringBuilder messageBuilder = new();

StringBuilder wordBuilder = new();

foreach (var match in GetWordRegex().Matches(e.Message).Cast<Match>())
{
var word = match.Value;

if (!GetLetterRegex().IsMatch([word[0]]))
{
messageBuilder.Append(word);
continue;
}

var lowerWord = word.ToLower();

if (!_words.TryGetValue(lowerWord, out var languageWord))
{
var syllablesCount = word.Count(letter => _vowels.Contains(char.ToLower(letter)));

syllablesCount = syllablesCount switch
{
1 => 2,
_ => _random.Next(3, 5)
};

for (var i = 0; i < syllablesCount; i++)
wordBuilder.Append(_syllables[_random.Next(_syllables.Length)]);

languageWord = wordBuilder.ToString();

wordBuilder.Clear();

_words.Add(lowerWord, languageWord);
}

if (IsUpper(word))
languageWord = languageWord.ToUpper();
else if (char.IsUpper(word[0]))
languageWord = char.ToUpper(languageWord[0]) + languageWord[1..];

messageBuilder.Append(languageWord);
}

e.Message = messageBuilder.ToString();
}

private static bool IsUpper(string str)
{
foreach (var c in str)
if (char.IsLower(c))
return false;

return true;
}

private bool TryGetTranslator(EntityUid entity, [NotNullWhen(true)] out EntityUid? translator)
{
return TryGetTranslator(_inventory.GetHandOrInventoryEntities(entity), out translator);
}

private bool TryGetTranslator(IEnumerable<EntityUid> entities, [NotNullWhen(true)] out EntityUid? translator)
{
foreach (var entity in entities)
{
if (EntityManager.HasComponent<VulpTranslatorComponent>(entity))
{
translator = entity;
return true;
}

if (EntityManager.TryGetComponent<StorageComponent>(entity, out var storage) && TryGetTranslator(storage.Container.ContainedEntities, out translator))
return true;
}

translator = null;
return false;
}

private void OnCheckLanguageUnderstand(CheckLanguageUnderstandEvent e)
{
if (!EntityManager.HasComponent<VulpLanguageSpeakerComponent>(e.Sender))
return;

if (EntityManager.HasComponent<VulpLanguageSpeakerComponent>(e.Listener))
e.Understand = true;
}
}
7 changes: 5 additions & 2 deletions Content.Server/Radio/EntitySystems/HeadsetSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
{
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly ChatSystem _chat = default!;

public override void Initialize()
{
Expand Down Expand Up @@ -99,8 +100,10 @@ public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component =

private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args)
{
if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
var listener = Transform(uid).ParentUid;

if (TryComp(listener, out ActorComponent? actor))
_netMan.ServerSendMessage(_chat.CheckLanguageUnderstand(args.MessageSource, listener) ? args.ChatMsg : args.LanguageChatMsg, actor.PlayerSession.Channel);
}

private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseEvent args)
Expand Down
2 changes: 1 addition & 1 deletion Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref
("originalName", nameEv.Name));

// log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios
_chat.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false);
_chat.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false, languageSource: args.MessageSource, languageMessage: args.LanguageMessage);
}

private void OnBeforeIntercomUiOpen(EntityUid uid, IntercomComponent component, BeforeActivatableUIOpenEvent args)
Expand Down
Loading

0 comments on commit 6463712

Please sign in to comment.