diff --git a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml index 1a6110da59..1071494fe4 100644 --- a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml +++ b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml @@ -40,23 +40,34 @@ StyleClasses="LabelSubText" /> - + - - + Visible="False" + Margin="0,4,0,0"> + + + + + + diff --git a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml.cs b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml.cs index a732ef3e89..a130ec292c 100644 --- a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml.cs +++ b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessagePanel.xaml.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Client._Sunrise.Messenger; using Content.Client.Resources; using Content.Client.Stylesheets; @@ -8,6 +9,7 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.RichText; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; @@ -21,6 +23,7 @@ public sealed partial class MessagePanel : PanelContainer [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly NetTexturesManager _netTexturesManager = default!; private ClientEmojiSystem? EmojiSystem => _entitySystemManager.GetEntitySystemOrNull(); private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem(); @@ -45,10 +48,36 @@ public sealed partial class MessagePanel : PanelContainer { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); + _netTexturesManager.ResourceLoaded += OnResourceLoaded; + ImageButton.OnPressed += _ => ShowFullImage(); + + ImageBorder.PanelOverride = new StyleBoxFlat + { + BackgroundColor = Color.Transparent, + BorderColor = Color.Transparent, + BorderThickness = new Thickness(2) + }; + + ImageButton.OnMouseEntered += _ => + { + if (ImageBorder.PanelOverride is StyleBoxFlat style) + { + style.BorderColor = Color.White.WithAlpha(0.6f); + } + }; + + ImageButton.OnMouseExited += _ => + { + if (ImageBorder.PanelOverride is StyleBoxFlat style) + { + style.BorderColor = Color.Transparent; + } + }; } public event Action? OnDeleteMessage; private Button? _deleteButton; + private string? _currentImagePath; public void UpdateMessage(MessengerMessage message, bool isOwnMessage, bool isPersonalChat, string? currentUserId) { @@ -60,6 +89,7 @@ public sealed partial class MessagePanel : PanelContainer var parsedContent = EmojiSystem?.ParseEmojis(message.Content) ?? message.Content; ContentLabel.SetMessage(FormattedMessage.FromMarkupPermissive(parsedContent), MessageTagsAllowed); + ContentLabel.Visible = !string.IsNullOrWhiteSpace(message.Content); if (isOwnMessage && _deleteButton == null) { @@ -116,6 +146,83 @@ public sealed partial class MessagePanel : PanelContainer { ReadStatusIcon.Visible = false; } + + UpdateImagePreview(message.ImagePath); + } + + private void UpdateImagePreview(string? imagePath) + { + if (string.IsNullOrWhiteSpace(imagePath)) + { + ImageButton.Visible = false; + _currentImagePath = null; + return; + } + + _currentImagePath = imagePath; + + var isAvailable = _netTexturesManager.EnsureResource(imagePath); + + if (isAvailable) + { + LoadImageTexture(imagePath); + } + else + { + ImageButton.Visible = false; + } + } + + private void ShowFullImage() + { + if (ImagePreview.Texture == null) + return; + + var window = new DefaultWindow + { + Title = Loc.GetString("messenger-image-preview-title"), + MinSize = new Vector2(600, 600) + }; + + var textureRect = new TextureRect + { + Texture = ImagePreview.Texture, + Stretch = TextureRect.StretchMode.KeepAspect, + HorizontalExpand = true, + VerticalExpand = true + }; + + window.Contents.AddChild(textureRect); + window.OpenCentered(); + } + + private void OnResourceLoaded(string resourcePath) + { + if (_currentImagePath == resourcePath) + { + LoadImageTexture(resourcePath); + } + } + + private void LoadImageTexture(string imagePath) + { + try + { + var uploadedPath = _netTexturesManager.GetUploadedPath(imagePath); + if (_resourceCache.TryGetResource(uploadedPath, out var textureResource)) + { + ImagePreview.Texture = textureResource.Texture; + ImageButton.Visible = true; + } + else + { + ImageButton.Visible = false; + } + } + catch + { + ImageButton.Visible = false; + } } /// diff --git a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUi.cs b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUi.cs index d16160847c..b904cb49d1 100644 --- a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUi.cs +++ b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUi.cs @@ -17,8 +17,8 @@ public sealed partial class MessengerUi : UIFragment 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.OnSendMessage += (recipientId, groupId, content, imagePath) => + SendMessengerMessage(MessengerUiAction.SendMessage, userInterface, recipientId: recipientId, groupId: groupId, content: content, imagePath: imagePath); _fragment.OnCreateGroup += (groupName) => SendMessengerMessage(MessengerUiAction.CreateGroup, userInterface, groupName: groupName); _fragment.OnAddToGroup += (groupId, userId) => @@ -39,6 +39,8 @@ public sealed partial class MessengerUi : UIFragment SendMessengerMessage(MessengerUiAction.DeleteMessage, userInterface, chatId: chatId, messageId: messageId); _fragment.OnTogglePin += (chatId) => SendMessengerMessage(MessengerUiAction.TogglePin, userInterface, chatId: chatId); + _fragment.OnRequestPhotos += () => + SendMessengerMessage(MessengerUiAction.RequestPhotos, userInterface); } public override void UpdateState(BoundUserInterfaceState state) @@ -59,9 +61,10 @@ public sealed partial class MessengerUi : UIFragment string? userId = null, string? chatId = null, bool? isMuted = null, - long? messageId = null) + long? messageId = null, + string? imagePath = null) { - var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted, messageId); + var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted, messageId, imagePath); var message = new CartridgeUiMessage(messengerMessage); userInterface.SendMessage(message); } diff --git a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUiFragment.xaml b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUiFragment.xaml index e6d9920aee..992c5c8fa4 100644 --- a/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUiFragment.xaml +++ b/Content.Client/_Sunrise/CartridgeLoader/Cartridges/MessengerUiFragment.xaml @@ -149,6 +149,12 @@ HorizontalExpand="True" Name="MessageInput" PlaceHolder="{Loc 'messenger-message-placeholder'}" /> + public sealed partial class MessengerCartridgeSystem { - private void UpdateUiState(EntityUid uid, EntityUid loaderUid, MessengerCartridgeComponent? component) + private void UpdateUiState(EntityUid uid, EntityUid loaderUid, MessengerCartridgeComponent? component, Dictionary? photoGallery = null) { if (!Resolve(uid, ref component)) return; @@ -57,7 +57,8 @@ public sealed partial class MessengerCartridgeSystem component.MutedGroupChats, unreadCounts, component.ActiveInvites, - component.PinnedChats + component.PinnedChats, + photoGallery ); _cartridgeLoader.UpdateCartridgeUiState(loaderUid, state); diff --git a/Content.Server/_Sunrise/CartridgeLoader/Cartridges/MessengerCartridgeSystem.cs b/Content.Server/_Sunrise/CartridgeLoader/Cartridges/MessengerCartridgeSystem.cs index a60331b639..bbdf9adfd0 100644 --- a/Content.Server/_Sunrise/CartridgeLoader/Cartridges/MessengerCartridgeSystem.cs +++ b/Content.Server/_Sunrise/CartridgeLoader/Cartridges/MessengerCartridgeSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.DeviceNetwork.Systems; using Content.Server.CartridgeLoader; using Content.Server.PDA.Ringer; @@ -129,4 +130,25 @@ public sealed partial class MessengerCartridgeSystem : EntitySystem _deviceNetwork.SetTransmitFrequency(loaderUid, originalFrequency.Value, deviceNetwork); } } + + /// + /// Пытается найти станцию для КПК. Если КПК не на станции, ищет любую станцию на той же карте. + /// + private EntityUid? GetBestStation(EntityUid pdaUid) + { + var station = _stationSystem.GetOwningStation(pdaUid); + if (station != null) + return station; + + var xform = Transform(pdaUid); + var mapId = xform.MapID; + + foreach (var s in _stationSystem.GetStations()) + { + if (Transform(s).MapID == mapId) + return s; + } + + return _stationSystem.GetStations().FirstOrDefault(); + } } diff --git a/Content.Server/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeSystem.cs b/Content.Server/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeSystem.cs new file mode 100644 index 0000000000..99b82eea2f --- /dev/null +++ b/Content.Server/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeSystem.cs @@ -0,0 +1,324 @@ +using System.Linq; +using Content.Server.CartridgeLoader; +using Content.Server.DeviceNetwork.Systems; +using Content.Server._Sunrise; +using Content.Server._Sunrise.Messenger; +using Content.Server.Station.Systems; +using Content.Shared.CartridgeLoader; +using Content.Shared.DeviceNetwork; +using Content.Shared.DeviceNetwork.Components; +using Content.Shared.Inventory; +using Content.Shared.PDA; +using Content.Shared._Sunrise.CartridgeLoader.Cartridges; +using Content.Shared._Sunrise.Messenger; +using Robust.Server.Player; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Audio.Systems; + +namespace Content.Server._Sunrise.CartridgeLoader.Cartridges; + +public sealed class PhotoCartridgeSystem : EntitySystem +{ + [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!; + [Dependency] private readonly NetTexturesManager _netTexturesManager = default!; + [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly MessengerServerSystem _messengerServer = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly SingletonDeviceNetServerSystem _singletonServer = default!; + [Dependency] private readonly IServerNetManager _netManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private ISawmill _sawmill = default!; + + private const int MaxPhotosPerUser = 50; + private const int MaxPhotoSizeBytes = 2 * 1024 * 1024; + private const double MinTimeBetweenCapturesSeconds = 1.0; + private const int MaxPhotoWidth = 512; + private const int MaxPhotoHeight = 512; + + private readonly Dictionary _lastCaptureTimes = new(); + + public override void Initialize() + { + base.Initialize(); + + _sawmill = _logManager.GetSawmill("photo.cartridge"); + + SubscribeLocalEvent(OnUiMessage); + SubscribeLocalEvent(OnUiReady); + + _netManager.RegisterNetMessage(OnPhotoCaptureMessage, accept: NetMessageAccept.Server); + } + + private void OnUiMessage(EntityUid uid, PhotoCartridgeComponent component, CartridgeMessageEvent args) + { + if (args is not PhotoUiMessageEvent photoMessage) + return; + + var loaderUid = GetEntity(args.LoaderUid); + if (loaderUid == EntityUid.Invalid) + return; + + switch (photoMessage.Action) + { + case PhotoUiAction.CapturePhoto: + HandleCapturePhoto(uid, component, loaderUid); + break; + case PhotoUiAction.SendPhotoToMessenger: + HandleSendPhotoToMessenger(uid, component, loaderUid, photoMessage.PhotoId, photoMessage.RecipientId, photoMessage.GroupId); + break; + case PhotoUiAction.RequestGallery: + UpdateUiState(uid, loaderUid, component); + break; + case PhotoUiAction.DeletePhoto: + HandleDeletePhoto(uid, component, loaderUid, photoMessage.PhotoId); + break; + case PhotoUiAction.ToggleFlash: + if (photoMessage.FlashEnabled.HasValue) + { + component.FlashEnabled = photoMessage.FlashEnabled.Value; + UpdateUiState(uid, loaderUid, component); + } + break; + } + } + + private void HandleDeletePhoto(EntityUid uid, PhotoCartridgeComponent component, EntityUid loaderUid, string? photoId) + { + if (string.IsNullOrEmpty(photoId) || !component.PhotoGallery.TryGetValue(photoId, out var metadata)) + return; + + component.PhotoGallery.Remove(photoId); + + UpdateUiState(uid, loaderUid, component); + } + + private void OnUiReady(EntityUid uid, PhotoCartridgeComponent component, CartridgeUiReadyEvent args) + { + UpdateUiState(uid, args.Loader, component); + } + + private void HandleCapturePhoto(EntityUid uid, PhotoCartridgeComponent component, EntityUid loaderUid) + { + if (component.PhotoGallery.Count >= MaxPhotosPerUser) + { + UpdateUiState(uid, loaderUid, component, errorMessage: Loc.GetString("photo-cartridge-limit-reached")); + return; + } + + if (component.FlashEnabled) + { + Spawn(component.FlashEffect, _transform.GetMapCoordinates(loaderUid)); + } + + _audio.PlayPvs(component.Sound, loaderUid); + + UpdateUiState(uid, loaderUid, component); + } + + /// + /// Обработчик сетевого сообщения с захваченным изображением от клиента + /// + private void OnPhotoCaptureMessage(PdaPhotoCaptureMessage msg) + { + if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var session)) + return; + + if (_lastCaptureTimes.TryGetValue(session, out var lastCapture)) + { + var timeSinceLastCapture = _gameTiming.CurTime - lastCapture; + if (timeSinceLastCapture.TotalSeconds < MinTimeBetweenCapturesSeconds) + { + _sawmill.Debug($"Photo capture rejected: cooldown active ({timeSinceLastCapture.TotalSeconds:F2}s < {MinTimeBetweenCapturesSeconds}s)"); + return; + } + } + + if (msg.ImageData.Length > MaxPhotoSizeBytes) + { + _sawmill.Warning($"Photo capture rejected: image too large ({msg.ImageData.Length} bytes > {MaxPhotoSizeBytes} bytes)"); + return; + } + + if (msg.Width <= 0 || msg.Height <= 0 || msg.Width > MaxPhotoWidth || msg.Height > MaxPhotoHeight) + { + _sawmill.Warning($"Photo capture rejected: invalid dimensions ({msg.Width}x{msg.Height})"); + return; + } + + var pdaUid = FindPlayerPda(session); + if (pdaUid == null) + { + _sawmill.Warning($"Photo capture rejected: no PDA found for player {session.Name}"); + return; + } + + if (!_cartridgeLoader.TryGetProgram(pdaUid.Value, out var cartridgeUid, out var photoComponent)) + { + _sawmill.Warning($"Photo capture rejected: photo cartridge not found in PDA {ToPrettyString(pdaUid.Value)}"); + return; + } + + if (photoComponent.PhotoGallery.Count >= MaxPhotosPerUser) + { + UpdateUiState(cartridgeUid.Value, pdaUid.Value, photoComponent, errorMessage: Loc.GetString("photo-cartridge-limit-reached")); + return; + } + + var photoId = Guid.NewGuid().ToString(); + var imagePath = $"/NetTextures/Messenger/{photoId}.png"; + + _netTexturesManager.RegisterDynamicResource(imagePath, msg.ImageData); + + var timestamp = _gameTiming.CurTime; + var metadata = new PhotoMetadata(photoId, imagePath, timestamp); + photoComponent.PhotoGallery[photoId] = metadata; + + _lastCaptureTimes[session] = timestamp; + + _sawmill.Info($"Photo captured from {session.Name}: {photoId}, path: {imagePath}, size: {msg.Width}x{msg.Height}, {msg.ImageData.Length} bytes"); + + UpdateUiState(cartridgeUid.Value, pdaUid.Value, photoComponent); + } + + /// + /// Находит КПК игрока по его сессии + /// + private EntityUid? FindPlayerPda(ICommonSession session) + { + if (session.AttachedEntity == null) + return null; + + var playerEntity = session.AttachedEntity.Value; + + if (_inventory.TryGetSlotEntity(playerEntity, "idcard", out var idCardEntity) && + TryComp(idCardEntity, out _)) + { + return idCardEntity; + } + + if (_inventory.TryGetSlotEntity(playerEntity, "belt", out var beltEntity) && + TryComp(beltEntity, out _)) + { + return beltEntity; + } + + var pdaQuery = EntityQueryEnumerator(); + while (pdaQuery.MoveNext(out var uid, out var pda)) + { + if (pda.PdaOwner == playerEntity) + { + return uid; + } + } + + return null; + } + + private void HandleSendPhotoToMessenger(EntityUid uid, PhotoCartridgeComponent component, EntityUid loaderUid, string? photoId, string? recipientId, string? groupId) + { + if (string.IsNullOrWhiteSpace(photoId)) + return; + + if (!component.PhotoGallery.TryGetValue(photoId, out var photoMetadata)) + { + UpdateUiState(uid, loaderUid, component, errorMessage: Loc.GetString("photo-cartridge-photo-not-found")); + return; + } + + var messengerServer = FindMessengerServer(loaderUid); + if (messengerServer == null) + { + UpdateUiState(uid, loaderUid, component, errorMessage: Loc.GetString("photo-cartridge-messenger-unavailable")); + return; + } + + if (!TryComp(loaderUid, out var pdaDevice)) + return; + + var userId = pdaDevice.Address; + if (string.IsNullOrEmpty(userId)) + return; + + var timestamp = _gameTiming.CurTime; + var content = Loc.GetString("photo-cartridge-photo-text"); + + if (!string.IsNullOrWhiteSpace(groupId)) + { + _messengerServer.SendGroupMessageWithImage(messengerServer.Value, userId, groupId, content, photoMetadata.ImagePath, timestamp); + } + else if (!string.IsNullOrWhiteSpace(recipientId)) + { + _messengerServer.SendPersonalMessageWithImage(messengerServer.Value, userId, recipientId, content, photoMetadata.ImagePath, timestamp); + } + else + { + UpdateUiState(uid, loaderUid, component, errorMessage: Loc.GetString("photo-cartridge-recipient-not-specified")); + return; + } + + UpdateUiState(uid, loaderUid, component); + } + + private EntityUid? FindMessengerServer(EntityUid pdaUid) + { + var station = _stationSystem.GetOwningStation(pdaUid); + if (station == null) + { + var xform = Transform(pdaUid); + var mapId = xform.MapID; + foreach (var s in _stationSystem.GetStations()) + { + if (Transform(s).MapID == mapId) + { + station = s; + break; + } + } + } + + if (station == null) + station = _stationSystem.GetStations().FirstOrDefault(); + + if (station == null) + return null; + + if (!_singletonServer.TryGetActiveServerAddress(station.Value, out var serverAddress)) + return null; + + var serverQuery = EntityQueryEnumerator(); + while (serverQuery.MoveNext(out var uid, out _, out var deviceNetwork)) + { + if (deviceNetwork.Address == serverAddress && _singletonServer.IsActiveServer(uid)) + { + return uid; + } + } + return null; + } + + private void UpdateUiState(EntityUid uid, EntityUid loaderUid, PhotoCartridgeComponent component, string? errorMessage = null) + { + var state = new PhotoUiState( + photos: component.PhotoGallery, + cameraReady: component.PhotoGallery.Count < MaxPhotosPerUser, + flashEnabled: component.FlashEnabled, + errorMessage: errorMessage + ); + _cartridgeLoader.UpdateCartridgeUiState(loaderUid, state); + } + + private EntityUid GetEntity(NetEntity netEntity) + { + return EntityManager.GetEntity(netEntity); + } +} diff --git a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Groups.cs b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Groups.cs index 83f97e7384..5b906a0499 100644 --- a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Groups.cs +++ b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Groups.cs @@ -101,14 +101,12 @@ public sealed partial class MessengerServerSystem if (group.Members.Contains(userId)) return; - // Проверяем, нет ли уже активного инвайта if (component.ActiveInvites.TryGetValue(userId, out var existingInvites)) { if (existingInvites.Any(inv => inv.GroupId == groupId)) return; } - // Создаем инвайт var invite = new MessengerGroupInvite( groupId, group.Name, @@ -144,12 +142,10 @@ public sealed partial class MessengerServerSystem _deviceNetwork.QueuePacket(uid, userId, invitePayload); } - // Отправляем обновленный список инвайтов SendInvitesList(uid, component, userId, serverDevice, pdaFrequency); return; } - // Для автоматических групп сохраняем старую логику (если разрешено) if (group.AutoGroupPrototypeId != null) { if (_prototypeManager.TryIndex(group.AutoGroupPrototypeId, out var autoGroupProto)) @@ -207,7 +203,8 @@ public sealed partial class MessengerServerSystem ["group_id"] = groupId, ["recipient_id"] = string.Empty, ["is_read"] = false, - ["message_id"] = systemMessage.MessageId + ["message_id"] = systemMessage.MessageId, + ["image_path"] = systemMessage.ImagePath ?? string.Empty }; foreach (var memberId in group.Members) @@ -273,7 +270,9 @@ public sealed partial class MessengerServerSystem ["group_id"] = msg.GroupId ?? string.Empty, ["recipient_id"] = msg.RecipientId ?? string.Empty, ["is_read"] = msg.IsRead, - ["message_id"] = msg.MessageId + ["message_id"] = msg.MessageId, + ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = msg.ImagePath ?? string.Empty }); } @@ -377,7 +376,9 @@ public sealed partial class MessengerServerSystem ["timestamp"] = timestamp.TotalSeconds, ["group_id"] = groupId, ["recipient_id"] = string.Empty, - ["is_read"] = false + ["is_read"] = false, + ["message_id"] = systemMessage.MessageId, + ["image_path"] = systemMessage.ImagePath ?? string.Empty }; foreach (var memberId in group.Members) @@ -463,7 +464,8 @@ public sealed partial class MessengerServerSystem ["group_id"] = groupId, ["recipient_id"] = string.Empty, ["is_read"] = false, - ["message_id"] = systemMessage.MessageId + ["message_id"] = systemMessage.MessageId, + ["image_path"] = systemMessage.ImagePath ?? string.Empty }; foreach (var memberId in group.Members) @@ -529,7 +531,9 @@ public sealed partial class MessengerServerSystem ["group_id"] = msg.GroupId ?? string.Empty, ["recipient_id"] = msg.RecipientId ?? string.Empty, ["is_read"] = msg.IsRead, - ["message_id"] = msg.MessageId + ["message_id"] = msg.MessageId, + ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = msg.ImagePath ?? string.Empty }); } @@ -722,7 +726,8 @@ public sealed partial class MessengerServerSystem ["group_id"] = groupId, ["recipient_id"] = string.Empty, ["is_read"] = false, - ["message_id"] = systemMessage.MessageId + ["message_id"] = systemMessage.MessageId, + ["image_path"] = systemMessage.ImagePath ?? string.Empty }; foreach (var memberId in group.Members) diff --git a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Messages.cs b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Messages.cs index 0a9c6fd542..fcc022ddcd 100644 --- a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Messages.cs +++ b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Messages.cs @@ -16,7 +16,16 @@ public sealed partial class MessengerServerSystem if (!args.Data.TryGetValue(MessengerCommands.CmdSendMessage, out NetworkPayload? messageData)) return; - if (!messageData.TryGetValue("content", out string? content) || string.IsNullOrWhiteSpace(content)) + if (!messageData.TryGetValue("content", out string? content)) + return; + + string? imagePath = null; + if (messageData.TryGetValue("image_path", out string? imgPath) && !string.IsNullOrWhiteSpace(imgPath)) + { + imagePath = imgPath; + } + + if (string.IsNullOrWhiteSpace(content) && string.IsNullOrWhiteSpace(imagePath)) return; if (!component.Users.TryGetValue(args.SenderAddress, out var sender)) @@ -28,21 +37,21 @@ public sealed partial class MessengerServerSystem if (messageData.TryGetValue("group_id", out string? groupId) && !string.IsNullOrWhiteSpace(groupId)) { - SendGroupMessage(uid, component, sender, groupId, content, timestamp); + SendGroupMessage(uid, component, sender, groupId, content, timestamp, imagePath); } else if (messageData.TryGetValue("recipient_id", out string? recipientId) && !string.IsNullOrWhiteSpace(recipientId)) { - SendPersonalMessage(uid, component, sender, recipientId, content, timestamp); + SendPersonalMessage(uid, component, sender, recipientId, content, timestamp, imagePath); } } - private void SendPersonalMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string recipientId, string content, TimeSpan timestamp) + private void SendPersonalMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string recipientId, string content, TimeSpan timestamp, string? imagePath = null) { if (!component.Users.ContainsKey(recipientId)) return; var messageId = GetNextMessageId(uid, component); - var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, null, recipientId, isRead: false, messageId, sender.JobIconId); + var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, null, recipientId, isRead: false, messageId, sender.JobIconId, imagePath); var chatId = GetPersonalChatId(sender.UserId, recipientId); if (!component.MessageHistory.TryGetValue(chatId, out var history)) @@ -91,7 +100,8 @@ public sealed partial class MessengerServerSystem ["recipient_id"] = message.RecipientId ?? string.Empty, ["is_read"] = message.IsRead, ["message_id"] = message.MessageId, - ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty + ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = message.ImagePath ?? string.Empty }; if (pdaFrequency.HasValue) @@ -121,7 +131,9 @@ public sealed partial class MessengerServerSystem ["group_id"] = message.GroupId ?? string.Empty, ["recipient_id"] = message.RecipientId ?? string.Empty, ["is_read"] = message.IsRead, - ["message_id"] = message.MessageId + ["message_id"] = message.MessageId, + ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = message.ImagePath ?? string.Empty } }, ["chat_id"] = chatId @@ -130,7 +142,7 @@ public sealed partial class MessengerServerSystem } } - private void SendGroupMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string groupId, string content, TimeSpan timestamp) + private void SendGroupMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string groupId, string content, TimeSpan timestamp, string? imagePath = null) { if (!component.Groups.TryGetValue(groupId, out var group)) return; @@ -139,7 +151,7 @@ public sealed partial class MessengerServerSystem return; var messageId = GetNextMessageId(uid, component); - var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, groupId, null, false, messageId, sender.JobIconId); + var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, groupId, null, false, messageId, sender.JobIconId, imagePath); if (!component.MessageHistory.TryGetValue(groupId, out var history)) { @@ -189,7 +201,8 @@ public sealed partial class MessengerServerSystem ["recipient_id"] = message.RecipientId ?? string.Empty, ["is_read"] = message.IsRead, ["message_id"] = message.MessageId, - ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty + ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = message.ImagePath ?? string.Empty }; foreach (var memberId in group.Members) @@ -205,6 +218,34 @@ public sealed partial class MessengerServerSystem } } + /// + /// Отправляет личное сообщение с изображением (используется PhotoCartridgeSystem) + /// + public void SendPersonalMessageWithImage(EntityUid uid, string senderUserId, string recipientId, string content, string imagePath, TimeSpan timestamp) + { + if (!TryComp(uid, out var component)) + return; + + if (!component.Users.TryGetValue(senderUserId, out var sender)) + return; + + SendPersonalMessage(uid, component, sender, recipientId, content, timestamp, imagePath); + } + + /// + /// Отправляет групповое сообщение с изображением (используется PhotoCartridgeSystem) + /// + public void SendGroupMessageWithImage(EntityUid uid, string senderUserId, string groupId, string content, string imagePath, TimeSpan timestamp) + { + if (!TryComp(uid, out var component)) + return; + + if (!component.Users.TryGetValue(senderUserId, out var sender)) + return; + + SendGroupMessage(uid, component, sender, groupId, content, timestamp, imagePath); + } + /// /// Обрабатывает удаление сообщения пользователем /// diff --git a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Queries.cs b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Queries.cs index d42e2f4889..920f3d251e 100644 --- a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Queries.cs +++ b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.Queries.cs @@ -210,7 +210,8 @@ public sealed partial class MessengerServerSystem ["recipient_id"] = msg.RecipientId ?? string.Empty, ["is_read"] = msg.IsRead, ["message_id"] = msg.MessageId, - ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty + ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = msg.ImagePath ?? string.Empty }); } } @@ -253,7 +254,8 @@ public sealed partial class MessengerServerSystem ["recipient_id"] = msg.RecipientId ?? string.Empty, ["is_read"] = msg.IsRead, ["message_id"] = msg.MessageId, - ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty + ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = msg.ImagePath ?? string.Empty }); } @@ -302,7 +304,8 @@ public sealed partial class MessengerServerSystem ["recipient_id"] = message.RecipientId ?? string.Empty, ["is_read"] = message.IsRead, ["message_id"] = message.MessageId, - ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty + ["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = message.ImagePath ?? string.Empty }); } diff --git a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.UserRegistration.cs b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.UserRegistration.cs index 6c427fe06f..fcd5058704 100644 --- a/Content.Server/_Sunrise/Messenger/MessengerServerSystem.UserRegistration.cs +++ b/Content.Server/_Sunrise/Messenger/MessengerServerSystem.UserRegistration.cs @@ -59,6 +59,15 @@ public sealed partial class MessengerServerSystem } var station = _stationSystem.GetOwningStation(args.Mob); + if (station == null) + { + var mobXform = Transform(args.Mob); + station = _stationSystem.GetStations().FirstOrDefault(s => Transform(s).MapID == mobXform.MapID); + } + + if (station == null) + station = _stationSystem.GetStations().FirstOrDefault(); + if (station == null) { Sawmill.Warning($"No station found for player: {ToPrettyString(args.Mob)}"); @@ -184,7 +193,6 @@ public sealed partial class MessengerServerSystem _deviceNetwork.QueuePacket(uid, userId, response, frequency: pdaFrequency, network: serverDevice.DeviceNetId); } - // Отправляем список пользователей новому пользователю var usersPayload = new NetworkPayload { [DeviceNetworkConstants.Command] = MessengerCommands.CmdUsersList, @@ -203,7 +211,6 @@ public sealed partial class MessengerServerSystem _deviceNetwork.QueuePacket(uid, userId, usersPayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId); } - // Отправляем список групп новому пользователю var groupsList = component.Groups.Values.ToList(); var groupsData = new List>(); @@ -270,7 +277,6 @@ public sealed partial class MessengerServerSystem _deviceNetwork.QueuePacket(uid, userId, groupsPayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId); } - // Регистрируем картридж мессенджера как фоновую программу, если он еще не зарегистрирован if (_cartridgeLoader.TryGetProgram(pdaUid, out var cartridgeUid, out _)) { if (TryComp(pdaUid, out var loader) && @@ -503,7 +509,8 @@ public sealed partial class MessengerServerSystem ["group_id"] = autoGroupProto.GroupId, ["recipient_id"] = string.Empty, ["is_read"] = false, - ["message_id"] = systemMessage.MessageId + ["message_id"] = systemMessage.MessageId, + ["image_path"] = systemMessage.ImagePath ?? string.Empty }; foreach (var memberId in group.Members) @@ -551,7 +558,9 @@ public sealed partial class MessengerServerSystem ["group_id"] = msg.GroupId ?? string.Empty, ["recipient_id"] = msg.RecipientId ?? string.Empty, ["is_read"] = msg.IsRead, - ["message_id"] = msg.MessageId + ["message_id"] = msg.MessageId, + ["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty, + ["image_path"] = msg.ImagePath ?? string.Empty }); } @@ -642,7 +651,6 @@ public sealed partial class MessengerServerSystem user.DepartmentId = departmentId; user.JobIconId = jobIconId; - // Отправляем обновленный список пользователей всем клиентам if (!TryComp(uid, out var serverDevice)) return; diff --git a/Content.Server/_Sunrise/NetTexturesCleanupSystem.cs b/Content.Server/_Sunrise/NetTexturesCleanupSystem.cs new file mode 100644 index 0000000000..c9e47836d9 --- /dev/null +++ b/Content.Server/_Sunrise/NetTexturesCleanupSystem.cs @@ -0,0 +1,25 @@ +using Content.Shared.GameTicking; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Server._Sunrise; + +/// +/// A small system that handles event-based cleanup for the NetTexturesManager. +/// Since NetTexturesManager is a standalone manager, it cannot safely subscribe to broadcast events. +/// +public sealed class NetTexturesCleanupSystem : EntitySystem +{ + [Dependency] private readonly NetTexturesManager _netTexturesManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRoundRestart); + } + + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + _netTexturesManager.ClearDynamicResources(); + } +} diff --git a/Content.Server/_Sunrise/NetTexturesManager.cs b/Content.Server/_Sunrise/NetTexturesManager.cs index 18e4a75f21..525d9535b0 100644 --- a/Content.Server/_Sunrise/NetTexturesManager.cs +++ b/Content.Server/_Sunrise/NetTexturesManager.cs @@ -11,6 +11,8 @@ using Robust.Shared.Network.Transfer; using Robust.Shared.Player; using Robust.Shared.Upload; using Robust.Shared.Utility; +using Robust.Shared.GameObjects; +using Content.Shared.GameTicking; using ByteHelpers = Robust.Shared.Utility.ByteHelpers; namespace Content.Server._Sunrise; @@ -20,7 +22,7 @@ namespace Content.Server._Sunrise; /// Uses High Bandwidth Transfer (WebSocket) to avoid blocking main game traffic. /// Textures are loaded into MemoryContentRoot on the client. /// -public sealed class NetTexturesManager + public sealed class NetTexturesManager { /// /// Transfer key for server -> client texture downloads via WebSocket @@ -32,25 +34,40 @@ public sealed class NetTexturesManager [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly ITransferManager _transferManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; private ISawmill _sawmill = default!; private const string AllowedPrefix = "/NetTextures/"; + /// + /// Dynamically registered in-memory resources that are not present on disk. + /// Key is the relative upload path used on the client (e.g. "NetTextures/Messenger/photo_123.png"). + /// + private readonly Dictionary _dynamicResources = new(); + public void Initialize() { _sawmill = _logManager.GetSawmill("network.textures"); _netManager.RegisterNetMessage(OnRequestNetworkResource); - // Register transfer key for High Bandwidth Transfer (WebSocket) _transferManager.RegisterTransferMessage(TransferKeyNetTextures); } + /// + /// Clears all dynamically registered in-memory resources. + /// Used during round restarts to prevent memory leaks. + /// + public void ClearDynamicResources() + { + _dynamicResources.Clear(); + _sawmill.Info("Cleared all dynamic NetTexture resources due to round restart."); + } + private void OnRequestNetworkResource(RequestNetworkResourceMessage msg) { if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var session)) return; - // Normalize the path - ensure it's rooted var resourcePath = msg.ResourcePath; ResPath resPath; @@ -63,10 +80,8 @@ public sealed class NetTexturesManager resPath = new ResPath("/") / resourcePath; } - // Clean the path to remove any .. sequences resPath = resPath.Clean(); - // Validate the path to prevent path traversal attacks if (!ValidateResourcePath(resPath, out var errorMessage)) { _sawmill.Warning($"Rejected resource request from {session.Name}: {errorMessage} (path: {msg.ResourcePath})"); @@ -84,15 +99,12 @@ public sealed class NetTexturesManager { errorMessage = null; - // Path must be rooted if (!path.IsRooted) { errorMessage = "Path must be rooted"; return false; } - // Check for dangerous path traversal sequences in the original string representation - // Even after Clean(), we should verify the path doesn't contain .. in segments var pathStr = path.ToString(); if (pathStr.Contains("../") || pathStr.Contains("..\\") || pathStr.StartsWith("..")) { @@ -100,16 +112,12 @@ public sealed class NetTexturesManager return false; } - // Only allow paths that start with /NetTextures/ - // This ensures clients can only access resources from the NetTextures directory if (!pathStr.StartsWith(AllowedPrefix, StringComparison.Ordinal)) { errorMessage = $"Path must start with {AllowedPrefix}"; return false; } - // Additional check: ensure the cleaned path doesn't escape the allowed directory - // by checking that it still starts with the allowed prefix after cleaning var relativePath = path.ToRelativePath(); var relativePathStr = relativePath.ToString(); @@ -132,60 +140,60 @@ public sealed class NetTexturesManager var startTime = DateTime.UtcNow; _sawmill.Debug($"[NetTextures] Starting transfer of {resourcePath} to {session.Name}"); - // Collect all files to send var filesToSend = new List<(ResPath Relative, byte[] Data)>(); - // Check if it's a directory (RSI files are directories) - // Try to find files in the directory first - var files = _resourceManager.ContentFindFiles(resourcePath).ToList(); - - if (files.Count == 0) + var relativeUploadPath = resourcePath.ToRelativePath(); + if (_dynamicResources.TryGetValue(relativeUploadPath, out var dynamicData)) { - // No files found in directory, try as single file - if (!_resourceManager.ContentFileExists(resourcePath)) - { - _sawmill.Warning($"Resource not found: {resourcePath}"); - return; - } - - if (!CollectSingleFile(resourcePath, filesToSend)) - return; + filesToSend.Add((relativeUploadPath, dynamicData)); } else { - // Directory - collect all files - foreach (var filePath in files) + + var files = _resourceManager.ContentFindFiles(resourcePath).ToList(); + + if (files.Count == 0) { - if (!filePath.TryRelativeTo(resourcePath, out var relativePath)) - continue; - - // relativePath is guaranteed to be non-null here because TryRelativeTo returned true - var relativePathValue = relativePath.Value; - - if (!_resourceManager.TryContentFileRead(filePath, out var stream)) + if (!_resourceManager.ContentFileExists(resourcePath)) { - _sawmill.Warning($"Failed to read file: {filePath}"); - continue; + _sawmill.Warning($"Resource not found: {resourcePath}"); + return; } - using (stream) - { - var data = new byte[stream.Length]; - stream.Read(data, 0, data.Length); - - // Calculate uploaded path: preserve the original path structure relative to Resources root - // Remove leading / and use as relative path for MemoryContentRoot - var relativeUploadPath = resourcePath.ToRelativePath(); - var uploadedPath = relativeUploadPath / relativePathValue; - - filesToSend.Add((uploadedPath, data)); - } + if (!CollectSingleFile(resourcePath, filesToSend)) + return; } + else + { + foreach (var filePath in files) + { + if (!filePath.TryRelativeTo(resourcePath, out var relativePath)) + continue; - _sawmill.Debug($"Collected resource directory {resourcePath} ({files.Count} files) for {session.Name}"); + var relativePathValue = relativePath.Value; + + if (!_resourceManager.TryContentFileRead(filePath, out var stream)) + { + _sawmill.Warning($"Failed to read file: {filePath}"); + continue; + } + + using (stream) + { + var data = new byte[stream.Length]; + stream.Read(data, 0, data.Length); + + var relativeUploadPath2 = resourcePath.ToRelativePath(); + var uploadedPath = relativeUploadPath2 / relativePathValue; + + filesToSend.Add((uploadedPath, data)); + } + } + + _sawmill.Debug($"Collected resource directory {resourcePath} ({files.Count} files) for {session.Name}"); + } } - // Send via High Bandwidth Transfer (WebSocket) to avoid blocking main game traffic try { var transferStartTime = DateTime.UtcNow; @@ -205,7 +213,6 @@ public sealed class NetTexturesManager catch (Exception ex) { _sawmill.Warning($"Failed to send resource via High Bandwidth Transfer to {session.Name}: {ex.Message}"); - // Fallback to regular network message if WebSocket transfer fails SendResourceFallback(session, filesToSend); } } @@ -226,7 +233,6 @@ public sealed class NetTexturesManager var data = new byte[stream.Length]; stream.Read(data, 0, data.Length); - // Calculate uploaded path: preserve the original path structure relative to Resources root var relativeUploadPath = filePath.ToRelativePath(); filesToSend.Add((relativeUploadPath, data)); } @@ -258,28 +264,72 @@ public sealed class NetTexturesManager var first = true; - foreach (var (relative, data) in files) + foreach (var (relative, data) in files) + { + if (!first) { - if (!first) - { - continueByte[0] = 1; - await stream.WriteAsync(continueByte); - } - - first = false; - - BinaryPrimitives.WriteUInt32LittleEndian(lengthBytes, (uint)Encoding.UTF8.GetByteCount(relative.CanonPath)); - await stream.WriteAsync(lengthBytes); - - BinaryPrimitives.WriteUInt32LittleEndian(lengthBytes, (uint)data.Length); - await stream.WriteAsync(lengthBytes); - - await stream.WriteAsync(Encoding.UTF8.GetBytes(relative.CanonPath)); - await stream.WriteAsync(data); + continueByte[0] = 1; + await stream.WriteAsync(continueByte); } - continueByte[0] = 0; - await stream.WriteAsync(continueByte); + first = false; + + BinaryPrimitives.WriteUInt32LittleEndian(lengthBytes, (uint)Encoding.UTF8.GetByteCount(relative.CanonPath)); + await stream.WriteAsync(lengthBytes); + + BinaryPrimitives.WriteUInt32LittleEndian(lengthBytes, (uint)data.Length); + await stream.WriteAsync(lengthBytes); + + await stream.WriteAsync(Encoding.UTF8.GetBytes(relative.CanonPath)); + await stream.WriteAsync(data); + } + + continueByte[0] = 0; + await stream.WriteAsync(continueByte); + } + + /// + /// Registers a dynamic in-memory network texture that is not present on disk. + /// The resourcePath must point inside /NetTextures/ and will be validated by . + /// + /// Rooted resource path, e.g. "/NetTextures/Messenger/photo_123.png". + /// Raw file bytes (PNG, WEBP, etc.). + public void RegisterDynamicResource(string resourcePath, byte[] data) + { + var path = resourcePath.StartsWith("/") + ? new ResPath(resourcePath) + : new ResPath("/") / resourcePath; + + path = path.Clean(); + + if (!ValidateResourcePath(path, out var error)) + { + _sawmill.Warning($"Failed to register dynamic NetTexture {resourcePath}: {error}"); + return; + } + + var relativeUploadPath = path.ToRelativePath(); + _dynamicResources[relativeUploadPath] = data; + _sawmill.Debug($"Registered dynamic NetTexture resource: {relativeUploadPath}"); + } + + /// + /// Unregisters a dynamic in-memory network texture. + /// + /// Rooted resource path, e.g. "/NetTextures/Messenger/photo_123.png". + public void UnregisterDynamicResource(string resourcePath) + { + var path = resourcePath.StartsWith("/") + ? new ResPath(resourcePath) + : new ResPath("/") / resourcePath; + + path = path.Clean(); + var relativeUploadPath = path.ToRelativePath(); + + if (_dynamicResources.Remove(relativeUploadPath)) + { + _sawmill.Debug($"Unregistered dynamic NetTexture resource: {relativeUploadPath}"); + } } } diff --git a/Content.Shared/CartridgeLoader/CartridgeLoaderComponent.cs b/Content.Shared/CartridgeLoader/CartridgeLoaderComponent.cs index f47112c794..4cd10a6b8e 100644 --- a/Content.Shared/CartridgeLoader/CartridgeLoaderComponent.cs +++ b/Content.Shared/CartridgeLoader/CartridgeLoaderComponent.cs @@ -33,7 +33,7 @@ public sealed partial class CartridgeLoaderComponent : Component /// The maximum amount of programs that can be installed on the cartridge loader entity /// [DataField] - public int DiskSpace = 8; + public int DiskSpace = 12; // Sunrise-Edit /// /// Controls whether the cartridge loader will play notifications if it supports it at all diff --git a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiMessageEvent.cs b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiMessageEvent.cs index 60a2d5c60a..d411d58004 100644 --- a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiMessageEvent.cs +++ b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiMessageEvent.cs @@ -18,6 +18,7 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent public readonly string? ChatId; public readonly bool? IsMuted; public readonly long? MessageId; + public readonly string? ImagePath; public MessengerUiMessageEvent( MessengerUiAction action, @@ -28,7 +29,8 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent string? userId = null, string? chatId = null, bool? isMuted = null, - long? messageId = null) + long? messageId = null, + string? imagePath = null) { Action = action; RecipientId = recipientId; @@ -39,6 +41,7 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent ChatId = chatId; IsMuted = isMuted; MessageId = messageId; + ImagePath = imagePath; } } @@ -60,5 +63,6 @@ public enum MessengerUiAction DeclineInvite, LeaveGroup, DeleteMessage, - TogglePin + TogglePin, + RequestPhotos } diff --git a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiState.cs b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiState.cs index bbda25a9c9..0151b1011b 100644 --- a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiState.cs +++ b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/MessengerUiState.cs @@ -64,6 +64,11 @@ public sealed class MessengerUiState : BoundUserInterfaceState /// public HashSet PinnedChats { get; } + /// + /// Галерея фотографий для выбора (опционально) + /// + public Dictionary? PhotoGallery { get; } + public MessengerUiState( bool isRegistered, bool serverAvailable, @@ -75,7 +80,8 @@ public sealed class MessengerUiState : BoundUserInterfaceState HashSet mutedGroupChats, Dictionary unreadCounts, List activeInvites, - HashSet pinnedChats) + HashSet pinnedChats, + Dictionary? photoGallery = null) { IsRegistered = isRegistered; ServerAvailable = serverAvailable; @@ -88,5 +94,6 @@ public sealed class MessengerUiState : BoundUserInterfaceState UnreadCounts = unreadCounts; ActiveInvites = activeInvites; PinnedChats = pinnedChats; + PhotoGallery = photoGallery; } } diff --git a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PdaPhotoCaptureMessage.cs b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PdaPhotoCaptureMessage.cs new file mode 100644 index 0000000000..5b925fd628 --- /dev/null +++ b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PdaPhotoCaptureMessage.cs @@ -0,0 +1,44 @@ +using Lidgren.Network; +using Robust.Shared.Network; +using Robust.Shared.Serialization; + +namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges; + +/// +/// Сообщение для отправки захваченного изображения с клиента на сервер +/// +public sealed class PdaPhotoCaptureMessage : NetMessage +{ + public override MsgGroups MsgGroup => MsgGroups.String; + + /// + /// Байты изображения (PNG или WebP) + /// + public byte[] ImageData { get; set; } = Array.Empty(); + + /// + /// Ширина изображения в пикселях + /// + public int Width { get; set; } + + /// + /// Высота изображения в пикселях + /// + public int Height { get; set; } + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) + { + Width = buffer.ReadInt32(); + Height = buffer.ReadInt32(); + var dataLength = buffer.ReadInt32(); + ImageData = buffer.ReadBytes(dataLength); + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) + { + buffer.Write(Width); + buffer.Write(Height); + buffer.Write(ImageData.Length); + buffer.Write(ImageData); + } +} diff --git a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeComponent.cs b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeComponent.cs new file mode 100644 index 0000000000..94822d67e6 --- /dev/null +++ b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoCartridgeComponent.cs @@ -0,0 +1,65 @@ +using Robust.Shared.Serialization; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges; + +/// +/// Компонент фото-картриджа для КПК +/// +[RegisterComponent] +public sealed partial class PhotoCartridgeComponent : Component +{ + /// + /// Галерея фотографий пользователя (PhotoId -> PhotoMetadata) + /// + [ViewVariables] + public Dictionary PhotoGallery = new(); + + /// + /// Звук срабатывания затвора + /// + [DataField] + public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/_Sunrise/camera_shot.ogg"); + + /// + /// Эффект вспышки + /// + [DataField] + public EntProtoId FlashEffect = "PhotoFlashEffect"; + + /// + /// Включена ли вспышка (пользовательская настройка) + /// + [DataField] + public bool FlashEnabled = true; +} + +/// +/// Метаданные фотографии в галерее +/// +[Serializable, NetSerializable] +public sealed class PhotoMetadata +{ + /// + /// Уникальный идентификатор фотографии (GUID) + /// + public string PhotoId { get; set; } + + /// + /// Путь к сетевому ресурсу изображения (например, "/NetTextures/Messenger/photo_123.png") + /// + public string ImagePath { get; set; } + + /// + /// Время создания фотографии (относительно начала раунда) + /// + public TimeSpan Timestamp { get; set; } + + public PhotoMetadata(string photoId, string imagePath, TimeSpan timestamp) + { + PhotoId = photoId; + ImagePath = imagePath; + Timestamp = timestamp; + } +} diff --git a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoUiMessageEvent.cs b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoUiMessageEvent.cs new file mode 100644 index 0000000000..ed7d411994 --- /dev/null +++ b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoUiMessageEvent.cs @@ -0,0 +1,45 @@ +using Content.Shared.CartridgeLoader; +using Robust.Shared.Serialization; + +namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges; + +/// +/// Событие сообщения UI фото-картриджа +/// +[Serializable, NetSerializable] +public sealed class PhotoUiMessageEvent : CartridgeMessageEvent +{ + public readonly PhotoUiAction Action; + public readonly string? PhotoId; + public readonly string? RecipientId; + public readonly string? GroupId; + + public PhotoUiMessageEvent( + PhotoUiAction action, + string? photoId = null, + string? recipientId = null, + string? groupId = null, + bool? flashEnabled = null) + { + Action = action; + PhotoId = photoId; + RecipientId = recipientId; + GroupId = groupId; + FlashEnabled = flashEnabled; + } + + public bool? FlashEnabled { get; } +} + +/// +/// Действия UI фото-картриджа +/// +[Serializable, NetSerializable] +public enum PhotoUiAction +{ + CapturePhoto, + SendPhotoToMessenger, + RequestGallery, + DeletePhoto, + ToggleFlash +} diff --git a/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoUiState.cs b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoUiState.cs new file mode 100644 index 0000000000..92b72f3ac4 --- /dev/null +++ b/Content.Shared/_Sunrise/CartridgeLoader/Cartridges/PhotoUiState.cs @@ -0,0 +1,42 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges; + +/// +/// Состояние UI фото-картриджа +/// +[Serializable, NetSerializable] +public sealed class PhotoUiState : BoundUserInterfaceState +{ + /// + /// Список фотографий в галерее (PhotoId -> PhotoMetadata) + /// + public Dictionary Photos { get; } + + /// + /// Статус камеры (готовность к съемке) + /// + public bool CameraReady { get; } + + /// + /// Включена ли вспышка + /// + public bool FlashEnabled { get; } + + /// + /// Сообщение об ошибке (если есть) + /// + public string? ErrorMessage { get; } + + public PhotoUiState( + Dictionary photos, + bool cameraReady, + bool flashEnabled, + string? errorMessage = null) + { + Photos = photos; + CameraReady = cameraReady; + FlashEnabled = flashEnabled; + ErrorMessage = errorMessage; + } +} diff --git a/Content.Shared/_Sunrise/Messenger/MessengerMessage.cs b/Content.Shared/_Sunrise/Messenger/MessengerMessage.cs index f256006884..b3ae859bb6 100644 --- a/Content.Shared/_Sunrise/Messenger/MessengerMessage.cs +++ b/Content.Shared/_Sunrise/Messenger/MessengerMessage.cs @@ -55,7 +55,23 @@ public sealed class MessengerMessage /// public ProtoId? SenderJobIconId { get; set; } - public MessengerMessage(string senderId, string senderName, string content, TimeSpan timestamp, string? groupId = null, string? recipientId = null, bool isRead = false, long messageId = 0, ProtoId? senderJobIconId = null) + /// + /// Путь к сетевому изображению, если сообщение содержит картинку. + /// Например: "/NetTextures/Messenger/photo_123.png" + /// + public string? ImagePath { get; set; } + + public MessengerMessage( + string senderId, + string senderName, + string content, + TimeSpan timestamp, + string? groupId = null, + string? recipientId = null, + bool isRead = false, + long messageId = 0, + ProtoId? senderJobIconId = null, + string? imagePath = null) { SenderId = senderId; SenderName = senderName; @@ -66,5 +82,6 @@ public sealed class MessengerMessage IsRead = isRead; MessageId = messageId; SenderJobIconId = senderJobIconId; + ImagePath = imagePath; } } diff --git a/Resources/Audio/_Sunrise/camera_shot.ogg b/Resources/Audio/_Sunrise/camera_shot.ogg new file mode 100644 index 0000000000..00beda9593 Binary files /dev/null and b/Resources/Audio/_Sunrise/camera_shot.ogg differ diff --git a/Resources/Locale/en-US/_prototypes/entities/objects/devices/cartridges.ftl b/Resources/Locale/en-US/_prototypes/entities/objects/devices/cartridges.ftl index 79f320d4ef..5c29a2cf4e 100644 --- a/Resources/Locale/en-US/_prototypes/entities/objects/devices/cartridges.ftl +++ b/Resources/Locale/en-US/_prototypes/entities/objects/devices/cartridges.ftl @@ -19,4 +19,6 @@ ent-AstroNavCartridge = AstroNav cartridge 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. \ No newline at end of file + .desc = A program for messaging between PDAs. +ent-PhotoCartridge = camera cartridge + .desc = A program for taking photos and managing a photo gallery. diff --git a/Resources/Locale/en-US/_strings/_sunrise/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/_strings/_sunrise/cartridge-loader/cartridges.ftl new file mode 100644 index 0000000000..85b120cca2 --- /dev/null +++ b/Resources/Locale/en-US/_strings/_sunrise/cartridge-loader/cartridges.ftl @@ -0,0 +1,14 @@ +photo-cartridge-photos-count = Photos: {$count}/{$max} +photo-cartridge-limit-reached = Photo limit reached +photo-cartridge-photo-not-found = Photo not found +photo-cartridge-messenger-unavailable = Messenger server unavailable +photo-cartridge-recipient-not-specified = Recipient not specified +photo-cartridge-photo-text = [Photo] +photo-cartridge-tab-camera = Camera +photo-cartridge-tab-gallery = Gallery +photo-cartridge-zoom-label = Zoom: +photo-cartridge-capture-button = Take Photo +photo-cartridge-overlay-button = 👁 Overlay +photo-cartridge-back-button = Back +photo-cartridge-delete-button = Delete +photo-cartridge-flash-label = Flash diff --git a/Resources/Locale/en-US/_strings/cartridge-loader/cartridges.ftl b/Resources/Locale/en-US/_strings/cartridge-loader/cartridges.ftl index f24940e20c..091b7da42b 100644 --- a/Resources/Locale/en-US/_strings/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/en-US/_strings/cartridge-loader/cartridges.ftl @@ -6,7 +6,8 @@ nano-task-program-name = NanoTask news-read-program-name = Station news crew-manifest-program-name = Crew manifest -messenger-program-name = Messenger MAX +messenger-program-name = RobustChat +photo-program-name = FlashSnap crew-manifest-cartridge-loading = Loading ... net-probe-program-name = NetProbe diff --git a/Resources/Locale/ru-RU/_prototypes/entities/objects/devices/cartridges.ftl b/Resources/Locale/ru-RU/_prototypes/entities/objects/devices/cartridges.ftl index 42eea5f658..b0c987fd4b 100644 --- a/Resources/Locale/ru-RU/_prototypes/entities/objects/devices/cartridges.ftl +++ b/Resources/Locale/ru-RU/_prototypes/entities/objects/devices/cartridges.ftl @@ -19,4 +19,6 @@ ent-AstroNavCartridge = Картридж АстроНав ent-NavigatorCartridge = картридж навигатора .desc = Программа для просмотра карты станции в целях навигации. ent-MessengerCartridge = картридж мессенджера - .desc = Программа для обмена сообщениями между КПК. \ No newline at end of file + .desc = Программа для обмена сообщениями между КПК. +ent-PhotoCartridge = картридж фотоаппарата + .desc = Программа для съемки фотографий и управления галереей. \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_strings/_sunrise/cartridge-loader/cartridges.ftl b/Resources/Locale/ru-RU/_strings/_sunrise/cartridge-loader/cartridges.ftl index 3bd69bef5c..2176d0780a 100644 --- a/Resources/Locale/ru-RU/_strings/_sunrise/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/ru-RU/_strings/_sunrise/cartridge-loader/cartridges.ftl @@ -2,3 +2,18 @@ plant-analyzer-program-name = БотаТек ent-PlantAnalyzerCartridge = картридж БотаТек .desc = Программа, предоставляющая инструменты для анализа растений ботаникам. + +photo-cartridge-photos-count = Фотографий: {$count}/{$max} +photo-cartridge-limit-reached = Достигнут лимит фотографий +photo-cartridge-photo-not-found = Фотография не найдена +photo-cartridge-messenger-unavailable = Сервер мессенджера недоступен +photo-cartridge-recipient-not-specified = Не указан получатель +photo-cartridge-photo-text = [Фото] +photo-cartridge-tab-camera = Камера +photo-cartridge-tab-gallery = Галерея +photo-cartridge-zoom-label = Зум: +photo-cartridge-capture-button = Сделать фото +photo-cartridge-overlay-button = 👁 Оверлей +photo-cartridge-back-button = Назад +photo-cartridge-delete-button = Удалить +photo-cartridge-flash-label = Вспышка diff --git a/Resources/Locale/ru-RU/_strings/cartridge-loader/cartridges.ftl b/Resources/Locale/ru-RU/_strings/cartridge-loader/cartridges.ftl index a2f1465363..056fafc6fd 100644 --- a/Resources/Locale/ru-RU/_strings/cartridge-loader/cartridges.ftl +++ b/Resources/Locale/ru-RU/_strings/cartridge-loader/cartridges.ftl @@ -4,7 +4,7 @@ notekeeper-program-name = Заметки nano-task-program-name = NanoTask news-read-program-name = Новости станции crew-manifest-program-name = Манифест экипажа -messenger-program-name = Мессенджер МАХ +messenger-program-name = RobustChat crew-manifest-cartridge-loading = Загрузка... net-probe-program-name = Зонд сетей net-probe-scan = Просканирован { $device }! @@ -26,6 +26,8 @@ astro-nav-program-name = АстроНав navigator-program-name = Навигатор navigator-cartridge-loading = Загрузка карты... +photo-program-name = FlashSnap + med-tek-program-name = МедТек # Wanted list cartridge wanted-list-program-name = Список разыскиваемых diff --git a/Resources/Locale/ru-RU/_strings/cartridge-loader/messenger.ftl b/Resources/Locale/ru-RU/_strings/cartridge-loader/messenger.ftl index 9634bd8050..57d92e3cb3 100644 --- a/Resources/Locale/ru-RU/_strings/cartridge-loader/messenger.ftl +++ b/Resources/Locale/ru-RU/_strings/cartridge-loader/messenger.ftl @@ -59,3 +59,7 @@ messenger-invite-user-to-group-title = Пригласить в { $groupName } messenger-delete-message = Удалить сообщение messenger-system-user-joined = { $userName } присоединился(ась) к группе messenger-system-user-left = { $userName } покинул(а) группу +messenger-photo-button-tooltip = Отправить фотографию +messenger-photo-picker-title = Выберите фотографию +messenger-image-preview-title = Просмотр изображения +messenger-photo-flash-label = Вспышка diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index 90fc8b7db6..12360be3a2 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -78,7 +78,7 @@ prefix: device-address-prefix-console savableAddress: false - type: WirelessNetworkConnection - range: 500 + range: 2000 # Sunrise-Edit - type: CartridgeLoader uiKey: enum.PdaUiKey.Key preinstalled: @@ -89,6 +89,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End cartridgeSlot: priority: -1 @@ -170,6 +171,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -188,6 +190,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -363,6 +366,7 @@ - PlantAnalyzerCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -548,6 +552,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -601,6 +606,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -1022,6 +1028,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -1108,6 +1115,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -1160,6 +1168,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: Tag # Ignore Chameleon tags tags: @@ -1413,6 +1422,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -1602,6 +1612,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -1636,6 +1647,7 @@ # Sunrise-Start - NavigatorCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-End - type: entity @@ -1803,6 +1815,7 @@ - NavigatorCartridge - AstroNavCartridge - MessengerCartridge + - PhotoCartridge # Sunrise-end - type: entity diff --git a/Resources/Prototypes/_Sunrise/Entities/Effects/photo.yml b/Resources/Prototypes/_Sunrise/Entities/Effects/photo.yml new file mode 100644 index 0000000000..4e28dce404 --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Entities/Effects/photo.yml @@ -0,0 +1,13 @@ +- type: entity + id: PhotoFlashEffect + categories: [ HideSpawnMenu ] + components: + - type: PointLight + enabled: true + radius: 10 + energy: 10 + netsync: false + - type: LightFade + duration: 0.5 + - type: TimedDespawn + lifetime: 0.5 diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/doctor.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/doctor.yml index c5bfe61c7e..bc4f83f353 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/doctor.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/doctor.yml @@ -26,6 +26,7 @@ - MedTekCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: PdaAnimationVisuals animatedState: pda-space-prison-doctor idInsertedLayerState: id_inserted-space-prison diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/engineer.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/engineer.yml index bcb75527d7..e84b9a9ab2 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/engineer.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/engineer.yml @@ -21,6 +21,7 @@ - NetProbeCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: entity parent: PrisonEngineerPDA diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/headofprison.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/headofprison.yml index 95b0d076f6..cf97729edc 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/headofprison.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/headofprison.yml @@ -27,6 +27,7 @@ - NewsReaderCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: entity parent: SecurityMetusPDA diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/pilot.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/pilot.yml index 7bc6e42821..7400507bd2 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/pilot.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/pilot.yml @@ -31,6 +31,7 @@ - AstroNavCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: PdaAnimationVisuals animatedState: pda-space-prison-pilot idInsertedLayerState: id_inserted-space-prison diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/prisoner.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/prisoner.yml index e6007da494..79b7b52c20 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/prisoner.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/prisoner.yml @@ -37,6 +37,7 @@ - NewsReaderCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: entity parent: PlanetPrisonerPDA diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/scientist.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/scientist.yml index 51cf8ff1d0..304f6447e7 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/scientist.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/scientist.yml @@ -26,6 +26,7 @@ - AstroNavCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: PdaAnimationVisuals animatedState: pda-space-prison-scientist idInsertedLayerState: id_inserted-space-prison diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/worker.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/worker.yml index 6b586239a3..9b824650f9 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/worker.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/Prison/pda/worker.yml @@ -26,6 +26,7 @@ - AstroNavCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: PdaAnimationVisuals animatedState: pda-space-prison-worker idInsertedLayerState: id_inserted-space-prison diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/cartridges.yml index c2f0209d3c..39c0f22d63 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/cartridges.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/cartridges.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity parent: BaseItem id: PlantAnalyzerCartridge name: PlantAnalyzerCartridge @@ -52,5 +52,22 @@ programName: messenger-program-name icon: sprite: _Sunrise/Interface/Misc/program_icons.rsi - state: max_logo2 + state: robust_chat - type: MessengerCartridge + +- type: entity + parent: BasePDACartridge + id: PhotoCartridge + name: photo cartridge + description: A program for taking photos and managing a photo gallery. + components: + - type: Sprite + state: cart-y + - type: UIFragment + ui: !type:PhotoUi + - type: Cartridge + programName: photo-program-name + icon: + sprite: _Sunrise/Interface/Misc/program_icons.rsi + state: flash_snap + - type: PhotoCartridge diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/pda.yml index 3b522b523d..238e2de213 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Devices/pda.yml @@ -187,6 +187,7 @@ - AstroNavCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: entity parent: CaptainPDA @@ -425,6 +426,7 @@ - AstroNavCartridge - NavigatorCartridge - MessengerCartridge + - PhotoCartridge - type: entity parent: BasePDA diff --git a/Resources/Prototypes/_Sunrise/Structures/Machines/messenger_server.yml b/Resources/Prototypes/_Sunrise/Structures/Machines/messenger_server.yml index 6df67ce1d5..d2646a9f08 100644 --- a/Resources/Prototypes/_Sunrise/Structures/Machines/messenger_server.yml +++ b/Resources/Prototypes/_Sunrise/Structures/Machines/messenger_server.yml @@ -36,9 +36,7 @@ receiveFrequencyId: Messenger autoConnect: false - type: WirelessNetworkConnection - range: 500 - - type: StationLimitedNetwork - allowNonStationPackets: true + range: 2000 - type: ApcPowerReceiver powerLoad: 200 - type: DeviceNetworkRequiresPower diff --git a/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/flash_snap.png b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/flash_snap.png new file mode 100644 index 0000000000..7ed184b32b Binary files /dev/null and b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/flash_snap.png differ diff --git a/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/games.png b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/games.png new file mode 100644 index 0000000000..60621537e6 Binary files /dev/null and b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/games.png differ diff --git a/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/meta.json b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/meta.json index 47dd5b23cc..6869799722 100644 --- a/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/meta.json +++ b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "discord: seraphimttt", + "copyright": "max icons - discord: seraphimttt. Other take here https://pixelexplosive.itch.io/pixel-art-ui-icon-pack-25-icons-32x32", "size": { "x": 32, "y": 32 @@ -15,6 +15,12 @@ }, { "name": "max_logo3" + }, + { + "name": "robust_chat" + }, + { + "name": "flash_snap" } ] } diff --git a/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/robust_chat.png b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/robust_chat.png new file mode 100644 index 0000000000..a343b40a99 Binary files /dev/null and b/Resources/Textures/_Sunrise/Interface/Misc/program_icons.rsi/robust_chat.png differ