Revert "фотоапарат и доработка месенжера"
This reverts commit 5f3f836fe5.
This commit is contained in:
parent
e154d5b410
commit
416c0ed8bd
53 changed files with 150 additions and 1985 deletions
|
|
@ -40,34 +40,23 @@
|
|||
StyleClasses="LabelSubText" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer
|
||||
Orientation="Vertical"
|
||||
<Control
|
||||
HorizontalExpand="True"
|
||||
MinSize="0,20"
|
||||
Name="ContentContainer">
|
||||
<RichTextLabel
|
||||
Name="ContentLabel"
|
||||
HorizontalExpand="True" />
|
||||
<ContainerButton
|
||||
Name="ImageButton"
|
||||
HorizontalExpand="True"
|
||||
Visible="False"
|
||||
Margin="0,4,0,0">
|
||||
<PanelContainer Name="ImageBorder" HorizontalExpand="True">
|
||||
<TextureRect
|
||||
Name="ImagePreview"
|
||||
HorizontalExpand="True"
|
||||
Stretch="KeepAspect" />
|
||||
</PanelContainer>
|
||||
</ContainerButton>
|
||||
</BoxContainer>
|
||||
<TextureRect
|
||||
Name="ReadStatusIcon"
|
||||
SetWidth="16"
|
||||
SetHeight="16"
|
||||
TextureScale="0.5,0.5"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,2,2"
|
||||
Visible="False" />
|
||||
MinSize="0,20" />
|
||||
<TextureRect
|
||||
Name="ReadStatusIcon"
|
||||
SetWidth="16"
|
||||
SetHeight="16"
|
||||
TextureScale="0.5,0.5"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,2,2"
|
||||
Visible="False" />
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Numerics;
|
||||
using Content.Client._Sunrise.Messenger;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
|
|
@ -9,7 +8,6 @@ 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;
|
||||
|
|
@ -23,7 +21,6 @@ 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<ClientEmojiSystem>();
|
||||
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
|
|
@ -48,36 +45,10 @@ 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<long>? OnDeleteMessage;
|
||||
private Button? _deleteButton;
|
||||
private string? _currentImagePath;
|
||||
|
||||
public void UpdateMessage(MessengerMessage message, bool isOwnMessage, bool isPersonalChat, string? currentUserId)
|
||||
{
|
||||
|
|
@ -89,7 +60,6 @@ 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)
|
||||
{
|
||||
|
|
@ -146,83 +116,6 @@ 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<TextureResource>(uploadedPath, out var textureResource))
|
||||
{
|
||||
ImagePreview.Texture = textureResource.Texture;
|
||||
ImageButton.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImageButton.Visible = false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ImageButton.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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, imagePath) =>
|
||||
SendMessengerMessage(MessengerUiAction.SendMessage, userInterface, recipientId: recipientId, groupId: groupId, content: content, imagePath: imagePath);
|
||||
_fragment.OnSendMessage += (recipientId, groupId, content) =>
|
||||
SendMessengerMessage(MessengerUiAction.SendMessage, userInterface, recipientId: recipientId, groupId: groupId, content: content);
|
||||
_fragment.OnCreateGroup += (groupName) =>
|
||||
SendMessengerMessage(MessengerUiAction.CreateGroup, userInterface, groupName: groupName);
|
||||
_fragment.OnAddToGroup += (groupId, userId) =>
|
||||
|
|
@ -39,8 +39,6 @@ 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)
|
||||
|
|
@ -61,10 +59,9 @@ public sealed partial class MessengerUi : UIFragment
|
|||
string? userId = null,
|
||||
string? chatId = null,
|
||||
bool? isMuted = null,
|
||||
long? messageId = null,
|
||||
string? imagePath = null)
|
||||
long? messageId = null)
|
||||
{
|
||||
var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted, messageId, imagePath);
|
||||
var messengerMessage = new MessengerUiMessageEvent(action, recipientId, groupId, content, groupName, userId, chatId, isMuted, messageId);
|
||||
var message = new CartridgeUiMessage(messengerMessage);
|
||||
userInterface.SendMessage(message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,12 +149,6 @@
|
|||
HorizontalExpand="True"
|
||||
Name="MessageInput"
|
||||
PlaceHolder="{Loc 'messenger-message-placeholder'}" />
|
||||
<Button
|
||||
Name="PhotoButton"
|
||||
Text="▣"
|
||||
SetWidth="30"
|
||||
Margin="4,0,0,0"
|
||||
ToolTip="{Loc 'messenger-photo-button-tooltip'}" />
|
||||
<Button
|
||||
Name="EmojiButton"
|
||||
Text="☻"
|
||||
|
|
|
|||
|
|
@ -19,14 +19,13 @@ using Robust.Shared.Input;
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MessengerUiFragment : BoxContainer
|
||||
{
|
||||
public event Action<string?, string?, string, string?>? OnSendMessage;
|
||||
public event Action<string?, string?, string>? OnSendMessage;
|
||||
public event Action<string>? OnCreateGroup;
|
||||
public event Action<string, string>? OnAddToGroup;
|
||||
public event Action<string, string>? OnRemoveFromGroup;
|
||||
|
|
@ -37,7 +36,6 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
public event Action<string>? OnLeaveGroup;
|
||||
public event Action<string, long>? OnDeleteMessage;
|
||||
public event Action<string>? OnTogglePin;
|
||||
public event Action? OnRequestPhotos;
|
||||
|
||||
private string? _currentChatId;
|
||||
private MessengerUiState? _currentState;
|
||||
|
|
@ -64,8 +62,6 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
|
||||
private CreateGroupDialog? _createGroupDialog;
|
||||
private AddUserDialog? _addUserDialog;
|
||||
|
|
@ -103,7 +99,6 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
CreateGroupButton.OnPressed += _ => ShowCreateGroupDialog();
|
||||
ToggleMembersButton.OnPressed += _ => ToggleMembersList();
|
||||
EmojiButton.OnPressed += _ => ShowEmojiPicker();
|
||||
PhotoButton.OnPressed += _ => RequestPhotos();
|
||||
SearchInput.OnTextChanged += OnSearchTextChanged;
|
||||
PersonalChatsTab.OnPressed += _ => SwitchTab(0);
|
||||
GroupChatsTab.OnPressed += _ => SwitchTab(1);
|
||||
|
|
@ -173,12 +168,6 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
MessageInput.Editable = canInteract && hasChatSelected;
|
||||
SendButton.Disabled = !canInteract || !hasChatSelected;
|
||||
EmojiButton.Disabled = !canInteract || !hasChatSelected;
|
||||
PhotoButton.Disabled = !canInteract || !hasChatSelected;
|
||||
|
||||
if (state.PhotoGallery != null && state.PhotoGallery.Count > 0)
|
||||
{
|
||||
ShowPhotoPicker(state.PhotoGallery);
|
||||
}
|
||||
|
||||
var savedChatId = _currentChatId;
|
||||
var savedGroupId = _currentGroupId;
|
||||
|
|
@ -1154,92 +1143,10 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
}
|
||||
|
||||
MessageInput.Clear();
|
||||
OnSendMessage?.Invoke(recipientId, groupId, messageText, null);
|
||||
OnSendMessage?.Invoke(recipientId, groupId, messageText);
|
||||
ScrollToBottom();
|
||||
}
|
||||
|
||||
private void SendPhoto(string imagePath)
|
||||
{
|
||||
if (_currentChatId == null)
|
||||
return;
|
||||
|
||||
string? recipientId = null;
|
||||
string? groupId = null;
|
||||
|
||||
if (_currentChatId.StartsWith("personal_"))
|
||||
{
|
||||
var parts = _currentChatId.Split('_');
|
||||
if (parts.Length >= 3 && _currentState?.CurrentUserId != null)
|
||||
{
|
||||
var userId1 = parts[1];
|
||||
var userId2 = parts[2];
|
||||
recipientId = userId1 == _currentState.CurrentUserId ? userId2 : userId1;
|
||||
}
|
||||
}
|
||||
else if (_currentState?.Groups.Any(g => g.GroupId == _currentChatId) ?? false)
|
||||
{
|
||||
groupId = _currentChatId;
|
||||
}
|
||||
|
||||
OnSendMessage?.Invoke(recipientId, groupId, string.Empty, imagePath);
|
||||
}
|
||||
|
||||
private void RequestPhotos()
|
||||
{
|
||||
if (_currentChatId == null)
|
||||
return;
|
||||
|
||||
OnRequestPhotos?.Invoke();
|
||||
}
|
||||
|
||||
private DefaultWindow? _photoPickerDialog;
|
||||
|
||||
private void ShowPhotoPicker(Dictionary<string, PhotoMetadata> photos)
|
||||
{
|
||||
if (_photoPickerDialog != null && _photoPickerDialog.IsOpen)
|
||||
return;
|
||||
|
||||
var dialog = new DefaultWindow
|
||||
{
|
||||
Title = Loc.GetString("messenger-photo-picker-title"),
|
||||
MinSize = new Vector2(560, 620),
|
||||
};
|
||||
_photoPickerDialog = dialog;
|
||||
|
||||
var scroll = new ScrollContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true
|
||||
};
|
||||
|
||||
var grid = new GridContainer
|
||||
{
|
||||
Columns = 2,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
foreach (var (id, metadata) in photos)
|
||||
{
|
||||
var photoControl = new PhotoItemControl(id, metadata,
|
||||
_netTexturesManager,
|
||||
_resourceCache,
|
||||
_gameTiming);
|
||||
|
||||
photoControl.MinSize = new Vector2(120, 120);
|
||||
photoControl.OnPressed += _ =>
|
||||
{
|
||||
SendPhoto(metadata.ImagePath);
|
||||
dialog.Close();
|
||||
};
|
||||
grid.AddChild(photoControl);
|
||||
}
|
||||
|
||||
scroll.AddChild(grid);
|
||||
dialog.Contents.AddChild(scroll);
|
||||
dialog.OnClose += () => _photoPickerDialog = null;
|
||||
dialog.OpenCentered();
|
||||
}
|
||||
|
||||
private string GetPersonalChatId(string userId)
|
||||
{
|
||||
if (_currentState?.CurrentUserId == null)
|
||||
|
|
@ -1792,7 +1699,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
/// <summary>
|
||||
/// Создает кнопку чата с общими элементами
|
||||
/// </summary>
|
||||
private Button CreateChatButton(string chatId, string chatName, bool isPinned, int unreadCount,
|
||||
private Button CreateChatButton(string chatId, string chatName, bool isPinned, int unreadCount,
|
||||
ProtoId<JobIconPrototype>? jobIconId, Action<string, string> onSelect, Action<string>? onTogglePin)
|
||||
{
|
||||
var button = new Button
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Enums;
|
||||
using Content.Client.Viewport;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class PhotoCaptureOverlay : Overlay
|
||||
{
|
||||
private readonly IPlayerManager _playerManager;
|
||||
private readonly SharedTransformSystem _transformSystem;
|
||||
private readonly IStateManager _stateManager;
|
||||
private readonly IEyeManager _eyeManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
public PhotoCaptureOverlay(IPlayerManager playerManager, SharedTransformSystem transformSystem, IStateManager stateManager, IEyeManager eyeManager, IEntityManager entityManager)
|
||||
{
|
||||
_playerManager = playerManager;
|
||||
_transformSystem = transformSystem;
|
||||
_stateManager = stateManager;
|
||||
_eyeManager = eyeManager;
|
||||
_entityManager = entityManager;
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (_stateManager.CurrentState is not IMainViewportState viewportState)
|
||||
return;
|
||||
|
||||
if (args.Viewport.Eye != _eyeManager.CurrentEye)
|
||||
return;
|
||||
|
||||
var screenHandle = args.ScreenHandle;
|
||||
var viewportControl = (Control)viewportState.Viewport;
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not { } player)
|
||||
return;
|
||||
|
||||
var playerPos = _transformSystem.GetWorldPosition(player);
|
||||
|
||||
var system = _entityManager.System<PhotoCartridgeClientSystem>();
|
||||
var targetPos = system.GetCameraPosition(player, system.CaptureDistance);
|
||||
|
||||
var targetScreen = _eyeManager.WorldToScreen(targetPos);
|
||||
var playerScreen = _eyeManager.WorldToScreen(playerPos);
|
||||
var offsetScreen = _eyeManager.WorldToScreen(playerPos + new Vector2(1, 0));
|
||||
var pixelsPerMeter = (offsetScreen - playerScreen).Length();
|
||||
|
||||
if (pixelsPerMeter < 1) return;
|
||||
|
||||
var halfSize = (3.0f * pixelsPerMeter) / 2.0f;
|
||||
var rect = new UIBox2(targetScreen.X - halfSize, targetScreen.Y - halfSize, targetScreen.X + halfSize, targetScreen.Y + halfSize);
|
||||
|
||||
var vpRect = viewportControl.GlobalPixelRect;
|
||||
var color = Color.Black.WithAlpha(0.5f);
|
||||
|
||||
screenHandle.DrawRect(new UIBox2(vpRect.Left, vpRect.Top, vpRect.Right, rect.Top), color);
|
||||
screenHandle.DrawRect(new UIBox2(vpRect.Left, rect.Bottom, vpRect.Right, vpRect.Bottom), color);
|
||||
screenHandle.DrawRect(new UIBox2(vpRect.Left, rect.Top, rect.Left, rect.Bottom), color);
|
||||
screenHandle.DrawRect(new UIBox2(rect.Right, rect.Top, vpRect.Right, rect.Bottom), color);
|
||||
|
||||
screenHandle.DrawRect(rect, Color.Red.WithAlpha(0.3f), false);
|
||||
var borderThickness = 2f;
|
||||
screenHandle.DrawRect(new UIBox2(rect.Left, rect.Top, rect.Right, rect.Top + borderThickness), Color.Red);
|
||||
screenHandle.DrawRect(new UIBox2(rect.Left, rect.Bottom - borderThickness, rect.Right, rect.Bottom), Color.Red);
|
||||
screenHandle.DrawRect(new UIBox2(rect.Left, rect.Top, rect.Left + borderThickness, rect.Bottom), Color.Red);
|
||||
screenHandle.DrawRect(new UIBox2(rect.Right - borderThickness, rect.Top, rect.Right, rect.Bottom), Color.Red);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Network;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class PhotoCartridgeClientSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private TimeSpan _nextCaptureTime = TimeSpan.Zero;
|
||||
public bool CameraReady => _timing.CurTime >= _nextCaptureTime;
|
||||
|
||||
private PhotoCaptureOverlay? _overlay;
|
||||
public bool OverlayEnabled { get; private set; }
|
||||
public float CaptureDistance { get; set; } = 2.0f;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private const int TargetPhotoWidth = 256;
|
||||
private const int TargetPhotoHeight = 256;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("photo.cartridge.client");
|
||||
_netManager.RegisterNetMessage<PdaPhotoCaptureMessage>(accept: NetMessageAccept.Server);
|
||||
_overlay = new PhotoCaptureOverlay(_playerManager, _transformSystem, _stateManager, _eyeManager, _entityManager);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_overlayManager.RemoveOverlay<PhotoCaptureOverlay>();
|
||||
}
|
||||
|
||||
public void SetOverlayEnabled(bool enabled)
|
||||
{
|
||||
OverlayEnabled = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
if (!_overlayManager.HasOverlay<PhotoCaptureOverlay>())
|
||||
_overlayManager.AddOverlay(_overlay!);
|
||||
}
|
||||
else
|
||||
{
|
||||
_overlayManager.RemoveOverlay<PhotoCaptureOverlay>();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 GetCameraPosition(EntityUid source, float distance)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent<TransformComponent>(source, out var xform))
|
||||
return Vector2.Zero;
|
||||
|
||||
var sourcePos = _transformSystem.GetWorldPosition(source);
|
||||
|
||||
var lookRotation = _transformSystem.GetWorldRotation(source);
|
||||
var ignoreEnt = source;
|
||||
|
||||
if (xform.ParentUid.IsValid() && !_entityManager.HasComponent<MapComponent>(xform.ParentUid) && !_entityManager.HasComponent<MapGridComponent>(xform.ParentUid))
|
||||
{
|
||||
lookRotation = _transformSystem.GetWorldRotation(xform.ParentUid);
|
||||
ignoreEnt = xform.ParentUid;
|
||||
}
|
||||
|
||||
var gridRot = Angle.Zero;
|
||||
if (xform.GridUid != null)
|
||||
gridRot = _transformSystem.GetWorldRotation(xform.GridUid.Value);
|
||||
|
||||
var relativeRot = lookRotation - gridRot;
|
||||
var directionVec = gridRot.RotateVec(relativeRot.GetCardinalDir().ToVec());
|
||||
|
||||
var ray = new CollisionRay(sourcePos, directionVec, (int) Content.Shared.Physics.CollisionGroup.Opaque);
|
||||
|
||||
var results = _physics.IntersectRayWithPredicate(xform.MapID, ray, distance, uid =>
|
||||
{
|
||||
return uid == source ||
|
||||
uid == ignoreEnt ||
|
||||
_entityManager.HasComponent<MobStateComponent>(uid);
|
||||
}, false).ToList();
|
||||
|
||||
if (results.Count > 0)
|
||||
{
|
||||
return sourcePos + directionVec * Math.Max(0.1f, results[0].Distance - 0.2f);
|
||||
}
|
||||
|
||||
return sourcePos + directionVec * distance;
|
||||
}
|
||||
|
||||
public void CaptureAndSendPhoto(EntityUid source)
|
||||
{
|
||||
if (_timing.CurTime < _nextCaptureTime)
|
||||
return;
|
||||
|
||||
_nextCaptureTime = _timing.CurTime + TimeSpan.FromSeconds(1.5);
|
||||
|
||||
Timer.Spawn(500, () => CaptureInternal(source));
|
||||
}
|
||||
|
||||
private void CaptureInternal(EntityUid source)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
{
|
||||
_sawmill.Warning("Cannot capture photo: client not connected to server");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not { } player)
|
||||
return;
|
||||
|
||||
if (_stateManager.CurrentState is not IMainViewportState viewportState || viewportState.Viewport is not Control control)
|
||||
return;
|
||||
|
||||
var viewport = viewportState.Viewport.Viewport;
|
||||
var sourcePos = _transformSystem.GetWorldPosition(source);
|
||||
|
||||
var targetPos = GetCameraPosition(source, CaptureDistance);
|
||||
var targetScreen = _eyeManager.WorldToScreen(targetPos);
|
||||
|
||||
var sourceScreen = _eyeManager.WorldToScreen(sourcePos);
|
||||
var offsetScreen = _eyeManager.WorldToScreen(sourcePos + new Vector2(1, 0));
|
||||
var logicalPixelsPerMeter = (offsetScreen - sourceScreen).Length();
|
||||
|
||||
viewport.Screenshot(img => {
|
||||
var scaleX = (float)img.Width / control.Size.X;
|
||||
var scaleY = (float)img.Height / control.Size.Y;
|
||||
|
||||
var texturePixelsPerMeter = logicalPixelsPerMeter * scaleX;
|
||||
var size = (int)(3.0f * texturePixelsPerMeter);
|
||||
|
||||
var localTargetLogical = targetScreen - control.GlobalPosition;
|
||||
var centerX = localTargetLogical.X * scaleX;
|
||||
var centerY = localTargetLogical.Y * scaleY;
|
||||
|
||||
var x = (int)(centerX - size / 2f);
|
||||
var y = (int)(centerY - size / 2f);
|
||||
|
||||
x = Math.Clamp(x, 0, img.Width);
|
||||
y = Math.Clamp(y, 0, img.Height);
|
||||
var w = Math.Clamp(size, 1, img.Width - x);
|
||||
var h = Math.Clamp(size, 1, img.Height - y);
|
||||
|
||||
var finalRect = new SixLabors.ImageSharp.Rectangle(x, y, w, h);
|
||||
TakeScreenshot(img, finalRect);
|
||||
});
|
||||
}
|
||||
|
||||
private void TakeScreenshot<T>(Image<T> screenshot, SixLabors.ImageSharp.Rectangle cropRect) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessCapturedImage(screenshot, cropRect);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Error processing screenshot: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
screenshot.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessCapturedImage<T>(Image<T> image, SixLabors.ImageSharp.Rectangle cropRect) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var processed = image.Clone(ctx =>
|
||||
{
|
||||
ctx.Crop(cropRect);
|
||||
ctx.Resize(TargetPhotoWidth, TargetPhotoHeight);
|
||||
});
|
||||
|
||||
var width = processed.Width;
|
||||
var height = processed.Height;
|
||||
|
||||
byte[] imageData;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
processed.SaveAsPng(memoryStream);
|
||||
imageData = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
processed.Dispose();
|
||||
SendPhotoToServer(imageData, width, height);
|
||||
}
|
||||
|
||||
private void SendPhotoToServer(byte[] imageData, int width, int height)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
return;
|
||||
|
||||
var message = new PdaPhotoCaptureMessage
|
||||
{
|
||||
ImageData = imageData,
|
||||
Width = width,
|
||||
Height = height
|
||||
};
|
||||
|
||||
_netManager.ClientSendMessage(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed partial class PhotoUi : UIFragment
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
private PhotoUiFragment? _fragment;
|
||||
|
||||
public override Control GetUIFragmentRoot()
|
||||
{
|
||||
return _fragment!;
|
||||
}
|
||||
|
||||
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_fragment = new PhotoUiFragment();
|
||||
if (fragmentOwner.HasValue)
|
||||
_fragment.SetOwner(fragmentOwner.Value);
|
||||
|
||||
_fragment.OnCapturePhoto += () =>
|
||||
{
|
||||
if (fragmentOwner.HasValue)
|
||||
_entitySystemManager.GetEntitySystem<PhotoCartridgeClientSystem>().CaptureAndSendPhoto(fragmentOwner.Value);
|
||||
|
||||
SendPhotoMessage(PhotoUiAction.CapturePhoto, userInterface);
|
||||
};
|
||||
_fragment.OnSendPhotoToMessenger += (photoId, recipientId, groupId) =>
|
||||
SendPhotoMessage(PhotoUiAction.SendPhotoToMessenger, userInterface, photoId: photoId, recipientId: recipientId, groupId: groupId);
|
||||
_fragment.OnRequestGallery += () =>
|
||||
SendPhotoMessage(PhotoUiAction.RequestGallery, userInterface);
|
||||
|
||||
_fragment.OnDeletePhoto += photoId =>
|
||||
SendPhotoMessage(PhotoUiAction.DeletePhoto, userInterface, photoId: photoId);
|
||||
|
||||
_fragment.OnToggleFlash += flashEnabled =>
|
||||
SendPhotoMessage(PhotoUiAction.ToggleFlash, userInterface, flashEnabled: flashEnabled);
|
||||
}
|
||||
|
||||
public override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
if (state is not PhotoUiState photoState)
|
||||
return;
|
||||
|
||||
_fragment?.UpdateState(photoState);
|
||||
}
|
||||
|
||||
private void SendPhotoMessage(
|
||||
PhotoUiAction action,
|
||||
BoundUserInterface userInterface,
|
||||
string? photoId = null,
|
||||
string? recipientId = null,
|
||||
string? groupId = null,
|
||||
bool? flashEnabled = null)
|
||||
{
|
||||
var photoMessage = new PhotoUiMessageEvent(action, photoId, recipientId, groupId, flashEnabled);
|
||||
var message = new CartridgeUiMessage(photoMessage);
|
||||
userInterface.SendMessage(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<cartridges:PhotoUiFragment xmlns:cartridges="clr-namespace:Content.Client._Sunrise.CartridgeLoader.Cartridges"
|
||||
xmlns:viewport="clr-namespace:Content.Client.Viewport"
|
||||
xmlns="https://spacestation14.io" Margin="4"
|
||||
Orientation="Vertical">
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0,0,0,4">
|
||||
<Button Name="CameraTabBtn" Text="Камера" HorizontalExpand="True" ToggleMode="True" Pressed="True"/>
|
||||
<Button Name="GalleryTabBtn" Text="Галерея" HorizontalExpand="True" ToggleMode="True"/>
|
||||
</BoxContainer>
|
||||
|
||||
<Label Name="ErrorMessageLabel" Text="" StyleClasses="Danger" Visible="False"/>
|
||||
<Label Name="PhotoCountLabel" Text="Фотографий: 0/50" Align="Right" StyleClasses="LabelSubText"/>
|
||||
|
||||
<BoxContainer Name="CameraView" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Visible="True">
|
||||
<PanelContainer Name="CameraPreviewContainer" HorizontalExpand="True" VerticalExpand="True" Margin="0,0,0,4">
|
||||
<viewport:ScalingViewport Name="CameraPreview" HorizontalExpand="True" VerticalExpand="True"/>
|
||||
</PanelContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0,0,0,4">
|
||||
<Label Text="Зум:" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||
<Slider Name="ZoomSlider" HorizontalExpand="True" MinValue="0" MaxValue="1" Value="0" Margin="0,0,8,0"/>
|
||||
<CheckBox Name="FlashCheckBox" Text="{Loc 'messenger-photo-flash-label'}" Pressed="True"/>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Button Name="CaptureButton" Text="Сделать фото" HorizontalExpand="True" MinHeight="40"/>
|
||||
<Button Name="ToggleOverlayButton" Text="👁 Оверлей" MinWidth="80" MinHeight="40"
|
||||
ToggleMode="True" Margin="4,0,0,0"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Name="GalleryView" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Visible="False">
|
||||
<ScrollContainer Name="GalleryScroll" HorizontalExpand="True" VerticalExpand="True">
|
||||
<GridContainer Name="GalleryContainer" Columns="2" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Name="ImageViewOverlay" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Visible="False" Margin="4">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Button Name="BackFromImageParams" Text="Назад" HorizontalAlignment="Left"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Button Name="DeleteImageBtn" Text="Удалить" StyleClasses="Caution" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
<PanelContainer HorizontalExpand="True" VerticalExpand="True" Margin="0,4,0,0">
|
||||
<TextureRect Name="FullImageRect" HorizontalExpand="True" VerticalExpand="True" Stretch="KeepAspectCentered"/>
|
||||
</PanelContainer>
|
||||
<Label Name="FullImageTimestamp" Align="Center" StyleClasses="LabelSubText"/>
|
||||
</BoxContainer>
|
||||
|
||||
</cartridges:PhotoUiFragment>
|
||||
|
|
@ -1,386 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PhotoUiFragment : BoxContainer
|
||||
{
|
||||
public event Action? OnCapturePhoto;
|
||||
public event Action<string>? OnDeletePhoto;
|
||||
public event Action<string, string?, string?>? OnSendPhotoToMessenger;
|
||||
public event Action? OnRequestGallery;
|
||||
public event Action<bool>? OnToggleFlash;
|
||||
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private Dictionary<string, PhotoItemControl> _photoControls = new();
|
||||
private string? _currentViewPhotoId;
|
||||
private EntityUid? _ownerEntity;
|
||||
private bool _serverCameraReady = true;
|
||||
|
||||
private readonly Robust.Shared.Graphics.Eye _previewEye = new();
|
||||
|
||||
public PhotoUiFragment()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
HorizontalExpand = true;
|
||||
VerticalExpand = true;
|
||||
|
||||
CaptureButton.OnPressed += _ => OnCapturePhoto?.Invoke();
|
||||
BackFromImageParams.OnPressed += _ => CloseFullPhoto();
|
||||
DeleteImageBtn.OnPressed += _ =>
|
||||
{
|
||||
if (_currentViewPhotoId != null)
|
||||
{
|
||||
OnDeletePhoto?.Invoke(_currentViewPhotoId);
|
||||
CloseFullPhoto();
|
||||
}
|
||||
};
|
||||
|
||||
FlashCheckBox.OnToggled += args => OnToggleFlash?.Invoke(args.Pressed);
|
||||
|
||||
ToggleOverlayButton.OnPressed += _ => ToggleCaptureAreaOverlay();
|
||||
ToggleOverlayButton.Pressed = _entityManager.System<PhotoCartridgeClientSystem>().OverlayEnabled;
|
||||
|
||||
CameraTabBtn.OnPressed += _ => SwitchTab(true);
|
||||
GalleryTabBtn.OnPressed += _ => SwitchTab(false);
|
||||
SwitchTab(true);
|
||||
|
||||
var system = _entityManager.System<PhotoCartridgeClientSystem>();
|
||||
ZoomSlider.Value = (system.CaptureDistance - 2.0f) / 8.0f;
|
||||
ZoomSlider.OnValueChanged += args =>
|
||||
{
|
||||
system.CaptureDistance = 2.0f + args.Value * 8.0f;
|
||||
};
|
||||
|
||||
SetupCameraPreview();
|
||||
}
|
||||
|
||||
public void SetOwner(EntityUid owner)
|
||||
{
|
||||
_ownerEntity = owner;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
_entityManager.System<PhotoCartridgeClientSystem>().SetOverlayEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void SwitchTab(bool showCamera)
|
||||
{
|
||||
if (ImageViewOverlay.Visible)
|
||||
ImageViewOverlay.Visible = false;
|
||||
|
||||
CameraTabBtn.Pressed = showCamera;
|
||||
GalleryTabBtn.Pressed = !showCamera;
|
||||
|
||||
CameraView.Visible = showCamera;
|
||||
GalleryView.Visible = !showCamera;
|
||||
|
||||
if (!showCamera)
|
||||
{
|
||||
OnRequestGallery?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseFullPhoto()
|
||||
{
|
||||
ImageViewOverlay.Visible = false;
|
||||
GalleryView.Visible = true;
|
||||
|
||||
CameraTabBtn.Pressed = false;
|
||||
GalleryTabBtn.Pressed = true;
|
||||
}
|
||||
|
||||
private void OpenFullPhoto(string photoId, PhotoMetadata metadata, Texture? texture)
|
||||
{
|
||||
_currentViewPhotoId = photoId;
|
||||
CameraView.Visible = false;
|
||||
GalleryView.Visible = false;
|
||||
|
||||
ImageViewOverlay.Visible = true;
|
||||
FullImageRect.Texture = texture;
|
||||
FullImageTimestamp.Text = FormatTimestamp(metadata.Timestamp);
|
||||
}
|
||||
|
||||
private string FormatTimestamp(TimeSpan timestamp)
|
||||
{
|
||||
var age = _gameTiming.CurTime - timestamp;
|
||||
var realTimeOfCapture = DateTime.UtcNow - age;
|
||||
var pdaTime = realTimeOfCapture + TimeSpan.FromHours(3);
|
||||
return pdaTime.ToString("HH:mm:ss");
|
||||
}
|
||||
|
||||
private void SetupCameraPreview()
|
||||
{
|
||||
if (CameraPreview != null)
|
||||
{
|
||||
CameraPreview.Eye = _previewEye;
|
||||
CameraPreview.ViewportSize = new Robust.Shared.Maths.Vector2i(256, 256);
|
||||
CameraPreview.RenderScaleMode = ScalingViewportRenderScaleMode.Fixed;
|
||||
CameraPreview.FixedRenderScale = 1;
|
||||
|
||||
_previewEye.Zoom = _eyeManager.CurrentEye.Zoom * 0.4f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переключает видимость оверлея области съемки
|
||||
/// </summary>
|
||||
private void ToggleCaptureAreaOverlay()
|
||||
{
|
||||
var system = _entityManager.System<PhotoCartridgeClientSystem>();
|
||||
system.SetOverlayEnabled(!system.OverlayEnabled);
|
||||
ToggleOverlayButton.Pressed = system.OverlayEnabled;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var photoSystem = _entityManager.System<PhotoCartridgeClientSystem>();
|
||||
|
||||
CaptureButton.Disabled = !_serverCameraReady || !photoSystem.CameraReady;
|
||||
|
||||
if (!CameraView.Visible || CameraPreview == null)
|
||||
return;
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is { } player)
|
||||
{
|
||||
var source = _ownerEntity ?? player;
|
||||
var xform = _entityManager.GetComponent<TransformComponent>(source);
|
||||
var sourcePos = xform.MapPosition;
|
||||
|
||||
var targetPos = photoSystem.GetCameraPosition(source, photoSystem.CaptureDistance);
|
||||
|
||||
_previewEye.Position = sourcePos.Offset(targetPos - sourcePos.Position);
|
||||
_previewEye.Rotation = _eyeManager.CurrentEye.Rotation;
|
||||
|
||||
_previewEye.Zoom = new Vector2(3f / 8f, 3f / 8f);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(PhotoUiState state)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(state.ErrorMessage))
|
||||
{
|
||||
ErrorMessageLabel.Text = state.ErrorMessage;
|
||||
ErrorMessageLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ErrorMessageLabel.Visible = false;
|
||||
}
|
||||
|
||||
_serverCameraReady = state.CameraReady;
|
||||
FlashCheckBox.Pressed = state.FlashEnabled;
|
||||
PhotoCountLabel.Text = Loc.GetString("photo-cartridge-photos-count", ("count", state.Photos.Count), ("max", 50));
|
||||
|
||||
UpdateGallery(state.Photos);
|
||||
}
|
||||
|
||||
private void UpdateGallery(Dictionary<string, PhotoMetadata> photos)
|
||||
{
|
||||
var toRemove = _photoControls.Keys.Except(photos.Keys).ToList();
|
||||
foreach (var photoId in toRemove)
|
||||
{
|
||||
if (_photoControls.TryGetValue(photoId, out var control))
|
||||
{
|
||||
control.Dispose();
|
||||
_photoControls.Remove(photoId);
|
||||
}
|
||||
}
|
||||
|
||||
var sortedPhotos = photos.OrderByDescending(p => p.Value.Timestamp);
|
||||
|
||||
foreach (var (photoId, metadata) in sortedPhotos)
|
||||
{
|
||||
if (!_photoControls.ContainsKey(photoId))
|
||||
{
|
||||
var control = new PhotoItemControl(photoId, metadata, _netTexturesManager, _resourceCache, _gameTiming);
|
||||
control.OnPressed += _ => OpenFullPhoto(photoId, metadata, control.GetTexture());
|
||||
GalleryContainer.AddChild(control);
|
||||
_photoControls[photoId] = control;
|
||||
}
|
||||
else
|
||||
{
|
||||
_photoControls[photoId].UpdateMetadata(metadata);
|
||||
_photoControls[photoId].SetPositionInParent(0);
|
||||
}
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
foreach (var (photoId, _) in sortedPhotos)
|
||||
{
|
||||
if (_photoControls.TryGetValue(photoId, out var control))
|
||||
{
|
||||
control.SetPositionInParent(index++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Элемент управления для отображения одной фотографии в галерее
|
||||
/// </summary>
|
||||
public sealed class PhotoItemControl : ContainerButton
|
||||
{
|
||||
private readonly string _photoId;
|
||||
private PhotoMetadata _metadata;
|
||||
private readonly NetTexturesManager _netTexturesManager;
|
||||
private readonly IResourceCache _resourceCache;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
private TextureRect _previewRect;
|
||||
private Label _timestampLabel;
|
||||
|
||||
public PhotoItemControl(string photoId, PhotoMetadata metadata, NetTexturesManager netTexturesManager, IResourceCache resourceCache, IGameTiming gameTiming)
|
||||
{
|
||||
_photoId = photoId;
|
||||
_metadata = metadata;
|
||||
_netTexturesManager = netTexturesManager;
|
||||
_resourceCache = resourceCache;
|
||||
_gameTiming = gameTiming;
|
||||
|
||||
var vBox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(2)
|
||||
};
|
||||
AddChild(vBox);
|
||||
|
||||
_previewRect = new TextureRect
|
||||
{
|
||||
MinHeight = 84,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||
Margin = new Thickness(0, 0, 0, 2)
|
||||
};
|
||||
|
||||
var borderPanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.Transparent,
|
||||
BorderColor = Color.Transparent,
|
||||
BorderThickness = new Thickness(2)
|
||||
}
|
||||
};
|
||||
vBox.AddChild(borderPanel);
|
||||
borderPanel.AddChild(_previewRect);
|
||||
|
||||
OnMouseEntered += _ =>
|
||||
{
|
||||
if (borderPanel.PanelOverride is StyleBoxFlat style)
|
||||
{
|
||||
style.BorderColor = Color.White.WithAlpha(0.6f);
|
||||
}
|
||||
};
|
||||
|
||||
OnMouseExited += _ =>
|
||||
{
|
||||
if (borderPanel.PanelOverride is StyleBoxFlat style)
|
||||
{
|
||||
style.BorderColor = Color.Transparent;
|
||||
}
|
||||
};
|
||||
|
||||
_timestampLabel = new Label
|
||||
{
|
||||
Text = FormatTimestamp(metadata.Timestamp),
|
||||
Align = Label.AlignMode.Center,
|
||||
StyleClasses = { "LabelSubText" },
|
||||
ClipText = true
|
||||
};
|
||||
vBox.AddChild(_timestampLabel);
|
||||
|
||||
LoadImage();
|
||||
}
|
||||
|
||||
public Texture? GetTexture()
|
||||
{
|
||||
return _previewRect.Texture;
|
||||
}
|
||||
|
||||
public void UpdateMetadata(PhotoMetadata metadata)
|
||||
{
|
||||
_metadata = metadata;
|
||||
_timestampLabel.Text = FormatTimestamp(metadata.Timestamp);
|
||||
}
|
||||
|
||||
private void LoadImage()
|
||||
{
|
||||
var isAvailable = _netTexturesManager.EnsureResource(_metadata.ImagePath);
|
||||
|
||||
if (isAvailable)
|
||||
{
|
||||
LoadImageTexture(_metadata.ImagePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_netTexturesManager.ResourceLoaded += OnResourceLoaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResourceLoaded(string resourcePath)
|
||||
{
|
||||
if (resourcePath == _metadata.ImagePath)
|
||||
{
|
||||
LoadImageTexture(resourcePath);
|
||||
_netTexturesManager.ResourceLoaded -= OnResourceLoaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadImageTexture(string imagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uploadedPath = _netTexturesManager.GetUploadedPath(imagePath);
|
||||
if (_resourceCache.TryGetResource<TextureResource>(uploadedPath, out var textureResource))
|
||||
{
|
||||
_previewRect.Texture = textureResource.Texture;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatTimestamp(TimeSpan timestamp)
|
||||
{
|
||||
var age = _gameTiming.CurTime - timestamp;
|
||||
var realTimeOfCapture = DateTime.UtcNow - age;
|
||||
var pdaTime = realTimeOfCapture + TimeSpan.FromHours(3);
|
||||
|
||||
return pdaTime.ToString("HH:mm:ss");
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ public sealed partial class MessengerCartridgeSystem
|
|||
return;
|
||||
}
|
||||
|
||||
var station = GetBestStation(pdaUid);
|
||||
var station = _stationSystem.GetOwningStation(pdaUid);
|
||||
if (station == null)
|
||||
{
|
||||
component.ServerAddress = null;
|
||||
|
|
@ -175,7 +175,7 @@ public sealed partial class MessengerCartridgeSystem
|
|||
|
||||
component.LoaderUid = loaderUid;
|
||||
|
||||
var station = GetBestStation(pdaUid);
|
||||
var station = _stationSystem.GetOwningStation(pdaUid);
|
||||
if (station == null)
|
||||
{
|
||||
Sawmill.Warning($"No station found for PDA: {ToPrettyString(pdaUid)}");
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public sealed partial class MessengerCartridgeSystem
|
|||
{
|
||||
case MessengerUiAction.SendMessage:
|
||||
if (message.Content != null)
|
||||
SendMessage(uid, component, loaderUid, deviceNetwork, message.RecipientId, message.GroupId, message.Content, message.ImagePath);
|
||||
SendMessage(uid, component, loaderUid, deviceNetwork, message.RecipientId, message.GroupId, message.Content);
|
||||
break;
|
||||
case MessengerUiAction.CreateGroup:
|
||||
if (message.GroupName != null)
|
||||
|
|
@ -72,29 +72,10 @@ public sealed partial class MessengerCartridgeSystem
|
|||
if (message.ChatId != null)
|
||||
TogglePin(uid, component, message.ChatId);
|
||||
break;
|
||||
case MessengerUiAction.RequestPhotos:
|
||||
RequestPhotos(uid, component, loaderUid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestPhotos(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid)
|
||||
{
|
||||
var photoGallery = new Dictionary<string, PhotoMetadata>();
|
||||
|
||||
foreach (var cartridgeUid in _cartridgeLoader.GetInstalled(loaderUid))
|
||||
{
|
||||
if (TryComp<PhotoCartridgeComponent>(cartridgeUid, out var photoComp))
|
||||
{
|
||||
photoGallery = photoComp.PhotoGallery;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component, photoGallery);
|
||||
}
|
||||
|
||||
private void SendMessage(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string? recipientId, string? groupId, string content, string? imagePath = null)
|
||||
private void SendMessage(EntityUid uid, MessengerCartridgeComponent component, EntityUid loaderUid, DeviceNetworkComponent deviceNetwork, string? recipientId, string? groupId, string content)
|
||||
{
|
||||
if (component.ServerAddress == null || !component.IsRegistered)
|
||||
return;
|
||||
|
|
@ -117,22 +98,15 @@ public sealed partial class MessengerCartridgeSystem
|
|||
return;
|
||||
}
|
||||
|
||||
var messagePayload = new NetworkPayload
|
||||
{
|
||||
["content"] = content,
|
||||
["recipient_id"] = recipientId ?? string.Empty,
|
||||
["group_id"] = groupId ?? string.Empty
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(imagePath))
|
||||
{
|
||||
messagePayload["image_path"] = imagePath;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdSendMessage,
|
||||
[MessengerCommands.CmdSendMessage] = messagePayload
|
||||
[MessengerCommands.CmdSendMessage] = new NetworkPayload
|
||||
{
|
||||
["content"] = content,
|
||||
["recipient_id"] = recipientId ?? string.Empty,
|
||||
["group_id"] = groupId ?? string.Empty
|
||||
}
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(loaderUid, component.ServerAddress, payload, frequency: messengerFreq, network: pdaDevice.DeviceNetId);
|
||||
|
|
|
|||
|
|
@ -282,7 +282,6 @@ public sealed partial class MessengerCartridgeSystem
|
|||
messageData.TryGetValue("is_read", out object? isReadObj);
|
||||
messageData.TryGetValue("message_id", out object? messageIdObj);
|
||||
messageData.TryGetValue("sender_job_icon_id", out object? senderJobIconIdObj);
|
||||
messageData.TryGetValue("image_path", out object? imagePathObj);
|
||||
|
||||
var groupId = groupIdObj?.ToString();
|
||||
var recipientId = recipientIdObj?.ToString();
|
||||
|
|
@ -305,12 +304,8 @@ public sealed partial class MessengerCartridgeSystem
|
|||
senderJobIconId = new ProtoId<JobIconPrototype>(senderJobIconIdObj.ToString()!);
|
||||
}
|
||||
|
||||
var imagePath = imagePathObj?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(imagePath))
|
||||
imagePath = null;
|
||||
|
||||
var timestamp = TimeSpan.FromSeconds(timestampSeconds);
|
||||
messages.Add(new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId, senderJobIconId, imagePath));
|
||||
messages.Add(new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId, senderJobIconId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(chatId) && messages.Count >= 0)
|
||||
|
|
@ -424,7 +419,6 @@ public sealed partial class MessengerCartridgeSystem
|
|||
packet.Data.TryGetValue("is_read", out object? isReadObj);
|
||||
packet.Data.TryGetValue("message_id", out object? messageIdObj);
|
||||
packet.Data.TryGetValue("sender_job_icon_id", out object? senderJobIconIdObj);
|
||||
packet.Data.TryGetValue("image_path", out object? imagePathObj);
|
||||
|
||||
var groupId = groupIdObj?.ToString();
|
||||
var recipientId = recipientIdObj?.ToString();
|
||||
|
|
@ -447,12 +441,8 @@ public sealed partial class MessengerCartridgeSystem
|
|||
senderJobIconId = new ProtoId<JobIconPrototype>(senderJobIconIdObj.ToString()!);
|
||||
}
|
||||
|
||||
var imagePath = imagePathObj?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(imagePath))
|
||||
imagePath = null;
|
||||
|
||||
var timestamp = TimeSpan.FromSeconds(timestampSeconds);
|
||||
var message = new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId, senderJobIconId, imagePath);
|
||||
var message = new MessengerMessage(senderId, senderName, content, timestamp, groupId, recipientId, isRead, messageId, senderJobIconId);
|
||||
|
||||
string chatId;
|
||||
if (!string.IsNullOrEmpty(groupId))
|
||||
|
|
@ -582,6 +572,7 @@ public sealed partial class MessengerCartridgeSystem
|
|||
{
|
||||
component.ActiveInvites.Add(invite);
|
||||
|
||||
// Воспроизводим рингтон при получении нового инвайта
|
||||
if (TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
{
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Content.Server._Sunrise.CartridgeLoader.Cartridges;
|
|||
/// </summary>
|
||||
public sealed partial class MessengerCartridgeSystem
|
||||
{
|
||||
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, MessengerCartridgeComponent? component, Dictionary<string, PhotoMetadata>? photoGallery = null)
|
||||
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, MessengerCartridgeComponent? component)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
|
@ -57,8 +57,7 @@ public sealed partial class MessengerCartridgeSystem
|
|||
component.MutedGroupChats,
|
||||
unreadCounts,
|
||||
component.ActiveInvites,
|
||||
component.PinnedChats,
|
||||
photoGallery
|
||||
component.PinnedChats
|
||||
);
|
||||
|
||||
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Linq;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.CartridgeLoader;
|
||||
using Content.Server.PDA.Ringer;
|
||||
|
|
@ -130,25 +129,4 @@ public sealed partial class MessengerCartridgeSystem : EntitySystem
|
|||
_deviceNetwork.SetTransmitFrequency(loaderUid, originalFrequency.Value, deviceNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Пытается найти станцию для КПК. Если КПК не на станции, ищет любую станцию на той же карте.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,324 +0,0 @@
|
|||
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<ICommonSession, TimeSpan> _lastCaptureTimes = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = _logManager.GetSawmill("photo.cartridge");
|
||||
|
||||
SubscribeLocalEvent<PhotoCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
|
||||
SubscribeLocalEvent<PhotoCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
||||
|
||||
_netManager.RegisterNetMessage<PdaPhotoCaptureMessage>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик сетевого сообщения с захваченным изображением от клиента
|
||||
/// </summary>
|
||||
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<PhotoCartridgeComponent>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Находит КПК игрока по его сессии
|
||||
/// </summary>
|
||||
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<PdaComponent>(idCardEntity, out _))
|
||||
{
|
||||
return idCardEntity;
|
||||
}
|
||||
|
||||
if (_inventory.TryGetSlotEntity(playerEntity, "belt", out var beltEntity) &&
|
||||
TryComp<PdaComponent>(beltEntity, out _))
|
||||
{
|
||||
return beltEntity;
|
||||
}
|
||||
|
||||
var pdaQuery = EntityQueryEnumerator<PdaComponent>();
|
||||
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<DeviceNetworkComponent>(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<MessengerServerComponent>(station.Value, out var serverAddress))
|
||||
return null;
|
||||
|
||||
var serverQuery = EntityQueryEnumerator<MessengerServerComponent, DeviceNetworkComponent>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -101,12 +101,14 @@ 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,
|
||||
|
|
@ -142,10 +144,12 @@ public sealed partial class MessengerServerSystem
|
|||
_deviceNetwork.QueuePacket(uid, userId, invitePayload);
|
||||
}
|
||||
|
||||
// Отправляем обновленный список инвайтов
|
||||
SendInvitesList(uid, component, userId, serverDevice, pdaFrequency);
|
||||
return;
|
||||
}
|
||||
|
||||
// Для автоматических групп сохраняем старую логику (если разрешено)
|
||||
if (group.AutoGroupPrototypeId != null)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<MessengerAutoGroupPrototype>(group.AutoGroupPrototypeId, out var autoGroupProto))
|
||||
|
|
@ -203,8 +207,7 @@ public sealed partial class MessengerServerSystem
|
|||
["group_id"] = groupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false,
|
||||
["message_id"] = systemMessage.MessageId,
|
||||
["image_path"] = systemMessage.ImagePath ?? string.Empty
|
||||
["message_id"] = systemMessage.MessageId
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
|
|
@ -270,9 +273,7 @@ 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,
|
||||
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty,
|
||||
["image_path"] = msg.ImagePath ?? string.Empty
|
||||
["message_id"] = msg.MessageId
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -376,9 +377,7 @@ public sealed partial class MessengerServerSystem
|
|||
["timestamp"] = timestamp.TotalSeconds,
|
||||
["group_id"] = groupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false,
|
||||
["message_id"] = systemMessage.MessageId,
|
||||
["image_path"] = systemMessage.ImagePath ?? string.Empty
|
||||
["is_read"] = false
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
|
|
@ -464,8 +463,7 @@ public sealed partial class MessengerServerSystem
|
|||
["group_id"] = groupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false,
|
||||
["message_id"] = systemMessage.MessageId,
|
||||
["image_path"] = systemMessage.ImagePath ?? string.Empty
|
||||
["message_id"] = systemMessage.MessageId
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
|
|
@ -531,9 +529,7 @@ 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,
|
||||
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty,
|
||||
["image_path"] = msg.ImagePath ?? string.Empty
|
||||
["message_id"] = msg.MessageId
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -726,8 +722,7 @@ public sealed partial class MessengerServerSystem
|
|||
["group_id"] = groupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false,
|
||||
["message_id"] = systemMessage.MessageId,
|
||||
["image_path"] = systemMessage.ImagePath ?? string.Empty
|
||||
["message_id"] = systemMessage.MessageId
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
|
|
|
|||
|
|
@ -16,16 +16,7 @@ public sealed partial class MessengerServerSystem
|
|||
if (!args.Data.TryGetValue(MessengerCommands.CmdSendMessage, out NetworkPayload? messageData))
|
||||
return;
|
||||
|
||||
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))
|
||||
if (!messageData.TryGetValue("content", out string? content) || string.IsNullOrWhiteSpace(content))
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(args.SenderAddress, out var sender))
|
||||
|
|
@ -37,21 +28,21 @@ public sealed partial class MessengerServerSystem
|
|||
|
||||
if (messageData.TryGetValue("group_id", out string? groupId) && !string.IsNullOrWhiteSpace(groupId))
|
||||
{
|
||||
SendGroupMessage(uid, component, sender, groupId, content, timestamp, imagePath);
|
||||
SendGroupMessage(uid, component, sender, groupId, content, timestamp);
|
||||
}
|
||||
else if (messageData.TryGetValue("recipient_id", out string? recipientId) && !string.IsNullOrWhiteSpace(recipientId))
|
||||
{
|
||||
SendPersonalMessage(uid, component, sender, recipientId, content, timestamp, imagePath);
|
||||
SendPersonalMessage(uid, component, sender, recipientId, content, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendPersonalMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string recipientId, string content, TimeSpan timestamp, string? imagePath = null)
|
||||
private void SendPersonalMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string recipientId, string content, TimeSpan timestamp)
|
||||
{
|
||||
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, imagePath);
|
||||
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, null, recipientId, isRead: false, messageId, sender.JobIconId);
|
||||
var chatId = GetPersonalChatId(sender.UserId, recipientId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(chatId, out var history))
|
||||
|
|
@ -100,8 +91,7 @@ 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,
|
||||
["image_path"] = message.ImagePath ?? string.Empty
|
||||
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty
|
||||
};
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
|
|
@ -131,9 +121,7 @@ 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,
|
||||
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty,
|
||||
["image_path"] = message.ImagePath ?? string.Empty
|
||||
["message_id"] = message.MessageId
|
||||
}
|
||||
},
|
||||
["chat_id"] = chatId
|
||||
|
|
@ -142,7 +130,7 @@ public sealed partial class MessengerServerSystem
|
|||
}
|
||||
}
|
||||
|
||||
private void SendGroupMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string groupId, string content, TimeSpan timestamp, string? imagePath = null)
|
||||
private void SendGroupMessage(EntityUid uid, MessengerServerComponent component, MessengerUser sender, string groupId, string content, TimeSpan timestamp)
|
||||
{
|
||||
if (!component.Groups.TryGetValue(groupId, out var group))
|
||||
return;
|
||||
|
|
@ -151,7 +139,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, imagePath);
|
||||
var message = new MessengerMessage(sender.UserId, sender.Name, content, timestamp, groupId, null, false, messageId, sender.JobIconId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(groupId, out var history))
|
||||
{
|
||||
|
|
@ -201,8 +189,7 @@ 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,
|
||||
["image_path"] = message.ImagePath ?? string.Empty
|
||||
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
|
|
@ -218,34 +205,6 @@ public sealed partial class MessengerServerSystem
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет личное сообщение с изображением (используется PhotoCartridgeSystem)
|
||||
/// </summary>
|
||||
public void SendPersonalMessageWithImage(EntityUid uid, string senderUserId, string recipientId, string content, string imagePath, TimeSpan timestamp)
|
||||
{
|
||||
if (!TryComp<MessengerServerComponent>(uid, out var component))
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(senderUserId, out var sender))
|
||||
return;
|
||||
|
||||
SendPersonalMessage(uid, component, sender, recipientId, content, timestamp, imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет групповое сообщение с изображением (используется PhotoCartridgeSystem)
|
||||
/// </summary>
|
||||
public void SendGroupMessageWithImage(EntityUid uid, string senderUserId, string groupId, string content, string imagePath, TimeSpan timestamp)
|
||||
{
|
||||
if (!TryComp<MessengerServerComponent>(uid, out var component))
|
||||
return;
|
||||
|
||||
if (!component.Users.TryGetValue(senderUserId, out var sender))
|
||||
return;
|
||||
|
||||
SendGroupMessage(uid, component, sender, groupId, content, timestamp, imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обрабатывает удаление сообщения пользователем
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -210,8 +210,7 @@ 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,
|
||||
["image_path"] = msg.ImagePath ?? string.Empty
|
||||
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -254,8 +253,7 @@ 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,
|
||||
["image_path"] = msg.ImagePath ?? string.Empty
|
||||
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -304,8 +302,7 @@ 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,
|
||||
["image_path"] = message.ImagePath ?? string.Empty
|
||||
["sender_job_icon_id"] = message.SenderJobIconId?.Id ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,15 +59,6 @@ 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)}");
|
||||
|
|
@ -193,6 +184,7 @@ public sealed partial class MessengerServerSystem
|
|||
_deviceNetwork.QueuePacket(uid, userId, response, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
|
||||
// Отправляем список пользователей новому пользователю
|
||||
var usersPayload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdUsersList,
|
||||
|
|
@ -211,6 +203,7 @@ 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<Dictionary<string, object>>();
|
||||
|
||||
|
|
@ -277,6 +270,7 @@ public sealed partial class MessengerServerSystem
|
|||
_deviceNetwork.QueuePacket(uid, userId, groupsPayload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
|
||||
// Регистрируем картридж мессенджера как фоновую программу, если он еще не зарегистрирован
|
||||
if (_cartridgeLoader.TryGetProgram<MessengerCartridgeComponent>(pdaUid, out var cartridgeUid, out _))
|
||||
{
|
||||
if (TryComp<CartridgeLoaderComponent>(pdaUid, out var loader) &&
|
||||
|
|
@ -509,8 +503,7 @@ public sealed partial class MessengerServerSystem
|
|||
["group_id"] = autoGroupProto.GroupId,
|
||||
["recipient_id"] = string.Empty,
|
||||
["is_read"] = false,
|
||||
["message_id"] = systemMessage.MessageId,
|
||||
["image_path"] = systemMessage.ImagePath ?? string.Empty
|
||||
["message_id"] = systemMessage.MessageId
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
|
|
@ -558,9 +551,7 @@ 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,
|
||||
["sender_job_icon_id"] = msg.SenderJobIconId?.Id ?? string.Empty,
|
||||
["image_path"] = msg.ImagePath ?? string.Empty
|
||||
["message_id"] = msg.MessageId
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -651,6 +642,7 @@ public sealed partial class MessengerServerSystem
|
|||
user.DepartmentId = departmentId;
|
||||
user.JobIconId = jobIconId;
|
||||
|
||||
// Отправляем обновленный список пользователей всем клиентам
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server._Sunrise;
|
||||
|
||||
/// <summary>
|
||||
/// A small system that handles event-based cleanup for the NetTexturesManager.
|
||||
/// Since NetTexturesManager is a standalone manager, it cannot safely subscribe to broadcast events.
|
||||
/// </summary>
|
||||
public sealed class NetTexturesCleanupSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
}
|
||||
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
_netTexturesManager.ClearDynamicResources();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ 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;
|
||||
|
|
@ -22,7 +20,7 @@ namespace Content.Server._Sunrise;
|
|||
/// Uses High Bandwidth Transfer (WebSocket) to avoid blocking main game traffic.
|
||||
/// Textures are loaded into MemoryContentRoot on the client.
|
||||
/// </summary>
|
||||
public sealed class NetTexturesManager
|
||||
public sealed class NetTexturesManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Transfer key for server -> client texture downloads via WebSocket
|
||||
|
|
@ -34,40 +32,25 @@ namespace Content.Server._Sunrise;
|
|||
[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/";
|
||||
|
||||
/// <summary>
|
||||
/// 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").
|
||||
/// </summary>
|
||||
private readonly Dictionary<ResPath, byte[]> _dynamicResources = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("network.textures");
|
||||
_netManager.RegisterNetMessage<RequestNetworkResourceMessage>(OnRequestNetworkResource);
|
||||
|
||||
// Register transfer key for High Bandwidth Transfer (WebSocket)
|
||||
_transferManager.RegisterTransferMessage(TransferKeyNetTextures);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all dynamically registered in-memory resources.
|
||||
/// Used during round restarts to prevent memory leaks.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
|
|
@ -80,8 +63,10 @@ namespace Content.Server._Sunrise;
|
|||
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})");
|
||||
|
|
@ -99,12 +84,15 @@ namespace Content.Server._Sunrise;
|
|||
{
|
||||
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(".."))
|
||||
{
|
||||
|
|
@ -112,12 +100,16 @@ namespace Content.Server._Sunrise;
|
|||
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();
|
||||
|
||||
|
|
@ -140,60 +132,60 @@ namespace Content.Server._Sunrise;
|
|||
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)>();
|
||||
|
||||
var relativeUploadPath = resourcePath.ToRelativePath();
|
||||
if (_dynamicResources.TryGetValue(relativeUploadPath, out var dynamicData))
|
||||
// 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)
|
||||
{
|
||||
filesToSend.Add((relativeUploadPath, 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var files = _resourceManager.ContentFindFiles(resourcePath).ToList();
|
||||
|
||||
if (files.Count == 0)
|
||||
// Directory - collect all files
|
||||
foreach (var filePath in files)
|
||||
{
|
||||
if (!_resourceManager.ContentFileExists(resourcePath))
|
||||
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))
|
||||
{
|
||||
_sawmill.Warning($"Resource not found: {resourcePath}");
|
||||
return;
|
||||
_sawmill.Warning($"Failed to read file: {filePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CollectSingleFile(resourcePath, filesToSend))
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var filePath in files)
|
||||
using (stream)
|
||||
{
|
||||
if (!filePath.TryRelativeTo(resourcePath, out var relativePath))
|
||||
continue;
|
||||
var data = new byte[stream.Length];
|
||||
stream.Read(data, 0, data.Length);
|
||||
|
||||
var relativePathValue = relativePath.Value;
|
||||
// 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;
|
||||
|
||||
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));
|
||||
}
|
||||
filesToSend.Add((uploadedPath, data));
|
||||
}
|
||||
|
||||
_sawmill.Debug($"Collected resource directory {resourcePath} ({files.Count} files) for {session.Name}");
|
||||
}
|
||||
|
||||
_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;
|
||||
|
|
@ -213,6 +205,7 @@ namespace Content.Server._Sunrise;
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -233,6 +226,7 @@ namespace Content.Server._Sunrise;
|
|||
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));
|
||||
}
|
||||
|
|
@ -264,72 +258,28 @@ namespace Content.Server._Sunrise;
|
|||
|
||||
var first = true;
|
||||
|
||||
foreach (var (relative, data) in files)
|
||||
{
|
||||
if (!first)
|
||||
foreach (var (relative, data) in files)
|
||||
{
|
||||
continueByte[0] = 1;
|
||||
await stream.WriteAsync(continueByte);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a dynamic in-memory network texture that is not present on disk.
|
||||
/// The resourcePath must point inside /NetTextures/ and will be validated by <see cref="ValidateResourcePath"/>.
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">Rooted resource path, e.g. "/NetTextures/Messenger/photo_123.png".</param>
|
||||
/// <param name="data">Raw file bytes (PNG, WEBP, etc.).</param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a dynamic in-memory network texture.
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">Rooted resource path, e.g. "/NetTextures/Messenger/photo_123.png".</param>
|
||||
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}");
|
||||
}
|
||||
continueByte[0] = 0;
|
||||
await stream.WriteAsync(continueByte);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public sealed partial class CartridgeLoaderComponent : Component
|
|||
/// The maximum amount of programs that can be installed on the cartridge loader entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int DiskSpace = 12; // Sunrise-Edit
|
||||
public int DiskSpace = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the cartridge loader will play notifications if it supports it at all
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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,
|
||||
|
|
@ -29,8 +28,7 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent
|
|||
string? userId = null,
|
||||
string? chatId = null,
|
||||
bool? isMuted = null,
|
||||
long? messageId = null,
|
||||
string? imagePath = null)
|
||||
long? messageId = null)
|
||||
{
|
||||
Action = action;
|
||||
RecipientId = recipientId;
|
||||
|
|
@ -41,7 +39,6 @@ public sealed class MessengerUiMessageEvent : CartridgeMessageEvent
|
|||
ChatId = chatId;
|
||||
IsMuted = isMuted;
|
||||
MessageId = messageId;
|
||||
ImagePath = imagePath;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +60,5 @@ public enum MessengerUiAction
|
|||
DeclineInvite,
|
||||
LeaveGroup,
|
||||
DeleteMessage,
|
||||
TogglePin,
|
||||
RequestPhotos
|
||||
TogglePin
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,6 @@ public sealed class MessengerUiState : BoundUserInterfaceState
|
|||
/// </summary>
|
||||
public HashSet<string> PinnedChats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Галерея фотографий для выбора (опционально)
|
||||
/// </summary>
|
||||
public Dictionary<string, PhotoMetadata>? PhotoGallery { get; }
|
||||
|
||||
public MessengerUiState(
|
||||
bool isRegistered,
|
||||
bool serverAvailable,
|
||||
|
|
@ -80,8 +75,7 @@ public sealed class MessengerUiState : BoundUserInterfaceState
|
|||
HashSet<string> mutedGroupChats,
|
||||
Dictionary<string, int> unreadCounts,
|
||||
List<MessengerGroupInvite> activeInvites,
|
||||
HashSet<string> pinnedChats,
|
||||
Dictionary<string, PhotoMetadata>? photoGallery = null)
|
||||
HashSet<string> pinnedChats)
|
||||
{
|
||||
IsRegistered = isRegistered;
|
||||
ServerAvailable = serverAvailable;
|
||||
|
|
@ -94,6 +88,5 @@ public sealed class MessengerUiState : BoundUserInterfaceState
|
|||
UnreadCounts = unreadCounts;
|
||||
ActiveInvites = activeInvites;
|
||||
PinnedChats = pinnedChats;
|
||||
PhotoGallery = photoGallery;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
using Lidgren.Network;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение для отправки захваченного изображения с клиента на сервер
|
||||
/// </summary>
|
||||
public sealed class PdaPhotoCaptureMessage : NetMessage
|
||||
{
|
||||
public override MsgGroups MsgGroup => MsgGroups.String;
|
||||
|
||||
/// <summary>
|
||||
/// Байты изображения (PNG или WebP)
|
||||
/// </summary>
|
||||
public byte[] ImageData { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>
|
||||
/// Ширина изображения в пикселях
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Высота изображения в пикселях
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Компонент фото-картриджа для КПК
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class PhotoCartridgeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Галерея фотографий пользователя (PhotoId -> PhotoMetadata)
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Dictionary<string, PhotoMetadata> PhotoGallery = new();
|
||||
|
||||
/// <summary>
|
||||
/// Звук срабатывания затвора
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/_Sunrise/camera_shot.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Эффект вспышки
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId FlashEffect = "PhotoFlashEffect";
|
||||
|
||||
/// <summary>
|
||||
/// Включена ли вспышка (пользовательская настройка)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool FlashEnabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Метаданные фотографии в галерее
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PhotoMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Уникальный идентификатор фотографии (GUID)
|
||||
/// </summary>
|
||||
public string PhotoId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Путь к сетевому ресурсу изображения (например, "/NetTextures/Messenger/photo_123.png")
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Время создания фотографии (относительно начала раунда)
|
||||
/// </summary>
|
||||
public TimeSpan Timestamp { get; set; }
|
||||
|
||||
public PhotoMetadata(string photoId, string imagePath, TimeSpan timestamp)
|
||||
{
|
||||
PhotoId = photoId;
|
||||
ImagePath = imagePath;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
using Content.Shared.CartridgeLoader;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Событие сообщения UI фото-картриджа
|
||||
/// </summary>
|
||||
[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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Действия UI фото-картриджа
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum PhotoUiAction
|
||||
{
|
||||
CapturePhoto,
|
||||
SendPhotoToMessenger,
|
||||
RequestGallery,
|
||||
DeletePhoto,
|
||||
ToggleFlash
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние UI фото-картриджа
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PhotoUiState : BoundUserInterfaceState
|
||||
{
|
||||
/// <summary>
|
||||
/// Список фотографий в галерее (PhotoId -> PhotoMetadata)
|
||||
/// </summary>
|
||||
public Dictionary<string, PhotoMetadata> Photos { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Статус камеры (готовность к съемке)
|
||||
/// </summary>
|
||||
public bool CameraReady { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Включена ли вспышка
|
||||
/// </summary>
|
||||
public bool FlashEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение об ошибке (если есть)
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
public PhotoUiState(
|
||||
Dictionary<string, PhotoMetadata> photos,
|
||||
bool cameraReady,
|
||||
bool flashEnabled,
|
||||
string? errorMessage = null)
|
||||
{
|
||||
Photos = photos;
|
||||
CameraReady = cameraReady;
|
||||
FlashEnabled = flashEnabled;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -55,23 +55,7 @@ public sealed class MessengerMessage
|
|||
/// </summary>
|
||||
public ProtoId<JobIconPrototype>? SenderJobIconId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Путь к сетевому изображению, если сообщение содержит картинку.
|
||||
/// Например: "/NetTextures/Messenger/photo_123.png"
|
||||
/// </summary>
|
||||
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<JobIconPrototype>? senderJobIconId = null,
|
||||
string? imagePath = null)
|
||||
public MessengerMessage(string senderId, string senderName, string content, TimeSpan timestamp, string? groupId = null, string? recipientId = null, bool isRead = false, long messageId = 0, ProtoId<JobIconPrototype>? senderJobIconId = null)
|
||||
{
|
||||
SenderId = senderId;
|
||||
SenderName = senderName;
|
||||
|
|
@ -82,6 +66,5 @@ public sealed class MessengerMessage
|
|||
IsRead = isRead;
|
||||
MessageId = messageId;
|
||||
SenderJobIconId = senderJobIconId;
|
||||
ImagePath = imagePath;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -19,6 +19,4 @@ 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.
|
||||
ent-PhotoCartridge = camera cartridge
|
||||
.desc = A program for taking photos and managing a photo gallery.
|
||||
.desc = A program for messaging between PDAs.
|
||||
|
|
@ -6,8 +6,7 @@ nano-task-program-name = NanoTask
|
|||
news-read-program-name = Station news
|
||||
|
||||
crew-manifest-program-name = Crew manifest
|
||||
messenger-program-name = RobustChat
|
||||
photo-program-name = FlashSnap
|
||||
messenger-program-name = Messenger MAX
|
||||
crew-manifest-cartridge-loading = Loading ...
|
||||
|
||||
net-probe-program-name = NetProbe
|
||||
|
|
|
|||
|
|
@ -19,6 +19,4 @@ ent-AstroNavCartridge = Картридж АстроНав
|
|||
ent-NavigatorCartridge = картридж навигатора
|
||||
.desc = Программа для просмотра карты станции в целях навигации.
|
||||
ent-MessengerCartridge = картридж мессенджера
|
||||
.desc = Программа для обмена сообщениями между КПК.
|
||||
ent-PhotoCartridge = картридж фотоаппарата
|
||||
.desc = Программа для съемки фотографий и управления галереей.
|
||||
.desc = Программа для обмена сообщениями между КПК.
|
||||
|
|
@ -2,10 +2,3 @@ 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 = [Фото]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ notekeeper-program-name = Заметки
|
|||
nano-task-program-name = NanoTask
|
||||
news-read-program-name = Новости станции
|
||||
crew-manifest-program-name = Манифест экипажа
|
||||
messenger-program-name = RobustChat
|
||||
messenger-program-name = Мессенджер МАХ
|
||||
crew-manifest-cartridge-loading = Загрузка...
|
||||
net-probe-program-name = Зонд сетей
|
||||
net-probe-scan = Просканирован { $device }!
|
||||
|
|
@ -26,8 +26,6 @@ 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 = Список разыскиваемых
|
||||
|
|
|
|||
|
|
@ -59,7 +59,3 @@ 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 = Вспышка
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
prefix: device-address-prefix-console
|
||||
savableAddress: false
|
||||
- type: WirelessNetworkConnection
|
||||
range: 2000 # Sunrise-Edit
|
||||
range: 500
|
||||
- type: CartridgeLoader
|
||||
uiKey: enum.PdaUiKey.Key
|
||||
preinstalled:
|
||||
|
|
@ -89,7 +89,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
cartridgeSlot:
|
||||
priority: -1
|
||||
|
|
@ -171,7 +170,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -190,7 +188,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -366,7 +363,6 @@
|
|||
- PlantAnalyzerCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -552,7 +548,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -606,7 +601,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -1028,7 +1022,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -1115,7 +1108,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -1168,7 +1160,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
- type: Tag # Ignore Chameleon tags
|
||||
tags:
|
||||
|
|
@ -1422,7 +1413,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -1612,7 +1602,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -1647,7 +1636,6 @@
|
|||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
|
||||
- type: entity
|
||||
|
|
@ -1815,7 +1803,6 @@
|
|||
- NavigatorCartridge
|
||||
- AstroNavCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-end
|
||||
|
||||
- type: entity
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
- 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
|
||||
|
|
@ -26,7 +26,6 @@
|
|||
- MedTekCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-doctor
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
- NetProbeCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
|
||||
- type: entity
|
||||
parent: PrisonEngineerPDA
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
- NewsReaderCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
|
||||
- type: entity
|
||||
parent: SecurityMetusPDA
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@
|
|||
- AstroNavCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-pilot
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
- NewsReaderCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
|
||||
- type: entity
|
||||
parent: PlanetPrisonerPDA
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
- AstroNavCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-scientist
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
- AstroNavCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
- type: PdaAnimationVisuals
|
||||
animatedState: pda-space-prison-worker
|
||||
idInsertedLayerState: id_inserted-space-prison
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- type: entity
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: PlantAnalyzerCartridge
|
||||
name: PlantAnalyzerCartridge
|
||||
|
|
@ -52,22 +52,5 @@
|
|||
programName: messenger-program-name
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Misc/program_icons.rsi
|
||||
state: robust_chat
|
||||
state: max_logo2
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -187,7 +187,6 @@
|
|||
- AstroNavCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
|
||||
- type: entity
|
||||
parent: CaptainPDA
|
||||
|
|
@ -426,7 +425,6 @@
|
|||
- AstroNavCartridge
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
|
||||
- type: entity
|
||||
parent: BasePDA
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
receiveFrequencyId: Messenger
|
||||
autoConnect: false
|
||||
- type: WirelessNetworkConnection
|
||||
range: 2000
|
||||
range: 500
|
||||
- type: StationLimitedNetwork
|
||||
allowNonStationPackets: true
|
||||
- type: ApcPowerReceiver
|
||||
powerLoad: 200
|
||||
- type: DeviceNetworkRequiresPower
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 565 B |
Binary file not shown.
|
Before Width: | Height: | Size: 534 B |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "max icons - discord: seraphimttt. Other take here https://pixelexplosive.itch.io/pixel-art-ui-icon-pack-25-icons-32x32",
|
||||
"copyright": "discord: seraphimttt",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
|
|
@ -15,12 +15,6 @@
|
|||
},
|
||||
{
|
||||
"name": "max_logo3"
|
||||
},
|
||||
{
|
||||
"name": "robust_chat"
|
||||
},
|
||||
{
|
||||
"name": "flash_snap"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 536 B |
Loading…
Add table
Reference in a new issue