ыыыыы, месенжер макс, ихихяхя (#3789)
Co-authored-by: Daniel <77834935+Orvex07@users.noreply.github.com>
|
|
@ -1,8 +1,8 @@
|
|||
<pda:PdaWindow xmlns="https://spacestation14.io"
|
||||
xmlns:pda="clr-namespace:Content.Client.PDA"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="576 450"
|
||||
SetSize="576 450">
|
||||
MinSize="576 750"
|
||||
SetSize="576 750"> <!-- Sunrise-Edit -->
|
||||
<!-- This: (Margin="1 1 3 0") is necessary so the navigation bar doesn't sticks into the black content border. -->
|
||||
<BoxContainer Name="NavigationBar" HorizontalExpand="True" MinHeight="32" Margin="1 1 3 0">
|
||||
<pda:PdaNavigationButton Name="HomeButton" SetWidth="32" CurrentTabBorderThickness="0 0 2 0" IsCurrent="True"/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<pda:PdaWindow xmlns="https://spacestation14.io"
|
||||
<pda:PdaWindow xmlns="https://spacestation14.io"
|
||||
xmlns:pda="clr-namespace:Content.Client.PDA"
|
||||
MouseFilter="Stop">
|
||||
<PanelContainer Name="Background" Access="Public" StyleClasses="PdaBackgroundRect" />
|
||||
<!-- The negative markin fixes a gap between the window edges and the decorative panel -->
|
||||
<PanelContainer Name="AccentH" Margin="-1 170 -2 170" Access="Public" StyleClasses="PdaBackground" />
|
||||
<!-- Sunrise-Start -->
|
||||
<PanelContainer Name="AccentH" Margin="-1 280 -2 280" Access="Public" StyleClasses="PdaBackground" />
|
||||
<!-- Sunrise-End -->
|
||||
<PanelContainer Name="AccentV" Margin="220 -1 220 -1" Access="Public" StyleClasses="PdaBackground" />
|
||||
<PanelContainer Name="Border" StyleClasses="PdaBorderRect" />
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ public partial class MapGridControl : LayoutContainer
|
|||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
MinSize = new Vector2(MathF.Round(SizeFull / 2f), MathF.Round(SizeFull / 2f)); // Sunrise-Edit
|
||||
MinSize = new Vector2(MathF.Round(SizeFull / 2f), SizeFull - 68); // Sunrise-Edit: Я не ебу как это сделать без хардкода
|
||||
RectClipContent = true;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
ActualRadarRange = WorldRange;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
MinSize="300,400">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="8">
|
||||
<Label
|
||||
Name="SearchLabel"
|
||||
Text="{Loc 'messenger-add-user-search'}"
|
||||
Margin="0,0,0,4" />
|
||||
<LineEdit
|
||||
Name="SearchInput"
|
||||
PlaceHolder="{Loc 'messenger-add-user-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
Margin="0,0,0,8" />
|
||||
<ScrollContainer
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Name="UsersScrollContainer">
|
||||
<BoxContainer
|
||||
Name="UsersList"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True" />
|
||||
</ScrollContainer>
|
||||
<Button
|
||||
Name="CloseButton"
|
||||
Text="{Loc 'messenger-add-user-cancel'}"
|
||||
HorizontalExpand="True"
|
||||
Margin="0,4,0,0" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using System.Linq;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AddUserDialog : DefaultWindow
|
||||
{
|
||||
public event Action<string>? OnUserSelected;
|
||||
public event Action? OnClose;
|
||||
|
||||
private List<MessengerUser> _availableUsers = new();
|
||||
|
||||
public AddUserDialog()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
Title = Loc.GetString("messenger-add-user-title");
|
||||
|
||||
SearchInput.OnTextChanged += _ => UpdateUsersList();
|
||||
CloseButton.OnPressed += _ =>
|
||||
{
|
||||
OnClose?.Invoke();
|
||||
Close();
|
||||
};
|
||||
}
|
||||
|
||||
public void SetTitle(string title)
|
||||
{
|
||||
Title = title;
|
||||
}
|
||||
|
||||
public void SetAvailableUsers(List<MessengerUser> users)
|
||||
{
|
||||
_availableUsers = users;
|
||||
UpdateUsersList();
|
||||
}
|
||||
|
||||
private void UpdateUsersList()
|
||||
{
|
||||
UsersList.RemoveAllChildren();
|
||||
var searchText = SearchInput.Text.ToLowerInvariant();
|
||||
|
||||
var filteredUsers = _availableUsers.Where(u =>
|
||||
string.IsNullOrEmpty(searchText) ||
|
||||
(u.Name?.ToLowerInvariant().Contains(searchText) ?? false))
|
||||
.ToList();
|
||||
|
||||
foreach (var user in filteredUsers)
|
||||
{
|
||||
var userButton = new Button
|
||||
{
|
||||
Text = user.Name,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(2)
|
||||
};
|
||||
|
||||
var userId = user.UserId;
|
||||
userButton.OnPressed += _ =>
|
||||
{
|
||||
OnUserSelected?.Invoke(userId);
|
||||
Close();
|
||||
};
|
||||
|
||||
UsersList.AddChild(userButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<DefaultWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'messenger-create-group-title'}"
|
||||
MinSize="300,150">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="8">
|
||||
<Label
|
||||
Text="{Loc 'messenger-create-group-label'}"
|
||||
Margin="0,0,0,4" />
|
||||
<LineEdit
|
||||
Name="NameInput"
|
||||
PlaceHolder="{Loc 'messenger-create-group-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
Margin="0,0,0,8" />
|
||||
<BoxContainer
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<Button
|
||||
Name="CreateButton"
|
||||
Text="{Loc 'messenger-create-group-button-create'}"
|
||||
HorizontalExpand="True"
|
||||
Margin="0,0,4,0" />
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
Text="{Loc 'messenger-create-group-button-cancel'}"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CreateGroupDialog : DefaultWindow
|
||||
{
|
||||
public event Action<string>? OnCreate;
|
||||
public event Action? OnCancel;
|
||||
|
||||
public CreateGroupDialog()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
CreateButton.OnPressed += _ =>
|
||||
{
|
||||
var groupName = NameInput.Text.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(groupName))
|
||||
{
|
||||
OnCreate?.Invoke(groupName);
|
||||
Close();
|
||||
}
|
||||
};
|
||||
|
||||
CancelButton.OnPressed += _ =>
|
||||
{
|
||||
OnCancel?.Invoke();
|
||||
Close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<PanelContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True"
|
||||
Margin="4,0"
|
||||
Name="MessagePanelContainer">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
MinSize="0,30"
|
||||
Name="MessageBox">
|
||||
<BoxContainer
|
||||
Orientation="Horizontal"
|
||||
Margin="0,0,0,4"
|
||||
Name="Header">
|
||||
<Label
|
||||
Name="SenderLabel"
|
||||
Text=""
|
||||
StyleClasses="Bold"
|
||||
HorizontalExpand="True"
|
||||
MaxWidth="240"
|
||||
HorizontalAlignment="Left"
|
||||
Align="Left"
|
||||
RectClipContent="True" />
|
||||
<BoxContainer
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalExpand="False"
|
||||
Name="RightContainer">
|
||||
<Label
|
||||
Name="TimeLabel"
|
||||
Text=""
|
||||
HorizontalAlignment="Right"
|
||||
StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<Control
|
||||
HorizontalExpand="True"
|
||||
MinSize="0,20"
|
||||
Name="ContentContainer">
|
||||
<RichTextLabel
|
||||
Name="ContentLabel"
|
||||
HorizontalExpand="True"
|
||||
MinSize="0,20" />
|
||||
<TextureRect
|
||||
Name="ReadStatusIcon"
|
||||
SetWidth="16"
|
||||
SetHeight="16"
|
||||
TextureScale="0.5,0.5"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,2,2"
|
||||
Visible="False" />
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Content.Client._Sunrise.Messenger;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MessagePanel : PanelContainer
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
private ClientEmojiSystem? EmojiSystem => _entitySystemManager.GetEntitySystemOrNull<ClientEmojiSystem>();
|
||||
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
private static readonly Type[] MessageTagsAllowed =
|
||||
[
|
||||
typeof(BoldItalicTag),
|
||||
typeof(BoldTag),
|
||||
typeof(BulletTag),
|
||||
typeof(ColorTag),
|
||||
typeof(HeadingTag),
|
||||
typeof(ItalicTag),
|
||||
typeof(EmojiTag),
|
||||
];
|
||||
|
||||
public MessagePanel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void UpdateMessage(MessengerMessage message, bool isOwnMessage, bool isPersonalChat, string? currentUserId)
|
||||
{
|
||||
SenderLabel.Text = message.SenderName;
|
||||
TimeLabel.Text = message.Timestamp.ToString(@"hh\:mm");
|
||||
|
||||
var parsedContent = EmojiSystem?.ParseEmojis(message.Content) ?? message.Content;
|
||||
ContentLabel.SetMessage(FormattedMessage.FromMarkupPermissive(parsedContent), MessageTagsAllowed);
|
||||
|
||||
var roundedButtonTex = _resourceCache.GetTexture("/Textures/Interface/Nano/rounded_button_bordered.svg.96dpi.png");
|
||||
var roundedStyleBox = new StyleBoxTexture
|
||||
{
|
||||
Texture = roundedButtonTex
|
||||
};
|
||||
roundedStyleBox.SetPatchMargin(StyleBox.Margin.All, 5);
|
||||
roundedStyleBox.SetPadding(StyleBox.Margin.All, 2);
|
||||
roundedStyleBox.SetContentMarginOverride(StyleBox.Margin.Left, 8);
|
||||
roundedStyleBox.SetContentMarginOverride(StyleBox.Margin.Right, 8);
|
||||
roundedStyleBox.SetContentMarginOverride(StyleBox.Margin.Top, 6);
|
||||
roundedStyleBox.SetContentMarginOverride(StyleBox.Margin.Bottom, 6);
|
||||
|
||||
if (isOwnMessage)
|
||||
{
|
||||
roundedStyleBox.Modulate = Color.FromHex("#2A4A5A");
|
||||
}
|
||||
else
|
||||
{
|
||||
roundedStyleBox.Modulate = Color.FromHex("#1E1E22");
|
||||
}
|
||||
|
||||
MessagePanelContainer.PanelOverride = roundedStyleBox;
|
||||
|
||||
if (isPersonalChat && isOwnMessage && message.RecipientId != null)
|
||||
{
|
||||
ReadStatusIcon.Visible = true;
|
||||
var readStatusTexturePath = message.IsRead
|
||||
? new ResPath("/Textures/Interface/Actions/eyeopen.png")
|
||||
: new ResPath("/Textures/Interface/Actions/eyeclose.png");
|
||||
ReadStatusIcon.Texture = _resourceCache.GetTexture(readStatusTexturePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadStatusIcon.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed partial class MessengerUi : UIFragment
|
||||
{
|
||||
private MessengerUiFragment? _fragment;
|
||||
|
||||
public override Control GetUIFragmentRoot()
|
||||
{
|
||||
return _fragment!;
|
||||
}
|
||||
|
||||
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
|
||||
{
|
||||
_fragment = new MessengerUiFragment();
|
||||
_fragment.OnSendMessage += (recipientId, groupId, content) =>
|
||||
SendMessengerMessage(MessengerUiAction.SendMessage, userInterface, recipientId: recipientId, groupId: groupId, content: content);
|
||||
_fragment.OnCreateGroup += (groupName) =>
|
||||
SendMessengerMessage(MessengerUiAction.CreateGroup, userInterface, groupName: groupName);
|
||||
_fragment.OnAddToGroup += (groupId, userId) =>
|
||||
SendMessengerMessage(MessengerUiAction.AddToGroup, userInterface, groupId: groupId, userId: userId);
|
||||
_fragment.OnRemoveFromGroup += (groupId, userId) =>
|
||||
SendMessengerMessage(MessengerUiAction.RemoveFromGroup, userInterface, groupId: groupId, userId: userId);
|
||||
_fragment.OnRequestMessages += (chatId) =>
|
||||
SendMessengerMessage(MessengerUiAction.RequestMessages, userInterface, chatId: chatId);
|
||||
_fragment.OnToggleMute += (chatId, isMuted) =>
|
||||
SendMessengerMessage(MessengerUiAction.ToggleMute, userInterface, chatId: chatId, isMuted: isMuted);
|
||||
}
|
||||
|
||||
public override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
if (state is not MessengerUiState messengerState)
|
||||
return;
|
||||
|
||||
_fragment?.UpdateState(messengerState);
|
||||
}
|
||||
|
||||
private void SendMessengerMessage(
|
||||
MessengerUiAction action,
|
||||
BoundUserInterface userInterface,
|
||||
string? recipientId = null,
|
||||
string? groupId = null,
|
||||
string? content = null,
|
||||
string? groupName = null,
|
||||
string? userId = null,
|
||||
string? chatId = null,
|
||||
bool? isMuted = null)
|
||||
{
|
||||
var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted);
|
||||
var message = new CartridgeUiMessage(messengerMessage);
|
||||
userInterface.SendMessage(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<cartridges1:MessengerUiFragment
|
||||
Margin="1,0,2,0"
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cartridges1="clr-namespace:Content.Client._Sunrise.CartridgeLoader.Cartridges">
|
||||
<PanelContainer StyleClasses="BackgroundDark" />
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Horizontal"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
SetWidth="200"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Horizontal"
|
||||
Margin="4,0"
|
||||
SetHeight="26">
|
||||
<Label
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalExpand="True"
|
||||
Name="StatusLabel"
|
||||
Text="{Loc 'messenger-status-connecting'}" />
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 2" />
|
||||
<LineEdit
|
||||
HorizontalExpand="True"
|
||||
Margin="4"
|
||||
Name="SearchInput"
|
||||
PlaceHolder="{Loc 'messenger-search-placeholder'}" />
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Horizontal"
|
||||
Margin="4,2">
|
||||
<Button
|
||||
Name="PersonalChatsTab"
|
||||
Text="{Loc 'messenger-tab-personal'}"
|
||||
HorizontalExpand="True"
|
||||
ToggleMode="True" />
|
||||
<Button
|
||||
Name="GroupChatsTab"
|
||||
Text="{Loc 'messenger-tab-groups'}"
|
||||
HorizontalExpand="True"
|
||||
ToggleMode="True" />
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 2" />
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Name="ChatsContainer"
|
||||
Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 2" />
|
||||
<Button
|
||||
Disabled="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="4,0,4,4"
|
||||
Name="CreateGroupButton"
|
||||
Text="{Loc 'messenger-create-group-button'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" SetWidth="2" VerticalExpand="True" />
|
||||
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Horizontal"
|
||||
Margin="4,0"
|
||||
SetHeight="26">
|
||||
<Label
|
||||
HorizontalExpand="True"
|
||||
Name="ChatNameLabel"
|
||||
Text="{Loc 'messenger-chat-select'}" />
|
||||
<CheckBox
|
||||
Name="MuteCheckBox"
|
||||
Text="🔇"
|
||||
ToolTip="{Loc 'messenger-mute-tooltip'}"
|
||||
Visible="False"
|
||||
Margin="0,0,4,0" />
|
||||
<Button
|
||||
Name="ToggleMembersButton"
|
||||
Text="≡"
|
||||
SetWidth="30"
|
||||
ToolTip="{Loc 'messenger-members-toggle-show'}"
|
||||
Visible="False" />
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 2" />
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Orientation="Horizontal"
|
||||
VerticalExpand="True">
|
||||
<ScrollContainer
|
||||
HorizontalExpand="True"
|
||||
Name="MessagesContainer"
|
||||
VerticalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Name="MessagesList"
|
||||
Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" SetWidth="2" VerticalExpand="True" />
|
||||
<ScrollContainer
|
||||
HorizontalExpand="True"
|
||||
Name="MembersContainer"
|
||||
VerticalExpand="True"
|
||||
Visible="False">
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Name="MembersList"
|
||||
Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 2" />
|
||||
<BoxContainer
|
||||
HorizontalExpand="True"
|
||||
Margin="4,0,4,4"
|
||||
Name="InputContainer"
|
||||
Orientation="Horizontal"
|
||||
Visible="False">
|
||||
<LineEdit
|
||||
Editable="False"
|
||||
HorizontalExpand="True"
|
||||
Name="MessageInput"
|
||||
PlaceHolder="{Loc 'messenger-message-placeholder'}" />
|
||||
<Button
|
||||
Name="EmojiButton"
|
||||
Text="☻"
|
||||
SetWidth="30"
|
||||
Margin="4,0,0,0"
|
||||
ToolTip="{Loc 'messenger-emoji-button-tooltip'}" />
|
||||
<Button
|
||||
Disabled="True"
|
||||
Margin="4,0,0,0"
|
||||
Name="SendButton"
|
||||
Text="➤"
|
||||
SetWidth="30"
|
||||
ToolTip="{Loc 'messenger-send-button'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</cartridges1:MessengerUiFragment>
|
||||
8
Content.Client/_Sunrise/Messenger/EmojiSystem.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Content.Client._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Клиентская реализация системы эмодзи
|
||||
/// </summary>
|
||||
public sealed class ClientEmojiSystem : Shared._Sunrise.Messenger.EmojiSystem
|
||||
{
|
||||
}
|
||||
83
Content.Client/_Sunrise/Messenger/EmojiTag.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Тег для отображения эмодзи мессенджера в RichText.
|
||||
/// Разрешает только эмодзи из прототипов, чтобы игроки не могли использовать произвольные текстуры.
|
||||
/// </summary>
|
||||
public sealed class EmojiTag : IMarkupTagHandler
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
private static SpriteSystem? _spriteSystem;
|
||||
|
||||
public string Name => "emoji";
|
||||
|
||||
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
|
||||
{
|
||||
control = null;
|
||||
|
||||
if (!node.Attributes.TryGetValue("id", out var rawId) || !rawId.TryGetString(out var emojiId))
|
||||
return false;
|
||||
|
||||
if (!_prototypeManager.TryIndex<EmojiPrototype>(emojiId, out var emoji))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var spriteSpec = new SpriteSpecifier.Rsi(new ResPath(emoji.SpritePath), emoji.SpriteState);
|
||||
|
||||
_spriteSystem ??= _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
var state = _spriteSystem.RsiStateLike(spriteSpec);
|
||||
|
||||
if (state.IsAnimated)
|
||||
{
|
||||
var animatedRect = new AnimatedTextureRect
|
||||
{
|
||||
MinWidth = 50,
|
||||
MinHeight = 50,
|
||||
HorizontalAlignment = Control.HAlignment.Stretch,
|
||||
VerticalAlignment = Control.VAlignment.Stretch,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
};
|
||||
animatedRect.SetFromSpriteSpecifier(spriteSpec);
|
||||
animatedRect.DisplayRect.HorizontalExpand = true;
|
||||
animatedRect.DisplayRect.VerticalExpand = true;
|
||||
animatedRect.DisplayRect.Stretch = TextureRect.StretchMode.KeepAspectCentered;
|
||||
control = animatedRect;
|
||||
}
|
||||
else
|
||||
{
|
||||
var texture = _spriteSystem.Frame0(spriteSpec);
|
||||
var textureRect = new TextureRect
|
||||
{
|
||||
Texture = texture,
|
||||
MinWidth = 50,
|
||||
MinHeight = 50,
|
||||
HorizontalAlignment = Control.HAlignment.Stretch,
|
||||
VerticalAlignment = Control.VAlignment.Stretch,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
};
|
||||
control = textureRect;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
using Content.Shared._Sunrise.Messenger;
|
||||
|
||||
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Компонент картриджа мессенджера для КПК
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MessengerCartridgeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Адрес сервера мессенджера, к которому подключен этот КПК
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? ServerAddress;
|
||||
|
||||
/// <summary>
|
||||
/// ID текущего пользователя в системе мессенджера
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? UserId;
|
||||
|
||||
/// <summary>
|
||||
/// Зарегистрирован ли пользователь на сервере
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool IsRegistered = false;
|
||||
|
||||
/// <summary>
|
||||
/// Время последней попытки регистрации
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan? LastRegistrationAttempt;
|
||||
|
||||
/// <summary>
|
||||
/// UID загрузчика картриджей (КПК)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? LoaderUid;
|
||||
|
||||
/// <summary>
|
||||
/// Список пользователей
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public List<MessengerUser> Users = new();
|
||||
|
||||
/// <summary>
|
||||
/// Список групп
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public List<MessengerGroup> Groups = new();
|
||||
|
||||
/// <summary>
|
||||
/// История сообщений по чатам
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Dictionary<string, List<MessengerMessage>> MessageHistory = new();
|
||||
|
||||
/// <summary>
|
||||
/// Сообщения для текущего чата
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public List<MessengerMessage> Messages = new();
|
||||
|
||||
/// <summary>
|
||||
/// Время последней проверки статуса сервера
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan? LastStatusCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Последний запрошенный chatId для истории сообщений
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? LastRequestedChatId;
|
||||
|
||||
/// <summary>
|
||||
/// Заглушенные личные чаты (chatId)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HashSet<string> MutedPersonalChats = new();
|
||||
|
||||
/// <summary>
|
||||
/// Заглушенные групповые чаты (groupId)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HashSet<string> MutedGroupChats = new();
|
||||
|
||||
/// <summary>
|
||||
/// Время последнего обновления списка пользователей
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public TimeSpan? LastUsersUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Количество непрочитанных сообщений с сервера (chatId -> количество)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Dictionary<string, int> ServerUnreadCounts = new();
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
|
||||
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы картриджа мессенджера, отвечающая за подключение к серверу
|
||||
/// </summary>
|
||||
public sealed partial class MessengerCartridgeSystem
|
||||
{
|
||||
private void CheckServerStatus(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid)
|
||||
{
|
||||
if (!TryGetPdaAndDeviceNetwork(loaderUid, out var pdaUid, out _))
|
||||
return;
|
||||
|
||||
var station = _stationSystem.GetOwningStation(pdaUid);
|
||||
if (station == null)
|
||||
{
|
||||
component.ServerAddress = null;
|
||||
component.IsRegistered = false;
|
||||
component.LastRegistrationAttempt = null;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_singletonServer.TryGetActiveServerAddress<MessengerServerComponent>(station.Value, out var serverAddress))
|
||||
{
|
||||
component.ServerAddress = null;
|
||||
component.IsRegistered = false;
|
||||
component.LastRegistrationAttempt = null;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.ServerAddress != serverAddress)
|
||||
{
|
||||
component.ServerAddress = serverAddress;
|
||||
component.IsRegistered = false;
|
||||
component.UserId = null;
|
||||
component.LastRegistrationAttempt = null;
|
||||
if (TryGetPdaAndDeviceNetwork(loaderUid, out _, out var deviceNetwork))
|
||||
{
|
||||
TryConnectToServer(uid, component, loaderUid);
|
||||
}
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
else if (component.ServerAddress == null)
|
||||
{
|
||||
component.IsRegistered = false;
|
||||
component.UserId = null;
|
||||
component.LastRegistrationAttempt = null;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
else if (!component.IsRegistered)
|
||||
{
|
||||
if (TryGetPdaAndDeviceNetwork(loaderUid, out _, out var deviceNetwork))
|
||||
{
|
||||
TryConnectToServer(uid, component, loaderUid);
|
||||
}
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
if (!component.LastUsersUpdate.HasValue ||
|
||||
(currentTime - component.LastUsersUpdate.Value).TotalSeconds >= 10.0)
|
||||
{
|
||||
component.LastUsersUpdate = currentTime;
|
||||
if (TryGetPdaAndDeviceNetwork(loaderUid, out _, out var deviceNetwork))
|
||||
{
|
||||
RequestUsers(uid, component, loaderUid, deviceNetwork);
|
||||
RequestGroups(uid, component, loaderUid, deviceNetwork);
|
||||
}
|
||||
}
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCartridgeActivated(EntityUid uid, MessengerCartridgeComponent component, CartridgeActivatedEvent args)
|
||||
{
|
||||
TryConnectToServer(uid, component, args.Loader);
|
||||
}
|
||||
|
||||
private void OnCartridgeAdded(EntityUid uid, MessengerCartridgeComponent component, CartridgeAddedEvent args)
|
||||
{
|
||||
component.LoaderUid = args.Loader;
|
||||
TryConnectToServer(uid, component, args.Loader);
|
||||
|
||||
_cartridgeLoader.RegisterBackgroundProgram(args.Loader, uid);
|
||||
}
|
||||
|
||||
private void OnUiReady(EntityUid uid, MessengerCartridgeComponent component, CartridgeUiReadyEvent args)
|
||||
{
|
||||
if (!component.IsRegistered && component.ServerAddress == null)
|
||||
{
|
||||
TryConnectToServer(uid, component, args.Loader);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUiState(uid, args.Loader, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryConnectToServer(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid)
|
||||
{
|
||||
if (!TryGetPdaAndDeviceNetwork(loaderUid, out var pdaUid, out var deviceNetwork))
|
||||
{
|
||||
Sawmill.Warning($"Failed to get PDA and DeviceNetwork: {ToPrettyString(loaderUid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
component.LoaderUid = loaderUid;
|
||||
|
||||
var station = _stationSystem.GetOwningStation(pdaUid);
|
||||
if (station == null)
|
||||
{
|
||||
Sawmill.Warning($"No station found for PDA: {ToPrettyString(pdaUid)}");
|
||||
component.ServerAddress = null;
|
||||
component.IsRegistered = false;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_singletonServer.TryGetActiveServerAddress<MessengerServerComponent>(station.Value, out var serverAddress))
|
||||
{
|
||||
Sawmill.Warning($"No active messenger server found on station: {ToPrettyString(station.Value)}");
|
||||
component.ServerAddress = null;
|
||||
component.IsRegistered = false;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(serverAddress))
|
||||
{
|
||||
Sawmill.Warning($"Server address is empty, server may not be connected to DeviceNetwork yet");
|
||||
component.ServerAddress = null;
|
||||
component.IsRegistered = false;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
Sawmill.Debug($"Found active server address: {serverAddress}");
|
||||
|
||||
if (component.ServerAddress != serverAddress)
|
||||
{
|
||||
component.ServerAddress = serverAddress;
|
||||
component.IsRegistered = false;
|
||||
component.UserId = null;
|
||||
component.LastRegistrationAttempt = null;
|
||||
}
|
||||
|
||||
if (component.IsRegistered && component.ServerAddress == serverAddress)
|
||||
{
|
||||
Sawmill.Debug($"Already registered, updating UI state");
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!component.IsRegistered)
|
||||
{
|
||||
RegisterUser(uid, component, loaderUid, deviceNetwork, pdaUid);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterUser(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, EntityUid pdaUid)
|
||||
{
|
||||
if (component.ServerAddress == null)
|
||||
{
|
||||
Sawmill.Warning($"Cannot register: ServerAddress is null");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
if (component.LastRegistrationAttempt.HasValue)
|
||||
{
|
||||
var timeSinceLastAttempt = currentTime - component.LastRegistrationAttempt.Value;
|
||||
if (timeSinceLastAttempt.TotalSeconds < 5.0)
|
||||
{
|
||||
Sawmill.Debug($"Registration attempt too soon, waiting: {timeSinceLastAttempt.TotalSeconds:F2}s");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
component.LastRegistrationAttempt = currentTime;
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdRegisterUser,
|
||||
[MessengerCommands.CmdRegisterUser] = new NetworkPayload
|
||||
{
|
||||
["pda_uid"] = GetNetEntity(pdaUid)
|
||||
}
|
||||
};
|
||||
|
||||
uint? messengerFrequency = GetMessengerFrequency();
|
||||
if (!messengerFrequency.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(deviceNetwork.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
Sawmill.Warning($"Server address {component.ServerAddress} is not present in network {deviceNetwork.DeviceNetId}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
Sawmill.Error($"Failed to get DeviceNetworkComponent after setting frequency");
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
Sawmill.Debug($"PDA DeviceNetwork: Address={pdaDevice.Address}, TransmitFrequency={pdaDevice.TransmitFrequency}, ReceiveFrequency={pdaDevice.ReceiveFrequency}, DeviceNetId={pdaDevice.DeviceNetId}");
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
Sawmill.Warning($"Server address {component.ServerAddress} is not present in network {pdaDevice.DeviceNetId} (PDA network)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Debug($"Server address {component.ServerAddress} found in network {pdaDevice.DeviceNetId}");
|
||||
}
|
||||
|
||||
var pdaTransform = Transform(loaderUid);
|
||||
var pdaPos = _transformSystem.GetWorldPosition(pdaTransform);
|
||||
Sawmill.Debug($"PDA position: {pdaPos}, MapId: {pdaTransform.MapID}");
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFrequency, network: pdaDevice.DeviceNetId);
|
||||
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
|
||||
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы картриджа мессенджера, отвечающая за отправку сообщений и запросы
|
||||
/// </summary>
|
||||
public sealed partial class MessengerCartridgeSystem
|
||||
{
|
||||
private void OnUiMessage(EntityUid uid, MessengerCartridgeComponent component, CartridgeMessageEvent args)
|
||||
{
|
||||
if (args is not MessengerUiMessageEvent message)
|
||||
return;
|
||||
|
||||
var loaderUid = GetEntity(args.LoaderUid);
|
||||
if (!TryGetPdaAndDeviceNetwork(loaderUid, out var pdaUid, out var deviceNetwork))
|
||||
return;
|
||||
|
||||
switch (message.Action)
|
||||
{
|
||||
case MessengerUiAction.SendMessage:
|
||||
if (message.Content != null)
|
||||
SendMessage(uid, component, loaderUid, deviceNetwork, message.RecipientId, message.GroupId, message.Content);
|
||||
break;
|
||||
case MessengerUiAction.CreateGroup:
|
||||
if (message.GroupName != null)
|
||||
CreateGroup(uid, component, loaderUid, deviceNetwork, message.GroupName);
|
||||
break;
|
||||
case MessengerUiAction.AddToGroup:
|
||||
if (message.GroupId != null && message.UserId != null)
|
||||
AddToGroup(uid, component, loaderUid, deviceNetwork, message.GroupId, message.UserId);
|
||||
break;
|
||||
case MessengerUiAction.RemoveFromGroup:
|
||||
if (message.GroupId != null && message.UserId != null)
|
||||
RemoveFromGroup(uid, component, loaderUid, deviceNetwork, message.GroupId, message.UserId);
|
||||
break;
|
||||
case MessengerUiAction.RequestUsers:
|
||||
RequestUsers(uid, component, loaderUid, deviceNetwork);
|
||||
break;
|
||||
case MessengerUiAction.RequestGroups:
|
||||
RequestGroups(uid, component, loaderUid, deviceNetwork);
|
||||
break;
|
||||
case MessengerUiAction.RequestMessages:
|
||||
if (message.ChatId != null)
|
||||
RequestMessages(uid, component, loaderUid, deviceNetwork, message.ChatId);
|
||||
break;
|
||||
case MessengerUiAction.ToggleMute:
|
||||
if (message.ChatId != null && message.IsMuted.HasValue)
|
||||
ToggleMute(uid, component, message.ChatId, message.IsMuted.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SendMessage(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string? recipientId, string? groupId, string content)
|
||||
{
|
||||
if (component.ServerAddress == null || !component.IsRegistered)
|
||||
return;
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdSendMessage,
|
||||
[MessengerCommands.CmdSendMessage] = new NetworkPayload
|
||||
{
|
||||
["content"] = content,
|
||||
["recipient_id"] = recipientId ?? string.Empty,
|
||||
["group_id"] = groupId ?? string.Empty
|
||||
}
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
|
||||
private void CreateGroup(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string groupName)
|
||||
{
|
||||
if (component.ServerAddress == null || !component.IsRegistered)
|
||||
return;
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdCreateGroup,
|
||||
[MessengerCommands.CmdCreateGroup] = new NetworkPayload
|
||||
{
|
||||
["name"] = groupName
|
||||
}
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
|
||||
private void AddToGroup(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string groupId, string userId)
|
||||
{
|
||||
if (component.ServerAddress == null || !component.IsRegistered)
|
||||
return;
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdAddToGroup,
|
||||
[MessengerCommands.CmdAddToGroup] = new NetworkPayload
|
||||
{
|
||||
["group_id"] = groupId,
|
||||
["user_id"] = userId
|
||||
}
|
||||
};
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
|
||||
private void RemoveFromGroup(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string groupId, string userId)
|
||||
{
|
||||
if (component.ServerAddress == null || !component.IsRegistered)
|
||||
return;
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdRemoveFromGroup,
|
||||
[MessengerCommands.CmdRemoveFromGroup] = new NetworkPayload
|
||||
{
|
||||
["group_id"] = groupId,
|
||||
["user_id"] = userId
|
||||
}
|
||||
};
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
|
||||
private void RequestUsers(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork)
|
||||
{
|
||||
if (component.ServerAddress == null)
|
||||
return;
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdGetUsers
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
|
||||
private void RequestGroups(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork)
|
||||
{
|
||||
if (component.ServerAddress == null)
|
||||
return;
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdGetGroups
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
|
||||
private void RequestMessages(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string chatId)
|
||||
{
|
||||
if (component.ServerAddress == null)
|
||||
return;
|
||||
|
||||
component.LastRequestedChatId = chatId;
|
||||
|
||||
component.ServerUnreadCounts.Remove(chatId);
|
||||
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (!messengerFreq.HasValue)
|
||||
return;
|
||||
|
||||
SetMessengerFrequency(loaderUid, deviceNetwork, out var originalFreq);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var pdaDevice))
|
||||
{
|
||||
RestoreFrequency(loaderUid, deviceNetwork, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(pdaDevice.DeviceNetId, component.ServerAddress))
|
||||
{
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdGetMessages,
|
||||
[MessengerCommands.CmdGetMessages] = new NetworkPayload
|
||||
{
|
||||
["chat_id"] = chatId
|
||||
}
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
RestoreFrequency(loaderUid, pdaDevice, originalFreq);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,478 @@
|
|||
using System.Linq;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Server.CartridgeLoader;
|
||||
using Content.Server.PDA.Ringer;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.PDA.Ringer;
|
||||
|
||||
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы картриджа мессенджера, отвечающая за обработку входящих пакетов
|
||||
/// </summary>
|
||||
public sealed partial class MessengerCartridgeSystem
|
||||
{
|
||||
private void OnPacketReceived(EntityUid uid, MessengerCartridgeComponent component, CartridgeDeviceNetPacketEvent args)
|
||||
{
|
||||
var packet = args.PacketEvent;
|
||||
|
||||
if (!packet.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var loaderUid = args.Loader;
|
||||
if (loaderUid == EntityUid.Invalid)
|
||||
{
|
||||
Sawmill.Warning($"Packet received but LoaderUid is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case MessengerCommands.CmdUserRegistered:
|
||||
HandleUserRegistered(uid, component, packet, loaderUid);
|
||||
break;
|
||||
case MessengerCommands.CmdUsersList:
|
||||
HandleUsersList(uid, component, packet, loaderUid);
|
||||
break;
|
||||
case MessengerCommands.CmdGroupsList:
|
||||
HandleGroupsList(uid, component, packet, loaderUid);
|
||||
break;
|
||||
case MessengerCommands.CmdMessagesList:
|
||||
HandleMessagesList(uid, component, packet, loaderUid);
|
||||
break;
|
||||
case MessengerCommands.CmdMessageReceived:
|
||||
HandleMessageReceived(uid, component, packet, loaderUid);
|
||||
break;
|
||||
case MessengerCommands.CmdGroupCreated:
|
||||
HandleGroupCreated(uid, component, packet, loaderUid);
|
||||
break;
|
||||
case MessengerCommands.CmdUserAddedToGroup:
|
||||
HandleUserAddedToGroup(uid, component, packet, loaderUid);
|
||||
break;
|
||||
default:
|
||||
Sawmill.Warning($"Unknown command received: {command}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUserRegistered(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
|
||||
{
|
||||
if (!packet.Data.TryGetValue("user_id", out string? userId))
|
||||
{
|
||||
Sawmill.Warning($"UserRegistered packet missing user_id");
|
||||
return;
|
||||
}
|
||||
|
||||
Sawmill.Info($"User registered successfully: {userId}");
|
||||
component.UserId = userId;
|
||||
component.IsRegistered = true;
|
||||
component.LastRegistrationAttempt = null;
|
||||
|
||||
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
|
||||
{
|
||||
RequestUsers(uid, component, loaderUid, deviceNetwork);
|
||||
RequestGroups(uid, component, loaderUid, deviceNetwork);
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
|
||||
private void HandleUsersList(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent args, EntityUid loaderUid)
|
||||
{
|
||||
if (!args.Data.TryGetValue("users", out List<Dictionary<string, object>>? usersData))
|
||||
return;
|
||||
|
||||
var users = new List<MessengerUser>();
|
||||
foreach (var userData in usersData)
|
||||
{
|
||||
if (!userData.TryGetValue("user_id", out object? userIdObj) ||
|
||||
!userData.TryGetValue("user_name", out object? userNameObj))
|
||||
continue;
|
||||
|
||||
var userId = userIdObj?.ToString();
|
||||
var userName = userNameObj?.ToString();
|
||||
|
||||
if (userId == null || userName == null)
|
||||
continue;
|
||||
|
||||
userData.TryGetValue("job_title", out object? jobTitleObj);
|
||||
userData.TryGetValue("department_id", out object? departmentIdObj);
|
||||
|
||||
var jobTitle = jobTitleObj?.ToString();
|
||||
var departmentId = departmentIdObj?.ToString();
|
||||
|
||||
users.Add(new MessengerUser(userId, userName, jobTitle, departmentId));
|
||||
}
|
||||
|
||||
component.Users = users;
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
|
||||
private void HandleGroupsList(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
|
||||
{
|
||||
if (!packet.Data.TryGetValue("groups", out List<Dictionary<string, object>>? groupsData))
|
||||
return;
|
||||
|
||||
Dictionary<string, int>? serverUnreadCounts = null;
|
||||
if (packet.Data.TryGetValue("unread_counts", out object? unreadCountsObj))
|
||||
{
|
||||
if (unreadCountsObj is Dictionary<string, object> unreadCountsDict)
|
||||
{
|
||||
serverUnreadCounts = new Dictionary<string, int>();
|
||||
foreach (var (chatId, countObj) in unreadCountsDict)
|
||||
{
|
||||
if (int.TryParse(countObj?.ToString(), out var count))
|
||||
{
|
||||
serverUnreadCounts[chatId] = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groups = new List<MessengerGroup>();
|
||||
foreach (var groupData in groupsData)
|
||||
{
|
||||
if (!groupData.TryGetValue("group_id", out object? groupIdObj) ||
|
||||
!groupData.TryGetValue("group_name", out object? groupNameObj))
|
||||
continue;
|
||||
|
||||
var groupId = groupIdObj?.ToString();
|
||||
var groupName = groupNameObj?.ToString();
|
||||
|
||||
if (groupId == null || groupName == null)
|
||||
continue;
|
||||
|
||||
List<string>? membersList = null;
|
||||
if (groupData.TryGetValue("members", out object? membersObj))
|
||||
{
|
||||
if (membersObj is List<object> membersObjList)
|
||||
{
|
||||
membersList = membersObjList.Select(m => m?.ToString() ?? string.Empty).Where(m => !string.IsNullOrEmpty(m)).ToList();
|
||||
}
|
||||
else if (membersObj is List<string> membersStringList)
|
||||
{
|
||||
membersList = membersStringList;
|
||||
}
|
||||
else if (membersObj is IEnumerable<object> membersEnumerable)
|
||||
{
|
||||
membersList = membersEnumerable.Select(m => m?.ToString() ?? string.Empty).Where(m => !string.IsNullOrEmpty(m)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
groupData.TryGetValue("group_type", out object? groupTypeObj);
|
||||
groupData.TryGetValue("auto_group_prototype_id", out object? autoGroupPrototypeIdObj);
|
||||
groupData.TryGetValue("owner_id", out object? ownerIdObj);
|
||||
|
||||
var groupType = MessengerGroupType.UserCreated;
|
||||
if (groupTypeObj != null && int.TryParse(groupTypeObj.ToString(), out var typeInt))
|
||||
{
|
||||
groupType = (MessengerGroupType)typeInt;
|
||||
}
|
||||
|
||||
var autoGroupPrototypeId = autoGroupPrototypeIdObj?.ToString();
|
||||
var ownerId = ownerIdObj?.ToString();
|
||||
|
||||
groups.Add(new MessengerGroup(groupId, groupName, new HashSet<string>(membersList ?? new List<string>()), groupType, autoGroupPrototypeId, ownerId));
|
||||
}
|
||||
|
||||
component.Groups = groups;
|
||||
|
||||
if (serverUnreadCounts != null)
|
||||
{
|
||||
foreach (var (chatId, count) in serverUnreadCounts)
|
||||
{
|
||||
component.ServerUnreadCounts[chatId] = count;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
|
||||
private void HandleMessagesList(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
|
||||
{
|
||||
if (!packet.Data.TryGetValue("messages", out List<Dictionary<string, object>>? messagesData))
|
||||
return;
|
||||
|
||||
var isStatusUpdate = packet.Data.TryGetValue("chat_id", out object? updateChatIdObj);
|
||||
var updateChatId = updateChatIdObj?.ToString();
|
||||
|
||||
var chatId = isStatusUpdate ? updateChatId : component.LastRequestedChatId;
|
||||
if (string.IsNullOrEmpty(chatId))
|
||||
{
|
||||
if (messagesData.Count > 0)
|
||||
{
|
||||
var firstMessage = messagesData[0];
|
||||
firstMessage.TryGetValue("group_id", out object? groupIdObj);
|
||||
firstMessage.TryGetValue("recipient_id", out object? recipientIdObj);
|
||||
|
||||
var groupId = groupIdObj?.ToString();
|
||||
var recipientId = recipientIdObj?.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(groupId))
|
||||
{
|
||||
chatId = groupId;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(recipientId) && !string.IsNullOrEmpty(component.UserId))
|
||||
{
|
||||
var ids = new[] { recipientId, component.UserId }.OrderBy(x => x).ToArray();
|
||||
chatId = $"personal_{ids[0]}_{ids[1]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var messages = new List<MessengerMessage>();
|
||||
foreach (var messageData in messagesData)
|
||||
{
|
||||
if (!messageData.TryGetValue("sender_id", out object? senderIdObj) ||
|
||||
!messageData.TryGetValue("sender_name", out object? senderNameObj) ||
|
||||
!messageData.TryGetValue("content", out object? contentObj) ||
|
||||
!messageData.TryGetValue("timestamp", out object? timestampObj))
|
||||
continue;
|
||||
|
||||
var senderId = senderIdObj?.ToString();
|
||||
var senderName = senderNameObj?.ToString();
|
||||
var content = contentObj?.ToString();
|
||||
|
||||
if (senderId == null || senderName == null || content == null)
|
||||
continue;
|
||||
|
||||
if (!double.TryParse(timestampObj?.ToString(), out var timestampSeconds))
|
||||
continue;
|
||||
|
||||
messageData.TryGetValue("group_id", out object? groupIdObj);
|
||||
messageData.TryGetValue("recipient_id", out object? recipientIdObj);
|
||||
messageData.TryGetValue("is_read", out object? isReadObj);
|
||||
|
||||
var groupId = groupIdObj?.ToString();
|
||||
var recipientId = recipientIdObj?.ToString();
|
||||
|
||||
var isRead = false;
|
||||
if (isReadObj != null && bool.TryParse(isReadObj.ToString(), out var isReadValue))
|
||||
{
|
||||
isRead = isReadValue;
|
||||
}
|
||||
|
||||
var timestamp = TimeSpan.FromSeconds(timestampSeconds);
|
||||
messages.Add(new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(chatId) && messages.Count >= 0)
|
||||
{
|
||||
if (component.MessageHistory.TryGetValue(chatId, out var existingMessages))
|
||||
{
|
||||
var existingDict = new Dictionary<string, MessengerMessage>();
|
||||
for (int i = 0; i < existingMessages.Count; i++)
|
||||
{
|
||||
var msg = existingMessages[i];
|
||||
var key = $"{msg.SenderId}_{msg.Timestamp.TotalSeconds}_{msg.Content}_{i}";
|
||||
|
||||
if (existingDict.ContainsKey(key))
|
||||
{
|
||||
key = $"{msg.SenderId}_{msg.Timestamp.TotalSeconds}_{msg.Content.GetHashCode()}_{i}";
|
||||
}
|
||||
existingDict[key] = msg;
|
||||
}
|
||||
|
||||
var hasStatusUpdate = false;
|
||||
foreach (var newMessage in messages)
|
||||
{
|
||||
var matchingMessage = existingMessages.FirstOrDefault(m =>
|
||||
m.SenderId == newMessage.SenderId &&
|
||||
Math.Abs(m.Timestamp.TotalSeconds - newMessage.Timestamp.TotalSeconds) < 0.001 &&
|
||||
m.Content == newMessage.Content);
|
||||
|
||||
if (matchingMessage != null)
|
||||
{
|
||||
if (matchingMessage.IsRead != newMessage.IsRead)
|
||||
{
|
||||
matchingMessage.IsRead = newMessage.IsRead;
|
||||
hasStatusUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newMessages = messages.Where(newMsg =>
|
||||
{
|
||||
return !existingMessages.Any(existingMsg =>
|
||||
existingMsg.SenderId == newMsg.SenderId &&
|
||||
Math.Abs(existingMsg.Timestamp.TotalSeconds - newMsg.Timestamp.TotalSeconds) < 0.001 &&
|
||||
existingMsg.Content == newMsg.Content);
|
||||
}).ToList();
|
||||
|
||||
if (newMessages.Count > 0)
|
||||
{
|
||||
existingMessages.AddRange(newMessages);
|
||||
existingMessages = existingMessages.OrderBy(m => m.Timestamp)
|
||||
.ThenBy(m => m.SenderId)
|
||||
.ThenBy(m => m.Content)
|
||||
.ToList();
|
||||
}
|
||||
component.MessageHistory[chatId] = existingMessages;
|
||||
}
|
||||
else
|
||||
component.MessageHistory[chatId] = messages.OrderBy(m => m.Timestamp).ToList();
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
}
|
||||
|
||||
private void HandleMessageReceived(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
|
||||
{
|
||||
if (!packet.Data.TryGetValue("sender_id", out object? senderIdObj) ||
|
||||
!packet.Data.TryGetValue("sender_name", out object? senderNameObj) ||
|
||||
!packet.Data.TryGetValue("content", out object? contentObj) ||
|
||||
!packet.Data.TryGetValue("timestamp", out object? timestampObj))
|
||||
return;
|
||||
|
||||
var senderId = senderIdObj?.ToString();
|
||||
var senderName = senderNameObj?.ToString();
|
||||
var content = contentObj?.ToString();
|
||||
|
||||
if (senderId == null || senderName == null || content == null)
|
||||
return;
|
||||
|
||||
if (!double.TryParse(timestampObj?.ToString(), out var timestampSeconds))
|
||||
return;
|
||||
|
||||
packet.Data.TryGetValue("group_id", out object? groupIdObj);
|
||||
packet.Data.TryGetValue("recipient_id", out object? recipientIdObj);
|
||||
packet.Data.TryGetValue("is_read", out object? isReadObj);
|
||||
|
||||
var groupId = groupIdObj?.ToString();
|
||||
var recipientId = recipientIdObj?.ToString();
|
||||
|
||||
var isRead = false;
|
||||
if (isReadObj != null && bool.TryParse(isReadObj.ToString(), out var isReadValue))
|
||||
{
|
||||
isRead = isReadValue;
|
||||
}
|
||||
|
||||
var timestamp = TimeSpan.FromSeconds(timestampSeconds);
|
||||
var message = new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead);
|
||||
|
||||
string chatId;
|
||||
if (!string.IsNullOrEmpty(groupId))
|
||||
{
|
||||
chatId = groupId;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(senderId) && !string.IsNullOrEmpty(component.UserId))
|
||||
{
|
||||
string otherUserId;
|
||||
if (component.UserId == senderId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(recipientId))
|
||||
{
|
||||
otherUserId = senderId;
|
||||
}
|
||||
else
|
||||
{
|
||||
otherUserId = recipientId;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
otherUserId = senderId;
|
||||
}
|
||||
|
||||
var ids = new[] { component.UserId, otherUserId }.OrderBy(x => x).ToArray();
|
||||
chatId = $"personal_{ids[0]}_{ids[1]}";
|
||||
}
|
||||
else
|
||||
{
|
||||
chatId = senderId;
|
||||
}
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(chatId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[chatId] = history;
|
||||
}
|
||||
|
||||
var messageExists = history.Any(m =>
|
||||
m.SenderId == message.SenderId &&
|
||||
Math.Abs(m.Timestamp.TotalSeconds - message.Timestamp.TotalSeconds) < 0.001 &&
|
||||
m.Content == message.Content);
|
||||
|
||||
if (!messageExists)
|
||||
{
|
||||
history.Add(message);
|
||||
history = history.OrderBy(m => m.Timestamp)
|
||||
.ThenBy(m => m.SenderId)
|
||||
.ThenBy(m => m.Content)
|
||||
.ToList();
|
||||
component.MessageHistory[chatId] = history;
|
||||
}
|
||||
else
|
||||
{
|
||||
var existingMessage = history.FirstOrDefault(m =>
|
||||
m.SenderId == message.SenderId &&
|
||||
Math.Abs(m.Timestamp.TotalSeconds - message.Timestamp.TotalSeconds) < 0.001 &&
|
||||
m.Content == message.Content);
|
||||
|
||||
if (existingMessage != null)
|
||||
{
|
||||
existingMessage.IsRead = message.IsRead;
|
||||
}
|
||||
}
|
||||
|
||||
var isGroupChat = chatId == "common" || chatId.StartsWith("dept_") || chatId.StartsWith("group_");
|
||||
var isMuted = isGroupChat
|
||||
? component.MutedGroupChats.Contains(chatId)
|
||||
: component.MutedPersonalChats.Contains(chatId);
|
||||
|
||||
var isSender = component.UserId == senderId;
|
||||
var isChatOpen = component.LastRequestedChatId == chatId;
|
||||
|
||||
if (!isSender && !isChatOpen && isGroupChat)
|
||||
{
|
||||
component.ServerUnreadCounts.TryGetValue(chatId, out var currentCount);
|
||||
component.ServerUnreadCounts[chatId] = currentCount + 1;
|
||||
}
|
||||
|
||||
if (!isMuted && !isSender && TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
{
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
|
||||
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
|
||||
{
|
||||
RequestUsers(uid, component, loaderUid, deviceNetwork);
|
||||
RequestGroups(uid, component, loaderUid, deviceNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGroupCreated(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
|
||||
{
|
||||
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
|
||||
RequestGroups(uid, component, loaderUid, deviceNetwork);
|
||||
}
|
||||
|
||||
private void HandleUserAddedToGroup(EntityUid uid, MessengerCartridgeComponent component, DeviceNetworkPacketEvent packet, EntityUid loaderUid)
|
||||
{
|
||||
if (!packet.Data.TryGetValue("user_id", out object? addedUserIdObj) ||
|
||||
!packet.Data.TryGetValue("group_id", out object? groupIdObj))
|
||||
return;
|
||||
|
||||
var addedUserId = addedUserIdObj?.ToString();
|
||||
var groupId = groupIdObj?.ToString();
|
||||
|
||||
if (addedUserId == component.UserId && !string.IsNullOrEmpty(groupId))
|
||||
{
|
||||
if (TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
{
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryComp<DeviceNetworkComponent>(loaderUid, out var deviceNetwork))
|
||||
RequestGroups(uid, component, loaderUid, deviceNetwork);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
using System.Linq;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы картриджа мессенджера, отвечающая за обновление UI состояния
|
||||
/// </summary>
|
||||
public sealed partial class MessengerCartridgeSystem
|
||||
{
|
||||
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, MessengerCartridgeComponent? component)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var unreadCounts = new Dictionary<string, int>();
|
||||
if (component.ServerAddress != null && component.UserId != null)
|
||||
{
|
||||
foreach (var (chatId, messages) in component.MessageHistory)
|
||||
{
|
||||
if (messages == null || messages.Count == 0)
|
||||
continue;
|
||||
|
||||
if (chatId.StartsWith("personal_"))
|
||||
{
|
||||
var unreadCount = messages.Count(m => !m.IsRead && m.RecipientId == component.UserId && !string.IsNullOrEmpty(m.RecipientId));
|
||||
|
||||
if (component.ServerUnreadCounts.TryGetValue(chatId, out var serverCount) && serverCount > unreadCount)
|
||||
{
|
||||
unreadCount = serverCount;
|
||||
}
|
||||
|
||||
if (unreadCount > 0)
|
||||
{
|
||||
unreadCounts[chatId] = unreadCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (chatId, count) in component.ServerUnreadCounts)
|
||||
{
|
||||
if (!chatId.StartsWith("personal_") && count > 0)
|
||||
{
|
||||
unreadCounts[chatId] = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var state = new MessengerUiState(
|
||||
component.IsRegistered,
|
||||
component.ServerAddress != null,
|
||||
component.UserId,
|
||||
component.Users,
|
||||
component.Groups,
|
||||
component.MessageHistory,
|
||||
component.MutedPersonalChats,
|
||||
component.MutedGroupChats,
|
||||
unreadCounts
|
||||
);
|
||||
|
||||
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
|
||||
}
|
||||
|
||||
private void ToggleMute(EntityUid uid, MessengerCartridgeComponent component, string chatId, bool isMuted)
|
||||
{
|
||||
var isGroup = component.Groups.Any(g => g.GroupId == chatId);
|
||||
|
||||
if (isGroup)
|
||||
{
|
||||
if (isMuted)
|
||||
component.MutedGroupChats.Add(chatId);
|
||||
else
|
||||
component.MutedGroupChats.Remove(chatId);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isMuted)
|
||||
component.MutedPersonalChats.Add(chatId);
|
||||
else
|
||||
component.MutedPersonalChats.Remove(chatId);
|
||||
}
|
||||
|
||||
if (component.LoaderUid.HasValue)
|
||||
UpdateUiState(uid, component.LoaderUid.Value, component);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.CartridgeLoader;
|
||||
using Content.Server.PDA.Ringer;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Система картриджа мессенджера для КПК
|
||||
/// </summary>
|
||||
public sealed partial class MessengerCartridgeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
|
||||
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServer = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly RingerSystem _ringer = default!;
|
||||
|
||||
private ISawmill Sawmill { get; set; } = default!;
|
||||
private const string MessengerFrequencyId = "Messenger";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Sawmill = _logManager.GetSawmill("messenger.cartridge");
|
||||
|
||||
SubscribeLocalEvent<MessengerCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
|
||||
SubscribeLocalEvent<MessengerCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
||||
SubscribeLocalEvent<MessengerCartridgeComponent, CartridgeActivatedEvent>(OnCartridgeActivated);
|
||||
SubscribeLocalEvent<MessengerCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
|
||||
SubscribeLocalEvent<MessengerCartridgeComponent, CartridgeDeviceNetPacketEvent>(OnPacketReceived);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<MessengerCartridgeComponent>();
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
|
||||
while (query.MoveNext(out var uid, out var component))
|
||||
{
|
||||
if (component.LoaderUid == null)
|
||||
continue;
|
||||
|
||||
if (component.LastStatusCheck.HasValue)
|
||||
{
|
||||
var timeSinceLastCheck = currentTime - component.LastStatusCheck.Value;
|
||||
if (timeSinceLastCheck.TotalSeconds < 2.0)
|
||||
continue;
|
||||
}
|
||||
|
||||
component.LastStatusCheck = currentTime;
|
||||
|
||||
CheckServerStatus(uid, component, component.LoaderUid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetPdaAndDeviceNetwork(EntityUid loaderUid, out EntityUid pdaUid, out DeviceNetworkComponent deviceNetwork)
|
||||
{
|
||||
pdaUid = EntityUid.Invalid;
|
||||
deviceNetwork = null!;
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(loaderUid, out var device))
|
||||
return false;
|
||||
|
||||
pdaUid = loaderUid;
|
||||
deviceNetwork = device;
|
||||
return true;
|
||||
}
|
||||
|
||||
private EntityUid GetEntity(NetEntity netEntity)
|
||||
{
|
||||
return EntityManager.GetEntity(netEntity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает частоту Messenger
|
||||
/// </summary>
|
||||
private uint? GetMessengerFrequency()
|
||||
{
|
||||
if (_prototypeManager.TryIndex<DeviceFrequencyPrototype>(MessengerFrequencyId, out var messengerFrequency))
|
||||
{
|
||||
return messengerFrequency.Frequency;
|
||||
}
|
||||
Sawmill.Error($"Messenger frequency prototype not found: {MessengerFrequencyId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает частоту передачи на Messenger
|
||||
/// </summary>
|
||||
private void SetMessengerFrequency(EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, out uint? originalFrequency)
|
||||
{
|
||||
originalFrequency = deviceNetwork.TransmitFrequency;
|
||||
var messengerFreq = GetMessengerFrequency();
|
||||
if (messengerFreq.HasValue)
|
||||
{
|
||||
_deviceNetwork.SetTransmitFrequency(loaderUid, messengerFreq.Value, deviceNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Восстанавливает исходную частоту передачи
|
||||
/// </summary>
|
||||
private void RestoreFrequency(EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, uint? originalFrequency)
|
||||
{
|
||||
if (originalFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.SetTransmitFrequency(loaderUid, originalFrequency.Value, deviceNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,14 +48,12 @@ public sealed class NavigatorCartridgeSystem : EntitySystem
|
|||
var stationName = "Unknown Station";
|
||||
NetEntity? mapUid = null;
|
||||
|
||||
if (owningStation != null && TryComp<MetaDataComponent>(owningStation.Value, out var metaData))
|
||||
if (owningStation != null)
|
||||
{
|
||||
stationName = metaData.EntityName;
|
||||
|
||||
// Try to get the station's primary grid for the map
|
||||
stationName = MetaData(owningStation.Value).EntityName;
|
||||
|
||||
if (TryComp<StationDataComponent>(owningStation.Value, out var stationData) && stationData.Grids.Count > 0)
|
||||
{
|
||||
// Get the first grid as the map reference and convert to NetEntity
|
||||
mapUid = GetNetEntity(stationData.Grids.First());
|
||||
}
|
||||
}
|
||||
|
|
@ -63,4 +61,4 @@ public sealed class NavigatorCartridgeSystem : EntitySystem
|
|||
var state = new NavigatorUiState(mapUid, stationName);
|
||||
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Content.Server/_Sunrise/Messenger/EmojiSystem.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Серверная реализация системы эмодзи
|
||||
/// </summary>
|
||||
public sealed class ServerEmojiSystem : Content.Shared._Sunrise.Messenger.EmojiSystem
|
||||
{
|
||||
}
|
||||
26
Content.Server/_Sunrise/Messenger/MessengerCommands.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Константы команд для мессенджера
|
||||
/// </summary>
|
||||
public static class MessengerCommands
|
||||
{
|
||||
// Входящие команды
|
||||
public const string CmdRegisterUser = "messenger_register_user";
|
||||
public const string CmdSendMessage = "messenger_send_message";
|
||||
public const string CmdCreateGroup = "messenger_create_group";
|
||||
public const string CmdAddToGroup = "messenger_add_to_group";
|
||||
public const string CmdRemoveFromGroup = "messenger_remove_from_group";
|
||||
public const string CmdGetUsers = "messenger_get_users";
|
||||
public const string CmdGetGroups = "messenger_get_groups";
|
||||
public const string CmdGetMessages = "messenger_get_messages";
|
||||
|
||||
// Исходящие команды
|
||||
public const string CmdUserRegistered = "messenger_user_registered";
|
||||
public const string CmdUsersList = "messenger_users_list";
|
||||
public const string CmdGroupsList = "messenger_groups_list";
|
||||
public const string CmdMessagesList = "messenger_messages_list";
|
||||
public const string CmdMessageReceived = "messenger_message_received";
|
||||
public const string CmdGroupCreated = "messenger_group_created";
|
||||
public const string CmdUserAddedToGroup = "messenger_user_added_to_group";
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Компонент сервера мессенджера, который обрабатывает сообщения между КПК
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(MessengerServerSystem))]
|
||||
public sealed partial class MessengerServerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Словарь зарегистрированных пользователей. Ключ - адрес DeviceNetwork КПК
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<string, MessengerUser> Users = new();
|
||||
|
||||
/// <summary>
|
||||
/// Словарь групп. Ключ - ID группы
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<string, MessengerGroup> Groups = new();
|
||||
|
||||
/// <summary>
|
||||
/// История сообщений. Ключ - ID чата (userId для личных, groupId для групп)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<string, List<MessengerMessage>> MessageHistory = new();
|
||||
|
||||
/// <summary>
|
||||
/// Максимальное количество сообщений в истории для одного чата
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxMessageHistory = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Счетчик для генерации уникальных ID групп
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public int GroupIdCounter = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Количество непрочитанных сообщений по пользователям и чатам.
|
||||
/// Ключ - userId, значение - словарь (chatId -> количество непрочитанных)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<string, Dictionary<string, int>> UnreadCounts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Открытые чаты пользователей. Ключ - userId, значение - chatId открытого чата
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<string, string?> OpenChats = new();
|
||||
|
||||
/// <summary>
|
||||
/// Прототип частоты для PDA устройств
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<DeviceFrequencyPrototype> PdaFrequencyId = "PDA";
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы мессенджера, отвечающая за управление группами
|
||||
/// </summary>
|
||||
public sealed partial class MessengerServerSystem
|
||||
{
|
||||
private void HandleCreateGroup(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!args.Data.TryGetValue(MessengerCommands.CmdCreateGroup, out NetworkPayload? groupData))
|
||||
return;
|
||||
|
||||
if (!groupData.TryGetValue("name", out string? groupName) || string.IsNullOrWhiteSpace(groupName))
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(args.SenderAddress, out var creator))
|
||||
return;
|
||||
|
||||
var groupId = $"group_{++component.GroupIdCounter}";
|
||||
var members = new HashSet<string> { creator.UserId };
|
||||
|
||||
var group = new MessengerGroup(groupId, groupName, members, MessengerGroupType.UserCreated, null, creator.UserId);
|
||||
component.Groups[groupId] = group;
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
var membersList = new List<object>();
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
membersList.Add(memberId);
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdGroupCreated,
|
||||
["group_id"] = group.GroupId,
|
||||
["group_name"] = group.Name,
|
||||
["group_type"] = (int)group.Type,
|
||||
["auto_group_prototype_id"] = group.AutoGroupPrototypeId ?? string.Empty,
|
||||
["owner_id"] = group.OwnerId ?? string.Empty,
|
||||
["members"] = membersList
|
||||
};
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAddToGroup(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!args.Data.TryGetValue(MessengerCommands.CmdAddToGroup, out NetworkPayload? addData))
|
||||
return;
|
||||
|
||||
if (!addData.TryGetValue("group_id", out string? groupId))
|
||||
return;
|
||||
|
||||
if (!addData.TryGetValue("user_id", out string? userId))
|
||||
return;
|
||||
|
||||
if (!component.Groups.TryGetValue(groupId, out var group))
|
||||
return;
|
||||
|
||||
if (group.Type != MessengerGroupType.UserCreated)
|
||||
{
|
||||
if (group.AutoGroupPrototypeId != null)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<MessengerAutoGroupPrototype>(group.AutoGroupPrototypeId, out var autoGroupProto))
|
||||
{
|
||||
if (!autoGroupProto.AllowManualMemberManagement)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.Users.TryGetValue(args.SenderAddress, out var adder))
|
||||
return;
|
||||
|
||||
if (group.OwnerId != adder.UserId)
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(userId, out _))
|
||||
return;
|
||||
|
||||
if (group.Members.Contains(userId))
|
||||
return;
|
||||
|
||||
group.Members.Add(userId);
|
||||
|
||||
if (!component.Users.TryGetValue(userId, out var addedUser))
|
||||
return;
|
||||
|
||||
var timestamp = GetStationTime();
|
||||
var messageText = Loc.GetString("messenger-system-user-added-by", ("adderName", adder.Name), ("userName", addedUser.Name));
|
||||
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, groupId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(groupId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[groupId] = history;
|
||||
}
|
||||
history.Add(systemMessage);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
var messagePayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = "system",
|
||||
["sender_name"] = Loc.GetString("messenger-system-name"),
|
||||
["content"] = messageText,
|
||||
["timestamp"] = timestamp.TotalSeconds,
|
||||
["group_id"] = groupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
|
||||
}
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUserAddedToGroup,
|
||||
["group_id"] = groupId,
|
||||
["user_id"] = userId
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRemoveFromGroup(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!args.Data.TryGetValue(MessengerCommands.CmdRemoveFromGroup, out NetworkPayload? removeData))
|
||||
return;
|
||||
|
||||
if (!removeData.TryGetValue("group_id", out string? groupId))
|
||||
return;
|
||||
|
||||
if (!removeData.TryGetValue("user_id", out string? userId))
|
||||
return;
|
||||
|
||||
if (!component.Groups.TryGetValue(groupId, out var group))
|
||||
return;
|
||||
|
||||
if (group.Type != MessengerGroupType.UserCreated)
|
||||
{
|
||||
if (group.AutoGroupPrototypeId != null)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<MessengerAutoGroupPrototype>(group.AutoGroupPrototypeId, out var autoGroupProto))
|
||||
{
|
||||
if (!autoGroupProto.AllowManualMemberManagement)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.Users.TryGetValue(args.SenderAddress, out var remover))
|
||||
return;
|
||||
|
||||
if (group.OwnerId != remover.UserId)
|
||||
return;
|
||||
|
||||
if (userId == group.OwnerId)
|
||||
return;
|
||||
|
||||
if (!group.Members.Contains(userId))
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(userId, out var removedUser))
|
||||
return;
|
||||
|
||||
group.Members.Remove(userId);
|
||||
|
||||
var timestamp = GetStationTime();
|
||||
var messageText = Loc.GetString("messenger-system-user-removed-by", ("removerName", remover.Name), ("userName", removedUser.Name));
|
||||
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, groupId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(groupId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[groupId] = history;
|
||||
}
|
||||
history.Add(systemMessage);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
var messagePayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = "system",
|
||||
["sender_name"] = Loc.GetString("messenger-system-name"),
|
||||
["content"] = messageText,
|
||||
["timestamp"] = timestamp.TotalSeconds,
|
||||
["group_id"] = groupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы мессенджера, отвечающая за обработку сообщений
|
||||
/// </summary>
|
||||
public sealed partial class MessengerServerSystem
|
||||
{
|
||||
private void HandleSendMessage(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!args.Data.TryGetValue(MessengerCommands.CmdSendMessage, out NetworkPayload? messageData))
|
||||
return;
|
||||
|
||||
if (!messageData.TryGetValue("content", out string? content) || string.IsNullOrWhiteSpace(content))
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(args.SenderAddress, out var sender))
|
||||
return;
|
||||
|
||||
var timestamp = GetStationTime();
|
||||
|
||||
if (messageData.TryGetValue("group_id", out string? groupId) && !string.IsNullOrWhiteSpace(groupId))
|
||||
{
|
||||
SendGroupMessage(uid, component, sender, groupId, content, timestamp);
|
||||
}
|
||||
else if (messageData.TryGetValue("recipient_id", out string? recipientId) && !string.IsNullOrWhiteSpace(recipientId))
|
||||
{
|
||||
SendPersonalMessage(uid, component, sender, recipientId, content, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendPersonalMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string recipientId, string content, TimeSpan timestamp)
|
||||
{
|
||||
if (!component.Users.ContainsKey(recipientId))
|
||||
return;
|
||||
|
||||
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, null, recipientId, isRead: false);
|
||||
var chatId = GetPersonalChatId(sender.UserId, recipientId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(chatId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[chatId] = history;
|
||||
}
|
||||
|
||||
history.Add(message);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
var isChatOpen = component.OpenChats.TryGetValue(recipientId, out var openChatId) && openChatId == chatId;
|
||||
|
||||
if (isChatOpen)
|
||||
{
|
||||
message.IsRead = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!component.UnreadCounts.TryGetValue(recipientId, out var recipientUnreads))
|
||||
{
|
||||
recipientUnreads = new Dictionary<string, int>();
|
||||
component.UnreadCounts[recipientId] = recipientUnreads;
|
||||
}
|
||||
recipientUnreads.TryGetValue(chatId, out var currentCount);
|
||||
recipientUnreads[chatId] = currentCount + 1;
|
||||
}
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = message.SenderId,
|
||||
["sender_name"] = message.SenderName,
|
||||
["content"] = message.Content,
|
||||
["timestamp"] = message.Timestamp.TotalSeconds,
|
||||
["group_id"] = message.GroupId ?? string.Empty,
|
||||
["recipient_id"] = message.RecipientId ?? string.Empty,
|
||||
["is_read"] = message.IsRead
|
||||
};
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, recipientId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
_deviceNetwork.QueuePacket(uid, sender.UserId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, recipientId, payload);
|
||||
_deviceNetwork.QueuePacket(uid, sender.UserId, payload);
|
||||
}
|
||||
|
||||
if (isChatOpen && pdaFrequency.HasValue)
|
||||
{
|
||||
var updatePayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessagesList,
|
||||
["messages"] = new List<Dictionary<string, object>>
|
||||
{
|
||||
new ()
|
||||
{
|
||||
["sender_id"] = message.SenderId,
|
||||
["sender_name"] = message.SenderName,
|
||||
["content"] = message.Content,
|
||||
["timestamp"] = message.Timestamp.TotalSeconds,
|
||||
["group_id"] = message.GroupId ?? string.Empty,
|
||||
["recipient_id"] = message.RecipientId ?? string.Empty,
|
||||
["is_read"] = message.IsRead
|
||||
}
|
||||
},
|
||||
["chat_id"] = chatId
|
||||
};
|
||||
_deviceNetwork.QueuePacket(uid, sender.UserId, updatePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendGroupMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string groupId, string content, TimeSpan timestamp)
|
||||
{
|
||||
if (!component.Groups.TryGetValue(groupId, out var group))
|
||||
return;
|
||||
|
||||
if (!group.Members.Contains(sender.UserId))
|
||||
return;
|
||||
|
||||
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, groupId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(groupId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[groupId] = history;
|
||||
}
|
||||
|
||||
history.Add(message);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (memberId == sender.UserId)
|
||||
continue;
|
||||
|
||||
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == groupId;
|
||||
|
||||
if (!isMemberChatOpen)
|
||||
{
|
||||
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
|
||||
{
|
||||
memberUnreads = new Dictionary<string, int>();
|
||||
component.UnreadCounts[memberId] = memberUnreads;
|
||||
}
|
||||
memberUnreads.TryGetValue(groupId, out var currentCount);
|
||||
memberUnreads[groupId] = currentCount + 1;
|
||||
}
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = message.SenderId,
|
||||
["sender_name"] = message.SenderName,
|
||||
["content"] = message.Content,
|
||||
["timestamp"] = message.Timestamp.TotalSeconds,
|
||||
["group_id"] = message.GroupId ?? string.Empty,
|
||||
["recipient_id"] = message.RecipientId ?? string.Empty,
|
||||
["is_read"] = message.IsRead
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы мессенджера, отвечающая за обработку запросов данных
|
||||
/// </summary>
|
||||
public sealed partial class MessengerServerSystem
|
||||
{
|
||||
private void HandleGetUsers(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
var usersList = component.Users.Values.ToList();
|
||||
var usersData = new List<Dictionary<string, object>>();
|
||||
|
||||
foreach (var user in usersList)
|
||||
{
|
||||
usersData.Add(new Dictionary<string, object>
|
||||
{
|
||||
["user_id"] = user.UserId,
|
||||
["user_name"] = user.Name,
|
||||
["job_title"] = user.JobTitle ?? string.Empty,
|
||||
["department_id"] = user.DepartmentId ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUsersList,
|
||||
["users"] = usersData
|
||||
};
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGetGroups(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
var senderUserId = args.SenderAddress;
|
||||
if (string.IsNullOrEmpty(senderUserId))
|
||||
return;
|
||||
|
||||
var groupsList = component.Groups.Values.ToList();
|
||||
var groupsData = new List<Dictionary<string, object>>();
|
||||
|
||||
foreach (var group in groupsList)
|
||||
{
|
||||
if (group.GroupId == "common")
|
||||
{
|
||||
if (!group.Members.Contains(senderUserId))
|
||||
continue;
|
||||
}
|
||||
else if (group.GroupId.StartsWith("dept_"))
|
||||
{
|
||||
if (!group.Members.Contains(senderUserId))
|
||||
continue;
|
||||
}
|
||||
else if (group.GroupId.StartsWith("group_"))
|
||||
{
|
||||
if (!group.Members.Contains(senderUserId))
|
||||
continue;
|
||||
}
|
||||
|
||||
var membersList = new List<object>();
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
membersList.Add(memberId);
|
||||
}
|
||||
|
||||
var unreadCount = 0;
|
||||
if (component.UnreadCounts.TryGetValue(senderUserId, out var userUnreads))
|
||||
{
|
||||
userUnreads.TryGetValue(group.GroupId, out unreadCount);
|
||||
}
|
||||
|
||||
groupsData.Add(new Dictionary<string, object>
|
||||
{
|
||||
["group_id"] = group.GroupId,
|
||||
["group_name"] = group.Name,
|
||||
["group_type"] = (int)group.Type,
|
||||
["auto_group_prototype_id"] = group.AutoGroupPrototypeId ?? string.Empty,
|
||||
["owner_id"] = group.OwnerId ?? string.Empty,
|
||||
["members"] = membersList,
|
||||
["unread_count"] = unreadCount
|
||||
});
|
||||
}
|
||||
|
||||
var unreadCountsData = new Dictionary<string, int>();
|
||||
if (component.UnreadCounts.TryGetValue(senderUserId, out var senderUnreads))
|
||||
{
|
||||
foreach (var (chatId, count) in senderUnreads)
|
||||
{
|
||||
unreadCountsData[chatId] = count;
|
||||
}
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdGroupsList,
|
||||
["groups"] = groupsData,
|
||||
["unread_counts"] = unreadCountsData
|
||||
};
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGetMessages(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
if (!args.Data.TryGetValue(MessengerCommands.CmdGetMessages, out NetworkPayload? messageRequest))
|
||||
return;
|
||||
|
||||
if (!messageRequest.TryGetValue("chat_id", out string? chatId))
|
||||
return;
|
||||
|
||||
var userId = args.SenderAddress;
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
component.OpenChats[userId] = chatId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(userId) && component.UnreadCounts.TryGetValue(userId, out var userUnreads))
|
||||
{
|
||||
userUnreads.Remove(chatId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(userId) && component.MessageHistory.TryGetValue(chatId, out var chatMessages))
|
||||
{
|
||||
var updatedSenders = new HashSet<string>();
|
||||
var hasUpdates = false;
|
||||
foreach (var message in chatMessages)
|
||||
{
|
||||
if (message.RecipientId == userId && !string.IsNullOrEmpty(message.RecipientId) && !message.IsRead)
|
||||
{
|
||||
message.IsRead = true;
|
||||
hasUpdates = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(message.SenderId) && message.SenderId != userId)
|
||||
{
|
||||
updatedSenders.Add(message.SenderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedSenders.Count > 0)
|
||||
{
|
||||
uint? updatePdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var updatePdaFreq))
|
||||
{
|
||||
updatePdaFrequency = updatePdaFreq.Frequency;
|
||||
}
|
||||
|
||||
foreach (var senderId in updatedSenders)
|
||||
{
|
||||
var sortedChatMessages = chatMessages.OrderBy(m => m.Timestamp)
|
||||
.ThenBy(m => m.SenderId)
|
||||
.ThenBy(m => m.Content)
|
||||
.ToList();
|
||||
|
||||
var senderMessagesData = new List<Dictionary<string, object>>();
|
||||
foreach (var msg in sortedChatMessages)
|
||||
{
|
||||
if (msg.SenderId == senderId || msg.RecipientId == senderId)
|
||||
{
|
||||
senderMessagesData.Add(new Dictionary<string, object>
|
||||
{
|
||||
["sender_id"] = msg.SenderId,
|
||||
["sender_name"] = msg.SenderName,
|
||||
["content"] = msg.Content,
|
||||
["timestamp"] = msg.Timestamp.TotalSeconds,
|
||||
["group_id"] = msg.GroupId ?? string.Empty,
|
||||
["recipient_id"] = msg.RecipientId ?? string.Empty,
|
||||
["is_read"] = msg.IsRead
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var updatePayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessagesList,
|
||||
["messages"] = senderMessagesData,
|
||||
["chat_id"] = chatId
|
||||
};
|
||||
|
||||
if (updatePdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, senderId, updatePayload, frequency: updatePdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, senderId, updatePayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates)
|
||||
{
|
||||
var sortedChatMessages = chatMessages.OrderBy(m => m.Timestamp)
|
||||
.ThenBy(m => m.SenderId)
|
||||
.ThenBy(m => m.Content)
|
||||
.ToList();
|
||||
|
||||
var recipientMessagesData = new List<Dictionary<string, object>>();
|
||||
foreach (var msg in sortedChatMessages)
|
||||
{
|
||||
recipientMessagesData.Add(new Dictionary<string, object>
|
||||
{
|
||||
["sender_id"] = msg.SenderId,
|
||||
["sender_name"] = msg.SenderName,
|
||||
["content"] = msg.Content,
|
||||
["timestamp"] = msg.Timestamp.TotalSeconds,
|
||||
["group_id"] = msg.GroupId ?? string.Empty,
|
||||
["recipient_id"] = msg.RecipientId ?? string.Empty,
|
||||
["is_read"] = msg.IsRead
|
||||
});
|
||||
}
|
||||
|
||||
var recipientPayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessagesList,
|
||||
["messages"] = recipientMessagesData,
|
||||
["chat_id"] = chatId
|
||||
};
|
||||
|
||||
uint? recipientPdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var recipientPdaFreq))
|
||||
{
|
||||
recipientPdaFrequency = recipientPdaFreq.Frequency;
|
||||
}
|
||||
|
||||
if (recipientPdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, userId, recipientPayload, frequency: recipientPdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, userId, recipientPayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(chatId, out var messages))
|
||||
messages = new List<MessengerMessage>();
|
||||
|
||||
var sortedMessages = messages.OrderBy(m => m.Timestamp)
|
||||
.ThenBy(m => m.SenderId)
|
||||
.ThenBy(m => m.Content)
|
||||
.ToList();
|
||||
|
||||
var messagesData = new List<Dictionary<string, object>>();
|
||||
foreach (var message in sortedMessages)
|
||||
{
|
||||
messagesData.Add(new Dictionary<string, object>
|
||||
{
|
||||
["sender_id"] = message.SenderId,
|
||||
["sender_name"] = message.SenderName,
|
||||
["content"] = message.Content,
|
||||
["timestamp"] = message.Timestamp.TotalSeconds,
|
||||
["group_id"] = message.GroupId ?? string.Empty,
|
||||
["recipient_id"] = message.RecipientId ?? string.Empty,
|
||||
["is_read"] = message.IsRead
|
||||
});
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessagesList,
|
||||
["messages"] = messagesData,
|
||||
["chat_id"] = chatId
|
||||
};
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared.GameTicking;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Часть системы мессенджера, отвечающая за регистрацию пользователей
|
||||
/// </summary>
|
||||
public sealed partial class MessengerServerSystem
|
||||
{
|
||||
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
EntityUid? pdaUid = null;
|
||||
|
||||
if (_inventory.TryGetSlotEntity(args.Mob, "idcard", out var idCardEntity) &&
|
||||
TryComp<PdaComponent>(idCardEntity, out _))
|
||||
{
|
||||
pdaUid = idCardEntity;
|
||||
}
|
||||
else if (_inventory.TryGetSlotEntity(args.Mob, "belt", out var beltEntity) &&
|
||||
TryComp<PdaComponent>(beltEntity, out _))
|
||||
{
|
||||
pdaUid = beltEntity;
|
||||
}
|
||||
else
|
||||
{
|
||||
var handsQuery = EntityQueryEnumerator<PdaComponent>();
|
||||
while (handsQuery.MoveNext(out var uid, out var pda))
|
||||
{
|
||||
if (pda.PdaOwner == args.Mob)
|
||||
{
|
||||
pdaUid = uid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pdaUid == null)
|
||||
{
|
||||
Sawmill.Warning($"No PDA found for player: {ToPrettyString(args.Mob)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var station = _stationSystem.GetOwningStation(args.Mob);
|
||||
if (station == null)
|
||||
{
|
||||
Sawmill.Warning($"No station found for player: {ToPrettyString(args.Mob)}");
|
||||
return;
|
||||
}
|
||||
|
||||
Sawmill.Debug($"Player station: {ToPrettyString(station.Value)}");
|
||||
|
||||
var serverQuery = EntityQueryEnumerator<MessengerServerComponent, SingletonDeviceNetServerComponent, DeviceNetworkComponent>();
|
||||
EntityUid? serverUid = null;
|
||||
MessengerServerComponent? serverComponent = null;
|
||||
DeviceNetworkComponent? serverDevice = null;
|
||||
|
||||
int serverCount = 0;
|
||||
while (serverQuery.MoveNext(out var uid, out var comp, out var singleton, out var device))
|
||||
{
|
||||
serverCount++;
|
||||
var serverStation = _stationSystem.GetOwningStation(uid);
|
||||
|
||||
if (serverStation != station)
|
||||
continue;
|
||||
|
||||
if (!_singletonServer.IsActiveServer(uid, singleton))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
serverUid = uid;
|
||||
serverComponent = comp;
|
||||
serverDevice = device;
|
||||
break;
|
||||
}
|
||||
|
||||
if (serverCount == 0)
|
||||
{
|
||||
Sawmill.Warning($"No messenger servers found on station: {ToPrettyString(station.Value)}");
|
||||
}
|
||||
|
||||
if (serverUid == null || serverComponent == null || serverDevice == null)
|
||||
{
|
||||
Sawmill.Warning($"No active messenger server found for player: {ToPrettyString(args.Mob)}");
|
||||
return;
|
||||
}
|
||||
|
||||
Sawmill.Debug($"Server DeviceNetwork before registration: Address={serverDevice.Address}, TransmitFrequency={serverDevice.TransmitFrequency}, ReceiveFrequency={serverDevice.ReceiveFrequency}");
|
||||
|
||||
RegisterUserFromPda(serverUid.Value, serverComponent, pdaUid.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует пользователя на сервере мессенджера по его PDA
|
||||
/// </summary>
|
||||
private void RegisterUserFromPda(EntityUid uid, MessengerServerComponent component, EntityUid pdaUid)
|
||||
{
|
||||
if (!TryComp<PdaComponent>(pdaUid, out var pda))
|
||||
{
|
||||
Sawmill.Warning($"PDA component not found: {ToPrettyString(pdaUid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(pdaUid, out var pdaDevice))
|
||||
{
|
||||
Sawmill.Warning($"DeviceNetwork component not found on PDA: {ToPrettyString(pdaUid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(pdaDevice.Address))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = pdaDevice.Address;
|
||||
var userName = pda.OwnerName ?? Loc.GetString("messenger-user-unknown");
|
||||
|
||||
string? jobTitle = null;
|
||||
string? departmentId = null;
|
||||
|
||||
if (pda.ContainedId != null && TryComp<IdCardComponent>(pda.ContainedId.Value, out var idCard))
|
||||
{
|
||||
jobTitle = idCard.LocalizedJobTitle;
|
||||
if (idCard.JobDepartments.Count > 0)
|
||||
{
|
||||
departmentId = idCard.JobDepartments[0];
|
||||
}
|
||||
}
|
||||
|
||||
var user = new MessengerUser(userId, userName, jobTitle, departmentId);
|
||||
component.Users[userId] = user;
|
||||
|
||||
AddUserToAutoGroups(uid, component, userId, userName, departmentId);
|
||||
}
|
||||
|
||||
private void HandleRegisterUser(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!args.Data.TryGetValue(MessengerCommands.CmdRegisterUser, out NetworkPayload? userData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userData.TryGetValue("pda_uid", out NetEntity netPdaUid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pdaUid = EntityManager.GetEntity(netPdaUid);
|
||||
|
||||
if (!TryComp<PdaComponent>(pdaUid, out var pda))
|
||||
{
|
||||
Sawmill.Warning($"PDA component not found: {ToPrettyString(pdaUid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = args.SenderAddress;
|
||||
var userName = pda.OwnerName ?? Loc.GetString("messenger-user-unknown");
|
||||
|
||||
string? jobTitle = null;
|
||||
string? departmentId = null;
|
||||
|
||||
if (pda.ContainedId != null && TryComp<IdCardComponent>(pda.ContainedId.Value, out var idCard))
|
||||
{
|
||||
jobTitle = idCard.LocalizedJobTitle;
|
||||
if (idCard.JobDepartments.Count > 0)
|
||||
{
|
||||
departmentId = idCard.JobDepartments[0];
|
||||
}
|
||||
}
|
||||
|
||||
var user = new MessengerUser(userId, userName, jobTitle, departmentId);
|
||||
component.Users[userId] = user;
|
||||
|
||||
AddUserToAutoGroups(uid, component, userId, userName, departmentId);
|
||||
|
||||
var response = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUserRegistered,
|
||||
["user_id"] = userId,
|
||||
["user_name"] = userName,
|
||||
["job_title"] = jobTitle ?? string.Empty,
|
||||
["department_id"] = departmentId ?? string.Empty
|
||||
};
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
{
|
||||
Sawmill.Warning($"Server does not have DeviceNetworkComponent: {ToPrettyString(uid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
uint? pdaFrequency;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Error($"PDA frequency prototype not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsAddressPresent(serverDevice.DeviceNetId, args.SenderAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_deviceNetwork.QueuePacket(uid, args.SenderAddress, response, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает автоматические группы на основе прототипов
|
||||
/// </summary>
|
||||
private void CreateAutoGroups(MessengerServerComponent component)
|
||||
{
|
||||
int created = 0;
|
||||
foreach (var autoGroupProto in _prototypeManager.EnumeratePrototypes<MessengerAutoGroupPrototype>())
|
||||
{
|
||||
if (component.Groups.ContainsKey(autoGroupProto.GroupId))
|
||||
continue;
|
||||
|
||||
var group = new MessengerGroup(
|
||||
autoGroupProto.GroupId,
|
||||
_loc.GetString(autoGroupProto.Name),
|
||||
new HashSet<string>(),
|
||||
MessengerGroupType.Automatic,
|
||||
autoGroupProto.ID
|
||||
);
|
||||
component.Groups[autoGroupProto.GroupId] = group;
|
||||
created++;
|
||||
}
|
||||
|
||||
Sawmill.Info($"Created {created} automatic messenger groups");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет пользователя в автоматические группы на основе прототипов
|
||||
/// </summary>
|
||||
private void AddUserToAutoGroups(EntityUid uid, MessengerServerComponent component, string userId, string userName, string? departmentId)
|
||||
{
|
||||
foreach (var autoGroupProto in _prototypeManager.EnumeratePrototypes<MessengerAutoGroupPrototype>())
|
||||
{
|
||||
bool shouldAdd = false;
|
||||
|
||||
if (autoGroupProto.AddAllUsers)
|
||||
{
|
||||
shouldAdd = true;
|
||||
}
|
||||
else if (departmentId != null && autoGroupProto.Departments.Count > 0)
|
||||
{
|
||||
shouldAdd = autoGroupProto.Departments.Contains(departmentId);
|
||||
}
|
||||
|
||||
if (!shouldAdd)
|
||||
continue;
|
||||
|
||||
if (!component.Groups.TryGetValue(autoGroupProto.GroupId, out var group))
|
||||
{
|
||||
group = new MessengerGroup(
|
||||
autoGroupProto.GroupId,
|
||||
_loc.GetString(autoGroupProto.Name),
|
||||
new HashSet<string>(),
|
||||
MessengerGroupType.Automatic,
|
||||
autoGroupProto.ID
|
||||
);
|
||||
component.Groups[autoGroupProto.GroupId] = group;
|
||||
}
|
||||
|
||||
if (!group.Members.Contains(userId))
|
||||
{
|
||||
group.Members.Add(userId);
|
||||
|
||||
var timestamp = GetStationTime();
|
||||
var messageText = Loc.GetString("messenger-system-user-added", ("userName", userName));
|
||||
var systemMessage = new MessengerMessage("system", Loc.GetString("messenger-system-name"), messageText, timestamp, autoGroupProto.GroupId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(autoGroupProto.GroupId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[autoGroupProto.GroupId] = history;
|
||||
}
|
||||
history.Add(systemMessage);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
continue;
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
var messagePayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = "system",
|
||||
["sender_name"] = Loc.GetString("messenger-system-name"),
|
||||
["content"] = messageText,
|
||||
["timestamp"] = timestamp.TotalSeconds,
|
||||
["group_id"] = autoGroupProto.GroupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (memberId == userId)
|
||||
continue;
|
||||
|
||||
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == autoGroupProto.GroupId;
|
||||
|
||||
if (!isMemberChatOpen)
|
||||
{
|
||||
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
|
||||
{
|
||||
memberUnreads = new Dictionary<string, int>();
|
||||
component.UnreadCounts[memberId] = memberUnreads;
|
||||
}
|
||||
memberUnreads.TryGetValue(autoGroupProto.GroupId, out var currentCount);
|
||||
memberUnreads[autoGroupProto.GroupId] = currentCount + 1;
|
||||
}
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, messagePayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, messagePayload);
|
||||
}
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUserAddedToGroup,
|
||||
["group_id"] = autoGroupProto.GroupId,
|
||||
["user_id"] = userId
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
if (memberId == userId)
|
||||
continue;
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Content.Server/_Sunrise/Messenger/MessengerServerSystem.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using System.Linq;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Система сервера мессенджера, обрабатывающая сообщения между КПК
|
||||
/// </summary>
|
||||
public sealed partial class MessengerServerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
|
||||
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServer = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
|
||||
private ISawmill Sawmill { get; set; } = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Sawmill = _logManager.GetSawmill("messenger.server");
|
||||
|
||||
SubscribeLocalEvent<MessengerServerComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
|
||||
SubscribeLocalEvent<MessengerServerComponent, DeviceNetServerConnectedEvent>(OnServerConnected);
|
||||
SubscribeLocalEvent<MessengerServerComponent, DeviceNetServerDisconnectedEvent>(OnServerDisconnected);
|
||||
SubscribeLocalEvent<MessengerServerComponent, RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||
}
|
||||
|
||||
private void OnRoundRestart(EntityUid uid, MessengerServerComponent component, RoundRestartCleanupEvent args)
|
||||
{
|
||||
component.Users.Clear();
|
||||
component.Groups.Clear();
|
||||
component.MessageHistory.Clear();
|
||||
component.GroupIdCounter = 0;
|
||||
}
|
||||
|
||||
private void OnServerConnected(EntityUid uid, MessengerServerComponent component, ref DeviceNetServerConnectedEvent args)
|
||||
{
|
||||
Sawmill.Info($"Messenger server connected: {ToPrettyString(uid)}");
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
{
|
||||
Sawmill.Error($"Server DeviceNetwork component not found: {ToPrettyString(uid)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_deviceNetwork.IsDeviceConnected(uid, serverDevice))
|
||||
{
|
||||
if (!_deviceNetwork.ConnectDevice(uid, serverDevice))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CreateAutoGroups(component);
|
||||
|
||||
foreach (var user in component.Users.Values)
|
||||
{
|
||||
AddUserToAutoGroups(uid, component, user.UserId, user.Name, user.DepartmentId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServerDisconnected(EntityUid uid, MessengerServerComponent component, ref DeviceNetServerDisconnectedEvent args)
|
||||
{
|
||||
}
|
||||
|
||||
private void OnPacketReceived(EntityUid uid, MessengerServerComponent component, DeviceNetworkPacketEvent args)
|
||||
{
|
||||
if (!_singletonServer.IsActiveServer(uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case MessengerCommands.CmdRegisterUser:
|
||||
HandleRegisterUser(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdSendMessage:
|
||||
HandleSendMessage(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdCreateGroup:
|
||||
HandleCreateGroup(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdAddToGroup:
|
||||
HandleAddToGroup(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdRemoveFromGroup:
|
||||
HandleRemoveFromGroup(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdGetUsers:
|
||||
HandleGetUsers(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdGetGroups:
|
||||
HandleGetGroups(uid, component, args);
|
||||
break;
|
||||
case MessengerCommands.CmdGetMessages:
|
||||
HandleGetMessages(uid, component, args);
|
||||
break;
|
||||
default:
|
||||
Sawmill.Warning($"Unknown command received: {command} from {args.SenderAddress}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Генерирует ID для личного чата между двумя пользователями
|
||||
/// </summary>
|
||||
private string GetPersonalChatId(string userId1, string userId2)
|
||||
{
|
||||
var ids = new[] { userId1, userId2 }.OrderBy(x => x).ToArray();
|
||||
return $"personal_{ids[0]}_{ids[1]}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ограничивает историю сообщений до указанного количества
|
||||
/// </summary>
|
||||
private void TrimMessageHistory(List<MessengerMessage> history, int maxCount)
|
||||
{
|
||||
if (history.Count > maxCount)
|
||||
{
|
||||
var toRemove = history.Count - maxCount;
|
||||
history.RemoveRange(0, toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает время станции (обычное время, как в КПК)
|
||||
/// </summary>
|
||||
private TimeSpan GetStationTime()
|
||||
{
|
||||
return (DateTime.UtcNow + TimeSpan.FromHours(3)).TimeOfDay;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
using Content.Shared.CartridgeLoader;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Событие сообщения UI мессенджера
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MessengerUiMessageEvent : CartridgeMessageEvent
|
||||
{
|
||||
public readonly MessengerUiAction Action;
|
||||
public readonly string? RecipientId;
|
||||
public readonly string? GroupId;
|
||||
public readonly string? Content;
|
||||
public readonly string? GroupName;
|
||||
public readonly string? UserId;
|
||||
public readonly string? ChatId;
|
||||
public readonly bool? IsMuted;
|
||||
|
||||
public MessengerUiMessageEvent(
|
||||
MessengerUiAction action,
|
||||
string? recipientId = null,
|
||||
string? groupId = null,
|
||||
string? content = null,
|
||||
string? groupName = null,
|
||||
string? userId = null,
|
||||
string? chatId = null,
|
||||
bool? isMuted = null)
|
||||
{
|
||||
Action = action;
|
||||
RecipientId = recipientId;
|
||||
GroupId = groupId;
|
||||
Content = content;
|
||||
GroupName = groupName;
|
||||
UserId = userId;
|
||||
ChatId = chatId;
|
||||
IsMuted = isMuted;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Действия UI мессенджера
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum MessengerUiAction
|
||||
{
|
||||
SendMessage,
|
||||
CreateGroup,
|
||||
AddToGroup,
|
||||
RemoveFromGroup,
|
||||
RequestUsers,
|
||||
RequestGroups,
|
||||
RequestMessages,
|
||||
ToggleMute
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using Content.Shared._Sunrise.Messenger;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние UI мессенджера
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MessengerUiState : BoundUserInterfaceState
|
||||
{
|
||||
/// <summary>
|
||||
/// Зарегистрирован ли пользователь
|
||||
/// </summary>
|
||||
public bool IsRegistered { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Доступен ли сервер
|
||||
/// </summary>
|
||||
public bool ServerAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ID текущего пользователя
|
||||
/// </summary>
|
||||
public string? CurrentUserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Список пользователей
|
||||
/// </summary>
|
||||
public List<MessengerUser> Users { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Список групп
|
||||
/// </summary>
|
||||
public List<MessengerGroup> Groups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// История сообщений по чатам
|
||||
/// </summary>
|
||||
public Dictionary<string, List<MessengerMessage>> MessageHistory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Заглушенные личные чаты (chatId)
|
||||
/// </summary>
|
||||
public HashSet<string> MutedPersonalChats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Заглушенные групповые чаты (groupId)
|
||||
/// </summary>
|
||||
public HashSet<string> MutedGroupChats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Количество непрочитанных сообщений по чатам (chatId -> количество)
|
||||
/// </summary>
|
||||
public Dictionary<string, int> UnreadCounts { get; }
|
||||
|
||||
public MessengerUiState(
|
||||
bool isRegistered,
|
||||
bool serverAvailable,
|
||||
string? currentUserId,
|
||||
List<MessengerUser> users,
|
||||
List<MessengerGroup> groups,
|
||||
Dictionary<string, List<MessengerMessage>> messageHistory,
|
||||
HashSet<string> mutedPersonalChats,
|
||||
HashSet<string> mutedGroupChats,
|
||||
Dictionary<string, int> unreadCounts)
|
||||
{
|
||||
IsRegistered = isRegistered;
|
||||
ServerAvailable = serverAvailable;
|
||||
CurrentUserId = currentUserId;
|
||||
Users = users;
|
||||
Groups = groups;
|
||||
MessageHistory = messageHistory;
|
||||
MutedPersonalChats = mutedPersonalChats;
|
||||
MutedGroupChats = mutedGroupChats;
|
||||
UnreadCounts = unreadCounts;
|
||||
}
|
||||
}
|
||||
31
Content.Shared/_Sunrise/Messenger/EmojiPrototype.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Прототип эмодзи для мессенджера
|
||||
/// </summary>
|
||||
[Prototype("emoji")]
|
||||
public sealed partial class EmojiPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Короткий код эмодзи (например, ":smile:")
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string Code { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Путь к спрайту эмодзи
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string SpritePath { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние спрайта
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string SpriteState { get; private set; } = default!;
|
||||
}
|
||||
36
Content.Shared/_Sunrise/Messenger/EmojiSystem.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Система для работы с эмодзи в мессенджере
|
||||
/// </summary>
|
||||
public abstract class EmojiSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Парсит текст сообщения и заменяет коды эмодзи на их представление в формате для RichTextLabel
|
||||
/// </summary>
|
||||
public string ParseEmojis(string text)
|
||||
{
|
||||
foreach (var emoji in PrototypeManager.EnumeratePrototypes<EmojiPrototype>())
|
||||
{
|
||||
var escapedCode = Regex.Escape(emoji.Code);
|
||||
var pattern = $@"(?<![a-zA-Z0-9_]){escapedCode}(?![a-zA-Z0-9_])";
|
||||
var replacement = $@"[emoji id=""{emoji.ID}""]";
|
||||
text = Regex.Replace(text, pattern, replacement, RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает все эмодзи
|
||||
/// </summary>
|
||||
public IEnumerable<EmojiPrototype> GetAllEmojis()
|
||||
{
|
||||
return PrototypeManager.EnumeratePrototypes<EmojiPrototype>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Прототип автоматической группы мессенджера (департаменты, общий чат и т.д.)
|
||||
/// </summary>
|
||||
[Prototype("messengerAutoGroup")]
|
||||
public sealed partial class MessengerAutoGroupPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Название группы (LocId)
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public LocId Name { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Уникальный ID группы (используется для идентификации в системе)
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string GroupId { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Если true, добавляет всех пользователей автоматически
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AddAllUsers { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Список департаментов, пользователи которых будут автоматически добавлены в группу
|
||||
/// Если пусто и AddAllUsers = false, группа не будет создана автоматически
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<DepartmentPrototype>> Departments { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Можно ли добавлять/удалять участников вручную
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AllowManualMemberManagement { get; private set; } = false;
|
||||
}
|
||||
50
Content.Shared/_Sunrise/Messenger/MessengerGroup.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Информация о группе в мессенджере
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MessengerGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный ID группы
|
||||
/// </summary>
|
||||
public string GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Название группы
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID участников группы
|
||||
/// </summary>
|
||||
public HashSet<string> Members { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Тип группы
|
||||
/// </summary>
|
||||
public MessengerGroupType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID прототипа автоматической группы (null для пользовательских групп)
|
||||
/// </summary>
|
||||
public string? AutoGroupPrototypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID владельца группы (создателя). null для автоматических групп
|
||||
/// </summary>
|
||||
public string? OwnerId { get; set; }
|
||||
|
||||
public MessengerGroup(string groupId, string name, HashSet<string> members, MessengerGroupType type = MessengerGroupType.UserCreated, string? autoGroupPrototypeId = null, string? ownerId = null)
|
||||
{
|
||||
GroupId = groupId;
|
||||
Name = name;
|
||||
Members = members;
|
||||
Type = type;
|
||||
AutoGroupPrototypeId = autoGroupPrototypeId;
|
||||
OwnerId = ownerId;
|
||||
}
|
||||
}
|
||||
20
Content.Shared/_Sunrise/Messenger/MessengerGroupType.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Тип группы в мессенджере
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum MessengerGroupType
|
||||
{
|
||||
/// <summary>
|
||||
/// Пользовательская группа, созданная игроком
|
||||
/// </summary>
|
||||
UserCreated,
|
||||
|
||||
/// <summary>
|
||||
/// Автоматическая группа (департамент, общий чат и т.д.)
|
||||
/// </summary>
|
||||
Automatic
|
||||
}
|
||||
56
Content.Shared/_Sunrise/Messenger/MessengerMessage.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение в мессенджере
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MessengerMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// ID отправителя
|
||||
/// </summary>
|
||||
public string SenderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя отправителя
|
||||
/// </summary>
|
||||
public string SenderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Текст сообщения
|
||||
/// </summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Время отправки (относительно начала раунда)
|
||||
/// </summary>
|
||||
public TimeSpan Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID группы (null для личных сообщений)
|
||||
/// </summary>
|
||||
public string? GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID получателя (null для групповых сообщений)
|
||||
/// </summary>
|
||||
public string? RecipientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Прочитано ли сообщение получателем (только для личных сообщений)
|
||||
/// </summary>
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
public MessengerMessage(string senderId, string senderName, string content, TimeSpan timestamp, string? groupId = null, string? recipientId = null, bool isRead = false)
|
||||
{
|
||||
SenderId = senderId;
|
||||
SenderName = senderName;
|
||||
Content = content;
|
||||
Timestamp = timestamp;
|
||||
GroupId = groupId;
|
||||
RecipientId = recipientId;
|
||||
IsRead = isRead;
|
||||
}
|
||||
}
|
||||
38
Content.Shared/_Sunrise/Messenger/MessengerUser.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Информация о пользователе мессенджера
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MessengerUser
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный ID пользователя (адрес DeviceNetwork КПК)
|
||||
/// </summary>
|
||||
public string UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя пользователя
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Должность пользователя (опционально)
|
||||
/// </summary>
|
||||
public string? JobTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID отдела пользователя (опционально)
|
||||
/// </summary>
|
||||
public string? DepartmentId { get; set; }
|
||||
|
||||
public MessengerUser(string userId, string name, string? jobTitle = null, string? departmentId = null)
|
||||
{
|
||||
UserId = userId;
|
||||
Name = name;
|
||||
JobTitle = jobTitle;
|
||||
DepartmentId = departmentId;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Robust.Shared;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Shared._Sunrise.SunriseCCVars;
|
||||
|
|
@ -574,4 +574,20 @@ public sealed partial class SunriseCCVars : CVars
|
|||
|
||||
public static readonly CVarDef<int> GameIPBlockingUnhandledMessageRateLimit =
|
||||
CVarDef.Create("game.ipblocking_unhandled_message_rate_limit", 10, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Messenger Emoji
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Недавно использованные смайлики в мессенджере (разделены запятыми, максимум 5).
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> MessengerRecentEmojis =
|
||||
CVarDef.Create("messenger.recent_emojis", "", CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Избранные смайлики в мессенджере (разделены запятыми).
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> MessengerFavoriteEmojis =
|
||||
CVarDef.Create("messenger.favorite_emojis", "", CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,3 +18,5 @@ ent-AstroNavCartridge = AstroNav cartridge
|
|||
.desc = A program for navigation that provides GPS coordinates.
|
||||
ent-NavigatorCartridge = navigator cartridge
|
||||
.desc = A program for viewing the station map for navigation purposes.
|
||||
ent-MessengerCartridge = messenger cartridge
|
||||
.desc = A program for messaging between PDAs.
|
||||
|
|
@ -62,6 +62,8 @@ ent-MedicalScannerMachineCircuitboard = medical scanner machine board
|
|||
.desc = A machine printed circuit board for a medical scanner.
|
||||
ent-CrewMonitoringServerMachineCircuitboard = crew monitoring server machine board
|
||||
.desc = A machine printed circuit board for a crew monitoring server.
|
||||
ent-MessengerServerMachineCircuitboard = messenger server machine board
|
||||
.desc = A machine printed circuit board for a messenger server.
|
||||
ent-CryoPodMachineCircuitboard = cryo pod machine board
|
||||
.desc = A machine printed circuit board for a cryo pod.
|
||||
ent-ChemMasterMachineCircuitboard = ChemMaster 4000 machine board
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
ent-MessengerServer = messenger server
|
||||
.desc = Server that handles messaging between PDAs on the station.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
messenger-auto-group-common-name = Общий
|
||||
department-engineering = Инженерный
|
||||
department-security = Служба безопастности
|
||||
department-medical = Медицинский
|
||||
department-science = РНД
|
||||
department-cargo = Карго
|
||||
department-civilian = Гражданские
|
||||
department-command = Командный
|
||||
|
|
@ -6,6 +6,7 @@ nano-task-program-name = NanoTask
|
|||
news-read-program-name = Station news
|
||||
|
||||
crew-manifest-program-name = Crew manifest
|
||||
messenger-program-name = Messenger MAX
|
||||
crew-manifest-cartridge-loading = Loading ...
|
||||
|
||||
net-probe-program-name = NetProbe
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
messenger-status-connecting = Connecting...
|
||||
messenger-status-disconnected = Server unavailable
|
||||
messenger-status-connected = Connected
|
||||
messenger-search-placeholder = Search...
|
||||
messenger-tab-personal = Personal
|
||||
messenger-tab-groups = Groups
|
||||
messenger-create-group-button = Create group
|
||||
messenger-chat-select = Select chat
|
||||
messenger-mute-tooltip = Mute
|
||||
messenger-members-toggle-show = Show members
|
||||
messenger-members-toggle-hide = Hide members
|
||||
messenger-emoji-button-tooltip = Select emojis
|
||||
messenger-message-placeholder = Enter message...
|
||||
messenger-send-button = Send
|
||||
messenger-members-header = Members:
|
||||
messenger-member-owner = { $name } (Owner)
|
||||
messenger-remove-member-tooltip = Remove from group
|
||||
messenger-no-members = No members
|
||||
messenger-add-member-button = Add
|
||||
messenger-create-group-title = Create group
|
||||
messenger-create-group-label = Group name:
|
||||
messenger-create-group-placeholder = Enter name
|
||||
messenger-create-group-button-create = Create
|
||||
messenger-create-group-button-cancel = Cancel
|
||||
messenger-add-user-title = Add member
|
||||
messenger-add-user-to-group-title = Add member to { $groupName }
|
||||
messenger-no-users-available = No users available to add
|
||||
messenger-add-user-search = Search:
|
||||
messenger-add-user-placeholder = Enter user name
|
||||
messenger-add-user-cancel = Cancel
|
||||
messenger-emoji-picker-title = Select emojis
|
||||
messenger-connection-label = { $status ->
|
||||
[connecting] { messenger-status-connecting }
|
||||
[disconnected] { messenger-status-disconnected }
|
||||
*[connected] { messenger-status-connected }
|
||||
}
|
||||
messenger-system-user-added = added { $userName } to the group
|
||||
messenger-system-user-removed = removed { $userName } from the group
|
||||
messenger-system-user-added-by = { $adderName } added { $userName } to the group
|
||||
messenger-system-user-removed-by = { $removerName } removed { $userName } from the group
|
||||
messenger-emoji-recent-title = Recently used
|
||||
messenger-emoji-recent-empty-hint = Recently used emojis will appear here
|
||||
messenger-emoji-favorite-title = Favorites
|
||||
messenger-emoji-favorite-hint = ПКМ по общему списку для добавления.
|
||||
ПКМ по избраном для удаления.
|
||||
messenger-emoji-all-title = All emojis
|
||||
messenger-user-unknown = Unknown
|
||||
messenger-system-name = System
|
||||
|
|
@ -6,6 +6,7 @@ device-frequency-prototype-name-lights = Smart Lights
|
|||
device-frequency-prototype-name-mailing-units = Mailing Units
|
||||
device-frequency-prototype-name-pdas = PDAs
|
||||
device-frequency-prototype-name-fax = Fax
|
||||
device-frequency-prototype-name-messenger = Messenger
|
||||
device-frequency-prototype-name-basic-device = Basic Devices
|
||||
device-frequency-prototype-name-cyborg-control = Cyborg Control
|
||||
device-frequency-prototype-name-robotics-console = Robotics Console
|
||||
|
|
|
|||
|
|
@ -18,3 +18,5 @@ ent-AstroNavCartridge = Картридж АстроНав
|
|||
.desc = Программа для навигации, предоставляющая GPS-координаты.
|
||||
ent-NavigatorCartridge = картридж навигатора
|
||||
.desc = Программа для просмотра карты станции в целях навигации.
|
||||
ent-MessengerCartridge = картридж мессенджера
|
||||
.desc = Программа для обмена сообщениями между КПК.
|
||||
|
|
@ -62,6 +62,8 @@ ent-MedicalScannerMachineCircuitboard = медицинский сканер (м
|
|||
.desc = { ent-BaseMachineCircuitboard.desc }
|
||||
ent-CrewMonitoringServerMachineCircuitboard = сервер мониторинга экипажа (машинная плата)
|
||||
.desc = Печатная плата для сервера мониторинга экипажа.
|
||||
ent-MessengerServerMachineCircuitboard = сервер мессенджера (машинная плата)
|
||||
.desc = Печатная плата для сервера мессенджера.
|
||||
ent-CryoPodMachineCircuitboard = криокапсула (машинная плата)
|
||||
.desc = Печатная плата для криокапсулы.
|
||||
ent-ChemMasterMachineCircuitboard = химмастер 4000 (машинная плата)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
ent-MessengerServer = сервер мессенджера
|
||||
.desc = Сервер, обрабатывающий сообщения между КПК на станции.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
messenger-auto-group-common-name = Общий
|
||||
department-engineering = Инженерный
|
||||
department-security = Служба безопастности
|
||||
department-medical = Медицинский
|
||||
department-science = РНД
|
||||
department-cargo = Карго
|
||||
department-civilian = Гражданские
|
||||
department-command = Командный
|
||||
|
|
@ -4,6 +4,7 @@ notekeeper-program-name = Заметки
|
|||
nano-task-program-name = NanoTask
|
||||
news-read-program-name = Новости станции
|
||||
crew-manifest-program-name = Манифест экипажа
|
||||
messenger-program-name = Мессенджер МАХ
|
||||
crew-manifest-cartridge-loading = Загрузка...
|
||||
net-probe-program-name = Зонд сетей
|
||||
net-probe-scan = Просканирован { $device }!
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
messenger-status-connecting = Подключение...
|
||||
messenger-status-disconnected = Сервер недоступен
|
||||
messenger-status-connected = Подключено
|
||||
messenger-search-placeholder = Поиск...
|
||||
messenger-tab-personal = Личные
|
||||
messenger-tab-groups = Группы
|
||||
messenger-create-group-button = Создать группу
|
||||
messenger-chat-select = Выберите чат
|
||||
messenger-mute-tooltip = Отключить звук
|
||||
messenger-members-toggle-show = Показать участников
|
||||
messenger-members-toggle-hide = Скрыть участников
|
||||
messenger-emoji-button-tooltip = Выбрать смайлики
|
||||
messenger-message-placeholder = Введите сообщение...
|
||||
messenger-send-button = Отправить
|
||||
messenger-members-header = Участники:
|
||||
messenger-member-owner = { $name } (Владелец)
|
||||
messenger-remove-member-tooltip = Удалить из группы
|
||||
messenger-no-members = Нет участников
|
||||
messenger-add-member-button = Добавить
|
||||
messenger-create-group-title = Создать группу
|
||||
messenger-create-group-label = Название группы:
|
||||
messenger-create-group-placeholder = Введите название
|
||||
messenger-create-group-button-create = Создать
|
||||
messenger-create-group-button-cancel = Отмена
|
||||
messenger-add-user-title = Добавить участника
|
||||
messenger-add-user-to-group-title = Добавить участника в { $groupName }
|
||||
messenger-no-users-available = Нет доступных пользователей для добавления
|
||||
messenger-add-user-search = Поиск:
|
||||
messenger-add-user-placeholder = Введите имя пользователя
|
||||
messenger-add-user-cancel = Отмена
|
||||
messenger-emoji-picker-title = Выбрать смайлики
|
||||
messenger-connection-label = { $status ->
|
||||
[connecting] { messenger-status-connecting }
|
||||
[disconnected] { messenger-status-disconnected }
|
||||
*[connected] { messenger-status-connected }
|
||||
}
|
||||
messenger-system-user-added = добавил(а) { $userName } в группу
|
||||
messenger-system-user-removed = удалил(а) { $userName } из группы
|
||||
messenger-system-user-added-by = { $adderName } добавил(а) { $userName } в группу
|
||||
messenger-system-user-removed-by = { $removerName } удалил(а) { $userName } из группы
|
||||
messenger-emoji-recent-title = Недавно использованные
|
||||
messenger-emoji-recent-empty-hint = Здесь будут находиться недавно использованные смайлики
|
||||
messenger-emoji-favorite-title = Избранные
|
||||
messenger-emoji-favorite-hint = ПКМ по общему списку для добавления.
|
||||
ПКМ по избранным для удаления.
|
||||
messenger-emoji-all-title = Все смайлики
|
||||
messenger-user-unknown = Неизвестно
|
||||
messenger-system-name = Система
|
||||
|
|
@ -6,6 +6,7 @@ device-frequency-prototype-name-lights = Умное освещение
|
|||
device-frequency-prototype-name-mailing-units = Почтовый блок
|
||||
device-frequency-prototype-name-pdas = КПК
|
||||
device-frequency-prototype-name-fax = Факс
|
||||
device-frequency-prototype-name-messenger = Мессенджер
|
||||
device-frequency-prototype-name-basic-device = Базовые устройства
|
||||
device-frequency-prototype-name-cyborg-control = Управление киборгами
|
||||
device-frequency-prototype-name-robotics-console = Консоль управления робототехникой
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ meta:
|
|||
engineVersion: 270.0.0
|
||||
forkId: ""
|
||||
forkVersion: ""
|
||||
time: 12/30/2025 22:44:48
|
||||
time: 01/16/2026 21:15:11
|
||||
entityCount: 3153
|
||||
maps:
|
||||
- 23
|
||||
|
|
@ -12511,6 +12511,13 @@ entities:
|
|||
- type: Transform
|
||||
pos: 70.5,41
|
||||
parent: 1
|
||||
- proto: MessengerServer
|
||||
entities:
|
||||
- uid: 1911
|
||||
components:
|
||||
- type: Transform
|
||||
pos: 46.5,65.5
|
||||
parent: 1
|
||||
- proto: MetalFoamGrenade
|
||||
entities:
|
||||
- uid: 2192
|
||||
|
|
@ -15837,15 +15844,6 @@ entities:
|
|||
parent: 1
|
||||
- type: Fixtures
|
||||
fixtures: {}
|
||||
- proto: StationMapBroken
|
||||
entities:
|
||||
- uid: 1911
|
||||
components:
|
||||
- type: Transform
|
||||
pos: 46.5,65.5
|
||||
parent: 1
|
||||
- type: Fixtures
|
||||
fixtures: {}
|
||||
- proto: Stunbaton
|
||||
entities:
|
||||
- uid: 2683
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
id: Turret
|
||||
name: device-frequency-prototype-name-turret
|
||||
frequency: 2152
|
||||
|
||||
|
||||
# AI turret controllers send data to their turrets on this frequency
|
||||
- type: deviceFrequency
|
||||
id: TurretControlAI
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- type: entity
|
||||
- type: entity
|
||||
id: AutolatheMachineCircuitboard
|
||||
parent: BaseMachineCircuitboard
|
||||
name: autolathe machine board
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- type: entity
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: BasePDACartridge
|
||||
abstract: true
|
||||
|
|
@ -173,24 +173,3 @@
|
|||
sprite: Objects/Devices/gps.rsi
|
||||
state: icon
|
||||
- type: AstroNavCartridge
|
||||
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: NavigatorCartridge
|
||||
name: navigator cartridge
|
||||
description: A program for viewing the station map for navigation purposes.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Devices/cartridge.rsi
|
||||
state: cart-nav
|
||||
- type: Icon
|
||||
sprite: Objects/Devices/cartridge.rsi
|
||||
state: cart-nav
|
||||
- type: UIFragment
|
||||
ui: !type:NavigatorUi
|
||||
- type: Cartridge
|
||||
programName: navigator-program-name
|
||||
icon:
|
||||
sprite: Structures/Wallmounts/signs.rsi
|
||||
state: space
|
||||
- type: NavigatorCartridge
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
cartridgeSlot:
|
||||
priority: -1
|
||||
name: device-pda-slot-component-slot-name-cartridge
|
||||
|
|
@ -165,6 +166,7 @@
|
|||
- NewsReaderCartridge
|
||||
- NavigatorCartridge
|
||||
- WantedListCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: BasePDA
|
||||
|
|
@ -180,6 +182,7 @@
|
|||
- NewsReaderCartridge
|
||||
- NavigatorCartridge
|
||||
- MedTekCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: BasePDA
|
||||
|
|
@ -352,6 +355,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- PlantAnalyzerCartridge
|
||||
- MessengerCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -534,6 +538,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: BasePDA
|
||||
|
|
@ -583,6 +588,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: BasePDA
|
||||
|
|
@ -1000,6 +1006,7 @@
|
|||
- NewsReaderCartridge
|
||||
- WantedListCartridge
|
||||
- LogProbeCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: BaseSecurityPDA
|
||||
|
|
@ -1082,6 +1089,7 @@
|
|||
- WantedListCartridge
|
||||
- LogProbeCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: CentcomPDA
|
||||
|
|
@ -1130,6 +1138,7 @@
|
|||
- WantedListCartridge
|
||||
- MedTekCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
- type: Tag # Ignore Chameleon tags
|
||||
tags:
|
||||
- DoorBumpOpener
|
||||
|
|
@ -1379,6 +1388,7 @@
|
|||
- WantedListCartridge
|
||||
- LogProbeCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: ERTLeaderPDA
|
||||
|
|
@ -1564,6 +1574,7 @@
|
|||
- NewsReaderCartridge
|
||||
- WantedListCartridge
|
||||
- LogProbeCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: BaseMedicalPDA
|
||||
|
|
@ -1594,6 +1605,7 @@
|
|||
- NewsReaderCartridge
|
||||
- WantedListCartridge
|
||||
- MedTekCartridge
|
||||
- MessengerCartridge # Sunrise-Edit
|
||||
|
||||
- type: entity
|
||||
parent: ClownPDA
|
||||
|
|
|
|||
|
|
@ -2,3 +2,8 @@
|
|||
id: SurveillanceCameraHandheld
|
||||
name: device-frequency-prototype-name-body-camera
|
||||
frequency: 1940
|
||||
|
||||
- type: deviceFrequency
|
||||
id: Messenger
|
||||
name: device-frequency-prototype-name-messenger
|
||||
frequency: 2203
|
||||
|
|
|
|||
|
|
@ -126,3 +126,15 @@
|
|||
stackRequirements:
|
||||
Manipulator: 4
|
||||
Steel: 5
|
||||
|
||||
- type: entity
|
||||
id: MessengerServerMachineCircuitboard
|
||||
parent: BaseMachineCircuitboard
|
||||
name: messenger server machine board
|
||||
description: A machine printed circuit board for a messenger server.
|
||||
components:
|
||||
- type: MachineBoard
|
||||
prototype: MessengerServer
|
||||
stackRequirements:
|
||||
Steel: 1
|
||||
Cable: 2
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- MedTekCartridge
|
||||
- MessengerCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-doctor
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- NetProbeCartridge
|
||||
- MessengerCartridge
|
||||
|
||||
- type: entity
|
||||
parent: PrisonEngineerPDA
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
- NotekeeperCartridge
|
||||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- MessengerCartridge
|
||||
|
||||
- type: entity
|
||||
parent: SecurityMetusPDA
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-pilot
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
- CrewManifestCartridge
|
||||
- NewsReaderCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
|
||||
- type: entity
|
||||
parent: PlanetPrisonerPDA
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-scientist
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
- NanoTaskCartridge
|
||||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-worker
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -16,3 +16,41 @@
|
|||
sprite: Objects/Specific/Hydroponics/plant_analyzer.rsi
|
||||
state: icon
|
||||
- type: PlantAnalyzerCartridge
|
||||
|
||||
- type: entity
|
||||
parent: BasePDACartridge
|
||||
id: NavigatorCartridge
|
||||
name: navigator cartridge
|
||||
description: A program for viewing the station map for navigation purposes.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Devices/cartridge.rsi
|
||||
state: cart-nav
|
||||
- type: Icon
|
||||
sprite: Objects/Devices/cartridge.rsi
|
||||
state: cart-nav
|
||||
- type: UIFragment
|
||||
ui: !type:NavigatorUi
|
||||
- type: Cartridge
|
||||
programName: navigator-program-name
|
||||
icon:
|
||||
sprite: Structures/Wallmounts/signs.rsi
|
||||
state: space
|
||||
- type: NavigatorCartridge
|
||||
|
||||
- type: entity
|
||||
parent: BasePDACartridge
|
||||
id: MessengerCartridge
|
||||
name: messenger cartridge
|
||||
description: A program for messaging between PDAs.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: cart-y
|
||||
- type: UIFragment
|
||||
ui: !type:MessengerUi
|
||||
- type: Cartridge
|
||||
programName: messenger-program-name
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Misc/program_icons.rsi
|
||||
state: max_logo2
|
||||
- type: MessengerCartridge
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@
|
|||
- NavigatorCartridge
|
||||
- WantedListCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge
|
||||
|
||||
- type: entity
|
||||
parent: CaptainPDA
|
||||
|
|
@ -422,6 +423,7 @@
|
|||
- NotekeeperCartridge
|
||||
- NewsReaderCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge
|
||||
|
||||
- type: entity
|
||||
parent: BasePDA
|
||||
|
|
|
|||
70
Resources/Prototypes/_Sunrise/Messenger/auto_groups.yml
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
- type: messengerAutoGroup
|
||||
id: CommonChat
|
||||
name: messenger-auto-group-common-name
|
||||
groupId: common
|
||||
addAllUsers: true
|
||||
departments: []
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentEngineering
|
||||
name: department-engineering
|
||||
groupId: dept_engineering
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Engineering
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentSecurity
|
||||
name: department-security
|
||||
groupId: dept_security
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Security
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentMedical
|
||||
name: department-medical
|
||||
groupId: dept_medical
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Medical
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentScience
|
||||
name: department-science
|
||||
groupId: dept_science
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Science
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentCargo
|
||||
name: department-cargo
|
||||
groupId: dept_cargo
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Cargo
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentCivilian
|
||||
name: department-civilian
|
||||
groupId: dept_civilian
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Civilian
|
||||
allowManualMemberManagement: false
|
||||
|
||||
- type: messengerAutoGroup
|
||||
id: DepartmentCommand
|
||||
name: department-command
|
||||
groupId: dept_command
|
||||
addAllUsers: false
|
||||
departments:
|
||||
- Command
|
||||
allowManualMemberManagement: false
|
||||
1703
Resources/Prototypes/_Sunrise/Messenger/smiles.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
- type: entity
|
||||
id: MessengerServer
|
||||
parent: [ BaseMachinePowered, ConstructibleMachine ]
|
||||
name: messenger server
|
||||
description: Server that handles messaging between PDAs on the station.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Structures/Machines/server.rsi
|
||||
snapCardinals: true
|
||||
layers:
|
||||
- state: server-off
|
||||
- state: server-on
|
||||
visible: false
|
||||
map: [ "enum.PowerDeviceVisualLayers.Powered" ]
|
||||
- state: variant-research
|
||||
- state: server_o
|
||||
map: ["enum.WiresVisualLayers.MaintenancePanel"]
|
||||
- type: Construction
|
||||
graph: Machine
|
||||
node: machine
|
||||
containers:
|
||||
- machine_board
|
||||
- machine_parts
|
||||
- type: Machine
|
||||
board: MessengerServerMachineCircuitboard
|
||||
- type: ContainerContainer
|
||||
containers:
|
||||
machine_board: !type:Container
|
||||
machine_parts: !type:Container
|
||||
- type: MessengerServer
|
||||
maxMessageHistory: 100
|
||||
- type: SingletonDeviceNetServer
|
||||
- type: DeviceNetwork
|
||||
deviceNetId: Wireless
|
||||
transmitFrequencyId: Messenger
|
||||
receiveFrequencyId: Messenger
|
||||
autoConnect: false
|
||||
- type: WirelessNetworkConnection
|
||||
range: 500
|
||||
- type: StationLimitedNetwork
|
||||
allowNonStationPackets: true
|
||||
- type: ApcPowerReceiver
|
||||
powerLoad: 200
|
||||
- type: DeviceNetworkRequiresPower
|
||||
- type: ExtensionCableReceiver
|
||||
- type: WiresPanel
|
||||
- type: WiresVisuals
|
||||
- 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: MetalGlassBreak
|
||||
- !type:SpawnEntitiesBehavior
|
||||
spawn:
|
||||
SheetSteel1:
|
||||
min: 1
|
||||
max: 2
|
||||
- type: Appearance
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.PowerDeviceVisuals.Powered:
|
||||
enum.PowerDeviceVisualLayers.Powered:
|
||||
True: {visible: true}
|
||||
False: {visible: false}
|
||||
- type: AmbientOnPowered
|
||||
- type: AmbientSound
|
||||
volume: -9
|
||||
range: 5
|
||||
enabled: false
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/server_fans.ogg
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,760 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "",
|
||||
"size": {
|
||||
"x": 90,
|
||||
"y": 53
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "dash1",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.25,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.25,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.25,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.2,
|
||||
0.6,
|
||||
0.6,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.25
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dash3",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.2,
|
||||
0.6,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ded_snegurochka2",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.4,
|
||||
0.2,
|
||||
0.2,
|
||||
0.25,
|
||||
0.25,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.5,
|
||||
0.2,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "drinks",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.3,
|
||||
0.15,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.05,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.15,
|
||||
0.2,
|
||||
0.15,
|
||||
0.2,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.2,
|
||||
0.15,
|
||||
0.5,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "feminist",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.2,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.2,
|
||||
0.1,
|
||||
0.3
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "feminist_en",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.2,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.2,
|
||||
0.1,
|
||||
0.3
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "first_move",
|
||||
"delays": [
|
||||
[
|
||||
0.2,
|
||||
0.5,
|
||||
0.15,
|
||||
0.7,
|
||||
0.8,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "friends",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.5,
|
||||
0.15,
|
||||
0.5,
|
||||
0.12,
|
||||
0.5,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.15,
|
||||
0.7,
|
||||
0.2,
|
||||
0.5,
|
||||
0.12,
|
||||
1.0,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.7,
|
||||
0.3,
|
||||
1.0,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "morpheus",
|
||||
"delays": [
|
||||
[
|
||||
2.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
2.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
1.5,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
3.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "orc",
|
||||
"delays": [
|
||||
[
|
||||
0.3,
|
||||
0.7,
|
||||
0.2,
|
||||
0.6,
|
||||
0.3,
|
||||
0.5,
|
||||
0.2,
|
||||
0.5,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.2,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.1,
|
||||
0.1,
|
||||
0.8
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "party",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pooh",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.15,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.1,
|
||||
0.7,
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.1,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pooh_door",
|
||||
"delays": [
|
||||
[
|
||||
0.11,
|
||||
0.2,
|
||||
0.15,
|
||||
0.15,
|
||||
0.11,
|
||||
0.11,
|
||||
0.2,
|
||||
0.15,
|
||||
0.15,
|
||||
0.11,
|
||||
0.11,
|
||||
0.2,
|
||||
0.15,
|
||||
0.15,
|
||||
0.11,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.5,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.11
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "pooh_on_ball",
|
||||
"delays": [
|
||||
[
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11,
|
||||
0.11
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "santa2",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.4,
|
||||
0.2,
|
||||
0.2,
|
||||
0.25,
|
||||
0.25,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "take_example",
|
||||
"delays": [
|
||||
[
|
||||
0.2,
|
||||
0.2,
|
||||
0.5,
|
||||
0.1,
|
||||
0.18,
|
||||
0.15,
|
||||
0.18,
|
||||
0.15,
|
||||
0.18,
|
||||
0.15,
|
||||
0.1,
|
||||
0.1,
|
||||
0.18,
|
||||
0.1,
|
||||
0.18,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.18,
|
||||
0.1,
|
||||
0.18,
|
||||
0.18,
|
||||
0.18,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.15,
|
||||
0.1,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "to_babruysk",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.12,
|
||||
0.7,
|
||||
0.1,
|
||||
0.1,
|
||||
1.0
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "vampire",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.3,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
1.5,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.8,
|
||||
0.2,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.5,
|
||||
0.1,
|
||||
1.0,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 514 B |
|
After Width: | Height: | Size: 578 B |
|
After Width: | Height: | Size: 246 B |
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "discord: seraphimttt",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "max_logo1"
|
||||
},
|
||||
{
|
||||
"name": "max_logo2"
|
||||
},
|
||||
{
|
||||
"name": "max_logo3"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/_Sunrise/Interface/Misc/smiles.rsi/acute.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
Resources/Textures/_Sunrise/Interface/Misc/smiles.rsi/agree.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |