Сообщения в ПДА (#564)

* add: Сообщения в ПДА

* Search bar and colors in messages on the PDA
This commit is contained in:
Spatison
2024-08-06 16:58:30 +03:00
committed by GitHub
parent ea1ed76465
commit d023d29e54
28 changed files with 929 additions and 13 deletions

View File

@@ -26,7 +26,7 @@ namespace Content.Client.PDA
public const int ProgramContentView = 3; public const int ProgramContentView = 3;
private string _pdaOwner = Loc.GetString("comp-pda-ui-unknown"); private string _pdaOwnerName = Loc.GetString("comp-pda-ui-unknown");
private string _owner = Loc.GetString("comp-pda-ui-unknown"); private string _owner = Loc.GetString("comp-pda-ui-unknown");
private string _jobTitle = Loc.GetString("comp-pda-ui-unassigned"); private string _jobTitle = Loc.GetString("comp-pda-ui-unassigned");
private string _stationName = Loc.GetString("comp-pda-ui-unknown"); private string _stationName = Loc.GetString("comp-pda-ui-unknown");
@@ -96,7 +96,7 @@ namespace Content.Client.PDA
PdaOwnerButton.OnPressed += _ => PdaOwnerButton.OnPressed += _ =>
{ {
_clipboard.SetText(_pdaOwner); _clipboard.SetText(_pdaOwnerName);
}; };
IdInfoButton.OnPressed += _ => IdInfoButton.OnPressed += _ =>
@@ -138,9 +138,9 @@ namespace Content.Client.PDA
if (state.PdaOwnerInfo.ActualOwnerName != null) if (state.PdaOwnerInfo.ActualOwnerName != null)
{ {
_pdaOwner = state.PdaOwnerInfo.ActualOwnerName; _pdaOwnerName = state.PdaOwnerInfo.ActualOwnerName;
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner", PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("actualOwnerName", _pdaOwner))); ("actualOwnerName", _pdaOwnerName)));
} }

View File

@@ -0,0 +1,38 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared._White.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Robust.Client.UserInterface;
namespace Content.Client._White.CartridgeLoader.Cartridges;
public sealed partial class MessagesUi : UIFragment
{
private MessagesUiFragment _fragment;
public override Control GetUIFragmentRoot()
{
return _fragment;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new MessagesUiFragment();
_fragment.OnMessageSent += note => SendMessagesMessage(MessagesUiAction.Send, note, null, userInterface);
_fragment.OnButtonPressed += userUid => SendMessagesMessage(MessagesUiAction.ChangeChat, null, userUid, userInterface);
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not MessagesUiState messagesState)
return;
_fragment.UpdateState(messagesState.Mode, messagesState.Contents, messagesState.Name);
}
private void SendMessagesMessage(MessagesUiAction action, string? stringInput, int? uidInput, BoundUserInterface userInterface)
{
var messagesMessage = new MessagesUiMessageEvent(action, stringInput, uidInput);
var message = new CartridgeUiMessage(messagesMessage);
userInterface.SendMessage(message);
}
}

View File

