Механика вещания через консоль связи (#3070)
This commit is contained in:
parent
dab2c2e8f5
commit
48bb8fabd7
8 changed files with 272 additions and 32 deletions
|
|
@ -27,6 +27,7 @@ namespace Content.Client.Communications.UI
|
|||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
_menu.OnToggleRelay += ToggleRelayPressed; // Sunrise-Edit
|
||||
}
|
||||
|
||||
public void AlertLevelSelected(string level)
|
||||
|
|
@ -58,6 +59,13 @@ namespace Content.Client.Communications.UI
|
|||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private void ToggleRelayPressed()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleToggleRelayMessage());
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
public void CallShuttle()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleCallEmergencyShuttleMessage());
|
||||
|
|
@ -91,6 +99,14 @@ namespace Content.Client.Communications.UI
|
|||
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
|
||||
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
|
||||
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
|
||||
|
||||
// Sunrise-Start
|
||||
_menu.CanRelay = commsState.CanRelay;
|
||||
_menu.IsRelaying = commsState.IsRelaying;
|
||||
_menu.RelayCooldownRemaining = commsState.RelayCooldownRemaining;
|
||||
_menu.RelayTimeRemaining = commsState.RelayTimeRemaining;
|
||||
_menu.UpdateRelayUi();
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,23 +9,25 @@
|
|||
VerticalExpand="True"
|
||||
Margin="6 6 6 5">
|
||||
|
||||
<TextEdit Name="MessageInput"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="100"/>
|
||||
|
||||
<!-- ButtonsPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom"
|
||||
SeparationOverride="4">
|
||||
Margin="0 2">
|
||||
|
||||
<!-- AnnouncePart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-announcement-header'}"
|
||||
Name="AnnouncementLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<Button Name="AnnounceButton"
|
||||
<TextEdit Name="MessageInput"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="100"/>
|
||||
|
||||
<Button Name="AnnounceButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-announcement-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}"
|
||||
|
|
@ -33,30 +35,66 @@
|
|||
Margin="0 0 1 0"
|
||||
Disabled="True"/>
|
||||
|
||||
<Button Name="BroadcastButton"
|
||||
<Button Name="BroadcastButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-broadcast-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}"
|
||||
StyleClasses="OpenBoth"/>
|
||||
</BoxContainer>
|
||||
|
||||
<OptionButton Name="AlertLevelButton"
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-alert-level-header'}"
|
||||
Name="AlertLevelLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<OptionButton Name="AlertLevelButton"
|
||||
Access="Public"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-relay-header'}"
|
||||
Name="RelayLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<Button Name="RelayButton"
|
||||
Access="Public"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
Text="{Loc 'comms-console-menu-relay-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-relay-button-tooltip'}"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<RichTextLabel Name="RelayStatusLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<!-- EmergencyPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="6">
|
||||
<controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label HorizontalExpand="True" Text="{Loc 'comms-console-menu-emergency-header'}"
|
||||
Name="EmergencyLabel" VAlign="Center"
|
||||
StyleClasses="LabelHeading" Align="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
Text="Placeholder Text"
|
||||
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ namespace Content.Client.Communications.UI
|
|||
public bool CanAnnounce;
|
||||
public bool CanBroadcast;
|
||||
public bool CanCall;
|
||||
// Sunrise-Start
|
||||
public bool CanRelay;
|
||||
public bool IsRelaying;
|
||||
public float RelayCooldownRemaining;
|
||||
public float RelayTimeRemaining;
|
||||
// Sunrise-End
|
||||
public bool AlertLevelSelectable;
|
||||
public bool CountdownStarted;
|
||||
public string CurrentLevel = string.Empty;
|
||||
|
|
@ -28,6 +34,7 @@ namespace Content.Client.Communications.UI
|
|||
public event Action<string>? OnAlertLevel;
|
||||
public event Action<string>? OnAnnounce;
|
||||
public event Action<string>? OnBroadcast;
|
||||
public event Action? OnToggleRelay; // Sunrise-Edit
|
||||
|
||||
public CommunicationsConsoleMenu()
|
||||
{
|
||||
|
|
@ -58,6 +65,11 @@ namespace Content.Client.Communications.UI
|
|||
BroadcastButton.OnPressed += _ => OnBroadcast?.Invoke(Rope.Collapse(MessageInput.TextRope));
|
||||
BroadcastButton.Disabled = !CanBroadcast;
|
||||
|
||||
// Sunrise-Start
|
||||
RelayButton.OnPressed += _ => OnToggleRelay?.Invoke();
|
||||
RelayButton.Disabled = !CanRelay;
|
||||
// Sunrise-End
|
||||
|
||||
AlertLevelButton.OnItemSelected += args =>
|
||||
{
|
||||
var metadata = AlertLevelButton.GetItemMetadata(args.Id);
|
||||
|
|
@ -78,6 +90,7 @@ namespace Content.Client.Communications.UI
|
|||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
UpdateRelayUi(); // Sunrise-Edit
|
||||
}
|
||||
|
||||
// The current alert could make levels unselectable, so we need to ensure that the UI reacts properly.
|
||||
|
|
@ -122,6 +135,7 @@ namespace Content.Client.Communications.UI
|
|||
if (!CountdownStarted)
|
||||
{
|
||||
CountdownLabel.SetMessage(string.Empty);
|
||||
CountdownLabel.Visible = false; // Sunrise-Edit
|
||||
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-call-shuttle");
|
||||
return;
|
||||
}
|
||||
|
|
@ -132,6 +146,36 @@ namespace Content.Client.Communications.UI
|
|||
var infoText = Loc.GetString($"comms-console-menu-time-remaining",
|
||||
("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture)));
|
||||
CountdownLabel.SetMessage(infoText);
|
||||
CountdownLabel.Visible = true; // Sunrise-Edit
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
public void UpdateRelayUi()
|
||||
{
|
||||
RelayButton.Disabled = !CanRelay && !IsRelaying;
|
||||
if (IsRelaying)
|
||||
{
|
||||
RelayButton.Text = Loc.GetString("comms-console-menu-relay-stop");
|
||||
var remaining = TimeSpan.FromSeconds(Math.Max(0f, RelayTimeRemaining));
|
||||
var text = Loc.GetString("comms-console-menu-relay-time-left", ("time", remaining.ToString(@"mm\:ss", CultureInfo.CurrentCulture)));
|
||||
RelayStatusLabel.Visible = true;
|
||||
RelayStatusLabel.SetMessage(text);
|
||||
}
|
||||
else if (RelayCooldownRemaining > 0f)
|
||||
{
|
||||
var remaining = TimeSpan.FromSeconds(RelayCooldownRemaining);
|
||||
RelayButton.Text = Loc.GetString("comms-console-menu-relay-button");
|
||||
RelayStatusLabel.SetMessage(Loc.GetString("comms-console-menu-relay-cooldown", ("time", remaining.ToString(@"mm\:ss", CultureInfo.CurrentCulture))));
|
||||
RelayStatusLabel.Visible = true;
|
||||
RelayButton.Disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
RelayButton.Text = Loc.GetString("comms-console-menu-relay-button");
|
||||
RelayStatusLabel.Visible = false;
|
||||
RelayStatusLabel.SetMessage(string.Empty);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
// Sunrise-end
|
||||
|
||||
// Sunrise-start - Use speaker network for station announcements
|
||||
if (playTts && (playDefault || announcementSound != null))
|
||||
if (playTts)
|
||||
{
|
||||
if (playDefault && announcementSound == null)
|
||||
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
||||
|
|
|
|||
|
|
@ -80,6 +80,24 @@ namespace Content.Server.Communications
|
|||
// Sunrise-Start
|
||||
[DataField("announceVoice", customTypeSerializer:typeof(PrototypeIdSerializer<TTSVoicePrototype>))]
|
||||
public string AnnounceVoice = "Hanson";
|
||||
// Sunrise-Start
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsRelaying;
|
||||
|
||||
[ViewVariables]
|
||||
public float RelayTimeRemaining;
|
||||
|
||||
[ViewVariables]
|
||||
public float RelayCooldownRemaining;
|
||||
|
||||
[DataField]
|
||||
public float RelayDuration = 60f;
|
||||
|
||||
[DataField]
|
||||
public float RelayCooldown = 300f;
|
||||
|
||||
[DataField]
|
||||
public float RelayRange = 7f;
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ using Content.Server.AlertLevel;
|
|||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Screens.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared._Sunrise.TTS; // Sunrise-edit
|
||||
using Content.Shared._Sunrise.TTS;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
|
|
@ -18,6 +19,9 @@ using Content.Shared.DeviceNetwork;
|
|||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
|
|
@ -55,6 +59,11 @@ namespace Content.Server.Communications
|
|||
|
||||
// On console init, set cooldown
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, MapInitEvent>(OnCommunicationsConsoleMapInit);
|
||||
|
||||
// Sunrise-Start
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleToggleRelayMessage>(OnToggleRelayMessage);
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, ListenEvent>(OnEntitySpokeNearbyRelay);
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
|
@ -68,6 +77,23 @@ namespace Content.Server.Communications
|
|||
comp.AnnouncementCooldownRemaining -= frameTime;
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
if (comp.RelayCooldownRemaining > 0f)
|
||||
comp.RelayCooldownRemaining -= frameTime;
|
||||
|
||||
if (comp.IsRelaying)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
StopRelay(uid, comp, announce: true);
|
||||
else
|
||||
{
|
||||
comp.RelayTimeRemaining -= frameTime;
|
||||
if (comp.RelayTimeRemaining <= 0f)
|
||||
StopRelay(uid, comp, announce: true);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
comp.UIUpdateAccumulator += frameTime;
|
||||
|
||||
if (comp.UIUpdateAccumulator < UIUpdateInterval)
|
||||
|
|
@ -159,13 +185,20 @@ namespace Content.Server.Communications
|
|||
}
|
||||
}
|
||||
|
||||
var canRelay = comp.RelayCooldownRemaining <= 0f && !comp.IsRelaying && this.IsPowered(uid, EntityManager); // Sunrise-Edit
|
||||
_uiSystem.SetUiState(uid, CommunicationsConsoleUiKey.Key, new CommunicationsConsoleInterfaceState(
|
||||
CanAnnounce(comp),
|
||||
CanCallOrRecall(comp),
|
||||
levels,
|
||||
currentLevel,
|
||||
currentDelay,
|
||||
_roundEndSystem.ExpectedCountdownEnd
|
||||
_roundEndSystem.ExpectedCountdownEnd,
|
||||
// Sunrise-Start
|
||||
canRelay,
|
||||
comp.IsRelaying,
|
||||
MathF.Max(0f, comp.RelayCooldownRemaining),
|
||||
MathF.Max(0f, comp.RelayTimeRemaining)
|
||||
// Sunrise-End
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -338,6 +371,67 @@ namespace Content.Server.Communications
|
|||
_roundEndSystem.CancelRoundEndCountdown(uid);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(message.Actor):player} has recalled the shuttle.");
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
private void OnToggleRelayMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleToggleRelayMessage message)
|
||||
{
|
||||
if (comp.IsRelaying)
|
||||
{
|
||||
StopRelay(uid, comp, announce: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (comp.RelayCooldownRemaining > 0f)
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
comp.IsRelaying = true;
|
||||
comp.RelayTimeRemaining = comp.RelayDuration;
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
EnsureComp<ActiveListenerComponent>(uid).Range = comp.RelayRange;
|
||||
|
||||
var startText = Loc.GetString("comms-console-relay-started");
|
||||
var title = Loc.GetString(comp.Title);
|
||||
_chatSystem.DispatchStationAnnouncement(uid, startText, sender: title, playDefault: true, playTts: true, colorOverride: comp.Color, announceVoice: comp.AnnounceVoice, announcementSound: comp.Sound);
|
||||
}
|
||||
|
||||
private void OnEntitySpokeNearbyRelay(EntityUid uid, CommunicationsConsoleComponent comp, ListenEvent ev)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
var voice = comp.AnnounceVoice;
|
||||
if (TryComp<TTSComponent>(ev.Source, out var ttsComponent))
|
||||
{
|
||||
voice = ttsComponent.VoicePrototypeId;
|
||||
}
|
||||
_chatSystem.DispatchStationAnnouncement(uid, ev.Message, sender: Loc.GetString(comp.Title), playDefault: false, playTts: true, colorOverride: comp.Color, announceVoice: voice);
|
||||
}
|
||||
|
||||
private void StopRelay(EntityUid uid, CommunicationsConsoleComponent comp, bool announce)
|
||||
{
|
||||
if (!comp.IsRelaying && comp.RelayCooldownRemaining > 0f)
|
||||
{
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
return;
|
||||
}
|
||||
|
||||
comp.IsRelaying = false;
|
||||
comp.RelayTimeRemaining = 0f;
|
||||
comp.RelayCooldownRemaining = comp.RelayCooldown;
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
RemCompDeferred<ActiveListenerComponent>(uid);
|
||||
|
||||
if (announce)
|
||||
{
|
||||
var stopText = Loc.GetString("comms-console-relay-stopped");
|
||||
var title = Loc.GetString(comp.Title);
|
||||
_chatSystem.DispatchStationAnnouncement(uid, stopText, sender: title, playDefault: true, playTts: true, colorOverride: comp.Color, announceVoice: comp.AnnounceVoice, announcementSound: comp.Sound);
|
||||
}
|
||||
}
|
||||
// Sunrise-End
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -18,8 +18,14 @@ namespace Content.Shared.Communications
|
|||
public List<string>? AlertLevels;
|
||||
public string CurrentAlert;
|
||||
public float CurrentAlertDelay;
|
||||
// Sunrise-Start
|
||||
public readonly bool CanRelay;
|
||||
public readonly bool IsRelaying;
|
||||
public readonly float RelayCooldownRemaining;
|
||||
public readonly float RelayTimeRemaining;
|
||||
// Sunrise-End
|
||||
|
||||
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
|
||||
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null, bool canRelay = false, bool isRelaying = false, float relayCooldownRemaining = 0f, float relayTimeRemaining = 0f) // Sunrise-Edit
|
||||
{
|
||||
CanAnnounce = canAnnounce;
|
||||
CanCall = canCall;
|
||||
|
|
@ -28,6 +34,12 @@ namespace Content.Shared.Communications
|
|||
AlertLevels = alertLevels;
|
||||
CurrentAlert = currentAlert;
|
||||
CurrentAlertDelay = currentAlertDelay;
|
||||
// Sunrise-Start
|
||||
CanRelay = canRelay;
|
||||
IsRelaying = isRelaying;
|
||||
RelayCooldownRemaining = relayCooldownRemaining;
|
||||
RelayTimeRemaining = relayTimeRemaining;
|
||||
// Sunrise-End
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +75,13 @@ namespace Content.Shared.Communications
|
|||
}
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CommunicationsConsoleToggleRelayMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class CommunicationsConsoleCallEmergencyShuttleMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1 +1,12 @@
|
|||
comms-console-announcement-title-prison = Космическая тюрьма
|
||||
comms-console-announcement-title-prison = Космическая тюрьма
|
||||
comms-console-menu-relay-button = Включить
|
||||
comms-console-menu-relay-button-tooltip = Включить или выключить вещание голоса на динамики.
|
||||
comms-console-menu-relay-stop = Выключить
|
||||
comms-console-menu-relay-cooldown = Перезарядка: { $time }.
|
||||
comms-console-menu-announcement-header = Оповещения
|
||||
comms-console-menu-relay-header = Вещание
|
||||
comms-console-menu-relay-time-left = Осталось: { $time }
|
||||
comms-console-menu-emergency-header = Эвакуация
|
||||
comms-console-menu-alert-level-header = Код угрозы
|
||||
comms-console-relay-started = Начато вещание через консоль связи.
|
||||
comms-console-relay-stopped = Вещание через консоль связи остановлено.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue