Update radio prefix parsing (#13777)

This commit is contained in:
Leon Friedrich
2023-02-19 06:27:56 +13:00
committed by GitHub
parent 63a0c76ecc
commit 75a559fa55
32 changed files with 659 additions and 606 deletions

View File

@@ -1,5 +1,133 @@
using Content.Shared.Popups;
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Chat;
public abstract class SharedChatSystem : EntitySystem
{
public const char RadioCommonPrefix = ';';
public const char RadioChannelPrefix = ':';
public const char LocalPrefix = '.';
public const char ConsolePrefix = '/';
public const char DeadPrefix = '\\';
public const char LOOCPrefix = '(';
public const char OOCPrefix = '[';
public const char EmotesPrefix = '@';
public const char AdminPrefix = ']';
public const char WhisperPrefix = ',';
public const char DefaultChannelKey = 'h';
public const string CommonChannel = "Common";
public static string DefaultChannelPrefix = $"{RadioChannelPrefix}{DefaultChannelKey}";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <summary>
/// Cache of the keycodes for faster lookup.
/// </summary>
private Dictionary<char, RadioChannelPrototype> _keyCodes = new();
public override void Initialize()
{
base.Initialize();
DebugTools.Assert(_prototypeManager.HasIndex<RadioChannelPrototype>(CommonChannel));
_prototypeManager.PrototypesReloaded += OnPrototypeReload;
CacheRadios();
}
private void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{
if (obj.ByType.ContainsKey(typeof(RadioChannelPrototype)))
CacheRadios();
}
private void CacheRadios()
{
_keyCodes.Clear();
foreach (var proto in _prototypeManager.EnumeratePrototypes<RadioChannelPrototype>())
{
_keyCodes.Add(proto.KeyCode, proto);
}
}
public override void Shutdown()
{
_prototypeManager.PrototypesReloaded -= OnPrototypeReload;
}
/// <summary>
/// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested
/// channel. Returns true if a radio message was attempted, even if the channel is invalid.
/// </summary>
/// <param name="source">Source of the message</param>
/// <param name="input">The message to be modified</param>
/// <param name="output">The modified message</param>
/// <param name="channel">The channel that was requested, if any</param>
/// <param name="quiet">Whether or not to generate an informative pop-up message.</param>
/// <returns></returns>
public bool TryProccessRadioMessage(
EntityUid source,
string input,
out string output,
out RadioChannelPrototype? channel,
bool quiet = false)
{
output = input.Trim();
channel = null;
if (input.Length == 0)
return false;
if (input.StartsWith(RadioCommonPrefix))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
channel = _prototypeManager.Index<RadioChannelPrototype>(CommonChannel);
return true;
}
if (!input.StartsWith(RadioChannelPrefix))
return false;
if (input.Length < 2 || char.IsWhiteSpace(input[1]))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
if (!quiet)
_popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source);
return true;
}
var channelKey = input[1];
output = SanitizeMessageCapital(input[2..].TrimStart());
if (channelKey == DefaultChannelKey)
{
var ev = new GetDefaultRadioChannelEvent();
RaiseLocalEvent(source, ev);
if (ev.Channel != null)
_prototypeManager.TryIndex(ev.Channel, out channel);
return true;
}
if (!_keyCodes.TryGetValue(channelKey, out channel) && !quiet)
{
var msg = Loc.GetString("chat-manager-no-such-channel", ("key", channelKey));
_popup.PopupEntity(msg, source, source);
}
return true;
}
public string SanitizeMessageCapital(string message)
{
if (string.IsNullOrEmpty(message))
return message;
// Capitalize first letter
message = char.ToUpper(message[0]) + message.Remove(0, 1);
return message;
}
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.Electrocution;
using Content.Shared.Explosion;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Radio;
using Content.Shared.Slippery;
using Content.Shared.Strip.Components;
using Content.Shared.Temperature;
@@ -21,6 +22,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, BeforeStripEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SeeIdentityAttemptEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
}
protected void RelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, T args) where T : EntityEventArgs, IInventoryRelayEvent

View File

@@ -1,8 +1,9 @@
using Content.Shared.Radio;
using Content.Shared.Chat;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
namespace Content.Shared.Radio.Components;
/// <summary>
/// This component is currently used for providing access to channels for "HeadsetComponent"s.
/// It should be used for intercoms and other radios in future.
@@ -13,10 +14,8 @@ public sealed class EncryptionKeyComponent : Component
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public HashSet<string> Channels = new();
/// <summary>
/// This variable defines what channel will be used with using ":h" (department channel prefix).
/// Headset read DefaultChannel of first encryption key installed.
/// This is the channel that will be used when using the default/department prefix (<see cref="SharedChatSystem.DefaultChannelKey"/>).
/// </summary>
[DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public readonly string? DefaultChannel;

View File

@@ -0,0 +1,56 @@
using Content.Shared.Chat;
using Content.Shared.Tools;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Radio.Components;
/// <summary>
/// This component is by entities that can contain encryption keys
/// </summary>
[RegisterComponent]
public sealed class EncryptionKeyHolderComponent : Component
{
/// <summary>
/// Whether or not encryption keys can be removed from the headset.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keysUnlocked")]
public bool KeysUnlocked = true;
/// <summary>
/// The tool required to extract the encryption keys from the headset.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keysExtractionMethod", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string KeysExtractionMethod = "Screwing";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keySlots")]
public int KeySlots = 2;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keyExtractionSound")]
public SoundSpecifier KeyExtractionSound = new SoundPathSpecifier("/Audio/Items/pistol_magout.ogg");
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keyInsertionSound")]
public SoundSpecifier KeyInsertionSound = new SoundPathSpecifier("/Audio/Items/pistol_magin.ogg");
[ViewVariables]
public Container KeyContainer = default!;
public const string KeyContainerName = "key_slots";
/// <summary>
/// Combined set of radio channels provided by all contained keys.
/// </summary>
[ViewVariables]
public HashSet<string> Channels = new();
/// <summary>
/// This is the channel that will be used when using the default/department prefix (<see cref="SharedChatSystem.DefaultChannelKey"/>).
/// </summary>
[ViewVariables]
public string? DefaultChannel;
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Inventory;
namespace Content.Shared.Radio.Components;
/// <summary>
/// This component relays radio messages to the parent entity's chat when equipped.
/// </summary>
[RegisterComponent]
public sealed class HeadsetComponent : Component
{
[DataField("enabled")]
public bool Enabled = true;
public bool IsEquipped = false;
[DataField("requiredSlot")]
public SlotFlags RequiredSlot = SlotFlags.EARS;
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.Radio.Components;
namespace Content.Shared.Radio;
public sealed class EncryptionChannelsChangedEvent : EntityEventArgs
{
public readonly EncryptionKeyHolderComponent Component;
public EncryptionChannelsChangedEvent(EncryptionKeyHolderComponent component)
{
Component = component;
}
}

View File

@@ -1,55 +0,0 @@
using Content.Server.Radio.Components;
using Content.Shared.Examine;
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Server.Radio.EntitySystems;
public sealed class EncryptionKeySystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EncryptionKeyComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(EntityUid uid, EncryptionKeyComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if(component.Channels.Count > 0)
{
args.PushMarkup(Loc.GetString("examine-encryption-key-channels-prefix"));
EncryptionKeySystem.GetChannelsExamine(component.Channels, args, _protoManager, "examine-headset-channel");
if (component.DefaultChannel != null)
{
var proto = _protoManager.Index<RadioChannelPrototype>(component.DefaultChannel);
args.PushMarkup(Loc.GetString("examine-encryption-key-default-channel", ("channel", component.DefaultChannel), ("color", proto.Color)));
}
}
}
/// <summary>
/// A static method for formating list of radio channels for examine events.
/// </summary>
/// <param name="channels">HashSet of channels in headset, encryptionkey or etc.</param>
/// <param name="protoManager">IPrototypeManager for getting prototypes of channels with their variables.</param>
/// <param name="channelFTLPattern">String that provide id of pattern in .ftl files to format channel with variables of it.</param>
public static void GetChannelsExamine(HashSet<string> channels, ExaminedEvent examineEvent, IPrototypeManager protoManager, string channelFTLPattern)
{
foreach (var id in channels)
{
var proto = protoManager.Index<RadioChannelPrototype>(id);
string keyCode = "" + proto.KeyCode;
if (id != "Common")
keyCode = ":" + keyCode;
examineEvent.PushMarkup(Loc.GetString(channelFTLPattern,
("color", proto.Color),
("key", keyCode),
("id", proto.LocalizedName),
("freq", proto.Frequency)));
}
}
}

View File

@@ -0,0 +1,196 @@
using System.Linq;
using Content.Shared.Chat;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Radio.Components;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Radio.EntitySystems;
/// <summary>
/// This system manages encryption keys & key holders for use with radio channels.
/// </summary>
public sealed class EncryptionKeySystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EncryptionKeyComponent, ExaminedEvent>(OnKeyExamined);
SubscribeLocalEvent<EncryptionKeyHolderComponent, ExaminedEvent>(OnHolderExamined);
SubscribeLocalEvent<EncryptionKeyHolderComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<EncryptionKeyHolderComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<EncryptionKeyHolderComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<EncryptionKeyHolderComponent, EntRemovedFromContainerMessage>(OnContainerModified);
}
public void UpdateChannels(EntityUid uid, EncryptionKeyHolderComponent component)
{
if (!component.Initialized)
return;
component.Channels.Clear();
component.DefaultChannel = null;
foreach (var ent in component.KeyContainer.ContainedEntities)
{
if (TryComp<EncryptionKeyComponent>(ent, out var key))
{
component.Channels.UnionWith(key.Channels);
component.DefaultChannel ??= key.DefaultChannel;
}
}
RaiseLocalEvent(uid, new EncryptionChannelsChangedEvent(component));
}
private void OnContainerModified(EntityUid uid, EncryptionKeyHolderComponent component, ContainerModifiedMessage args)
{
if (args.Container.ID == EncryptionKeyHolderComponent.KeyContainerName)
UpdateChannels(uid, component);
}
private void OnInteractUsing(EntityUid uid, EncryptionKeyHolderComponent component, InteractUsingEvent args)
{
if (!TryComp<ContainerManagerComponent>(uid, out var storage))
return;
if (TryComp<EncryptionKeyComponent>(args.Used, out var key))
{
args.Handled = true;
if (!component.KeysUnlocked)
{
if (_timing.IsFirstTimePredicted)
_popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-are-locked"), uid, Filter.Local(), false);
return;
}
if (component.KeySlots <= component.KeyContainer.ContainedEntities.Count)
{
if (_timing.IsFirstTimePredicted)
_popupSystem.PopupEntity(Loc.GetString("headset-encryption-key-slots-already-full"), uid, Filter.Local(), false);
return;
}
if (component.KeyContainer.Insert(args.Used))
{
if (_timing.IsFirstTimePredicted)
_popupSystem.PopupEntity(Loc.GetString("headset-encryption-key-successfully-installed"), uid, Filter.Local(), false);
_audio.PlayPredicted(component.KeyInsertionSound, args.Target, args.User);
return;
}
}
if (!TryComp<ToolComponent>(args.Used, out var tool) || !tool.Qualities.Contains(component.KeysExtractionMethod))
return;
args.Handled = true;
if (component.KeyContainer.ContainedEntities.Count == 0)
{
if (_timing.IsFirstTimePredicted)
_popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-no-keys"), uid, Filter.Local(), false);
return;
}
if (!_toolSystem.UseTool(args.Used, args.User, uid, 0f, 0f, component.KeysExtractionMethod, toolComponent: tool))
return;
var contained = component.KeyContainer.ContainedEntities.ToArray();
_container.EmptyContainer(component.KeyContainer, entMan: EntityManager);
foreach (var ent in contained)
{
_hands.PickupOrDrop(args.User, ent);
}
// if tool use ever gets predicted this needs changing.
_popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-all-extracted"), uid, args.User);
_audio.PlayPvs(component.KeyExtractionSound, args.Target);
}
private void OnStartup(EntityUid uid, EncryptionKeyHolderComponent component, ComponentStartup args)
{
component.KeyContainer = _container.EnsureContainer<Container>(uid, EncryptionKeyHolderComponent.KeyContainerName);
UpdateChannels(uid, component);
}
private void OnHolderExamined(EntityUid uid, EncryptionKeyHolderComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (component.KeyContainer.ContainedEntities.Count == 0)
{
args.PushMarkup(Loc.GetString("examine-headset-no-keys"));
return;
}
if (component.Channels.Count > 0)
{
args.PushMarkup(Loc.GetString("examine-headset-channels-prefix"));
AddChannelsExamine(component.Channels, component.DefaultChannel, args, _protoManager, "examine-headset-channel");
}
}
private void OnKeyExamined(EntityUid uid, EncryptionKeyComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if(component.Channels.Count > 0)
{
args.PushMarkup(Loc.GetString("examine-encryption-key-channels-prefix"));
AddChannelsExamine(component.Channels, component.DefaultChannel, args, _protoManager, "examine-headset-channel");
}
}
/// <summary>
/// A method for formating list of radio channels for examine events.
/// </summary>
/// <param name="channels">HashSet of channels in headset, encryptionkey or etc.</param>
/// <param name="protoManager">IPrototypeManager for getting prototypes of channels with their variables.</param>
/// <param name="channelFTLPattern">String that provide id of pattern in .ftl files to format channel with variables of it.</param>
public void AddChannelsExamine(HashSet<string> channels, string? defaultChannel, ExaminedEvent examineEvent, IPrototypeManager protoManager, string channelFTLPattern)
{
RadioChannelPrototype? proto;
foreach (var id in channels)
{
proto = protoManager.Index<RadioChannelPrototype>(id);
var key = id == SharedChatSystem.CommonChannel
? SharedChatSystem.RadioCommonPrefix.ToString()
: $"{SharedChatSystem.RadioChannelPrefix}{proto.KeyCode}";
examineEvent.PushMarkup(Loc.GetString(channelFTLPattern,
("color", proto.Color),
("key", key),
("id", proto.LocalizedName),
("freq", proto.Frequency)));
}
if (defaultChannel != null && _protoManager.TryIndex(defaultChannel, out proto))
{
var msg = Loc.GetString("examine-default-channel",
("prefix", SharedChatSystem.DefaultChannelPrefix),
("channel", defaultChannel),
("color", proto.Color));
examineEvent.PushMarkup(msg);
}
}
}

View File

@@ -0,0 +1,38 @@
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Radio.Components;
namespace Content.Shared.Radio.EntitySystems;
public abstract class SharedHeadsetSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HeadsetComponent, InventoryRelayedEvent<GetDefaultRadioChannelEvent>>(OnGetDefault);
SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
}
private void OnGetDefault(EntityUid uid, HeadsetComponent component, InventoryRelayedEvent<GetDefaultRadioChannelEvent> args)
{
if (!component.Enabled || !component.IsEquipped)
{
// don't provide default channels from pocket slots.
return;
}
if (TryComp(uid, out EncryptionKeyHolderComponent? keyHolder))
args.Args.Channel ??= keyHolder.DefaultChannel;
}
protected virtual void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args)
{
component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot);
}
protected virtual void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
{
component.IsEquipped = false;
}
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.Chat;
using Content.Shared.Inventory;
namespace Content.Shared.Radio;
public sealed class GetDefaultRadioChannelEvent : EntityEventArgs, IInventoryRelayEvent
{
/// <summary>
/// Id of the default <see cref="RadioChannelPrototype"/> that will get addressed when using the
/// department/default channel prefix. See <see cref="SharedChatSystem.DefaultChannelKey"/>.
/// </summary>
public string? Channel;
public SlotFlags TargetSlots => ~SlotFlags.POCKET;
}

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Threading;
using Content.Shared.Interaction;
using Content.Shared.Tools.Components;
using Robust.Shared.GameStates;
@@ -19,6 +20,32 @@ public abstract class SharedToolSystem : EntitySystem
SubscribeLocalEvent<MultipleToolComponent, ComponentHandleState>(OnMultipleToolHandleState);
}
public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
float doAfterDelay, string toolQualityNeeded, object? doAfterCompleteEvent = null, object? doAfterCancelledEvent = null, EntityUid? doAfterEventTarget = null,
Func<bool>? doAfterCheck = null, ToolComponent? toolComponent = null)
{
return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded },
doAfterCompleteEvent, doAfterCancelledEvent, doAfterEventTarget, doAfterCheck, toolComponent);
}
public virtual bool UseTool(
EntityUid tool,
EntityUid user,
EntityUid? target,
float fuel,
float doAfterDelay,
IEnumerable<string> toolQualitiesNeeded,
object? doAfterCompleteEvent = null,
object? doAfterCancelledEvent = null,
EntityUid? doAfterEventTarget = null,
Func<bool>? doAfterCheck = null,
ToolComponent? toolComponent = null,
CancellationToken? cancelToken = null)
{
// predicted tools when.
return false;
}
private void OnMultipleToolHandleState(EntityUid uid, MultipleToolComponent component, ref ComponentHandleState args)
{
if (args.Current is not MultipleToolComponentState state)