Merge remote-tracking branch 'space-sunrise/master'
# Conflicts: # Resources/Prototypes/_Sunrise/Catalog/uplink_catalog.yml # Resources/migration.yml
This commit is contained in:
commit
e521cac521
167 changed files with 4215 additions and 1193 deletions
|
|
@ -43,14 +43,12 @@
|
|||
MinSize="0 0"
|
||||
SizeFlagsStretchRatio="2"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Name="PageTextContainer"
|
||||
MinSize="0 0"
|
||||
Orientation="Vertical"
|
||||
SizeFlagsStretchRatio="2"
|
||||
VerticalExpand="True">
|
||||
<!-- Sunrise-Start -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<RichTextLabel Margin="8,8,8,8" Name="PageText" VerticalAlignment="Top"/>
|
||||
<GridContainer Name="ArticlePhotosContainer" Columns="2" HorizontalExpand="True" HSeparationOverride="10" VSeparationOverride="10" Margin="10,0,10,10"/>
|
||||
</BoxContainer>
|
||||
<RichTextLabel Margin="8,8,8,8" Name="PageText" VerticalAlignment="Top"/>
|
||||
<!-- Sunrise-End -->
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5,5,5,5">
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
using Content.Client._Sunrise.UserInterface.CustomControls;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.RichText;
|
||||
using Content.Client.UserInterface.RichText;
|
||||
using Content.Shared.MassMedia.Systems;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Client._Sunrise;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NewsReaderUiFragment : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
public event Action? OnNextButtonPressed;
|
||||
public event Action? OnPrevButtonPressed;
|
||||
|
||||
|
|
@ -21,6 +27,7 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
|
|||
public NewsReaderUiFragment()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Next.OnPressed += _ => OnNextButtonPressed?.Invoke();
|
||||
Prev.OnPressed += _ => OnPrevButtonPressed?.Invoke();
|
||||
|
|
@ -47,6 +54,81 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
|
|||
var author = Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author ?? Loc.GetString("news-read-ui-no-author"));
|
||||
Author.SetMessage(FormattedMessage.FromMarkupPermissive(author), UserFormattableTags.BaseAllowedTags);
|
||||
|
||||
// Sunrise-Start
|
||||
ArticlePhotosContainer.Children.Clear();
|
||||
if (article.PhotoPaths != null)
|
||||
{
|
||||
foreach (var path in article.PhotoPaths)
|
||||
{
|
||||
var photoButton = new ContainerButton
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 4),
|
||||
DefaultCursorShape = Control.CursorShape.Hand
|
||||
};
|
||||
|
||||
var border = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.Transparent,
|
||||
BorderColor = Color.Transparent,
|
||||
BorderThickness = new Thickness(2)
|
||||
}
|
||||
};
|
||||
|
||||
var textureRect = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered,
|
||||
MinHeight = 150
|
||||
};
|
||||
|
||||
border.AddChild(textureRect);
|
||||
photoButton.AddChild(border);
|
||||
ArticlePhotosContainer.AddChild(photoButton);
|
||||
|
||||
photoButton.OnPressed += _ => PhotoPreviewWindow.Open(textureRect.Texture);
|
||||
|
||||
photoButton.OnMouseEntered += _ =>
|
||||
{
|
||||
if (border.PanelOverride is StyleBoxFlat style)
|
||||
{
|
||||
style.BorderColor = Color.White.WithAlpha(0.4f);
|
||||
}
|
||||
};
|
||||
|
||||
photoButton.OnMouseExited += _ =>
|
||||
{
|
||||
if (border.PanelOverride is StyleBoxFlat style)
|
||||
{
|
||||
style.BorderColor = Color.Transparent;
|
||||
}
|
||||
};
|
||||
|
||||
if (_netTexturesManager.EnsureResource(path))
|
||||
{
|
||||
var uploaded = _netTexturesManager.GetUploadedPath(path);
|
||||
if (_resourceCache.TryGetResource<TextureResource>(uploaded, out var tex))
|
||||
textureRect.Texture = tex.Texture;
|
||||
}
|
||||
else
|
||||
{
|
||||
void OnLoaded(string loadedPath)
|
||||
{
|
||||
if (loadedPath != path) return;
|
||||
var uploaded = _netTexturesManager.GetUploadedPath(path);
|
||||
if (_resourceCache.TryGetResource<TextureResource>(uploaded, out var tex))
|
||||
textureRect.Texture = tex.Texture;
|
||||
_netTexturesManager.ResourceLoaded -= OnLoaded;
|
||||
}
|
||||
_netTexturesManager.ResourceLoaded += OnLoaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
Prev.Disabled = targetNum <= 1;
|
||||
Next.Disabled = targetNum >= totalNum;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ namespace Content.Client.Lobby.UI
|
|||
_configurationManager.OnValueChanged(SunriseCCVars.ServerName, OnServerNameChanged, true);
|
||||
|
||||
Chat.SetChatOpacity();
|
||||
Chat.ToggleEmojiButton(true); // Sunrise-Add
|
||||
|
||||
ServerName.Text = Loc.GetString("ui-lobby-welcome", ("name", _serverName));
|
||||
LoadIcons();
|
||||
|
|
|
|||
|
|
@ -40,6 +40,24 @@
|
|||
<RichTextLabel Name="PreviewLabel" VerticalAlignment="Top" Margin="9 3" MaxWidth="360"/>
|
||||
</ScrollContainer>
|
||||
</Control>
|
||||
<!-- Sunrise-Start -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="11 5 11 0">
|
||||
<Label Text="{Loc news-write-ui-photos-label}" Margin="6 0 10 0" VerticalAlignment="Center"/>
|
||||
<Button Name="ButtonAddPhoto" Text="{Loc news-write-ui-add-photo-text}" SetHeight="24"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="PhotoCountLabel" Text="0/10" StyleClasses="LabelWeak" VerticalAlignment="Center" Margin="0 0 6 0"/>
|
||||
</BoxContainer>
|
||||
<Control Name="PhotosPanel" SetHeight="120" Margin="11 5 11 0">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#1a1a1c" BorderThickness="1" BorderColor="#3B3E56"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<ScrollContainer Name="PhotosScroll" HScrollEnabled="True" VScrollEnabled="False" VerticalExpand="True">
|
||||
<BoxContainer Name="PhotosContainer" Orientation="Horizontal" SeparationOverride="5" Margin="5 5" VerticalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</Control>
|
||||
<!-- Sunrise-End -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="12 5 12 8">
|
||||
<Control>
|
||||
<Button Name="ButtonCancel" SetHeight="32" SetWidth="85"
|
||||
|
|
@ -47,11 +65,11 @@
|
|||
</Control>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="ButtonSaveDraft" SetHeight="32" SetWidth="85"
|
||||
StyleClasses="OpenRight" Text="{Loc news-write-ui-save-text}"/>
|
||||
<Button Name="ButtonPreview" SetHeight="32" SetWidth="85"
|
||||
StyleClasses="OpenBoth" Text="{Loc news-write-ui-preview-text}"/>
|
||||
<Button Name="ButtonPublish" SetHeight="32" SetWidth="85" Text="{Loc news-write-ui-publish-text}" Access="Public"/>
|
||||
<Button Name="ButtonSaveDraft" SetHeight="32" SetWidth="120"
|
||||
StyleClasses="OpenRight" Text="{Loc news-write-ui-save-text}"/> <!-- Sunrise-Edit -->
|
||||
<Button Name="ButtonPreview" SetHeight="32" SetWidth="120"
|
||||
StyleClasses="OpenBoth" Text="{Loc news-write-ui-preview-text}"/> <!-- Sunrise-Edit -->
|
||||
<Button Name="ButtonPublish" SetHeight="32" SetWidth="120" Text="{Loc news-write-ui-publish-text}" Access="Public"/> <!-- Sunrise-Edit -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using Content.Client.Message;
|
||||
using System.Numerics;
|
||||
using Content.Client._Sunrise;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.RichText;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.RichText;
|
||||
using Content.Shared.MassMedia.Systems;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
|
@ -16,14 +17,20 @@ namespace Content.Client.MassMedia.Ui;
|
|||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ArticleEditorPanel : Control
|
||||
{
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
public event Action? PublishButtonPressed;
|
||||
public event Action<string, string>? ArticleDraftUpdated;
|
||||
public event Action<string, string, List<string>?>? ArticleDraftUpdated; // Sunrise-Edit
|
||||
public event Action? RequestPhotosPressed; // Sunrise-Add
|
||||
|
||||
private bool _preview;
|
||||
public List<string> PhotoPaths = new(); // Sunrise-Add
|
||||
|
||||
public ArticleEditorPanel()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
ButtonPublish.StyleClasses.Add(StyleClass.ButtonOpenLeft);
|
||||
ButtonPublish.StyleClasses.Add(StyleClass.Positive);
|
||||
|
|
@ -49,10 +56,11 @@ public sealed partial class ArticleEditorPanel : Control
|
|||
ButtonPreview.OnPressed += OnPreview;
|
||||
ButtonCancel.OnPressed += OnCancel;
|
||||
ButtonPublish.OnPressed += OnPublish;
|
||||
ButtonSaveDraft.OnPressed += OnDraftSaved;
|
||||
ButtonSaveDraft.OnPressed += _ => OnDraftSaved();
|
||||
ButtonAddPhoto.OnPressed += _ => RequestPhotosPressed?.Invoke(); // Sunrise-Add
|
||||
|
||||
TitleField.OnTextChanged += args => OnTextChanged(args.Text.Length, args.Control, SharedNewsSystem.MaxTitleLength);
|
||||
ContentField.OnTextChanged += args => OnTextChanged(Rope.CalcTotalLength(args.TextRope), args.Control, SharedNewsSystem.MaxContentLength);
|
||||
TitleField.OnTextChanged += OnTitleChanged;
|
||||
ContentField.OnTextChanged += OnContentChanged;
|
||||
}
|
||||
|
||||
private void OnTextChanged(long length, Control control, long maxLength)
|
||||
|
|
@ -75,7 +83,7 @@ public sealed partial class ArticleEditorPanel : Control
|
|||
}
|
||||
|
||||
// save draft regardless; they can edit down the length later
|
||||
ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope));
|
||||
ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope), PhotoPaths); // Sunrise-Edit
|
||||
}
|
||||
|
||||
private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
|
||||
|
|
@ -85,8 +93,16 @@ public sealed partial class ArticleEditorPanel : Control
|
|||
TextEditPanel.Visible = !_preview;
|
||||
PreviewPanel.Visible = _preview;
|
||||
|
||||
var articleBody = Rope.Collapse(ContentField.TextRope);
|
||||
PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), UserFormattableTags.BaseAllowedTags);
|
||||
if (_preview)
|
||||
{
|
||||
var articleBody = Rope.Collapse(ContentField.TextRope);
|
||||
PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), UserFormattableTags.BaseAllowedTags);
|
||||
ButtonPreview.Text = Loc.GetString("news-write-ui-write-text");
|
||||
}
|
||||
else
|
||||
{
|
||||
ButtonPreview.Text = Loc.GetString("news-write-ui-preview-text");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
|
||||
|
|
@ -98,16 +114,18 @@ public sealed partial class ArticleEditorPanel : Control
|
|||
private void OnPublish(BaseButton.ButtonEventArgs eventArgs)
|
||||
{
|
||||
PublishButtonPressed?.Invoke();
|
||||
Reset();
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
private void OnDraftSaved(BaseButton.ButtonEventArgs eventArgs)
|
||||
private void OnDraftSaved()
|
||||
{
|
||||
ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope));
|
||||
ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope), PhotoPaths); // Sunrise-Edit
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
private void OnTitleChanged(LineEdit.LineEditEventArgs args) => OnTextChanged(args.Text.Length, args.Control, SharedNewsSystem.MaxTitleLength);
|
||||
private void OnContentChanged(TextEdit.TextEditEventArgs args) => OnTextChanged(Rope.CalcTotalLength(args.TextRope), args.Control, SharedNewsSystem.MaxContentLength);
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_preview = false;
|
||||
|
|
@ -116,9 +134,115 @@ public sealed partial class ArticleEditorPanel : Control
|
|||
PreviewLabel.SetMarkup("");
|
||||
TitleField.Text = "";
|
||||
ContentField.TextRope = Rope.Leaf.Empty;
|
||||
ArticleDraftUpdated?.Invoke(string.Empty, string.Empty);
|
||||
// Sunrise-Start
|
||||
PhotoPaths.Clear();
|
||||
UpdatePhotosUI();
|
||||
// Sunrise-End
|
||||
ArticleDraftUpdated?.Invoke(string.Empty, string.Empty, null); // Sunrise-Edit
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
public void UpdatePhotosUI()
|
||||
{
|
||||
PhotosContainer.Children.Clear();
|
||||
foreach (var path in PhotoPaths)
|
||||
{
|
||||
var photoControl = new SelectedPhotoControl(path, _netTexturesManager, _resourceCache);
|
||||
photoControl.OnRemove += () =>
|
||||
{
|
||||
PhotoPaths.Remove(path);
|
||||
UpdatePhotosUI();
|
||||
ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope), PhotoPaths);
|
||||
};
|
||||
PhotosContainer.AddChild(photoControl);
|
||||
}
|
||||
|
||||
UpdatePhotoCountLabel();
|
||||
}
|
||||
|
||||
private void UpdatePhotoCountLabel()
|
||||
{
|
||||
PhotoCountLabel.Text = $"{PhotoPaths.Count}/10";
|
||||
ButtonAddPhoto.Disabled = PhotoPaths.Count >= 10;
|
||||
PhotoCountLabel.ModulateSelfOverride = PhotoPaths.Count >= 10 ? Color.Red : null;
|
||||
}
|
||||
|
||||
private sealed class SelectedPhotoControl : Control
|
||||
{
|
||||
public event Action? OnRemove;
|
||||
|
||||
private readonly string _path;
|
||||
private readonly NetTexturesManager _netTextures;
|
||||
private readonly IResourceCache _cache;
|
||||
private readonly TextureRect _textureRect;
|
||||
|
||||
public SelectedPhotoControl(string path, NetTexturesManager netTextures, IResourceCache cache)
|
||||
{
|
||||
_path = path;
|
||||
_netTextures = netTextures;
|
||||
_cache = cache;
|
||||
|
||||
SetSize = new Vector2(100, 100);
|
||||
|
||||
_textureRect = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered
|
||||
};
|
||||
AddChild(_textureRect);
|
||||
|
||||
var removeButton = new Button
|
||||
{
|
||||
StyleClasses = { StyleClass.ButtonSmall },
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
SetSize = new Vector2(24, 24),
|
||||
Margin = new Thickness(0, 2, 2, 0),
|
||||
};
|
||||
removeButton.AddChild(
|
||||
new Label
|
||||
{
|
||||
Text = "✖",
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 0, 2),
|
||||
});
|
||||
removeButton.OnPressed += _ => OnRemove?.Invoke();
|
||||
AddChild(removeButton);
|
||||
|
||||
_netTextures.ResourceLoaded += OnResourceLoaded;
|
||||
|
||||
if (_netTextures.EnsureResource(path))
|
||||
{
|
||||
UpdateTexture();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResourceLoaded(string path)
|
||||
{
|
||||
if (path == _path)
|
||||
UpdateTexture();
|
||||
}
|
||||
|
||||
private void UpdateTexture()
|
||||
{
|
||||
var uploaded = _netTextures.GetUploadedPath(_path);
|
||||
if (_cache.TryGetResource<TextureResource>(uploaded, out var tex))
|
||||
_textureRect.Texture = tex.Texture;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_netTextures.ResourceLoaded -= OnResourceLoaded;
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
|
@ -128,5 +252,8 @@ public sealed partial class ArticleEditorPanel : Control
|
|||
ButtonPreview.OnPressed -= OnPreview;
|
||||
ButtonCancel.OnPressed -= OnCancel;
|
||||
ButtonPublish.OnPressed -= OnPublish;
|
||||
ButtonSaveDraft.OnPressed -= _ => OnDraftSaved();
|
||||
TitleField.OnTextChanged -= OnTitleChanged;
|
||||
ContentField.OnTextChanged -= OnContentChanged;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using JetBrains.Annotations;
|
|||
using Content.Shared.MassMedia.Systems;
|
||||
using Content.Shared.MassMedia.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.MassMedia.Ui;
|
||||
|
|
@ -12,6 +11,7 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
|
|||
{
|
||||
[ViewVariables]
|
||||
private NewsWriterMenu? _menu;
|
||||
private PhotoSelectorWindow? _selector; // Sunrise-Edit
|
||||
|
||||
public NewsWriterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
|
|
@ -29,6 +29,7 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
|
|||
|
||||
_menu.CreateButtonPressed += OnCreateButtonPressed;
|
||||
_menu.ArticleEditorPanel.ArticleDraftUpdated += OnArticleDraftUpdated;
|
||||
_menu.ArticleEditorPanel.RequestPhotosPressed += OnRequestPhotosPressed; // Sunrise-Edit
|
||||
|
||||
SendMessage(new NewsWriterArticlesRequestMessage());
|
||||
}
|
||||
|
|
@ -36,11 +37,57 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
|
|||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (state is not NewsWriterBoundUserInterfaceState cast)
|
||||
// Sunrise-Start
|
||||
if (state is NewsWriterBoundUserInterfaceState cast)
|
||||
{
|
||||
_menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish, cast.DraftTitle, cast.DraftContent);
|
||||
if (_menu != null)
|
||||
{
|
||||
_menu.ArticleEditorPanel.PhotoPaths = cast.DraftPhotoPaths != null ? new List<string>(cast.DraftPhotoPaths) : new List<string>();
|
||||
_menu.ArticleEditorPanel.UpdatePhotosUI();
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
base.ReceiveMessage(message);
|
||||
if (message is NewsWriterPhotosMessage photosMsg)
|
||||
{
|
||||
if (_selector != null && _selector.IsOpen)
|
||||
{
|
||||
_selector.Populate(photosMsg.Photos);
|
||||
return;
|
||||
}
|
||||
|
||||
_selector = new PhotoSelectorWindow();
|
||||
_selector.PhotoSelected += path =>
|
||||
{
|
||||
if (_menu == null) return;
|
||||
if (!_menu.ArticleEditorPanel.PhotoPaths.Contains(path))
|
||||
{
|
||||
_menu.ArticleEditorPanel.PhotoPaths.Add(path);
|
||||
_menu.ArticleEditorPanel.UpdatePhotosUI();
|
||||
OnArticleDraftUpdated(_menu.ArticleEditorPanel.TitleField.Text, Rope.Collapse(_menu.ArticleEditorPanel.ContentField.TextRope), _menu.ArticleEditorPanel.PhotoPaths);
|
||||
}
|
||||
};
|
||||
_selector.OnClose += () => _selector = null;
|
||||
_selector.Populate(photosMsg.Photos);
|
||||
_selector.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish, cast.DraftTitle, cast.DraftContent);
|
||||
_selector?.Close();
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
private void OnPublishButtonPressed()
|
||||
{
|
||||
|
|
@ -62,7 +109,7 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
|
|||
: $"{stringContent[..(SharedNewsSystem.MaxContentLength - 3)]}...";
|
||||
|
||||
|
||||
SendMessage(new NewsWriterPublishMessage(name, content));
|
||||
SendMessage(new NewsWriterPublishMessage(name, content, _menu.ArticleEditorPanel.PhotoPaths)); // Sunrise-Edit
|
||||
}
|
||||
|
||||
private void OnDeleteButtonPressed(int articleNum)
|
||||
|
|
@ -78,8 +125,15 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
|
|||
SendMessage(new NewsWriterRequestDraftMessage());
|
||||
}
|
||||
|
||||
private void OnArticleDraftUpdated(string title, string content)
|
||||
private void OnArticleDraftUpdated(string title, string content, List<string>? photoPaths) // Sunrise-Edit
|
||||
{
|
||||
SendMessage(new NewsWriterSaveDraftMessage(title, content));
|
||||
SendMessage(new NewsWriterSaveDraftMessage(title, content, photoPaths)); // Sunrise-Edit
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private void OnRequestPhotosPressed()
|
||||
{
|
||||
SendMessage(new NewsWriterRequestPhotosMessage());
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
xmlns:ui="clr-namespace:Content.Client.MassMedia.Ui"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'news-write-ui-default-title'}"
|
||||
MinSize="348 443"
|
||||
SetSize="348 443">
|
||||
MinSize="500 600"
|
||||
SetSize="510 600"> <!-- Sunrise-Edit -->
|
||||
|
||||
<ui:ArticleEditorPanel Name="ArticleEditorPanel" HorizontalAlignment="Left" VerticalExpand="True"
|
||||
MinWidth="410" MinHeight="370" Margin="0 0 0 30" Access="Public" Visible="False"/>
|
||||
MinWidth="510" MinHeight="370" Margin="0 0 0 30" Access="Public" Visible="False"/> <!-- Sunrise-Edit -->
|
||||
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<Control VerticalExpand="True" HorizontalExpand="True" Margin="10 10 10 0">
|
||||
|
|
|
|||
5
Content.Client/MassMedia/Ui/PhotoSelectorWindow.xaml
Normal file
5
Content.Client/MassMedia/Ui/PhotoSelectorWindow.xaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io">
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<GridContainer Name="PhotosContainer" Columns="2" HSeparationOverride="5" VSeparationOverride="5" Margin="5"/>
|
||||
</ScrollContainer>
|
||||
</DefaultWindow>
|
||||
60
Content.Client/MassMedia/Ui/PhotoSelectorWindow.xaml.cs
Normal file
60
Content.Client/MassMedia/Ui/PhotoSelectorWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client._Sunrise;
|
||||
using Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.MassMedia.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PhotoSelectorWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
public event Action<string>? PhotoSelected;
|
||||
|
||||
public PhotoSelectorWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Title = Loc.GetString("news-write-ui-select-photo-title");
|
||||
MinSize = new Vector2(560, 620);
|
||||
}
|
||||
|
||||
public void Populate(List<PhotoMetadata> photos)
|
||||
{
|
||||
PhotosContainer.Children.Clear();
|
||||
|
||||
if (photos.Count == 0)
|
||||
{
|
||||
PhotosContainer.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("news-write-ui-no-photos"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Margin = new Thickness(0, 20)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var metadata in photos.OrderByDescending(p => p.Timestamp))
|
||||
{
|
||||
var photoControl = new PhotoItemControl(metadata.ImagePath, metadata, _netTexturesManager, _resourceCache, _gameTiming);
|
||||
photoControl.MinSize = new Vector2(120, 120);
|
||||
photoControl.OnPressed += _ =>
|
||||
{
|
||||
PhotoSelected?.Invoke(metadata.ImagePath);
|
||||
Close();
|
||||
};
|
||||
PhotosContainer.AddChild(photoControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ using Content.Shared._Sunrise.CollectiveMind;
|
|||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Decals;
|
||||
|
|
@ -140,6 +141,8 @@ public sealed partial class ChatUIController : UIController
|
|||
private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbles
|
||||
= new();
|
||||
|
||||
private bool _roundEnded; // Sunrise-Add
|
||||
|
||||
private readonly HashSet<ChatBox> _chats = new();
|
||||
public IReadOnlySet<ChatBox> Chats => _chats;
|
||||
|
||||
|
|
@ -190,6 +193,7 @@ public sealed partial class ChatUIController : UIController
|
|||
_net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
||||
_net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
|
||||
SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
|
||||
SubscribeNetworkEvent<RoundEndMessageEvent>((_, _) => OnRoundEnd()); // Sunrise-Add
|
||||
_config.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
|
||||
_chatNameColorsEnabled = _config.GetCVar(CCVars.ChatEnableColorName);
|
||||
|
||||
|
|
@ -250,6 +254,7 @@ public sealed partial class ChatUIController : UIController
|
|||
}
|
||||
|
||||
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
|
||||
_config.OnValueChanged(CCVars.OocEnabled, _ => UpdateChannelPermissions()); // Sunrise-Add
|
||||
|
||||
InitializeHighlights();
|
||||
}
|
||||
|
|
@ -422,11 +427,24 @@ public sealed partial class ChatUIController : UIController
|
|||
if (args.NewState is GameplayState)
|
||||
{
|
||||
PreferredChannel = ChatSelectChannel.Local;
|
||||
_roundEnded = false; // Sunrise-Add
|
||||
}
|
||||
else
|
||||
{
|
||||
_roundEnded = false; // Sunrise-Add
|
||||
}
|
||||
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private void OnRoundEnd()
|
||||
{
|
||||
_roundEnded = true;
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
public void SetSpeechBubbleRoot(LayoutContainer root)
|
||||
{
|
||||
_speechBubbleRoot.Orphan();
|
||||
|
|
@ -594,8 +612,29 @@ public sealed partial class ChatUIController : UIController
|
|||
CanSendChannelsChanged?.Invoke(CanSendChannels);
|
||||
FilterableChannelsChanged?.Invoke(FilterableChannels);
|
||||
SelectableChannelsChanged?.Invoke(SelectableChannels);
|
||||
|
||||
// Sunrise-Start
|
||||
var showEmoji = ShouldShowEmojiButton();
|
||||
foreach (var chat in _chats)
|
||||
{
|
||||
chat.ToggleEmojiButton(showEmoji);
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private bool ShouldShowEmojiButton()
|
||||
{
|
||||
if (_state.CurrentState is not GameplayStateBase)
|
||||
return true;
|
||||
|
||||
if (_roundEnded || _config.GetCVar(CCVars.OocEnabled)) // Sunrise-Edit
|
||||
return true;
|
||||
|
||||
return _admin.HasFlag(AdminFlags.Adminchat) || (_ghost is { IsGhost: true });
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
public void ClearUnfilteredUnreads(ChatChannel channels)
|
||||
{
|
||||
foreach (var channel in _unreadMessages.Keys.ToArray())
|
||||
|
|
@ -746,6 +785,18 @@ public sealed partial class ChatUIController : UIController
|
|||
if (text.Length == 0)
|
||||
return (ChatSelectChannel.None, text, null, null); // Sunrise-Edit
|
||||
|
||||
// Sunrise-Start
|
||||
if (text.StartsWith(SharedChatSystem.RadioChannelPrefix) && text.Length > 2 && text.IndexOf(SharedChatSystem.RadioChannelPrefix, 1) > 0)
|
||||
{
|
||||
var secondColon = text.IndexOf(SharedChatSystem.RadioChannelPrefix, 1);
|
||||
var space = text.IndexOf(' ');
|
||||
if (space == -1 || space > secondColon)
|
||||
{
|
||||
return (ChatSelectChannel.None, text, null, null);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
// We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command
|
||||
// because ????????
|
||||
|
||||
|
|
@ -953,6 +1004,7 @@ public sealed partial class ChatUIController : UIController
|
|||
public void RegisterChat(ChatBox chat)
|
||||
{
|
||||
_chats.Add(chat);
|
||||
chat.ToggleEmojiButton(ShouldShowEmojiButton()); // Sunrise-Edit
|
||||
}
|
||||
|
||||
public void UnregisterChat(ChatBox chat)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public class ChatInputBox : PanelContainer
|
|||
public readonly ChannelSelectorButton ChannelSelector;
|
||||
public readonly HistoryLineEdit Input;
|
||||
public readonly ChannelFilterButton FilterButton;
|
||||
public Button? EmojiButton; // Sunrise-Add
|
||||
protected readonly BoxContainer Container;
|
||||
protected ChatChannel ActiveChannel { get; private set; } = ChatChannel.Local;
|
||||
|
||||
|
|
@ -52,6 +53,28 @@ public class ChatInputBox : PanelContainer
|
|||
ChannelSelector.OnChannelSelect += UpdateActiveChannel;
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
public void ToggleEmojiButton(bool visible)
|
||||
{
|
||||
if (visible && EmojiButton == null)
|
||||
{
|
||||
EmojiButton = new Button
|
||||
{
|
||||
Name = "EmojiButton",
|
||||
Text = "☻",
|
||||
SetWidth = 30,
|
||||
ToolTip = Loc.GetString("messenger-emoji-button-tooltip")
|
||||
};
|
||||
// Insert before FilterButton
|
||||
Container.AddChild(EmojiButton);
|
||||
FilterButton.SetPositionInParent(Container.ChildCount - 1);
|
||||
}
|
||||
|
||||
if (EmojiButton != null)
|
||||
EmojiButton.Visible = visible;
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
private void UpdateActiveChannel(ChatSelectChannel selectedChannel)
|
||||
{
|
||||
ActiveChannel = (ChatChannel) selectedChannel;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ using Robust.Shared.Input;
|
|||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Robust.Client.UserInterface.RichText; // Sunrise-Edit
|
||||
using Content.Client._Sunrise.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using static Robust.Client.UserInterface.Controls.LineEdit;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Chat.Widgets;
|
||||
|
|
@ -27,6 +28,18 @@ public partial class ChatBox : UIWidget
|
|||
// По умолчаюнию разрешены только RichTextEntry.DefaultTags.
|
||||
// Теги ниже нужны для корректного отображения иконок в чате
|
||||
private static readonly Type[] TagsAllowed =
|
||||
[
|
||||
typeof(BoldItalicTag),
|
||||
typeof(BoldTag),
|
||||
typeof(BulletTag),
|
||||
typeof(ColorTag),
|
||||
typeof(HeadingTag),
|
||||
typeof(ItalicTag),
|
||||
typeof(Client._Sunrise.UserInterface.RichText.RadioIconTag),
|
||||
typeof(_Sunrise.Messenger.EmojiTag),
|
||||
];
|
||||
|
||||
private static readonly Type[] TagsAllowedNoEmoji =
|
||||
[
|
||||
typeof(BoldItalicTag),
|
||||
typeof(BoldTag),
|
||||
|
|
@ -75,6 +88,37 @@ public partial class ChatBox : UIWidget
|
|||
{
|
||||
_controller.SetChatWindowOpacity(_configurationManager.GetCVar(CCVars.ChatWindowOpacity));
|
||||
}
|
||||
|
||||
private EmojiPickerWindow? _emojiPicker;
|
||||
|
||||
private bool _emojiButtonSubscribed;
|
||||
|
||||
public void ToggleEmojiButton(bool visible)
|
||||
{
|
||||
ChatInput.ToggleEmojiButton(visible);
|
||||
if (ChatInput.EmojiButton != null && !_emojiButtonSubscribed)
|
||||
{
|
||||
_emojiButtonSubscribed = true;
|
||||
ChatInput.EmojiButton.OnPressed += _ =>
|
||||
{
|
||||
if (_emojiPicker != null && _emojiPicker.IsOpen)
|
||||
{
|
||||
_emojiPicker.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_emojiPicker = new EmojiPickerWindow();
|
||||
_emojiPicker.OnEmojiSelected += emojiCode =>
|
||||
{
|
||||
ChatInput.Input.Text += emojiCode;
|
||||
ChatInput.Input.CursorPosition = ChatInput.Input.Text.Length;
|
||||
ChatInput.Input.GrabKeyboardFocus();
|
||||
};
|
||||
_emojiPicker.OnClose += () => _emojiPicker = null;
|
||||
_emojiPicker.OpenCentered();
|
||||
};
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
private void OnTextEntered(LineEditEventArgs args)
|
||||
|
|
@ -97,7 +141,7 @@ public partial class ChatBox : UIWidget
|
|||
|
||||
var color = msg.MessageColorOverride ?? msg.Channel.TextColor();
|
||||
|
||||
AddLine(msg.WrappedMessage, color);
|
||||
AddLine(msg.WrappedMessage, color, msg.Channel); // Sunrise-Edit
|
||||
}
|
||||
|
||||
private void OnHighlightsUpdated(string highlights)
|
||||
|
|
@ -155,14 +199,24 @@ public partial class ChatBox : UIWidget
|
|||
}
|
||||
// Sunrise-End
|
||||
|
||||
public void AddLine(string message, Color color)
|
||||
public void AddLine(string message, Color color, ChatChannel channel = ChatChannel.None)
|
||||
{
|
||||
// Sunrise-Start
|
||||
var allowEmoji = channel == ChatChannel.None ||
|
||||
(channel & (ChatChannel.OOC | ChatChannel.LOOC | ChatChannel.Dead | ChatChannel.AdminRelated | ChatChannel.Server)) != 0;
|
||||
|
||||
if (allowEmoji && _entManager.TrySystem<Client._Sunrise.Messenger.ClientEmojiSystem>(out var emojiSystem))
|
||||
{
|
||||
message = emojiSystem.ParseEmojis(message);
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
var formatted = new FormattedMessage(3);
|
||||
formatted.PushColor(color);
|
||||
formatted.AddMarkupOrThrow(message);
|
||||
formatted.Pop();
|
||||
Contents.AddMessage(formatted);
|
||||
Contents.SetMessage(^1, formatted, TagsAllowed); // Sunrise-Edit
|
||||
Contents.SetMessage(^1, formatted, allowEmoji ? TagsAllowed : TagsAllowedNoEmoji); // Sunrise-Edit
|
||||
}
|
||||
|
||||
public void Focus(ChatSelectChannel? channel = null)
|
||||
|
|
|
|||
|
|
@ -314,6 +314,25 @@ namespace Content.Client.Viewport
|
|||
return Vector2.Transform(vpLocal, matrix);
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
public Vector2 WorldToRenderTargetPixels(Vector2 map)
|
||||
{
|
||||
if (_eye == null)
|
||||
return default;
|
||||
|
||||
EnsureViewportCreated();
|
||||
|
||||
return _viewport!.WorldToLocal(map);
|
||||
}
|
||||
public Vector2 RenderTargetPixelsToWorld(Vector2 pixels)
|
||||
{
|
||||
if (_eye == null || _viewport == null)
|
||||
return default;
|
||||
|
||||
return _viewport.LocalToWorld(pixels).Position;
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
public Matrix3x2 GetWorldToScreenMatrix()
|
||||
{
|
||||
EnsureViewportCreated();
|
||||
|
|
|
|||
|
|
@ -568,7 +568,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
|
||||
var lifetime = 0.4f;
|
||||
|
||||
if (TryComp<TimedDespawnComponent>(gunUid, out var despawn))
|
||||
if (TryComp<TimedDespawnComponent>(ent, out var despawn)) // Sunrise-Edit
|
||||
{
|
||||
lifetime = despawn.Lifetime;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ using Robust.Client.AutoGenerated;
|
|||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
|
|
@ -12,10 +16,16 @@ public sealed partial class AddUserDialog : DefaultWindow
|
|||
{
|
||||
public event Action<string>? OnUserSelected;
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
private List<MessengerUser> _availableUsers = new();
|
||||
|
||||
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
public AddUserDialog()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
Title = Loc.GetString("messenger-add-user-title");
|
||||
|
||||
|
|
@ -47,11 +57,29 @@ public sealed partial class AddUserDialog : DefaultWindow
|
|||
{
|
||||
var userButton = new Button
|
||||
{
|
||||
Text = user.Name,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(2)
|
||||
};
|
||||
|
||||
var buttonContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(4, 0)
|
||||
};
|
||||
|
||||
CreateJobIconRect(user.JobIconId, buttonContainer);
|
||||
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = user.Name,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(4, 0)
|
||||
};
|
||||
buttonContainer.AddChild(nameLabel);
|
||||
|
||||
userButton.AddChild(buttonContainer);
|
||||
|
||||
var userId = user.UserId;
|
||||
userButton.OnPressed += _ =>
|
||||
{
|
||||
|
|
@ -62,4 +90,36 @@ public sealed partial class AddUserDialog : DefaultWindow
|
|||
UsersList.AddChild(userButton);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateJobIconRect(ProtoId<JobIconPrototype>? jobIconId, Control container)
|
||||
{
|
||||
if (jobIconId != null && _prototypeManager.TryIndex(jobIconId.Value, out JobIconPrototype? jobIcon))
|
||||
{
|
||||
var iconRect = new TextureRect
|
||||
{
|
||||
Texture = GetSpriteSystem().Frame0(jobIcon.Icon),
|
||||
SetWidth = 20,
|
||||
SetHeight = 20,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Margin = new Thickness(0, 0, 4, 0)
|
||||
};
|
||||
container.AddChild(iconRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unknownIconId = new ProtoId<JobIconPrototype>("JobIconUnknown");
|
||||
if (_prototypeManager.TryIndex(unknownIconId, out JobIconPrototype? unknownIcon))
|
||||
{
|
||||
var iconRect = new TextureRect
|
||||
{
|
||||
Texture = GetSpriteSystem().Frame0(unknownIcon.Icon),
|
||||
SetWidth = 20,
|
||||
SetHeight = 20,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Margin = new Thickness(0, 0, 4, 0)
|
||||
};
|
||||
container.AddChild(iconRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System.Numerics;
|
||||
using Content.Client._Sunrise.Messenger;
|
||||
using Content.Client._Sunrise.UserInterface.CustomControls;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
|
|
@ -175,25 +175,7 @@ public sealed partial class MessagePanel : PanelContainer
|
|||
|
||||
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();
|
||||
PhotoPreviewWindow.Open(ImagePreview.Texture);
|
||||
}
|
||||
|
||||
private void OnResourceLoaded(string resourcePath)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client._Sunrise.Messenger;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Robust.Client.AutoGenerated;
|
||||
|
|
@ -17,9 +15,9 @@ using Robust.Client.UserInterface.XAML;
|
|||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Client._Sunrise.UserInterface.CustomControls;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
|
|
@ -69,27 +67,10 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
|
||||
private CreateGroupDialog? _createGroupDialog;
|
||||
private AddUserDialog? _addUserDialog;
|
||||
private DefaultWindow? _emojiPickerDialog;
|
||||
private EmojiPickerWindow? _emojiPickerDialog;
|
||||
|
||||
private readonly List<string> _recentEmojis = new();
|
||||
private readonly HashSet<string> _favoriteEmojis = new();
|
||||
|
||||
private BoxContainer? _emojiPickerContentContainer;
|
||||
|
||||
private ClientEmojiSystem EmojiSystem => _entitySystemManager.GetEntitySystem<ClientEmojiSystem>();
|
||||
private SpriteSystem GetSpriteSystem() => _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
private static readonly Type[] MessageTagsAllowed =
|
||||
[
|
||||
typeof(BoldItalicTag),
|
||||
typeof(BoldTag),
|
||||
typeof(BulletTag),
|
||||
typeof(ColorTag),
|
||||
typeof(HeadingTag),
|
||||
typeof(ItalicTag),
|
||||
typeof(EmojiTag),
|
||||
];
|
||||
|
||||
public MessengerUiFragment()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
|
@ -113,8 +94,6 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
PersonalChatsTab.Pressed = true;
|
||||
|
||||
MessagesContainer.OnScrolled += OnMessagesScrolled;
|
||||
|
||||
LoadSavedEmojis();
|
||||
}
|
||||
|
||||
private void OnMessagesScrolled()
|
||||
|
|
@ -552,6 +531,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
var pinnedChats = state.PinnedChats;
|
||||
|
||||
var sortedUsers = usersList.OrderByDescending(u => pinnedChats.Contains(GetPersonalChatId(u.UserId)))
|
||||
.ThenByDescending(u => state.UnreadCounts.TryGetValue(GetPersonalChatId(u.UserId), out var count) && count > 0)
|
||||
.ThenBy(u => u.Name)
|
||||
.ToList();
|
||||
|
||||
|
|
@ -582,6 +562,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
var pinnedChats = state.PinnedChats;
|
||||
|
||||
var sortedGroups = state.Groups.OrderByDescending(g => pinnedChats.Contains(g.GroupId))
|
||||
.ThenByDescending(g => state.UnreadCounts.TryGetValue(g.GroupId, out var count) && count > 0)
|
||||
.ThenBy(g => g.Name)
|
||||
.ToList();
|
||||
|
||||
|
|
@ -1218,7 +1199,7 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
foreach (var (id, metadata) in photos)
|
||||
foreach (var (id, metadata) in photos.OrderByDescending(p => p.Value.Timestamp))
|
||||
{
|
||||
var photoControl = new PhotoItemControl(id, metadata,
|
||||
_netTexturesManager,
|
||||
|
|
@ -1342,449 +1323,24 @@ public sealed partial class MessengerUiFragment : BoxContainer
|
|||
{
|
||||
if (_emojiPickerDialog != null && _emojiPickerDialog.IsOpen)
|
||||
{
|
||||
UpdateEmojiPickerContent();
|
||||
_emojiPickerDialog.UpdateEmojiPickerContent();
|
||||
return;
|
||||
}
|
||||
|
||||
var emojiSystem = EmojiSystem;
|
||||
var spriteSystem = GetSpriteSystem();
|
||||
|
||||
_emojiPickerDialog?.Close();
|
||||
|
||||
var dialog = new DefaultWindow
|
||||
{
|
||||
Title = Loc.GetString("messenger-emoji-picker-title"),
|
||||
MinSize = new Vector2(445, 400),
|
||||
Resizable = false,
|
||||
};
|
||||
var dialog = new EmojiPickerWindow();
|
||||
_emojiPickerDialog = dialog;
|
||||
|
||||
var container = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(8)
|
||||
};
|
||||
|
||||
var scrollContainer = new ScrollContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
HScrollEnabled = false,
|
||||
ReserveScrollbarSpace = true
|
||||
};
|
||||
|
||||
var contentContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 5)
|
||||
};
|
||||
_emojiPickerContentContainer = contentContainer;
|
||||
|
||||
BuildEmojiPickerContent(contentContainer, emojiSystem, spriteSystem);
|
||||
|
||||
scrollContainer.AddChild(contentContainer);
|
||||
container.AddChild(scrollContainer);
|
||||
|
||||
dialog.Contents.AddChild(container);
|
||||
dialog.OnClose += () =>
|
||||
{
|
||||
_emojiPickerDialog = null;
|
||||
_emojiPickerContentContainer = null;
|
||||
};
|
||||
dialog.OpenCentered();
|
||||
}
|
||||
|
||||
private void UpdateEmojiPickerContent()
|
||||
{
|
||||
if (_emojiPickerContentContainer == null)
|
||||
return;
|
||||
|
||||
var emojiSystem = EmojiSystem;
|
||||
var spriteSystem = GetSpriteSystem();
|
||||
|
||||
_emojiPickerContentContainer.RemoveAllChildren();
|
||||
BuildEmojiPickerContent(_emojiPickerContentContainer, emojiSystem, spriteSystem);
|
||||
}
|
||||
|
||||
private void BuildEmojiPickerContent(BoxContainer contentContainer, ClientEmojiSystem emojiSystem, SpriteSystem spriteSystem)
|
||||
{
|
||||
var allEmojis = emojiSystem.GetAllEmojis().ToList();
|
||||
var emojiDict = allEmojis.ToDictionary(e => e.Code, e => e);
|
||||
|
||||
BuildRecentEmojisSection(contentContainer, emojiDict, spriteSystem);
|
||||
BuildFavoriteEmojisSection(contentContainer, allEmojis, spriteSystem);
|
||||
BuildAllEmojisSection(contentContainer, allEmojis, spriteSystem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает стиль панели для секций эмодзи
|
||||
/// </summary>
|
||||
private StyleBoxTexture CreateEmojiPanelStyle()
|
||||
{
|
||||
var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/rounded_button_bordered.svg.96dpi.png");
|
||||
var panelStyle = new StyleBoxTexture
|
||||
{
|
||||
Texture = panelTex
|
||||
};
|
||||
panelStyle.SetPatchMargin(StyleBox.Margin.All, 5);
|
||||
panelStyle.SetContentMarginOverride(StyleBox.Margin.All, 8);
|
||||
panelStyle.Modulate = Color.FromHex("#2F2F35");
|
||||
return panelStyle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает секцию недавних эмодзи
|
||||
/// </summary>
|
||||
private void BuildRecentEmojisSection(BoxContainer contentContainer, Dictionary<string, EmojiPrototype> emojiDict, SpriteSystem spriteSystem)
|
||||
{
|
||||
var recentPanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
};
|
||||
recentPanel.PanelOverride = CreateEmojiPanelStyle();
|
||||
|
||||
var recentSection = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var recentLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-recent-title"),
|
||||
StyleClasses = { "Bold" },
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
};
|
||||
recentSection.AddChild(recentLabel);
|
||||
|
||||
if (_recentEmojis.Count > 0)
|
||||
{
|
||||
var recentContainer = new GridContainer
|
||||
{
|
||||
Columns = 5,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
var recentToShow = _recentEmojis.TakeLast(5).Reverse().ToList();
|
||||
foreach (var emojiCode in recentToShow)
|
||||
{
|
||||
if (emojiDict.TryGetValue(emojiCode, out var emoji))
|
||||
{
|
||||
var button = CreateEmojiButton(emoji, spriteSystem, false);
|
||||
recentContainer.AddChild(button);
|
||||
}
|
||||
}
|
||||
recentSection.AddChild(recentContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var hintLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-recent-empty-hint"),
|
||||
StyleClasses = { "LabelSubText" }
|
||||
};
|
||||
recentSection.AddChild(hintLabel);
|
||||
}
|
||||
recentPanel.AddChild(recentSection);
|
||||
contentContainer.AddChild(recentPanel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает секцию избранных эмодзи
|
||||
/// </summary>
|
||||
private void BuildFavoriteEmojisSection(BoxContainer contentContainer, List<EmojiPrototype> allEmojis, SpriteSystem spriteSystem)
|
||||
{
|
||||
var favoritePanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
};
|
||||
favoritePanel.PanelOverride = CreateEmojiPanelStyle();
|
||||
|
||||
var favoriteSection = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var favoriteLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-favorite-title"),
|
||||
StyleClasses = { "Bold" },
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
};
|
||||
favoriteSection.AddChild(favoriteLabel);
|
||||
|
||||
if (_favoriteEmojis.Count > 0)
|
||||
{
|
||||
var favoriteContainer = new GridContainer
|
||||
{
|
||||
Columns = 5,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
var favoriteEmojisList = allEmojis.Where(e => _favoriteEmojis.Contains(e.Code)).ToList();
|
||||
foreach (var emoji in favoriteEmojisList)
|
||||
{
|
||||
var button = CreateEmojiButton(emoji, spriteSystem, true);
|
||||
favoriteContainer.AddChild(button);
|
||||
}
|
||||
favoriteSection.AddChild(favoriteContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var hintFavoriteLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-favorite-hint"),
|
||||
StyleClasses = { "LabelSubText" },
|
||||
Margin = new Thickness(0, 4, 0, 0)
|
||||
};
|
||||
favoriteSection.AddChild(hintFavoriteLabel);
|
||||
}
|
||||
favoritePanel.AddChild(favoriteSection);
|
||||
contentContainer.AddChild(favoritePanel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает секцию всех эмодзи
|
||||
/// </summary>
|
||||
private void BuildAllEmojisSection(BoxContainer contentContainer, List<EmojiPrototype> allEmojis, SpriteSystem spriteSystem)
|
||||
{
|
||||
var allPanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 0)
|
||||
};
|
||||
allPanel.PanelOverride = CreateEmojiPanelStyle();
|
||||
|
||||
var allSection = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var allLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-all-title"),
|
||||
StyleClasses = { "Bold" },
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
};
|
||||
allSection.AddChild(allLabel);
|
||||
|
||||
var allEmojisContainer = new GridContainer
|
||||
{
|
||||
Columns = 5,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var nonFavoriteEmojis = allEmojis.Where(e => !_favoriteEmojis.Contains(e.Code)).ToList();
|
||||
foreach (var emoji in nonFavoriteEmojis)
|
||||
{
|
||||
var button = CreateEmojiButton(emoji, spriteSystem, false);
|
||||
allEmojisContainer.AddChild(button);
|
||||
}
|
||||
|
||||
allSection.AddChild(allEmojisContainer);
|
||||
allPanel.AddChild(allSection);
|
||||
contentContainer.AddChild(allPanel);
|
||||
}
|
||||
|
||||
private Button CreateEmojiButton(EmojiPrototype emoji, SpriteSystem spriteSystem, bool isFavorite)
|
||||
{
|
||||
var emojiButton = new Button
|
||||
{
|
||||
MinSize = new Vector2(75, 75),
|
||||
MaxSize = new Vector2(75, 75),
|
||||
ToolTip = emoji.Code
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var spriteSpec = new SpriteSpecifier.Rsi(new ResPath(emoji.SpritePath), emoji.SpriteState);
|
||||
var state = spriteSystem.RsiStateLike(spriteSpec);
|
||||
|
||||
emojiButton.Label.Visible = false;
|
||||
|
||||
if (state.IsAnimated)
|
||||
{
|
||||
var animatedRect = new AnimatedTextureRect
|
||||
{
|
||||
SetWidth = 65,
|
||||
SetHeight = 65,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center
|
||||
};
|
||||
animatedRect.SetFromSpriteSpecifier(spriteSpec);
|
||||
animatedRect.DisplayRect.HorizontalExpand = true;
|
||||
animatedRect.DisplayRect.VerticalExpand = true;
|
||||
animatedRect.DisplayRect.Stretch = TextureRect.StretchMode.KeepAspectCentered;
|
||||
emojiButton.AddChild(animatedRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var texture = spriteSystem.Frame0(spriteSpec);
|
||||
var textureRect = new TextureRect
|
||||
{
|
||||
Texture = texture,
|
||||
SetWidth = 45,
|
||||
SetHeight = 45,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered
|
||||
};
|
||||
emojiButton.AddChild(textureRect);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
emojiButton.Text = emoji.Code;
|
||||
}
|
||||
|
||||
var emojiCode = emoji.Code;
|
||||
|
||||
emojiButton.OnPressed += _ =>
|
||||
dialog.OnEmojiSelected += (emojiCode) =>
|
||||
{
|
||||
var currentText = MessageInput.Text;
|
||||
MessageInput.Text = currentText + emojiCode;
|
||||
MessageInput.CursorPosition = MessageInput.Text.Length;
|
||||
|
||||
AddToRecentEmojis(emojiCode);
|
||||
|
||||
if (_emojiPickerDialog != null && _emojiPickerDialog.IsOpen)
|
||||
{
|
||||
UpdateEmojiPickerContent();
|
||||
}
|
||||
};
|
||||
|
||||
emojiButton.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
{
|
||||
args.Handle();
|
||||
ToggleFavoriteEmoji(emojiCode);
|
||||
|
||||
if (_emojiPickerDialog != null && _emojiPickerDialog.IsOpen)
|
||||
{
|
||||
UpdateEmojiPickerContent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return emojiButton;
|
||||
}
|
||||
|
||||
private void AddToRecentEmojis(string emojiCode)
|
||||
{
|
||||
_recentEmojis.Remove(emojiCode);
|
||||
_recentEmojis.Add(emojiCode);
|
||||
if (_recentEmojis.Count > 5)
|
||||
{
|
||||
_recentEmojis.RemoveAt(0);
|
||||
}
|
||||
SaveRecentEmojis();
|
||||
}
|
||||
|
||||
private void ToggleFavoriteEmoji(string emojiCode)
|
||||
{
|
||||
if (!_favoriteEmojis.Add(emojiCode))
|
||||
{
|
||||
_favoriteEmojis.Remove(emojiCode);
|
||||
}
|
||||
|
||||
SaveFavoriteEmojis();
|
||||
}
|
||||
|
||||
private void LoadSavedEmojis()
|
||||
{
|
||||
HashSet<string>? allEmojis = null;
|
||||
try
|
||||
{
|
||||
allEmojis = EmojiSystem.GetAllEmojis().Select(e => e.Code).ToHashSet();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var recentEmojisStr = _configurationManager.GetCVar(SunriseCCVars.MessengerRecentEmojis);
|
||||
if (!string.IsNullOrWhiteSpace(recentEmojisStr))
|
||||
{
|
||||
var emojiCodes = recentEmojisStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
_recentEmojis.Clear();
|
||||
foreach (var code in emojiCodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
continue;
|
||||
|
||||
if (allEmojis != null && !allEmojis.Contains(code))
|
||||
continue;
|
||||
|
||||
_recentEmojis.Add(code);
|
||||
if (_recentEmojis.Count >= 5)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_recentEmojis.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var favoriteEmojisStr = _configurationManager.GetCVar(SunriseCCVars.MessengerFavoriteEmojis);
|
||||
if (!string.IsNullOrWhiteSpace(favoriteEmojisStr))
|
||||
{
|
||||
var emojiCodes = favoriteEmojisStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
_favoriteEmojis.Clear();
|
||||
foreach (var code in emojiCodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
continue;
|
||||
|
||||
if (allEmojis != null && !allEmojis.Contains(code))
|
||||
continue;
|
||||
|
||||
_favoriteEmojis.Add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_favoriteEmojis.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveRecentEmojis()
|
||||
{
|
||||
try
|
||||
{
|
||||
var emojiCodesStr = string.Join(",", _recentEmojis);
|
||||
_configurationManager.SetCVar(SunriseCCVars.MessengerRecentEmojis, emojiCodesStr);
|
||||
_configurationManager.SaveToFile();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveFavoriteEmojis()
|
||||
{
|
||||
try
|
||||
{
|
||||
var emojiCodesStr = string.Join(",", _favoriteEmojis);
|
||||
_configurationManager.SetCVar(SunriseCCVars.MessengerFavoriteEmojis, emojiCodesStr);
|
||||
_configurationManager.SaveToFile();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
dialog.OnClose += () => _emojiPickerDialog = null;
|
||||
dialog.OpenCentered();
|
||||
}
|
||||
|
||||
#region UI Helper Methods
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public sealed class PhotoCaptureOverlay : Overlay
|
|||
}
|
||||
|
||||
var sourcePos = _transformSystem.GetWorldPosition(source.Value);
|
||||
var targetPos = photoSystem.GetCameraPosition(source.Value, photoSystem.CaptureDistance);
|
||||
var targetPos = photoSystem.GetClampedCapturePosition(source.Value, photoSystem.CaptureDistance);
|
||||
|
||||
var targetScreen = _eyeManager.WorldToScreen(targetPos);
|
||||
var playerScreen = _eyeManager.WorldToScreen(sourcePos);
|
||||
|
|
@ -58,10 +58,13 @@ public sealed class PhotoCaptureOverlay : Overlay
|
|||
|
||||
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 size = 3.0f * pixelsPerMeter;
|
||||
|
||||
size = Math.Min(size, Math.Min(vpRect.Width, vpRect.Height));
|
||||
var halfSize = size / 2.0f;
|
||||
|
||||
var rect = UIBox2.FromDimensions(targetScreen.X - halfSize, targetScreen.Y - halfSize, size, size);
|
||||
var color = Color.Black.WithAlpha(0.5f);
|
||||
|
||||
screenHandle.DrawRect(new UIBox2(vpRect.Left, vpRect.Top, vpRect.Right, rect.Top), color);
|
||||
|
|
|
|||
|
|
@ -1,39 +1,41 @@
|
|||
using System.IO;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
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.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class PhotoCartridgeClientSystem : EntitySystem
|
||||
{
|
||||
[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 IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly NetTexturesManager _netTexturesManager = default!;
|
||||
|
||||
private TimeSpan _nextCaptureTime = TimeSpan.Zero;
|
||||
public bool CameraReady => _timing.CurTime >= _nextCaptureTime;
|
||||
public float CaptureDistance { get; set; } = 2.0f;
|
||||
public float MinCaptureDistance { get; set; } = 1.0f;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
|
|
@ -43,7 +45,6 @@ public sealed class PhotoCartridgeClientSystem : EntitySystem
|
|||
public override void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("photo.cartridge.client");
|
||||
_netManager.RegisterNetMessage<PdaPhotoCaptureMessage>(accept: NetMessageAccept.Server);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
|
|
@ -92,6 +93,9 @@ public sealed class PhotoCartridgeClientSystem : EntitySystem
|
|||
|
||||
public void CaptureAndSendPhoto(EntityUid source)
|
||||
{
|
||||
if (TryComp<CartridgeComponent>(source, out var cart) && cart.LoaderUid.HasValue)
|
||||
source = cart.LoaderUid.Value;
|
||||
|
||||
if (_timing.CurTime < _nextCaptureTime)
|
||||
return;
|
||||
|
||||
|
|
@ -100,6 +104,32 @@ public sealed class PhotoCartridgeClientSystem : EntitySystem
|
|||
Timer.Spawn(500, () => CaptureInternal(source));
|
||||
}
|
||||
|
||||
public Vector2 GetClampedCapturePosition(EntityUid source, float distance)
|
||||
{
|
||||
var targetPos = GetCameraPosition(source, distance);
|
||||
|
||||
if (_stateManager.CurrentState is not IMainViewportState viewportState)
|
||||
return targetPos;
|
||||
|
||||
var targetScreen = _eyeManager.WorldToScreen(targetPos);
|
||||
var sourcePos = _transformSystem.GetWorldPosition(source);
|
||||
var sourceScreen = _eyeManager.WorldToScreen(sourcePos);
|
||||
var offsetScreen = _eyeManager.WorldToScreen(sourcePos + new Vector2(1, 0));
|
||||
var pixelsPerMeter = (offsetScreen - sourceScreen).Length();
|
||||
|
||||
var size = 3.0f * pixelsPerMeter;
|
||||
var control = (Control)viewportState.Viewport;
|
||||
var vpRect = control.GlobalPixelRect;
|
||||
|
||||
size = Math.Min(size, Math.Min(vpRect.Width, vpRect.Height));
|
||||
var halfSize = size / 2f;
|
||||
|
||||
var clampedX = Math.Clamp(targetScreen.X, vpRect.Left + halfSize, vpRect.Right - halfSize);
|
||||
var clampedY = Math.Clamp(targetScreen.Y, vpRect.Top + halfSize, vpRect.Bottom - halfSize);
|
||||
|
||||
return _eyeManager.ScreenToMap(new Vector2(clampedX, clampedY)).Position;
|
||||
}
|
||||
|
||||
private void CaptureInternal(EntityUid source)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
|
|
@ -108,51 +138,47 @@ public sealed class PhotoCartridgeClientSystem : EntitySystem
|
|||
return;
|
||||
}
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not { } player)
|
||||
if (_playerManager.LocalEntity is not { })
|
||||
return;
|
||||
|
||||
if (_stateManager.CurrentState is not IMainViewportState viewportState || viewportState.Viewport is not Control control)
|
||||
if (_stateManager.CurrentState is not IMainViewportState viewportState)
|
||||
return;
|
||||
|
||||
var viewport = viewportState.Viewport.Viewport;
|
||||
var sourcePos = _transformSystem.GetWorldPosition(source);
|
||||
var targetPos = GetClampedCapturePosition(source, CaptureDistance);
|
||||
|
||||
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();
|
||||
var center = viewport.WorldToRenderTargetPixels(targetPos);
|
||||
var p1 = viewport.WorldToRenderTargetPixels(sourcePos);
|
||||
var p2 = viewport.WorldToRenderTargetPixels(sourcePos + new Vector2(1, 0));
|
||||
var pixelSize = 3.0f * (p2 - p1).Length();
|
||||
|
||||
viewport.Screenshot(img => {
|
||||
var scaleX = (float)img.Width / control.Size.X;
|
||||
var scaleY = (float)img.Height / control.Size.Y;
|
||||
var size = (int)pixelSize;
|
||||
size = Math.Min(size, Math.Min(img.Width, img.Height));
|
||||
|
||||
var texturePixelsPerMeter = logicalPixelsPerMeter * scaleX;
|
||||
var size = (int)(3.0f * texturePixelsPerMeter);
|
||||
var left = (int)(center.X - size / 2f);
|
||||
var top = (int)(center.Y - size / 2f);
|
||||
|
||||
var localTargetLogical = targetScreen - control.GlobalPosition;
|
||||
var centerX = localTargetLogical.X * scaleX;
|
||||
var centerY = localTargetLogical.Y * scaleY;
|
||||
left = Math.Clamp(left, 0, img.Width - size);
|
||||
top = Math.Clamp(top, 0, img.Height - size);
|
||||
|
||||
var x = (int)(centerX - size / 2f);
|
||||
var y = (int)(centerY - size / 2f);
|
||||
if (size <= 0)
|
||||
{
|
||||
img.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
var finalRect = new Rectangle(left, top, size, size);
|
||||
TakeScreenshot(source, img, finalRect);
|
||||
});
|
||||
}
|
||||
|
||||
private void TakeScreenshot<T>(Image<T> screenshot, SixLabors.ImageSharp.Rectangle cropRect) where T : unmanaged, IPixel<T>
|
||||
private void TakeScreenshot<T>(EntityUid source, Image<T> screenshot, Rectangle cropRect) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessCapturedImage(screenshot, cropRect);
|
||||
ProcessCapturedImage(source, screenshot, cropRect);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -164,40 +190,48 @@ public sealed class PhotoCartridgeClientSystem : EntitySystem
|
|||
}
|
||||
}
|
||||
|
||||
private void ProcessCapturedImage<T>(Image<T> image, SixLabors.ImageSharp.Rectangle cropRect) where T : unmanaged, IPixel<T>
|
||||
private void ProcessCapturedImage<T>(EntityUid source, Image<T> image, Rectangle cropRect) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var processed = image.Clone(ctx =>
|
||||
{
|
||||
ctx.Crop(cropRect);
|
||||
ctx.Resize(TargetPhotoWidth, TargetPhotoHeight);
|
||||
});
|
||||
if (cropRect.Width <= 0 || cropRect.Height <= 0)
|
||||
return;
|
||||
|
||||
var width = processed.Width;
|
||||
var height = processed.Height;
|
||||
var rescaled = new Image<T>(TargetPhotoWidth, TargetPhotoHeight);
|
||||
var rescaledSpan = rescaled.GetPixelSpan();
|
||||
var sourceSpan = image.GetPixelSpan();
|
||||
|
||||
float scaleX = (float)cropRect.Width / TargetPhotoWidth;
|
||||
float scaleY = (float)cropRect.Height / TargetPhotoHeight;
|
||||
|
||||
for (int y = 0; y < TargetPhotoHeight; y++)
|
||||
{
|
||||
for (int x = 0; x < TargetPhotoWidth; x++)
|
||||
{
|
||||
int srcX = cropRect.X + (int)(x * scaleX);
|
||||
int srcY = cropRect.Y + (int)(y * scaleY);
|
||||
|
||||
if (srcX >= 0 && srcX < image.Width && srcY >= 0 && srcY < image.Height)
|
||||
{
|
||||
rescaledSpan[y * TargetPhotoWidth + x] = sourceSpan[srcY * image.Width + srcX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var width = rescaled.Width;
|
||||
var height = rescaled.Height;
|
||||
|
||||
byte[] imageData;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
processed.SaveAsPng(memoryStream);
|
||||
rescaled.SaveAsPng(memoryStream);
|
||||
imageData = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
processed.Dispose();
|
||||
SendPhotoToServer(imageData, width, height);
|
||||
rescaled.Dispose();
|
||||
SendPhotoToServer(source, imageData, width, height);
|
||||
}
|
||||
|
||||
private void SendPhotoToServer(byte[] imageData, int width, int height)
|
||||
private void SendPhotoToServer(EntityUid source, byte[] imageData, int width, int height)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
return;
|
||||
|
||||
var message = new PdaPhotoCaptureMessage
|
||||
{
|
||||
ImageData = imageData,
|
||||
Width = width,
|
||||
Height = height
|
||||
};
|
||||
|
||||
_netManager.ClientSendMessage(message);
|
||||
_netTexturesManager.SendPhotoToServer(_entityManager.GetNetEntity(source), imageData, width, height);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Viewport;
|
||||
|
|
@ -67,10 +65,10 @@ public sealed partial class PhotoUiFragment : BoxContainer
|
|||
SwitchTab(true);
|
||||
|
||||
var system = _entityManager.System<PhotoCartridgeClientSystem>();
|
||||
ZoomSlider.Value = (system.CaptureDistance - 2.0f) / 8.0f;
|
||||
ZoomSlider.Value = (system.CaptureDistance - system.MinCaptureDistance) / 8.0f;
|
||||
ZoomSlider.OnValueChanged += args =>
|
||||
{
|
||||
system.CaptureDistance = 2.0f + args.Value * 8.0f;
|
||||
system.CaptureDistance = system.MinCaptureDistance + args.Value * 8.0f;
|
||||
};
|
||||
|
||||
SetupCameraPreview();
|
||||
|
|
@ -140,7 +138,7 @@ public sealed partial class PhotoUiFragment : BoxContainer
|
|||
if (CameraPreview != null)
|
||||
{
|
||||
CameraPreview.Eye = _previewEye;
|
||||
CameraPreview.ViewportSize = new Robust.Shared.Maths.Vector2i(256, 256);
|
||||
CameraPreview.ViewportSize = new Vector2i(256, 256);
|
||||
CameraPreview.RenderScaleMode = ScalingViewportRenderScaleMode.Fixed;
|
||||
CameraPreview.FixedRenderScale = 1;
|
||||
|
||||
|
|
@ -175,7 +173,7 @@ public sealed partial class PhotoUiFragment : BoxContainer
|
|||
var xform = _entityManager.GetComponent<TransformComponent>(source);
|
||||
var sourcePos = xform.MapPosition;
|
||||
|
||||
var targetPos = photoSystem.GetCameraPosition(source, photoSystem.CaptureDistance);
|
||||
var targetPos = photoSystem.GetClampedCapturePosition(source, photoSystem.CaptureDistance);
|
||||
|
||||
_previewEye.Position = sourcePos.Offset(targetPos - sourcePos.Position);
|
||||
_previewEye.Rotation = _eyeManager.CurrentEye.Rotation;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@ using System.IO;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared._Sunrise.NetTextures;
|
||||
using Robust.Client;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Upload;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Transfer;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._Sunrise;
|
||||
|
|
@ -39,14 +37,14 @@ public sealed class NetTexturesManager
|
|||
|
||||
private const string UploadedPrefix = "/Uploaded";
|
||||
private readonly HashSet<string> _requestedResources = new();
|
||||
private readonly Dictionary<string, ResPath> _pendingResources = new(); // resourcePath -> ResPath
|
||||
private readonly Dictionary<string, ResPath> _pendingResources = new();
|
||||
|
||||
private readonly MemoryContentRoot _netTexturesContentRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a network texture becomes available.
|
||||
/// </summary>
|
||||
public event Action<string>? ResourceLoaded; // resourcePath
|
||||
public event Action<string>? ResourceLoaded;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
|
|
@ -55,10 +53,8 @@ public sealed class NetTexturesManager
|
|||
|
||||
_transferManager.RegisterTransferMessage(TransferKeyNetTextures, ReceiveNetTexturesTransfer);
|
||||
|
||||
// NetworkResourceUploadMessage is already registered by SharedNetworkResourceManager
|
||||
// We'll check for loaded resources in Update() method very frequently
|
||||
_netManager.RegisterNetMessage<PdaPhotoCaptureMessage>(accept: NetMessageAccept.Server);
|
||||
|
||||
// Clear resources when disconnecting to ensure fresh state on reconnection
|
||||
_baseClient.RunLevelChanged += OnRunLevelChanged;
|
||||
}
|
||||
|
||||
|
|
@ -78,20 +74,16 @@ public sealed class NetTexturesManager
|
|||
|
||||
try
|
||||
{
|
||||
// Read transfer stream using the same format as SharedNetworkResourceManager
|
||||
// But without IAsyncEnumerable to avoid sandbox violations
|
||||
await ReadTransferStream(stream, (relative, data) =>
|
||||
{
|
||||
fileCount++;
|
||||
totalSize += data.Length;
|
||||
_sawmill.Verbose($"Storing NetTexture: {relative} ({ByteHelpers.FormatBytes(data.Length)})");
|
||||
|
||||
// Store file on main thread (required for MemoryContentRoot)
|
||||
_taskManager.RunOnMainThread(() =>
|
||||
{
|
||||
_netTexturesContentRoot.AddOrUpdateFile(relative, data);
|
||||
|
||||
// Check if any pending resources are now available
|
||||
CheckPendingResourcesAfterLoad(relative);
|
||||
});
|
||||
});
|
||||
|
|
@ -149,7 +141,6 @@ public sealed class NetTexturesManager
|
|||
var relativePath = resPath.ToRelativePath();
|
||||
bool exists = false;
|
||||
|
||||
// For RSI directories, check if all files are present
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
|
|
@ -157,7 +148,6 @@ public sealed class NetTexturesManager
|
|||
}
|
||||
else
|
||||
{
|
||||
// Single file - check through resource manager (our content root is added there)
|
||||
var uploadedPath = (new ResPath(UploadedPrefix) / relativePath).ToRootedPath();
|
||||
exists = _resourceManager.ContentFileExists(uploadedPath);
|
||||
}
|
||||
|
|
@ -188,34 +178,24 @@ public sealed class NetTexturesManager
|
|||
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
// If there are no pending resources, skip checking
|
||||
if (_pendingResources.Count == 0)
|
||||
return;
|
||||
|
||||
// Check for loaded resources every frame when there are pending resources
|
||||
// This ensures we catch resources as soon as they're loaded
|
||||
var completedResources = new List<string>();
|
||||
foreach (var (resourcePath, resPath) in _pendingResources)
|
||||
{
|
||||
// Check if resource is available
|
||||
// MemoryContentRoot stores paths relative to /Uploaded prefix
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
|
||||
bool exists = false;
|
||||
ResPath checkPath;
|
||||
|
||||
// For RSI directories, check if all files are present
|
||||
// This ensures all PNG files are present, not just meta.json
|
||||
// Check if path ends with .rsi (more reliable than Extension property)
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
// Check if all RSI files are present by reading meta.json and verifying all PNG files exist
|
||||
exists = CheckRsiFilesComplete(relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single file - check through resource manager
|
||||
var uploadedPath = (new ResPath(UploadedPrefix) / relativePath).ToRootedPath();
|
||||
exists = _resourceManager.ContentFileExists(uploadedPath);
|
||||
}
|
||||
|
|
@ -241,7 +221,6 @@ public sealed class NetTexturesManager
|
|||
/// <returns>True if the resource is available, false if it's being requested</returns>
|
||||
public bool EnsureResource(string resourcePath)
|
||||
{
|
||||
// Normalize the path
|
||||
ResPath resPath;
|
||||
if (resourcePath.StartsWith("/"))
|
||||
{
|
||||
|
|
@ -253,14 +232,11 @@ public sealed class NetTexturesManager
|
|||
resPath = rootPath / resourcePath;
|
||||
}
|
||||
|
||||
// Check if the resource is actually available
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
var uploadedPath = (new ResPath(UploadedPrefix) / relativePath).ToRootedPath();
|
||||
|
||||
bool isAvailable = false;
|
||||
|
||||
// For RSI directories, check for meta.json
|
||||
// Check if path ends with .rsi (more reliable than Extension property)
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
|
|
@ -270,40 +246,31 @@ public sealed class NetTexturesManager
|
|||
}
|
||||
else
|
||||
{
|
||||
// Single file
|
||||
isAvailable = _resourceManager.ContentFileExists(uploadedPath);
|
||||
}
|
||||
|
||||
if (isAvailable)
|
||||
{
|
||||
// Resource is available
|
||||
if (!_requestedResources.Contains(resourcePath))
|
||||
_requestedResources.Add(resourcePath);
|
||||
// Remove from pending if it was there
|
||||
_pendingResources.Remove(resourcePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resource is not available yet
|
||||
// If it's already in pending, we're already tracking it
|
||||
if (_pendingResources.ContainsKey(resourcePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it was requested before but not in pending, add it to pending to track it
|
||||
if (_requestedResources.Contains(resourcePath))
|
||||
{
|
||||
_pendingResources[resourcePath] = resPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request the resource for the first time
|
||||
RequestResource(resourcePath);
|
||||
_pendingResources[resourcePath] = resPath;
|
||||
|
||||
// Immediately check if the resource is already available (might be cached or loaded very fast)
|
||||
// This helps catch resources that load synchronously
|
||||
CheckResourceImmediately(resourcePath, resPath);
|
||||
|
||||
return false;
|
||||
|
|
@ -314,7 +281,6 @@ public sealed class NetTexturesManager
|
|||
if (_requestedResources.Contains(resourcePath))
|
||||
return;
|
||||
|
||||
// Check if client is connected to server before trying to send message
|
||||
if (!_netManager.IsConnected)
|
||||
{
|
||||
_sawmill.Debug($"Cannot request resource {resourcePath}: client not connected to server");
|
||||
|
|
@ -340,17 +306,13 @@ public sealed class NetTexturesManager
|
|||
var relativePath = resPath.ToRelativePath();
|
||||
bool exists = false;
|
||||
|
||||
// For RSI directories, check if all files are present
|
||||
// Check if path ends with .rsi (more reliable than Extension property)
|
||||
var pathStr = relativePath.ToString();
|
||||
if (pathStr.EndsWith(".rsi") || pathStr.EndsWith(".rsi/"))
|
||||
{
|
||||
// Check if all RSI files are present by reading meta.json and verifying all PNG files exist
|
||||
exists = CheckRsiFilesComplete(relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single file - check through resource manager
|
||||
var uploadedPath = (new ResPath(UploadedPrefix) / relativePath).ToRootedPath();
|
||||
exists = _resourceManager.ContentFileExists(uploadedPath);
|
||||
}
|
||||
|
|
@ -383,7 +345,7 @@ public sealed class NetTexturesManager
|
|||
|
||||
var relativePath = resPath.ToRelativePath();
|
||||
var path = new ResPath(UploadedPrefix) / relativePath;
|
||||
return path.ToRootedPath(); // Ensure it's always rooted
|
||||
return path.ToRootedPath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -395,17 +357,14 @@ public sealed class NetTexturesManager
|
|||
{
|
||||
try
|
||||
{
|
||||
// Read meta.json from uploaded path (check through VFS)
|
||||
var uploadedPath = (new ResPath(UploadedPrefix) / relativePath).ToRootedPath();
|
||||
var metaUploadedPath = (uploadedPath / "meta.json").ToRootedPath();
|
||||
|
||||
// First check if meta.json exists in VFS
|
||||
if (!_resourceManager.ContentFileExists(metaUploadedPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to read meta.json to verify it's accessible
|
||||
if (!_resourceManager.TryContentFileRead(metaUploadedPath, out var metaStream))
|
||||
{
|
||||
return false;
|
||||
|
|
@ -413,22 +372,17 @@ public sealed class NetTexturesManager
|
|||
|
||||
using (metaStream)
|
||||
{
|
||||
// Read JSON text
|
||||
using var reader = new StreamReader(metaStream);
|
||||
var jsonText = reader.ReadToEnd();
|
||||
|
||||
// Simple regex to extract state names from JSON
|
||||
// Matches "name": "statename" patterns
|
||||
var namePattern = new Regex(@"""name""\s*:\s*""([^""]+)""", RegexOptions.Compiled);
|
||||
var matches = namePattern.Matches(jsonText);
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
// No states found, might be invalid JSON or empty states array
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if all PNG files for each state exist in VFS
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (match.Groups.Count < 2)
|
||||
|
|
@ -440,7 +394,6 @@ public sealed class NetTexturesManager
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check if PNG file exists in VFS (not just MemoryContentRoot)
|
||||
var pngUploadedPath = (uploadedPath / $"{stateName}.png").ToRootedPath();
|
||||
if (!_resourceManager.ContentFileExists(pngUploadedPath))
|
||||
{
|
||||
|
|
@ -458,5 +411,32 @@ public sealed class NetTexturesManager
|
|||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a captured photo to the server.
|
||||
/// This method is used by PhotoCartridgeClientSystem to avoid registering its own network message.
|
||||
/// </summary>
|
||||
/// <param name="loaderUid">Uid of the cartridge loader (PDA) that took the photo</param>
|
||||
/// <param name="imageData">PNG image data</param>
|
||||
/// <param name="width">Image width in pixels</param>
|
||||
/// <param name="height">Image height in pixels</param>
|
||||
public void SendPhotoToServer(NetEntity loaderUid, byte[] imageData, int width, int height)
|
||||
{
|
||||
if (!_netManager.IsConnected)
|
||||
{
|
||||
_sawmill.Warning("Cannot send photo: client not connected to server");
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new PdaPhotoCaptureMessage
|
||||
{
|
||||
LoaderUid = loaderUid,
|
||||
ImageData = imageData,
|
||||
Width = width,
|
||||
Height = height
|
||||
};
|
||||
|
||||
_netManager.ClientSendMessage(message);
|
||||
_sawmill.Debug($"Sent photo to server: {width}x{height}, {imageData.Length} bytes, loader: {loaderUid}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,398 @@
|
|||
using Content.Client._Sunrise.Messenger;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Resources;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client._Sunrise.UserInterface.CustomControls;
|
||||
|
||||
public sealed class EmojiPickerWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
private readonly BoxContainer? _emojiPickerContentContainer;
|
||||
private readonly List<string> _recentEmojis = new();
|
||||
private readonly HashSet<string> _favoriteEmojis = new();
|
||||
|
||||
public event Action<string>? OnEmojiSelected;
|
||||
|
||||
private ClientEmojiSystem EmojiSystem => _entitySystemManager.GetEntitySystem<ClientEmojiSystem>();
|
||||
private SpriteSystem SpriteSystem => _entitySystemManager.GetEntitySystem<SpriteSystem>();
|
||||
|
||||
public EmojiPickerWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Title = Loc.GetString("messenger-emoji-picker-title");
|
||||
MinSize = new Vector2(445, 400);
|
||||
Resizable = false;
|
||||
|
||||
var container = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(8)
|
||||
};
|
||||
|
||||
var scrollContainer = new ScrollContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
HScrollEnabled = false,
|
||||
ReserveScrollbarSpace = true
|
||||
};
|
||||
|
||||
_emojiPickerContentContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 5)
|
||||
};
|
||||
|
||||
scrollContainer.AddChild(_emojiPickerContentContainer);
|
||||
container.AddChild(scrollContainer);
|
||||
Contents.AddChild(container);
|
||||
|
||||
LoadSavedEmojis();
|
||||
UpdateEmojiPickerContent();
|
||||
}
|
||||
|
||||
public void UpdateEmojiPickerContent()
|
||||
{
|
||||
if (_emojiPickerContentContainer == null)
|
||||
return;
|
||||
|
||||
_emojiPickerContentContainer.RemoveAllChildren();
|
||||
var allEmojis = EmojiSystem.GetAllEmojis().ToList();
|
||||
var emojiDict = allEmojis.ToDictionary(e => e.Code, e => e);
|
||||
|
||||
BuildRecentEmojisSection(_emojiPickerContentContainer, emojiDict);
|
||||
BuildFavoriteEmojisSection(_emojiPickerContentContainer, allEmojis);
|
||||
BuildAllEmojisSection(_emojiPickerContentContainer, allEmojis);
|
||||
}
|
||||
|
||||
private StyleBoxTexture CreateEmojiPanelStyle()
|
||||
{
|
||||
var panelTex = _resourceCache.GetTexture("/Textures/Interface/Nano/rounded_button_bordered.svg.96dpi.png");
|
||||
var panelStyle = new StyleBoxTexture
|
||||
{
|
||||
Texture = panelTex
|
||||
};
|
||||
panelStyle.SetPatchMargin(StyleBox.Margin.All, 5);
|
||||
panelStyle.SetContentMarginOverride(StyleBox.Margin.All, 8);
|
||||
panelStyle.Modulate = Color.FromHex("#2F2F35");
|
||||
return panelStyle;
|
||||
}
|
||||
|
||||
private void BuildRecentEmojisSection(BoxContainer contentContainer, Dictionary<string, EmojiPrototype> emojiDict)
|
||||
{
|
||||
var recentPanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
PanelOverride = CreateEmojiPanelStyle()
|
||||
};
|
||||
|
||||
var recentSection = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
recentSection.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-recent-title"),
|
||||
StyleClasses = { "Bold" },
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
});
|
||||
|
||||
if (_recentEmojis.Count > 0)
|
||||
{
|
||||
var recentContainer = new GridContainer
|
||||
{
|
||||
Columns = 5,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
var recentToShow = _recentEmojis.TakeLast(5).Reverse().ToList();
|
||||
foreach (var emojiCode in recentToShow)
|
||||
{
|
||||
if (emojiDict.TryGetValue(emojiCode, out var emoji))
|
||||
{
|
||||
recentContainer.AddChild(CreateEmojiButton(emoji, false));
|
||||
}
|
||||
}
|
||||
recentSection.AddChild(recentContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
recentSection.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-recent-empty-hint"),
|
||||
StyleClasses = { "LabelSubText" }
|
||||
});
|
||||
}
|
||||
recentPanel.AddChild(recentSection);
|
||||
contentContainer.AddChild(recentPanel);
|
||||
}
|
||||
|
||||
private void BuildFavoriteEmojisSection(BoxContainer contentContainer, List<EmojiPrototype> allEmojis)
|
||||
{
|
||||
var favoritePanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
PanelOverride = CreateEmojiPanelStyle()
|
||||
};
|
||||
|
||||
var favoriteSection = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
favoriteSection.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-favorite-title"),
|
||||
StyleClasses = { "Bold" },
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
});
|
||||
|
||||
if (_favoriteEmojis.Count > 0)
|
||||
{
|
||||
var favoriteContainer = new GridContainer
|
||||
{
|
||||
Columns = 5,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
var favoriteEmojisList = allEmojis.Where(e => _favoriteEmojis.Contains(e.Code)).ToList();
|
||||
foreach (var emoji in favoriteEmojisList)
|
||||
{
|
||||
favoriteContainer.AddChild(CreateEmojiButton(emoji, true));
|
||||
}
|
||||
favoriteSection.AddChild(favoriteContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
favoriteSection.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-favorite-hint"),
|
||||
StyleClasses = { "LabelSubText" },
|
||||
Margin = new Thickness(0, 4, 0, 0)
|
||||
});
|
||||
}
|
||||
favoritePanel.AddChild(favoriteSection);
|
||||
contentContainer.AddChild(favoritePanel);
|
||||
}
|
||||
|
||||
private void BuildAllEmojisSection(BoxContainer contentContainer, List<EmojiPrototype> allEmojis)
|
||||
{
|
||||
var allPanel = new PanelContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
PanelOverride = CreateEmojiPanelStyle()
|
||||
};
|
||||
|
||||
var allSection = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
allSection.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("messenger-emoji-all-title"),
|
||||
StyleClasses = { "Bold" },
|
||||
Margin = new Thickness(0, 0, 0, 4)
|
||||
});
|
||||
|
||||
var allEmojisContainer = new GridContainer
|
||||
{
|
||||
Columns = 5,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
var nonFavoriteEmojis = allEmojis.Where(e => !_favoriteEmojis.Contains(e.Code)).ToList();
|
||||
foreach (var emoji in nonFavoriteEmojis)
|
||||
{
|
||||
allEmojisContainer.AddChild(CreateEmojiButton(emoji, false));
|
||||
}
|
||||
|
||||
allSection.AddChild(allEmojisContainer);
|
||||
allPanel.AddChild(allSection);
|
||||
contentContainer.AddChild(allPanel);
|
||||
}
|
||||
|
||||
private Button CreateEmojiButton(EmojiPrototype emoji, bool isFavorite)
|
||||
{
|
||||
var emojiButton = new Button
|
||||
{
|
||||
MinSize = new Vector2(75, 75),
|
||||
MaxSize = new Vector2(75, 75),
|
||||
ToolTip = emoji.Code
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var spriteSpec = new SpriteSpecifier.Rsi(new ResPath(emoji.SpritePath), emoji.SpriteState);
|
||||
var state = SpriteSystem.RsiStateLike(spriteSpec);
|
||||
|
||||
emojiButton.Label.Visible = false;
|
||||
|
||||
if (state.IsAnimated)
|
||||
{
|
||||
var animatedRect = new AnimatedTextureRect
|
||||
{
|
||||
SetWidth = 65,
|
||||
SetHeight = 65,
|
||||
HorizontalAlignment = Control.HAlignment.Center,
|
||||
VerticalAlignment = Control.VAlignment.Center
|
||||
};
|
||||
animatedRect.SetFromSpriteSpecifier(spriteSpec);
|
||||
animatedRect.DisplayRect.HorizontalExpand = true;
|
||||
animatedRect.DisplayRect.VerticalExpand = true;
|
||||
animatedRect.DisplayRect.Stretch = TextureRect.StretchMode.KeepAspectCentered;
|
||||
emojiButton.AddChild(animatedRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var texture = SpriteSystem.Frame0(spriteSpec);
|
||||
var textureRect = new TextureRect
|
||||
{
|
||||
Texture = texture,
|
||||
SetWidth = 45,
|
||||
SetHeight = 45,
|
||||
HorizontalAlignment = Control.HAlignment.Center,
|
||||
VerticalAlignment = Control.VAlignment.Center,
|
||||
Stretch = TextureRect.StretchMode.KeepAspectCentered
|
||||
};
|
||||
emojiButton.AddChild(textureRect);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
emojiButton.Text = emoji.Code;
|
||||
}
|
||||
|
||||
var emojiCode = emoji.Code;
|
||||
|
||||
emojiButton.OnPressed += _ =>
|
||||
{
|
||||
OnEmojiSelected?.Invoke(emojiCode);
|
||||
AddToRecentEmojis(emojiCode);
|
||||
UpdateEmojiPickerContent();
|
||||
};
|
||||
|
||||
emojiButton.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
{
|
||||
args.Handle();
|
||||
ToggleFavoriteEmoji(emojiCode);
|
||||
UpdateEmojiPickerContent();
|
||||
}
|
||||
};
|
||||
|
||||
return emojiButton;
|
||||
}
|
||||
|
||||
private void AddToRecentEmojis(string emojiCode)
|
||||
{
|
||||
_recentEmojis.Remove(emojiCode);
|
||||
_recentEmojis.Add(emojiCode);
|
||||
if (_recentEmojis.Count > 5)
|
||||
{
|
||||
_recentEmojis.RemoveAt(0);
|
||||
}
|
||||
SaveRecentEmojis();
|
||||
}
|
||||
|
||||
private void ToggleFavoriteEmoji(string emojiCode)
|
||||
{
|
||||
if (!_favoriteEmojis.Add(emojiCode))
|
||||
{
|
||||
_favoriteEmojis.Remove(emojiCode);
|
||||
}
|
||||
SaveFavoriteEmojis();
|
||||
}
|
||||
|
||||
private void LoadSavedEmojis()
|
||||
{
|
||||
HashSet<string>? allEmojis = null;
|
||||
try
|
||||
{
|
||||
allEmojis = EmojiSystem.GetAllEmojis().Select(e => e.Code).ToHashSet();
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
var recentEmojisStr = _configurationManager.GetCVar(SunriseCCVars.MessengerRecentEmojis);
|
||||
if (!string.IsNullOrWhiteSpace(recentEmojisStr))
|
||||
{
|
||||
var emojiCodes = recentEmojisStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
_recentEmojis.Clear();
|
||||
foreach (var code in emojiCodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code) || (allEmojis != null && !allEmojis.Contains(code)))
|
||||
continue;
|
||||
_recentEmojis.Add(code);
|
||||
if (_recentEmojis.Count >= 5) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { _recentEmojis.Clear(); }
|
||||
|
||||
try
|
||||
{
|
||||
var favoriteEmojisStr = _configurationManager.GetCVar(SunriseCCVars.MessengerFavoriteEmojis);
|
||||
if (!string.IsNullOrWhiteSpace(favoriteEmojisStr))
|
||||
{
|
||||
var emojiCodes = favoriteEmojisStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
_favoriteEmojis.Clear();
|
||||
foreach (var code in emojiCodes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code) || (allEmojis != null && !allEmojis.Contains(code)))
|
||||
continue;
|
||||
_favoriteEmojis.Add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { _favoriteEmojis.Clear(); }
|
||||
}
|
||||
|
||||
private void SaveRecentEmojis()
|
||||
{
|
||||
try
|
||||
{
|
||||
var emojiCodesStr = string.Join(",", _recentEmojis);
|
||||
_configurationManager.SetCVar(SunriseCCVars.MessengerRecentEmojis, emojiCodesStr);
|
||||
_configurationManager.SaveToFile();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void SaveFavoriteEmojis()
|
||||
{
|
||||
try
|
||||
{
|
||||
var emojiCodesStr = string.Join(",", _favoriteEmojis);
|
||||
_configurationManager.SetCVar(SunriseCCVars.MessengerFavoriteEmojis, emojiCodesStr);
|
||||
_configurationManager.SaveToFile();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
namespace Content.Client._Sunrise.UserInterface.CustomControls;
|
||||
|
||||
public sealed class PhotoPreviewWindow : DefaultWindow
|
||||
{
|
||||
public PhotoPreviewWindow(Texture texture)
|
||||
{
|
||||
Title = Loc.GetString("messenger-image-preview-title");
|
||||
MinSize = new Vector2(600, 600);
|
||||
|
||||
var textureRect = new TextureRect
|
||||
{
|
||||
Texture = texture,
|
||||
Stretch = TextureRect.StretchMode.KeepAspect,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true
|
||||
};
|
||||
|
||||
Contents.AddChild(textureRect);
|
||||
}
|
||||
|
||||
public static void Open(Texture? texture)
|
||||
{
|
||||
if (texture == null)
|
||||
return;
|
||||
|
||||
var window = new PhotoPreviewWindow(texture);
|
||||
window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
|
@ -176,7 +176,13 @@ public sealed partial class AnomalySystem
|
|||
Audio.PlayPvs(component.GeneratingFinishedSound, uid);
|
||||
|
||||
var message = Loc.GetString("anomaly-generator-announcement");
|
||||
_radio.SendRadioMessage(uid, message, _prototype.Index<RadioChannelPrototype>(component.ScienceChannel), uid);
|
||||
// _radio.SendRadioMessage(uid, message, _prototype.Index<RadioChannelPrototype>(component.ScienceChannel), uid);
|
||||
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(uid)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(component.ScienceChannel) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGenerator()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Content.Server.Materials;
|
|||
using Content.Server.Radiation.Systems;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Prototypes;
|
||||
|
|
@ -39,6 +40,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
|||
[Dependency] private readonly RadiationSystem _radiation = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
|
||||
public const float MinParticleVariation = 0.8f;
|
||||
public const float MaxParticleVariation = 1.2f;
|
||||
|
|
|
|||
|
|
@ -242,9 +242,17 @@ namespace Content.Server.Cargo.Systems
|
|||
("orderAmount", order.OrderQuantity),
|
||||
("approver", order.Approver ?? string.Empty),
|
||||
("cost", cost));
|
||||
_radio.SendRadioMessage(uid, message, account.RadioChannel, uid, escapeMarkup: false);
|
||||
// Sunrise-Start
|
||||
/*_radio.SendRadioMessage(uid, message, account.RadioChannel, uid, escapeMarkup: false);
|
||||
if (CargoOrderConsoleComponent.BaseAnnouncementChannel != account.RadioChannel)
|
||||
_radio.SendRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false);
|
||||
_radio.SendRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false);*/
|
||||
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(uid)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(CargoOrderConsoleComponent.BaseAnnouncementChannel) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
ConsolePopup(args.Actor, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName)));
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Content.Server.Popups;
|
|||
using Content.Server.Stack;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Shared.Cargo;
|
||||
|
|
@ -38,6 +39,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
|
|||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
private EntityQuery<CargoSellBlacklistComponent> _blacklistQuery;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Content.Server.Popups;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Server.StationRecords;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Shared.Access.Systems;
|
||||
|
|
@ -30,6 +31,7 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
|||
[Dependency] private readonly StationRecordsSystem _records = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -170,8 +172,17 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
|||
// this is impossible
|
||||
_ => "not-wanted"
|
||||
};
|
||||
_radio.SendRadioMessage(ent, Loc.GetString($"criminal-records-console-{statusString}", args),
|
||||
ent.Comp.SecurityChannel, ent);
|
||||
// Sunrise-Start
|
||||
var locMsg = Loc.GetString($"criminal-records-console-{statusString}", args);
|
||||
// _radio.SendRadioMessage(ent, locMsg,
|
||||
// ent.Comp.SecurityChannel, ent);
|
||||
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(ent)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(ent.Comp.SecurityChannel) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, locMsg);
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,4 +34,10 @@ public sealed partial class NewsWriterComponent : Component
|
|||
/// </summary>
|
||||
[DataField, ViewVariables]
|
||||
public string DraftContent = "";
|
||||
|
||||
/// <summary>
|
||||
/// This stores the working photo paths of the current article
|
||||
/// </summary>
|
||||
[DataField, ViewVariables]
|
||||
public List<string>? DraftPhotoPaths;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ using Robust.Shared.Utility;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Server.MassMedia.Systems;
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
[Dependency] private readonly DiscordWebhook _discord = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IBaseServer _baseServer = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
private WebhookIdentifier? _webhookId = null;
|
||||
private Color _webhookEmbedColor;
|
||||
|
|
@ -84,6 +86,7 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
subs.Event<NewsWriterPublishMessage>(OnWriteUiPublishMessage);
|
||||
subs.Event<NewsWriterSaveDraftMessage>(OnNewsWriterDraftUpdatedMessage);
|
||||
subs.Event<NewsWriterRequestDraftMessage>(OnRequestArticleDraftMessage);
|
||||
subs.Event<NewsWriterRequestPhotosMessage>(OnRequestPhotosMessage);
|
||||
});
|
||||
|
||||
// News reader
|
||||
|
|
@ -165,7 +168,11 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
return;
|
||||
|
||||
if (!CanUse(msg.Actor, ent.Owner))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("news-write-no-access-popup"), ent, msg.Actor, PopupType.SmallCaution);
|
||||
_audio.PlayPvs(ent.Comp.NoAccessSound, ent);
|
||||
return;
|
||||
}
|
||||
|
||||
ent.Comp.PublishEnabled = false;
|
||||
ent.Comp.NextPublish = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.PublishCooldown);
|
||||
|
|
@ -176,8 +183,9 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
|
||||
var title = msg.Title.Trim();
|
||||
var content = msg.Content.Trim();
|
||||
var photoPaths = msg.PhotoPaths;
|
||||
|
||||
if (TryAddNews(ent, title, content, out var article, authorName, msg.Actor))
|
||||
if (TryAddNews(ent, title, content, out var article, authorName, msg.Actor, photoPaths))
|
||||
{
|
||||
_audio.PlayPvs(ent.Comp.ConfirmSound, ent);
|
||||
|
||||
|
|
@ -197,7 +205,7 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
/// <param name="content">Content of the news article.</param>
|
||||
/// <param name="author">Author of the news article.</param>
|
||||
/// <param name="actor">Entity which caused the news article to publish. Used for admin logs.</param>
|
||||
public bool TryAddNews(EntityUid uid, string title, string content, [NotNullWhen(true)] out NewsArticle? article, string? author = null, EntityUid? actor = null)
|
||||
public bool TryAddNews(EntityUid uid, string title, string content, [NotNullWhen(true)] out NewsArticle? article, string? author = null, EntityUid? actor = null, List<string>? photoPaths = null)
|
||||
{
|
||||
if (!TryGetArticles(uid, out var articles))
|
||||
{
|
||||
|
|
@ -210,7 +218,8 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
Title = title.Length <= MaxTitleLength ? title : $"{title[..MaxTitleLength]}...",
|
||||
Content = content.Length <= MaxContentLength ? content : $"{content[..MaxContentLength]}...",
|
||||
Author = author,
|
||||
ShareTime = _ticker.RoundDuration()
|
||||
ShareTime = _ticker.RoundDuration(),
|
||||
PhotoPaths = photoPaths?.Take(10).ToList()
|
||||
};
|
||||
|
||||
articles.Add(article.Value);
|
||||
|
|
@ -327,7 +336,7 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
if (!TryGetArticles(ent, out var articles))
|
||||
return;
|
||||
|
||||
var state = new NewsWriterBoundUserInterfaceState(articles.ToArray(), ent.Comp.PublishEnabled, ent.Comp.NextPublish, ent.Comp.DraftTitle, ent.Comp.DraftContent);
|
||||
var state = new NewsWriterBoundUserInterfaceState(articles.ToArray(), ent.Comp.PublishEnabled, ent.Comp.NextPublish, ent.Comp.DraftTitle, ent.Comp.DraftContent, ent.Comp.DraftPhotoPaths);
|
||||
_ui.SetUiState(ent.Owner, NewsWriterUiKey.Key, state);
|
||||
}
|
||||
|
||||
|
|
@ -390,6 +399,7 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
{
|
||||
ent.Comp.DraftTitle = args.DraftTitle;
|
||||
ent.Comp.DraftContent = args.DraftContent;
|
||||
ent.Comp.DraftPhotoPaths = args.DraftPhotoPaths;
|
||||
}
|
||||
|
||||
private void OnRequestArticleDraftMessage(Entity<NewsWriterComponent> ent, ref NewsWriterRequestDraftMessage msg)
|
||||
|
|
@ -397,6 +407,25 @@ public sealed class NewsSystem : SharedNewsSystem
|
|||
UpdateWriterUi(ent);
|
||||
}
|
||||
|
||||
private void OnRequestPhotosMessage(Entity<NewsWriterComponent> ent, ref NewsWriterRequestPhotosMessage msg)
|
||||
{
|
||||
var photos = new List<PhotoMetadata>();
|
||||
|
||||
// Find all PDAs or cartridges with photos on the actor
|
||||
var query = EntityQueryEnumerator<PhotoCartridgeComponent>();
|
||||
while (query.MoveNext(out var uid, out var photoComp))
|
||||
{
|
||||
// Check if this entity is child of the actor (in inventory, hands, etc.)
|
||||
if (_transform.ContainsEntity(msg.Actor, uid))
|
||||
{
|
||||
photos.AddRange(photoComp.PhotoGallery.Values);
|
||||
}
|
||||
}
|
||||
|
||||
var photosMsg = new NewsWriterPhotosMessage(photos);
|
||||
_ui.ServerSendUiMessage(ent.Owner, NewsWriterUiKey.Key, photosMsg, msg.Actor);
|
||||
}
|
||||
|
||||
#region Discord Hook
|
||||
|
||||
private void OnRoundEndMessageEvent(RoundEndMessageEvent ev)
|
||||
|
|
|
|||
|
|
@ -94,16 +94,36 @@ public sealed partial class ResearchSystem
|
|||
("amount", technologyPrototype.Cost),
|
||||
("approver", getIdentityEvent.Title ?? string.Empty)
|
||||
);
|
||||
_radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
|
||||
// _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
|
||||
|
||||
if (technologyPrototype.RadioChannels.Any())
|
||||
// Sunrise-Start
|
||||
if (_messenger.GetServerEntity(_stationSystem.GetOwningStation(uid)) is var (server, _))
|
||||
{
|
||||
var mainGroupId = _messenger.GetGroupIdByRadioChannel(component.AnnouncementChannel);
|
||||
if (mainGroupId != null)
|
||||
_messenger.SendSystemMessageToGroup(server, mainGroupId, message);
|
||||
|
||||
if (technologyPrototype.RadioChannels.Any())
|
||||
{
|
||||
foreach (var radioChannelId in technologyPrototype.RadioChannels)
|
||||
{
|
||||
var groupId = _messenger.GetGroupIdByRadioChannel(radioChannelId);
|
||||
|
||||
if (groupId != null)
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*if (technologyPrototype.RadioChannels.Any())
|
||||
foreach (var radioChannelId in technologyPrototype.RadioChannels)
|
||||
{
|
||||
if (PrototypeManager.TryIndex(radioChannelId, out var radioChannel))
|
||||
{
|
||||
_radio.SendRadioMessage(uid, message, radioChannel, uid, escapeMarkup: false);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Research.Components;
|
||||
|
|
@ -22,6 +24,8 @@ namespace Content.Server.Research.Systems
|
|||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Salvage;
|
||||
using Robust.Server.GameObjects;
|
||||
|
|
@ -45,6 +46,7 @@ namespace Content.Server.Salvage
|
|||
[Dependency] private readonly ShuttleConsoleSystem _shuttleConsoles = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
|
@ -64,8 +66,14 @@ namespace Content.Server.Salvage
|
|||
private void Report(EntityUid source, string channelName, string messageKey, params (string, object)[] args)
|
||||
{
|
||||
var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
|
||||
var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
|
||||
_radioSystem.SendRadioMessage(source, message, channel, source);
|
||||
// var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
|
||||
// _radioSystem.SendRadioMessage(source, message, channel, source);
|
||||
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(source)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(channelName) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
|
|
|||
|
|
@ -179,6 +179,11 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
|||
return false;
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
if (TryComp<DroneConsoleComponent>(uid, out var droneConsole) && droneConsole.Entity == null)
|
||||
droneConsole.Entity = GetShuttleConsole(uid, droneConsole);
|
||||
// Sunrise-End
|
||||
|
||||
var pilotComponent = EnsureComp<PilotComponent>(user);
|
||||
var console = pilotComponent.Console;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using Content.Server.Inventory;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Roles;
|
||||
|
|
@ -22,6 +24,8 @@ public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
|
|||
[Dependency] private readonly ServerInventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly RadioSystem _radioSystem = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _record = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
|
||||
protected override void SelectBorgModule(Entity<BorgSwitchableTypeComponent> ent, ProtoId<BorgTypePrototype> borgType)
|
||||
{
|
||||
|
|
@ -129,8 +133,14 @@ public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
|
|||
|
||||
private void Report(EntityUid source, string channelName, string message)
|
||||
{
|
||||
var channel = Prototypes.Index<RadioChannelPrototype>(channelName);
|
||||
_radioSystem.SendRadioMessage(source, message, channel, source);
|
||||
// var channel = Prototypes.Index<RadioChannelPrototype>(channelName);
|
||||
// _radioSystem.SendRadioMessage(source, message, channel, source);
|
||||
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(source)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(channelName) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Server.StationRecords.Components;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Server.GameObjects;
|
||||
|
|
@ -11,6 +12,7 @@ public sealed partial class GeneralStationRecordConsoleSystem : EntitySystem
|
|||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Pinpointer;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Trigger;
|
||||
using Content.Shared.Trigger.Components.Effects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
|
@ -13,6 +16,8 @@ public sealed class RattleOnTriggerSystem : EntitySystem
|
|||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly NavMapSystem _navMap = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -43,7 +48,18 @@ public sealed class RattleOnTriggerSystem : EntitySystem
|
|||
var posText = FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString(target.Value));
|
||||
|
||||
var message = Loc.GetString(messageId, ("user", target.Value), ("position", posText));
|
||||
// Sends a message to the radio channel specified by the implant
|
||||
_radio.SendRadioMessage(ent.Owner, message, _prototypeManager.Index(ent.Comp.RadioChannel), ent.Owner);
|
||||
|
||||
// Sunrise-Start
|
||||
var sentToMessenger = false;
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(ent.Owner)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(ent.Comp.RadioChannel) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
sentToMessenger = true;
|
||||
}
|
||||
|
||||
if (!sentToMessenger)
|
||||
_radio.SendRadioMessage(ent.Owner, message, _prototypeManager.Index(ent.Comp.RadioChannel), ent.Owner);
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using Content.Server.Atmos.EntitySystems;
|
|||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared.Abilities.Goliath;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage;
|
||||
|
|
@ -35,6 +37,8 @@ public sealed class SupermatterSystem : AccUpdateEntitySystem
|
|||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
|
||||
private readonly Dictionary<EntityUid, Entity<SupermatterComponent>> _supermatters = [];
|
||||
private DamageGroupPrototype? _brute;
|
||||
|
|
@ -125,10 +129,24 @@ public sealed class SupermatterSystem : AccUpdateEntitySystem
|
|||
supermatter.Comp.LastSendedDurability = supermatter.Comp.Durability;
|
||||
|
||||
if (currentDurability > lastDurability)
|
||||
_radioSystem.SendRadioMessage(supermatter.Owner, $"The crystal is regenerating. Durability: {currentDurability}%", _engi, supermatter.Owner);
|
||||
{
|
||||
// _radioSystem.SendRadioMessage(supermatter.Owner, $"The crystal is regenerating. Durability: {currentDurability}%", _engi, supermatter.Owner);
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(supermatter.Owner)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(_engi.ID) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, $"The crystal is regenerating. Durability: {currentDurability}%");
|
||||
}
|
||||
}
|
||||
else switch (currentDurability)
|
||||
{
|
||||
case > 75: _radioSystem.SendRadioMessage(supermatter.Owner, $"Attention! The crystal is destabilizing. Durability: {currentDurability}%", _engi, supermatter.Owner); break;
|
||||
case > 75:
|
||||
// _radioSystem.SendRadioMessage(supermatter.Owner, $"Attention! The crystal is destabilizing. Durability: {currentDurability}%", _engi, supermatter.Owner);
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(supermatter.Owner)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel(_engi.ID) is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, $"Attention! The crystal is destabilizing. Durability: {currentDurability}%");
|
||||
}
|
||||
break;
|
||||
case > 50: _chat.DispatchServerAnnouncement($"Attention! The crystal is destabilizing. Durability: {currentDurability}%", Color.Yellow); break;
|
||||
case > 25: _chat.DispatchServerAnnouncement($"Critical state of the crystal! Durability: {currentDurability}%", Color.OrangeRed); break;
|
||||
default: _chat.DispatchServerAnnouncement($"Crystal destruction is inevitable. Current durability: {currentDurability}%", Color.Red); break;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Content.Server.CriminalRecords.Systems;
|
|||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords.Systems;
|
||||
using Content.Server._Sunrise.Messenger;
|
||||
using Content.Shared._Sunrise.AddWantedStatus;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Clothing.Components;
|
||||
|
|
@ -20,6 +21,7 @@ public sealed partial class AddWantedStatusSystem : EntitySystem
|
|||
[Dependency] private readonly StationRecordsSystem _records = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly RadioSystem _radio = default!;
|
||||
[Dependency] private readonly MessengerServerSystem _messenger = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -96,6 +98,14 @@ public sealed partial class AddWantedStatusSystem : EntitySystem
|
|||
reason = Loc.GetString("wanted-list-unknown-reason-label");
|
||||
|
||||
var message = Loc.GetString("criminal-records-console-wanted", [("name", wantedName), ("officer", officer), ("reason", reason), ("job", wantedJobTitle)]);
|
||||
_radio.SendRadioMessage(sender, message, "Security", sender);
|
||||
// _radio.SendRadioMessage(sender, message, "Security", sender);
|
||||
|
||||
// Sunrise-Start
|
||||
if (_messenger.GetServerEntity(_station.GetOwningStation(sender)) is var (server, _) &&
|
||||
_messenger.GetGroupIdByRadioChannel("Security") is { } groupId)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server, groupId, message);
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,17 +118,33 @@ public sealed partial class MessengerCartridgeSystem
|
|||
|
||||
userData.TryGetValue("job_title", out object? jobTitleObj);
|
||||
userData.TryGetValue("department_id", out object? departmentIdObj);
|
||||
userData.TryGetValue("department_ids", out object? departmentIdsObj);
|
||||
userData.TryGetValue("job_icon_id", out object? jobIconIdObj);
|
||||
|
||||
var jobTitle = jobTitleObj?.ToString();
|
||||
var departmentId = departmentIdObj?.ToString();
|
||||
var departmentIds = new List<string>();
|
||||
if (departmentIdsObj is List<object> deptList)
|
||||
{
|
||||
departmentIds.AddRange(deptList.Select(d => d.ToString() ?? string.Empty).Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
}
|
||||
else if (departmentIdsObj is List<string> deptStringList)
|
||||
{
|
||||
departmentIds.AddRange(deptStringList.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
}
|
||||
|
||||
if (departmentIds.Count == 0 && departmentId != null && !string.IsNullOrWhiteSpace(departmentId))
|
||||
{
|
||||
departmentIds.Add(departmentId);
|
||||
}
|
||||
|
||||
ProtoId<JobIconPrototype>? jobIconId = null;
|
||||
if (jobIconIdObj != null && !string.IsNullOrEmpty(jobIconIdObj.ToString()))
|
||||
if (jobIconIdObj != null && !string.Empty.Equals(jobIconIdObj.ToString()))
|
||||
{
|
||||
jobIconId = new ProtoId<JobIconPrototype>(jobIconIdObj.ToString()!);
|
||||
}
|
||||
|
||||
users.Add(new MessengerUser(userId, userName, jobTitle, departmentId, jobIconId));
|
||||
users.Add(new MessengerUser(userId, userName, jobTitle, departmentIds, jobIconId));
|
||||
}
|
||||
|
||||
component.Users = users;
|
||||
|
|
@ -202,19 +218,20 @@ public sealed partial class MessengerCartridgeSystem
|
|||
groups.Add(new MessengerGroup(groupId, groupName, new HashSet<string>(membersList ?? new List<string>()), groupType, autoGroupPrototypeId, ownerId));
|
||||
}
|
||||
|
||||
component.Groups = groups;
|
||||
|
||||
var existingGroupIds = component.Groups.Select(g => g.GroupId).ToHashSet();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Type == MessengerGroupType.Automatic && group.AutoGroupPrototypeId != null)
|
||||
{
|
||||
if (!component.MutedGroupChats.Contains(group.GroupId))
|
||||
if (!existingGroupIds.Contains(group.GroupId) && !component.MutedGroupChats.Contains(group.GroupId))
|
||||
{
|
||||
component.MutedGroupChats.Add(group.GroupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component.Groups = groups;
|
||||
|
||||
if (serverUnreadCounts != null)
|
||||
{
|
||||
foreach (var (chatId, count) in serverUnreadCounts)
|
||||
|
|
@ -538,9 +555,23 @@ public sealed partial class MessengerCartridgeSystem
|
|||
component.ServerUnreadCounts[chatId] = currentCount + 1;
|
||||
}
|
||||
|
||||
if (!isMuted && !isSender && TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
if (!isMuted && !isSender)
|
||||
{
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
if (TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
|
||||
string notificationMessage;
|
||||
if (isGroupChat)
|
||||
{
|
||||
var groupName = component.Groups.FirstOrDefault(g => g.GroupId == chatId)?.Name ?? chatId ?? string.Empty;
|
||||
notificationMessage = Loc.GetString("messenger-group-notification-message", ("name", groupName));
|
||||
}
|
||||
else
|
||||
{
|
||||
notificationMessage = Loc.GetString("messenger-notification-message", ("name", senderName ?? string.Empty));
|
||||
}
|
||||
|
||||
_cartridgeLoader.SendNotification(loaderUid, Loc.GetString("messenger-program-name"), notificationMessage);
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
|
|
@ -581,11 +612,7 @@ public sealed partial class MessengerCartridgeSystem
|
|||
if (isNewInvite)
|
||||
{
|
||||
component.ActiveInvites.Add(invite);
|
||||
|
||||
if (TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
{
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
}
|
||||
_cartridgeLoader.SendNotification(loaderUid, Loc.GetString("messenger-program-name"), Loc.GetString("messenger-invite-notification-message", ("name", groupName ?? string.Empty)));
|
||||
}
|
||||
|
||||
UpdateUiState(uid, loaderUid, component);
|
||||
|
|
@ -644,9 +671,10 @@ public sealed partial class MessengerCartridgeSystem
|
|||
|
||||
if (addedUserId == component.UserId && !string.IsNullOrEmpty(groupId))
|
||||
{
|
||||
if (TryComp<RingerComponent>(loaderUid, out var ringer))
|
||||
var group = component.Groups.FirstOrDefault(g => g.GroupId == groupId);
|
||||
if (group != null)
|
||||
{
|
||||
_ringer.RingerPlayRingtone(loaderUid);
|
||||
_cartridgeLoader.SendNotification(loaderUid, Loc.GetString("messenger-program-name"), Loc.GetString("messenger-invite-notification-message", ("name", group.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
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;
|
||||
|
||||
|
|
@ -24,16 +17,12 @@ 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!;
|
||||
|
||||
|
|
@ -56,7 +45,7 @@ public sealed class PhotoCartridgeSystem : EntitySystem
|
|||
SubscribeLocalEvent<PhotoCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
|
||||
SubscribeLocalEvent<PhotoCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
||||
|
||||
_netManager.RegisterNetMessage<PdaPhotoCaptureMessage>(OnPhotoCaptureMessage, accept: NetMessageAccept.Server);
|
||||
_netTexturesManager.OnPhotoCaptureMessage = OnPhotoCaptureMessage;
|
||||
}
|
||||
|
||||
private void OnUiMessage(EntityUid uid, PhotoCartridgeComponent component, CartridgeMessageEvent args)
|
||||
|
|
@ -155,22 +144,30 @@ public sealed class PhotoCartridgeSystem : EntitySystem
|
|||
return;
|
||||
}
|
||||
|
||||
var pdaUid = FindPlayerPda(session);
|
||||
if (pdaUid == null)
|
||||
var entity = GetEntity(msg.LoaderUid);
|
||||
PhotoCartridgeComponent? photoComponent;
|
||||
EntityUid? cartridgeUid;
|
||||
EntityUid pdaUid = EntityUid.Invalid;
|
||||
|
||||
if (_cartridgeLoader.TryGetProgram(entity, out cartridgeUid, out photoComponent))
|
||||
{
|
||||
_sawmill.Warning($"Photo capture rejected: no PDA found for player {session.Name}");
|
||||
return;
|
||||
pdaUid = entity;
|
||||
}
|
||||
else if (TryComp(entity, out photoComponent) && TryComp<CartridgeComponent>(entity, out var cartridge) && cartridge.LoaderUid.HasValue)
|
||||
{
|
||||
pdaUid = cartridge.LoaderUid.Value;
|
||||
cartridgeUid = entity;
|
||||
}
|
||||
|
||||
if (!_cartridgeLoader.TryGetProgram<PhotoCartridgeComponent>(pdaUid.Value, out var cartridgeUid, out var photoComponent))
|
||||
if (photoComponent == null || cartridgeUid == null)
|
||||
{
|
||||
_sawmill.Warning($"Photo capture rejected: photo cartridge not found in PDA {ToPrettyString(pdaUid.Value)}");
|
||||
_sawmill.Warning($"Photo capture rejected: photo cartridge not found for entity {ToPrettyString(entity)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (photoComponent.PhotoGallery.Count >= MaxPhotosPerUser)
|
||||
{
|
||||
UpdateUiState(cartridgeUid.Value, pdaUid.Value, photoComponent, errorMessage: Loc.GetString("photo-cartridge-limit-reached"));
|
||||
UpdateUiState(cartridgeUid!.Value, pdaUid, photoComponent, errorMessage: Loc.GetString("photo-cartridge-limit-reached"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -187,41 +184,7 @@ public sealed class PhotoCartridgeSystem : EntitySystem
|
|||
|
||||
_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;
|
||||
UpdateUiState(cartridgeUid!.Value, pdaUid, photoComponent);
|
||||
}
|
||||
|
||||
private void HandleSendPhotoToMessenger(EntityUid uid, PhotoCartridgeComponent component, EntityUid loaderUid, string? photoId, string? recipientId, string? groupId)
|
||||
|
|
|
|||
|
|
@ -246,6 +246,81 @@ public sealed partial class MessengerServerSystem
|
|||
SendGroupMessage(uid, component, sender, groupId, content, timestamp, imagePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет системное сообщение в группу (используется для оповещений департаментов)
|
||||
/// </summary>
|
||||
public void SendSystemMessageToGroup(EntityUid uid, string groupId, string content)
|
||||
{
|
||||
if (!TryComp<MessengerServerComponent>(uid, out var component))
|
||||
return;
|
||||
|
||||
if (!component.Groups.TryGetValue(groupId, out var group))
|
||||
return;
|
||||
|
||||
var timestamp = GetStationTime();
|
||||
var messageId = GetNextMessageId(uid, component);
|
||||
var senderName = Loc.GetString("messenger-system-name");
|
||||
var message = new MessengerMessage("system", senderName, content, timestamp, groupId, null, false, messageId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(groupId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[groupId] = history;
|
||||
}
|
||||
|
||||
history.Add(message);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = message.SenderId,
|
||||
["sender_name"] = message.SenderName,
|
||||
["content"] = message.Content,
|
||||
["timestamp"] = message.Timestamp.TotalSeconds,
|
||||
["group_id"] = message.GroupId ?? string.Empty,
|
||||
["recipient_id"] = message.RecipientId ?? string.Empty,
|
||||
["is_read"] = message.IsRead,
|
||||
["message_id"] = message.MessageId,
|
||||
["sender_job_icon_id"] = string.Empty,
|
||||
["image_path"] = string.Empty
|
||||
};
|
||||
|
||||
foreach (var memberId in group.Members)
|
||||
{
|
||||
var isMemberChatOpen = component.OpenChats.TryGetValue(memberId, out var memberOpenChatId) && memberOpenChatId == groupId;
|
||||
|
||||
if (!isMemberChatOpen)
|
||||
{
|
||||
if (!component.UnreadCounts.TryGetValue(memberId, out var memberUnreads))
|
||||
{
|
||||
memberUnreads = new Dictionary<string, int>();
|
||||
component.UnreadCounts[memberId] = memberUnreads;
|
||||
}
|
||||
memberUnreads.TryGetValue(groupId, out var currentCount);
|
||||
memberUnreads[groupId] = currentCount + 1;
|
||||
}
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, memberId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обрабатывает удаление сообщения пользователем
|
||||
/// </summary>
|
||||
|
|
@ -326,4 +401,79 @@ public sealed partial class MessengerServerSystem
|
|||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Sends a fake message to a specific user (used for spam).
|
||||
/// </summary>
|
||||
public void SendFakePersonalMessage(EntityUid uid, string recipientId, string senderId, string senderName, string content)
|
||||
{
|
||||
if (!TryComp<MessengerServerComponent>(uid, out var component))
|
||||
return;
|
||||
|
||||
if (!component.Users.ContainsKey(recipientId))
|
||||
return;
|
||||
|
||||
var timestamp = GetStationTime();
|
||||
var messageId = GetNextMessageId(uid, component);
|
||||
var message = new MessengerMessage(senderId, senderName, content, timestamp, null, recipientId, isRead: false, messageId);
|
||||
var chatId = GetPersonalChatId(senderId, recipientId);
|
||||
|
||||
if (!component.MessageHistory.TryGetValue(chatId, out var history))
|
||||
{
|
||||
history = new List<MessengerMessage>();
|
||||
component.MessageHistory[chatId] = history;
|
||||
}
|
||||
|
||||
history.Add(message);
|
||||
TrimMessageHistory(history, component.MaxMessageHistory);
|
||||
|
||||
var isChatOpen = component.OpenChats.TryGetValue(recipientId, out var openChatId) && openChatId == chatId;
|
||||
|
||||
if (isChatOpen)
|
||||
{
|
||||
message.IsRead = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!component.UnreadCounts.TryGetValue(recipientId, out var recipientUnreads))
|
||||
{
|
||||
recipientUnreads = new Dictionary<string, int>();
|
||||
component.UnreadCounts[recipientId] = recipientUnreads;
|
||||
}
|
||||
recipientUnreads.TryGetValue(chatId, out var currentCount);
|
||||
recipientUnreads[chatId] = currentCount + 1;
|
||||
}
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
return;
|
||||
|
||||
uint? pdaFrequency = null;
|
||||
if (_prototypeManager.TryIndex(component.PdaFrequencyId, out var pdaFreq))
|
||||
{
|
||||
pdaFrequency = pdaFreq.Frequency;
|
||||
}
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = MessengerCommands.CmdMessageReceived,
|
||||
["sender_id"] = message.SenderId,
|
||||
["sender_name"] = message.SenderName,
|
||||
["content"] = message.Content,
|
||||
["timestamp"] = message.Timestamp.TotalSeconds,
|
||||
["group_id"] = message.GroupId ?? string.Empty,
|
||||
["recipient_id"] = message.RecipientId ?? string.Empty,
|
||||
["is_read"] = message.IsRead,
|
||||
["message_id"] = message.MessageId,
|
||||
["sender_job_icon_id"] = string.Empty,
|
||||
["image_path"] = string.Empty
|
||||
};
|
||||
|
||||
if (pdaFrequency.HasValue)
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, recipientId, payload, frequency: pdaFrequency, network: serverDevice.DeviceNetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceNetwork.QueuePacket(uid, recipientId, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
181
Content.Server/_Sunrise/Messenger/MessengerServerSystem.Spam.cs
Normal file
181
Content.Server/_Sunrise/Messenger/MessengerServerSystem.Spam.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System.Linq;
|
||||
using Content.Shared._Sunrise.Messenger;
|
||||
using Content.Shared._Sunrise.SunriseCCVars;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
public sealed partial class MessengerServerSystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private bool _spamEnabled;
|
||||
private float _spamMinTime;
|
||||
private float _spamMaxTime;
|
||||
private float _spamPlayerPercentage;
|
||||
|
||||
private TimeSpan _nextTick = TimeSpan.Zero;
|
||||
private readonly TimeSpan _refreshCooldown = TimeSpan.FromSeconds(1);
|
||||
|
||||
private void InitializeSpam()
|
||||
{
|
||||
Subs.CVar(_cfg, SunriseCCVars.MessengerSpamEnabled, OnSpamEnabledChanged, true);
|
||||
Subs.CVar(_cfg, SunriseCCVars.MessengerSpamMinTime, OnSpamMinTimeChanged, true);
|
||||
Subs.CVar(_cfg, SunriseCCVars.MessengerSpamMaxTime, OnSpamMaxTimeChanged, true);
|
||||
Subs.CVar(_cfg, SunriseCCVars.MessengerSpamPlayerPercentage, OnSpamPlayerPercentageChanged, true);
|
||||
}
|
||||
|
||||
private void OnSpamEnabledChanged(bool value)
|
||||
{
|
||||
_spamEnabled = value;
|
||||
}
|
||||
|
||||
private void OnSpamMinTimeChanged(float value)
|
||||
{
|
||||
UpdateSpamTiming(value, _spamMaxTime);
|
||||
}
|
||||
|
||||
private void OnSpamMaxTimeChanged(float value)
|
||||
{
|
||||
UpdateSpamTiming(_spamMinTime, value);
|
||||
}
|
||||
|
||||
private void OnSpamPlayerPercentageChanged(float value)
|
||||
{
|
||||
_spamPlayerPercentage = value;
|
||||
}
|
||||
|
||||
private void UpdateSpamTiming(float min, float max)
|
||||
{
|
||||
_spamMinTime = min;
|
||||
_spamMaxTime = max;
|
||||
|
||||
if (_spamMinTime > _spamMaxTime)
|
||||
{
|
||||
(_spamMinTime, _spamMaxTime) = (_spamMaxTime, _spamMinTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSpamTimer(StationMessengerSpamComponent component)
|
||||
{
|
||||
component.Timer = 0;
|
||||
component.NextSpamTime = _random.NextFloat(_spamMinTime, _spamMaxTime);
|
||||
}
|
||||
|
||||
private void UpdateSpam(float frameTime)
|
||||
{
|
||||
if (_nextTick > _timing.CurTime)
|
||||
return;
|
||||
|
||||
_nextTick = _timing.CurTime + _refreshCooldown;
|
||||
|
||||
if (!_spamEnabled)
|
||||
return;
|
||||
|
||||
var query = EntityQueryEnumerator<StationMessengerSpamComponent>();
|
||||
while (query.MoveNext(out var uid, out var spam))
|
||||
{
|
||||
if (spam.NextSpamTime <= 0)
|
||||
{
|
||||
ResetSpamTimer(spam);
|
||||
continue;
|
||||
}
|
||||
|
||||
spam.Timer += (float)_refreshCooldown.TotalSeconds;
|
||||
|
||||
if (spam.Timer < spam.NextSpamTime)
|
||||
continue;
|
||||
|
||||
ResetSpamTimer(spam);
|
||||
|
||||
var serverResult = GetServerEntity(uid);
|
||||
if (serverResult != null)
|
||||
{
|
||||
Sawmill.Info($"Triggering spam wave for station {uid} (Server: {serverResult.Value.Item1})");
|
||||
SendSpamWave(serverResult.Value.Item1, serverResult.Value.Item2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Warning($"Could not find messenger server for station {uid}, skipping spam wave.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SendSpamWave(EntityUid uid, MessengerServerComponent component)
|
||||
{
|
||||
var prototypes = _prototypeManager.EnumeratePrototypes<MessengerSpamPrototype>().ToList();
|
||||
if (prototypes.Count == 0)
|
||||
return;
|
||||
|
||||
var players = new List<(EntityUid Uid, MessengerServerComponent Component, MessengerUser User)>();
|
||||
|
||||
foreach (var user in component.Users.Values)
|
||||
{
|
||||
// Skip spam bots
|
||||
if (user.UserId.StartsWith("spam_"))
|
||||
continue;
|
||||
|
||||
players.Add((uid, component, user));
|
||||
}
|
||||
|
||||
if (players.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetCount = (int)(players.Count * _spamPlayerPercentage);
|
||||
if (targetCount < 1)
|
||||
targetCount = 1;
|
||||
|
||||
_random.Shuffle(players);
|
||||
|
||||
var count = Math.Min(targetCount, players.Count);
|
||||
|
||||
Sawmill.Info($"Sending spam to {count} users (Total: {players.Count}, Target %: {_spamPlayerPercentage})");
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var p = players[i];
|
||||
SendSpamToUser(p.Uid, p.Component, p.User, prototypes);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendSpamToUser(EntityUid uid, MessengerServerComponent component, MessengerUser user, List<MessengerSpamPrototype> prototypes)
|
||||
{
|
||||
var proto = _random.Pick(prototypes);
|
||||
|
||||
var senderName = GetRandomString(proto.SenderDataset);
|
||||
if (proto.NameDataset != null && proto.SurnameDataset != null)
|
||||
{
|
||||
senderName += " " + GetRandomString(proto.NameDataset);
|
||||
senderName += " " + GetRandomString(proto.SurnameDataset);
|
||||
}
|
||||
|
||||
var messageContent = GetRandomString(proto.MessageDataset);
|
||||
if (string.IsNullOrWhiteSpace(messageContent))
|
||||
return;
|
||||
|
||||
var senderId = $"spam_{Math.Abs(senderName.GetHashCode())}";
|
||||
|
||||
if (!component.Users.ContainsKey(senderId))
|
||||
{
|
||||
var spamUser = new MessengerUser(senderId, senderName);
|
||||
component.Users.Add(senderId, spamUser);
|
||||
}
|
||||
|
||||
SendFakePersonalMessage(uid, user.UserId, senderId, senderName, messageContent);
|
||||
}
|
||||
|
||||
private string GetRandomString(string datasetId)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(datasetId, out LocalizedDatasetPrototype? dataset))
|
||||
return "Error";
|
||||
|
||||
var key = _random.Pick(dataset.Values);
|
||||
return _loc.GetString(key);
|
||||
}
|
||||
}
|
||||
|
|
@ -143,23 +143,20 @@ public sealed partial class MessengerServerSystem
|
|||
var userName = pda.OwnerName ?? Loc.GetString("messenger-user-unknown");
|
||||
|
||||
string? jobTitle = null;
|
||||
string? departmentId = null;
|
||||
ProtoId<JobIconPrototype>? jobIconId = null;
|
||||
var departments = new List<string>();
|
||||
ProtoId<JobIconPrototype> jobIconId = "JobIconUnknown";
|
||||
|
||||
if (pda.ContainedId != null && TryComp<IdCardComponent>(pda.ContainedId.Value, out var idCard))
|
||||
{
|
||||
jobTitle = idCard.LocalizedJobTitle;
|
||||
if (idCard.JobDepartments.Count > 0)
|
||||
{
|
||||
departmentId = idCard.JobDepartments[0];
|
||||
}
|
||||
departments.AddRange(idCard.JobDepartments.Select(d => (string) d));
|
||||
jobIconId = idCard.JobIcon;
|
||||
}
|
||||
|
||||
var user = new MessengerUser(userId, userName, jobTitle, departmentId, jobIconId);
|
||||
var user = new MessengerUser(userId, userName, jobTitle, departments, jobIconId);
|
||||
component.Users[userId] = user;
|
||||
|
||||
AddUserToAutoGroups(uid, component, userId, userName, departmentId);
|
||||
AddUserToAutoGroups(uid, component, userId, userName, departments);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
{
|
||||
|
|
@ -184,8 +181,9 @@ public sealed partial class MessengerServerSystem
|
|||
["user_id"] = userId,
|
||||
["user_name"] = userName,
|
||||
["job_title"] = jobTitle ?? string.Empty,
|
||||
["department_id"] = departmentId ?? string.Empty,
|
||||
["job_icon_id"] = jobIconId?.Id ?? string.Empty
|
||||
["department_id"] = user.DepartmentId ?? string.Empty,
|
||||
["department_ids"] = user.DepartmentIds,
|
||||
["job_icon_id"] = jobIconId.Id
|
||||
};
|
||||
|
||||
if (_deviceNetwork.IsAddressPresent(serverDevice.DeviceNetId, userId))
|
||||
|
|
@ -202,6 +200,7 @@ public sealed partial class MessengerServerSystem
|
|||
["user_name"] = u.Name,
|
||||
["job_title"] = u.JobTitle ?? string.Empty,
|
||||
["department_id"] = u.DepartmentId ?? string.Empty,
|
||||
["department_ids"] = u.DepartmentIds,
|
||||
["job_icon_id"] = u.JobIconId?.Id ?? string.Empty
|
||||
}).ToList()
|
||||
};
|
||||
|
|
@ -312,17 +311,14 @@ public sealed partial class MessengerServerSystem
|
|||
|
||||
string? userName = null;
|
||||
string? jobTitle = null;
|
||||
string? departmentId = null;
|
||||
ProtoId<JobIconPrototype>? jobIconId = null;
|
||||
var departments = new List<string>();
|
||||
ProtoId<JobIconPrototype> jobIconId = "JobIconUnknown";
|
||||
|
||||
if (pda.ContainedId != null && TryComp<IdCardComponent>(pda.ContainedId.Value, out var idCard))
|
||||
{
|
||||
userName = idCard.FullName;
|
||||
jobTitle = idCard.LocalizedJobTitle;
|
||||
if (idCard.JobDepartments.Count > 0)
|
||||
{
|
||||
departmentId = idCard.JobDepartments[0];
|
||||
}
|
||||
departments.AddRange(idCard.JobDepartments.Select(d => (string) d));
|
||||
jobIconId = idCard.JobIcon;
|
||||
}
|
||||
|
||||
|
|
@ -331,10 +327,10 @@ public sealed partial class MessengerServerSystem
|
|||
userName = pda.OwnerName ?? Loc.GetString("messenger-user-unknown");
|
||||
}
|
||||
|
||||
var user = new MessengerUser(userId, userName, jobTitle, departmentId, jobIconId);
|
||||
var user = new MessengerUser(userId, userName, jobTitle, departments, jobIconId);
|
||||
component.Users[userId] = user;
|
||||
|
||||
AddUserToAutoGroups(uid, component, userId, userName, departmentId);
|
||||
AddUserToAutoGroups(uid, component, userId, userName, departments);
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
{
|
||||
|
|
@ -359,8 +355,9 @@ public sealed partial class MessengerServerSystem
|
|||
["user_id"] = userId,
|
||||
["user_name"] = userName,
|
||||
["job_title"] = jobTitle ?? string.Empty,
|
||||
["department_id"] = departmentId ?? string.Empty,
|
||||
["job_icon_id"] = jobIconId?.Id ?? string.Empty
|
||||
["department_id"] = user.DepartmentId ?? string.Empty,
|
||||
["department_ids"] = user.DepartmentIds,
|
||||
["job_icon_id"] = jobIconId.Id
|
||||
};
|
||||
|
||||
if (_deviceNetwork.IsAddressPresent(serverDevice.DeviceNetId, args.SenderAddress))
|
||||
|
|
@ -377,6 +374,7 @@ public sealed partial class MessengerServerSystem
|
|||
["user_name"] = u.Name,
|
||||
["job_title"] = u.JobTitle ?? string.Empty,
|
||||
["department_id"] = u.DepartmentId ?? string.Empty,
|
||||
["department_ids"] = u.DepartmentIds,
|
||||
["job_icon_id"] = u.JobIconId?.Id ?? string.Empty
|
||||
}).ToList()
|
||||
};
|
||||
|
|
@ -431,7 +429,7 @@ public sealed partial class MessengerServerSystem
|
|||
/// <summary>
|
||||
/// Добавляет пользователя в автоматические группы на основе прототипов
|
||||
/// </summary>
|
||||
private void AddUserToAutoGroups(EntityUid uid, MessengerServerComponent component, string userId, string userName, string? departmentId)
|
||||
private void AddUserToAutoGroups(EntityUid uid, MessengerServerComponent component, string userId, string userName, IEnumerable<string> departments)
|
||||
{
|
||||
foreach (var autoGroupProto in _prototypeManager.EnumeratePrototypes<MessengerAutoGroupPrototype>())
|
||||
{
|
||||
|
|
@ -441,9 +439,9 @@ public sealed partial class MessengerServerSystem
|
|||
{
|
||||
shouldAdd = true;
|
||||
}
|
||||
else if (departmentId != null && autoGroupProto.Departments.Count > 0)
|
||||
else if (autoGroupProto.Departments.Count > 0)
|
||||
{
|
||||
shouldAdd = autoGroupProto.Departments.Contains(departmentId);
|
||||
shouldAdd = autoGroupProto.Departments.Any(d => departments.Contains((string) d));
|
||||
}
|
||||
|
||||
if (!shouldAdd)
|
||||
|
|
@ -630,25 +628,22 @@ public sealed partial class MessengerServerSystem
|
|||
return;
|
||||
|
||||
string? jobTitle = null;
|
||||
string? departmentId = null;
|
||||
ProtoId<JobIconPrototype>? jobIconId = null;
|
||||
var departments = new List<string>();
|
||||
ProtoId<JobIconPrototype> jobIconId = "JobIconUnknown";
|
||||
|
||||
if (pdaComp.ContainedId != null && TryComp<IdCardComponent>(pdaComp.ContainedId.Value, out var idCard))
|
||||
{
|
||||
jobTitle = idCard.LocalizedJobTitle;
|
||||
if (idCard.JobDepartments.Count > 0)
|
||||
{
|
||||
departmentId = idCard.JobDepartments[0];
|
||||
}
|
||||
departments.AddRange(idCard.JobDepartments.Select(d => (string) d));
|
||||
jobIconId = idCard.JobIcon;
|
||||
}
|
||||
|
||||
var needsUpdate = user.JobTitle != jobTitle || user.DepartmentId != departmentId || user.JobIconId != jobIconId;
|
||||
var needsUpdate = user.JobTitle != jobTitle || !user.DepartmentIds.SequenceEqual(departments) || user.JobIconId != jobIconId;
|
||||
|
||||
if (needsUpdate)
|
||||
{
|
||||
user.JobTitle = jobTitle;
|
||||
user.DepartmentId = departmentId;
|
||||
user.DepartmentIds = departments;
|
||||
user.JobIconId = jobIconId;
|
||||
|
||||
if (!TryComp<DeviceNetworkComponent>(uid, out var serverDevice))
|
||||
|
|
@ -669,6 +664,7 @@ public sealed partial class MessengerServerSystem
|
|||
["user_name"] = u.Name,
|
||||
["job_title"] = u.JobTitle ?? string.Empty,
|
||||
["department_id"] = u.DepartmentId ?? string.Empty,
|
||||
["department_ids"] = u.DepartmentIds,
|
||||
["job_icon_id"] = u.JobIconId?.Id ?? string.Empty
|
||||
}).ToList()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Server.Station.Systems;
|
||||
|
|
@ -9,6 +10,8 @@ using Content.Shared._Sunrise.Messenger;
|
|||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Server.CartridgeLoader;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
|
|
@ -25,9 +28,23 @@ public sealed partial class MessengerServerSystem : EntitySystem
|
|||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
private ISawmill Sawmill { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Находит GroupId для указанного радиоканала
|
||||
/// </summary>
|
||||
public string? GetGroupIdByRadioChannel(string radioChannelId)
|
||||
{
|
||||
foreach (var proto in _prototypeManager.EnumeratePrototypes<MessengerAutoGroupPrototype>())
|
||||
{
|
||||
if (proto.RadioChannel == radioChannelId)
|
||||
return proto.GroupId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
|
@ -39,6 +56,14 @@ public sealed partial class MessengerServerSystem : EntitySystem
|
|||
SubscribeLocalEvent<MessengerServerComponent, DeviceNetServerDisconnectedEvent>(OnServerDisconnected);
|
||||
SubscribeLocalEvent<MessengerServerComponent, RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
|
||||
|
||||
InitializeSpam();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
UpdateSpam(frameTime);
|
||||
}
|
||||
|
||||
private void OnRoundRestart(EntityUid uid, MessengerServerComponent component, RoundRestartCleanupEvent args)
|
||||
|
|
@ -72,7 +97,7 @@ public sealed partial class MessengerServerSystem : EntitySystem
|
|||
|
||||
foreach (var user in component.Users.Values)
|
||||
{
|
||||
AddUserToAutoGroups(uid, component, user.UserId, user.Name, user.DepartmentId);
|
||||
AddUserToAutoGroups(uid, component, user.UserId, user.Name, user.DepartmentIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,4 +197,27 @@ public sealed partial class MessengerServerSystem : EntitySystem
|
|||
{
|
||||
return ++component.MessageIdCounter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Находит сущность сервера мессенджера для станции
|
||||
/// </summary>
|
||||
public (EntityUid, MessengerServerComponent)? GetServerEntity(EntityUid? station)
|
||||
{
|
||||
if (station == null)
|
||||
return null;
|
||||
|
||||
var query = EntityQueryEnumerator<MessengerServerComponent, SingletonDeviceNetServerComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp, out var singleton))
|
||||
{
|
||||
if (!_singletonServer.IsActiveServer(uid, singleton))
|
||||
continue;
|
||||
|
||||
if (_stationSystem.GetOwningStation(uid) != station)
|
||||
continue;
|
||||
|
||||
return (uid, comp);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
namespace Content.Server._Sunrise.Messenger;
|
||||
|
||||
/// <summary>
|
||||
/// Component for handling messenger spam timing on a station/entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class StationMessengerSpamComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Current timer in seconds.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Timer;
|
||||
|
||||
/// <summary>
|
||||
/// Target time in seconds for the next spam wave.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float NextSpamTime;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared._Sunrise.NetTextures;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.ContentPack;
|
||||
|
|
@ -11,8 +12,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;
|
||||
|
|
@ -45,11 +44,20 @@ namespace Content.Server._Sunrise;
|
|||
/// </summary>
|
||||
private readonly Dictionary<ResPath, byte[]> _dynamicResources = new();
|
||||
|
||||
/// <summary>
|
||||
/// Callback for handling photo captures. PhotoCartridgeSystem registers itself here.
|
||||
/// </summary>
|
||||
public Action<PdaPhotoCaptureMessage>? OnPhotoCaptureMessage { get; set; }
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("network.textures");
|
||||
_netManager.RegisterNetMessage<RequestNetworkResourceMessage>(OnRequestNetworkResource);
|
||||
|
||||
_netManager.RegisterNetMessage<PdaPhotoCaptureMessage>(
|
||||
msg => OnPhotoCaptureMessage?.Invoke(msg),
|
||||
accept: NetMessageAccept.Server);
|
||||
|
||||
_transferManager.RegisterTransferMessage(TransferKeyNetTextures);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -168,10 +168,18 @@ public sealed partial class GeneralStationRecordConsoleSystem
|
|||
if (ent.Comp.Silent)
|
||||
return;
|
||||
|
||||
// Sunrise-Start
|
||||
var server = _messenger.GetServerEntity(_station.GetOwningStation(ent));
|
||||
|
||||
foreach (var channel in ent.Comp.AnnouncementChannels)
|
||||
{
|
||||
_radio.SendRadioMessage(ent, message, channel, ent);
|
||||
//_radio.SendRadioMessage(ent, message, channel, ent);
|
||||
if (_messenger.GetGroupIdByRadioChannel(channel) is { } groupId && server != null)
|
||||
{
|
||||
_messenger.SendSystemMessageToGroup(server.Value.Item1, groupId, message);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
_audio.PlayPvs(ent.Comp.SuccessfulSound, ent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ public sealed partial class CCVars
|
|||
/// Also looks weird on slow spacing for unrelated reasons. If you do want to enable this, you should probably turn on instaspacing.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> MonstermosRipTiles =
|
||||
CVarDef.Create("atmos.monstermos_rip_tiles", false, CVar.SERVERONLY);
|
||||
CVarDef.Create("atmos.monstermos_rip_tiles", true, CVar.SERVERONLY); // Sunrise-Edit
|
||||
|
||||
/// <summary>
|
||||
/// Whether explosive depressurization will cause the grid to gain an impulse.
|
||||
/// Needs <see cref="MonstermosEqualization"/> and <see cref="MonstermosDepressurization"/> to be enabled to work.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AtmosGridImpulse =
|
||||
CVarDef.Create("atmos.grid_impulse", false, CVar.SERVERONLY);
|
||||
CVarDef.Create("atmos.grid_impulse", true, CVar.SERVERONLY); // Sunrise-Edit
|
||||
|
||||
/// <summary>
|
||||
/// What fraction of air from a spaced tile escapes every tick.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Shared._Sunrise.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.MassMedia.Systems;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
|
|
@ -17,14 +18,16 @@ public sealed class NewsWriterBoundUserInterfaceState : BoundUserInterfaceState
|
|||
public readonly TimeSpan NextPublish;
|
||||
public readonly string DraftTitle;
|
||||
public readonly string DraftContent;
|
||||
public readonly List<string>? DraftPhotoPaths;
|
||||
|
||||
public NewsWriterBoundUserInterfaceState(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish, string draftTitle, string draftContent)
|
||||
public NewsWriterBoundUserInterfaceState(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish, string draftTitle, string draftContent, List<string>? draftPhotoPaths = null)
|
||||
{
|
||||
Articles = articles;
|
||||
PublishEnabled = publishEnabled;
|
||||
NextPublish = nextPublish;
|
||||
DraftTitle = draftTitle;
|
||||
DraftContent = draftContent;
|
||||
DraftPhotoPaths = draftPhotoPaths;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,12 +36,14 @@ public sealed class NewsWriterPublishMessage : BoundUserInterfaceMessage
|
|||
{
|
||||
public readonly string Title;
|
||||
public readonly string Content;
|
||||
public readonly List<string>? PhotoPaths;
|
||||
|
||||
|
||||
public NewsWriterPublishMessage(string title, string content)
|
||||
public NewsWriterPublishMessage(string title, string content, List<string>? photoPaths = null)
|
||||
{
|
||||
Title = title;
|
||||
Content = content;
|
||||
PhotoPaths = photoPaths;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,11 +68,13 @@ public sealed class NewsWriterSaveDraftMessage : BoundUserInterfaceMessage
|
|||
{
|
||||
public readonly string DraftTitle;
|
||||
public readonly string DraftContent;
|
||||
public readonly List<string>? DraftPhotoPaths;
|
||||
|
||||
public NewsWriterSaveDraftMessage(string draftTitle, string draftContent)
|
||||
public NewsWriterSaveDraftMessage(string draftTitle, string draftContent, List<string>? draftPhotoPaths = null)
|
||||
{
|
||||
DraftTitle = draftTitle;
|
||||
DraftContent = draftContent;
|
||||
DraftPhotoPaths = draftPhotoPaths;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,3 +82,19 @@ public sealed class NewsWriterSaveDraftMessage : BoundUserInterfaceMessage
|
|||
public sealed class NewsWriterRequestDraftMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NewsWriterRequestPhotosMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NewsWriterPhotosMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public readonly List<PhotoMetadata> Photos;
|
||||
|
||||
public NewsWriterPhotosMessage(List<PhotoMetadata> photos)
|
||||
{
|
||||
Photos = photos;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ public struct NewsArticle
|
|||
|
||||
[ViewVariables]
|
||||
public TimeSpan ShareTime;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<string>? PhotoPaths;
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ public abstract partial class SharedVendingMachineSystem : EntitySystem
|
|||
|
||||
uint restock = amount;
|
||||
|
||||
if (type == InventoryType.Regular)
|
||||
if (type == InventoryType.Regular || type == InventoryType.Contraband)
|
||||
{
|
||||
var chanceOfMissingStock = 1 - restockQuality;
|
||||
var result = Randomizer.NextFloat(0, 1);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,14 @@ public sealed class PdaPhotoCaptureMessage : NetMessage
|
|||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uid of the cartridge loader (PDA) that took the photo
|
||||
/// </summary>
|
||||
public NetEntity LoaderUid { get; set; }
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
LoaderUid = buffer.ReadNetEntity();
|
||||
Width = buffer.ReadInt32();
|
||||
Height = buffer.ReadInt32();
|
||||
var dataLength = buffer.ReadInt32();
|
||||
|
|
@ -36,6 +42,7 @@ public sealed class PdaPhotoCaptureMessage : NetMessage
|
|||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
buffer.Write(LoaderUid);
|
||||
buffer.Write(Width);
|
||||
buffer.Write(Height);
|
||||
buffer.Write(ImageData.Length);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Shared.Radio;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
|
|
@ -24,6 +25,12 @@ public sealed partial class MessengerAutoGroupPrototype : IPrototype
|
|||
[DataField(required: true)]
|
||||
public string GroupId { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Связанный радиоканал (для системных оповещений)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<RadioChannelPrototype>? RadioChannel { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Если true, добавляет всех пользователей автоматически
|
||||
/// </summary>
|
||||
|
|
|
|||
22
Content.Shared/_Sunrise/Messenger/MessengerSpamPrototype.cs
Normal file
22
Content.Shared/_Sunrise/Messenger/MessengerSpamPrototype.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._Sunrise.Messenger;
|
||||
|
||||
[Prototype]
|
||||
public sealed partial class MessengerSpamPrototype : IPrototype
|
||||
{
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string SenderDataset { get; private set; } = string.Empty;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string MessageDataset { get; private set; } = string.Empty;
|
||||
|
||||
[DataField]
|
||||
public string? NameDataset { get; private set; }
|
||||
|
||||
[DataField]
|
||||
public string? SurnameDataset { get; private set; }
|
||||
}
|
||||
|
|
@ -26,21 +26,26 @@ public sealed class MessengerUser
|
|||
public string? JobTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ID отдела пользователя (опционально)
|
||||
/// ID главного отдела пользователя (опционально)
|
||||
/// </summary>
|
||||
public string? DepartmentId { get; set; }
|
||||
public string? DepartmentId => DepartmentIds.Count > 0 ? DepartmentIds[0] : null;
|
||||
|
||||
/// <summary>
|
||||
/// Список всех отделов пользователя
|
||||
/// </summary>
|
||||
public List<string> DepartmentIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// ID иконки роли пользователя (опционально)
|
||||
/// </summary>
|
||||
public ProtoId<JobIconPrototype>? JobIconId { get; set; }
|
||||
|
||||
public MessengerUser(string userId, string name, string? jobTitle = null, string? departmentId = null, ProtoId<JobIconPrototype>? jobIconId = null)
|
||||
public MessengerUser(string userId, string name, string? jobTitle = null, List<string>? departmentIds = null, ProtoId<JobIconPrototype>? jobIconId = null)
|
||||
{
|
||||
UserId = userId;
|
||||
Name = name;
|
||||
JobTitle = jobTitle;
|
||||
DepartmentId = departmentId;
|
||||
DepartmentIds = departmentIds ?? new List<string>();
|
||||
JobIconId = jobIconId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,4 +590,33 @@ public sealed partial class SunriseCCVars : CVars
|
|||
/// </summary>
|
||||
public static readonly CVarDef<string> MessengerFavoriteEmojis =
|
||||
CVarDef.Create("messenger.favorite_emojis", "", CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* Messenger Spam
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Enables the mechanic where players receive spam messages on their PDA.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> MessengerSpamEnabled =
|
||||
CVarDef.Create("messenger.spam_enabled", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Minimum time between spam waves in seconds.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> MessengerSpamMinTime =
|
||||
CVarDef.Create("messenger.spam_min_time", 300f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time between spam waves in seconds.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> MessengerSpamMaxTime =
|
||||
CVarDef.Create("messenger.spam_max_time", 600f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Percentage of players (0.0 to 1.0) who will receive spam during a wave.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> MessengerSpamPlayerPercentage =
|
||||
CVarDef.Create("messenger.spam_player_percentage", 0.4f, CVar.SERVERONLY);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25400,3 +25400,220 @@
|
|||
id: 1645
|
||||
time: '2026-01-30T05:26:06.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3819
|
||||
- author: Orvex07
|
||||
changes:
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D \u0443\u0434\u0430\
|
||||
\u043B\u0435\u043D\u043D\u044B\u0439 \u043A\u043E\u043D\u0442\u0440\u043E\u043B\
|
||||
\u043B\u0435\u0440 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F\
|
||||
\ \u0448\u0430\u0442\u0442\u043B\u043E\u043C \u0434\u0438\u0432\u0435\u0440\u0441\
|
||||
\u0438\u043E\u043D\u043D\u043E\u0433\u043E \u043E\u0442\u0440\u044F\u0434\u0430"
|
||||
type: Fix
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D \u0443\u0434\u0430\
|
||||
\u043B\u0435\u043D\u043D\u044B\u0439 \u043A\u043E\u043D\u0442\u0440\u043E\u043B\
|
||||
\u043B\u0435\u0440 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F\
|
||||
\ \u0448\u0430\u0442\u0442\u043B\u043E\u043C \u044F\u0434\u0435\u0440\u043D\u044B\
|
||||
\u0445 \u043E\u043F\u0435\u0440\u0430\u0442\u0438\u0432\u043D\u0438\u043A\u043E\
|
||||
\u0432"
|
||||
type: Fix
|
||||
id: 1646
|
||||
time: '2026-01-30T05:55:56.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3812
|
||||
- author: Kinar_7
|
||||
changes:
|
||||
- message: "\u0441\u0435\u043A\u0440\u0435\u043D\u044B\u0439 \u0438\u043D\u0432\u0435\
|
||||
\u043D\u0442\u0430\u0440\u044C \u0432\u0435\u043D\u0434\u043E\u043C\u0430\u0442\
|
||||
\u0430 \u0434\u0435\u0442\u0435\u043A\u0442\u0438\u0432\u0430 \u0442\u0435\u043F\
|
||||
\u0435\u0440\u044C \u0438\u043C\u0435\u0435\u0442 \u043E\u0442\u0441\u044B\u043B\
|
||||
\u043A\u0443 \u043D\u0430 \u0434\u0438\u0441\u043A\u043E \u044D\u043B\u0438\u0437\
|
||||
\u0438\u0443\u043C."
|
||||
type: Tweak
|
||||
- message: "\u0435\u0449\u0451 \u0447\u0430\u0441\u0442\u044C \u0432\u0435\u043D\
|
||||
\u0434\u043E\u043C\u0430\u0442\u043E\u0432 \u0442\u0435\u043F\u0435\u0440\u044C\
|
||||
\ \u043C\u0435\u043D\u044F\u044E\u0442 \u0438\u043D\u0432\u0435\u043D\u0442\u0430\
|
||||
\u0440\u044C \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\
|
||||
\u0438 \u043E\u0442 \u043E\u043D\u043B\u0430\u0439\u043D\u0430"
|
||||
type: Tweak
|
||||
- message: "\u0441\u0435\u043A\u0440\u0435\u0442\u043D\u044B\u0435 \u0438\u043D\u0432\
|
||||
\u0435\u043D\u0442\u0430\u0440\u0438 \u0448\u0435\u0432\u0432\u0435\u043D\u0434\
|
||||
\u0430 \u0438 \u043C\u0438\u0441\u0442\u0435\u0440\u0430 \u0447\u0430\u043D\u0433\
|
||||
\u0430 \u0431\u044B\u043B\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\
|
||||
\u044B."
|
||||
type: Tweak
|
||||
- message: "\u0411\u043E\u0435\u0432\u043E\u0439 \u043D\u0430\u0431\u043E\u0440\
|
||||
\ \u0433\u043E\u0440\u043D\u0438\u0447\u043D\u043E\u0439 \u0441\u0438\u043D\u0434\
|
||||
\u0438\u043A\u0430\u0442\u0430 \u0432 \u0441\u0438\u043D\u0434\u0438\u0448\u043A\
|
||||
\u0430\u0444\u0435, \u0431\u044B\u043B \u043F\u0435\u0440\u0435\u043D\u0435\u0441\
|
||||
\u0435\u043D \u0441 \u0441\u0435\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E\
|
||||
\ \u0438\u043D\u0432\u0435\u043D\u0442\u0430\u0440\u044F \u0432 \u0435\u043C\
|
||||
\u0430\u0433\u043D\u0443\u0442\u044B\u0439."
|
||||
type: Tweak
|
||||
id: 1647
|
||||
time: '2026-01-30T14:06:45.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3777
|
||||
- author: Orvex
|
||||
changes:
|
||||
- message: "\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0446\u0435\
|
||||
\u043B\u044C \u043D\u0430 \u043A\u0440\u0430\u0436\u0443 \u0440\u0443\u0447\u043D\
|
||||
\u043E\u0433\u043E \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0438\u043D\u0433\
|
||||
\u0430"
|
||||
type: Add
|
||||
- message: "\u0421\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u044B\u0439 \u0440\
|
||||
\u0443\u0447\u043D\u043E\u0439 \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0438\
|
||||
\u043D\u0433 \u0437\u0430\u043C\u0435\u043D\u0435\u043D \u043D\u0430 \u0430\u043B\
|
||||
\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u0443\u044E \u0432\
|
||||
\u0435\u0440\u0441\u0438\u044E"
|
||||
type: Tweak
|
||||
id: 1648
|
||||
time: '2026-01-31T08:40:10.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3799
|
||||
- author: KaiserMaus
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043E \u0440\u0443\u043A\
|
||||
\u043E\u0432\u043E\u0434\u0441\u0442\u0432\u043E \u043F\u043E \u0432\u0437\u0440\
|
||||
\u044B\u0432\u043D\u043E\u043C\u0443 \u0448\u043D\u0443\u0440\u0443 \u0438 \u0434\
|
||||
\u0435\u0442\u043E\u043D\u0430\u0442\u043E\u0440\u0430\u0445."
|
||||
type: Add
|
||||
- message: "\u0428\u043B\u044E\u0437\u044B \u0438 \u043E\u0431\u044B\u0447\u043D\
|
||||
\u044B\u0435 \u0441\u0442\u0435\u043D\u044B \u043F\u043E\u0434\u0432\u0435\u0440\
|
||||
\u0436\u0435\u043D\u044B DeltaPressure."
|
||||
type: Tweak
|
||||
- message: "\u0423\u0432\u0435\u043B\u0438\u0447\u0435\u043D\u043E \u043A\u043E\u043B\
|
||||
\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043F\u0440\u0435\u0434\u0430\u0442\
|
||||
\u0435\u043B\u0435\u0439 \u043F\u0440\u0438 \u0432\u044B\u0441\u043E\u043A\u043E\
|
||||
\u043C \u043E\u043D\u043B\u0430\u0439\u043D\u0435."
|
||||
type: Tweak
|
||||
id: 1649
|
||||
time: '2026-01-31T22:27:45.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3832
|
||||
- author: VigersRay
|
||||
changes:
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u043F\u043E\u0434\
|
||||
\u0434\u0435\u0440\u0436\u043A\u0430 \u0441\u043C\u0430\u0439\u043B\u0438\u043A\
|
||||
\u043E\u0432 \u0434\u043B\u044F Non IC \u0447\u0430\u0442\u043E\u0432 (OOC,\
|
||||
\ \u043C\u0451\u0440\u0442\u0432\u044B\u0435, \u0430\u0434\u043C\u0438\u043D\
|
||||
\u0441\u043A\u0438\u0439)."
|
||||
type: Add
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u043D\u043E\u0432\u044B\
|
||||
\u0439 \u0441\u043C\u0430\u0439\u043B\u0438\u043A \u0447\u0435\u0440\u0435\u043F\
|
||||
\u0430."
|
||||
type: Add
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0432\u043E\u0437\
|
||||
\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0432\u0441\u0442\u0440\u0430\
|
||||
\u0438\u0432\u0430\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0441\u0442\u0438\
|
||||
\ \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0441\
|
||||
\ \u041A\u041F\u041A."
|
||||
type: Add
|
||||
- message: "\u0421\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0435 \u0443\u0432\u0435\
|
||||
\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0435 \u043E\u0431 \u0438\u0437\u0443\
|
||||
\u0447\u0435\u043D\u0438\u044F\u0445, \u043E\u0431\u044C\u044F\u0432\u043B\u0435\
|
||||
\u043D\u0438\u0438 \u0440\u043E\u0437\u044B\u0441\u043A\u0430, \u043E\u0434\u043E\
|
||||
\u0431\u0440\u0435\u043D\u0438\u0438 \u043F\u043E\u043A\u0443\u043F\u043E\u043A\
|
||||
\ \u043A\u0430\u0440\u0433\u043E, \u0441\u043E\u0437\u0434\u0430\u043D\u0438\
|
||||
\u0435 \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439, \u043F\u0440\u0438\u0442\
|
||||
\u044F\u0433\u0438\u0432\u0430\u043D\u0438\u0435 \u043E\u0431\u043B\u043E\u043C\
|
||||
\u043A\u043E\u0432, \u0442\u0440\u0430\u043D\u0441\u0444\u043E\u0440\u043C\u0430\
|
||||
\u0446\u0438\u044F \u0431\u043E\u0440\u0433\u0430, \u043F\u0440\u0435\u0434\u0441\
|
||||
\u043C\u0435\u0440\u0442\u043D\u044B\u0439 \u0445\u0440\u0438\u043F, \u0441\u043E\
|
||||
\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0421\u041C\u0430 \u0438 \u0438\u0437\
|
||||
\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0441\u0442\u0430\u043D\u0446\u0438\
|
||||
\u043E\u043D\u043D\u044B\u0445 \u0437\u0430\u043F\u0438\u0441\u0435\u0439 \u043F\
|
||||
\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u044B \u0438\u0437 \u0440\u0430\
|
||||
\u0434\u0438\u043E \u0432 \u0433\u0440\u0443\u043F\u044B \u043E\u0442\u0434\u0435\
|
||||
\u043B\u043E\u0432 \u0432 \u043C\u0435\u0441\u0441\u0435\u043D\u0436\u0435\u0440\
|
||||
\u0435."
|
||||
type: Tweak
|
||||
- message: "C\u043D\u0438\u043C\u043A\u0438 \u0438\u0437 \u0444\u043E\u0442\u043E\
|
||||
\u0430\u043F\u0430\u0440\u0430\u0442\u0430 \u0441\u0442\u0430\u043B\u044B \u0431\
|
||||
\u043E\u043B\u0435\u0435 \u0447\u0435\u0442\u043A\u0438\u043C\u0438. \u041F\u0440\
|
||||
\u043E\u0449\u0435 \u0433\u043E\u0432\u043E\u0440\u044F \u0431\u0435\u0437 \u043C\
|
||||
\u044B\u043B\u0430."
|
||||
type: Tweak
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0430 \u0432\u043E\u0437\
|
||||
\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0443\u043C\u0435\u043D\u044C\
|
||||
\u0448\u0438\u0442\u044C \u0437\u0443\u043C \u0447\u0442\u043E\u0431\u044B \u0441\
|
||||
\u0434\u0435\u043B\u0430\u0442\u044C \u0441\u0435\u043B\u0444\u0438."
|
||||
type: Tweak
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043E \u043E\u0442\u043E\
|
||||
\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0438\u043A\u043E\u043D\u043E\
|
||||
\u043A \u0434\u043E\u043B\u0436\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u0438\
|
||||
\ \u043F\u0440\u0438\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0438 \u043F\u043E\
|
||||
\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 \u0432 \u043F\
|
||||
\u0440\u0438\u0432\u0430\u0442\u043D\u0443\u044E \u0433\u0440\u0443\u043F\u043F\
|
||||
\u0443."
|
||||
type: Tweak
|
||||
- message: "\u0422\u0435\u043F\u0435\u0440\u044C \u0447\u0430\u0442\u044B \u043A\
|
||||
\u043E\u0442\u043E\u0440\u044B\u0435 \u0438\u043C\u0435\u044E\u0442 \u043D\u0435\
|
||||
\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u044B\u0435 \u0441\u043E\u043E\
|
||||
\u0431\u0449\u0435\u043D\u0438\u044F \u043F\u043E\u0434\u043D\u0438\u043C\u0430\
|
||||
\u044E\u0442\u0441\u044F \u0432\u0432\u0435\u0440\u0445 \u0441\u043F\u0438\u0441\
|
||||
\u043A\u0430."
|
||||
type: Tweak
|
||||
- message: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043E \u0442\u0435\u043A\
|
||||
\u0441\u0442\u043E\u0432\u043E\u0435 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\
|
||||
\u0435\u043D\u0438\u0435 \u043E\u0431 \u043D\u043E\u0432\u044B\u0445 \u0441\u043E\
|
||||
\u043E\u0431\u0449\u0435\u043D\u0438\u044F\u0445 \u0438\u043B\u0438 \u043F\u0440\
|
||||
\u0438\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u044F\u0445."
|
||||
type: Tweak
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u043E\u0448\
|
||||
\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043A\u043E\u0442\u043E\u0440\u043E\
|
||||
\u0439 \u0444\u043E\u0442\u043E\u0433\u0440\u0430\u0444\u0438\u0438 \u0441\u043E\
|
||||
\u0445\u0440\u0430\u043D\u044F\u043B\u0438\u0441\u044C \u043D\u0435 \u0432 \u0442\
|
||||
\u043E\u0442 \u043A\u043F\u043A \u0441 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\
|
||||
\u043E \u0444\u043E\u0442\u043A\u0430 \u0431\u044B\u043B\u0430 \u0441\u0434\u0435\
|
||||
\u043B\u0430\u043D\u0430."
|
||||
type: Fix
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E \u0441\u043C\
|
||||
\u0435\u0449\u0435\u043D\u0438\u0435 \u043E\u0431\u043B\u0430\u0441\u0442\u0438\
|
||||
\ \u0441\u044C\u0435\u043C\u043A\u0438 \u043F\u0440\u0438 \u043D\u0435\u0441\
|
||||
\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u044B\u0445 \u0440\u0430\u0437\
|
||||
\u0440\u0435\u0448\u0435\u043D\u0438\u044F\u0445 \u044D\u043A\u0440\u0430\u043D\
|
||||
\u0430."
|
||||
type: Fix
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u043E\u0448\
|
||||
\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043A\u043E\u0442\u043E\u0440\u043E\
|
||||
\u0439 \u0434\u043E\u043B\u0436\u043D\u043E\u0441\u0442\u0438 \u0441\u043E\u0441\
|
||||
\u0442\u043E\u044F\u0449\u0438\u0435 \u0432 \u043D\u0435\u0441\u043A\u043E\u043B\
|
||||
\u044C\u043A\u0438\u0445 \u0434\u0435\u043F\u0430\u0440\u0442\u0430\u043C\u0435\
|
||||
\u043D\u0442\u0430\u0445 \u043F\u043E\u043F\u0430\u0434\u0430\u043B\u0438 \u0442\
|
||||
\u043E\u043B\u044C\u043A\u043E \u0432 \u043E\u0434\u0438\u043D \u0433\u0440\u0443\
|
||||
\u043F\u043E\u0432\u043E\u0439 \u0447\u0430\u0442."
|
||||
type: Fix
|
||||
- message: "\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u043E\u0448\
|
||||
\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u043F\u043E\
|
||||
\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0438 \u043A \u0441\u0435\u0440\
|
||||
\u0432\u0435\u0440\u0443."
|
||||
type: Fix
|
||||
- message: "\u0423\u0434\u0430\u043B\u0435\u043D\u044B \u0430\u0434\u043C\u0438\u043D\
|
||||
\u044B \u0438\u0437 \u043C\u0435\u0441\u0435\u043D\u0436\u0435\u0440\u0430."
|
||||
type: Remove
|
||||
id: 1650
|
||||
time: '2026-02-01T00:55:47.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3833
|
||||
- author: VigersRay
|
||||
changes:
|
||||
- message: "\u041C\u0435\u0441\u0441\u0435\u043D\u0434\u0436\u0435\u0440 \u0441\u0442\
|
||||
\u0430\u043B \u043D\u0430\u0441\u0442\u043E\u043B\u044C\u043A\u043E \u0442\u0435\
|
||||
\u0445\u043D\u043E\u043B\u043E\u0433\u0438\u0447\u043D\u044B\u043C, \u0447\u0442\
|
||||
\u043E \u0432 \u043D\u0435\u0433\u043E \u043F\u0440\u043E\u0431\u0440\u0430\u043B\
|
||||
\u0438\u0441\u044C \u043A\u043E\u0441\u043C\u0438\u0447\u0435\u0441\u043A\u0438\
|
||||
\u0435 \u043C\u043E\u0448\u0435\u043D\u043D\u0438\u043A\u0438. \u0421\u043E\u043E\
|
||||
\u0431\u0449\u0435\u043D\u0438\u044F \u043E\u0442 \u043E\u0434\u0438\u043D\u043E\
|
||||
\u043A\u0438\u0445 \u0432\u0434\u043E\u0432, \u0441\u043E\u0431\u0430\u043A\
|
||||
\ \u043F\u043E 500, \u043F\u043E\u0445\u043E\u0442\u043B\u0438\u0432\u044B\u0445\
|
||||
\ \u0441\u043A\u0440\u0435\u043B\u043B\u043E\u0447\u043E\u043A \u0438 \u0431\
|
||||
\u0435\u0437\u0443\u043C\u043D\u044B\u0445 \u043A\u0443\u043B\u044C\u0442\u0438\
|
||||
\u0441\u0442\u043E\u0432 \u0442\u0435\u043F\u0435\u0440\u044C \u0434\u043E\u0441\
|
||||
\u0442\u0443\u043F\u043D\u044B \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\
|
||||
\u043E \u0431\u0435\u0441\u043F\u043B\u0430\u0442\u043D\u043E \u043A\u0430\u0436\
|
||||
\u0434\u043E\u043C\u0443 \u0447\u043B\u0435\u043D\u0443 \u044D\u043A\u0438\u043F\
|
||||
\u0430\u0436\u0430."
|
||||
type: Add
|
||||
- message: "\u041D\u0435\u0439\u0440\u043E\u0441\u0435\u0442\u044C \u0442\u0435\u043F\
|
||||
\u0435\u0440\u044C \u043F\u0438\u0448\u0435\u0442 \u0432\u0430\u043C \u0447\u0435\
|
||||
\u0439\u043D\u0436\u043B\u043E\u0433\u0438, \u0432\u043E\u0442 \u0442\u0430\u043A\
|
||||
\ \u0432\u043E\u0442, \u0434\u0443\u043C\u041E\u0439\u0442\u0435, \u0438\u0445\
|
||||
\u0438\u0445\u044F\u0445\u044F."
|
||||
type: Tweak
|
||||
id: 1651
|
||||
time: '2026-02-01T04:01:38.0000000+00:00'
|
||||
url: https://github.com/space-sunrise/sunrise-station/pull/3835
|
||||
|
|
|
|||
|
|
@ -72,3 +72,5 @@ ent-ClothingOuterLeatherCoat = leather jacket
|
|||
.desc = Perfect for performances!
|
||||
ent-ClothingOuterLeatherCoatOpened = leather jacket
|
||||
.desc = Perfect for performances!
|
||||
ent-ClothingOuterAerostaticBomberJacketArmored = armored aerostatic bomber jacket
|
||||
.desc = A jacket once worn by the revolutionary air brigades during the Antecentennial Revolution. There are quite a few pockets on the inside, mostly for storing notebooks and compasses.
|
||||
|
|
@ -11,5 +11,5 @@ ent-DeathRattleImplanterFreelance = { ent-BaseImplantOnlyImplanterSyndi }
|
|||
.suffix = freelance death rattle
|
||||
.desc = { ent-BaseImplantOnlyImplanterSyndi.desc }
|
||||
ent-ScramImplanterProto = { ent-ScramImplanter }
|
||||
.suffix = prototype
|
||||
.suffix = prototype scram
|
||||
.desc = { ent-ScramImplanter.desc }
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ ent-WeaponIAR52Borg = cyborg's IAR-52
|
|||
.desc = High-speed SMG, equipped with a universal connector for other types of pistol magazines.
|
||||
ent-HypoBorgSaboteurSyndicate = the cyborg saboteur's hypospray
|
||||
.desc = { ent-BaseItem.desc }
|
||||
ent-HandheldCrewMonitorBorg = handheld robot crew monitor
|
||||
ent-HandheldEmergencyCrewMonitorBorg = handheld robot crew monitor
|
||||
.desc = A hand-held crew monitor that runs off of your own power cells, which displays the status of the crew suit sensors.
|
||||
ent-WeaponPlasmaCutterBorg = borg plasma cutter
|
||||
.desc = A mining tool that fires low-damage plasma bolts at a short range. This one is modified for cyborg use.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ ent-MagazineBR64 = BR64 magazine
|
|||
.desc = Standart EarthGov type of heavy magazines.
|
||||
ent-BaseMagazineBauer127 = magazine (15mm anti-materiel)
|
||||
.desc = A large magazine for a heavy weapon, holds 15×115mm anti-materiel rounds.
|
||||
ent-MagazineBauer127Large = large magazine (15mm space anti-materiel)
|
||||
.desc = { ent-BaseMagazineBauer127.desc }
|
||||
ent-MagazineBauer127 = { ent-BaseMagazineBauer127 }
|
||||
.desc = { ent-BaseMagazineBauer127.desc }
|
||||
ent-MagazineBauer127Penetrator = magazine (15mm penetrator)
|
||||
|
|
@ -17,6 +19,15 @@ ent-BaseMagazineDragunov = magazine Dragunov (7,62mmR)
|
|||
ent-MagazineDragunovExtended = magazine Dragunov (7,62mmR)
|
||||
.suffix = Sunrise
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovSP = magazine Dragunov (7,62mmR SP)
|
||||
.suffix = Sunrise
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovHP = magazine Dragunov (7,62mmR HP)
|
||||
.suffix = Sunrise
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovFMJ = magazine Dragunov (7,62mmR FMJ)
|
||||
.suffix = Sunrise
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunov = magazine Dragunov (7,62mmR)
|
||||
.suffix = Sunrise
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ ent-CableDet = explosive cord
|
|||
|
||||
ent-CableDetStack = explosive cord
|
||||
.desc = Explosive cord for removing whatever is in your way.
|
||||
.suffix = Full
|
||||
|
||||
ent-CableDetStack10 = { ent-CableDetStack }
|
||||
.suffix = 10
|
||||
|
||||
ent-CableDetStack1 = { ent-CableDetStack }
|
||||
.suffix = 1
|
||||
|
||||
ent-EmptyDetonator = detonator cap
|
||||
.desc = A detonator cap. Requires a trigger and wire.
|
||||
|
|
|
|||
|
|
@ -51,3 +51,4 @@ guide-entry-sr-rule-cep = Политика эскалации конфликто
|
|||
guide-entry-sr-rule-ccp = Политика создания персонажей
|
||||
guide-entry-sr-rule-pana = Препятствование аресту неантагонистами
|
||||
guide-entry-improvised-grenade-casing = Improvised grenade casing
|
||||
guide-entry-detonator-triggers = Explosive cord and triggers
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
## Targets
|
||||
## Targets
|
||||
|
||||
uplink-core-extraction-toolbox-name = Core Extraction Toolbox
|
||||
uplink-core-extraction-toolbox-desc = A toolbox containing everything you need to remove a nuclear bomb's plutonium core. Instructions not included.
|
||||
|
|
@ -149,3 +149,4 @@ uplink-energy-dome-desc = A personal shield generator that protects the wearer f
|
|||
uplink-syndicate-teleporter-name = Hand syndicate teleporter
|
||||
uplink-syndicate-teleporter-desc = An experimental hand teleporting device. Teleports its owner forward in a small area. Be careful not to end up in the wall.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ messenger-emoji-favorite-title = Favorites
|
|||
messenger-emoji-favorite-hint = ПКМ по общему списку для добавления.
|
||||
ПКМ по избраном для удаления.
|
||||
messenger-emoji-all-title = All emojis
|
||||
|
||||
messenger-notification-message = Новое сообщение от { $name }
|
||||
messenger-group-notification-message = Новое сообщение в группе { $name }
|
||||
messenger-invite-notification-message = Вас пригласили в группу { $name }
|
||||
messenger-user-unknown = Unknown
|
||||
messenger-system-name = System
|
||||
messenger-leave-group = Leave group
|
||||
|
|
|
|||
|
|
@ -36,3 +36,7 @@ news-write-ui-richtext-tooltip = News articles support rich text
|
|||
|
||||
news-pda-notification-header = New news article
|
||||
news-publish-admin-announcement = {$actor} published news article {$title} by {$author}
|
||||
news-write-ui-photos-label = Photos:
|
||||
news-write-ui-add-photo-text = Add Photo
|
||||
news-write-ui-select-photo-title = Select PDA Photo
|
||||
news-write-ui-no-photos = No photos found on PDA
|
||||
|
|
|
|||
208
Resources/Locale/ru-RU/_Sunrise/messenger/spam.ftl
Normal file
208
Resources/Locale/ru-RU/_Sunrise/messenger/spam.ftl
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Casino
|
||||
messenger-spam-casino-sender-1 = МегаСтавка — ставь или проиграешь!
|
||||
messenger-spam-casino-sender-2 = Онлайн казино МегаСтавка — 256 лет на рынке полулегальных азартных игр
|
||||
messenger-spam-casino-sender-3 = Сэкси дилеры в лучшем онлайн казино
|
||||
messenger-spam-casino-sender-4 = Все остальные онлайн казино — кидалы. Мы не такие.
|
||||
|
||||
messenger-spam-casino-message-1 = Думаете жать одну кнопку на автомате это занятие для умственно неполноценных? Докажите что это не так сыграв у нас!
|
||||
messenger-spam-casino-message-2 = В нашем казино нет никаких «подкруток» или прочих нечестных приёмов. Вам просто не везёт!
|
||||
messenger-spam-casino-message-3 = Проиграли всю зарплату в друго казино? Проиграйте ещё одну у нас! В этот раз точно повезёт!
|
||||
messenger-spam-casino-message-4 = Зачем работать на корпорации, когда можно спустить свою жизнь чуть более приятным способом?
|
||||
messenger-spam-casino-message-5 = Игровая зависимость этом миф! Они просто не могут оторваться от 450 увлекательных игр в нашем казино!
|
||||
messenger-spam-casino-message-6 = Отделения МегаСтавки есть везде — от Элизиума до Аурума! Да, может даже на вашей станции!
|
||||
messenger-spam-casino-message-7 = В Онлайн Казино МегаСтавка мы не чешем колоду, гарантируем честную раздачу и наши колоды заряжены не в киосках как у конкурентов!
|
||||
|
||||
# Dating
|
||||
messenger-spam-dating-sender-1 = СладкаяЦыпа
|
||||
messenger-spam-dating-sender-2 = Одинокая русская невеста
|
||||
messenger-spam-dating-sender-3 = ТаярскаяКрасотка57
|
||||
messenger-spam-dating-sender-4 = СкРеЛлОчКа))0
|
||||
messenger-spam-dating-sender-5 = Унаточка_чмаффки
|
||||
messenger-spam-dating-sender-6 = Фанатка_ЕРП_82
|
||||
messenger-spam-dating-sender-7 = Босс_Качалки93
|
||||
|
||||
messenger-spam-dating-message-1 = Классные фотки на БыстрыхСвиданиях! Я бы пообщалась побольше, но у меня кончаются кредиты… если ты мне не скинешь немного XD (БыстрыеСвидания).
|
||||
messenger-spam-dating-message-2 = Подпишись на мой профиль — мой аккаунт [gender-female]@[gender-female].[pick("ru","ck","tj","ur","nt")], и получишь доступ к моим фото~
|
||||
messenger-spam-dating-message-3 = Слушай, у меня мало времени. Ты хочешь большой и чистой любви? Если хочешь, то ответь. Ещё никто не ответил…
|
||||
messenger-spam-dating-message-4 = У вас (1) новое сообщение от Куколки с 4 размером!
|
||||
messenger-spam-dating-message-5 = Я просто ОБОЖАЮ людей! Такие властные, сильные и умные… ммф… встретимся?
|
||||
messenger-spam-dating-message-6 = Я просто БЕЗ УМА от вульпакин! Хочу пошевелить с тобой хвостиками! Скинь аудио как ты воешь!
|
||||
messenger-spam-dating-message-7 = Я, как это говорится у меня на родине, «перфоманс артист».
|
||||
messenger-spam-dating-message-8 = Мой отец учил меня не стесняться, поэтому… не хочешь вступить в наш клуб? Всего 250 кредитов…
|
||||
messenger-spam-dating-message-9 = Я просто ВЛЮБЛЕНА в таких скреллов как ты! Такие большие, упругие и сочные щупальца на голове!
|
||||
messenger-spam-dating-message-10 = Я просто СХОЖУ С УМА от греев! Скинешь фотку своих больших глаз?
|
||||
messenger-spam-dating-message-11 = Плазмамен? А там внизу так же горячо?~
|
||||
messenger-spam-dating-message-12 = Все таяры такие милые? Обещаю почесать за ушком когда мы встретимся <3
|
||||
messenger-spam-dating-message-13 = Говорят что КПБ не устают в постели… это правда? Если так, то это свидание!
|
||||
messenger-spam-dating-message-14 = У Кидан такие большие глаза… скажем так, им есть на что посмотреть…
|
||||
messenger-spam-dating-message-15 = Легендарная гибкость и влажность слаймоменов… Хочу опробовать на себе!
|
||||
messenger-spam-dating-message-16 = Всегда хотелось пригреть драска… кстати, ты сегодня мальчик или девочка?
|
||||
messenger-spam-dating-message-17 = Надеюсь что диона переживёт отсутствие света под одеялом?~
|
||||
messenger-spam-dating-message-18 = Все остальные такие неженки… а вот воксы — самое то. Особенно пираты, ух… Встретимся?
|
||||
messenger-spam-dating-message-19 = У вас (2) новых просмотров профиля: Секси Скреллка и Сосед-по-Качалке
|
||||
|
||||
# Finance
|
||||
messenger-spam-finance-sender-1 = Ассоциация Галактических Платежей
|
||||
messenger-spam-finance-sender-2 = Бюро Отличного Бизнеса
|
||||
messenger-spam-finance-sender-3 = Надёжные Электронные Платежи
|
||||
messenger-spam-finance-sender-4 = Финансовый Департамент NanoTransen
|
||||
messenger-spam-finance-sender-5 = Главное Казначейство
|
||||
|
||||
messenger-spam-finance-message-1 = Роскошные часы по бросовым ценам! Есть экземпляры с древнего Альтама!
|
||||
messenger-spam-finance-message-2 = Часы, Ювелирные Изделия и Аксессуары из костей, Сумки и Кошельки из кожи
|
||||
messenger-spam-finance-message-3 = Внесите на счёт $100 и мгновенно получите назад $300! «Реально работает!» — Бобби
|
||||
messenger-spam-finance-message-4 = Кредиты для граждан ТСФ! Всего лишь от 99% годовых! Для остальных — 100%!
|
||||
messenger-spam-finance-message-5 = К нам поступила жалоба от одного из Ваших коллег по поводу его отношениий с Вами. После разбирательств вы были объявлены в розыск за домогательства. Я могу убедить его отозвать заявление за символические 5000 кредитов.
|
||||
messenger-spam-finance-message-6 = Мы убедительно просим Вас открыть ОТЧЕТ О ЖАЛОБЕ (прилагается), чтобы ответить на поступившую на вас жалобу, иначе вам будет начислен штраф.
|
||||
|
||||
# Medicine
|
||||
messenger-spam-medicine-sender-1 = На часах полшестого?
|
||||
messenger-spam-medicine-sender-2 = Имеете проблемы с дисфункцией?
|
||||
messenger-spam-medicine-sender-3 = Слишком часто «болит голова»?
|
||||
|
||||
messenger-spam-medicine-message-1 = Доктор Максман: РЕАЛЬНЫЕ доктора, РЕАЛЬНАЯ наука, РЕАЛЬНЫЕ результаты! Уникальная мазь на основе дегидроза фуфломицина!
|
||||
messenger-spam-medicine-message-2 = Доктор Максман был создан Джорджем Окуляром, сертифицированным ЦК урологом который лишь в этом секторе помог больше 70 000 пациентов с «мужскими проблемами». Может даже вашему начальнику!
|
||||
messenger-spam-medicine-message-3 = После семи лет исследований доктор Окуляр и его команда разработали эту простую и революционную формулу улучшения для мужчин. Редкий фуфломицин удалось синтезировать патентованым способом с помощью дегидроза монооксида дигидрогена!
|
||||
messenger-spam-medicine-message-4 = Мужчины всех видов сообщают об УДИВИТЕЛЬНОМ увеличении длины, ширины и выносливости.
|
||||
|
||||
# Prince
|
||||
messenger-spam-prince-sender-1 = Др.
|
||||
messenger-spam-prince-sender-2 = Наследный принц
|
||||
messenger-spam-prince-sender-3 = Король-регент
|
||||
messenger-spam-prince-sender-4 = Профессор
|
||||
messenger-spam-prince-sender-5 = Капитан
|
||||
|
||||
messenger-spam-prince-name-1 = Роберт
|
||||
messenger-spam-prince-name-2 = Альфред
|
||||
messenger-spam-prince-name-3 = Джозефат
|
||||
messenger-spam-prince-name-4 = Кингсли
|
||||
messenger-spam-prince-name-5 = Сехи
|
||||
messenger-spam-prince-name-6 = Жуан
|
||||
|
||||
messenger-spam-prince-surname-1 = Мугавэ
|
||||
messenger-spam-prince-surname-2 = Нкем
|
||||
messenger-spam-prince-surname-3 = Гвембеш
|
||||
messenger-spam-prince-surname-4 = Абимбола
|
||||
messenger-spam-prince-surname-5 = Ндим
|
||||
messenger-spam-prince-surname-6 = Эну
|
||||
|
||||
messenger-spam-prince-message-1 = ВАШИ СРЕДСТВА БЫЛИ ПЕРЕВЕДЕНЫ В БАНК РАЗВИТИЯ. ДЛЯ ДАЛЬНЕЙШЕГО ПЕРЕВОДА ДЕНЕЖНЫХ СРЕДСТВ СООБЩИТЕ СВОЙ НОМЕР АККАУНТА И ПИН-КОД.
|
||||
messenger-spam-prince-message-2 = Мы рады сообщить вам, что в связи с задержкой нам было поручено НЕМЕДЛЕННО перевести все средства на ваш счет. Для подтверждения счёта переведите 1000 кредитов. Не стоит волноваться, мы вернём вам кредиты после проверки.
|
||||
messenger-spam-prince-message-3 = Уважаемый получатель средств, сообщаем Вам, что перевод наследства окончательно одобрен и деньги готовы для получения Вами. Всё что Вам нужно сделать — отправить на этот же номер столько кредитов сколько Вы сможете. Это простая банковская формальность. В качестве благодарности мы вышлем Вам в 10 раз больше кредитов.
|
||||
messenger-spam-prince-message-4 = Из-за отсутствия у меня доверенных лиц мне требуется финансовый счет за пределами моего мира чтобы немедленно внести сумму в размере ПЯТИ МИЛЛИОНОВ кредитов.
|
||||
messenger-spam-prince-message-5 = Приветствую вас, сэр или мэм. Я с огромным сожалением сообщаю вам, что я умираю, и, из-за отсутствия наследников я выбрал вас, чтобы вы получили все мои сбережения за всю мою жизнь в размере 1,5 миллиарда кредитов. Но у меня нет вашего номера аккаунта и пин-кода, пожалуйста, сообщите их пока не поздно.
|
||||
|
||||
# Adult
|
||||
messenger-spam-adult-sender-1 = Подразделение Морального Духа NanoTransen
|
||||
messenger-spam-adult-sender-2 = Вы одиноки?
|
||||
messenger-spam-adult-sender-3 = Дон Солевая
|
||||
messenger-spam-adult-sender-4 = www.wetskrell.nt
|
||||
|
||||
messenger-spam-adult-message-1 = Подразделение Морального Духа NanoTransen предоставляет вам качественные развлекательные сайты. www.wetskrell.nt — один из таких сайтов и, специально для вас, ЦК сделало его бесплатным! Осталось только перейти по ссылке!
|
||||
messenger-spam-adult-message-2 = WetSkrell.nt является ксенофильским веб-сайтом, одобренным NT для использования членами экипажа мужского пола среди множества станций и аванпостов.
|
||||
messenger-spam-adult-message-3 = Wetskrell.nt обеспечивает высочайшее качество мужских развлечений для сотрудников Nanotrasen. Почти все модели старше 18 лет!
|
||||
messenger-spam-adult-message-4 = Просто введите номер и пин-код своего банковского аккаунта Nanotrasen. После этого вы получите безлимитный доступ ко ВСЕМУ контенту www.wetskrell.nt!
|
||||
|
||||
# Lottery
|
||||
messenger-spam-lottery-sender-1 = Вы выиграли бесплатные билеты!
|
||||
messenger-spam-lottery-sender-2 = Нажмите здесь, чтобы получить свой приз!
|
||||
messenger-spam-lottery-sender-3 = Вы 1000-й посетитель!
|
||||
messenger-spam-lottery-sender-4 = Вы - счастливый обладатель главного приза!
|
||||
|
||||
messenger-spam-lottery-message-1 = Вы выиграли билеты на новейший боевик БИТВА ЗА СТАНЦИЮ
|
||||
messenger-spam-lottery-message-2 = Вы выиграли билеты на новейшую криминальную драму ПО СЛЕДАМ МАРТЫШЕК
|
||||
messenger-spam-lottery-message-3 = Вы выиграли билеты на новейшую романтическую комедию БОЛЬШАЯ РАЗБОРКА В МАЛЕНЬКОЙ КРОВАТИ
|
||||
messenger-spam-lottery-message-4 = Вы выиграли билеты на скандальный блокбастер ПОЛНЫЙ ДОСТУП
|
||||
messenger-spam-lottery-message-5 = Вы выиграли билеты на скандальную комедию КАПИТАН МЁРТВ!
|
||||
messenger-spam-lottery-message-6 = Вы выиграли билеты на фильм ужасов УДАР СПЯЩЕГО КАРПА
|
||||
messenger-spam-lottery-message-7 = Вы выиграли билеты на слэшер ТУННЕЛЬНЫЙ СНЕГОВИК
|
||||
messenger-spam-lottery-message-8 = Вы выиграли билеты на трагикомедию БУДНИ ОХРАНЫ
|
||||
messenger-spam-lottery-message-9 = Вы выиграли билеты на %УДАЛЕНО АВТОМАТИЧЕСКИМ ФИЛЬТРОМ. СЛАВА NANOTRASEN%
|
||||
messenger-spam-lottery-message-10 = Вы выиграли билеты на короткометражный фильм ВСЕ КОНТРАКТЫ РАСТОРГНУТЫ
|
||||
messenger-spam-lottery-message-11 = Вы выиграли билеты на романтическую комедию ПЕРВЫЙ ХОНК!
|
||||
messenger-spam-lottery-message-12 = Вы выиграли билеты на приключенческий фильм ВОЛШЕБНИКИ ИЗ ДАЛЁКОГО КОСМОСА
|
||||
messenger-spam-lottery-message-13 = Вы выиграли билеты на фэнтези ПОСЛЕДНИЙ СЫН ХОНКОМАТЕРИ
|
||||
messenger-spam-lottery-message-14 = Вы выиграли билеты на боевик МЕСТЬ СИНДИКАТА, ЭПИЗОД III!
|
||||
messenger-spam-lottery-message-15 = Вы выиграли билеты на документальный фильм СЛАВА НТ
|
||||
messenger-spam-lottery-message-16 = Вы выиграли билеты на эротический фильм МОХНАТАЯ ПОДРУГА
|
||||
messenger-spam-lottery-message-17 = Вы выиграли билеты на эротический фильм КЛУБ КОЖЕВЕННОГО МАСТЕРСТВА
|
||||
messenger-spam-lottery-message-18 = Вы выиграли билеты на боевик РОБАСТБОРГ
|
||||
messenger-spam-lottery-message-19 = Вы выиграли билеты на драму 28 ОТТЕНКОВ ВУЛЬПАКИН
|
||||
messenger-spam-lottery-message-20 = Вы выиграли билеты на новейший триллер ЕРЕСЬ В ЦИРКЕ
|
||||
|
||||
# Events
|
||||
messenger-spam-events-sender-1 = Тамада, баян, услуги
|
||||
messenger-spam-events-sender-2 = Баянист Тамада Дискотека
|
||||
messenger-spam-events-sender-3 = Свадьба за 1000 кредитов!
|
||||
messenger-spam-events-sender-4 = Организуем праздники несмотря на легальность!
|
||||
|
||||
messenger-spam-events-message-1 = Увлекательные конкусы для ВАШЕЙ свадьбы!
|
||||
messenger-spam-events-message-2 = Невесты РЫДАЮТ от радости! Женихи теряют сознание от счастья!
|
||||
messenger-spam-events-message-3 = 7227051245544 — Игорь
|
||||
messenger-spam-events-message-4 = Только лучшие аниматоры со всего сектора! Специализированные конкурсы для сотрудников Nanotrasen! Посвящение в капитаны! Торт для начальника службы безопасности!
|
||||
messenger-spam-events-message-5 = я играть музыка кляссный беру дешево пиши да
|
||||
|
||||
# Emergency
|
||||
messenger-spam-emergency-sender-1 = МАМА ПОМОГИ!
|
||||
messenger-spam-emergency-sender-2 = СРОЧНО!
|
||||
messenger-spam-emergency-sender-3 = Папа, я вляпался
|
||||
messenger-spam-emergency-sender-4 = ПОЖАЛУЙСТА!
|
||||
|
||||
messenger-spam-emergency-message-1 = я врезался в корабль и офицер говорит что можно всё замять за 10000 кредитов, переведи пожалуйста
|
||||
messenger-spam-emergency-message-2 = ОНА САМА УМЕРЛА, Я НЕ ВИНОВАТ! УМОЛЯЮ, МНЕ НУЖНО 5000 НА НОВЫЙ МУЛЬТИПАСПОРТ! СКОРЕЕ, ОНИ СКОРО БУДУТ ЗДЕСЬ
|
||||
messenger-spam-emergency-message-3 = Меня машина убила! Переведи сколько можешь, иначе они не пришьют мне ноги обратно!
|
||||
messenger-spam-emergency-message-4 = Это твой сын, памаги! пропорщик говорид что может памочь! Иго номир щёта 133782, банк развития Цыфея
|
||||
|
||||
# Cult
|
||||
messenger-spam-cult-sender-1 = Церковь Всех-и-Сразу
|
||||
messenger-spam-cult-sender-2 = Общественная организация «Слава Ран'ис»
|
||||
messenger-spam-cult-sender-3 = Орден «ПРОТИВ ВУЛЬП»
|
||||
messenger-spam-cult-sender-4 = Движение «ЗА ВУЛЬП»
|
||||
messenger-spam-cult-sender-5 = КРОВЬ БОГУ КРОВИ
|
||||
|
||||
messenger-spam-cult-message-1 = Если вам небезразлично наше дело, заходите к нам по адресу: Сектор Ардей, планета Тифон-14, здание №1756B, этаж -76, офис 4
|
||||
messenger-spam-cult-message-2 = Наши идею могут показаться немного радикальными, но, уверяю вас, это всего лишь слухи. Напоминаем, что следущая всеобщая молитва в пятницу, и приносите жертвоприношения с собой.
|
||||
messenger-spam-cult-message-3 = Привет! Тебе одиноко? Не видишь смысла в жизни? Ну что ж, тогда мы будем рады тебе! Всего за 500 кредитов в месяц ты можешь вступить в наше дружное общество.
|
||||
messenger-spam-cult-message-4 = Вам же не всё равно? А?! Или всё равно?! Ты бесхребетная крыса или уверенный в себе гуманоид?! Если у тебя кишка не тонка, то найдёшь нас сам.
|
||||
messenger-spam-cult-message-5 = Пожалуйста, спасите меня, я здесь против своей воли.
|
||||
messenger-spam-cult-message-6 = Вы уверены что живёте праведной жизнью? Если нет, пошлите нам 100 кредитов и мы помолимся за вас. Если да, то тоже пошлите, нам нужны пожертвования.
|
||||
messenger-spam-cult-message-7 = Мы знаем. Мы найдём тебя сами, не нужно ничего делать.
|
||||
|
||||
# Pets
|
||||
messenger-spam-pets-sender-1 = Экзотические питомцы
|
||||
messenger-spam-pets-sender-2 = Друзья наши меньшие
|
||||
messenger-spam-pets-sender-3 = Питомцы, домашние и не очень
|
||||
messenger-spam-pets-sender-4 = Торговцы Элизиума
|
||||
|
||||
messenger-spam-pets-message-1 = Торговый корабль «Пупсик» приглашает вас к себе на борт. Большой выбор свиней и кабанов самых разных пород, включая редких взрывных.
|
||||
messenger-spam-pets-message-2 = Дикие родственники таяр, вульпакин, воксов, киданов, унатхов, людей в ассортименте. Внимание! Владение нашим товаром может быть запрещено вашим законодательством!
|
||||
messenger-spam-pets-message-3 = Если вы не купите у нас хоть что-то, мы пристрелим щенка. Вы думаете я шучу? У вас есть 1 час.
|
||||
messenger-spam-pets-message-4 = ГРОМ 74 27 99 14. Геннадий, Роман, Олег, Михаил, 7 4 2 7 9 9 1 4
|
||||
messenger-spam-pets-message-5 = Магазин «Мышеловка»! Мыши, крысы, большие и маленькие, плотоядные и не очень! При покупке колесо сыра в подарок! Одобрено Федерацией Мышей!
|
||||
messenger-spam-pets-message-6 = Огромный выбор питомцев, вкусные корма, защита от блох и вшей и акссесуары для друзей наших меньших, а так же вульпакин и таяр.
|
||||
|
||||
# Weapons
|
||||
messenger-spam-weapons-sender-1 = Оружие для всей семьи
|
||||
messenger-spam-weapons-sender-2 = Стволы по дешёвке
|
||||
messenger-spam-weapons-sender-3 = ПУШКИ ПУШКИ ПУШКИ
|
||||
messenger-spam-weapons-sender-4 = Большие пушки для больших парней
|
||||
messenger-spam-weapons-sender-5 = Мануфактура «У Бобби»
|
||||
|
||||
messenger-spam-weapons-message-1 = Самый большой выбор огнестрела в секторе. От дамских пистолетов до снайперской винтовки! Для совершения покупки требуется лицензия на владение оружием.
|
||||
messenger-spam-weapons-message-2 = Лучшее от Синдиката. Пистолет %УДАЛЕНО%, усыпляющая %УДАЛЕНО%, пробивающая стены %УДАЛЕНО% и даже легендарный %УДАЛЕНО% меч. Агентам скидки, сотрудникам НТ бесплатн…9э%8%?3Р1… ВНИМАНИЕ! Перехвачено враждебное сообщение. Не обращайте внимания. Возвращайтесь к работе. Слава Nanotrasen!
|
||||
messenger-spam-weapons-message-3 = Нужно что-то особое? Как насчёт магнума что сделает дыру в стене из пластали? Пневматический зонтик-ружьё? Надувной клоун со взрывчаткой внутри? Если вы об этом подумали, у нас это есть.
|
||||
messenger-spam-weapons-message-4 = Давай к делу. Тебе нужно оружие? Меня зовут Борис и у меня для тебя есть пушки. Не спрашивай откуда, просто покупай.
|
||||
messenger-spam-weapons-message-5 = Гранаты. Дымовые шашки. Тротил. Бомбы. Снаряды. Мины. С4. Ракеты. Биологическое оружие. Ядерные боеголовки. Распродажа! Успей, пока не урвали! Никаких лицензий, только бабки!
|
||||
|
||||
# Error
|
||||
messenger-spam-error-sender-1 = ОШ;Б…;кА
|
||||
messenger-spam-error-sender-2 = 25-j*%...o1q
|
||||
messenger-spam-error-sender-3 = СБОЙ СИСТЕМЫ СООБЩЕНИЙ
|
||||
messenger-spam-error-sender-4 = ОШИБКА
|
||||
|
||||
messenger-spam-error-message-1 = …от этого корабля, блядь. Ты кто такой, сука? Не пиши сюда больше блядь, я тебя найд…
|
||||
messenger-spam-error-message-2 = …имание, агент. Дальше будут перечислены ваши цели. Никто не должен уйти живым. Первую цель зов…
|
||||
messenger-spam-error-message-3 = …ася, хватить, блядь! Водки он напился, идиот. У НАС ЗАВТРА НА ТРАНС СОЛНЕЧНУЮ ФЕДЕРАЦИЮ НАЛЁТ, КАКАЯ ВОДКА! Сука, офицер СССП ещё называе…
|
||||
messenger-spam-error-message-4 = …ы меня не любишь? Потому что я скрелл?! …извини, я сорвалась. Я вся на нервах из-за работы… Прилетай ко мне, я соску…
|
||||
messenger-spam-error-message-5 = …еальные стулья! У нас лучшие стулья в секторе! С обивкой и без, с кожей и с инкрустированным ураном! Есть даже передвижные стулья с педалями! Кроме тог…
|
||||
messenger-spam-error-message-6 = …а сковороду тем временем налейте еще немного масла и выложите тертую свеклу. Обжарьте пару минут и добавьте уксус. Тушите еще минут 5, а после выложите томатную пасту. Томите на медленном огне еще 5-7 минут. Зате…
|
||||
|
|
@ -31,7 +31,7 @@ ent-ClothingOuterArmorAmberNewJan = { ent-ClothingOuterArmorAmberJan }
|
|||
ent-ClothingOuterArmorAbductor = жилет абдуктора
|
||||
.desc = { ent-ClothingOuterArmorBasic.desc }
|
||||
ent-ClothingOuterArmorPlate = плитоносец
|
||||
.desc = Стандартный бронежилет типа II, обеспечивающий хорошую защиту от пуль и лазеров благодаря вставленным фронтальной и тыльной плитам, но не дающий особой защиты от остального.
|
||||
.desc = Стандартный бронежилет типа II, обеспечивающий хорошую защиту от пуль благодаря вставленным фронтальной и тыльной плитам, но не дающий особой защиты от остального.
|
||||
ent-ClothingOuterArmorStab = противоножевой жилет
|
||||
.desc = Несмотря на название, это обычный бронежилет типа II, обеспечивающий хорошую защиту от ударов и колющих атак, но не более.
|
||||
ent-ClothingOuterArmorHeatAbsorb = теплозащитный жилет
|
||||
|
|
|
|||
|
|
@ -72,3 +72,5 @@ ent-ClothingOuterLeatherCoat = кожаная куртка
|
|||
.desc = Прекрасно подходит для выступлений!
|
||||
ent-ClothingOuterLeatherCoatOpened = кожаная куртка
|
||||
.desc = Прекрасно подходит для выступлений!
|
||||
ent-ClothingOuterAerostaticBomberJacketArmored = бронированная аэростатическая куртка-бомбер
|
||||
.desc = Куртка, которую носили революционные воздушные бригады во время Мировой революции. Внутри довольно много карманов, в основном для хранения блокнотов и компасов.
|
||||
|
|
@ -76,6 +76,18 @@ ent-MobPirateT2ShotgunGlass = мушкетонщик пиратов
|
|||
.desc = Знает толк в дробовиках, вооружен мушкетоном. Меньше дробовик, больше ручная пушка. Стреляет всем, что влезло в ствол.
|
||||
ent-MobPirateT2Musket = мушкетер пиратов
|
||||
.desc = Пират вооруженый старинным мушкетом крупного калибра и штыком.
|
||||
ent-MobPirateT2MusketEMP = { ent-MobPirateT2Musket }
|
||||
.suffix = ЭМИ
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketBlast = { ent-MobPirateT2Musket }
|
||||
.suffix = Взрыв
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketFrag = { ent-MobPirateT2Musket }
|
||||
.suffix = Осколки
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketPenetrator = { ent-MobPirateT2Musket }
|
||||
.suffix = Бронебой
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MachineCannon = ядромётчик пиратов
|
||||
.desc = Пират с ручной пушкой с ленточным питанием... Что?!.
|
||||
ent-MobPirateT2Mech = Большой Ярк
|
||||
|
|
@ -130,3 +142,238 @@ ent-MobPirateT3Boss = Красный Черепень
|
|||
.desc = Тяжёлый боевой экзокостюм СССП разновидности «Дюранда». Медленный, но неумолимый. Вооружён старым пулемётом крупного калибра. Может выдать рывок, выпустить огонь или скрыться в дыму, сопровождая всё жутким смехом.
|
||||
ent-MobPirateT3Tachanka = Тачанка
|
||||
.desc = Пулемёт! Установлен и заряжен!
|
||||
|
||||
# timed despawn variants reuse base names/descriptions
|
||||
ent-MobPirateT1KnifeTimed = { ent-MobPirateT1Knife }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Knife.desc }
|
||||
ent-MobPirateT1MonkeyKnuckleDustersTimed = { ent-MobPirateT1MonkeyKnuckleDusters }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyKnuckleDusters.desc }
|
||||
ent-MobPirateT1PickaxeTimed = { ent-MobPirateT1Pickaxe }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Pickaxe.desc }
|
||||
ent-MobPirateT1PistolTimed = { ent-MobPirateT1Pistol }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Pistol.desc }
|
||||
ent-MobPirateT1MosinTimed = { ent-MobPirateT1Mosin }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Mosin.desc }
|
||||
ent-MobPirateT1PKATimed = { ent-MobPirateT1PKA }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1PKA.desc }
|
||||
ent-MobPirateT1MonkeyFlareGunTimed = { ent-MobPirateT1MonkeyFlareGun }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyFlareGun.desc }
|
||||
ent-MobPirateT1MonkeyFlareGunCoinTimed = { ent-MobPirateT1MonkeyFlareGunCoin }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyFlareGunCoin.desc }
|
||||
ent-MobPirateT1MonkeyFlareGunIncendiaryTimed = { ent-MobPirateT1MonkeyFlareGunIncendiary }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyFlareGunIncendiary.desc }
|
||||
ent-MobPirateT1MonkeyFlareGunUraniumTimed = { ent-MobPirateT1MonkeyFlareGunUranium }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyFlareGunUranium.desc }
|
||||
ent-MobPirateT1ScrapSMGTimed = { ent-MobPirateT1ScrapSMG }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1ScrapSMG.desc }
|
||||
ent-MobPirateT1SMGTimed = { ent-MobPirateT1SMG }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1SMG.desc }
|
||||
ent-MobPirateT1FlintlockTimed = { ent-MobPirateT1Flintlock }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Flintlock.desc }
|
||||
ent-MobPirateT1MonkeyFireBombTimed = { ent-MobPirateT1MonkeyFireBomb }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyFireBomb.desc }
|
||||
ent-MobPirateT1MonkeyEDaggerTimed = { ent-MobPirateT1MonkeyEDagger }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1MonkeyEDagger.desc }
|
||||
ent-MobPirateT1HunterTimed = { ent-MobPirateT1Hunter }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Hunter.desc }
|
||||
ent-MobPirateT1HunterSmashTimed = { ent-MobPirateT1HunterSmash }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1HunterSmash.desc }
|
||||
ent-MobPirateT1BombardierFireTimed = { ent-MobPirateT1BombardierFire }
|
||||
.suffix = Деспавн 20мин, Зажиг
|
||||
.desc = { ent-MobPirateT1BombardierFire.desc }
|
||||
ent-MobPirateT1BombardierGlassTimed = { ent-MobPirateT1BombardierGlass }
|
||||
.suffix = Деспавн 20мин, Картечь
|
||||
.desc = { ent-MobPirateT1BombardierGlass.desc }
|
||||
ent-MobPirateT1BombardierPipeTimed = { ent-MobPirateT1BombardierPipe }
|
||||
.suffix = Деспавн 20мин, Бомба
|
||||
.desc = { ent-MobPirateT1BombardierPipe.desc }
|
||||
ent-MobPirateT1ECutlassTimed = { ent-MobPirateT1ECutlass }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1ECutlass.desc }
|
||||
ent-MobPirateT1EPickaxeTimed = { ent-MobPirateT1EPickaxe }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1EPickaxe.desc }
|
||||
ent-MobPirateT1PlasmaCutterTimed = { ent-MobPirateT1PlasmaCutter }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1PlasmaCutter.desc }
|
||||
ent-MobPirateT1ScrapRifleTimed = { ent-MobPirateT1ScrapRifle }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1ScrapRifle.desc }
|
||||
ent-MobPirateT1RifleTimed = { ent-MobPirateT1Rifle }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Rifle.desc }
|
||||
ent-MobPirateT1BorgAssaultTimed = { ent-MobPirateT1BorgAssault }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1BorgAssault.desc }
|
||||
ent-MobPirateT1BorgMiningTimed = { ent-MobPirateT1BorgMining }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1BorgMining.desc }
|
||||
ent-MobPirateT1BorgDerelictTimed = { ent-MobPirateT1BorgDerelict }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1BorgDerelict.desc }
|
||||
ent-MobPirateT2CutlassTimed = { ent-MobPirateT2Cutlass }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Cutlass.desc }
|
||||
ent-MobPirateT2ECutlassTimed = { ent-MobPirateT2ECutlass }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2ECutlass.desc }
|
||||
ent-MobPirateT2SledgehammerTimed = { ent-MobPirateT2Sledgehammer }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Sledgehammer.desc }
|
||||
ent-MobPirateT2GauntletDrillTimed = { ent-MobPirateT2GauntletDrill }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2GauntletDrill.desc }
|
||||
ent-MobPirateT2ShotgunTimed = { ent-MobPirateT2Shotgun }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Shotgun.desc }
|
||||
ent-MobPirateT2ShotgunMiniBallTimed = { ent-MobPirateT2ShotgunMiniBall }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2ShotgunMiniBall.desc }
|
||||
ent-MobPirateT2ShotgunGlassTimed = { ent-MobPirateT2ShotgunGlass }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2ShotgunGlass.desc }
|
||||
ent-MobPirateT2MusketTimed = { ent-MobPirateT2Musket }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketEMPTimed = { ent-MobPirateT2MusketEMP }
|
||||
.suffix = Деспавн 20мин, ЭМИ
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketBlastTimed = { ent-MobPirateT2MusketBlast }
|
||||
.suffix = Деспавн 20мин, Взрыв
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketFragTimed = { ent-MobPirateT2MusketFrag }
|
||||
.suffix = Деспавн 20мин, Осколки
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MusketPenetratorTimed = { ent-MobPirateT2MusketPenetrator }
|
||||
.suffix = Деспавн 20мин, Бронебой
|
||||
.desc = { ent-MobPirateT2Musket.desc }
|
||||
ent-MobPirateT2MachineCannonTimed = { ent-MobPirateT2MachineCannon }
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2MachineCannon.desc }
|
||||
ent-MobPirateT2DoubleFlintlockTimed = { ent-MobPirateT2DoubleFlintlock }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2DoubleFlintlock.desc }
|
||||
ent-MobPirateT2DoubleRevolverTimed = { ent-MobPirateT2DoubleRevolver }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2DoubleRevolver.desc }
|
||||
ent-MobPirateT2RevolverCutlassTimed = { ent-MobPirateT2RevolverCutlass }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2RevolverCutlass.desc }
|
||||
ent-MobPirateT2GrapeshotTimed = { ent-MobPirateT2Grapeshot }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Grapeshot.desc }
|
||||
ent-MobPirateT2CannoballTimed = { ent-MobPirateT2Cannoball }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Cannoball.desc }
|
||||
ent-MobPirateT2MechTimed = { ent-MobPirateT2Mech }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Mech.desc }
|
||||
ent-MobPirateT2JuggernautTimed = { ent-MobPirateT2Juggernaut }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Juggernaut.desc }
|
||||
ent-MobPirateT3BolaTimed = { ent-MobPirateT3Bola }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Bola.desc }
|
||||
ent-MobPirateT3StunTimed = { ent-MobPirateT3Stun }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Stun.desc }
|
||||
ent-MobPirateT3Shield1984Timed = { ent-MobPirateT3Shield1984 }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Shield1984.desc }
|
||||
ent-MobPirateT3ShieldSMGTimed = { ent-MobPirateT3ShieldSMG }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3ShieldSMG.desc }
|
||||
ent-MobPirateT3DMRTimed = { ent-MobPirateT3DMR }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3DMR.desc }
|
||||
ent-MobPirateT3Rifle2Timed = { ent-MobPirateT3Rifle2 }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Rifle2.desc }
|
||||
ent-MobPirateT3RifleTimed = { ent-MobPirateT3Rifle }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Rifle.desc }
|
||||
ent-MobPirateT3LaserSMGTimed = { ent-MobPirateT3LaserSMG }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3LaserSMG.desc }
|
||||
ent-MobPirateT3LaserCannonTimed = { ent-MobPirateT3LaserCannon }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3LaserCannon.desc }
|
||||
ent-MobPirateT3RevolverTimed = { ent-MobPirateT3Revolver }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Revolver.desc }
|
||||
ent-MobPirateT3SMGTimed = { ent-MobPirateT3SMG }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3SMG.desc }
|
||||
ent-MobPirateT3L6Timed = { ent-MobPirateT3L6 }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3L6.desc }
|
||||
ent-MobPirateT3TachankaTimed = { ent-MobPirateT3Tachanka }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Tachanka.desc }
|
||||
ent-MobPirateT3RPDTimed = { ent-MobPirateT3RPD }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3RPD.desc }
|
||||
ent-MobPirateT3GrenadierTimed = { ent-MobPirateT3Grenadier }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Grenadier.desc }
|
||||
ent-MobPirateT3InfiltratorTimed = { ent-MobPirateT3Infiltrator }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Infiltrator.desc }
|
||||
ent-MobPirateT3SniperTimed = { ent-MobPirateT3Sniper }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Sniper.desc }
|
||||
ent-MobPirateT1BossTimed = { ent-MobPirateT1Boss }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT1Boss.desc }
|
||||
ent-MobPirateT2BossTimed = { ent-MobPirateT2Boss }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT2Boss.desc }
|
||||
ent-MobPirateT3BossTimed = { ent-MobPirateT3Boss }
|
||||
|
||||
.suffix = Деспавн 20мин
|
||||
.desc = { ent-MobPirateT3Boss.desc }
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ ent-SmokeScreenImplanter = { ent-Implanter }
|
|||
.desc = { ent-BaseImplantOnlyImplanterSyndi.desc }
|
||||
.suffix = Дым
|
||||
ent-CreepyLaughImplanter = { ent-Implanter }
|
||||
.suffix = Санрайз, Смех
|
||||
.suffix = Смех ужаса
|
||||
.desc = { ent-BaseImplantOnlyImplanterSyndi.desc }
|
||||
ent-RadioImplanterFreelance = { ent-Implanter }
|
||||
.suffix = Фриланс, радио
|
||||
|
|
@ -11,5 +11,5 @@ ent-DeathRattleImplanterFreelance = { ent-Implanter }
|
|||
.suffix = Фриланс, предсмертный хрип
|
||||
.desc = { ent-BaseImplantOnlyImplanterSyndi.desc }
|
||||
ent-ScramImplanterProto = { ent-ScramImplanter }
|
||||
.suffix = прототип
|
||||
.suffix = прототип побега
|
||||
.desc = { ent-ScramImplanter.desc }
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ ent-WeaponIAR52Borg = IAR-52 киборга
|
|||
.desc = Высокоскорострельный ПП, оборудован универсальным разъёмом для других типов пистолетных магазинов.
|
||||
ent-HypoBorgSaboteurSyndicate = гипоспрей киборга диверсанта
|
||||
.desc = { ent-BaseItem.desc }
|
||||
ent-HandheldCrewMonitorBorg = портативный монитор экипажа киборга
|
||||
ent-HandheldEmergencyCrewMonitorBorg = портативный монитор экипажа киборга
|
||||
.desc = { ent-HandheldCrewMonitor.desc }
|
||||
ent-WeaponPlasmaCutterBorg = плазменный резак борга
|
||||
.desc = Инструмент для добычи, стреляющий плазменными зарядами с малым уроном на близком расстоянии. Этот модифицирован для использования киборгами и имеет автоматический режим стрельбы.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
ent-BaseMagazinePistolCaselessRifleExtended = расширенный пистолетный магазин (.20 безгильзовый)
|
||||
ent-BaseMagazinePistolCaselessRifleExtended = расширенный пистолетный магазин (.25 безгильзовый)
|
||||
.desc = { ent-BaseMagazinePistolCaselessRifle.desc }
|
||||
ent-MagazineCannonBallMini = чемодан с ядрами
|
||||
.desc = Чемодан для аккуратного хранения ядер от пиратской пушки с ленточной подачей.
|
||||
ent-MagazinePistolSubMachineGunCaselessExtended = Расширенный магазин (.20 безгильзовые)
|
||||
ent-MagazinePistolSubMachineGunCaselessExtended = Расширенный магазин (.25 безгильзовые)
|
||||
.desc = { ent-BaseMagazineLightRifle.desc }
|
||||
|
|
|
|||
|
|
@ -14,23 +14,24 @@ ent-MagazineBauer127Frag = магазин (15мм осколочные)
|
|||
.desc = Большой магазин для тяжёлого оружия, вмещает патроны 15x115 мм осколочного действия.
|
||||
ent-MagazineBauer127Emp = магазин (15мм ЭМИ)
|
||||
.desc = Большой магазин для тяжёлого оружия, вмещает патроны 15x115 мм ЭМИ действия.
|
||||
ent-BaseMagazineDragunov = магазин Драгунова (7,62ммR)
|
||||
ent-BaseMagazineDragunov = магазин (7,62ммR)
|
||||
.desc = { ent-BaseItem.desc }
|
||||
ent-MagazineDragunovExtended = магазин Драгунова (7,62ммR)
|
||||
.suffix = Восход
|
||||
ent-MagazineDragunovExtended = магазин (7,62ммR)
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunov = магазин Драгунова (7,62ммR)
|
||||
.suffix = Восход
|
||||
ent-MagazineDragunovSP = магазин (7,62ммR SP)
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovHP = магазин (7,62ммR HP)
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovFMJ = магазин (7,62ммR FMJ)
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunov = магазин (7,62ммR)
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovEmpty = магазин (7,62ммR любой)
|
||||
.suffix = Восход, пустой
|
||||
.suffix = пустой
|
||||
.desc = { ent-MagazineDragunov.desc }
|
||||
ent-MagazineDragunovIncendiary = магазин (7,62ммR зажигательные)
|
||||
.suffix = Восход
|
||||
.desc = { ent-MagazineDragunov.desc }
|
||||
ent-MagazineDragunovPractice = магазин (7,62ммR учебные)
|
||||
.suffix = Восход
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
ent-MagazineDragunovUranium = магазин (7,62ммR урановые)
|
||||
.suffix = Восход
|
||||
.desc = { ent-BaseMagazineDragunov.desc }
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ ent-WeaponPistolDeagleMetus = Desert Eagle Metus
|
|||
.desc = На рукояти потёртости от старых рук, тяжёлое и уверенное, как приговор, что не раз выносился без разбирательств.
|
||||
.suffix = Санрайз
|
||||
ent-WeaponRevolverSpearhead = Авангард
|
||||
.desc = Громоздкий авторевольвер револьвер, который иногда используется штурмовыми отрядами и офицерами спецподразделений, а также гражданскими правоохранительными органами. Стреляет патронами .45 Магнум.
|
||||
.desc = Громоздкий авторевольвер, который иногда используется штурмовыми отрядами и офицерами спецподразделений, а также гражданскими правоохранительными органами. Стреляет патронами .45 Магнум.
|
||||
ent-WeaponRevolverSpearheadBlack = Авангард
|
||||
.desc = { ent-WeaponRevolverSpearhead.desc }
|
||||
ent-WeaponPistolM1984 = D1984
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ ent-CableDet = взрывной шнур
|
|||
ent-CableDetStack = взрывной шнур
|
||||
.desc = Взрывной шнур, чтобы убрать всё, что стоит на пути.
|
||||
.suffix = Полный
|
||||
ent-CableDetStack10 = { ent-CableDetStack }
|
||||
.desc = { ent-CableDetStack.desc }
|
||||
.suffix = 10
|
||||
ent-CableDetStack1 = { ent-CableDetStack }
|
||||
.desc = { ent-CableDetStack.desc }
|
||||
.suffix = 1
|
||||
|
||||
ent-ParcelWrapAdmeme = блюспейс-обёртка
|
||||
.desc = Бумага для упаковки предметов при транспортировке. Эта, кажется, способна прятать внутри подозрительно много пространства.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
guide-entry-ammunition = Боеприпасы
|
||||
guide-entry-improvised-grenade-casing = Самодельная граната (корпус)
|
||||
guide-entry-detonator-triggers = Взрывной шнур и триггеры
|
||||
|
||||
guide-entry-disease = Разумная болезнь
|
||||
guide-entry-expeditions = Экспедиции
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ research-technology-basic-cyberlimbs = Базовые кибер-конечно
|
|||
research-technology-implant-extractor-safety = Безопасное извлечение имплантов
|
||||
research-technology-implant-extractor = Извлечение имплантов
|
||||
research-technology-advanced-surgery = Продвинутая хирургия
|
||||
research-technology-hanheld-crew-monitor = Портативный мониторинг
|
||||
research-technology-hanheld-crew-monitor = Портативный мониторинг Пульс-Гард
|
||||
research-technology-mechanized-medical-treatment = Механизированное лечение
|
||||
research-technology-handcraft-nvd = Кустарные ПНВ
|
||||
research-technology-basic-nvd = Продвинутое ПНВ
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ uplink-magazine-bauer127-extended-desc = Магазин для Bauer127. Сод
|
|||
uplink-magazine-dl6902-name = Короб-магазин DL6902 (7,62мм)
|
||||
uplink-magazine-dl6902-desc = Магазин для Dl6902. Содержит 200 патронов калибра 7,62х39мм.
|
||||
uplink-magazine-dragunov-desc = Магазин для Драгунова. Содержит 10 патронов калибра 7,62х54R.
|
||||
uplink-magazine-dragunov-sp-desc = Магазин для Драгунова. Содержит 10 патронов SP калибра 7,62х54R.
|
||||
uplink-magazine-dragunov-hp-desc = Магазин для Драгунова. Содержит 10 патронов HP калибра 7,62х54R.
|
||||
uplink-magazine-dragunov-fmj-desc = Магазин для Драгунова. Содержит 10 патронов FMJ калибра 7,62х54R.
|
||||
uplink-magazine-dragunov-incendiary-desc = Магазин для Драгунова. Содержит 10 зажигательных патронов калибра 7,62х54R.
|
||||
uplink-magazine-dragunov-extended-desc = Магазин для Драгунова. Содержит 20 патронов калибра 7,62х54R.
|
||||
uplink-magazine-bulldog-uraniumslug-desc = Барабанный магазин на 8 урановых пулевых патронов. Совместим с "Бульдогом".
|
||||
uplink-magazine-bulldog-uranium-desc = Барабанный магазин на 8 патронов урановой дроби, легко поражает экипаж нанотрейзен лучевой болезнью.
|
||||
|
|
@ -46,10 +50,10 @@ uplink-skm24-ammo-desc = Винтовочный магазин на 30 патр
|
|||
uplink-estoc-ammo-name = Магазин для винтовки (.20)
|
||||
uplink-estoc-ammo-desc = Магазин на 25 патронов. Совместим с Эсток.
|
||||
## Weapon (Sunrise)
|
||||
uplink-c40r-name = C-40r
|
||||
uplink-c40r-desc = Безгильзовый пистолет-пулемёт C-40r, великолепно работает на ближней дистанции.
|
||||
uplink-c40r-bundle-name = Набор "C-40r"
|
||||
uplink-c40r-bundle-desc = Включает C-40r вместе с несколькими магазинами для быстрой перестрелки.
|
||||
uplink-c40r-bundle-desc = Более старый: Культовый пистолет-пулемет C-40r в комплекте с тремя магазинами тяжелого калибра.
|
||||
uplink-c40r-name = C-40r биокодированный
|
||||
uplink-c40r-desc = Культовый пистолет-пулемет C-40r в комплекте с коробкой стандартных патронов 40-го калибра.
|
||||
|
||||
uplink-magazine-127-desc = Магазин Bauer SR-127 на 7 патронов предназначеных для уничтожения мехов, киборгов или стркутур таких как решетки и окна, пары попаданий достаточно для пролома стены.
|
||||
uplink-magazine-127pen-desc = Магазин Bauer SR-127 на 7 патронов предназначеных для ликвидации защищенных противников а так же целей за укрытиями и стенами, прекрасно сочетаются с термальным зрением.
|
||||
|
|
@ -202,8 +206,8 @@ uplink-smoke-screen-implanter-name = Имплантер Дымовой Заве
|
|||
uplink-smoke-screen-implanter-desc = Создает небольшое облако дыма, в котором вы можете скрыться. Можно использовать до трех раз, прежде чем у вас закончится газ.
|
||||
uplink-creepy-laugh-implanter-name = Имплантер Жуткого Смеха
|
||||
uplink-creepy-laugh-implanter-desc = Аудиоимплант, воспроизводящий фирменный смех синди-киборга. Раздражает, пугает, стиль гарантирован.
|
||||
uplink-scram-implanter-proto-name = Имплантер Прототип-Побег
|
||||
uplink-scram-implanter-proto-desc = Имплант на 2 заряда с огромной перезарядкой в 20 минут. Телепортирует вас в крупном радиусе, пытается перенести на свободную клетку, иногда может сбоить. Он точно безопасен?
|
||||
uplink-scram-implanter-proto-name = Имплантер Прототип-Побега
|
||||
uplink-scram-implanter-proto-desc = Имплант на 1 заряд и огромной перезарядкой в 15 минут. Телепортирует вас в крупном радиусе, пытается перенести на свободную клетку, иногда может сбоить. Он точно безопасен?
|
||||
|
||||
## Ammo Kits and Bundle
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ messenger-emoji-favorite-title = Избранные
|
|||
messenger-emoji-favorite-hint = ПКМ по общему списку для добавления.
|
||||
ПКМ по избранным для удаления.
|
||||
messenger-emoji-all-title = Все смайлики
|
||||
|
||||
messenger-notification-message = Новое сообщение от { $name }
|
||||
messenger-group-notification-message = Новое сообщение в группе { $name }
|
||||
messenger-invite-notification-message = Вас пригласили в группу { $name }
|
||||
messenger-user-unknown = Неизвестно
|
||||
messenger-system-name = Система
|
||||
messenger-leave-group = Выйти из группы
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# also used in MakeGhostRuleWindow and MakeGhostRoleCommand
|
||||
# also used in MakeGhostRuleWindow and MakeGhostRoleCommand
|
||||
ghost-role-component-default-rules =
|
||||
Вы не помните ничего из своей предыдущей жизни, если администратор не сказал вам обратное.
|
||||
Вы не помните ничего из своей предыдущей жизни и не помните ничего из того, что узнали, будучи призраком.
|
||||
|
|
@ -292,3 +292,9 @@ ghost-role-information-artifact-description = Осуществляйте сво
|
|||
ghost-role-information-syndie-assaultborg-name = Штурмовой киборг Синдиката
|
||||
ghost-role-information-syndie-assaultborg-description = Ядерным оперативникам требуется подкрепление. Вы, хладнокремниевая машина для убийств, будете им помогать. Больше дакки!
|
||||
ghost-role-information-expedition-pirate-rules = Вы [color=red][bold]Умный Пират[/bold][/color] более умный ваших товарищей, не дайте утилизаторам вас ограбить. Вам [color=red][bold]запрещено[/bold][/color] покидать комплекс и саму экспедицию на шаттле.
|
||||
ghost-role-information-rules-expedition-pirate-antagonist =
|
||||
Вы — [color=red][bold]NPC-пират[/bold][/color]. Ваши намерения вредят станции и её экипажу.
|
||||
Вы должны [bold]защищать[/bold] свою территорию и сотрудничать с другими пиратами.
|
||||
Вам [color=green][bold]можно[/bold][/color] покинуть экспедицию на шаттле, но [color=red][bold]только[/bold][/color] если [color=orange][bold]утилизаторы[/bold][/color] (живые или мёртвые) находятся на шаттле.
|
||||
Помните, что вы как [color=red][bold]NPC-пират[/bold][/color] [bold]ограничены[/bold] временем: через [color=red][bold]20 минут[/bold][/color] вас автоматически переместят в наблюдателя.
|
||||
Вы не помните ничего из своей предыдущей жизни и не помните ничего из того, что узнали, будучи призраком.
|
||||
|
|
|
|||
|
|
@ -41,3 +41,7 @@ news-write-ui-richtext-tooltip =
|
|||
{ "[bullet/]bullet[/color]" }
|
||||
news-pda-notification-header = Новая новостная статья
|
||||
news-publish-admin-announcement = { $actor } опубликовал(а) новостную статью { $title } за авторством { $author }
|
||||
news-write-ui-photos-label = Фотографии:
|
||||
news-write-ui-add-photo-text = Добавить фото
|
||||
news-write-ui-select-photo-title = Выберите фото с КПК
|
||||
news-write-ui-no-photos = На КПК нет фотографий
|
||||
|
|
|
|||
|
|
@ -211,16 +211,10 @@ uplink-sniper-bundle-name = Набор снайпера
|
|||
uplink-sniper-bundle-desc = Неприметный чемодан, в котором находятся Христов, 10 запасных патронов и удобная маскировка, маскировка обладает умеренной базовой защитой.
|
||||
uplink-c20r-bundle-name = Набор "C-20r"
|
||||
uplink-c20r-bundle-desc = Старый добрый: Классический пистолет-пулемёт C-20r в комплекте с тремя магазинами.
|
||||
uplink-c40r-bundle-name = Набор "C-40r"
|
||||
uplink-c40r-bundle-desc = Более старый: Культовый пистолет-пулемет C-40r в комплекте с тремя магазинами тяжелого калибра.
|
||||
uplink-c40r-name = C-40r биокодированный
|
||||
uplink-c40r-desc = Культовый пистолет-пулемет C-40r в комплекте с коробкой стандартных патронов 40-го калибра.
|
||||
uplink-bulldog-bundle-name = Набор "Бульдог"
|
||||
uplink-bulldog-bundle-desc = Простой и надёжный: содержит популярный дробовик Бульдог, барабан пуль и три барабана дроби а так же термальный визор.
|
||||
uplink-grenade-launcher-china-lake-name = Набор "China-Lake"
|
||||
uplink-grenade-launcher-china-lake-desc = Старый гранатомёт China-Lake и сумкой запасных снарядов. Может стрелять как контактными, так и неконтактными гранатами.
|
||||
uplink-grenade-launcher-m79-bundle-name = Набор "М79"
|
||||
uplink-grenade-launcher-m79-bundle-desc = Набор однозарядного гранатомёта вместе с сумкой запасных снарядов, чтобы начать гранатомётную вечеринку в джунглях.
|
||||
uplink-grenade-launcher-gl70-name = Набор "GL-70"
|
||||
uplink-grenade-launcher-gl70-desc = Набор с многозарядным автоматическим гранатомётом с барабаном на 6 снарядов и сумкой запасных снарядов. Может стрелять как контактными, так и неконтактными гранатами.
|
||||
uplink-l6-saw-bundle-name = Набор "L6 Saw"
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ entities:
|
|||
- type: SpreaderGrid
|
||||
- type: Shuttle
|
||||
dampingModifier: 0.25
|
||||
- type: AssaultOpsShuttle
|
||||
- type: ImplicitRoof
|
||||
- type: GridPathfinding
|
||||
- type: Gravity
|
||||
|
|
|
|||
|
|
@ -530,6 +530,7 @@ entities:
|
|||
- type: OccluderTree
|
||||
- type: Shuttle
|
||||
dampingModifier: 0.25
|
||||
- type: NukeOpsShuttle
|
||||
- type: RadiationGridResistance
|
||||
- type: GravityShake
|
||||
shakeTimes: 10
|
||||
|
|
|
|||
|
|
@ -579,6 +579,7 @@ entities:
|
|||
- type: OccluderTree
|
||||
- type: Shuttle
|
||||
dampingModifier: 0.25
|
||||
- type: NukeOpsShuttle
|
||||
- type: RadiationGridResistance
|
||||
- type: GravityShake
|
||||
shakeTimes: 10
|
||||
|
|
|
|||
|
|
@ -655,15 +655,7 @@ entities:
|
|||
- type: GridPathfinding
|
||||
- type: ImplicitRoof
|
||||
- type: ExplosionAirtightGrid
|
||||
- proto: AirlockExternalGlassNukeopLocked
|
||||
entities:
|
||||
- uid: 344
|
||||
components:
|
||||
- type: Transform
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: -8.5,2.5
|
||||
parent: 1
|
||||
- proto: AirlockExternalNukeopLocked
|
||||
- proto: AirlockExternalGlassCargoLocked
|
||||
entities:
|
||||
- uid: 83
|
||||
components:
|
||||
|
|
@ -739,6 +731,12 @@ entities:
|
|||
rot: -1.5707963267948966 rad
|
||||
pos: -4.5,-6.5
|
||||
parent: 1
|
||||
- uid: 344
|
||||
components:
|
||||
- type: Transform
|
||||
rot: 1.5707963267948966 rad
|
||||
pos: -8.5,2.5
|
||||
parent: 1
|
||||
- proto: AirlockSalvageLocked
|
||||
entities:
|
||||
- uid: 4
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ entities:
|
|||
- type: SpreaderGrid
|
||||
- type: Shuttle
|
||||
dampingModifier: 0.25
|
||||
- type: SecurityShuttle
|
||||
- type: GridPathfinding
|
||||
- type: Gravity
|
||||
gravityShakeSound: !type:SoundPathSpecifier
|
||||
|
|
|
|||
|
|
@ -148,10 +148,9 @@
|
|||
maxCharges: 3
|
||||
# Sunrise-Start
|
||||
- type: AutoRecharge
|
||||
rechargeDuration: 600
|
||||
rechargeDuration: 300
|
||||
# Sunrise-End
|
||||
- type: Action
|
||||
useDelay: 5 # Sunrise-Edit
|
||||
checkCanInteract: false
|
||||
itemIconStyle: BigAction
|
||||
priority: -20
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@
|
|||
id: LockerFillParamedic
|
||||
table: !type:AllSelector
|
||||
children:
|
||||
- id: HandheldCrewMonitor # Sunrise-Edit
|
||||
- id: HandheldEmergencyCrewMonitor # Sunrise-Edit
|
||||
- id: ClothingOuterHardsuitVoidParamed
|
||||
- id: ClothingOuterCoatParamedicWB
|
||||
- id: ClothingHeadHatParamedicsoft
|
||||
|
|
|
|||
|
|
@ -19,3 +19,4 @@
|
|||
ClothingEyesGlassesSunglasses: 2
|
||||
contrabandInventory:
|
||||
ToyFigurineBartender: 1
|
||||
BoxBeanbag: 1 # Sunrise Edit
|
||||
|
|
|
|||
|
|
@ -11,4 +11,10 @@
|
|||
contrabandInventory:
|
||||
FoodBakedDumplings: 2
|
||||
FoodSoupMiso: 2
|
||||
# Sunrise start
|
||||
FoodRiceBoiled: 2
|
||||
FoodRiceEgg: 2
|
||||
FoodRicePork: 2
|
||||
# Sunrise end
|
||||
# rice?
|
||||
# Yes for rice
|
||||
|
|
@ -25,4 +25,11 @@
|
|||
FoodBoxDonkpocket: 1
|
||||
FoodFrozenSandwich: 2
|
||||
FoodFrozenSandwichStrawberry: 2
|
||||
|
||||
# Sunrise Start
|
||||
FoodMeatFish: 3
|
||||
FoodMeatHuman: 3
|
||||
FoodMeatClown: 2
|
||||
FoodMeatAnomaly: 1
|
||||
emaggedInventory:
|
||||
FoodMeatDragon: 2
|
||||
# Sunrise End
|
||||
|
|
|
|||
|
|
@ -18,3 +18,11 @@
|
|||
ClothingHeadsetSecurity: 2
|
||||
contrabandInventory:
|
||||
ToyFigurineDetective: 1
|
||||
# Sunrise Start
|
||||
ClothingEyesBinoclardLenses: 1
|
||||
ClothingHandsGlovesAerostatic: 1
|
||||
ClothingNeckHorrific: 1
|
||||
ClothingOuterAerostaticBomberJacketArmored: 1
|
||||
ClothingShoesGreenLizardskin: 1
|
||||
ClothingUniformJumpsuitSuperstarCop: 1
|
||||
# Sunrise End
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
ClothingUniformJumpsuitLawyerGood: 1
|
||||
ClothingUniformJumpskirtLawyerGood: 1
|
||||
ClothingShoesBootsLaceup: 2
|
||||
ClothingHeadsetIAA: 2
|
||||
ClothingHeadsetLawyer: 2 # Sunrise Edit no more IAA headset abuse
|
||||
ClothingNeckLawyerbadge: 2
|
||||
BriefcaseBrown: 2
|
||||
ClothingHandsGlovesColorWhite: 1
|
||||
|
|
|
|||
|
|
@ -12,3 +12,6 @@
|
|||
WeaponProtoKineticAccelerator: 4
|
||||
contrabandInventory:
|
||||
PlushieCarp: 1
|
||||
PlushieMagicarp: 1 # Sunrise-Edit
|
||||
PlushieHolocarp: 1 # Sunrise-Edit
|
||||
# no rainbow carp((
|
||||
|
|
@ -19,9 +19,9 @@
|
|||
ToyFigurineFootsoldier: 1
|
||||
ToyFigurineNukieElite: 1
|
||||
ToyFigurineNukieCommander: 1
|
||||
ClothingUniformJumpskirtTacticalMaid: 5
|
||||
ClothingHandsTacticalMaidGloves: 5
|
||||
emaggedInventory:
|
||||
ClothingOuterCoatSyndieCapArmored: 1
|
||||
ClothingOuterWinterSyndieCapArmored: 1
|
||||
ClothingHeadHatTacticalMaidHeadband: 5
|
||||
ClothingUniformJumpskirtTacticalMaid: 5 # Sunrise-edit
|
||||
ClothingHandsTacticalMaidGloves: 5 # Sunrise-edit
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@
|
|||
- type: ArmorSparkEffect #starlight
|
||||
- type: StaminaResistance # Sunrise-Add
|
||||
damageCoefficient: 0.9 # Sunrise-Add
|
||||
- type: Item
|
||||
shape:
|
||||
- 0,0,2,2
|
||||
# Sunrise-End
|
||||
|
||||
#Standard armor vest, allowed for security and bartenders
|
||||
|
|
|
|||
|
|
@ -1165,11 +1165,6 @@
|
|||
- WantedListCartridge
|
||||
- MedTekCartridge
|
||||
- AstroNavCartridge
|
||||
# Sunrise-Start
|
||||
- NavigatorCartridge
|
||||
- MessengerCartridge
|
||||
- PhotoCartridge
|
||||
# Sunrise-End
|
||||
- type: Tag # Ignore Chameleon tags
|
||||
tags:
|
||||
- DoorBumpOpener
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue