Files
wwdpublic/Content.Server/Radio/EntitySystems/RadioSystem.cs
FoxxoTrystan 849b33047c Languages Markers (#510)
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Require #459 

Add 3 optional settings for LanguagePrototypes to play with richtext
tags to they could be reconized as Makings

color - Set a specefic color to the text.
fontId - Set a font to the text by using the Id.
fontSize - Set the size of the text

All 3 are optional if not set message will be handeled like normal.

This should be mostly used to know what language your currently speaking
and assist with markings.
Take note those changes happent only in the TextBox chat, bubblechat is
left unchanged.

---

# TODO

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

- [x] Add Markings
- [x] Add Fonts

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

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


![image](https://github.com/Simple-Station/Einstein-Engines/assets/45297731/10c3956b-c964-41af-ba0e-37ad1be8119e)

![image](https://github.com/Simple-Station/Einstein-Engines/assets/45297731/4377bdd8-a52e-4b62-bd70-fa9ba36c8d8b)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑 FoxxoTrystan
- add: Languages are now marked in the chat!

---------

Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Signed-off-by: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com>
Co-authored-by: fox <daytimer253@gmail.com>
Co-authored-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
2024-07-09 20:01:38 +01:00

206 lines
8.9 KiB
C#

using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Server.Language;
using Content.Server.Power.Components;
using Content.Server.Radio.Components;
using Content.Server.Speech;
using Content.Server.VoiceMask;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Language;
using Content.Shared.Radio;
using Content.Shared.Radio.Components;
using Content.Shared.Speech;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
namespace Content.Server.Radio.EntitySystems;
/// <summary>
/// This system handles intrinsic radios and the general process of converting radio messages into chat messages.
/// </summary>
public sealed class RadioSystem : EntitySystem
{
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly LanguageSystem _language = default!;
// set used to prevent radio feedback loops.
private readonly HashSet<string> _messages = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntitySpokeEvent>(OnIntrinsicSpeak);
}
private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntitySpokeEvent args)
{
if (args.Channel != null && component.Channels.Contains(args.Channel.ID))
{
SendRadioMessage(uid, args.Message, args.Channel, uid, args.Language);
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args)
{
if (TryComp(uid, out ActorComponent? actor))
{
// Einstein-Engines - languages mechanic
var listener = component.Owner;
var msg = args.OriginalChatMsg;
if (listener != null && !_language.CanUnderstand(listener, args.Language.ID))
msg = args.LanguageObfuscatedChatMsg;
_netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.Channel);
}
}
/// <summary>
/// Send radio message to all active radio listeners
/// </summary>
public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true)
{
SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language);
}
/// <summary>
/// Send radio message to all active radio listeners
/// </summary>
/// <param name="messageSource">Entity that spoke the message</param>
/// <param name="radioSource">Entity that picked up the message and will send it, e.g. headset</param>
public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true)
{
if (language == null)
language = _language.GetLanguage(messageSource);
// TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this.
if (!_messages.Add(message))
return;
var name = TryComp(messageSource, out VoiceMaskComponent? mask) && mask.Enabled
? mask.VoiceName
: MetaData(messageSource).EntityName;
// Delta-V: Support syrinx voice mask on radio.
if (TryComp(messageSource, out SyrinxVoiceMaskComponent? syrinx) && syrinx.Enabled)
name = syrinx.VoiceName;
name = FormattedMessage.EscapeText(name);
// most radios are relayed to chat, so lets parse the chat message beforehand
SpeechVerbPrototype speech;
if (mask != null
&& mask.Enabled
&& mask.SpeechVerb != null
&& _prototype.TryIndex<SpeechVerbPrototype>(mask.SpeechVerb, out var proto))
{
speech = proto;
}
else
speech = _chat.GetSpeechVerb(messageSource, message);
var content = escapeMarkup
? FormattedMessage.EscapeText(message)
: message;
var wrappedMessage = WrapRadioMessage(messageSource, channel, name, content, language);
var msg = new ChatMessage(ChatChannel.Radio, content, wrappedMessage, NetEntity.Invalid, null);
// ... you guess it
var obfuscated = _language.ObfuscateSpeech(content, language);
var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated, language);
var notUdsMsg = new ChatMessage(ChatChannel.Radio, obfuscated, obfuscatedWrapped, NetEntity.Invalid, null);
var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language);
var sendAttemptEv = new RadioSendAttemptEvent(channel, radioSource);
RaiseLocalEvent(ref sendAttemptEv);
RaiseLocalEvent(radioSource, ref sendAttemptEv);
var canSend = !sendAttemptEv.Cancelled;
var sourceMapId = Transform(radioSource).MapID;
var hasActiveServer = HasActiveServer(sourceMapId, channel.ID);
var hasMicro = HasComp<RadioMicrophoneComponent>(radioSource);
var speakerQuery = GetEntityQuery<RadioSpeakerComponent>();
var radioQuery = EntityQueryEnumerator<ActiveRadioComponent, TransformComponent>();
while (canSend && radioQuery.MoveNext(out var receiver, out var radio, out var transform))
{
if (!radio.ReceiveAllChannels)
{
if (!radio.Channels.Contains(channel.ID) || (TryComp<IntercomComponent>(receiver, out var intercom) &&
!intercom.SupportedChannels.Contains(channel.ID)))
continue;
}
if (!channel.LongRange && transform.MapID != sourceMapId && !radio.GlobalReceive)
continue;
// don't need telecom server for long range channels or handheld radios and intercoms
var needServer = !channel.LongRange && (!hasMicro || !speakerQuery.HasComponent(receiver));
if (needServer && !hasActiveServer)
continue;
// check if message can be sent to specific receiver
var attemptEv = new RadioReceiveAttemptEvent(channel, radioSource, receiver);
RaiseLocalEvent(ref attemptEv);
RaiseLocalEvent(receiver, ref attemptEv);
if (attemptEv.Cancelled)
continue;
// send the message
RaiseLocalEvent(receiver, ref ev);
}
if (name != Name(messageSource))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} as {name} on {channel.LocalizedName}: {message}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {message}");
_replay.RecordServerMessage(msg);
_messages.Remove(message);
}
private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, string name, string message, LanguagePrototype language)
{
var speech = _chat.GetSpeechVerb(source, message);
return Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap",
("color", channel.Color),
("languageColor", language.Color ?? channel.Color),
("fontType", language.FontId ?? speech.FontId),
("fontSize", language.FontSize ?? speech.FontSize),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", message));
}
/// <inheritdoc cref="TelecomServerComponent"/>
private bool HasActiveServer(MapId mapId, string channelId)
{
var servers = EntityQuery<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent, TransformComponent>();
foreach (var (_, keys, power, transform) in servers)
{
if (transform.MapID == mapId &&
power.Powered &&
keys.Channels.Contains(channelId))
{
return true;
}
}
return false;
}
}