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