using System.IO; using System.Numerics; using System.Threading.Tasks; using Content.Client.Interactable; using Content.Shared.ActionBlocker; using Robust.Client.AutoGenerated; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.CustomControls; 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 { [GenerateTypedNameReferences] public sealed partial class InstrumentMenu : DefaultWindow { [Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IFileDialogManager _dialogs = default!; [Dependency] private readonly IPlayerManager _player = default!; private bool _isMidiFileDialogueWindowOpen; public event Action? OnOpenBand; public event Action? OnOpenChannels; public event Action? OnCloseBands; public event Action? OnCloseChannels; public EntityUid Entity; public InstrumentMenu() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); 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; MinSize = SetSize = new Vector2(400, 150); } public void SetInstrument(Entity entity) { 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; } 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)) { _entManager.System().SetMaster(Entity, instrument.Master); } } private void ChannelsButtonOnPressed(ButtonEventArgs obj) { OnOpenChannels?.Invoke(); } private void InstrumentOnMidiPlaybackEnded() { MidiPlaybackSetButtonsDisabled(true); } public void MidiPlaybackSetButtonsDisabled(bool disabled) { if (disabled) { 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(Entity, out var instrument)) { return; } if (!_entManager.System() .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().OpenInput(Entity, instrument); } else { _entManager.System().CloseInput(Entity, false); OnCloseChannels?.Invoke(); } } 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(); // 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().CanInteract(localEntity.Value, Entity)) return false; // We check that we're in range unobstructed just in case. return _entManager.System().InRangeUnobstructed(localEntity.Value, Entity); } private void MidiStopButtonOnPressed(ButtonEventArgs? obj) { MidiPlaybackSetButtonsDisabled(true); _entManager.System().CloseMidi(Entity, false); OnCloseChannels?.Invoke(); } private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj) { var instrument = _entManager.System(); 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().SetPlayerTick(Entity, (int)Math.Ceiling(PlaybackSlider.Value)); } private void PlaybackSliderKeyUp(GUIBoundKeyEventArgs args) { if (args.Function != EngineKeyFunctions.UIClick) return; _entManager.System().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); } } }