mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
* Added base type for UI panels of MIDI input sources. * Renamed BandMenu into BandMidiSource. * Implemented the UI overhaul, skipping channels dialog functionality for now. * Refactored for conventions and reordered type members. Got rid of SetInstrument/ClearInstrument again. * Decreased background opacity for UnavailableOverlay to improve readability. * Localization * Added text to InputMidiSource so players know what it does. * Changed window MinSize to SetSize. Allows players to reduce window size if desired. * Improved button looks by adding style classes. * cleanup * leftover cleanup * Reverted fetching InstrumentSystem via IoCManager (crashes it, woops). * added filter bar to file list * refactored almost all system/manager interaction into instrument BUI * text changed for instruments-component-menu-input-notice-label * fixed InstrumentBoundUserInterface.Dispose not unsubscribing from everything * minor cleanup * finished configuration menu & cleanup * Comments and fixes for InstrumentMidiSourceBase * Fixed RA0051 * Removed unncecessary null check * Added methods for accessing the selected track. * Limited depth of search for instrument master to prevent deadlock. * Cached system access * Storing midis asynchronously * Actual MIDI data now only loaded before playback. * async removed from InstrumentBoundUserInterface.LoadStoredUserMidis * Replaced GetComponent calls with TryGetComponent * Localized MIDI playback track timer * Searching for next track using raw index now * Fixed CS0168 * XML comments, minor fixes and disabled MIDI library management while a track is playing. * removed timers with simple watchdog and switched to MidiFileCollectionManager * Missed one (timer) * Changed access for MidiFileCollectionManager.UserMidiDirectory field to public. * Added instrument config control for MIDI collection utilities. * redesigned instrument config menu * Removed rename function, updated adding implementation * more redesigns * fixed _isMidiFileDialogueWindowOpen not resetting correctly in certain cases. * file scoped namespaces * replaced EntityUid with Entity<InstrumentComponent> * if spaces * returning Tooltip instead of Control * replaced SetEntity with public field * readonly system fields * InstrumentMenu MinSize * xml comment for InstrumentMidiSourceBase * small refactor * more cleanup * TryGetComponent to GetComponent on Open() * more formatting issues * switched to [Dependency] for systems * playback improvements * disable playback button if no file selected and playing * activating shuffle now selects track if none is selected * rearranged playback buttons * channels overhauled * Play/Stop label to Play only * Removed all channel on/off * Program now default channel name display selection * improved RemoveButton.Disable behavior * channels now toggable by clicking anywhere * Shuffle no longer selects already selected track. * Loop & Shuffle no longer both selectable at once * minor improvements to channel item visuals * minor channel toggle fixes * xml docs & code cleanup * `RefreshBandRequest` now sent automatically when band source becomes visible * converted 5 items tuple to MidiChannelInfo record * added markup support to instrument menu configuration header * added note to midi channels config header explaining update behavior.
324 lines
11 KiB
C#
324 lines
11 KiB
C#
using Content.Client.Interactable;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Instruments;
|
|
using Content.Shared.Instruments.UI;
|
|
using Robust.Client.Audio.Midi;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Shared.Audio.Midi;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Client.Instruments.UI;
|
|
|
|
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)
|
|
{
|
|
_sawmill = _logManager.GetSawmill(SawmillCategory);
|
|
}
|
|
|
|
protected override void Open()
|
|
{
|
|
base.Open();
|
|
|
|
var instrument = EntMan.GetComponent<InstrumentComponent>(Owner);
|
|
|
|
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)
|
|
{
|
|
entities.Add((EntMan.GetEntity(netEnt.Item1), netEnt.Item2));
|
|
}
|
|
|
|
_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))
|
|
{
|
|
instrumentComp.LoopMidi = toggled;
|
|
}
|
|
|
|
_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
|
|
{
|
|
if (!EntMan.TryGetComponent<InstrumentComponent>(Owner, out var instrument))
|
|
return;
|
|
|
|
// Close any song that is already playing.
|
|
if (instrument.IsMidiOpen)
|
|
_instruments.CloseMidi(Owner, false, instrument);
|
|
|
|
if (!_fileSource.IsPlaying)
|
|
return;
|
|
|
|
if (!PlayCheck())
|
|
return;
|
|
|
|
if (!_instruments.OpenMidi(Owner, trackData, instrument))
|
|
_fileSource.IsPlaying = false;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_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);
|