@@ -0,0 +1,129 @@
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Content.Shared._White.CartridgeLoader.Cartridges;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Client._White.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class MessagesUiFragment : BoxContainer
{
public event Action<string>? OnMessageSent;
public event Action<int?>? OnButtonPressed;
private string _searchText = string.Empty;
public MessagesUiFragment()
{
RobustXamlLoader.Load(this);
Input.OnTextEntered += _ =>
{
if (!string.IsNullOrEmpty(Input.Text))
OnMessageSent?.Invoke(Input.Text);
Input.Clear();
};
BackButton.OnPressed += _ => OnButtonPressed?.Invoke(null);
SearchBar.OnTextChanged += OnSearchTextChanged;
UpdateState(MessagesUiStateMode.UserList, [], null);
}
public void UpdateState(MessagesUiStateMode mode, List<(MessagesUser, int?)>? contents, string? name)
{
MessageContainer.DisposeAllChildren();
Input.Orphan();
BackButton.Orphan();
SearchBar.Visible = false;
if (contents == null)
return;
if (mode == MessagesUiStateMode.Chat)
{
HeaderLabel.Text = name;
foreach (var (senderName, message) in contents)
{
AddNote($"{senderName.Name} {message}");
}
OverContainer.AddChild(Input);
HeaderBox.AddChild(BackButton);
BackButton.SetPositionInParent(0);
}
else if (mode == MessagesUiStateMode.Error)
{
HeaderLabel.Text = Loc.GetString("messages-pda-error-header");
AddNote(Loc.GetString("messages-pda-error-message"));
}
else
{
SearchBar.Visible = true;
HeaderLabel.Text = Loc.GetString("messages-pda-chat-choice");
foreach (var (messagesUser, userUid) in contents)
{
AddButton(userUid, messagesUser.Name + ", " + messagesUser.Job, messagesUser.Department);
}
}
}
///<summary>
/// Adding a button for selecting a chat
///</summary>
private void AddButton(int? userUid, string userName, string departmentId)
{
var styleClass = IoCManager.Resolve<IPrototypeManager>().Index<DepartmentPrototype>(departmentId).ButtonStyle;
var button = new Button
{
Text = userName,
HorizontalExpand = true,
ClipText = true,
StyleClasses = { styleClass },
MinWidth = 60
};
button.OnPressed += _ => OnButtonPressed?.Invoke(userUid);
MessageContainer.AddChild(button);
}
///<summary>
/// Adding a text label to the message container
///</summary>
private void AddNote(string note)
{
MessageContainer.AddChild(new Label
{
Text = note,
HorizontalExpand = true,
ClipText = false
});
}
private bool ButtonIsVisible(Button button)
{
return string.IsNullOrEmpty(_searchText) || button.Text == null || button.Text.Contains(_searchText, StringComparison.OrdinalIgnoreCase);
}
private void UpdateVisibleButtons()
{
foreach (var child in MessageContainer.Children)
{
if (child is Button button)
button.Visible = ButtonIsVisible(button);
}
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
{
_searchText = args.Text;
UpdateVisibleButtons();
// Reset scroll bar so they can see the relevant results.
MessagesScroll.SetScrollValue(Vector2.Zero);
}
}

View File

@@ -0,0 +1,17 @@
<cartridges1:MessagesUiFragment
xmlns="https://spacestation14.io"
xmlns:cartridges1="clr-namespace:Content.Client._White.CartridgeLoader.Cartridges"
Margin="1 0 2 0" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<PanelContainer StyleClasses="BackgroundDark"/>
<BoxContainer Name="OverContainer" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="HeaderBox">
<Button Name="BackButton" Text="{Loc 'messages-pda-ui-back'}" HorizontalExpand="False"/>
<Label Name="HeaderLabel"/>
</BoxContainer>
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True" Margin="0 4" />
<ScrollContainer Name="MessagesScroll" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical" Name="MessageContainer" HorizontalExpand="True" VerticalExpand="True"/>
</ScrollContainer>
<LineEdit Name="Input" HorizontalExpand="True" SetHeight="32"/>
</BoxContainer>
</cartridges1:MessagesUiFragment>

View File

@@ -56,7 +56,7 @@ public sealed class SingletonDeviceNetServerSystem : EntitySystem
if (!server.Available) if (!server.Available)
{ {
DisconnectServer(uid,server, device); DisconnectServer(uid, server, device);
continue; continue;
} }

View File

@@ -88,7 +88,7 @@ public sealed class RenameCommand : IConsoleCommand
{ {
if (pda.OwnerName == oldName) if (pda.OwnerName == oldName)
{ {
pdaSystem.SetOwner(uid, pda, name); pdaSystem.SetOwnerName(uid, pda, name);
} }
} }
} }

View File

@@ -14,6 +14,7 @@ using Content.Shared.CartridgeLoader;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Light.Components; using Content.Shared.Light.Components;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Roles;
using Robust.Server.Containers; using Robust.Server.Containers;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
@@ -90,12 +91,24 @@ namespace Content.Server.PDA
UpdatePdaUi(uid, pda); UpdatePdaUi(uid, pda);
} }
public void SetOwner(EntityUid uid, PdaComponent pda, string ownerName) public void SetOwnerName(EntityUid uid, PdaComponent pda, string ownerName)
{ {
pda.OwnerName = ownerName; pda.OwnerName = ownerName;
UpdatePdaUi(uid, pda); UpdatePdaUi(uid, pda);
} }
public void SetOwnerJob(EntityUid uid, PdaComponent pda, string ownerJob)
{
pda.OwnerJob = ownerJob;
UpdatePdaUi(uid, pda);
}
public void SetOwnerDepartment(EntityUid uid, PdaComponent pda, string ownerDepartment)
{
pda.OwnerDepartment = ownerDepartment;
UpdatePdaUi(uid, pda);
}
private void OnStationRenamed(StationRenamedEvent ev) private void OnStationRenamed(StationRenamedEvent ev)
{ {
UpdateAllPdaUisOnStation(); UpdateAllPdaUisOnStation();
@@ -172,6 +185,7 @@ namespace Content.Server.PDA
new PdaIdInfoText new PdaIdInfoText
{ {
ActualOwnerName = pda.OwnerName, ActualOwnerName = pda.OwnerName,
ActualOwnerJob = pda.OwnerJob, // WD EDIT
IdOwner = id?.FullName, IdOwner = id?.FullName,
JobTitle = id?.JobTitle, JobTitle = id?.JobTitle,
StationAlertLevel = pda.StationAlertLevel, StationAlertLevel = pda.StationAlertLevel,

View File

@@ -12,6 +12,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Server.Roles.Jobs;
using Content.Shared.Humanoid.Prototypes; using Content.Shared.Humanoid.Prototypes;
using Content.Shared.PDA; using Content.Shared.PDA;
using Content.Shared.Preferences; using Content.Shared.Preferences;
@@ -49,6 +50,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
[Dependency] private readonly SharedAccessSystem _accessSystem = default!; [Dependency] private readonly SharedAccessSystem _accessSystem = default!;
[Dependency] private readonly IdentitySystem _identity = default!; [Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly JobSystem _jobs = default!; // WD EDIT
[Dependency] private readonly ArrivalsSystem _arrivalsSystem = default!; [Dependency] private readonly ArrivalsSystem _arrivalsSystem = default!;
[Dependency] private readonly ContainerSpawnPointSystem _containerSpawnPointSystem = default!; [Dependency] private readonly ContainerSpawnPointSystem _containerSpawnPointSystem = default!;
@@ -337,7 +339,14 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
_accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess); _accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess);
if (pdaComponent != null) if (pdaComponent != null)
_pdaSystem.SetOwner(idUid.Value, pdaComponent, characterName); {
_pdaSystem.SetOwnerName(idUid.Value, pdaComponent, characterName);
//WD EDIT START
_pdaSystem.SetOwnerJob(idUid.Value, pdaComponent, jobPrototype.LocalizedName);
_jobs.TryGetDepartment(jobPrototype.ID, out var department);
_pdaSystem.SetOwnerDepartment(idUid.Value, pdaComponent, department?.ID ?? "Specific");
//WD EDIT END
}
} }

View File

@@ -0,0 +1,20 @@
namespace Content.Server._White.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class MessagesCartridgeComponent : Component
{
/// <summary>
/// The component of the last contacted server
/// </summary>
[DataField]
public EntityUid? LastServer;
/// <summary>
/// The message system user id of the crew the user is chatting with
/// </summary>
[DataField]
public int? ChatUid;
[DataField]
public int? UserUid;
}

View File

@@ -0,0 +1,290 @@
using Content.Server._White.Radio.Components;
using Content.Server._White.Radio.EntitySystems;
using Content.Server.Administration.Commands;
using Content.Server.CartridgeLoader;
using Content.Shared.CartridgeLoader;
using Content.Shared.PDA;
using Robust.Shared.Map;
using Content.Server.GameTicking;
using Robust.Shared.Timing;
using Content.Server.DeviceNetwork.Systems;
using Content.Shared.DeviceNetwork;
using Content.Server.Station.Systems;
using Content.Shared._White.CartridgeLoader.Cartridges;
using Content.Shared.Access.Components;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server._White.CartridgeLoader.Cartridges;
public sealed class MessagesCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly MessagesServerSystem _messagesServerSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MessagesCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<MessagesCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<MessagesCartridgeComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<MessagesCartridgeComponent, CartridgeActivatedEvent>(OnCartActivation);
SubscribeLocalEvent<MessagesCartridgeComponent, CartridgeDeactivatedEvent>(OnCartDeactivation);
SubscribeLocalEvent<MessagesCartridgeComponent, CartridgeAddedEvent>(OnCartInsertion);
SubscribeLocalEvent<MessagesCartridgeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<MessagesCartridgeComponent, ComponentRemove>(OnRemove);
}
private void OnInit(EntityUid uid, MessagesCartridgeComponent component, ComponentInit args)
{
var stationId = _stationSystem.GetOwningStation(uid);
if (stationId.HasValue &&
_singletonServerSystem.TryGetActiveServerAddress<MessagesServerComponent>(stationId.Value,
out var address) && TryComp(uid, out CartridgeComponent? cartComponent))
{
SendName(uid, component, cartComponent, address);
component.UserUid = cartComponent.LoaderUid?.Id;
}
}
private void OnRemove(EntityUid uid, MessagesCartridgeComponent component, ComponentRemove args)
{
if (component.LastServer == null || !TryComp<MessagesServerComponent>(component.LastServer, out var messagesServerComponent) || component.UserUid == null)
return;
messagesServerComponent.NameDict.Remove(component.UserUid.Value);
}
/// <summary>
/// This gets called when the ui fragment needs to be updated for the first time after activating
/// </summary>
private void OnUiReady(EntityUid uid, MessagesCartridgeComponent component, CartridgeUiReadyEvent args)
{
var stationId = _stationSystem.GetOwningStation(uid);
if (stationId.HasValue && _singletonServerSystem.TryGetActiveServerAddress<MessagesServerComponent>(stationId.Value, out var address) && TryComp(uid, out CartridgeComponent? cartComponent))
SendName(uid, component, cartComponent, address);
UpdateUiState(uid, component);
}
/// <summary>
/// The ui messages received here get wrapped by a CartridgeMessageEvent and are relayed from the <see cref="CartridgeLoaderSystem"/>
/// </summary>
/// <remarks>
/// The cartridge specific ui message event needs to inherit from the CartridgeMessageEvent
/// </remarks>
private void OnUiMessage(EntityUid uid, MessagesCartridgeComponent component, CartridgeMessageEvent args)
{
if (args is not MessagesUiMessageEvent messageEvent)
return;
if (messageEvent.Action == MessagesUiAction.Send && TryComp(uid, out CartridgeComponent? cartComponent) && component.UserUid is { } userId && component.ChatUid != null && messageEvent.StringInput != null)
{
var stationId = _stationSystem.GetOwningStation(uid);
if (!stationId.HasValue)
return;
MessagesMessageData messageData = new()
{
SenderId = userId,
ReceiverId = component.ChatUid.Value,
Content = messageEvent.StringInput,
Time = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan)
};
var packet = new NetworkPayload()
{
[MessagesNetworkKeys.Message] = messageData
};
_singletonServerSystem.TryGetActiveServerAddress<MessagesServerComponent>(stationId.Value, out var address);
_deviceNetworkSystem.QueuePacket(uid, address, packet);
}
else
{
if (messageEvent.Action == MessagesUiAction.ChangeChat)
component.ChatUid = messageEvent.TargetChatUid;
}
UpdateUiState(uid, component);
}
/// <summary>
/// On cart insertion, register as background process.
/// </summary>
private void OnCartInsertion(EntityUid uid, MessagesCartridgeComponent component, CartridgeAddedEvent args)
{
_cartridgeLoaderSystem.RegisterBackgroundProgram(args.Loader, uid);
component.UserUid = args.Loader.Id;
}
/// <summary>
/// On cartridge activation, connect to messages network.
/// </summary>
private void OnCartActivation(EntityUid uid, MessagesCartridgeComponent component, CartridgeActivatedEvent args)
{
_deviceNetworkSystem.ConnectDevice(uid);
var stationId = _stationSystem.GetOwningStation(uid);
if (stationId.HasValue && _singletonServerSystem.TryGetActiveServerAddress<MessagesServerComponent>(stationId.Value, out var address) && TryComp(uid, out CartridgeComponent? cartComponent))
SendName(uid, component, cartComponent, address);
}
/// <summary>
/// On cartridge deactivation, disconnect from messages network.
/// </summary>
private void OnCartDeactivation(EntityUid uid, MessagesCartridgeComponent component, CartridgeDeactivatedEvent args)
{
_deviceNetworkSystem.DisconnectDevice(uid, null);
}
/// <summary>
/// React and respond to packets from the server
/// </summary>
private void OnPacketReceived(EntityUid uid, MessagesCartridgeComponent component, DeviceNetworkPacketEvent args)
{
if (!TryComp(uid, out CartridgeComponent? cartComponent))
return;
component.LastServer = args.Sender;
if (args.Data.TryGetValue<MessagesMessageData>(MessagesNetworkKeys.Message, out var message) && cartComponent.LoaderUid != null)
{
if (message.ReceiverId == cartComponent.LoaderUid.Value.Id)
{
TryGetName(message.SenderId, component, out var name);
var subtitleString = Loc.GetString("messages-pda-notification-header", ("name", name));
_cartridgeLoaderSystem.SendNotification(
cartComponent.LoaderUid.Value,
subtitleString,
message.Content);
}
}
UpdateUiState(uid, component);
}
/// <summary>
/// Updates the user's name in the storage component.
/// </summary>
private void SendName(EntityUid uid, MessagesCartridgeComponent component, CartridgeComponent cartComponent, string? address)
{
TryGetMessagesUser(cartComponent, out var messagesUser);
var packet = new NetworkPayload()
{
[MessagesNetworkKeys.UserId] = component.UserUid,
[MessagesNetworkKeys.NewUser] = messagesUser
};
_deviceNetworkSystem.QueuePacket(uid, address, packet);
}
/// <summary>
/// Retrieves the name of the given user from the last contacted server
/// </summary>
private bool TryGetName(int key, MessagesCartridgeComponent component, out string name)
{
if (component.LastServer != null && _messagesServerSystem.TryGetUserFromDict(component.LastServer, key, out var messagesUser))
{
name = messagesUser.Name;
return true;
}
name = Loc.GetString("messages-pda-connection-error");
return false;
}
/// <summary>
/// Returns the user's name, job title and job department
/// </summary>
public bool TryGetMessagesUser(CartridgeComponent component, out MessagesUser messagesUser)
{
var pda = component.LoaderUid;
if (pda == null)
{
messagesUser = new MessagesUser(Loc.GetString("messages-pda-unknown-name"), Loc.GetString("messages-pda-unknown-job"), "Specific");
return false;
}
var pdaComponent = CompOrNull<PdaComponent>(pda);
if (pdaComponent?.OwnerName == null)
{
messagesUser = new MessagesUser(Loc.GetString("messages-pda-unknown-name"), Loc.GetString("messages-pda-unknown-job"), "Specific");
return false;
}
messagesUser = new MessagesUser(pdaComponent.OwnerName, pdaComponent.OwnerJob ?? Loc.GetString("messages-pda-unknown-job"), pdaComponent.OwnerDepartment ?? "Specific");
return true;
}
private void UpdateUiState(EntityUid uid, MessagesCartridgeComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (!TryComp(uid, out CartridgeComponent? cartComponent))
return;
if (cartComponent.LoaderUid == null)
return;
var loaderUid = cartComponent.LoaderUid.Value;
MessagesUiState state;
MapId mapId = Transform(uid).MapID;
int? currentUserId = component.UserUid;
if (currentUserId == null || component.LastServer == null)
{
state = new MessagesUiState(MessagesUiStateMode.Error, [], null);
_cartridgeLoaderSystem.UpdateCartridgeUiState(loaderUid, state);
return;
}
if (component.ChatUid == null) //if no chat is loaded, list users
{
List<(MessagesUser, int?)> userList = [];
var nameDict = _messagesServerSystem.GetNameDict(component.LastServer);
foreach (var nameEntry in nameDict.Keys)
{
if (nameEntry == currentUserId)
continue;
userList.Add((nameDict[nameEntry], nameEntry));
}
state = new MessagesUiState(MessagesUiStateMode.UserList, userList, null);
}
else
{
List<MessagesMessageData> messageList = []; //Else, list messages from the current chat
foreach (var message in _messagesServerSystem.GetMessages(component.LastServer, component.ChatUid.Value, currentUserId.Value))
{
if (message.SenderId == component.ChatUid && message.ReceiverId == currentUserId || message.ReceiverId == component.ChatUid && message.SenderId == currentUserId)
{
messageList.Add(message);
}
}
messageList.Sort
(
delegate (MessagesMessageData a, MessagesMessageData b)
{
return TimeSpan.Compare(a.Time, b.Time);
}
);
List<(MessagesUser, int?)> formattedMessageList = [];
foreach (var message in messageList)
{
TryGetName(message.SenderId, component, out var name);
var stationTime = message.Time.Subtract(_gameTicker.RoundStartTimeSpan);
var content = $"{stationTime.ToString("\\[hh\\:mm\\:ss\\]")} {name}: {message.Content}";
formattedMessageList.Add((new MessagesUser(content, Loc.GetString("messages-pda-unknown-job"), "Specific"), null));
}
TryGetName(component.ChatUid.Value, component, out var user);
state = new MessagesUiState(MessagesUiStateMode.Chat, formattedMessageList, user);
}
_cartridgeLoaderSystem.UpdateCartridgeUiState(loaderUid, state);
}
}

View File

@@ -58,7 +58,7 @@ public sealed class RandomHumanSystem : EntitySystem
var cardId = pdaComponent.ContainedId.Value; var cardId = pdaComponent.ContainedId.Value;
_card.TryChangeFullName(cardId, newProfile.Name, card); _card.TryChangeFullName(cardId, newProfile.Name, card);
_pda.SetOwner(idUid.Value, pdaComponent, newProfile.Name); _pda.SetOwnerName(idUid.Value, pdaComponent, newProfile.Name);
if (EntityManager.TryGetComponent(cardId, out StationRecordKeyStorageComponent? keyStorage) if (EntityManager.TryGetComponent(cardId, out StationRecordKeyStorageComponent? keyStorage)
&& keyStorage.Key is { } key) && keyStorage.Key is { } key)

View File

@@ -0,0 +1,26 @@
using Content.Server.Power.Components;
using Content.Shared._White.CartridgeLoader.Cartridges;
namespace Content.Server._White.Radio.Components;
/// <summary>
/// Entities with <see cref="MessagesServerComponent"/> are needed to transmit messages using PDAs.
/// They also need to be powered by <see cref="ApcPowerReceiverComponent"/>
/// in order for them to work on the same map as server.
/// </summary>
[RegisterComponent]
public sealed partial class MessagesServerComponent : Component
{
/// <summary>
/// The list of messages cached by the server.
/// </summary>
[DataField]
public List<MessagesMessageData> Messages = [];
/// <summary>
/// Dictionary translating uids to readable names
/// </summary>
[DataField]
public Dictionary<int, MessagesUser> NameDict = [];
}

