mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
merge remote wizden/master
This commit is contained in:
commit
3a9db586ff
324 changed files with 5855 additions and 3850 deletions
|
|
@ -1,22 +1,16 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.AlertLevel;
|
||||
|
||||
public sealed partial class AlertLevelDisplaySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private SpriteSystem _sprite = default!;
|
||||
[Dependency] private SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AlertLevelDisplayComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
}
|
||||
|
||||
[SubscribeLocalEvent]
|
||||
private void OnAppearanceChange(EntityUid uid, AlertLevelDisplayComponent alertLevelDisplay, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
|
|
@ -25,24 +19,19 @@ public sealed partial class AlertLevelDisplaySystem : EntitySystem
|
|||
}
|
||||
var layer = _sprite.LayerMapReserve((uid, args.Sprite), AlertLevelDisplay.Layer);
|
||||
|
||||
if (args.AppearanceData.TryGetValue(AlertLevelDisplay.Powered, out var poweredObject))
|
||||
if (_appearance.TryGetData<bool>(uid, AlertLevelDisplay.Powered, out var powered, component: args.Component))
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), layer, poweredObject is true);
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), layer, powered);
|
||||
}
|
||||
|
||||
if (!args.AppearanceData.TryGetValue(AlertLevelDisplay.CurrentLevel, out var level))
|
||||
if (!_appearance.TryGetData<ProtoId<AlertLevelPrototype>>(uid, AlertLevelDisplay.CurrentLevel, out var level, component: args.Component))
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, alertLevelDisplay.AlertVisuals.Values.First());
|
||||
return;
|
||||
}
|
||||
|
||||
if (alertLevelDisplay.AlertVisuals.TryGetValue((string)level, out var visual))
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, visual);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), layer, alertLevelDisplay.AlertVisuals.Values.First());
|
||||
}
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite),
|
||||
layer,
|
||||
alertLevelDisplay.AlertVisuals.GetValueOrDefault(level) ?? alertLevelDisplay.AlertVisuals.Values.First());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public sealed partial class MidiFileCollectionManager : IPostInjectInit
|
|||
/// <summary>
|
||||
/// Directory path to use inside UserData for storing MIDIs.
|
||||
/// </summary>
|
||||
private static readonly ResPath UserMidiDirectory = new("/UserMidis/");
|
||||
public static readonly ResPath UserMidiDirectory = new("/UserMidis/");
|
||||
|
||||
private const string SawmillCategory = "midifilecollection";
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem<So
|
|||
changeColor = reagentProto.MetamorphicChangeColor;
|
||||
fillSprite = reagentProto.MetamorphicSprite ?? fillSprite;
|
||||
}
|
||||
else if (reagentProto?.MetamorphicSprite == null)
|
||||
// The reagent has no metamorphic sprite, but the default one
|
||||
// will still be used. So we set the fill to visible.
|
||||
SpriteSystem.LayerSetVisible(ent, fillLayer, true);
|
||||
else
|
||||
SpriteSystem.LayerSetVisible(ent, fillLayer, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +1,109 @@
|
|||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.Communications;
|
||||
using Content.Shared.Station;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Communications.UI
|
||||
namespace Content.Client.Communications.UI;
|
||||
|
||||
public sealed partial class CommunicationsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
public sealed partial class CommunicationsConsoleBoundUserInterface : BoundUserInterface
|
||||
[Dependency] private IConfigurationManager _cfg = default!;
|
||||
[Dependency] private SharedStationSystem _station = default!;
|
||||
[Dependency] private AlertLevelSystem _alertLevel = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private CommunicationsConsoleMenu? _menu;
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
[Dependency] private IConfigurationManager _cfg = default!;
|
||||
base.Open();
|
||||
|
||||
[ViewVariables]
|
||||
private CommunicationsConsoleMenu? _menu;
|
||||
_menu = this.CreateWindow<CommunicationsConsoleMenu>();
|
||||
_menu.OnAnnounce += AnnounceButtonPressed;
|
||||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
}
|
||||
|
||||
public CommunicationsConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
public void AlertLevelSelected(ProtoId<AlertLevelPrototype> level)
|
||||
{
|
||||
if (_menu!.AlertLevelSelectable)
|
||||
{
|
||||
// TODO: This does not work until the console UI is predicted and uses component states.
|
||||
// Also someone decided to send BUI states regularly in an update loop, so this just gets randomly bulldozed until the message reaches the server.
|
||||
// _menu.CurrentAlertLevel = level;
|
||||
// _menu.AlertLevelSelectable = false;
|
||||
// _menu.AlertLevelButton.Disabled = true;
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
public void EmergencyShuttleButtonPressed()
|
||||
{
|
||||
if (_menu!.CountdownStarted)
|
||||
RecallShuttle();
|
||||
else
|
||||
CallShuttle();
|
||||
}
|
||||
|
||||
public void AnnounceButtonPressed(string message)
|
||||
{
|
||||
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
var msg = SharedChatSystem.SanitizeAnnouncement(message, maxLength);
|
||||
SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
|
||||
}
|
||||
|
||||
public void BroadcastButtonPressed(string message)
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
|
||||
public void CallShuttle()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleCallEmergencyShuttleMessage());
|
||||
}
|
||||
|
||||
public void RecallShuttle()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleRecallEmergencyShuttleMessage());
|
||||
}
|
||||
|
||||
// TODO: Use component states and update in an AfterAutoHandleState subscription
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not CommunicationsConsoleInterfaceState commsState)
|
||||
return;
|
||||
|
||||
var stationUid = _station.GetOwningStation(Owner);
|
||||
|
||||
if (!EntMan.TryGetComponent<AlertLevelComponent>(stationUid, out var alertComp))
|
||||
return;
|
||||
|
||||
if (_menu != null)
|
||||
{
|
||||
base.Open();
|
||||
_menu.CanAnnounce = commsState.CanAnnounce;
|
||||
_menu.CanBroadcast = commsState.CanBroadcast;
|
||||
_menu.CanCall = commsState.CanCall;
|
||||
_menu.CountdownStarted = commsState.CountdownStarted;
|
||||
_menu.CountdownEnd = commsState.ExpectedCountdownEnd;
|
||||
|
||||
_menu = this.CreateWindow<CommunicationsConsoleMenu>();
|
||||
_menu.OnAnnounce += AnnounceButtonPressed;
|
||||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
}
|
||||
_menu.CurrentAlertLevel = alertComp.CurrentAlertLevel;
|
||||
_menu.SelectableAlertLevels = _alertLevel.GetSelectableAlertLevels((stationUid.Value, alertComp));
|
||||
_menu.AlertLevelSelectable = _alertLevel.CanChangeAlertLevel((stationUid.Value, alertComp));
|
||||
|
||||
public void AlertLevelSelected(string level)
|
||||
{
|
||||
if (_menu!.AlertLevelSelectable)
|
||||
{
|
||||
_menu.CurrentLevel = level;
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
}
|
||||
_menu.UpdateCountdown();
|
||||
_menu.UpdateAlertLevels();
|
||||
|
||||
public void EmergencyShuttleButtonPressed()
|
||||
{
|
||||
if (_menu!.CountdownStarted)
|
||||
RecallShuttle();
|
||||
else
|
||||
CallShuttle();
|
||||
}
|
||||
|
||||
public void AnnounceButtonPressed(string message)
|
||||
{
|
||||
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
var msg = SharedChatSystem.SanitizeAnnouncement(message, maxLength);
|
||||
SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
|
||||
}
|
||||
|
||||
public void BroadcastButtonPressed(string message)
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
|
||||
public void CallShuttle()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleCallEmergencyShuttleMessage());
|
||||
}
|
||||
|
||||
public void RecallShuttle()
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleRecallEmergencyShuttleMessage());
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not CommunicationsConsoleInterfaceState commsState)
|
||||
return;
|
||||
|
||||
if (_menu != null)
|
||||
{
|
||||
_menu.CanAnnounce = commsState.CanAnnounce;
|
||||
_menu.CanBroadcast = commsState.CanBroadcast;
|
||||
_menu.CanCall = commsState.CanCall;
|
||||
_menu.CountdownStarted = commsState.CountdownStarted;
|
||||
_menu.AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0;
|
||||
_menu.CurrentLevel = commsState.CurrentAlert;
|
||||
_menu.CountdownEnd = commsState.ExpectedCountdownEnd;
|
||||
|
||||
_menu.UpdateCountdown();
|
||||
_menu.UpdateAlertLevels(commsState.AlertLevels, _menu.CurrentLevel);
|
||||
_menu.AlertLevelButton.Disabled = !_menu.AlertLevelSelectable;
|
||||
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
|
||||
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
|
||||
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
|
||||
}
|
||||
_menu.AlertLevelButton.Disabled = !_menu.AlertLevelSelectable;
|
||||
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
|
||||
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
|
||||
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using System.Globalization;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Communications.UI
|
||||
|
|
@ -19,13 +21,15 @@ namespace Content.Client.Communications.UI
|
|||
public bool CanAnnounce;
|
||||
public bool CanBroadcast;
|
||||
public bool CanCall;
|
||||
public bool AlertLevelSelectable;
|
||||
public bool CountdownStarted;
|
||||
public string CurrentLevel = string.Empty;
|
||||
public TimeSpan? CountdownEnd;
|
||||
|
||||
public ProtoId<AlertLevelPrototype>? CurrentAlertLevel;
|
||||
public List<ProtoId<AlertLevelPrototype>> SelectableAlertLevels = new();
|
||||
public bool AlertLevelSelectable;
|
||||
|
||||
public event Action? OnEmergencyLevel;
|
||||
public event Action<string>? OnAlertLevel;
|
||||
public event Action<ProtoId<AlertLevelPrototype>>? OnAlertLevel;
|
||||
public event Action<string>? OnAnnounce;
|
||||
public event Action<string>? OnBroadcast;
|
||||
|
||||
|
|
@ -61,7 +65,7 @@ namespace Content.Client.Communications.UI
|
|||
AlertLevelButton.OnItemSelected += args =>
|
||||
{
|
||||
var metadata = AlertLevelButton.GetItemMetadata(args.Id);
|
||||
if (metadata != null && metadata is string cast)
|
||||
if (metadata != null && metadata is ProtoId<AlertLevelPrototype> cast)
|
||||
{
|
||||
OnAlertLevel?.Invoke(cast);
|
||||
}
|
||||
|
|
@ -78,41 +82,32 @@ namespace Content.Client.Communications.UI
|
|||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
// TODO: Countdown for alert level delay
|
||||
}
|
||||
|
||||
// The current alert could make levels unselectable, so we need to ensure that the UI reacts properly.
|
||||
// If the current alert is unselectable, the only item in the alerts list will be
|
||||
// the current alert. Otherwise, it will be the list of alerts, with the current alert
|
||||
// selected.
|
||||
public void UpdateAlertLevels(List<string>? alerts, string currentAlert)
|
||||
public void UpdateAlertLevels()
|
||||
{
|
||||
AlertLevelButton.Clear();
|
||||
|
||||
if (alerts == null)
|
||||
if (CurrentAlertLevel == null)
|
||||
return;
|
||||
|
||||
if (!AlertLevelSelectable)
|
||||
{
|
||||
var name = currentAlert;
|
||||
if (_loc.TryGetString($"alert-level-{currentAlert}", out var locName))
|
||||
{
|
||||
name = locName;
|
||||
}
|
||||
// selection is locked, only show the current alert
|
||||
var name = _loc.GetString($"alert-level-{CurrentAlertLevel}");
|
||||
AlertLevelButton.AddItem(name);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, currentAlert);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, CurrentAlertLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var alert in alerts)
|
||||
foreach (var alert in SelectableAlertLevels)
|
||||
{
|
||||
var name = alert;
|
||||
if (_loc.TryGetString($"alert-level-{alert}", out var locName))
|
||||
{
|
||||
name = locName;
|
||||
}
|
||||
var name = _loc.GetString($"alert-level-{alert}");
|
||||
AlertLevelButton.AddItem(name);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, alert);
|
||||
if (alert == currentAlert)
|
||||
{
|
||||
if (alert == CurrentAlertLevel)
|
||||
AlertLevelButton.Select(AlertLevelButton.ItemCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ namespace Content.Client.Entry
|
|||
|
||||
_prototypeManager.RegisterIgnore("utilityQuery");
|
||||
_prototypeManager.RegisterIgnore("utilityCurvePreset");
|
||||
_prototypeManager.RegisterIgnore("accent");
|
||||
_prototypeManager.RegisterIgnore("gasReaction");
|
||||
_prototypeManager.RegisterIgnore("seed"); // Seeds prototypes are server-only.
|
||||
_prototypeManager.RegisterIgnore("objective");
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<DefaultWindow Title="{Loc 'instruments-component-band-menu'}" MinSize="250 350" xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
<BoxContainer Orientation="Vertical" Align="Center" HorizontalExpand="true" VerticalExpand="true">
|
||||
<ItemList Name="BandList" SelectMode="Button" Margin="3 3 3 3"
|
||||
HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="8"/>
|
||||
<Button Name="RefreshButton" Text="{Loc 'instrument-component-band-refresh'}" TextAlign="Center"
|
||||
HorizontalExpand="true" Margin="5 5 5 5" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BandMenu : DefaultWindow
|
||||
{
|
||||
private readonly InstrumentBoundUserInterface _owner;
|
||||
|
||||
public EntityUid? Master;
|
||||
|
||||
public BandMenu(InstrumentBoundUserInterface owner)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_owner = owner;
|
||||
BandList.OnItemSelected += OnItemSelected;
|
||||
RefreshButton.OnPressed += OnRefreshPressed;
|
||||
}
|
||||
|
||||
private void OnRefreshPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
_owner.RefreshBands();
|
||||
}
|
||||
|
||||
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
_owner.Instruments.SetMaster(_owner.Owner, (EntityUid)args.ItemList[args.ItemIndex].Metadata!);
|
||||
BandList.Clear();
|
||||
Timer.Spawn(0, Close);
|
||||
}
|
||||
|
||||
public void Populate((NetEntity, string)[] nearby, IEntityManager entManager)
|
||||
{
|
||||
BandList.Clear();
|
||||
|
||||
foreach (var (nent, name) in nearby)
|
||||
{
|
||||
var uid = entManager.GetEntity(nent);
|
||||
var item = BandList.AddItem(name, null, true, uid);
|
||||
item.Selected = Master == uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Content.Client/Instruments/UI/BandMidiSource.xaml
Normal file
7
Content.Client/Instruments/UI/BandMidiSource.xaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<ui:BandMidiSource xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Instruments.UI"
|
||||
Orientation="Vertical">
|
||||
<Label Text="{Loc 'instruments-component-band-title-label'}" />
|
||||
<ItemList Name="BandList" SelectMode="Button" HorizontalExpand="True" VerticalExpand="True" />
|
||||
<Button Name="RefreshButton" Text="{Loc 'instruments-component-band-refresh'}" HorizontalExpand="True" />
|
||||
</ui:BandMidiSource>
|
||||
59
Content.Client/Instruments/UI/BandMidiSource.xaml.cs
Normal file
59
Content.Client/Instruments/UI/BandMidiSource.xaml.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BandMidiSource : InstrumentMidiSourceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when the list of currently available bands should be refreshed.
|
||||
/// </summary>
|
||||
public event Action? RefreshBandRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a band should be joined.
|
||||
/// </summary>
|
||||
public event Action<EntityUid>? JoinBandRequest;
|
||||
|
||||
public override string ButtonName => Loc.GetString("instruments-component-menu-band-midi-source-button");
|
||||
|
||||
public BandMidiSource()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
RefreshButton.OnPressed += OnRefreshPressed;
|
||||
BandList.OnItemSelected += OnBandItemSelected;
|
||||
}
|
||||
|
||||
protected override void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
if (newVisible)
|
||||
RefreshBandRequest?.Invoke();
|
||||
}
|
||||
|
||||
private void OnRefreshPressed(BaseButton.ButtonEventArgs _)
|
||||
{
|
||||
RefreshBandRequest?.Invoke();
|
||||
}
|
||||
|
||||
private void OnBandItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
var instrumentEntity = (EntityUid)args.ItemList[args.ItemIndex].Metadata!;
|
||||
JoinBandRequest?.Invoke(instrumentEntity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the current list of available bands with the passed one.
|
||||
/// </summary>
|
||||
/// <param name="entities">List of available entities for band joining.</param>
|
||||
public void Populate(IEnumerable<(EntityUid, string)> entities)
|
||||
{
|
||||
BandList.Clear();
|
||||
foreach (var tuple in entities)
|
||||
{
|
||||
BandList.AddItem(tuple.Item2, null, true, tuple.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Content.Client/Instruments/UI/ChannelItem.xaml
Normal file
19
Content.Client/Instruments/UI/ChannelItem.xaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" HorizontalExpand="True">
|
||||
<PanelContainer Name="ItemPanel">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#00000044" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="ChannelNumberLabel" Align="Right" HorizontalAlignment="Left" MinWidth="30" />
|
||||
<controls:VSpacer Spacing="10" />
|
||||
<Label Name="ChannelNameLabel" HorizontalAlignment="Center" />
|
||||
<controls:VSpacer HorizontalExpand="True" />
|
||||
<controls:SwitchButton Name="ChannelEnableSwitchButton"
|
||||
HorizontalAlignment="Right"
|
||||
OnStateText=""
|
||||
OffStateText="" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
71
Content.Client/Instruments/UI/ChannelItem.xaml.cs
Normal file
71
Content.Client/Instruments/UI/ChannelItem.xaml.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelItem : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when a channel is toggled by the user.
|
||||
/// </summary>
|
||||
public event Action<int, bool>? SwitchFilteredChannel;
|
||||
|
||||
/// <summary>
|
||||
/// MIDI channel ID this item is assigned to.
|
||||
/// </summary>
|
||||
public int ChannelId
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ChannelNumberLabel.Text = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI state (on/off) of this channel item.
|
||||
/// </summary>
|
||||
public bool ChannelState
|
||||
{
|
||||
get => ChannelEnableSwitchButton.Pressed;
|
||||
set => ChannelEnableSwitchButton.Pressed = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name of this channel item in the UI.
|
||||
/// </summary>
|
||||
public string? ChannelName
|
||||
{
|
||||
get => ChannelNameLabel.Text;
|
||||
set => ChannelNameLabel.Text = value;
|
||||
}
|
||||
|
||||
public ChannelItem()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
ChannelEnableSwitchButton.OnPressed += OnChannelEnableSwitchButtonPressed;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
ChannelState = !ChannelState;
|
||||
SwitchFilteredChannel?.Invoke(ChannelId, ChannelState);
|
||||
}
|
||||
|
||||
private void OnChannelEnableSwitchButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
SwitchFilteredChannel?.Invoke(ChannelId, ChannelState);
|
||||
}
|
||||
}
|
||||
27
Content.Client/Instruments/UI/ChannelsControl.xaml
Normal file
27
Content.Client/Instruments/UI/ChannelsControl.xaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ScrollContainer MinHeight="90">
|
||||
<BoxContainer Orientation="Vertical" Name="ChannelsContainer" SeparationOverride="3" />
|
||||
</ScrollContainer>
|
||||
<controls:HSpacer Spacing="10" />
|
||||
<GridContainer Columns="5" HSeparationOverride="1" VSeparationOverride="1">
|
||||
<!-- Name display selector -->
|
||||
<Label Text="{Loc 'instruments-component-channels-name-display-selector-label'}" />
|
||||
<controls:VSpacer Spacing="10" />
|
||||
<Button Name="TrackSelectorButton"
|
||||
Text="{Loc 'instruments-component-channels-name-display-selector-track-button'}"
|
||||
HorizontalExpand="true"
|
||||
StyleClasses="OpenBoth" />
|
||||
<Button Name="ProgramSelectorButton"
|
||||
Text="{Loc 'instruments-component-channels-name-display-selector-program-button'}"
|
||||
Pressed="True"
|
||||
HorizontalExpand="true"
|
||||
StyleClasses="OpenBoth" />
|
||||
<Button Name="InstrumentSelectorButton"
|
||||
Text="{Loc 'instruments-component-channels-name-display-selector-instrument-button'}"
|
||||
HorizontalExpand="true"
|
||||
StyleClasses="OpenBoth" />
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
116
Content.Client/Instruments/UI/ChannelsControl.xaml.cs
Normal file
116
Content.Client/Instruments/UI/ChannelsControl.xaml.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelsControl : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when a channel is toggled.
|
||||
/// </summary>
|
||||
public event Action<int, bool>? SwitchFilteredChannel;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a selector button is pressed in order to display different channel names.
|
||||
/// </summary>
|
||||
public event Action? ChannelsUpdateRequest;
|
||||
|
||||
public ChannelsControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
TrackSelectorButton.OnPressed += OnSelectorButtonPressed;
|
||||
InstrumentSelectorButton.OnPressed += OnSelectorButtonPressed;
|
||||
ProgramSelectorButton.OnPressed += OnSelectorButtonPressed;
|
||||
|
||||
var group = new ButtonGroup();
|
||||
TrackSelectorButton.Group = group;
|
||||
InstrumentSelectorButton.Group = group;
|
||||
ProgramSelectorButton.Group = group;
|
||||
|
||||
InitChannels();
|
||||
}
|
||||
|
||||
protected override void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
if (newVisible)
|
||||
ChannelsUpdateRequest?.Invoke();
|
||||
}
|
||||
|
||||
private void OnSelectorButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
ChannelsUpdateRequest?.Invoke();
|
||||
}
|
||||
|
||||
private void OnChannelSwitched(int id, bool state)
|
||||
{
|
||||
SwitchFilteredChannel?.Invoke(id, state);
|
||||
}
|
||||
|
||||
private void InitChannels()
|
||||
{
|
||||
for (var i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
var item = new ChannelItem
|
||||
{
|
||||
ChannelId = i,
|
||||
};
|
||||
ChannelsContainer.AddChild(item);
|
||||
item.SwitchFilteredChannel += OnChannelSwitched;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the current list of channels with the passed one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Calling this method overrides the toggle state of all channel items. Ensure to pass correct values.
|
||||
/// </remarks>
|
||||
/// <param name="channels">List of channels consisting of their index, display name and state.</param>
|
||||
public void SetChannels(MidiChannelInfo[] channels)
|
||||
{
|
||||
TrackSelectorButton.Disabled = true;
|
||||
InstrumentSelectorButton.Disabled = true;
|
||||
ProgramSelectorButton.Disabled = true;
|
||||
|
||||
for (var i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
if (i >= ChannelsContainer.ChildCount || i < 0)
|
||||
return;
|
||||
|
||||
if (ChannelsContainer.Children[i] is not ChannelItem cItem)
|
||||
return;
|
||||
|
||||
cItem.ChannelName = "";
|
||||
cItem.ChannelState = false;
|
||||
cItem.Visible = false;
|
||||
|
||||
if (i >= channels.Length)
|
||||
continue;
|
||||
|
||||
var channel = channels[i];
|
||||
|
||||
if (channel.TrackName.Length > 0)
|
||||
TrackSelectorButton.Disabled = false;
|
||||
if (channel.InstrumentName.Length > 0)
|
||||
InstrumentSelectorButton.Disabled = false;
|
||||
if (channel.ProgramName.Length > 0)
|
||||
ProgramSelectorButton.Disabled = false;
|
||||
|
||||
if (TrackSelectorButton.Pressed)
|
||||
cItem.ChannelName = channel.TrackName;
|
||||
if (InstrumentSelectorButton.Pressed)
|
||||
cItem.ChannelName = channel.InstrumentName;
|
||||
if (ProgramSelectorButton.Pressed)
|
||||
cItem.ChannelName = channel.ProgramName;
|
||||
|
||||
cItem.ChannelState = channel.FilterState;
|
||||
cItem.Visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<DefaultWindow Title="{Loc 'instruments-component-channels-menu'}" MinSize="250 350" xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="true" VerticalExpand="true" Align="Center">
|
||||
<ItemList Name="ChannelList" SelectMode="Multiple" Margin="3 3 3 3" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="8"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="true" VerticalExpand="true" Align="Center"
|
||||
SizeFlagsStretchRatio="1">
|
||||
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton Name="DisplayTrackNames"
|
||||
Text="{Loc 'instruments-component-channels-track-names-toggle'}"
|
||||
Margin="0 5 0 0" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
using Content.Shared.Instruments;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChannelsMenu : DefaultWindow
|
||||
{
|
||||
[Dependency] private IEntityManager _entityManager = null!;
|
||||
|
||||
private readonly InstrumentBoundUserInterface _owner;
|
||||
|
||||
public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_owner = owner;
|
||||
|
||||
ChannelList.OnItemSelected += OnItemSelected;
|
||||
ChannelList.OnItemDeselected += OnItemDeselected;
|
||||
AllButton.OnPressed += OnAllPressed;
|
||||
ClearButton.OnPressed += OnClearPressed;
|
||||
DisplayTrackNames.OnPressed += OnDisplayTrackNamesPressed;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated += UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
Populate();
|
||||
}
|
||||
|
||||
private void UpdateChannelList()
|
||||
{
|
||||
Populate(); // This is kind of in-efficent because we don't filter for which instrument updated its channels, but idc
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_owner.Instruments.OnChannelsUpdated -= UpdateChannelList;
|
||||
}
|
||||
|
||||
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
_owner.Instruments.SetFilteredChannel(_owner.Owner, (int)ChannelList[args.ItemIndex].Metadata!, false);
|
||||
}
|
||||
|
||||
private void OnItemDeselected(ItemList.ItemListDeselectedEventArgs args)
|
||||
{
|
||||
_owner.Instruments.SetFilteredChannel(_owner.Owner, (int)ChannelList[args.ItemIndex].Metadata!, true);
|
||||
}
|
||||
|
||||
private void OnAllPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
foreach (var item in ChannelList)
|
||||
{
|
||||
// TODO: Make this efficient jfc
|
||||
item.Selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
foreach (var item in ChannelList)
|
||||
{
|
||||
// TODO: Make this efficient jfc
|
||||
item.Selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks up the tree of instrument masters to find the truest master of them all.
|
||||
/// </summary>
|
||||
private ActiveInstrumentComponent ResolveActiveInstrument(InstrumentComponent? comp)
|
||||
{
|
||||
comp ??= _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
|
||||
var instrument = new Entity<InstrumentComponent>(_owner.Owner, comp);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (instrument.Comp.Master == null)
|
||||
break;
|
||||
|
||||
instrument = new Entity<InstrumentComponent>((EntityUid)instrument.Comp.Master,
|
||||
_entityManager.GetComponent<InstrumentComponent>((EntityUid)instrument.Comp.Master));
|
||||
}
|
||||
|
||||
return _entityManager.GetComponent<ActiveInstrumentComponent>(instrument.Owner);
|
||||
}
|
||||
|
||||
public void Populate()
|
||||
{
|
||||
ChannelList.Clear();
|
||||
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
|
||||
var activeInstrument = ResolveActiveInstrument(instrument);
|
||||
|
||||
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
var label = _owner.Loc.GetString("instrument-component-channel-name",
|
||||
("number", i));
|
||||
if (activeInstrument != null
|
||||
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
|
||||
&& resolvedMidiChannel != null)
|
||||
{
|
||||
if (DisplayTrackNames.Pressed)
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ TrackName: not null, InstrumentName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-multi",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName),
|
||||
("other", resolvedMidiChannel.InstrumentName)),
|
||||
{ TrackName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.TrackName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
label = resolvedMidiChannel switch
|
||||
{
|
||||
{ ProgramName: not null } =>
|
||||
Loc.GetString("instruments-component-channels-single",
|
||||
("channel", i),
|
||||
("name", resolvedMidiChannel.ProgramName)),
|
||||
_ => label,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var item = ChannelList.AddItem(label, null, true, i);
|
||||
|
||||
item.Selected = !instrument?.FilteredChannels[i] ?? false;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Client/Instruments/UI/FileMidiSource.xaml
Normal file
38
Content.Client/Instruments/UI/FileMidiSource.xaml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<ui:FileMidiSource xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Instruments.UI"
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical">
|
||||
|
||||
<!-- Track list -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 20">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<LineEdit Name="FilterBar" PlaceHolder="{Loc 'instruments-component-menu-files-filter'}"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<ItemList Name="TrackList" SelectMode="Single" VerticalExpand="True" />
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Button Name="AddButton" Text="{Loc 'instruments-component-menu-files-add-button'}" HorizontalExpand="True"
|
||||
StyleClasses="OpenRight" />
|
||||
<Button Name="RemoveButton" Text="{Loc 'instruments-component-menu-files-remove-button'}" Disabled="True"
|
||||
HorizontalExpand="True" StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Current track label -->
|
||||
<Label Name="CurrentTrackLabel" MinHeight="24" />
|
||||
|
||||
<!-- Track position -->
|
||||
<Slider Name="PlaybackSlider" HorizontalExpand="True" />
|
||||
|
||||
<!-- Track controls -->
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<Button Name="PlayButton" Text="{Loc 'instruments-component-menu-play-stop-button'}" ToggleMode="True"
|
||||
Margin="0 0 20 0" />
|
||||
<Button Name="LoopButton" Text="{Loc 'instruments-component-menu-loop-button'}" ToggleMode="True"
|
||||
StyleClasses="OpenRight" />
|
||||
<Button Name="ShuffleButton" Text="{Loc 'instruments-component-menu-shuffle-button'}" ToggleMode="True"
|
||||
StyleClasses="OpenLeft" />
|
||||
<BoxContainer HorizontalExpand="True" />
|
||||
<Label Name="TimeLabel" Text="--:--/--:--" Align="Right" />
|
||||
</BoxContainer>
|
||||
</ui:FileMidiSource>
|
||||
463
Content.Client/Instruments/UI/FileMidiSource.xaml.cs
Normal file
463
Content.Client/Instruments/UI/FileMidiSource.xaml.cs
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Client.Audio.Midi;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using Range = Robust.Client.UserInterface.Controls.Range;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FileMidiSource : InstrumentMidiSourceBase
|
||||
{
|
||||
private const double RecoveryGracePeriod = 2.0;
|
||||
|
||||
[Dependency] private IRobustRandom _random = default!;
|
||||
[Dependency] private MidiFileCollectionManager _midiCollection = default!;
|
||||
[Dependency] private IFileDialogManager _dialogs = default!;
|
||||
[Dependency] private IUserInterfaceManager _userInterfaceManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when MIDI data should be played.
|
||||
/// </summary>
|
||||
public event Action<byte[]>? StartPlayingRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when any currently playing MIDI track should be stopped.
|
||||
/// </summary>
|
||||
public event Action? StopPlayingRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the track position on the currently playing MIDI track should change.
|
||||
/// </summary>
|
||||
public event Action<int>? TrackPositionChangeRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when looping MIDI tracks is toggled.
|
||||
/// </summary>
|
||||
public event Action<bool>? LoopingToggled;
|
||||
|
||||
private readonly List<ResPath> _loadedTracks = [];
|
||||
private bool _isMidiFileDialogueWindowOpen;
|
||||
private bool _selectedInternally;
|
||||
private float _timeSinceLastRecoverAttempt;
|
||||
|
||||
/// <summary>
|
||||
/// The Instrument playing the tracks. This is needed for the UI to read out current track time.
|
||||
/// on <see cref="FrameUpdate" />.
|
||||
/// </summary>
|
||||
public Entity<InstrumentComponent> Instrument;
|
||||
|
||||
private bool IsShuffle => ShuffleButton.Pressed;
|
||||
private string CurrentFilter => FilterBar.Text;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ButtonName => Loc.GetString("instruments-component-menu-file-midi-source-button");
|
||||
|
||||
/// <summary>
|
||||
/// Controls if MIDI should currently be played. This is connected to the play button.
|
||||
/// </summary>
|
||||
public bool IsPlaying
|
||||
{
|
||||
get => PlayButton.Pressed;
|
||||
set => PlayButton.Pressed = value;
|
||||
}
|
||||
|
||||
public FileMidiSource()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
CurrentTrackLabel.Text = string.Empty;
|
||||
|
||||
_midiCollection.MidiFileAdded += AddTrack;
|
||||
_midiCollection.MidiFileRemoved += RemoveTrack;
|
||||
_midiCollection.MidiFilesReset += ReloadTrackList;
|
||||
|
||||
FilterBar.OnTextChanged += OnFilterBarTextChanged;
|
||||
TrackList.OnItemSelected += OnTrackListItemSelected;
|
||||
TrackList.OnItemDeselected += OnTrackListItemDeselected;
|
||||
AddButton.OnPressed += OnAddButtonPressed;
|
||||
RemoveButton.OnPressed += OnRemoveButtonPressed;
|
||||
PlayButton.OnToggled += OnPlayButtonToggled;
|
||||
LoopButton.OnToggled += OnLoopButtonToggled;
|
||||
ShuffleButton.OnPressed += OnShuffleButtonPressed;
|
||||
PlaybackSlider.OnValueChanged += OnPlaybackSliderValueChanged;
|
||||
PlaybackSlider.OnKeyBindUp += OnPlaybackSliderKeyBindUp;
|
||||
|
||||
RemoveButton.TooltipSupplier = SupplyFileManagementTooltip;
|
||||
ReloadTrackList();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (PlaybackSlider.Grabbed)
|
||||
return;
|
||||
|
||||
// Disable the playback button when nothing is selected and playing.
|
||||
PlayButton.Disabled = !TrackList.GetSelected().Any() && !Instrument.Comp.IsRendererAlive;
|
||||
|
||||
if (!Instrument.Comp.IsMidiOpen)
|
||||
{
|
||||
ResetTrackIndicators();
|
||||
PlaybackSlider.Disabled = true;
|
||||
|
||||
// My patience with the InstrumentSystem ran out, the system will be asked to play tracks until it behaves.
|
||||
if (!IsPlaying)
|
||||
return;
|
||||
|
||||
_timeSinceLastRecoverAttempt += args.DeltaSeconds;
|
||||
if (!(_timeSinceLastRecoverAttempt > RecoveryGracePeriod))
|
||||
return;
|
||||
|
||||
_timeSinceLastRecoverAttempt = 0;
|
||||
ReplayCurrentTrack();
|
||||
}
|
||||
else if (Instrument.Comp.IsRendererAlive)
|
||||
{
|
||||
PlaybackSlider.Disabled = false;
|
||||
PlaybackSlider.MaxValue = Instrument.Comp.PlayerTotalTick;
|
||||
PlaybackSlider.SetValueWithoutEvent(Instrument.Comp.PlayerTick);
|
||||
// TODO: SequencerTimeScale does not return the actual ticks/seconds, find solution.
|
||||
var totalTime =
|
||||
TimeSpan.FromSeconds(Math.Ceiling(Instrument.Comp.PlayerTotalTick /
|
||||
Instrument.Comp.Renderer!.SequencerTimeScale));
|
||||
var currentTime =
|
||||
TimeSpan.FromSeconds(Math.Ceiling(Instrument.Comp.PlayerTick /
|
||||
Instrument.Comp.Renderer!.SequencerTimeScale));
|
||||
TimeLabel.Text = totalTime.Hours < 1
|
||||
? Loc.GetString(
|
||||
"instruments-component-menu-files-track-playtime-seconds-minutes",
|
||||
("currentMinutes", currentTime.ToString("mm")),
|
||||
("currentSeconds", currentTime.ToString("ss")),
|
||||
("totalMinutes", totalTime.ToString("mm")),
|
||||
("totalSeconds", totalTime.ToString("ss")))
|
||||
: Loc.GetString(
|
||||
"instruments-component-menu-files-track-playtime-seconds-minutes-hours",
|
||||
("currentHours", currentTime.ToString("hh")),
|
||||
("currentMinutes", currentTime.ToString("mm")),
|
||||
("currentSeconds", currentTime.ToString("ss")),
|
||||
("totalHours", totalTime.ToString("hh")),
|
||||
("totalMinutes", totalTime.ToString("mm")),
|
||||
("totalSeconds", totalTime.ToString("ss")));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
base.Disable();
|
||||
TrackList.ClearSelected();
|
||||
IsPlaying = false;
|
||||
StopPlayingRequest?.Invoke();
|
||||
CurrentTrackLabel.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void OnFilterBarTextChanged(LineEdit.LineEditEventArgs obj)
|
||||
{
|
||||
FilterTrackList();
|
||||
}
|
||||
|
||||
private void OnTrackListItemSelected(ItemList.ItemListSelectedEventArgs obj)
|
||||
{
|
||||
UpdateFileRemoveButton();
|
||||
if (!_selectedInternally)
|
||||
{
|
||||
IsPlaying = false;
|
||||
StopPlaying();
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayTrackListItem(obj.ItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTrackListItemDeselected(ItemList.ItemListDeselectedEventArgs obj)
|
||||
{
|
||||
RemoveButton.Disabled = true;
|
||||
}
|
||||
|
||||
private async void OnAddButtonPressed(ButtonEventArgs obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isMidiFileDialogueWindowOpen)
|
||||
return;
|
||||
|
||||
var filters = new FileDialogFilters(new FileDialogFilters.Group("mid", "midi"));
|
||||
|
||||
// TODO: Once the file dialogue manager can handle focusing or closing windows, improve this logic to close
|
||||
// or focus the previously-opened window.
|
||||
_isMidiFileDialogueWindowOpen = true;
|
||||
var (stream, name) = await _dialogs.GetFileAndName(filters, FileAccess.Read);
|
||||
await using (stream)
|
||||
{
|
||||
// did the instrument menu get closed while waiting for the user to select a file?
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
if (stream == null)
|
||||
return;
|
||||
|
||||
if (name != null)
|
||||
await _midiCollection.AddMidiFile(new ResPath(name), stream);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_userInterfaceManager.Popup(Loc.GetString("instruments-component-menu-files-error"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isMidiFileDialogueWindowOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemoveButtonPressed(ButtonEventArgs obj)
|
||||
{
|
||||
if (TryGetSelectedTrackPath(out var trackPath) && trackPath is { } resPath)
|
||||
_midiCollection.RemoveMidiFile(resPath);
|
||||
UpdateFileRemoveButton();
|
||||
}
|
||||
|
||||
private void OnPlayButtonToggled(ButtonToggledEventArgs obj)
|
||||
{
|
||||
UpdateFileRemoveButton();
|
||||
if (PlayButton.Pressed)
|
||||
{
|
||||
if (!TryGetSelectedTrack(out var track))
|
||||
return;
|
||||
|
||||
StartPlaying(track);
|
||||
}
|
||||
else
|
||||
{
|
||||
StopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoopButtonToggled(ButtonToggledEventArgs obj)
|
||||
{
|
||||
if (LoopButton.Pressed && ShuffleButton.Pressed)
|
||||
ShuffleButton.Pressed = false;
|
||||
|
||||
LoopingToggled?.Invoke(LoopButton.Pressed);
|
||||
}
|
||||
|
||||
private void OnShuffleButtonPressed(ButtonEventArgs obj)
|
||||
{
|
||||
if (ShuffleButton.Pressed && LoopButton.Pressed)
|
||||
{
|
||||
LoopingToggled?.Invoke(false);
|
||||
LoopButton.Pressed = false;
|
||||
}
|
||||
|
||||
if (obj.Button.Pressed && !TrackList.GetSelected().Any())
|
||||
TrackList[_random.Next(0, TrackList.Count)].Selected = true;
|
||||
}
|
||||
|
||||
private void OnPlaybackSliderValueChanged(Range _)
|
||||
{
|
||||
// Do not seek while still grabbing.
|
||||
if (PlaybackSlider.Grabbed)
|
||||
return;
|
||||
|
||||
TrackPositionChangeRequest?.Invoke((int)Math.Ceiling(PlaybackSlider.Value));
|
||||
}
|
||||
|
||||
private void OnPlaybackSliderKeyBindUp(GUIBoundKeyEventArgs obj)
|
||||
{
|
||||
if (obj.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
TrackPositionChangeRequest?.Invoke((int)Math.Ceiling(PlaybackSlider.Value));
|
||||
}
|
||||
|
||||
private Tooltip? SupplyFileManagementTooltip(Control? sender)
|
||||
{
|
||||
if (sender is not Button { Disabled: true })
|
||||
return null;
|
||||
|
||||
var tooltipText = Loc.GetString("instruments-component-menu-files-file-remove-disabled-info");
|
||||
var tooltip = new Tooltip();
|
||||
tooltip.SetMessage(FormattedMessage.FromUnformatted(tooltipText));
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private void UpdateFileRemoveButton()
|
||||
{
|
||||
RemoveButton.Disabled = !TrackList.GetSelected().Any() || IsPlaying;
|
||||
}
|
||||
|
||||
private void ResetTrackIndicators()
|
||||
{
|
||||
PlaybackSlider.MaxValue = 1;
|
||||
PlaybackSlider.SetValueWithoutEvent(0);
|
||||
CurrentTrackLabel.Text = string.Empty;
|
||||
TimeLabel.Text = "--.--/--.--";
|
||||
}
|
||||
|
||||
private void PlayTrackListItem(int index)
|
||||
{
|
||||
if (index >= TrackList.Count || index < 0)
|
||||
return;
|
||||
|
||||
if (TrackList[index] is not { } item)
|
||||
return;
|
||||
|
||||
StartPlaying(item);
|
||||
}
|
||||
|
||||
private void StopPlaying()
|
||||
{
|
||||
StopPlayingRequest?.Invoke();
|
||||
ResetTrackIndicators();
|
||||
UpdateFileRemoveButton();
|
||||
}
|
||||
|
||||
private void StartPlaying(ItemList.Item item)
|
||||
{
|
||||
if (item.Metadata is not ResPath resPath)
|
||||
return;
|
||||
|
||||
if (item.Text == null)
|
||||
return;
|
||||
|
||||
_timeSinceLastRecoverAttempt = 0;
|
||||
var midiData = _midiCollection.GetMidiData(resPath);
|
||||
StartPlayingRequest?.Invoke(midiData);
|
||||
CurrentTrackLabel.Text = item.Text;
|
||||
UpdateFileRemoveButton();
|
||||
}
|
||||
|
||||
private void FilterTrackList()
|
||||
{
|
||||
var filterString = CurrentFilter.Trim().ToLowerInvariant();
|
||||
TrackList.Clear();
|
||||
|
||||
foreach (var track in _loadedTracks)
|
||||
{
|
||||
var trackName = track.FilenameWithoutExtension.Trim().ToLowerInvariant();
|
||||
if (trackName.Contains(filterString))
|
||||
TrackList.AddItem(track.FilenameWithoutExtension, null, true, track);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetSelectedTrackPath([NotNullWhen(true)] out ResPath? trackPath)
|
||||
{
|
||||
trackPath = null;
|
||||
if (!TryGetSelectedTrack(out var item) || item.Metadata is not ResPath resPath)
|
||||
return false;
|
||||
|
||||
trackPath = resPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetSelectedTrack([NotNullWhen(true)] out ItemList.Item? item)
|
||||
{
|
||||
return TrackList.GetSelected().TryFirstOrDefault(out item);
|
||||
}
|
||||
|
||||
private void AddTrack(ResPath trackPath)
|
||||
{
|
||||
_loadedTracks.Add(trackPath);
|
||||
FilterTrackList();
|
||||
}
|
||||
|
||||
private void RemoveTrack(ResPath trackPath)
|
||||
{
|
||||
_loadedTracks.Remove(trackPath);
|
||||
FilterTrackList();
|
||||
}
|
||||
|
||||
private void ReloadTrackList()
|
||||
{
|
||||
_loadedTracks.Clear();
|
||||
_loadedTracks.AddRange(_midiCollection.GetMidiFiles());
|
||||
FilterTrackList();
|
||||
}
|
||||
|
||||
private void ReplayCurrentTrack()
|
||||
{
|
||||
if (!TryGetSelectedTrack(out var track))
|
||||
return;
|
||||
|
||||
StartPlaying(track);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the next MIDI track from the list according to shuffle and filter settings.
|
||||
/// </summary>
|
||||
/// <remarks>Selecting another track causes its playback if <see cref="IsPlaying" /> is true.</remarks>
|
||||
public void SelectNextTrack()
|
||||
{
|
||||
// Only proceed if file panel is active.
|
||||
if (!Enabled)
|
||||
return;
|
||||
|
||||
// Only proceed if the play button is pressed.
|
||||
if (!IsPlaying)
|
||||
return;
|
||||
|
||||
// List is empty, return
|
||||
if (TrackList.Count == 0)
|
||||
return;
|
||||
|
||||
_selectedInternally = true;
|
||||
try
|
||||
{
|
||||
if (IsShuffle)
|
||||
{
|
||||
var randomIndex = _random.Next(0, TrackList.Count);
|
||||
var randomTrack = TrackList[randomIndex];
|
||||
if (randomTrack.Selected)
|
||||
{
|
||||
// Try going one up or down if we roll the already selected track.
|
||||
if (TrackList.Count - 1 > randomIndex)
|
||||
TrackList[randomIndex + 1].Selected = true;
|
||||
else if (randomIndex > 0)
|
||||
TrackList[randomIndex - 1].Selected = true;
|
||||
else
|
||||
randomTrack.Selected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
randomTrack.Selected = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryGetSelectedTrack(out var item))
|
||||
{
|
||||
TrackList[0].Selected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < TrackList.Count - 1; i++)
|
||||
{
|
||||
if (TrackList[i] != item)
|
||||
continue;
|
||||
TrackList[i + 1].Selected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
TrackList[0].Selected = false;
|
||||
TrackList[0].Selected = true;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_selectedInternally = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Content.Client/Instruments/UI/InputMidiSource.xaml
Normal file
6
Content.Client/Instruments/UI/InputMidiSource.xaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<ui:InputMidiSource xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Instruments.UI"
|
||||
Orientation="Vertical">
|
||||
<Label Text="{Loc 'instruments-component-menu-input-notice-label'}" Align="Center" HorizontalExpand="True"
|
||||
VerticalExpand="True" />
|
||||
</ui:InputMidiSource>
|
||||
38
Content.Client/Instruments/UI/InputMidiSource.xaml.cs
Normal file
38
Content.Client/Instruments/UI/InputMidiSource.xaml.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class InputMidiSource : InstrumentMidiSourceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when MIDI Input via system hardware should be opened.
|
||||
/// </summary>
|
||||
public event Action? OpenInputRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when MIDI Input should be closed.
|
||||
/// </summary>
|
||||
public event Action? CloseInputRequest;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ButtonName => Loc.GetString("instruments-component-menu-input-midi-source-button");
|
||||
|
||||
public InputMidiSource()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void Enable()
|
||||
{
|
||||
base.Enable();
|
||||
OpenInputRequest?.Invoke();
|
||||
}
|
||||
|
||||
public override void Disable()
|
||||
{
|
||||
base.Disable();
|
||||
CloseInputRequest?.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +1,324 @@
|
|||
using Content.Client.Interactable;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Instruments;
|
||||
using Content.Shared.Instruments.UI;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Client.Audio.Midi;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Audio.Midi;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Instruments.UI
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
public sealed partial class InstrumentBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
public sealed partial class InstrumentBoundUserInterface : BoundUserInterface
|
||||
private const int MaxSearchDepth = 16;
|
||||
private const string SawmillCategory = "instrumentui";
|
||||
|
||||
[Dependency] private IMidiManager _midiManager = default!;
|
||||
[Dependency] private ILocalizationManager _loc = default!;
|
||||
[Dependency] private ILogManager _logManager = default!;
|
||||
[Dependency] private InstrumentSystem _instruments = default!;
|
||||
[Dependency] private ActionBlockerSystem _actionBlockerSystem = default!;
|
||||
[Dependency] private InteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private SharedContainerSystem _sharedContainerSystem = default!;
|
||||
private readonly ISawmill _sawmill = default!;
|
||||
|
||||
private readonly FileMidiSource _fileSource = new();
|
||||
private readonly BandMidiSource _bandSource = new();
|
||||
private readonly InputMidiSource _inputSource = new();
|
||||
|
||||
private readonly ChannelsControl _channelsControl = new();
|
||||
private readonly MidiCollectionUtilsControl _midiCollectionUtilsControl = new();
|
||||
|
||||
private InstrumentMenu? _instrumentMenu;
|
||||
|
||||
public InstrumentBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
public IEntityManager Entities => EntMan;
|
||||
[Dependency] public IMidiManager MidiManager = default!;
|
||||
[Dependency] public IFileDialogManager FileDialogManager = default!;
|
||||
[Dependency] public ILocalizationManager Loc = default!;
|
||||
_sawmill = _logManager.GetSawmill(SawmillCategory);
|
||||
}
|
||||
|
||||
public readonly InstrumentSystem Instruments;
|
||||
public readonly ActionBlockerSystem ActionBlocker;
|
||||
public readonly SharedInteractionSystem Interactions;
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
[ViewVariables] private InstrumentMenu? _instrumentMenu;
|
||||
[ViewVariables] private BandMenu? _bandMenu;
|
||||
[ViewVariables] private ChannelsMenu? _channelsMenu;
|
||||
var instrument = EntMan.GetComponent<InstrumentComponent>(Owner);
|
||||
|
||||
public InstrumentBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
instrument.OnMidiPlaybackEnded += OnMidiPlaybackEnded;
|
||||
|
||||
_instruments.OnChannelsUpdated += OnChannelsUpdated;
|
||||
|
||||
_fileSource.StartPlayingRequest += OnStartPlayingRequest;
|
||||
_fileSource.StopPlayingRequest += OnStopPlayingRequest;
|
||||
_fileSource.LoopingToggled += OnLoopToggledRequest;
|
||||
_fileSource.TrackPositionChangeRequest += OnTrackPositionChangeRequest;
|
||||
_fileSource.Instrument = (Owner, instrument);
|
||||
|
||||
_bandSource.RefreshBandRequest += OnRefreshBandsRequest;
|
||||
_bandSource.JoinBandRequest += OnSetBandMasterRequest;
|
||||
|
||||
_inputSource.OpenInputRequest += OnOpenInputRequest;
|
||||
_inputSource.CloseInputRequest += OnCloseInputRequest;
|
||||
|
||||
_channelsControl.ChannelsUpdateRequest += OnChannelsUpdateRequest;
|
||||
_channelsControl.SwitchFilteredChannel += OnSwitchFilteredChannel;
|
||||
|
||||
_instrumentMenu = this.CreateWindow<InstrumentMenu>();
|
||||
|
||||
if (EntMan.TryGetComponent<MetaDataComponent>(Owner, out var metaData))
|
||||
_instrumentMenu.Title = metaData.EntityName;
|
||||
|
||||
_instrumentMenu.SetMidiAvailability(_midiManager.IsAvailable);
|
||||
_instrumentMenu.SetInstrument((Owner, instrument));
|
||||
|
||||
// Initialize sources and switch to the (probably) most used one.
|
||||
// Add additional concrete InstrumentMidiSourceBase types here.
|
||||
_instrumentMenu.SetupSources(_fileSource, _bandSource, _inputSource);
|
||||
_instrumentMenu.SwitchMode(_fileSource);
|
||||
|
||||
// Initialize controls used to configure various system parameters.
|
||||
// Append any additional configuration controls here.
|
||||
_instrumentMenu.AddConfigurationControl(
|
||||
_loc.GetString("instruments-component-menu-channels-label"),
|
||||
_channelsControl);
|
||||
_instrumentMenu.AddConfigurationControl(
|
||||
_loc.GetString("instruments-component-midi-file-collection-label"),
|
||||
_midiCollectionUtilsControl);
|
||||
}
|
||||
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
if (message is not InstrumentBandResponseBuiMessage bandRx)
|
||||
return;
|
||||
|
||||
var entities = new List<(EntityUid, string)>();
|
||||
foreach (var netEnt in bandRx.Nearby)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Instruments = Entities.System<InstrumentSystem>();
|
||||
ActionBlocker = Entities.System<ActionBlockerSystem>();
|
||||
Interactions = Entities.System<SharedInteractionSystem>();
|
||||
entities.Add((EntMan.GetEntity(netEnt.Item1), netEnt.Item2));
|
||||
}
|
||||
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
_bandSource.Populate(entities);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
|
||||
_fileSource.StartPlayingRequest -= OnStartPlayingRequest;
|
||||
_fileSource.StopPlayingRequest -= OnStopPlayingRequest;
|
||||
_fileSource.LoopingToggled -= OnLoopToggledRequest;
|
||||
_fileSource.TrackPositionChangeRequest -= OnTrackPositionChangeRequest;
|
||||
|
||||
_bandSource.RefreshBandRequest -= OnRefreshBandsRequest;
|
||||
_bandSource.JoinBandRequest -= OnSetBandMasterRequest;
|
||||
|
||||
_inputSource.OpenInputRequest -= OnOpenInputRequest;
|
||||
_inputSource.CloseInputRequest -= OnCloseInputRequest;
|
||||
|
||||
if (!EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument))
|
||||
return;
|
||||
|
||||
_fileSource.Instrument = (Owner, instrument);
|
||||
instrument.OnMidiPlaybackEnded -= OnMidiPlaybackEnded;
|
||||
}
|
||||
|
||||
private void OnSwitchFilteredChannel(int channelIndex, bool state)
|
||||
{
|
||||
_instruments.SetFilteredChannel(Owner, channelIndex, !state);
|
||||
}
|
||||
|
||||
private void OnChannelsUpdateRequest()
|
||||
{
|
||||
UpdateChannels();
|
||||
}
|
||||
|
||||
private void OnChannelsUpdated()
|
||||
{
|
||||
UpdateChannels();
|
||||
}
|
||||
|
||||
private void OnMidiPlaybackEnded()
|
||||
{
|
||||
_fileSource.SelectNextTrack();
|
||||
}
|
||||
|
||||
private void OnSetBandMasterRequest(EntityUid ent)
|
||||
{
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
_instruments.SetMaster(Owner, ent);
|
||||
}
|
||||
|
||||
private void OnRefreshBandsRequest()
|
||||
{
|
||||
SendMessage(new InstrumentBandRequestBuiMessage());
|
||||
}
|
||||
|
||||
private void OnLoopToggledRequest(bool toggled)
|
||||
{
|
||||
if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrumentComp))
|
||||
{
|
||||
if (message is InstrumentBandResponseBuiMessage bandRx)
|
||||
_bandMenu?.Populate(bandRx.Nearby, EntMan);
|
||||
instrumentComp.LoopMidi = toggled;
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
_instruments.UpdateRenderer(Owner);
|
||||
}
|
||||
|
||||
private void OnTrackPositionChangeRequest(int value)
|
||||
{
|
||||
_instruments.SetPlayerTick(Owner, value);
|
||||
}
|
||||
|
||||
private void OnOpenInputRequest()
|
||||
{
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
if (!EntMan.TryGetComponent<InstrumentComponent>(Owner, out var instrument))
|
||||
return;
|
||||
|
||||
_instruments.OpenInput(Owner, instrument);
|
||||
}
|
||||
|
||||
private void OnCloseInputRequest()
|
||||
{
|
||||
if (!EntMan.TryGetComponent<InstrumentComponent>(Owner, out var instrument))
|
||||
return;
|
||||
|
||||
_instruments.CloseInput(Owner, false, instrument);
|
||||
}
|
||||
|
||||
private void OnStopPlayingRequest()
|
||||
{
|
||||
if (!EntMan.TryGetComponent<InstrumentComponent>(Owner, out var instrument))
|
||||
return;
|
||||
|
||||
_instruments.CloseMidi(Owner, false, instrument);
|
||||
}
|
||||
|
||||
private void OnStartPlayingRequest(byte[] trackData)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_instrumentMenu = this.CreateWindow<InstrumentMenu>();
|
||||
_instrumentMenu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
|
||||
_instrumentMenu.OnOpenBand += OpenBandMenu;
|
||||
_instrumentMenu.OnOpenChannels += OpenChannelsMenu;
|
||||
_instrumentMenu.OnCloseChannels += CloseChannelsMenu;
|
||||
_instrumentMenu.OnCloseBands += CloseBandMenu;
|
||||
|
||||
_instrumentMenu.SetMIDI(MidiManager.IsAvailable);
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument))
|
||||
{
|
||||
_instrumentMenu.SetInstrument((Owner, instrument));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
if (!EntMan.TryGetComponent<InstrumentComponent>(Owner, out var instrument))
|
||||
return;
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument))
|
||||
{
|
||||
_instrumentMenu?.RemoveInstrument(instrument);
|
||||
}
|
||||
// Close any song that is already playing.
|
||||
if (instrument.IsMidiOpen)
|
||||
_instruments.CloseMidi(Owner, false, instrument);
|
||||
|
||||
_bandMenu?.Dispose();
|
||||
_channelsMenu?.Dispose();
|
||||
if (!_fileSource.IsPlaying)
|
||||
return;
|
||||
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
if (!_instruments.OpenMidi(Owner, trackData, instrument))
|
||||
_fileSource.IsPlaying = false;
|
||||
}
|
||||
|
||||
public void RefreshBands()
|
||||
catch (Exception e)
|
||||
{
|
||||
SendMessage(new InstrumentBandRequestBuiMessage());
|
||||
}
|
||||
|
||||
public void OpenBandMenu()
|
||||
{
|
||||
_bandMenu ??= new BandMenu(this);
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument))
|
||||
{
|
||||
_bandMenu.Master = instrument.Master;
|
||||
}
|
||||
|
||||
// Refresh cache...
|
||||
RefreshBands();
|
||||
|
||||
_bandMenu.OpenCenteredLeft();
|
||||
}
|
||||
|
||||
public void CloseBandMenu()
|
||||
{
|
||||
if(_bandMenu?.IsOpen ?? false)
|
||||
_bandMenu.Close();
|
||||
}
|
||||
|
||||
public void OpenChannelsMenu()
|
||||
{
|
||||
_channelsMenu ??= new ChannelsMenu(this);
|
||||
_channelsMenu.Populate();
|
||||
_channelsMenu.OpenCenteredRight();
|
||||
}
|
||||
|
||||
public void CloseChannelsMenu()
|
||||
{
|
||||
if(_channelsMenu?.IsOpen ?? false)
|
||||
_channelsMenu.Close();
|
||||
_sawmill.Error($"Failed to play next midi track: {e.Message}");
|
||||
_fileSource.IsPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool PlayCheck()
|
||||
{
|
||||
if (!EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument))
|
||||
return false;
|
||||
|
||||
var localEntity = PlayerManager.LocalEntity;
|
||||
|
||||
// If we don't have a player or controlled entity, we return.
|
||||
if (localEntity == null)
|
||||
return false;
|
||||
|
||||
// By default, allow an instrument to play itself and skip all other checks
|
||||
if (localEntity == Owner)
|
||||
return true;
|
||||
|
||||
// If we're a handheld instrument, we might be in a container. Get it just in case.
|
||||
_sharedContainerSystem.TryGetContainingContainer((Owner, null, null), out var conMan);
|
||||
|
||||
// If the instrument is handheld, and we're not holding it, we return.
|
||||
if (instrument.Handheld && (conMan == null || conMan.Owner != localEntity))
|
||||
return false;
|
||||
|
||||
if (!_actionBlockerSystem.CanInteract(localEntity.Value, Owner))
|
||||
return false;
|
||||
|
||||
if (!_interactionSystem.InRangeUnobstructed(localEntity.Value, Owner))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks up the tree of instrument masters to find the truest master of them all.
|
||||
/// </summary>
|
||||
private ActiveInstrumentComponent? ResolveActiveInstrument(InstrumentComponent comp)
|
||||
{
|
||||
var instrument = new Entity<InstrumentComponent>(Owner, comp);
|
||||
|
||||
for (var i = 0; i < MaxSearchDepth; i++)
|
||||
{
|
||||
if (instrument.Comp.Master is not { } master)
|
||||
break;
|
||||
|
||||
if (!EntMan.TryGetComponent<InstrumentComponent>(master, out var masterComp))
|
||||
break;
|
||||
|
||||
instrument = new Entity<InstrumentComponent>(master, masterComp);
|
||||
}
|
||||
|
||||
return EntMan.GetComponentOrNull<ActiveInstrumentComponent>(instrument.Owner);
|
||||
}
|
||||
|
||||
private void UpdateChannels()
|
||||
{
|
||||
if (!EntMan.TryGetComponent<InstrumentComponent>(Owner, out var instrument))
|
||||
return;
|
||||
|
||||
List<MidiChannelInfo> channelSettings = [];
|
||||
|
||||
var activeInstrument = ResolveActiveInstrument(instrument);
|
||||
|
||||
for (var i = 0; i < RobustMidiEvent.MaxChannels; i++)
|
||||
{
|
||||
var trackName = "";
|
||||
var instrumentName = "";
|
||||
var programName = "";
|
||||
if (activeInstrument != null
|
||||
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
|
||||
&& resolvedMidiChannel != null)
|
||||
{
|
||||
trackName = resolvedMidiChannel.TrackName ?? "";
|
||||
instrumentName = resolvedMidiChannel.InstrumentName ?? "";
|
||||
programName = resolvedMidiChannel.ProgramName ?? "";
|
||||
var state = !instrument?.FilteredChannels[i] ?? false;
|
||||
channelSettings.Add(new MidiChannelInfo(i, trackName, instrumentName, programName, state));
|
||||
}
|
||||
}
|
||||
|
||||
_channelsControl.SetChannels(channelSettings.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple DTO for relaying MIDI channel information
|
||||
/// </summary>
|
||||
/// <param name="Id">MIDI channel ID</param>
|
||||
/// <param name="TrackName">MIDI channel track name</param>
|
||||
/// <param name="ProgramName">MIDI channel track name</param>
|
||||
/// <param name="InstrumentName">MIDI channel track name</param>
|
||||
/// <param name="FilterState">MIDI channel filter state</param>
|
||||
public readonly record struct MidiChannelInfo(
|
||||
int Id,
|
||||
string TrackName,
|
||||
string ProgramName,
|
||||
string InstrumentName,
|
||||
bool FilterState);
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
<BoxContainer Orientation="Vertical" SeparationOverride="5">
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<Button Name="InputButton" ToggleMode="True" Text="{Loc 'instruments-component-menu-input-button'}" TextAlign="Center"
|
||||
HorizontalExpand="True" SizeFlagsStretchRatio="1" />
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="0.5" />
|
||||
<Button Name="BandButton" Text="{Loc 'instruments-component-menu-band-button'}" TextAlign="Center" HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="1"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="0.5" />
|
||||
<Button Name="FileButton" Text="{Loc 'instruments-component-menu-play-button'}" TextAlign="Center" HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="1" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<Button Name="LoopButton" ToggleMode="True" Text="{Loc 'instruments-component-menu-loop-button'}" TextAlign="Center" HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="1" />
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="0.5" />
|
||||
<Button Name="ChannelsButton" Text="{Loc 'instruments-component-menu-channels-button'}" TextAlign="Center" HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="1"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="0.5" />
|
||||
<Button Name="StopButton" Text="{Loc 'instruments-component-menu-stop-button'}" TextAlign="Center" HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="1" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="0.125" />
|
||||
<Slider Name="PlaybackSlider" HorizontalExpand="True" />
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="0.125" />
|
||||
</BoxContainer>
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="480 270"
|
||||
SetSize="500 500">
|
||||
<BoxContainer Orientation="Vertical" Margin="1,0,0,5" SeparationOverride="10">
|
||||
<TabContainer Name="MainTabContainer" VerticalExpand="True">
|
||||
<BoxContainer Name="InstrumentContainer" Orientation="Horizontal" VerticalExpand="True"
|
||||
SeparationOverride="10">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer Name="MidiSourceButtonsBoxContainer" Orientation="Vertical">
|
||||
<Label Text="{Loc 'instruments-component-menu-midi-source-button'}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<SpriteView Name="InstrumentSpriteView" Stretch="Fill" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="MidiSourcesContainer" VerticalExpand="True" HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<ScrollContainer Name="ConfigScrollContainer" HScrollEnabled="False" HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer
|
||||
Name="ConfigurationItemsContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True" />
|
||||
</ScrollContainer>
|
||||
</TabContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer Name="UnavailableOverlay" Visible="False" MouseFilter="Stop">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000E5" />
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000F5" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<Label VerticalAlignment="Center" HorizontalAlignment="Center" Align="Center"
|
||||
StyleClasses="LabelBig"
|
||||
Text="{Loc instruments-component-menu-no-midi-support}" />
|
||||
</PanelContainer>
|
||||
</DefaultWindow>
|
||||
</controls:FancyWindow>
|
||||
|
|
|
|||
|
|
@ -1,283 +1,144 @@
|
|||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Interactable;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using Range = Robust.Client.UserInterface.Controls.Range;
|
||||
|
||||
namespace Content.Client.Instruments.UI
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class InstrumentMenu : FancyWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class InstrumentMenu : DefaultWindow
|
||||
private InstrumentMidiSourceBase? _currentMode;
|
||||
|
||||
public InstrumentMenu()
|
||||
{
|
||||
[Dependency] private IEntityManager _entManager = default!;
|
||||
[Dependency] private IFileDialogManager _dialogs = default!;
|
||||
[Dependency] private IPlayerManager _player = default!;
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
private bool _isMidiFileDialogueWindowOpen;
|
||||
|
||||
public event Action? OnOpenBand;
|
||||
public event Action? OnOpenChannels;
|
||||
public event Action? OnCloseBands;
|
||||
public event Action? OnCloseChannels;
|
||||
|
||||
public EntityUid Entity;
|
||||
|
||||
public InstrumentMenu()
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(10),
|
||||
};
|
||||
|
||||
InputButton.OnToggled += MidiInputButtonOnOnToggled;
|
||||
BandButton.OnPressed += BandButtonOnPressed;
|
||||
BandButton.OnToggled += BandButtonOnToggled;
|
||||
FileButton.OnPressed += MidiFileButtonOnOnPressed;
|
||||
LoopButton.OnToggled += MidiLoopButtonOnOnToggled;
|
||||
ChannelsButton.OnPressed += ChannelsButtonOnPressed;
|
||||
StopButton.OnPressed += MidiStopButtonOnPressed;
|
||||
PlaybackSlider.OnValueChanged += PlaybackSliderSeek;
|
||||
PlaybackSlider.OnKeyBindUp += PlaybackSliderKeyUp;
|
||||
MainTabContainer.PanelStyleBoxOverride = styleBox;
|
||||
MainTabContainer.SetTabTitle(0, Loc.GetString("instruments-component-menu-play-tab-title"));
|
||||
MainTabContainer.SetTabTitle(1, Loc.GetString("instruments-component-menu-config-tab-title"));
|
||||
}
|
||||
|
||||
MinSize = SetSize = new Vector2(400, 150);
|
||||
}
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_currentMode?.Disable();
|
||||
}
|
||||
|
||||
public void SetInstrument(Entity<InstrumentComponent> entity)
|
||||
/// <summary>
|
||||
/// Registers the passed midi source controls as children of this instrument menu and creates a button
|
||||
/// for each one on the left hand side of the menu.
|
||||
/// </summary>
|
||||
/// <param name="sources">The sources to be registered</param>
|
||||
public void SetupSources(params InstrumentMidiSourceBase[] sources)
|
||||
{
|
||||
var group = new ButtonGroup();
|
||||
for (var i = 0; i < sources.Length; i++)
|
||||
{
|
||||
Entity = entity;
|
||||
var component = entity.Comp;
|
||||
component.OnMidiPlaybackEnded += InstrumentOnMidiPlaybackEnded;
|
||||
LoopButton.Disabled = !component.IsMidiOpen;
|
||||
LoopButton.Pressed = component.LoopMidi;
|
||||
ChannelsButton.Disabled = !component.IsRendererAlive;
|
||||
StopButton.Disabled = !component.IsMidiOpen;
|
||||
PlaybackSlider.MouseFilter = component.IsMidiOpen ? MouseFilterMode.Pass : MouseFilterMode.Ignore;
|
||||
}
|
||||
var source = sources[i];
|
||||
var button = new Button();
|
||||
button.Text = source.ButtonName;
|
||||
button.Group = group;
|
||||
button.OnPressed += _ => { SwitchMode(source); };
|
||||
MidiSourceButtonsBoxContainer.Children.Add(button);
|
||||
MidiSourcesContainer.Children.Add(source);
|
||||
source.VerticalExpand = true;
|
||||
source.HorizontalExpand = true;
|
||||
source.Visible = false;
|
||||
|
||||
public void RemoveInstrument(InstrumentComponent component)
|
||||
{
|
||||
component.OnMidiPlaybackEnded -= InstrumentOnMidiPlaybackEnded;
|
||||
}
|
||||
|
||||
public void SetMIDI(bool available)
|
||||
{
|
||||
UnavailableOverlay.Visible = !available;
|
||||
}
|
||||
|
||||
private void BandButtonOnPressed(ButtonEventArgs obj)
|
||||
{
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
OnOpenBand?.Invoke();
|
||||
}
|
||||
|
||||
private void BandButtonOnToggled(ButtonToggledEventArgs obj)
|
||||
{
|
||||
if (obj.Pressed)
|
||||
return;
|
||||
|
||||
if (_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument))
|
||||
// Set nicer style classes depending on button position.
|
||||
if (i == 0)
|
||||
{
|
||||
_entManager.System<InstrumentSystem>().SetMaster(Entity, instrument.Master);
|
||||
button.Pressed = true;
|
||||
button.StyleClasses.Add("OpenLeft");
|
||||
}
|
||||
}
|
||||
|
||||
private void ChannelsButtonOnPressed(ButtonEventArgs obj)
|
||||
{
|
||||
OnOpenChannels?.Invoke();
|
||||
}
|
||||
|
||||
private void InstrumentOnMidiPlaybackEnded()
|
||||
{
|
||||
MidiPlaybackSetButtonsDisabled(true);
|
||||
}
|
||||
|
||||
public void MidiPlaybackSetButtonsDisabled(bool disabled)
|
||||
{
|
||||
if (disabled)
|
||||
else if (i == sources.Length - 1)
|
||||
{
|
||||
OnCloseChannels?.Invoke();
|
||||
}
|
||||
|
||||
LoopButton.Disabled = disabled;
|
||||
StopButton.Disabled = disabled;
|
||||
|
||||
// Whether to allow the slider to receive events..
|
||||
PlaybackSlider.MouseFilter = !disabled ? MouseFilterMode.Pass : MouseFilterMode.Ignore;
|
||||
}
|
||||
|
||||
private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj)
|
||||
{
|
||||
if (_isMidiFileDialogueWindowOpen)
|
||||
return;
|
||||
|
||||
OnCloseBands?.Invoke();
|
||||
|
||||
var filters = new FileDialogFilters(new FileDialogFilters.Group("mid", "midi"));
|
||||
|
||||
// TODO: Once the file dialogue manager can handle focusing or closing windows, improve this logic to close
|
||||
// or focus the previously-opened window.
|
||||
_isMidiFileDialogueWindowOpen = true;
|
||||
|
||||
await using var file = await _dialogs.OpenFile(filters, FileAccess.Read);
|
||||
|
||||
_isMidiFileDialogueWindowOpen = false;
|
||||
|
||||
// did the instrument menu get closed while waiting for the user to select a file?
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
// The following checks are only in place to prevent players from playing MIDI songs locally.
|
||||
// There are equivalents for these checks on the server.
|
||||
|
||||
if (file == null)
|
||||
return;
|
||||
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.System<InstrumentSystem>()
|
||||
.OpenMidi(Entity,
|
||||
file.CopyToArray(),
|
||||
instrument))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MidiPlaybackSetButtonsDisabled(false);
|
||||
if (InputButton.Pressed)
|
||||
InputButton.Pressed = false;
|
||||
}
|
||||
|
||||
private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj)
|
||||
{
|
||||
OnCloseBands?.Invoke();
|
||||
|
||||
if (obj.Pressed)
|
||||
{
|
||||
if (!PlayCheck())
|
||||
return;
|
||||
|
||||
MidiStopButtonOnPressed(null);
|
||||
|
||||
if (_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument))
|
||||
_entManager.System<InstrumentSystem>().OpenInput(Entity, instrument);
|
||||
button.StyleClasses.Add("OpenRight");
|
||||
}
|
||||
else
|
||||
{
|
||||
_entManager.System<InstrumentSystem>().CloseInput(Entity, false);
|
||||
OnCloseChannels?.Invoke();
|
||||
button.StyleClasses.Add("OpenBoth");
|
||||
}
|
||||
}
|
||||
|
||||
private bool PlayCheck()
|
||||
{
|
||||
// TODO all of these checks should also be done server-side.
|
||||
if (!_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument))
|
||||
return false;
|
||||
|
||||
var localEntity = _player.LocalEntity;
|
||||
|
||||
// If we don't have a player or controlled entity, we return.
|
||||
if (localEntity == null)
|
||||
return false;
|
||||
|
||||
// By default, allow an instrument to play itself and skip all other checks
|
||||
if (localEntity == Entity)
|
||||
return true;
|
||||
|
||||
var container = _entManager.System<SharedContainerSystem>();
|
||||
// If we're a handheld instrument, we might be in a container. Get it just in case.
|
||||
container.TryGetContainingContainer((Entity, null, null), out var conMan);
|
||||
|
||||
// If the instrument is handheld and we're not holding it, we return.
|
||||
if (instrument.Handheld && (conMan == null || conMan.Owner != localEntity))
|
||||
return false;
|
||||
|
||||
if (!_entManager.System<ActionBlockerSystem>().CanInteract(localEntity.Value, Entity))
|
||||
return false;
|
||||
|
||||
// We check that we're in range unobstructed just in case.
|
||||
return _entManager.System<InteractionSystem>().InRangeUnobstructed(localEntity.Value, Entity);
|
||||
}
|
||||
|
||||
private void MidiStopButtonOnPressed(ButtonEventArgs? obj)
|
||||
{
|
||||
MidiPlaybackSetButtonsDisabled(true);
|
||||
|
||||
_entManager.System<InstrumentSystem>().CloseMidi(Entity, false);
|
||||
OnCloseChannels?.Invoke();
|
||||
}
|
||||
|
||||
private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj)
|
||||
{
|
||||
var instrument = _entManager.System<InstrumentSystem>();
|
||||
|
||||
if (_entManager.TryGetComponent(Entity, out InstrumentComponent? instrumentComp))
|
||||
{
|
||||
instrumentComp.LoopMidi = obj.Pressed;
|
||||
}
|
||||
|
||||
instrument.UpdateRenderer(Entity);
|
||||
}
|
||||
|
||||
private void PlaybackSliderSeek(Range _)
|
||||
{
|
||||
// Do not seek while still grabbing.
|
||||
if (PlaybackSlider.Grabbed)
|
||||
return;
|
||||
|
||||
_entManager.System<InstrumentSystem>().SetPlayerTick(Entity, (int)Math.Ceiling(PlaybackSlider.Value));
|
||||
}
|
||||
|
||||
private void PlaybackSliderKeyUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
_entManager.System<InstrumentSystem>().SetPlayerTick(Entity, (int)Math.Ceiling(PlaybackSlider.Value));
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!_entManager.TryGetComponent(Entity, out InstrumentComponent? instrument))
|
||||
return;
|
||||
|
||||
var hasMaster = instrument.Master != null;
|
||||
BandButton.ToggleMode = hasMaster;
|
||||
BandButton.Pressed = hasMaster;
|
||||
BandButton.Disabled = instrument.IsMidiOpen || instrument.IsInputOpen;
|
||||
ChannelsButton.Disabled = !instrument.IsRendererAlive;
|
||||
|
||||
if (!instrument.IsMidiOpen)
|
||||
{
|
||||
PlaybackSlider.MaxValue = 1;
|
||||
PlaybackSlider.SetValueWithoutEvent(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlaybackSlider.Grabbed)
|
||||
return;
|
||||
|
||||
PlaybackSlider.MaxValue = instrument.PlayerTotalTick;
|
||||
PlaybackSlider.SetValueWithoutEvent(instrument.PlayerTick);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the given control and registers it as an option for the configuration tab control.
|
||||
/// </summary>
|
||||
/// <param name="name">The string to display on the tab header for this configuration.</param>
|
||||
/// <param name="ctrl">The control used for this configuration.</param>
|
||||
public void AddConfigurationControl(string name, Control ctrl)
|
||||
{
|
||||
var formatted = new FormattedMessage();
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
BorderColor = Color.FromHex("#3D4059"),
|
||||
BorderThickness = new Thickness(2),
|
||||
};
|
||||
|
||||
var panel = new PanelContainer
|
||||
{
|
||||
PanelOverride = styleBox,
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
};
|
||||
|
||||
var header = new RichTextLabel
|
||||
{
|
||||
StyleClasses = { StyleClass.LabelKeyText },
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
};
|
||||
|
||||
ctrl.VerticalExpand = true;
|
||||
ctrl.Margin = new Thickness(5);
|
||||
|
||||
formatted.AddMarkupOrThrow($"[bold]{name}[/bold]");
|
||||
header.SetMessage(formatted, Color.FromHex("#A88B5E"));
|
||||
|
||||
panel.AddChild(ctrl);
|
||||
|
||||
ConfigurationItemsContainer.AddChild(header);
|
||||
ConfigurationItemsContainer.AddChild(panel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables & hides the currently active source control, then enables & shows the given one.
|
||||
/// </summary>
|
||||
/// <param name="source">The source control to enable.</param>
|
||||
/// <remarks>The given source must be registered first using the <see cref="SetupSources" /> function.</remarks>
|
||||
public void SwitchMode(InstrumentMidiSourceBase source)
|
||||
{
|
||||
_currentMode?.Disable();
|
||||
_currentMode = source;
|
||||
_currentMode.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When true, overlays the menu with a notice that MIDI is currently not available.
|
||||
/// </summary>
|
||||
/// <param name="available">Set to true when MIDI is available.</param>
|
||||
/// <remarks>This should be set to false when MIDI playback is not available on the system.</remarks>
|
||||
public void SetMidiAvailability(bool available)
|
||||
{
|
||||
UnavailableOverlay.Visible = !available;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes an entity with an <see cref="InstrumentComponent" /> and displays its sprite on the left hand side.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to display.</param>
|
||||
public void SetInstrument(Entity<InstrumentComponent> entity)
|
||||
{
|
||||
InstrumentSpriteView.SetEntity(entity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io" />
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A type of <see cref="BoxContainer" /> to be added to the <see cref="InstrumentMenu" /> for displaying and handling
|
||||
/// all necessary controls regarding a certain type of MIDI input (i.e.: Files, Bands, Hardware input).
|
||||
/// Must be initialized and registered to the menu inside <see cref="InstrumentBoundUserInterface" />.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public abstract partial class InstrumentMidiSourceBase : BoxContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// The string to display for the button on the left side of the instrument UI.
|
||||
/// </summary>
|
||||
public virtual string ButtonName { get; } = Loc.GetString("instruments-component-menu-unknown-midi-source-button");
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this source is currently the active one.
|
||||
/// </summary>
|
||||
protected bool Enabled { get; private set; }
|
||||
|
||||
protected InstrumentMidiSourceBase()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override if the source requires additional handling to become enabled.
|
||||
/// i.e.: Opening MIDI-Input.
|
||||
/// </summary>
|
||||
[MustCallBase]
|
||||
public virtual void Enable()
|
||||
{
|
||||
Visible = true;
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override if the source requires additional handling on being disabled.
|
||||
/// i.e.: Closing MIDI-Input.
|
||||
/// </summary>
|
||||
[MustCallBase]
|
||||
public virtual void Disable()
|
||||
{
|
||||
Enabled = false;
|
||||
Visible = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="MidiCountLabel" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="OpenDirectoryButton"
|
||||
Text="{Loc 'instruments-component-midi-file-collection-open-dir-button'}" HorizontalExpand="true"
|
||||
VerticalExpand="true" StyleClasses="OpenRight" />
|
||||
<Button Name="ReloadCollectionButton"
|
||||
Text="{Loc 'instruments-component-midi-file-collection-reload-button'}" HorizontalExpand="true"
|
||||
VerticalExpand="true" StyleClasses="OpenBoth" />
|
||||
<controls:ConfirmButton Name="DeleteAllButton"
|
||||
Text="{Loc 'instruments-component-midi-file-collection-delete-all-button'}"
|
||||
HorizontalExpand="true" VerticalExpand="true" StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using System.Linq;
|
||||
using Content.Client.Audio.Midi;
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.ContentPack;
|
||||
|
||||
namespace Content.Client.Instruments.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MidiCollectionUtilsControl : Control
|
||||
{
|
||||
[Dependency] private MidiFileCollectionManager _midiCollection = default!;
|
||||
[Dependency] private IResourceManager _resourceManager = default!;
|
||||
|
||||
public MidiCollectionUtilsControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_midiCollection.MidiFileAdded += _ => UpdateCount();
|
||||
_midiCollection.MidiFileRemoved += _ => UpdateCount();
|
||||
_midiCollection.MidiFilesReset += UpdateCount;
|
||||
|
||||
DeleteAllButton.StyleClasses.Add(StyleClass.Negative);
|
||||
|
||||
OpenDirectoryButton.OnPressed += OnOpenDirectoryButtonPressed;
|
||||
ReloadCollectionButton.OnPressed += OnReloadCollectionButtonPressed;
|
||||
DeleteAllButton.OnPressed += OnDeleteAllButtonPressed;
|
||||
|
||||
UpdateCount();
|
||||
}
|
||||
|
||||
private void OnOpenDirectoryButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
_resourceManager.UserData.OpenOsWindow(MidiFileCollectionManager.UserMidiDirectory);
|
||||
}
|
||||
|
||||
private void OnReloadCollectionButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
_midiCollection.ReloadLibrary();
|
||||
}
|
||||
|
||||
private void OnDeleteAllButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
_midiCollection.RemoveAllMidiFiles();
|
||||
}
|
||||
|
||||
private void UpdateCount()
|
||||
{
|
||||
MidiCountLabel.Text = Loc.GetString(
|
||||
"instruments-component-midi-file-collection-size-label",
|
||||
("count", _midiCollection.GetMidiFiles().Count()));
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ public sealed partial class CryoPodWindow : FancyWindow
|
|||
}
|
||||
|
||||
// Status checklist
|
||||
const float fallbackTemperatureRequirement = 213;
|
||||
const float fallbackTemperatureRequirement = Atmospherics.T0C;
|
||||
var hasTemperatureCheck = (hasGas && hasCorrectTemperature
|
||||
&& (lowestTempRequirement != null || msg.GasMix.Temperature < fallbackTemperatureRequirement));
|
||||
var hasChemicals = (hasBeaker && !isBeakerEmpty);
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ public sealed partial class ScreechShockWaveOverlay : Overlay
|
|||
_shader.SetParameter("times", _times);
|
||||
_shader.SetParameter("count", _currentCount);
|
||||
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
|
||||
_shader?.SetParameter("renderScale", args.Viewport.RenderScale * args.Viewport.Eye.Scale);
|
||||
|
||||
// finally do the rendering
|
||||
var worldHandle = args.WorldHandle;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Content.Shared.PDA;
|
|||
using Robust.Shared.Utility;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
|
|
@ -18,6 +19,7 @@ namespace Content.Client.PDA
|
|||
[Dependency] private IClipboardManager _clipboard = null!;
|
||||
[Dependency] private IGameTiming _gameTiming = default!;
|
||||
[Dependency] private IEntitySystemManager _entitySystem = default!;
|
||||
private readonly AlertLevelSystem _alert = default!;
|
||||
private readonly ClientGameTicker _gameTicker;
|
||||
|
||||
public const int HomeView = 0;
|
||||
|
|
@ -42,6 +44,7 @@ namespace Content.Client.PDA
|
|||
public PdaMenu()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_alert = _entitySystem.GetEntitySystem<AlertLevelSystem>();
|
||||
_gameTicker = _entitySystem.GetEntitySystem<ClientGameTicker>();
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
|
|
@ -172,17 +175,14 @@ namespace Content.Client.PDA
|
|||
StationTimeLabel.SetMarkup(Loc.GetString("comp-pda-ui-station-time",
|
||||
("time", stationTime.ToString("hh\\:mm\\:ss"))));
|
||||
|
||||
var alertLevel = state.PdaOwnerInfo.StationAlertLevel;
|
||||
var alertColor = state.PdaOwnerInfo.StationAlertColor;
|
||||
var alertLevelKey = alertLevel != null ? $"alert-level-{alertLevel}" : "alert-level-unknown";
|
||||
_alertLevel = Loc.GetString(alertLevelKey);
|
||||
(_alertLevel, _, _instructions) = _alert.AlertLevelData(state.PdaOwnerInfo.StationAlertLevel);
|
||||
|
||||
StationAlertLevelLabel.SetMarkup(Loc.GetString(
|
||||
"comp-pda-ui-station-alert-level",
|
||||
("color", alertColor),
|
||||
("level", _alertLevel)
|
||||
));
|
||||
_instructions = Loc.GetString($"{alertLevelKey}-instructions");
|
||||
StationAlertLevelInstructions.SetMarkup(Loc.GetString(
|
||||
"comp-pda-ui-station-alert-level-instructions",
|
||||
("instructions", _instructions))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Client.Speech.EntitySystems;
|
||||
|
||||
public sealed class DamagedSiliconAccentSystem : SharedDamagedSiliconAccentSystem;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Client.Speech.EntitySystems;
|
||||
|
||||
public sealed class RatvarianLanguageSystem : SharedRatvarianLanguageSystem
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Client.Speech.EntitySystems;
|
||||
|
||||
public sealed class SlurredSystem : SharedSlurredSystem
|
||||
{
|
||||
public override string Accentuate(string message, Entity<SlurredAccentComponent>? ent = null)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Client.Speech.EntitySystems;
|
||||
|
||||
public sealed class StutteringSystem : SharedStutteringSystem
|
||||
{
|
||||
public override string Accentuate(string message, Entity<StutteringAccentComponent>? ent = null)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
5
Content.Client/Suicide/SuicideSystem.cs
Normal file
5
Content.Client/Suicide/SuicideSystem.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Suicide;
|
||||
|
||||
namespace Content.Client.Suicide;
|
||||
|
||||
public sealed class SuicideSystem : SharedSuicideSystem;
|
||||
111
Content.IntegrationTests/Tests/Body/DetachableOrganSystemTest.cs
Normal file
111
Content.IntegrationTests/Tests/Body/DetachableOrganSystemTest.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.Shared.Body;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Body;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(DetachableOrganSystem))]
|
||||
public sealed class DetachableOrganSystemTest : GameTest
|
||||
{
|
||||
private const string DetachTestOldBody = "DetachTestOldBody";
|
||||
private const string DetachTestNewBody = "DetachTestNewBody";
|
||||
private const string DetachTestGrandParentOrgan = "DetachTestGrandParentOrgan";
|
||||
private const string DetachTestRootOrgan = "DetachTestRootOrgan";
|
||||
private const string DetachTestChildOrgan = "DetachTestChildOrgan";
|
||||
private const string DetachTestSiblingOrgan = "DetachTestSiblingOrgan";
|
||||
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = $@"
|
||||
- type: entity
|
||||
id: {DetachTestOldBody}
|
||||
components:
|
||||
- type: Body
|
||||
|
||||
- type: entity
|
||||
id: {DetachTestNewBody}
|
||||
components:
|
||||
- type: Body
|
||||
|
||||
- type: entity
|
||||
id: {DetachTestGrandParentOrgan}
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ParentOrgan
|
||||
- type: ChildOrgan
|
||||
|
||||
- type: entity
|
||||
id: {DetachTestRootOrgan}
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ParentOrgan
|
||||
- type: ChildOrgan
|
||||
- type: DetachableOrgan
|
||||
detachedBody: DetachTestNewBody
|
||||
|
||||
- type: entity
|
||||
id: {DetachTestChildOrgan}
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ChildOrgan
|
||||
|
||||
- type: entity
|
||||
id: {DetachTestSiblingOrgan}
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ChildOrgan
|
||||
";
|
||||
|
||||
[SidedDependency(Side.Server)] private SharedContainerSystem _container = default!;
|
||||
[SidedDependency(Side.Server)] private OrganRelationSystem _organRelation = default!;
|
||||
[SidedDependency(Side.Server)] private DetachableOrganSystem _detachableOrgan = default!;
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void Detach()
|
||||
{
|
||||
var oldBody = SSpawn(DetachTestOldBody);
|
||||
var oldBodyContainer = _container.GetContainer(oldBody, BodyComponent.ContainerID);
|
||||
|
||||
var grandParent = SSpawn(DetachTestGrandParentOrgan);
|
||||
var root = SSpawn(DetachTestRootOrgan);
|
||||
var child = SSpawn(DetachTestChildOrgan);
|
||||
var sibling = SSpawn(DetachTestSiblingOrgan);
|
||||
|
||||
_container.Insert(grandParent, oldBodyContainer, force: true);
|
||||
_container.Insert(root, oldBodyContainer, force: true);
|
||||
_container.Insert(child, oldBodyContainer, force: true);
|
||||
_container.Insert(sibling, oldBodyContainer, force: true);
|
||||
|
||||
_organRelation.Relate(grandParent, root);
|
||||
_organRelation.Relate(grandParent, sibling);
|
||||
_organRelation.Relate(root, child);
|
||||
|
||||
var newBody = _detachableOrgan.Detach(root);
|
||||
|
||||
Assert.That(newBody, Is.Not.Null);
|
||||
|
||||
var newBodyContained = _container.GetContainer(newBody!.Value, BodyComponent.ContainerID).ContainedEntities.ToList();
|
||||
Assert.That(newBodyContained, Is.EquivalentTo(new[] { root, child }));
|
||||
|
||||
var oldBodyContained = oldBodyContainer.ContainedEntities.ToList();
|
||||
Assert.That(oldBodyContained, Is.EquivalentTo(new[] { grandParent, sibling }));
|
||||
|
||||
var grandParentComp = SComp<ParentOrganComponent>(grandParent);
|
||||
Assert.That(grandParentComp.Children, Does.Not.Contain(root));
|
||||
Assert.That(grandParentComp.Children, Does.Contain(sibling));
|
||||
|
||||
Assert.That(SComp<ChildOrganComponent>(root).Parent, Is.Null);
|
||||
Assert.That(SComp<ParentOrganComponent>(root).Children, Does.Contain(child));
|
||||
Assert.That(SComp<ChildOrganComponent>(child).Parent, Is.EqualTo(root));
|
||||
|
||||
Assert.That(SComp<OrganComponent>(root).Body, Is.EqualTo(newBody));
|
||||
Assert.That(SComp<OrganComponent>(child).Body, Is.EqualTo(newBody));
|
||||
Assert.That(SComp<OrganComponent>(grandParent).Body, Is.EqualTo(oldBody));
|
||||
Assert.That(SComp<OrganComponent>(sibling).Body, Is.EqualTo(oldBody));
|
||||
}
|
||||
}
|
||||
67
Content.IntegrationTests/Tests/Body/InitialBodySpawnTest.cs
Normal file
67
Content.IntegrationTests/Tests/Body/InitialBodySpawnTest.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.Shared.Body;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Body;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(InitialBodySystem))]
|
||||
public sealed class InitialBodySpawnTest : GameTest
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: organCategory
|
||||
id: InitialBodySpawnTestParent
|
||||
|
||||
- type: organCategory
|
||||
id: InitialBodySpawnTestChild
|
||||
|
||||
- type: entity
|
||||
id: InitialBodySpawnTestBody
|
||||
components:
|
||||
- type: Body
|
||||
- type: InitialBody
|
||||
organs:
|
||||
InitialBodySpawnTestParent: InitialBodySpawnTestParentOrgan
|
||||
InitialBodySpawnTestChild: InitialBodySpawnTestChildOrgan
|
||||
relationships:
|
||||
InitialBodySpawnTestParent: [InitialBodySpawnTestChild]
|
||||
|
||||
- type: entity
|
||||
id: InitialBodySpawnTestParentOrgan
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ParentOrgan
|
||||
|
||||
- type: entity
|
||||
id: InitialBodySpawnTestChildOrgan
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ChildOrgan
|
||||
";
|
||||
|
||||
[SidedDependency(Side.Server)] private SharedContainerSystem _container = default!;
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void SpawningWiresUpOrganRelations()
|
||||
{
|
||||
var body = SSpawn("InitialBodySpawnTestBody");
|
||||
|
||||
var bodyContainer = _container.GetContainer(body, BodyComponent.ContainerID);
|
||||
var contained = bodyContainer.ContainedEntities.ToList();
|
||||
Assert.That(contained, Has.Count.EqualTo(2));
|
||||
|
||||
var parent = contained.Single(SEntMan.HasComponent<ParentOrganComponent>);
|
||||
var child = contained.Single(SEntMan.HasComponent<ChildOrganComponent>);
|
||||
|
||||
var parentComp = SComp<ParentOrganComponent>(parent);
|
||||
var childComp = SComp<ChildOrganComponent>(child);
|
||||
|
||||
Assert.That(parentComp.Children, Does.Contain(child));
|
||||
Assert.That(childComp.Parent, Is.EqualTo(parent));
|
||||
}
|
||||
}
|
||||
179
Content.IntegrationTests/Tests/Body/InitialBodyValidationTest.cs
Normal file
179
Content.IntegrationTests/Tests/Body/InitialBodyValidationTest.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.Shared.Body;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Body;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(InitialBodySystem))]
|
||||
public sealed class InitialBodyValidationTest : GameTest
|
||||
{
|
||||
[SidedDependency(Side.Server)] private IComponentFactory _componentFactory = default!;
|
||||
[SidedDependency(Side.Server)] private IPrototypeManager _prototype = default!;
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void RelatedOrgansCanBeRelated()
|
||||
{
|
||||
using var scope = Assert.EnterMultipleScope();
|
||||
|
||||
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
|
||||
continue;
|
||||
|
||||
if (!proto.TryComp<InitialBodyComponent>(out var initial, _componentFactory))
|
||||
continue;
|
||||
|
||||
if (initial.Relationships is not { } relationships)
|
||||
continue;
|
||||
|
||||
var organs = initial.Organs;
|
||||
foreach (var (parent, children) in relationships)
|
||||
{
|
||||
if (!organs.TryGetValue(parent, out var parentProtoId))
|
||||
continue;
|
||||
|
||||
var parentProto = _prototype.Index(parentProtoId);
|
||||
if (!parentProto.HasComp<ParentOrganComponent>(_componentFactory))
|
||||
{
|
||||
Assert.Fail($"{proto.ID}'s parent organ {parent} {parentProtoId} is missing {nameof(ParentOrganComponent)}");
|
||||
}
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (!organs.TryGetValue(child, out var childProtoId))
|
||||
continue;
|
||||
|
||||
var childProto = _prototype.Index(childProtoId);
|
||||
if (!childProto.HasComp<ChildOrganComponent>(_componentFactory))
|
||||
{
|
||||
Assert.Fail($"{proto.ID}'s child organ {child} {childProtoId} (child of {parent} {parentProtoId}) is missing {nameof(ChildOrganComponent)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void NoMultipleParents()
|
||||
{
|
||||
using var scope = Assert.EnterMultipleScope();
|
||||
|
||||
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
|
||||
continue;
|
||||
|
||||
if (!proto.TryComp<InitialBodyComponent>(out var initial, _componentFactory))
|
||||
continue;
|
||||
|
||||
if (initial.Relationships is not { } relationships)
|
||||
continue;
|
||||
|
||||
var parentOf = new Dictionary<ProtoId<OrganCategoryPrototype>, ProtoId<OrganCategoryPrototype>>();
|
||||
foreach (var (parent, children) in relationships)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (parentOf.TryGetValue(child, out var existingParent))
|
||||
{
|
||||
Assert.Fail($"{proto.ID}'s organ category {child} is claimed as a child by both {existingParent} and {parent}");
|
||||
}
|
||||
|
||||
parentOf[child] = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void InternalChildOrgansAreNotDetachable()
|
||||
{
|
||||
using var scope = Assert.EnterMultipleScope();
|
||||
|
||||
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
|
||||
continue;
|
||||
|
||||
if (!proto.HasComp<InternalChildOrganComponent>(_componentFactory))
|
||||
continue;
|
||||
|
||||
if (proto.HasComp<DetachableOrganComponent>(_componentFactory))
|
||||
{
|
||||
Assert.Fail($"{proto.ID} has both {nameof(InternalChildOrganComponent)} and {nameof(DetachableOrganComponent)}. Pick a lane, make your organs internal or detachable, but not both.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void NoCyclicalRelationships()
|
||||
{
|
||||
using var scope = Assert.EnterMultipleScope();
|
||||
|
||||
foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract || Pair.IsTestEntityPrototype(proto.ID))
|
||||
continue;
|
||||
|
||||
if (!proto.TryComp<InitialBodyComponent>(out var initial, _componentFactory))
|
||||
continue;
|
||||
|
||||
if (initial.Relationships is not { } relationships)
|
||||
continue;
|
||||
|
||||
var visited = new HashSet<ProtoId<OrganCategoryPrototype>>();
|
||||
var stack = new List<ProtoId<OrganCategoryPrototype>>();
|
||||
|
||||
foreach (var parent in relationships.Keys)
|
||||
{
|
||||
if (TryFindCycle(parent, relationships, visited, stack, out var cycle))
|
||||
{
|
||||
Assert.Fail($"{proto.ID} has a cycle in its InitialBodyComponent relationships: {string.Join(" -> ", cycle)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryFindCycle(
|
||||
ProtoId<OrganCategoryPrototype> node,
|
||||
Dictionary<ProtoId<OrganCategoryPrototype>, HashSet<ProtoId<OrganCategoryPrototype>>> relationships,
|
||||
HashSet<ProtoId<OrganCategoryPrototype>> visited,
|
||||
List<ProtoId<OrganCategoryPrototype>> stack,
|
||||
out List<ProtoId<OrganCategoryPrototype>> cycle)
|
||||
{
|
||||
if (stack.Contains(node))
|
||||
{
|
||||
var start = stack.IndexOf(node);
|
||||
cycle = stack.Skip(start).Append(node).ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
cycle = new List<ProtoId<OrganCategoryPrototype>>();
|
||||
|
||||
if (!visited.Add(node))
|
||||
return false;
|
||||
|
||||
if (!relationships.TryGetValue(node, out var children))
|
||||
return false;
|
||||
|
||||
stack.Add(node);
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (TryFindCycle(child, relationships, visited, stack, out cycle))
|
||||
return true;
|
||||
}
|
||||
|
||||
stack.RemoveAt(stack.Count - 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.Shared.Body;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Body;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(OrganRelationSystem))]
|
||||
public sealed class OrganRelationSystemTest : GameTest
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: OrganRelationTestOrgan
|
||||
components:
|
||||
- type: Organ
|
||||
- type: ParentOrgan
|
||||
- type: ChildOrgan
|
||||
";
|
||||
|
||||
[SidedDependency(Side.Server)] private OrganRelationSystem _organRelation = default!;
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void RelateAndOrphan()
|
||||
{
|
||||
var parent = SSpawn("OrganRelationTestOrgan");
|
||||
var child = SSpawn("OrganRelationTestOrgan");
|
||||
|
||||
_organRelation.Relate(parent, child);
|
||||
|
||||
var parentComp = SComp<ParentOrganComponent>(parent);
|
||||
var childComp = SComp<ChildOrganComponent>(child);
|
||||
|
||||
Assert.That(parentComp.Children, Does.Contain(child));
|
||||
Assert.That(childComp.Parent, Is.EqualTo(parent));
|
||||
|
||||
_organRelation.Orphan(child);
|
||||
|
||||
Assert.That(parentComp.Children, Does.Not.Contain(child));
|
||||
Assert.That(childComp.Parent, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void Traversal()
|
||||
{
|
||||
var grandParent = SSpawn("OrganRelationTestOrgan");
|
||||
var parent = SSpawn("OrganRelationTestOrgan");
|
||||
var child = SSpawn("OrganRelationTestOrgan");
|
||||
|
||||
_organRelation.Relate(grandParent, parent);
|
||||
_organRelation.Relate(parent, child);
|
||||
|
||||
var allChildren = _organRelation.AllChildren(grandParent).Select(e => e.Owner).ToList();
|
||||
Assert.That(allChildren, Is.EquivalentTo(new[] { parent, child }));
|
||||
|
||||
var allParents = _organRelation.AllParents(child).Select(e => e.Owner).ToList();
|
||||
Assert.That(allParents, Is.EquivalentTo(new[] { parent, grandParent }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void ParentDeletion()
|
||||
{
|
||||
var parent = SSpawn("OrganRelationTestOrgan");
|
||||
var child = SSpawn("OrganRelationTestOrgan");
|
||||
|
||||
_organRelation.Relate(parent, child);
|
||||
|
||||
SDeleteNow(parent);
|
||||
|
||||
Assert.That(SEntMan.Deleted(child), Is.False);
|
||||
Assert.That(SComp<ChildOrganComponent>(child).Parent, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[RunOnSide(Side.Server)]
|
||||
public void ChildDeletion()
|
||||
{
|
||||
var parent = SSpawn("OrganRelationTestOrgan");
|
||||
var child = SSpawn("OrganRelationTestOrgan");
|
||||
|
||||
_organRelation.Relate(parent, child);
|
||||
|
||||
SDeleteNow(child);
|
||||
|
||||
Assert.That(SComp<ParentOrganComponent>(parent).Children, Does.Not.Contain(child));
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,8 @@ public sealed class EdgeClobbering : InteractionTest
|
|||
var sTarget = SEntMan.GetEntity(Target!.Value);
|
||||
|
||||
await InteractUsing(Screw, false);
|
||||
SEntMan.EventBus.RaiseLocalEvent(sTarget, new OnTemperatureChangeEvent(0f, 0f, 0f));
|
||||
var ev = new TemperatureChangedEvent(0f, 0f);
|
||||
SEntMan.EventBus.RaiseLocalEvent(sTarget, ref ev);
|
||||
await AwaitDoAfters();
|
||||
|
||||
Assert.That(SEntMan.GetComponent<ConstructionComponent>(sTarget).Node, Is.EqualTo("C"));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.IntegrationTests.Utility;
|
||||
using Content.Server.Materials;
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Materials;
|
||||
|
||||
/// <summary>
|
||||
/// Tests to prevent Recycler loops, where the product of one recycling can be recycled again.
|
||||
/// </summary>
|
||||
[TestOf(typeof(MaterialReclaimerSystem))]
|
||||
[TestOf(typeof(MaterialReclaimerComponent))]
|
||||
public sealed class ReclaimerLoopTest : InteractionTest
|
||||
{
|
||||
// ProtoIDs we need
|
||||
private static readonly EntProtoId ApcId = "APCBasic";
|
||||
private static readonly EntProtoId FloorTileId = "FloorTileItemSteel";
|
||||
|
||||
private static readonly string[] Reclaimers = GameDataScrounger.EntitiesWithComponent("MaterialReclaimer");
|
||||
|
||||
[SidedDependency(Side.Server)] private readonly SharedMaterialReclaimerSystem _materialReclaimerSystem = null!;
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(Reclaimers))]
|
||||
[TestOf(typeof(MaterialReclaimerSystem))]
|
||||
[TestOf(typeof(MaterialReclaimerComponent))]
|
||||
[Description("For every material that a reclaimer can spawn, make sure that it cannot get stuck in a loop of spawning then recycling.")]
|
||||
[TrackingIssue("https://github.com/space-wizards/space-station-14/issues/39691")]
|
||||
public async Task MaterialSpawnLoopTest(string reclaimerId)
|
||||
{
|
||||
// Spawn the reclaimer
|
||||
await SpawnTarget(reclaimerId, PlayerCoords);
|
||||
Assume.That(STarget, Is.Not.Null, "STarget was null, did the reclaimer spawn correctly?");
|
||||
|
||||
var reclaimComp = Comp<MaterialReclaimerComponent>(Target);
|
||||
|
||||
// Power the reclaimer
|
||||
await SpawnEntity(ApcId, SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
// Set reclaimer to enabled
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
_materialReclaimerSystem.SetReclaimerEnabled((EntityUid)STarget, true);
|
||||
});
|
||||
|
||||
// Check that reclaimer enabled
|
||||
Assume.That(reclaimComp.Enabled, "The reclaimer did not get or stay enabled");
|
||||
|
||||
// Put a floor tile down
|
||||
await InteractUsing(FloorTileId);
|
||||
|
||||
// Reclaimer can't reclaim materials? Job's done.
|
||||
if (!reclaimComp.ReclaimMaterials)
|
||||
Assert.Ignore("Cannot reclaim materials");
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
// For each material, assert that it is not recyclable (and would thus cause a recycling loop)
|
||||
foreach (var material in ProtoMan.EnumeratePrototypes<MaterialPrototype>())
|
||||
{
|
||||
var matStack = material.StackEntity;
|
||||
Assert.That(
|
||||
matStack,
|
||||
Is.Not.Null,
|
||||
$"The material, {material.ID}, did not have a stackentity associated with it. You may need to add a stackEntity to its Reagents/Materials yml file.");
|
||||
|
||||
var matInHands = await PlaceInHands(matStack);
|
||||
var matInHandsUid = ToServer(matInHands);
|
||||
|
||||
// Assert we're holding material
|
||||
Assert.That(
|
||||
HandSys.GetActiveItem((SPlayer, Hands)),
|
||||
Is.EqualTo(matInHandsUid),
|
||||
$"The material, {matStack}, never got put in our hands.");
|
||||
|
||||
await Interact();
|
||||
|
||||
// Assert Hands not empty
|
||||
Assert.That(
|
||||
HandSys.GetActiveItem((SPlayer, Hands)),
|
||||
Is.Not.Null,
|
||||
$"The material that should not have been reclaimed, {matStack}, is no longer in our hands. The reclaimer was {reclaimerId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,8 @@ public sealed class PrototypeSaveTest : GameTest
|
|||
{
|
||||
// The only prototypes that should get ignored are those that REQUIRE setup to get a sprite. At that point it is
|
||||
// the responsibility of the spawner to ensure that a valid sprite is set.
|
||||
"VirtualItem"
|
||||
"VirtualItem",
|
||||
"DetachedBody"
|
||||
};
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,12 @@ using Content.Server.Pointing.Components;
|
|||
using Content.Server.Polymorph.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Server.Tabletop;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Administration.Systems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Body;
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.AlertLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Alert level component. This is the component given to a station to
|
||||
/// signify its alert level state.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AlertLevelComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The current set of alert levels on the station.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public AlertLevelPrototype? AlertLevels;
|
||||
|
||||
// Once stations are a prototype, this should be used.
|
||||
[DataField("alertLevelPrototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<AlertLevelPrototype>))]
|
||||
public string AlertLevelPrototype = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The current level on the station.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] public string CurrentLevel = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Is current station level can be changed by crew.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] public bool IsLevelLocked = false;
|
||||
|
||||
[ViewVariables] public float CurrentDelay = 0;
|
||||
[ViewVariables] public bool ActiveDelay;
|
||||
|
||||
/// <summary>
|
||||
/// If the level can be selected on the station.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool IsSelectable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AlertLevels == null
|
||||
|| !AlertLevels.Levels.TryGetValue(CurrentLevel, out var level))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return level.Selectable && !level.DisableSelection && !IsLevelLocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ public sealed partial class AlertLevelDisplaySystem : EntitySystem
|
|||
SubscribeLocalEvent<AlertLevelDisplayComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
}
|
||||
|
||||
private void OnAlertChanged(AlertLevelChangedEvent args)
|
||||
private void OnAlertChanged(ref AlertLevelChangedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<AlertLevelDisplayComponent, AppearanceComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var appearance))
|
||||
|
|
@ -33,7 +33,7 @@ public sealed partial class AlertLevelDisplaySystem : EntitySystem
|
|||
var stationUid = _stationSystem.GetOwningStation(uid);
|
||||
if (stationUid != null && TryComp(stationUid, out AlertLevelComponent? alert))
|
||||
{
|
||||
_appearance.SetData(uid, AlertLevelDisplay.CurrentLevel, alert.CurrentLevel, appearance);
|
||||
_appearance.SetData(uid, AlertLevelDisplay.CurrentLevel, alert.CurrentAlertLevel, appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.AlertLevel;
|
||||
|
||||
[Prototype("alertLevels")]
|
||||
public sealed partial class AlertLevelPrototype : IPrototype
|
||||
{
|
||||
[IdDataField] public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of alert levels. Keyed by string - the string key is the most important
|
||||
/// part here. Visualizers will use this in order to dictate what alert level to show on
|
||||
/// client side sprites, and localization uses each key to dictate the alert level name.
|
||||
/// </summary>
|
||||
[DataField("levels")] public Dictionary<string, AlertLevelDetail> Levels = new();
|
||||
|
||||
/// <summary>
|
||||
/// Default level that the station is on upon initialization.
|
||||
/// If this isn't in the dictionary, this will default to whatever .First() gives.
|
||||
/// </summary>
|
||||
[DataField("defaultLevel")] public string DefaultLevel { get; private set; } = default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alert level detail. Does not contain an ID, that is handled by
|
||||
/// the Levels field in AlertLevelPrototype.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class AlertLevelDetail
|
||||
{
|
||||
/// <summary>
|
||||
/// What is announced upon this alert level change. Can be a localized string.
|
||||
/// </summary>
|
||||
[DataField("announcement")] public string Announcement { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this alert level is selectable from a communications console.
|
||||
/// </summary>
|
||||
[DataField("selectable")] public bool Selectable { get; private set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If this alert level disables user selection while it is active. Beware -
|
||||
/// setting this while something is selectable will disable selection permanently!
|
||||
/// This should only apply to entities or gamemodes that auto-select an alert level,
|
||||
/// such as a nuclear bomb being set to active.
|
||||
/// </summary>
|
||||
[DataField("disableSelection")] public bool DisableSelection { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sound that this alert level will play in-game once selected.
|
||||
/// </summary>
|
||||
[DataField("sound")] public SoundSpecifier? Sound { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The color that this alert level will show in-game in chat.
|
||||
/// </summary>
|
||||
[DataField("color")] public Color Color { get; private set; } = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// The color to turn emergency lights on this station when they are active.
|
||||
/// </summary>
|
||||
[DataField("emergencyLightColor")] public Color EmergencyLightColor { get; private set; } = Color.FromHex("#FF4020");
|
||||
|
||||
/// <summary>
|
||||
/// Will this alert level force emergency lights on for the station that's active?
|
||||
/// </summary>
|
||||
[DataField("forceEnableEmergencyLights")] public bool ForceEnableEmergencyLights { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes for the shuttle to arrive when called.
|
||||
/// </summary>
|
||||
[DataField("shuttleTime")] public TimeSpan ShuttleTime { get; private set; } = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.AlertLevel;
|
||||
|
||||
public sealed partial class AlertLevelSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private IConfigurationManager _cfg = default!;
|
||||
[Dependency] private ChatSystem _chatSystem = default!;
|
||||
[Dependency] private SharedAudioSystem _audio = default!;
|
||||
[Dependency] private StationSystem _stationSystem = default!;
|
||||
|
||||
// Until stations are a prototype, this is how it's going to have to be.
|
||||
public const string DefaultAlertLevelSet = "stationAlerts";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<StationInitializedEvent>(OnStationInitialize);
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
|
||||
}
|
||||
|
||||
public override void Update(float time)
|
||||
{
|
||||
var query = EntityQueryEnumerator<AlertLevelComponent>();
|
||||
|
||||
while (query.MoveNext(out var station, out var alert))
|
||||
{
|
||||
if (alert.CurrentDelay <= 0)
|
||||
{
|
||||
if (alert.ActiveDelay)
|
||||
{
|
||||
RaiseLocalEvent(new AlertLevelDelayFinishedEvent());
|
||||
alert.ActiveDelay = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
alert.CurrentDelay -= time;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStationInitialize(StationInitializedEvent args)
|
||||
{
|
||||
if (!TryComp<AlertLevelComponent>(args.Station, out var alertLevelComponent))
|
||||
return;
|
||||
|
||||
if (!ProtoMan.TryIndex(alertLevelComponent.AlertLevelPrototype, out AlertLevelPrototype? alerts))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
alertLevelComponent.AlertLevels = alerts;
|
||||
|
||||
var defaultLevel = alertLevelComponent.AlertLevels.DefaultLevel;
|
||||
if (string.IsNullOrEmpty(defaultLevel))
|
||||
{
|
||||
defaultLevel = alertLevelComponent.AlertLevels.Levels.Keys.First();
|
||||
}
|
||||
|
||||
SetLevel(args.Station, defaultLevel, false, false, true);
|
||||
}
|
||||
|
||||
private void OnPrototypeReload(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
if (!args.ByType.TryGetValue(typeof(AlertLevelPrototype), out var alertPrototypes)
|
||||
|| !alertPrototypes.Modified.TryGetValue(DefaultAlertLevelSet, out var alertObject)
|
||||
|| alertObject is not AlertLevelPrototype alerts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var query = EntityQueryEnumerator<AlertLevelComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
comp.AlertLevels = alerts;
|
||||
|
||||
if (!comp.AlertLevels.Levels.ContainsKey(comp.CurrentLevel))
|
||||
{
|
||||
var defaultLevel = comp.AlertLevels.DefaultLevel;
|
||||
if (string.IsNullOrEmpty(defaultLevel))
|
||||
{
|
||||
defaultLevel = comp.AlertLevels.Levels.Keys.First();
|
||||
}
|
||||
|
||||
SetLevel(uid, defaultLevel, true, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
RaiseLocalEvent(new AlertLevelPrototypeReloadedEvent());
|
||||
}
|
||||
|
||||
public string GetLevel(EntityUid station, AlertLevelComponent? alert = null)
|
||||
{
|
||||
if (!Resolve(station, ref alert))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return alert.CurrentLevel;
|
||||
}
|
||||
|
||||
public float GetAlertLevelDelay(EntityUid station, AlertLevelComponent? alert = null)
|
||||
{
|
||||
if (!Resolve(station, ref alert))
|
||||
{
|
||||
return float.NaN;
|
||||
}
|
||||
|
||||
return alert.CurrentDelay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the default alert level for a station entity.
|
||||
/// Returns an empty string if the station has no alert levels defined.
|
||||
/// </summary>
|
||||
/// <param name="station">The station entity.</param>
|
||||
public string GetDefaultLevel(Entity<AlertLevelComponent?> station)
|
||||
{
|
||||
if (!Resolve(station.Owner, ref station.Comp) || station.Comp.AlertLevels == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return station.Comp.AlertLevels.DefaultLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the alert level based on the station's entity ID.
|
||||
/// </summary>
|
||||
/// <param name="station">Station entity UID.</param>
|
||||
/// <param name="level">Level to change the station's alert level to.</param>
|
||||
/// <param name="playSound">Play the alert level's sound.</param>
|
||||
/// <param name="announce">Say the alert level's announcement.</param>
|
||||
/// <param name="force">Force the alert change. This applies if the alert level is not selectable or not.</param>
|
||||
/// <param name="locked">Will it be possible to change level by crew.</param>
|
||||
public void SetLevel(EntityUid station, string level, bool playSound, bool announce, bool force = false,
|
||||
bool locked = false, MetaDataComponent? dataComponent = null, AlertLevelComponent? component = null)
|
||||
{
|
||||
if (!Resolve(station, ref component, ref dataComponent)
|
||||
|| component.AlertLevels == null
|
||||
|| !component.AlertLevels.Levels.TryGetValue(level, out var detail)
|
||||
|| component.CurrentLevel == level)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force)
|
||||
{
|
||||
if (!detail.Selectable
|
||||
|| component.CurrentDelay > 0
|
||||
|| component.IsLevelLocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.CurrentDelay = _cfg.GetCVar(CCVars.GameAlertLevelChangeDelay);
|
||||
component.ActiveDelay = true;
|
||||
}
|
||||
|
||||
component.CurrentLevel = level;
|
||||
component.IsLevelLocked = locked;
|
||||
|
||||
var stationName = dataComponent.EntityName;
|
||||
|
||||
var name = level.ToLower();
|
||||
|
||||
if (Loc.TryGetString($"alert-level-{level}", out var locName))
|
||||
{
|
||||
name = locName.ToLower();
|
||||
}
|
||||
|
||||
// Announcement text. Is passed into announcementFull.
|
||||
var announcement = detail.Announcement;
|
||||
|
||||
if (Loc.TryGetString(detail.Announcement, out var locAnnouncement))
|
||||
{
|
||||
announcement = locAnnouncement;
|
||||
}
|
||||
|
||||
// The full announcement to be spat out into chat.
|
||||
var announcementFull = Loc.GetString("alert-level-announcement", ("name", name), ("announcement", announcement));
|
||||
|
||||
var playDefault = false;
|
||||
if (playSound)
|
||||
{
|
||||
if (detail.Sound != null)
|
||||
{
|
||||
var filter = _stationSystem.GetInOwningStation(station);
|
||||
_audio.PlayGlobal(detail.Sound, filter, true, detail.Sound.Params);
|
||||
}
|
||||
else
|
||||
{
|
||||
playDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (announce)
|
||||
{
|
||||
_chatSystem.DispatchStationAnnouncement(station, announcementFull, playDefaultSound: playDefault,
|
||||
colorOverride: detail.Color, sender: stationName);
|
||||
}
|
||||
|
||||
RaiseLocalEvent(new AlertLevelChangedEvent(station, level));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AlertLevelDelayFinishedEvent : EntityEventArgs
|
||||
{}
|
||||
|
||||
public sealed class AlertLevelPrototypeReloadedEvent : EntityEventArgs
|
||||
{}
|
||||
|
||||
public sealed class AlertLevelChangedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Station { get; }
|
||||
public string AlertLevel { get; }
|
||||
|
||||
public AlertLevelChangedEvent(EntityUid station, string alertLevel)
|
||||
{
|
||||
Station = station;
|
||||
AlertLevel = alertLevel;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +1,76 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.Station;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.AlertLevel.Commands
|
||||
namespace Content.Server.AlertLevel.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed partial class SetAlertLevelCommand : LocalizedEntityCommands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Fun)]
|
||||
public sealed partial class SetAlertLevelCommand : LocalizedEntityCommands
|
||||
[Dependency] private AlertLevelSystem _alertLevelSystem = default!;
|
||||
[Dependency] private SharedStationSystem _stationSystem = default!;
|
||||
[Dependency] private IPrototypeManager _prototype = default!;
|
||||
|
||||
public override string Command => "setalertlevel";
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
[Dependency] private AlertLevelSystem _alertLevelSystem = default!;
|
||||
[Dependency] private StationSystem _stationSystem = default!;
|
||||
|
||||
public override string Command => "setalertlevel";
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
return args.Length switch
|
||||
{
|
||||
var levelNames = new string[] {};
|
||||
var player = shell.Player;
|
||||
if (player?.AttachedEntity != null)
|
||||
{
|
||||
var stationUid = _stationSystem.GetOwningStation(player.AttachedEntity.Value);
|
||||
if (stationUid != null)
|
||||
levelNames = GetStationLevelNames(stationUid.Value);
|
||||
}
|
||||
1 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<AlertLevelPrototype>(),
|
||||
Loc.GetString("cmd-setalertlevel-hint-1")),
|
||||
2 => CompletionResult.FromHintOptions(CompletionHelper.Booleans,
|
||||
LocalizationManager.GetString("cmd-setalertlevel-hint-2")),
|
||||
_ => CompletionResult.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
return args.Length switch
|
||||
{
|
||||
1 => CompletionResult.FromHintOptions(levelNames,
|
||||
LocalizationManager.GetString("cmd-setalertlevel-hint-1")),
|
||||
2 => CompletionResult.FromHintOptions(CompletionHelper.Booleans,
|
||||
LocalizationManager.GetString("cmd-setalertlevel-hint-2")),
|
||||
_ => CompletionResult.Empty,
|
||||
};
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player;
|
||||
if (player?.AttachedEntity == null)
|
||||
{
|
||||
shell.WriteLine(LocalizationManager.GetString("shell-only-players-can-run-this-command"));
|
||||
return;
|
||||
}
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
if (args.Length < 1)
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
var locked = false;
|
||||
if (args.Length > 1 && !bool.TryParse(args[1], out locked))
|
||||
bool? locked = null;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (bool.TryParse(args[1], out var parsedBool))
|
||||
{
|
||||
locked = parsedBool;
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteLine(LocalizationManager.GetString("shell-argument-must-be-boolean"));
|
||||
return;
|
||||
}
|
||||
|
||||
var player = shell.Player;
|
||||
if (player?.AttachedEntity == null)
|
||||
{
|
||||
shell.WriteLine(LocalizationManager.GetString("shell-only-players-can-run-this-command"));
|
||||
return;
|
||||
}
|
||||
|
||||
var stationUid = _stationSystem.GetOwningStation(player.AttachedEntity.Value);
|
||||
if (stationUid == null)
|
||||
{
|
||||
shell.WriteLine(LocalizationManager.GetString("cmd-setalertlevel-invalid-grid"));
|
||||
return;
|
||||
}
|
||||
|
||||
var level = args[0];
|
||||
var levelNames = GetStationLevelNames(stationUid.Value);
|
||||
if (!levelNames.Contains(level))
|
||||
{
|
||||
shell.WriteLine(LocalizationManager.GetString("cmd-setalertlevel-invalid-level"));
|
||||
return;
|
||||
}
|
||||
|
||||
_alertLevelSystem.SetLevel(stationUid.Value, level, true, true, true, locked);
|
||||
}
|
||||
|
||||
private string[] GetStationLevelNames(EntityUid station)
|
||||
var stationUid = _stationSystem.GetOwningStation(player.AttachedEntity.Value);
|
||||
if (stationUid == null)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent<AlertLevelComponent>(station, out var alertLevelComp))
|
||||
return new string[]{};
|
||||
|
||||
if (alertLevelComp.AlertLevels == null)
|
||||
return new string[]{};
|
||||
|
||||
return alertLevelComp.AlertLevels.Levels.Keys.ToArray();
|
||||
shell.WriteLine(LocalizationManager.GetString("cmd-setalertlevel-invalid-grid"));
|
||||
return;
|
||||
}
|
||||
|
||||
var level = args[0];
|
||||
if (!_prototype.HasIndex<AlertLevelPrototype>(level))
|
||||
{
|
||||
shell.WriteLine(LocalizationManager.GetString("cmd-setalertlevel-invalid-level"));
|
||||
return;
|
||||
}
|
||||
|
||||
_alertLevelSystem.SetLevel(stationUid.Value, level, force: true, locked: locked);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
namespace Content.Server.Atmos.Components
|
||||
using Content.Shared.Temperature.Components;
|
||||
|
||||
namespace Content.Server.Atmos.Components;
|
||||
|
||||
// not if i get there first - Flipp
|
||||
/// <summary>
|
||||
/// Represents that entity can be exposed to Atmos
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class AtmosExposedComponent : Component
|
||||
{
|
||||
// not if i get there first - Flipp
|
||||
/// <summary>
|
||||
/// Represents that entity can be exposed to Atmos
|
||||
/// Last real time we were exposed to the atmosphere!
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AtmosExposedComponent : Component
|
||||
{
|
||||
}
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan LastExposure = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of surface area this entity has exposed to the atmosphere in m^2.
|
||||
/// The human body has about 2m^2 of surface area so we use that as a default.
|
||||
/// This is multiplied by the <see cref="TemperatureComponent.ThermalConductivity"/>
|
||||
/// Source: https://pmc.ncbi.nlm.nih.gov/articles/PMC8953946/
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ExposedArea = 2f;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
|
|||
[Dependency] private EntityQuery<FirelockComponent> _firelockQuery = default!;
|
||||
|
||||
private const float ExposedUpdateDelay = 1f;
|
||||
private float _exposedTimer = 0f;
|
||||
|
||||
private HashSet<EntityUid> _entSet = new();
|
||||
|
||||
|
|
@ -103,24 +102,23 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
|
|||
UpdateProcessing(frameTime);
|
||||
UpdateHighPressure(frameTime);
|
||||
|
||||
_exposedTimer += frameTime;
|
||||
|
||||
if (_exposedTimer < ExposedUpdateDelay)
|
||||
return;
|
||||
var delay = TimeSpan.FromSeconds(ExposedUpdateDelay);
|
||||
|
||||
var query = EntityQueryEnumerator<AtmosExposedComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var transform))
|
||||
while (query.MoveNext(out var uid, out var exposed, out var transform))
|
||||
{
|
||||
if (exposed.LastExposure + delay > _gameTiming.CurTime)
|
||||
continue;
|
||||
|
||||
var air = GetContainingMixture((uid, transform));
|
||||
|
||||
if (air == null)
|
||||
continue;
|
||||
|
||||
var updateEvent = new AtmosExposedUpdateEvent(transform.Coordinates, air, transform);
|
||||
var updateEvent = new AtmosExposedUpdateEvent(transform.Coordinates, air, transform, ExposedUpdateDelay, exposed.ExposedArea);
|
||||
RaiseLocalEvent(uid, ref updateEvent);
|
||||
exposed.LastExposure = _gameTiming.CurTime;
|
||||
}
|
||||
|
||||
_exposedTimer -= ExposedUpdateDelay;
|
||||
}
|
||||
|
||||
private void CacheDecals()
|
||||
|
|
|
|||
|
|
@ -453,8 +453,7 @@ namespace Content.Server.Atmos.EntitySystems
|
|||
var source = EnsureComp<IgnitionSourceComponent>(uid);
|
||||
_ignitionSourceSystem.SetIgnited((uid, source));
|
||||
|
||||
if (TryComp(uid, out TemperatureComponent? temp))
|
||||
_temperatureSystem.ChangeHeat(uid, 12500 * flammable.FireStacks, false, temp);
|
||||
_temperatureSystem.ChangeHeat(uid, 12500 * flammable.FireStacks, false);
|
||||
|
||||
var ev = new GetFireProtectionEvent();
|
||||
// let the thing on fire handle it
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ public sealed partial class RottingSystem : SharedRottingSystem
|
|||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
args.Handled = component.CurrentTemperature < Atmospherics.T0C + 0.85f;
|
||||
args.Handled = component.Temperature < Atmospherics.T0C + 0.85f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
42
Content.Server/Body/BodyCommand.cs
Normal file
42
Content.Server/Body/BodyCommand.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Body;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Toolshed;
|
||||
|
||||
namespace Content.Server.Body;
|
||||
|
||||
[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class BodyCommand : ToolshedCommand
|
||||
{
|
||||
private SharedContainerSystem? _container;
|
||||
|
||||
[CommandImplementation("insert")]
|
||||
public void Insert([PipedArgument] EntityUid body, EntityUid organ)
|
||||
{
|
||||
_container ??= GetSys<SharedContainerSystem>();
|
||||
|
||||
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
|
||||
return;
|
||||
|
||||
_container.Insert(organ, container, force: true);
|
||||
}
|
||||
|
||||
[CommandImplementation("organs")]
|
||||
public IEnumerable<EntityUid> Organs([PipedArgument] EntityUid body)
|
||||
{
|
||||
_container ??= GetSys<SharedContainerSystem>();
|
||||
|
||||
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
|
||||
return Enumerable.Empty<EntityUid>();
|
||||
|
||||
return container.ContainedEntities.ToList();
|
||||
}
|
||||
|
||||
[CommandImplementation("organs")]
|
||||
public IEnumerable<EntityUid> Organs([PipedArgument] IEnumerable<EntityUid> body)
|
||||
{
|
||||
return body.SelectMany(Organs);
|
||||
}
|
||||
}
|
||||
112
Content.Server/Body/OrganCommand.cs
Normal file
112
Content.Server/Body/OrganCommand.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Body;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Toolshed;
|
||||
|
||||
namespace Content.Server.Body;
|
||||
|
||||
[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class OrganCommand : ToolshedCommand
|
||||
{
|
||||
private DetachableOrganSystem? _detachableOrgan;
|
||||
private SharedContainerSystem? _container;
|
||||
private OrganRelationSystem? _organRelation;
|
||||
|
||||
[CommandImplementation("parent")]
|
||||
public EntityUid? Parent([PipedArgument] EntityUid child)
|
||||
{
|
||||
if (!TryComp<ChildOrganComponent>(child, out var childOrgan))
|
||||
return null;
|
||||
|
||||
return childOrgan.Parent;
|
||||
}
|
||||
|
||||
[CommandImplementation("parent")]
|
||||
public IEnumerable<EntityUid?> Parent([PipedArgument] IEnumerable<EntityUid> children)
|
||||
{
|
||||
return children.Select(Parent);
|
||||
}
|
||||
|
||||
[CommandImplementation("children")]
|
||||
public IEnumerable<EntityUid> Children([PipedArgument] EntityUid parent)
|
||||
{
|
||||
if (!TryComp<ParentOrganComponent>(parent, out var parentOrgan))
|
||||
yield break;
|
||||
|
||||
foreach (var child in parentOrgan.Children)
|
||||
{
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
|
||||
[CommandImplementation("children")]
|
||||
public IEnumerable<EntityUid> Children([PipedArgument] IEnumerable<EntityUid> parents)
|
||||
{
|
||||
return parents.SelectMany(Children);
|
||||
}
|
||||
|
||||
[CommandImplementation("detach")]
|
||||
public EntityUid? Detach([PipedArgument] EntityUid organ)
|
||||
{
|
||||
_detachableOrgan ??= GetSys<DetachableOrganSystem>();
|
||||
return _detachableOrgan.Detach(organ);
|
||||
}
|
||||
|
||||
[CommandImplementation("detach")]
|
||||
public IEnumerable<EntityUid?> Detach([PipedArgument] IEnumerable<EntityUid> organs)
|
||||
{
|
||||
return organs.Select(Detach);
|
||||
}
|
||||
|
||||
[CommandImplementation("attach")]
|
||||
public void Attach([PipedArgument] EntityUid parent, EntityUid child)
|
||||
{
|
||||
_container ??= GetSys<SharedContainerSystem>();
|
||||
_organRelation ??= GetSys<OrganRelationSystem>();
|
||||
|
||||
if (!TryComp<ParentOrganComponent>(parent, out var parentOrgan) ||
|
||||
!TryComp<ChildOrganComponent>(child, out var childOrgan) ||
|
||||
!TryComp<OrganComponent>(parent, out var parentOrganComponent) ||
|
||||
parentOrganComponent.Body is not { } body)
|
||||
return;
|
||||
|
||||
if (!_container.TryGetContainer(body, BodyComponent.ContainerID, out var container))
|
||||
return;
|
||||
|
||||
_container.Insert(child, container, force: true);
|
||||
_organRelation.Relate((parent, parentOrgan), (child, childOrgan));
|
||||
}
|
||||
|
||||
[CommandImplementation("attach")]
|
||||
public void Attach([PipedArgument] IEnumerable<EntityUid> parents, EntityUid child)
|
||||
{
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
Attach(parent, child);
|
||||
}
|
||||
}
|
||||
|
||||
[CommandImplementation("is")]
|
||||
public bool Is([PipedArgument] EntityUid organ, ProtoId<OrganCategoryPrototype> category)
|
||||
{
|
||||
if (!TryComp<OrganComponent>(organ, out var organComp))
|
||||
return false;
|
||||
|
||||
return organComp.Category == category;
|
||||
}
|
||||
|
||||
[CommandImplementation("is")]
|
||||
public IEnumerable<bool> Is([PipedArgument] IEnumerable<EntityUid> organs, ProtoId<OrganCategoryPrototype> category)
|
||||
{
|
||||
return organs.Select(it => Is(it, category));
|
||||
}
|
||||
|
||||
[CommandImplementation("of_type")]
|
||||
public IEnumerable<EntityUid> Typed([PipedArgument] IEnumerable<EntityUid> organs, ProtoId<OrganCategoryPrototype> category)
|
||||
{
|
||||
return organs.Where(it => Is(it, category));
|
||||
}
|
||||
}
|
||||
|
|
@ -55,10 +55,10 @@ public sealed partial class ThermalRegulatorSystem : EntitySystem
|
|||
var totalMetabolismTempChange = ent.Comp1.MetabolismHeat - ent.Comp1.RadiatedHeat;
|
||||
|
||||
// implicit heat regulation
|
||||
var tempDiff = Math.Abs(ent.Comp2.CurrentTemperature - ent.Comp1.NormalBodyTemperature);
|
||||
var heatCapacity = _tempSys.GetHeatCapacity(ent, ent);
|
||||
var tempDiff = Math.Abs(ent.Comp2.Temperature - ent.Comp1.NormalBodyTemperature);
|
||||
var heatCapacity = ent.Comp2.HeatCapacity;
|
||||
var targetHeat = tempDiff * heatCapacity;
|
||||
if (ent.Comp2.CurrentTemperature > ent.Comp1.NormalBodyTemperature)
|
||||
if (ent.Comp2.Temperature > ent.Comp1.NormalBodyTemperature)
|
||||
{
|
||||
totalMetabolismTempChange -= Math.Min(targetHeat, ent.Comp1.ImplicitHeatRegulation);
|
||||
}
|
||||
|
|
@ -67,10 +67,10 @@ public sealed partial class ThermalRegulatorSystem : EntitySystem
|
|||
totalMetabolismTempChange += Math.Min(targetHeat, ent.Comp1.ImplicitHeatRegulation);
|
||||
}
|
||||
|
||||
_tempSys.ChangeHeat(ent, totalMetabolismTempChange, ignoreHeatResistance: true, ent);
|
||||
_tempSys.ChangeHeat((ent, ent.Comp2), totalMetabolismTempChange, ignoreHeatResistance: true);
|
||||
|
||||
// recalc difference and target heat
|
||||
tempDiff = Math.Abs(ent.Comp2.CurrentTemperature - ent.Comp1.NormalBodyTemperature);
|
||||
tempDiff = Math.Abs(ent.Comp2.Temperature - ent.Comp1.NormalBodyTemperature);
|
||||
targetHeat = tempDiff * heatCapacity;
|
||||
|
||||
// if body temperature is not within comfortable, thermal regulation
|
||||
|
|
@ -78,19 +78,19 @@ public sealed partial class ThermalRegulatorSystem : EntitySystem
|
|||
if (tempDiff < ent.Comp1.ThermalRegulationTemperatureThreshold)
|
||||
return;
|
||||
|
||||
if (ent.Comp2.CurrentTemperature > ent.Comp1.NormalBodyTemperature)
|
||||
if (ent.Comp2.Temperature > ent.Comp1.NormalBodyTemperature)
|
||||
{
|
||||
if (!_actionBlockerSys.CanSweat(ent))
|
||||
return;
|
||||
|
||||
_tempSys.ChangeHeat(ent, -Math.Min(targetHeat, ent.Comp1.SweatHeatRegulation), ignoreHeatResistance: true, ent);
|
||||
_tempSys.ChangeHeat((ent, ent.Comp2), -Math.Min(targetHeat, ent.Comp1.SweatHeatRegulation), ignoreHeatResistance: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_actionBlockerSys.CanShiver(ent))
|
||||
return;
|
||||
|
||||
_tempSys.ChangeHeat(ent, Math.Min(targetHeat, ent.Comp1.ShiveringHeatRegulation), ignoreHeatResistance: true, ent);
|
||||
_tempSys.ChangeHeat((ent, ent.Comp2), Math.Min(targetHeat, ent.Comp1.ShiveringHeatRegulation), ignoreHeatResistance: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using Content.Server.GameTicking;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Suicide;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Speech.Prototypes;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Speech.Prototypes;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
|
|||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration;
|
||||
|
|
@ -13,6 +12,7 @@ using Content.Shared.Examine;
|
|||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Players.RateLimiting;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Content.Server.Forensics;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Cloning.Events;
|
||||
|
|
@ -15,6 +14,7 @@ using Content.Shared.Movement.Pulling.Systems;
|
|||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Paper;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Store;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
using Content.Server.Chat;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Emoting.Systems;
|
||||
using Content.Server.Clothing.Systems;
|
||||
using Content.Server.Emoting.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Clumsy;
|
||||
|
|
@ -13,6 +12,7 @@ using Content.Shared.IdentityManagement;
|
|||
using Content.Shared.Mobs;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Random;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.AlertLevel;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Popups;
|
||||
|
|
@ -9,6 +8,7 @@ using Content.Server.Shuttles.Systems;
|
|||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Communications;
|
||||
|
|
@ -19,6 +19,7 @@ using Content.Shared.IdentityManagement;
|
|||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Communications
|
||||
{
|
||||
|
|
@ -44,7 +45,7 @@ namespace Content.Server.Communications
|
|||
// All events that refresh the BUI
|
||||
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
|
||||
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
|
||||
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent());
|
||||
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>((ref AlertLevelDelayFinishedEvent ev) => OnGenericBroadcastEvent());
|
||||
|
||||
// Messages from the BUI
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleSelectAlertLevelMessage>(OnSelectAlertLevelMessage);
|
||||
|
|
@ -104,7 +105,7 @@ namespace Content.Server.Communications
|
|||
/// Updates all comms consoles belonging to the station that the alert level was set on
|
||||
/// </summary>
|
||||
/// <param name="args">Alert level changed event arguments</param>
|
||||
private void OnAlertLevelChanged(AlertLevelChangedEvent args)
|
||||
private void OnAlertLevelChanged(ref AlertLevelChangedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<CommunicationsConsoleComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
|
|
@ -132,39 +133,10 @@ namespace Content.Server.Communications
|
|||
/// </summary>
|
||||
public void UpdateCommsConsoleInterface(EntityUid uid, CommunicationsConsoleComponent comp)
|
||||
{
|
||||
var stationUid = _stationSystem.GetOwningStation(uid);
|
||||
List<string>? levels = null;
|
||||
string currentLevel = default!;
|
||||
float currentDelay = 0;
|
||||
|
||||
if (stationUid != null)
|
||||
{
|
||||
if (TryComp(stationUid.Value, out AlertLevelComponent? alertComp) &&
|
||||
alertComp.AlertLevels != null)
|
||||
{
|
||||
if (alertComp.IsSelectable)
|
||||
{
|
||||
levels = new();
|
||||
foreach (var (id, detail) in alertComp.AlertLevels.Levels)
|
||||
{
|
||||
if (detail.Selectable)
|
||||
{
|
||||
levels.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentLevel = alertComp.CurrentLevel;
|
||||
currentDelay = _alertLevelSystem.GetAlertLevelDelay(stationUid.Value, alertComp);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use component states and predict the UI
|
||||
_uiSystem.SetUiState(uid, CommunicationsConsoleUiKey.Key, new CommunicationsConsoleInterfaceState(
|
||||
CanAnnounce(comp),
|
||||
CanCallOrRecall(comp),
|
||||
levels,
|
||||
currentLevel,
|
||||
currentDelay,
|
||||
_roundEndSystem.ExpectedCountdownEnd
|
||||
));
|
||||
}
|
||||
|
|
@ -222,7 +194,7 @@ namespace Content.Server.Communications
|
|||
var stationUid = _stationSystem.GetOwningStation(uid);
|
||||
if (stationUid != null)
|
||||
{
|
||||
_alertLevelSystem.SetLevel(stationUid.Value, message.Level, true, true);
|
||||
_alertLevelSystem.SetLevel(stationUid.Value, message.Level);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ namespace Content.Server.Construction
|
|||
SubscribeLocalEvent<ConstructionComponent, InteractUsingEvent>(EnqueueEvent,
|
||||
new []{typeof(AnchorableSystem), typeof(PryingSystem), typeof(WeldableSystem)},
|
||||
new []{typeof(EncryptionKeySystem)});
|
||||
SubscribeLocalEvent<ConstructionComponent, OnTemperatureChangeEvent>(EnqueueEvent);
|
||||
SubscribeLocalEvent<ConstructionComponent, TemperatureChangedEvent>(EnqueueRefEvent);
|
||||
SubscribeLocalEvent<ConstructionComponent, PartAssemblyPartInsertedEvent>(EnqueueEvent);
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +382,7 @@ namespace Content.Server.Construction
|
|||
|
||||
case TemperatureConstructionGraphStep temperatureChangeStep:
|
||||
{
|
||||
if (ev is not OnTemperatureChangeEvent)
|
||||
if (ev is not TemperatureChangedEvent)
|
||||
break;
|
||||
|
||||
// Some things, like microwaves, might need to block the temperature construction step from kicking in, or override it entirely.
|
||||
|
|
@ -400,7 +400,7 @@ namespace Content.Server.Construction
|
|||
}
|
||||
else if (TryComp<TemperatureComponent>(uid, out var tempComp))
|
||||
{
|
||||
temp = tempComp.CurrentTemperature;
|
||||
temp = tempComp.Temperature;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -544,6 +544,13 @@ namespace Content.Server.Construction
|
|||
|
||||
#region Event Handlers
|
||||
|
||||
// Why does this system have you subscribe to an event,
|
||||
// and then pass it through 5 layers of bullshit into a switch statement which tries to guess what event got passed?
|
||||
private void EnqueueRefEvent<T>(Entity<ConstructionComponent> entity, ref T args) where T : struct
|
||||
{
|
||||
EnqueueEvent(entity, entity.Comp, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a directed event to be handled by construction on the next update tick.
|
||||
/// Used as a handler for any events that construction can listen to. <seealso cref="InitializeInteractions"/>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
using System.Numerics;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Spawners.EntitySystems;
|
||||
using Content.Server.Stack;
|
||||
using Content.Shared.Destructible.Thresholds;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Stacks;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Spawners;
|
||||
|
||||
namespace Content.Server.Destructible.Thresholds.Behaviors
|
||||
{
|
||||
|
|
@ -16,15 +14,13 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||
[DataDefinition]
|
||||
public sealed partial class SpawnEntitiesBehavior : IThresholdBehavior
|
||||
{
|
||||
private static readonly EntProtoId TempEntityProtoId = "TemporaryEntityForTimedDespawnSpawners";
|
||||
|
||||
/// <summary>
|
||||
/// Entities spawned on reaching this threshold, from a min to a max.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<EntProtoId, MinMax> Spawn = [];
|
||||
public Dictionary<EntProtoId, MinMax> Spawn = new();
|
||||
|
||||
[DataField]
|
||||
[DataField("offset")]
|
||||
public float Offset { get; set; } = 0.5f;
|
||||
|
||||
[DataField("transferForensics")]
|
||||
|
|
@ -33,21 +29,12 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||
[DataField]
|
||||
public bool SpawnInContainer;
|
||||
|
||||
/// <summary>
|
||||
/// Time in seconds to wait before spawning entities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If positive, <see cref="DoTransferForensics"/> and <see cref="SpawnInContainer"/> are ignored.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public float SpawnAfter;
|
||||
|
||||
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
|
||||
{
|
||||
var tSys = system.EntityManager.System<TransformSystem>();
|
||||
var position = tSys.GetMapCoordinates(owner);
|
||||
|
||||
Vector2 GetRandomVector() => system.Random.NextVector2(Offset);
|
||||
var getRandomVector = () => new Vector2(system.Random.NextFloat(-Offset, Offset), system.Random.NextFloat(-Offset, Offset));
|
||||
|
||||
var executions = 1;
|
||||
if (system.EntityManager.TryGetComponent<StackComponent>(owner, out var stack))
|
||||
|
|
@ -66,23 +53,11 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||
if (count == 0)
|
||||
continue;
|
||||
|
||||
if (SpawnAfter > 0)
|
||||
{
|
||||
// TODO: TransferForensics is not supported here because the actual entity UID is not known until the spawner spawns it.
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var spawner = system.EntityManager.SpawnEntity(TempEntityProtoId, position.Offset(GetRandomVector()));
|
||||
var timedDespawn = system.EntityManager.GetComponent<TimedDespawnComponent>(spawner);
|
||||
timedDespawn.Lifetime = SpawnAfter;
|
||||
var spawnOnDespawn = system.EntityManager.GetComponent<SpawnOnDespawnComponent>(spawner);
|
||||
system.EntityManager.System<SpawnOnDespawnSystem>().SetPrototype((spawner, spawnOnDespawn), entityId);
|
||||
}
|
||||
}
|
||||
else if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory))
|
||||
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory))
|
||||
{
|
||||
var spawned = SpawnInContainer
|
||||
? system.EntityManager.SpawnNextToOrDrop(entityId, owner)
|
||||
: system.EntityManager.SpawnEntity(entityId, position.Offset(GetRandomVector()));
|
||||
: system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
|
||||
system.StackSystem.SetCount((spawned, null), count);
|
||||
|
||||
TransferForensics(spawned, system, owner);
|
||||
|
|
@ -93,7 +68,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
|
|||
{
|
||||
var spawned = SpawnInContainer
|
||||
? system.EntityManager.SpawnNextToOrDrop(entityId, owner)
|
||||
: system.EntityManager.SpawnEntity(entityId, position.Offset(GetRandomVector()));
|
||||
: system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
|
||||
|
||||
TransferForensics(spawned, system, owner);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public sealed partial class ElectrocutionSystem : SharedElectrocutionSystem
|
|||
[Dependency] private SharedJitteringSystem _jittering = default!;
|
||||
[Dependency] private SharedPopupSystem _popup = default!;
|
||||
[Dependency] private SharedStunSystem _stun = default!;
|
||||
[Dependency] private SharedStutteringSystem _stuttering = default!;
|
||||
[Dependency] private StutteringSystem _stuttering = default!;
|
||||
[Dependency] private TagSystem _tag = default!;
|
||||
[Dependency] private MetaDataSystem _metaData = default!;
|
||||
[Dependency] private TurfSystem _turf = default!;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.EntityEffects.Effects;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ using Content.Server.Administration.Managers;
|
|||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Players;
|
||||
|
|
|
|||
|
|
@ -1,45 +1,42 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Temperature.Systems;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Construction.EntitySystems;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceLinking.Events;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DeviceLinking.Events;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Kitchen.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Suicide;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Temperature.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
|
||||
namespace Content.Server.Kitchen.EntitySystems
|
||||
{
|
||||
|
|
@ -179,8 +176,7 @@ namespace Content.Server.Kitchen.EntitySystems
|
|||
var heatToAdd = time * component.BaseHeatMultiplier;
|
||||
foreach (var entity in component.Storage.ContainedEntities)
|
||||
{
|
||||
if (TryComp<TemperatureComponent>(entity, out var tempComp))
|
||||
_temperature.ChangeHeat(entity, heatToAdd * component.ObjectHeatMultiplier, false, tempComp);
|
||||
_temperature.ChangeHeat(entity, heatToAdd * component.ObjectHeatMultiplier, false);
|
||||
|
||||
foreach (var (_, soln) in _solutionContainer.EnumerateSolutions(entity))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using Content.Server.AlertLevel;
|
||||
using Content.Server.Audio;
|
||||
using Content.Server.Light.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Light.Components;
|
||||
|
|
@ -18,6 +18,7 @@ namespace Content.Server.Light.EntitySystems;
|
|||
public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
||||
{
|
||||
[Dependency] private AmbientSoundSystem _ambient = default!;
|
||||
[Dependency] private AlertLevelSystem _alert = default!;
|
||||
[Dependency] private BatterySystem _battery = default!;
|
||||
[Dependency] private PointLightSystem _pointLight = default!;
|
||||
[Dependency] private SharedAppearanceSystem _appearance = default!;
|
||||
|
|
@ -57,22 +58,17 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
|||
Loc.GetString(component.BatteryStateText[component.State]))));
|
||||
|
||||
// Show alert level on the light itself.
|
||||
if (!TryComp<AlertLevelComponent>(_station.GetOwningStation(uid), out var alerts))
|
||||
if (_station.GetOwningStation(uid) is not { } station)
|
||||
return;
|
||||
|
||||
if (alerts.AlertLevels == null)
|
||||
if (!_alert.TryGetLevel(station, out var level)
|
||||
|| !ProtoMan.Resolve(level, out var proto))
|
||||
return;
|
||||
|
||||
var name = alerts.CurrentLevel;
|
||||
|
||||
var color = Color.White;
|
||||
if (alerts.AlertLevels.Levels.TryGetValue(alerts.CurrentLevel, out var details))
|
||||
color = details.Color;
|
||||
|
||||
args.PushMarkup(
|
||||
Loc.GetString("emergency-light-component-on-examine-alert",
|
||||
("color", color.ToHex()),
|
||||
("level", Loc.GetString($"alert-level-{name.ToString().ToLower()}"))));
|
||||
("color", proto.Color.ToHex()),
|
||||
("level", proto.LocalizedName)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,12 +89,9 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
|||
}
|
||||
}
|
||||
|
||||
private void OnAlertLevelChanged(AlertLevelChangedEvent ev)
|
||||
private void OnAlertLevelChanged(ref AlertLevelChangedEvent ev)
|
||||
{
|
||||
if (!TryComp<AlertLevelComponent>(ev.Station, out var alert))
|
||||
return;
|
||||
|
||||
if (alert.AlertLevels == null || !alert.AlertLevels.Levels.TryGetValue(ev.AlertLevel, out var details))
|
||||
if (!ProtoMan.Resolve(ev.AlertLevel, out var level))
|
||||
return;
|
||||
|
||||
var query = EntityQueryEnumerator<EmergencyLightComponent, PointLightComponent, AppearanceComponent, TransformComponent>();
|
||||
|
|
@ -107,15 +100,15 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
|||
if (CompOrNull<StationMemberComponent>(xform.GridUid)?.Station != ev.Station)
|
||||
continue;
|
||||
|
||||
_pointLight.SetColor(uid, details.EmergencyLightColor, pointLight);
|
||||
_appearance.SetData(uid, EmergencyLightVisuals.Color, details.EmergencyLightColor, appearance);
|
||||
_pointLight.SetColor(uid, level.EmergencyLightColor, pointLight);
|
||||
_appearance.SetData(uid, EmergencyLightVisuals.Color, level.EmergencyLightColor, appearance);
|
||||
|
||||
if (details.ForceEnableEmergencyLights && !light.ForciblyEnabled)
|
||||
if (level.ForceEnableEmergencyLights && !light.ForciblyEnabled)
|
||||
{
|
||||
light.ForciblyEnabled = true;
|
||||
TurnOn((uid, light));
|
||||
}
|
||||
else if (!details.ForceEnableEmergencyLights && light.ForciblyEnabled)
|
||||
else if (!level.ForceEnableEmergencyLights && light.ForciblyEnabled)
|
||||
{
|
||||
// Previously forcibly enabled, and we went down an alert level.
|
||||
light.ForciblyEnabled = false;
|
||||
|
|
@ -174,10 +167,10 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
|||
if (!TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver))
|
||||
return;
|
||||
|
||||
if (!TryComp<AlertLevelComponent>(_station.GetOwningStation(entity.Owner), out var alerts))
|
||||
return;
|
||||
|
||||
if (alerts.AlertLevels == null || !alerts.AlertLevels.Levels.TryGetValue(alerts.CurrentLevel, out var details))
|
||||
// Show alert level on the light itself.
|
||||
if (_station.GetOwningStation(entity.Owner) is not { } station
|
||||
|| !_alert.TryGetLevel(station, out var level)
|
||||
|| !ProtoMan.Resolve(level, out var proto))
|
||||
{
|
||||
TurnOff(entity, Color.Red); // if no alert, default to off red state
|
||||
return;
|
||||
|
|
@ -185,8 +178,8 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
|||
|
||||
if (receiver.Powered && !entity.Comp.ForciblyEnabled) // Green alert
|
||||
{
|
||||
receiver.Load = (int) Math.Abs(entity.Comp.Wattage);
|
||||
TurnOff(entity, details.Color);
|
||||
receiver.Load = (int)Math.Abs(entity.Comp.Wattage);
|
||||
TurnOff(entity, proto.EmergencyLightColor);
|
||||
SetState(entity.Owner, entity.Comp, EmergencyLightState.Charging);
|
||||
}
|
||||
else if (!receiver.Powered) // If internal battery runs out it will end in off red state
|
||||
|
|
@ -196,7 +189,7 @@ public sealed partial class EmergencyLightSystem : SharedEmergencyLightSystem
|
|||
}
|
||||
else // Powered and enabled
|
||||
{
|
||||
TurnOn(entity, details.Color);
|
||||
TurnOn(entity, proto.EmergencyLightColor);
|
||||
SetState(entity.Owner, entity.Comp, EmergencyLightState.On);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ public sealed partial class HealthAnalyzerSystem : EntitySystem
|
|||
var bodyTemperature = float.NaN;
|
||||
|
||||
if (TryComp<TemperatureComponent>(entity, out var temp))
|
||||
bodyTemperature = temp.CurrentTemperature;
|
||||
bodyTemperature = temp.Temperature;
|
||||
|
||||
var bloodAmount = float.NaN;
|
||||
var bleeding = false;
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ public sealed partial class NPCUtilitySystem : EntitySystem
|
|||
if (!TryComp<TemperatureComponent>(targetUid, out var temperature))
|
||||
return 0f;
|
||||
|
||||
return temperature.CurrentTemperature <= con.MinTemp ? 1f : 0f;
|
||||
return temperature.Temperature <= con.MinTemp ? 1f : 0f;
|
||||
}
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Content.Server.AlertLevel;
|
||||
using Content.Server.Audio;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
|
|
@ -6,6 +5,7 @@ using Content.Server.Pinpointer;
|
|||
using Content.Server.Popups;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Coordinates.Helpers;
|
||||
using Content.Shared.DoAfter;
|
||||
|
|
@ -489,7 +489,7 @@ public sealed partial class NukeSystem : EntitySystem
|
|||
// let people know that a nuclear bomb was armed in their vicinity instead.
|
||||
// Otherwise, you could set every station to whatever AlertLevelOnActivate is.
|
||||
if (stationUid != null)
|
||||
_alertLevel.SetLevel(stationUid.Value, component.AlertLevelOnActivate, true, true, true, true);
|
||||
_alertLevel.SetLevel(stationUid.Value, component.AlertLevelOnActivate, true, true, true);
|
||||
|
||||
var pos = _transform.GetMapCoordinates(uid, xform: nukeXform);
|
||||
var x = (int) pos.X;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.AlertLevel;
|
||||
using Content.Server.CartridgeLoader;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Instruments;
|
||||
|
|
@ -10,6 +9,7 @@ using Content.Server.Station.Systems;
|
|||
using Content.Server.Store.Systems;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
|
|
@ -25,6 +25,7 @@ using Robust.Server.Containers;
|
|||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.PDA
|
||||
|
|
@ -41,6 +42,7 @@ namespace Content.Server.PDA
|
|||
[Dependency] private UnpoweredFlashlightSystem _unpoweredFlashlight = default!;
|
||||
[Dependency] private ContainerSystem _containerSystem = default!;
|
||||
[Dependency] private IdCardSystem _idCard = default!;
|
||||
[Dependency] private IPrototypeManager _prototype = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -143,7 +145,7 @@ namespace Content.Server.PDA
|
|||
UpdateAllPdaUisOnStation();
|
||||
}
|
||||
|
||||
private void OnAlertLevelChanged(AlertLevelChangedEvent args)
|
||||
private void OnAlertLevelChanged(ref AlertLevelChangedEvent args)
|
||||
{
|
||||
UpdateAllPdaUisOnStation();
|
||||
}
|
||||
|
|
@ -328,12 +330,11 @@ namespace Content.Server.PDA
|
|||
private void UpdateAlertLevel(EntityUid uid, PdaComponent pda)
|
||||
{
|
||||
var station = _station.GetOwningStation(uid);
|
||||
if (!TryComp(station, out AlertLevelComponent? alertComp) ||
|
||||
alertComp.AlertLevels == null)
|
||||
if (!TryComp(station, out AlertLevelComponent? alertComp))
|
||||
return;
|
||||
pda.StationAlertLevel = alertComp.CurrentLevel;
|
||||
if (alertComp.AlertLevels.Levels.TryGetValue(alertComp.CurrentLevel, out var details))
|
||||
pda.StationAlertColor = details.Color;
|
||||
pda.StationAlertLevel = alertComp.CurrentAlertLevel;
|
||||
if (_prototype.Resolve(alertComp.CurrentAlertLevel, out var level))
|
||||
pda.StationAlertColor = level.Color;
|
||||
}
|
||||
|
||||
private string? GetDeviceNetAddress(EntityUid uid)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Threading;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.AlertLevel;
|
||||
using Content.Shared.AlertLevel;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
|
|
@ -146,11 +146,11 @@ namespace Content.Server.RoundEnd
|
|||
if (requester != null)
|
||||
{
|
||||
var stationUid = _stationSystem.GetOwningStation(requester.Value);
|
||||
if (TryComp<AlertLevelComponent>(stationUid, out var alertLevel))
|
||||
if (TryComp<AlertLevelComponent>(stationUid, out var alertLevelComp))
|
||||
{
|
||||
duration = ProtoMan
|
||||
.Index<AlertLevelPrototype>(AlertLevelSystem.DefaultAlertLevelSet)
|
||||
.Levels[alertLevel.CurrentLevel].ShuttleTime;
|
||||
.Index(alertLevelComp.CurrentAlertLevel)
|
||||
.ShuttleTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class BackwardsAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class BarkAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Makes this entity speak like a sheep or a goat in all chat messages it sends.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class BleatingAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Causes all ListenAttemptEvents to fail on the entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class BlockListeningComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class FrontalLispComponent : BaseAccentComponent;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class GermanAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Hiss!
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class LizardAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Nyehh, my gabagool, see?
|
||||
/// Etc etc.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MobsterAccentComponent : BaseAccentComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Do you make all the rules?
|
||||
/// </summary>
|
||||
[DataField("isBoss")]
|
||||
public bool IsBoss = true;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class MonkeyAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Buzzz!
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MothAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class OwOAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using Content.Server.Speech.Prototypes;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Replaces full sentences or words within sentences with new strings.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ReplacementAccentComponent : BaseAccentComponent
|
||||
{
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<ReplacementAccentPrototype>), required: true)]
|
||||
public string Accent = default!;
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class RussianAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class ScrambledAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Rattle me bones!
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SkeletonAccentComponent : BaseAccentComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Chance that the message will be appended with "ACK ACK!"
|
||||
/// </summary>
|
||||
[DataField("ackChance")]
|
||||
public float ackChance = 0.3f; // Funnier if it doesn't happen every single time
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Makes... the... entity... talk... like... this...
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SlowAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[Access(typeof(SouthernAccentSystem))]
|
||||
public sealed partial class SouthernAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
using Content.Shared.Speech.Components;
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class SpanishAccentComponent : BaseAccentComponent;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Content.Server.Speech.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class UnblockableSpeechComponent : Component
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class BarkAccentSystem : RelayAccentSystem<BarkAccentComponent>
|
||||
{
|
||||
[Dependency] private IRobustRandom _random = default!;
|
||||
|
||||
private static readonly IReadOnlyList<string> Barks = new List<string>{
|
||||
" Гав!", " ГАВ", " вуф-вуф" // Corvax-Localization
|
||||
}.AsReadOnly();
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> SpecialWords = new Dictionary<string, string>()
|
||||
{
|
||||
{ "ah", "arf" },
|
||||
{ "Ah", "Arf" },
|
||||
{ "oh", "oof" },
|
||||
{ "Oh", "Oof" },
|
||||
//Corvax-Localization-Start
|
||||
{ "га", "гаф" },
|
||||
{ "Га", "Гаф" },
|
||||
{ "угу", "вуф" },
|
||||
{ "Угу", "Вуф" },
|
||||
//Corvax-Localization-End
|
||||
};
|
||||
|
||||
public override string Accentuate(string message, Entity<BarkAccentComponent>? ent = null)
|
||||
{
|
||||
foreach (var (word, repl) in SpecialWords)
|
||||
{
|
||||
message = message.Replace(word, repl);
|
||||
}
|
||||
|
||||
return message.Replace("!", _random.Pick(Barks))
|
||||
.Replace("l", "r").Replace("L", "R")
|
||||
.Replace("л", "р").Replace("Л", "Р");//Corvax-Localization
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Speech;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class BlockListeningSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BlockListeningComponent, ListenAttemptEvent>(OnListenAttempt);
|
||||
}
|
||||
|
||||
private void OnListenAttempt(EntityUid uid, BlockListeningComponent component, ListenAttemptEvent args)
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +1,19 @@
|
|||
using System.Text;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class DamagedSiliconAccentSystem : RelayAccentSystem<DamagedSiliconAccentComponent>
|
||||
public sealed partial class DamagedSiliconAccentSystem : SharedDamagedSiliconAccentSystem
|
||||
{
|
||||
[Dependency] private IRobustRandom _random = default!;
|
||||
[Dependency] private SharedBatterySystem _battery = default!;
|
||||
[Dependency] private PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private DestructibleSystem _destructibleSystem = default!;
|
||||
[Dependency] private DamageableSystem _damageable = default!;
|
||||
|
||||
protected override Type[]? AccentAfter => [typeof(ReplacementAccentSystem)];
|
||||
protected override Type[]? RelayAccentAfter => [typeof(ReplacementAccentSystem)];
|
||||
|
||||
public override string Accentuate(string message, Entity<DamagedSiliconAccentComponent>? ent = null)
|
||||
{
|
||||
if (ent == null)
|
||||
return message;
|
||||
|
||||
var uid = ent.Value.Owner;
|
||||
|
||||
if (ent.Value.Comp.EnableChargeCorruption)
|
||||
{
|
||||
var currentChargeLevel = 0.0f;
|
||||
if (ent.Value.Comp.OverrideChargeLevel.HasValue)
|
||||
{
|
||||
currentChargeLevel = ent.Value.Comp.OverrideChargeLevel.Value;
|
||||
}
|
||||
else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
|
||||
{
|
||||
currentChargeLevel = _battery.GetChargeLevel(battery.Value.AsNullable());
|
||||
}
|
||||
currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f);
|
||||
// Corrupt due to low power (drops characters on longer messages)
|
||||
message = CorruptPower(message, currentChargeLevel, ent.Value.Comp);
|
||||
}
|
||||
|
||||
if (ent.Value.Comp.EnableDamageCorruption)
|
||||
{
|
||||
var damage = FixedPoint2.Zero;
|
||||
if (ent.Value.Comp.OverrideTotalDamage.HasValue)
|
||||
{
|
||||
damage = ent.Value.Comp.OverrideTotalDamage.Value;
|
||||
}
|
||||
else if (TryComp<DamageableComponent>(uid, out var damageable))
|
||||
{
|
||||
damage = _damageable.GetTotalDamage((uid, damageable));
|
||||
}
|
||||
// Corrupt due to damage (drop, repeat, replace with symbols)
|
||||
message = CorruptDamage(message, damage, ent.Value);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public string CorruptPower(string message, float chargeLevel, DamagedSiliconAccentComponent comp)
|
||||
{
|
||||
// The first idxMin characters are SAFE
|
||||
var idxMin = comp.StartPowerCorruptionAtCharIdx;
|
||||
// Probability will max at idxMax
|
||||
var idxMax = comp.MaxPowerCorruptionAtCharIdx;
|
||||
|
||||
// Fast bails, would not have an effect
|
||||
if (chargeLevel > comp.ChargeThresholdForPowerCorruption || message.Length < idxMin)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
var outMsg = new StringBuilder();
|
||||
|
||||
var maxDropProb = comp.MaxDropProbFromPower * (1.0f - chargeLevel / comp.ChargeThresholdForPowerCorruption);
|
||||
|
||||
var idx = -1;
|
||||
foreach (var letter in message)
|
||||
{
|
||||
idx++;
|
||||
if (idx < idxMin) // Fast character, no effect
|
||||
{
|
||||
outMsg.Append(letter);
|
||||
continue;
|
||||
}
|
||||
|
||||
// use an x^2 interpolation to increase the drop probability until we hit idxMax
|
||||
var probToDrop = idx >= idxMax
|
||||
? maxDropProb
|
||||
: (float)Math.Pow(((double)idx - idxMin) / (idxMax - idxMin), 2.0) * maxDropProb;
|
||||
// Ensure we're in the range for Prob()
|
||||
probToDrop = Math.Clamp(probToDrop, 0.0f, 1.0f);
|
||||
|
||||
if (_random.Prob(probToDrop)) // Lose a character
|
||||
{
|
||||
// Additional chance to change to dot for flavor instead of full drop
|
||||
if (_random.Prob(comp.ProbToCorruptDotFromPower))
|
||||
{
|
||||
outMsg.Append('.');
|
||||
}
|
||||
}
|
||||
else // Character is safe
|
||||
{
|
||||
outMsg.Append(letter);
|
||||
}
|
||||
}
|
||||
return outMsg.ToString();
|
||||
}
|
||||
|
||||
private string CorruptDamage(string message, FixedPoint2 totalDamage, Entity<DamagedSiliconAccentComponent> ent)
|
||||
protected override string CorruptDamage(string message, FixedPoint2 totalDamage, Entity<DamagedSiliconAccentComponent> ent)
|
||||
{
|
||||
var outMsg = new StringBuilder();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Robust.Shared.Random; // Corvax-Localization
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class FrontalLispSystem : RelayAccentSystem<FrontalLispComponent>
|
||||
{
|
||||
// @formatter:off
|
||||
private static readonly Regex RegexUpperTh = new(@"[T]+[Ss]+|[S]+[Cc]+(?=[IiEeYy]+)|[C]+(?=[IiEeYy]+)|[P][Ss]+|([S]+[Tt]+|[T]+)(?=[Ii]+[Oo]+[Uu]*[Nn]*)|[C]+[Hh]+(?=[Ii]*[Ee]*)|[Z]+|[S]+|[X]+(?=[Ee]+)");
|
||||
private static readonly Regex RegexLowerTh = new(@"[t]+[s]+|[s]+[c]+(?=[iey]+)|[c]+(?=[iey]+)|[p][s]+|([s]+[t]+|[t]+)(?=[i]+[o]+[u]*[n]*)|[c]+[h]+(?=[i]*[e]*)|[z]+|[s]+|[x]+(?=[e]+)");
|
||||
private static readonly Regex RegexUpperEcks = new(@"[E]+[Xx]+[Cc]*|[X]+");
|
||||
private static readonly Regex RegexLowerEcks = new(@"[e]+[x]+[c]*|[x]+");
|
||||
// @formatter:on
|
||||
|
||||
// Corvax-Localization Start
|
||||
private static readonly Regex _regexLowerC = new Regex("с");// для "с" на "ш"/"с"
|
||||
private static readonly Regex _regexUpperC = new Regex("С");// для "С" на "Ш"/"С"
|
||||
private static readonly Regex _regexLowerCh = new Regex("ч");// для "ч" на "ш"/"ч"
|
||||
private static readonly Regex _regexUpperCh = new Regex("Ч");// для "Ч" на "Ш"/"Ч"
|
||||
private static readonly Regex _regexLowerTs = new Regex("ц");// для "ц" на "ч"/"ц"
|
||||
private static readonly Regex _regexUpperTs = new Regex("Ц");// для "Ц" на "Ч"/"Ц"
|
||||
private static readonly Regex _regexLowerT = new Regex(@"\B[т](?![АЕЁИОУЫЭЮЯаеёиоуыэюя])");
|
||||
private static readonly Regex _regexUpperT = new Regex(@"\B[Т](?![АЕЁИОУЫЭЮЯаеёиоуыэюя])");
|
||||
private static readonly Regex _regexLowerZ = new Regex("з");// для "з" на "ж"/"з"
|
||||
private static readonly Regex _regexUpperZ = new Regex("З");// для "З" на "Ж"/"З"
|
||||
// Corvax-Localization End
|
||||
|
||||
[Dependency] private IRobustRandom _random = default!; // Corvax-Localization
|
||||
|
||||
public override string Accentuate(string message, Entity<FrontalLispComponent>? ent = null)
|
||||
{
|
||||
// handles ts, sc(i|e|y), c(i|e|y), ps, st(io(u|n)), ch(i|e), z, s
|
||||
message = RegexUpperTh.Replace(message, "TH");
|
||||
message = RegexLowerTh.Replace(message, "th");
|
||||
// handles ex(c), x
|
||||
message = RegexUpperEcks.Replace(message, "EKTH");
|
||||
message = RegexLowerEcks.Replace(message, "ekth");
|
||||
|
||||
// Corvax-Localization Start
|
||||
message = _regexLowerC.Replace(message, _random.Prob(0.90f) ? "ш" : "с");
|
||||
message = _regexUpperC.Replace(message, _random.Prob(0.90f) ? "Ш" : "С");
|
||||
message = _regexLowerCh.Replace(message, _random.Prob(0.90f) ? "ш" : "ч");
|
||||
message = _regexUpperCh.Replace(message, _random.Prob(0.90f) ? "Ш" : "Ч");
|
||||
message = _regexLowerTs.Replace(message, _random.Prob(0.90f) ? "ч" : "ц");
|
||||
message = _regexUpperTs.Replace(message, _random.Prob(0.90f) ? "Ч" : "Ц");
|
||||
message = _regexLowerT.Replace(message, _random.Prob(0.90f) ? "ч" : "т");
|
||||
message = _regexUpperT.Replace(message, _random.Prob(0.90f) ? "Ч" : "Т");
|
||||
message = _regexLowerZ.Replace(message, _random.Prob(0.90f) ? "ж" : "з");
|
||||
message = _regexUpperZ.Replace(message, _random.Prob(0.90f) ? "Ж" : "З");
|
||||
// Corvax-Localization End
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
using Content.Server.Administration.Logs;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class MeleeSpeechSystem : SharedMeleeSpeechSystem
|
||||
{
|
||||
[Dependency] private IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private SharedActionsSystem _actionSystem = default!;
|
||||
[Dependency] private UserInterfaceSystem _uiSystem = default!;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<MeleeSpeechComponent, MeleeSpeechBattlecryChangedMessage>(OnBattlecryChanged);
|
||||
SubscribeLocalEvent<MeleeSpeechComponent, MeleeSpeechConfigureActionEvent>(OnConfigureAction);
|
||||
SubscribeLocalEvent<MeleeSpeechComponent, GetItemActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<MeleeSpeechComponent, MapInitEvent>(OnComponentMapInit);
|
||||
}
|
||||
private void OnComponentMapInit(EntityUid uid, MeleeSpeechComponent component, MapInitEvent args)
|
||||
{
|
||||
_actionSystem.AddAction(uid, ref component.ConfigureActionEntity, component.ConfigureAction, uid);
|
||||
}
|
||||
private void OnGetActions(EntityUid uid, MeleeSpeechComponent component, GetItemActionsEvent args)
|
||||
{
|
||||
args.AddAction(ref component.ConfigureActionEntity, component.ConfigureAction);
|
||||
}
|
||||
private void OnBattlecryChanged(EntityUid uid, MeleeSpeechComponent comp, MeleeSpeechBattlecryChangedMessage args)
|
||||
{
|
||||
if (!TryComp<MeleeSpeechComponent>(uid, out var meleeSpeechUser))
|
||||
return;
|
||||
var battlecry = args.Battlecry;
|
||||
if (battlecry.Length > comp.MaxBattlecryLength)
|
||||
battlecry = battlecry[..comp.MaxBattlecryLength];
|
||||
TryChangeBattlecry(uid, battlecry, meleeSpeechUser);
|
||||
}
|
||||
/// <summary>
|
||||
/// Attempts to open the Battlecry UI.
|
||||
/// </summary>
|
||||
private void OnConfigureAction(EntityUid uid, MeleeSpeechComponent comp, MeleeSpeechConfigureActionEvent args)
|
||||
{
|
||||
TryOpenUi(args.Performer, uid, comp);
|
||||
}
|
||||
public void TryOpenUi(EntityUid user, EntityUid source, MeleeSpeechComponent? component = null)
|
||||
{
|
||||
if (!Resolve(source, ref component))
|
||||
return;
|
||||
if (!TryComp<ActorComponent>(user, out var actor))
|
||||
return;
|
||||
_uiSystem.TryToggleUi(source, MeleeSpeechUiKey.Key, actor.PlayerSession);
|
||||
}
|
||||
/// <summary>
|
||||
/// Attempts to change the battlecry of an entity.
|
||||
/// Returns true/false.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logs changes to an entity's battlecry
|
||||
/// </remarks>
|
||||
public bool TryChangeBattlecry(EntityUid uid, string? battlecry, MeleeSpeechComponent? meleeSpeechComp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref meleeSpeechComp))
|
||||
return false;
|
||||
if (!string.IsNullOrWhiteSpace(battlecry))
|
||||
{
|
||||
battlecry = battlecry.Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
battlecry = null;
|
||||
}
|
||||
if (meleeSpeechComp.Battlecry == battlecry)
|
||||
return true;
|
||||
meleeSpeechComp.Battlecry = battlecry;
|
||||
Dirty(uid, meleeSpeechComp);
|
||||
_adminLogger.Add(LogType.ItemConfigure, LogImpact.Medium, $" {ToPrettyString(uid):entity}'s battlecry has been changed to {battlecry}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class OwOAccentSystem : RelayAccentSystem<OwOAccentComponent>
|
||||
{
|
||||
[Dependency] private IRobustRandom _random = default!;
|
||||
|
||||
private static readonly IReadOnlyList<string> Faces = new List<string>{
|
||||
" (•`ω´•)", " ;;w;;", " owo", " UwU", " >w<", " ^w^"
|
||||
}.AsReadOnly();
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> SpecialWords = new Dictionary<string, string>()
|
||||
{
|
||||
{ "you", "wu" },
|
||||
{ "ты", "ти" }, // Corvax-Localization
|
||||
};
|
||||
|
||||
public override string Accentuate(string message, Entity<OwOAccentComponent>? ent = null)
|
||||
{
|
||||
foreach (var (word, repl) in SpecialWords)
|
||||
{
|
||||
message = message.Replace(word, repl);
|
||||
}
|
||||
|
||||
return message.Replace("!", _random.Pick(Faces))
|
||||
.Replace("r", "w").Replace("R", "W")
|
||||
.Replace("l", "w").Replace("L", "W")
|
||||
// Corvax-Localization-Start
|
||||
.Replace("р", "в").Replace("Р", "В")
|
||||
.Replace("л", "в").Replace("Л", "В");
|
||||
// Corvax-Localization-End
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
using Content.Shared.Speech.EntitySystems;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class RatvarianLanguageSystem : SharedRatvarianLanguageSystem
|
||||
{
|
||||
public override void DoRatvarian(EntityUid uid, TimeSpan time, bool refresh)
|
||||
{
|
||||
if (refresh)
|
||||
Status.TryUpdateStatusEffectDuration(uid, Ratvarian, time);
|
||||
else
|
||||
Status.TryAddStatusEffectDuration(uid, Ratvarian, time);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue