ыыыыы, месенжер макс, ихихяхя (#3789)

Co-authored-by: Daniel <77834935+Orvex07@users.noreply.github.com>
This commit is contained in:
Vigers Ray 2026-01-26 22:44:54 +03:00 committed by GitHub
parent 25ba4b89b9
commit 95d7d4d348
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
364 changed files with 17397 additions and 46 deletions

View file

@ -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"/>

View file

@ -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">

View file

@ -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;

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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();
};
}
}

View file

@ -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>

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
namespace Content.Client._Sunrise.Messenger;
/// <summary>
/// Клиентская реализация системы эмодзи
/// </summary>
public sealed class ClientEmojiSystem : Shared._Sunrise.Messenger.EmojiSystem
{
}

View 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;
}
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,8 @@
namespace Content.Server._Sunrise.Messenger;
/// <summary>
/// Серверная реализация системы эмодзи
/// </summary>
public sealed class ServerEmojiSystem : Content.Shared._Sunrise.Messenger.EmojiSystem
{
}

View 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";
}

View file

@ -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";
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}
}
}

View 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;
}
}

View file

@ -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
}

View file

@ -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;
}
}

View 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!;
}

View 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>();
}
}

View file

@ -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;
}

View 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;
}
}

View 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
}

View 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;
}
}

View 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;
}
}

View file

@ -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);
}

View file

@ -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.

View file

@ -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

View file

@ -0,0 +1,2 @@
ent-MessengerServer = messenger server
.desc = Server that handles messaging between PDAs on the station.

View file

@ -0,0 +1,8 @@
messenger-auto-group-common-name = Общий
department-engineering = Инженерный
department-security = Служба безопастности
department-medical = Медицинский
department-science = РНД
department-cargo = Карго
department-civilian = Гражданские
department-command = Командный

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -18,3 +18,5 @@ ent-AstroNavCartridge = Картридж АстроНав
.desc = Программа для навигации, предоставляющая GPS-координаты.
ent-NavigatorCartridge = картридж навигатора
.desc = Программа для просмотра карты станции в целях навигации.
ent-MessengerCartridge = картридж мессенджера
.desc = Программа для обмена сообщениями между КПК.

View file

@ -62,6 +62,8 @@ ent-MedicalScannerMachineCircuitboard = медицинский сканер (м
.desc = { ent-BaseMachineCircuitboard.desc }
ent-CrewMonitoringServerMachineCircuitboard = сервер мониторинга экипажа (машинная плата)
.desc = Печатная плата для сервера мониторинга экипажа.
ent-MessengerServerMachineCircuitboard = сервер мессенджера (машинная плата)
.desc = Печатная плата для сервера мессенджера.
ent-CryoPodMachineCircuitboard = криокапсула (машинная плата)
.desc = Печатная плата для криокапсулы.
ent-ChemMasterMachineCircuitboard = химмастер 4000 (машинная плата)

View file

@ -0,0 +1,2 @@
ent-MessengerServer = сервер мессенджера
.desc = Сервер, обрабатывающий сообщения между КПК на станции.

View file

@ -0,0 +1,8 @@
messenger-auto-group-common-name = Общий
department-engineering = Инженерный
department-security = Служба безопастности
department-medical = Медицинский
department-science = РНД
department-cargo = Карго
department-civilian = Гражданские
department-command = Командный

View file

@ -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 }!

View file

@ -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 = Система

View file

@ -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 = Консоль управления робототехникой

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,4 @@
- type: entity
- type: entity
id: AutolatheMachineCircuitboard
parent: BaseMachineCircuitboard
name: autolathe machine board

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -24,6 +24,7 @@
- NanoTaskCartridge
- NewsReaderCartridge
- MedTekCartridge
- MessengerCartridge
- type: PdaAnimationVisuals
animatedState: pda-space-prison-doctor
idInsertedLayerState: id_inserted-space-prison

View file

@ -19,6 +19,7 @@
- NewsReaderCartridge
- AstroNavCartridge
- NetProbeCartridge
- MessengerCartridge
- type: entity
parent: PrisonEngineerPDA

View file

@ -25,6 +25,7 @@
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- MessengerCartridge
- type: entity
parent: SecurityMetusPDA

View file

@ -29,6 +29,7 @@
- NanoTaskCartridge
- NewsReaderCartridge
- AstroNavCartridge
- MessengerCartridge
- type: PdaAnimationVisuals
animatedState: pda-space-prison-pilot
idInsertedLayerState: id_inserted-space-prison

View file

@ -36,6 +36,7 @@
- CrewManifestCartridge
- NewsReaderCartridge
- NavigatorCartridge
- MessengerCartridge
- type: entity
parent: PlanetPrisonerPDA

View file

@ -24,6 +24,7 @@
- NanoTaskCartridge
- NewsReaderCartridge
- AstroNavCartridge
- MessengerCartridge
- type: PdaAnimationVisuals
animatedState: pda-space-prison-scientist
idInsertedLayerState: id_inserted-space-prison

View file

@ -24,6 +24,7 @@
- NanoTaskCartridge
- NewsReaderCartridge
- AstroNavCartridge
- MessengerCartridge
- type: PdaAnimationVisuals
animatedState: pda-space-prison-worker
idInsertedLayerState: id_inserted-space-prison

View file

@ -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

View file

@ -186,6 +186,7 @@
- NavigatorCartridge
- WantedListCartridge
- AstroNavCartridge
- MessengerCartridge
- type: entity
parent: CaptainPDA
@ -422,6 +423,7 @@
- NotekeeperCartridge
- NewsReaderCartridge
- AstroNavCartridge
- MessengerCartridge
- type: entity
parent: BasePDA

View 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

File diff suppressed because it is too large Load diff

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

View file

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Some files were not shown because too many files have changed in this diff Show more