View File

@@ -0,0 +1,128 @@
using System.Linq;
using Content.Server._White.CartridgeLoader.Cartridges;
using Content.Server._White.Radio.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Shared._White.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Content.Shared.DeviceNetwork;
namespace Content.Server._White.Radio.EntitySystems;
public sealed class MessagesServerSystem : EntitySystem
{
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
[Dependency] private readonly MessagesCartridgeSystem _messagesSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MessagesServerComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<MessagesServerComponent, ComponentInit>(OnInit);
}
private void OnInit(EntityUid uid, MessagesServerComponent component, ComponentInit args)
{
var query = EntityQueryEnumerator<MessagesCartridgeComponent>();
while (query.MoveNext(out var entityUid, out var cartridge))
{
if (!TryComp(entityUid, out CartridgeComponent? cartComponent))
continue;
_messagesSystem.TryGetMessagesUser(cartComponent, out var messagesUser);
if (cartridge.UserUid == null || messagesUser.Name == Loc.GetString("messages-pda-unknown-name"))
continue;
component.NameDict[cartridge.UserUid.Value] = messagesUser;
cartridge.LastServer = uid;
}
}
/// <summary>
/// Reacts to packets received from clients
/// </summary>
private void OnPacketReceived(EntityUid uid, MessagesServerComponent component, DeviceNetworkPacketEvent args)
{
if (!_singletonServerSystem.IsActiveServer(uid))
return;
if (args.Data.TryGetValue<MessagesUser>(MessagesNetworkKeys.NewUser, out var messagesUser) && args.Data.TryGetValue<int>(MessagesNetworkKeys.UserId, out var userId))
{
component.NameDict[userId] = messagesUser;
var packet = new NetworkPayload();
_deviceNetworkSystem.QueuePacket(uid, args.SenderAddress, packet);
}
if (args.Data.TryGetValue<MessagesMessageData>(MessagesNetworkKeys.Message, out var message))
SendMessage(uid, component, message);
}
/// <summary>
/// Broadcasts a message into the network
/// </summary>
private void SendMessage(EntityUid uid, MessagesServerComponent component, MessagesMessageData message)
{
component.Messages.Add(message);
var packet = new NetworkPayload()
{
[MessagesNetworkKeys.Message] = message
};
_deviceNetworkSystem.QueuePacket(uid, null, packet);
}
/// <summary>
/// Returns user
/// </summary>
public bool TryGetUserFromDict(EntityUid? uid, int key, out MessagesUser messagesUser)
{
if (!TryComp(uid, out MessagesServerComponent? component))
{
messagesUser = new MessagesUser(Loc.GetString("messages-pda-connection-error"), Loc.GetString("messages-pda-unknown-job"), "Specific");
return false;
}
if (component.NameDict.TryGetValue(key, out var keyValue))
{
messagesUser = keyValue;
return true;
}
messagesUser = new MessagesUser(Loc.GetString("messages-pda-user-missing"), Loc.GetString("messages-pda-unknown-job"), "Specific");
return false;
}
/// <summary>
/// Returns the name dictionary cache
/// </summary>
public Dictionary<int, MessagesUser> GetNameDict(EntityUid? uid)
{
if (!TryComp(uid, out MessagesServerComponent? component))
return new Dictionary<int, MessagesUser>();
return component.NameDict;
}
/// <summary>
/// Returns list of messages between the two users
/// </summary>
public List<MessagesMessageData> GetMessages(EntityUid? uid, int id1, int id2)
{
if (!TryComp(uid, out MessagesServerComponent? component))
return [];
return
[
..component.Messages.Where(message =>
message.SenderId == id1 && message.ReceiverId == id2 ||
message.SenderId == id2 && message.ReceiverId == id1)
];
}
}
public static class MessagesNetworkKeys
{
public const string NewUser = "new_user";
public const string UserId = "user_id";
public const string Message = "message";
}

View File

@@ -2,6 +2,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.PDA namespace Content.Shared.PDA
@@ -37,6 +38,8 @@ namespace Content.Shared.PDA
[ViewVariables] public bool FlashlightOn; [ViewVariables] public bool FlashlightOn;
[ViewVariables(VVAccess.ReadWrite)] public string? OwnerName; [ViewVariables(VVAccess.ReadWrite)] public string? OwnerName;
[ViewVariables(VVAccess.ReadWrite)] public string? OwnerJob; // WD EDIT
[ViewVariables(VVAccess.ReadWrite)] public string? OwnerDepartment; // WD EDIT
[ViewVariables] public string? StationName; [ViewVariables] public string? StationName;
[ViewVariables] public string? StationAlertLevel; [ViewVariables] public string? StationAlertLevel;
[ViewVariables] public Color StationAlertColor = Color.White; [ViewVariables] public Color StationAlertColor = Color.White;

View File

@@ -45,6 +45,7 @@ namespace Content.Shared.PDA
public struct PdaIdInfoText public struct PdaIdInfoText
{ {
public string? ActualOwnerName; public string? ActualOwnerName;
public string? ActualOwnerJob; // WD EDIT
public string? IdOwner; public string? IdOwner;
public string? JobTitle; public string? JobTitle;
public string? StationAlertLevel; public string? StationAlertLevel;

View File

@@ -0,0 +1,26 @@
using Content.Shared.CartridgeLoader;
using Robust.Shared.Serialization;
namespace Content.Shared._White.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class MessagesUiMessageEvent : CartridgeMessageEvent
{
public readonly MessagesUiAction Action;
public readonly int? TargetChatUid;
public readonly string? StringInput;
public MessagesUiMessageEvent(MessagesUiAction action, string? stringInput, int? targetChatUid)
{
Action = action;
TargetChatUid = targetChatUid;
StringInput = stringInput;
}
}
[Serializable, NetSerializable]
public enum MessagesUiAction : byte
{
Send,
ChangeChat
}

View File

@@ -0,0 +1,47 @@
using Robust.Shared.Serialization;
namespace Content.Shared._White.CartridgeLoader.Cartridges;
///<summary>
/// The state of the messages app interface.
/// Mode switches whether the UI should display a list of other users or a particular chat.
/// Contents contains either the names of users and their ids in the messages system or simply a list of message strings.
///</summary>
[Serializable, NetSerializable]
public sealed class MessagesUiState(MessagesUiStateMode mode, List<(MessagesUser, int?)> contents, string? name = null) : BoundUserInterfaceState
{
public List<(MessagesUser, int?)>? Contents = contents;
public MessagesUiStateMode Mode = mode;
public string? Name = name;
}
///<summary>
/// Enum representing the modes the program's UI can be in
///</summary>
[Serializable, NetSerializable]
public enum MessagesUiStateMode : byte
{
UserList,
Chat,
Error
}
///<summary>
/// Data of a single message in the system, containing the ids of the sender and recipient, the text content and the time it was sent.
///</summary>
[Serializable, NetSerializable]
public struct MessagesMessageData
{
public int SenderId;
public int ReceiverId;
public string Content;
public TimeSpan Time;
}
[Serializable]
public sealed class MessagesUser(string name, string job, string department)
{
public string Name = name;
public string Job = job;
public string Department = department;
}

View File

@@ -3,6 +3,7 @@ device-pda-slot-component-slot-name-cartridge = Cartridge
default-program-name = Program default-program-name = Program
notekeeper-program-name = Notekeeper notekeeper-program-name = Notekeeper
news-read-program-name = Station news news-read-program-name = Station news
messages-program-name = Messages
crew-manifest-program-name = Crew manifest crew-manifest-program-name = Crew manifest
crew-manifest-cartridge-loading = Loading ... crew-manifest-cartridge-loading = Loading ...

View File

@@ -0,0 +1,9 @@
messages-pda-error-header = ERROR
messages-pda-error-message = An error has occured in the messages system, have you tried turning it off and on again?
messages-pda-notification-header = A new message from {$name} has arrived on your PDA
messages-pda-connection-error = CONNECTION ERROR
messages-pda-unknown-name = Unknown
messages-pda-unknown-job = Unassigned
messages-pda-user-missing = NAME ENTRY MISSING
messages-pda-ui-back = Back
messages-pda-chat-choice = Select chat

View File

@@ -3,6 +3,7 @@ device-pda-slot-component-slot-name-cartridge = Картридж
default-program-name = Программа default-program-name = Программа
notekeeper-program-name = Заметки notekeeper-program-name = Заметки
news-read-program-name = Новости станции news-read-program-name = Новости станции
messages-program-name = Сообщения
crew-manifest-program-name = Манифест персонала crew-manifest-program-name = Манифест персонала
crew-manifest-cartridge-loading = Загрузка ... crew-manifest-cartridge-loading = Загрузка ...

View File

@@ -3,7 +3,7 @@
# For the PDA screen # For the PDA screen
comp-pda-ui = ID: [color=white]{ $owner }[/color], [color=yellow]{ CAPITALIZE($jobTitle) }[/color] comp-pda-ui = ID: [color=white]{ $owner }[/color], [color=yellow]{ CAPITALIZE($jobTitle) }[/color]
comp-pda-ui-blank = ID: comp-pda-ui-blank = ID:
comp-pda-ui-owner = Владелец: [color=white]{ $actualOwnerName }[/color] comp-pda-ui-owner = Владелец: [color=white]{$actualOwnerName}[/color]
comp-pda-io-program-list-button = Программы comp-pda-io-program-list-button = Программы
comp-pda-io-settings-button = Настройки comp-pda-io-settings-button = Настройки
comp-pda-io-program-fallback-title = Программа comp-pda-io-program-fallback-title = Программа

View File

@@ -0,0 +1,9 @@
messages-pda-error-header = ОШИБКА
messages-pda-error-message = В системе сообщений произошла ошибка, вы пробовали отключить и включить ее снова?
messages-pda-notification-header = На ваш ПДА поступило новое сообщение от {$name}
messages-pda-connection-error = ОШИБКА СОЕДЕНЕНИЯ
messages-pda-unknown-name = Неизвестный
messages-pda-unknown-job = Неназначен
messages-pda-user-missing = ОТСУТСТВУЕТ ЗАПИСЬ ИМЕНИ
messages-pda-ui-back = Назад
messages-pda-chat-choice = Выберите чат

View File

@@ -4,3 +4,9 @@ ent-NotekeeperCartridge = картридж Заметки
ent-NetProbeCartridge = картридж NetProbe ent-NetProbeCartridge = картридж NetProbe
.desc = Программа для получения адресов и частот сетевых устройств .desc = Программа для получения адресов и частот сетевых устройств
.suffix = { "" } .suffix = { "" }
ent-MessagesCartridge = картридж Сообщений
.desc = Программа для обмена сообщениями с другими членами экипажа
.suffix = { "" }
ent-MessagesCartridgeSyndicate = картридж Сообщений Синдиката
.desc = Программа для обмена сообщениями с другими агентами Синдиката
.suffix = { "" }

View File

@@ -0,0 +1,4 @@
ent-MessagesServer = сервер сообщений ПДА
.desc = Сервер, который позволяет передавать сообщения с помощью ПДА
ent-SyndicateMessagesServer = сервер сообщений ПДА Синдиката
.desc = Сервер, который позволяет обмениваться сообщениями с помощью ПДА между оперативниками Синдиката.

View File

@@ -101,3 +101,23 @@
id: BasicDevice id: BasicDevice
name: device-frequency-prototype-name-basic-device name: device-frequency-prototype-name-basic-device
frequency: 1280 frequency: 1280
- type: deviceFrequency
id: NTMessagesServer
name: device-frequency-prototype-name-nt-messages-server
frequency: 2790
- type: deviceFrequency
id: NTMessagesClient
name: device-frequency-prototype-name-nt-messages-client
frequency: 2791
- type: deviceFrequency
id: SyndicateMessagesServer
name: device-frequency-prototype-name-syndicate-messages-server
frequency: 2792
- type: deviceFrequency
id: SyndicateMessagesClient
name: device-frequency-prototype-name-syndicate-messages-client
frequency: 2793

View File

@@ -34,6 +34,54 @@
state: news_read state: news_read
- type: NewsReaderCartridge - type: NewsReaderCartridge
- type: entity
parent: BaseItem
id: MessagesCartridge
name: messages cartridge
description: A program for messageing other crew
components:
- type: Sprite
sprite: Objects/Devices/cartridge.rsi
state: cart-y
- type: UIFragment
ui: !type:MessagesUi
- type: Cartridge
programName: messages-program-name
icon:
sprite: Objects/Misc/books.rsi
state: book_icon
- type: MessagesCartridge
- type: DeviceNetwork
deviceNetId: Wireless
transmitFrequencyId: NTMessagesClient
receiveFrequencyId: NTMessagesServer
autoConnect: false
- type: StationLimitedNetwork
- type: entity
parent: BaseItem
id: MessagesCartridgeSyndicate
name: syndicate messages cartridge
description: A program for messageing other syndicate agents
components:
- type: Sprite
sprite: Objects/Devices/cartridge.rsi
state: cart-y
- type: UIFragment
ui: !type:MessagesUi
- type: Cartridge
programName: messages-program-name
icon:
sprite: Objects/Misc/books.rsi
state: book_icon
- type: MessagesCartridge
- type: DeviceNetwork
deviceNetId: Wireless
transmitFrequencyId: SyndicateMessagesClient
receiveFrequencyId: SyndicateMessagesServer
autoConnect: false
- type: StationLimitedNetwork
- type: entity - type: entity
parent: BaseItem parent: BaseItem
id: CrewManifestCartridge id: CrewManifestCartridge

View File

@@ -74,6 +74,7 @@
preinstalled: preinstalled:
- CrewManifestCartridge - CrewManifestCartridge
- NotekeeperCartridge - NotekeeperCartridge
- MessagesCartridge
- NewsReaderCartridge - NewsReaderCartridge
- BankCartridge - BankCartridge
cartridgeSlot: cartridgeSlot:
@@ -682,6 +683,7 @@
preinstalled: preinstalled:
- CrewManifestCartridge - CrewManifestCartridge
- NotekeeperCartridge - NotekeeperCartridge
- MessagesCartridge
- NewsReaderCartridge - NewsReaderCartridge
- LogProbeCartridge - LogProbeCartridge
@@ -766,6 +768,7 @@
uiKey: enum.PdaUiKey.Key uiKey: enum.PdaUiKey.Key
preinstalled: preinstalled:
- NotekeeperCartridge - NotekeeperCartridge
- MessagesCartridgeSyndicate
cartridgeSlot: cartridgeSlot:
priority: -1 priority: -1
name: Cartridge name: Cartridge
@@ -1067,6 +1070,7 @@
uiKey: enum.PdaUiKey.Key uiKey: enum.PdaUiKey.Key
preinstalled: preinstalled:
- NotekeeperCartridge - NotekeeperCartridge
- MessagesCartridgeSyndicate
cartridgeSlot: cartridgeSlot:
priority: -1 priority: -1
name: Cartridge name: Cartridge

View File

@@ -80,3 +80,69 @@
- EncryptionKeySecurity - EncryptionKeySecurity
- EncryptionKeyService - EncryptionKeyService
- EncryptionKeyCommand - EncryptionKeyCommand
- type: entity
id: MessagesServer
parent: BaseMachinePowered
name: PDA messaging server
description: Server that allows PDA messaging to function on the station.
components:
- type: Sprite
sprite: Structures/Machines/server.rsi
layers:
- state: server
- state: variant-research
- type: ApcPowerReceiver
powerLoad: 200
- type: ExtensionCableReceiver
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 600
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- trigger:
!type:DamageTrigger
damage: 300
behaviors:
- !type:DoActsBehavior
acts: ["Destruction"]
- !type:PlaySoundBehavior
sound:
collection: MetalBreak
- !type:SpawnEntitiesBehavior
spawn:
SheetSteel1:
min: 1
max: 2
- type: Appearance
- type: AmbientSound
volume: -9
range: 5
sound:
path: /Audio/Ambience/Objects/server_fans.ogg
- type: MessagesServer
- type: SingletonDeviceNetServer
- type: DeviceNetwork
deviceNetId: Wireless
transmitFrequencyId: NTMessagesServer
receiveFrequencyId: NTMessagesClient
autoConnect: false
- type: StationLimitedNetwork
- type: entity
id: SyndicateMessagesServer
parent: MessagesServer
name: Syndicate PDA messaging server
description: Server that allows PDA messaging between Syndicate operatives to function.
components:
- type: MessagesServer
- type: StationLimitedNetwork
- type: SingletonDeviceNetServer
- type: DeviceNetwork
deviceNetId: Wireless
transmitFrequencyId: SyndicateMessagesServer
receiveFrequencyId: SyndicateMessagesClient
autoConnect: false