merge remote wizden/master

This commit is contained in:
Dmitry 2026-07-30 05:04:04 +07:00
commit 3a9db586ff
324 changed files with 5855 additions and 3850 deletions

View file

@ -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());
}
}

View file

@ -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";

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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");

View file

@ -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>

View file

@ -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;
}
}
}

View 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>

View 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);
}
}
}

View 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>

View 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);
}
}

View 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>

View 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;
}
}
}

View file

@ -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>

View file

@ -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;
}
}
}

View 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>

View 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;
}
}
}

View 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>

View 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();
}
}

View file

@ -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);

View file

@ -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>

View file

@ -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);
}
}

View file

@ -0,0 +1 @@
<BoxContainer xmlns="https://spacestation14.io" />

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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()));
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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))

View file

@ -0,0 +1,5 @@
using Content.Shared.Speech.EntitySystems;
namespace Content.Client.Speech.EntitySystems;
public sealed class DamagedSiliconAccentSystem : SharedDamagedSiliconAccentSystem;

View file

@ -1,8 +0,0 @@
using Content.Shared.Speech.EntitySystems;
namespace Content.Client.Speech.EntitySystems;
public sealed class RatvarianLanguageSystem : SharedRatvarianLanguageSystem
{
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,5 @@
using Content.Shared.Suicide;
namespace Content.Client.Suicide;
public sealed class SuicideSystem : SharedSuicideSystem;

View 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));
}
}

View 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));
}
}

View 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;
}
}

View file

@ -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));
}
}

View file

@ -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"));

View file

@ -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}");
}
}
}
}

View file

@ -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]

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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()

View file

@ -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

View file

@ -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>

View 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);
}
}

View 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));
}
}

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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"/>

View file

@ -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);
}

View file

@ -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!;

View file

@ -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;

View file

@ -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;

View file

@ -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))
{

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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();

View file

@ -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;

View file

@ -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)

View file

@ -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;
}
}

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class BackwardsAccentComponent : BaseAccentComponent;

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class BarkAccentComponent : BaseAccentComponent;

View file

@ -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;

View file

@ -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
{
}

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class FrontalLispComponent : BaseAccentComponent;

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class GermanAccentComponent : BaseAccentComponent;

View file

@ -1,9 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
/// <summary>
/// Hiss!
/// </summary>
[RegisterComponent]
public sealed partial class LizardAccentComponent : BaseAccentComponent;

View file

@ -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;
}

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class MonkeyAccentComponent : BaseAccentComponent;

View file

@ -1,9 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
/// <summary>
/// Buzzz!
/// </summary>
[RegisterComponent]
public sealed partial class MothAccentComponent : BaseAccentComponent;

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class OwOAccentComponent : BaseAccentComponent;

View file

@ -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!;
}

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class RussianAccentComponent : BaseAccentComponent;

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class ScrambledAccentComponent : BaseAccentComponent;

View file

@ -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
}

View file

@ -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;

View file

@ -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;

View file

@ -1,6 +0,0 @@
using Content.Shared.Speech.Components;
namespace Content.Server.Speech.Components;
[RegisterComponent]
public sealed partial class SpanishAccentComponent : BaseAccentComponent;

View file

@ -1,7 +0,0 @@
namespace Content.Server.Speech.Components
{
[RegisterComponent]
public sealed partial class UnblockableSpeechComponent : Component
{
}
}

View file

@ -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
}
}

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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