diff --git a/Content.Client/Instruments/InstrumentComponent.cs b/Content.Client/Instruments/InstrumentComponent.cs index ebc29866eb..02435c2a1a 100644 --- a/Content.Client/Instruments/InstrumentComponent.cs +++ b/Content.Client/Instruments/InstrumentComponent.cs @@ -1,491 +1,71 @@ using System; using System.Collections.Generic; -using System.Linq; using Content.Shared.Instruments; -using Content.Shared.Physics; -using Robust.Client; using Robust.Client.Audio.Midi; using Robust.Shared.Audio.Midi; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Network; -using Robust.Shared.Players; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Timing; using Robust.Shared.ViewVariables; -namespace Content.Client.Instruments +namespace Content.Client.Instruments; + +[RegisterComponent, ComponentReference(typeof(SharedInstrumentComponent))] +public class InstrumentComponent : SharedInstrumentComponent { + public event Action? OnMidiPlaybackEnded; - [RegisterComponent] - public class InstrumentComponent : SharedInstrumentComponent - { + public IMidiRenderer? Renderer; - /// - /// Called when a midi song stops playing. - /// - public event Action? OnMidiPlaybackEnded; + public uint SequenceDelay; - [Dependency] private readonly IMidiManager _midiManager = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IClientNetManager _netManager = default!; + public uint SequenceStartTick; - private IMidiRenderer? _renderer; + public TimeSpan LastMeasured = TimeSpan.MinValue; - private InstrumentSystem _instrumentSystem = default!; + public int SentWithinASec; - [DataField("program")] - private byte _instrumentProgram = 1; + /// + /// A queue of MidiEvents to be sent to the server. + /// + [ViewVariables] + public readonly List MidiEventBuffer = new(); - [DataField("bank")] - private byte _instrumentBank; + /// + /// Whether a midi song will loop or not. + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool LoopMidi { get; set; } = false; - private uint _sequenceDelay; + /// + /// Whether this instrument is handheld or not. + /// + [ViewVariables] + [DataField("handheld")] + public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent. - private uint _sequenceStartTick; + /// + /// Whether there's a midi song being played or not. + /// + [ViewVariables] + public bool IsMidiOpen => Renderer?.Status == MidiRendererStatus.File; - [DataField("allowPercussion")] - private bool _allowPercussion; + /// + /// Whether the midi renderer is listening for midi input or not. + /// + [ViewVariables] + public bool IsInputOpen => Renderer?.Status == MidiRendererStatus.Input; - [DataField("allowProgramChange")] - private bool _allowProgramChange; + /// + /// Whether the midi renderer is alive or not. + /// + [ViewVariables] + public bool IsRendererAlive => Renderer != null; - [DataField("respectMidiLimits")] - private bool _respectMidiLimits = true; + [ViewVariables] + public int PlayerTotalTick => Renderer?.PlayerTotalTick ?? 0; - /// - /// A queue of MidiEvents to be sent to the server. - /// - [ViewVariables] - private readonly List _midiEventBuffer = new(); - - /// - /// Whether a midi song will loop or not. - /// - [ViewVariables(VVAccess.ReadWrite)] - public bool LoopMidi - { - get => _renderer?.LoopMidi ?? false; - set - { - if (_renderer != null) - { - _renderer.LoopMidi = value; - } - } - } - - /// - /// Changes the instrument the midi renderer will play. - /// - [ViewVariables(VVAccess.ReadWrite)] - public override byte InstrumentProgram - { - get => _instrumentProgram; - set - { - _instrumentProgram = value; - if (_renderer != null) - { - _renderer.MidiProgram = _instrumentProgram; - } - } - } - - /// - /// Changes the instrument bank the midi renderer will use. - /// - [ViewVariables(VVAccess.ReadWrite)] - public override byte InstrumentBank - { - get => _instrumentBank; - set - { - _instrumentBank = value; - if (_renderer != null) - { - _renderer.MidiBank = _instrumentBank; - } - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public override bool AllowPercussion - { - get => _allowPercussion; - set - { - _allowPercussion = value; - if (_renderer != null) - { - _renderer.DisablePercussionChannel = !_allowPercussion; - } - } - } - - [ViewVariables(VVAccess.ReadWrite)] - public override bool AllowProgramChange - { - get => _allowProgramChange; - set - { - _allowProgramChange = value; - if (_renderer != null) - { - _renderer.DisableProgramChangeEvent = !_allowProgramChange; - } - } - } - - /// - /// Whether this instrument is handheld or not. - /// - [ViewVariables] - [DataField("handheld")] - public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent. - - /// - /// Whether there's a midi song being played or not. - /// - [ViewVariables] - public bool IsMidiOpen => _renderer?.Status == MidiRendererStatus.File; - - /// - /// Whether the midi renderer is listening for midi input or not. - /// - [ViewVariables] - public bool IsInputOpen => _renderer?.Status == MidiRendererStatus.Input; - - /// - /// Whether the midi renderer is alive or not. - /// - [ViewVariables] - public bool IsRendererAlive => _renderer != null; - - [ViewVariables] - public int PlayerTotalTick => _renderer?.PlayerTotalTick ?? 0; - - [ViewVariables] - public int PlayerTick - { - get => _renderer?.PlayerTick ?? 0; - set - { - if (!IsRendererAlive || _renderer!.Status != MidiRendererStatus.File) return; - - _midiEventBuffer.Clear(); - - _renderer.PlayerTick = value; - var tick = _renderer.SequencerTick; - - // We add a "all notes off" message. - for (byte i = 0; i < 16; i++) - { - _midiEventBuffer.Add(new MidiEvent() - { - Tick = tick, Type = 176, - Control = 123, Velocity = 0, Channel = i, - }); - } - - // Now we add a Reset All Controllers message. - _midiEventBuffer.Add(new MidiEvent() - { - Tick = tick, Type = 176, - Control = 121, Value = 0, - }); - } - } - - protected override void Initialize() - { - base.Initialize(); - IoCManager.InjectDependencies(this); - _instrumentSystem = EntitySystem.Get(); - } - - protected virtual void SetupRenderer(bool fromStateChange = false) - { - if (IsRendererAlive) return; - - _sequenceDelay = 0; - _sequenceStartTick = 0; - _midiManager.OcclusionCollisionMask = (int) CollisionGroup.Impassable; - _renderer = _midiManager.GetNewRenderer(); - - if (_renderer != null) - { - _renderer.MidiBank = _instrumentBank; - _renderer.MidiProgram = _instrumentProgram; - _renderer.TrackingEntity = Owner; - _renderer.DisablePercussionChannel = !_allowPercussion; - _renderer.DisableProgramChangeEvent = !_allowProgramChange; - _renderer.OnMidiPlayerFinished += () => - { - OnMidiPlaybackEnded?.Invoke(); - EndRenderer(fromStateChange); - }; - } - - if (!fromStateChange) - { -#pragma warning disable 618 - SendNetworkMessage(new InstrumentStartMidiMessage()); -#pragma warning restore 618 - } - } - - protected void EndRenderer(bool fromStateChange = false) - { - if (IsInputOpen) - { - CloseInput(fromStateChange); - return; - } - - if (IsMidiOpen) - { - CloseMidi(fromStateChange); - return; - } - - _renderer?.StopAllNotes(); - - var renderer = _renderer; - - // We dispose of the synth two seconds from now to allow the last notes to stop from playing. - Owner.SpawnTimer(2000, () => { renderer?.Dispose(); }); - _renderer = null; - _midiEventBuffer.Clear(); - - if (!fromStateChange && IoCManager.Resolve().IsConnected) - { -#pragma warning disable 618 - SendNetworkMessage(new InstrumentStopMidiMessage()); -#pragma warning restore 618 - } - } - - protected override void Shutdown() - { - base.Shutdown(); - EndRenderer(); - } - - [Obsolete("Component Messages are deprecated, use Entity Events instead.")] - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, channel, session); - - switch (message) - { - case InstrumentMidiEventMessage midiEventMessage: - if (IsRendererAlive) - { - // If we're the ones sending the MidiEvents, we ignore this message. - if (IsInputOpen || IsMidiOpen) break; - } - else - { - // if we haven't started or finished some sequence - if (_sequenceStartTick == 0) - { - // we may have arrived late - SetupRenderer(true); - } - - // might be our own notes after we already finished playing - return; - } - - if (_sequenceStartTick <= 0) - { - _sequenceStartTick = midiEventMessage.MidiEvent - .Min(x => x.Tick) - 1; - } - - var sqrtLag = MathF.Sqrt(_netManager.ServerChannel!.Ping / 1000f); - var delay = (uint) (_renderer!.SequencerTimeScale * (.2 + sqrtLag)); - var delta = delay - _sequenceStartTick; - - _sequenceDelay = Math.Max(_sequenceDelay, delta); - - var currentTick = _renderer.SequencerTick; - - // ReSharper disable once ForCanBeConvertedToForeach - for (var i = 0; i < midiEventMessage.MidiEvent.Length; i++) - { - var ev = midiEventMessage.MidiEvent[i]; - var scheduled = ev.Tick + _sequenceDelay; - - if (scheduled <= currentTick) - { - _sequenceDelay += currentTick - ev.Tick; - scheduled = ev.Tick + _sequenceDelay; - } - - - _renderer?.ScheduleMidiEvent(ev, scheduled, true); - } - - break; - case InstrumentStartMidiMessage _: - { - SetupRenderer(true); - break; - } - case InstrumentStopMidiMessage _: - { - EndRenderer(true); - break; - } - } - } - - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - if (curState is not InstrumentState state) return; - - if (state.Playing) - { - SetupRenderer(true); - } - else - { - EndRenderer(true); - } - - AllowPercussion = state.AllowPercussion; - AllowProgramChange = state.AllowProgramChange; - InstrumentBank = state.InstrumentBank; - InstrumentProgram = state.InstrumentProgram; - } - - /// - public bool OpenInput() - { - SetupRenderer(); - - if (_renderer != null && _renderer.OpenInput()) - { - _renderer.OnMidiEvent += RendererOnMidiEvent; - return true; - } - - return false; - } - - /// - public bool CloseInput(bool fromStateChange = false) - { - if (_renderer == null || !_renderer.CloseInput()) - { - return false; - } - - EndRenderer(fromStateChange); - return true; - } - - public bool OpenMidi(ReadOnlySpan data) - { - SetupRenderer(); - - if (_renderer == null || !_renderer.OpenMidi(data)) - { - return false; - } - - _renderer.OnMidiEvent += RendererOnMidiEvent; - return true; - } - - /// - public bool CloseMidi(bool fromStateChange = false) - { - if (_renderer == null || !_renderer.CloseMidi()) - { - return false; - } - - EndRenderer(fromStateChange); - return true; - } - - /// - /// Called whenever the renderer receives a midi event. - /// - /// The received midi event - private void RendererOnMidiEvent(MidiEvent midiEvent) - { - _midiEventBuffer.Add(midiEvent); - } - - private TimeSpan _lastMeasured = TimeSpan.MinValue; - - private int _sentWithinASec; - - private static readonly TimeSpan OneSecAgo = TimeSpan.FromSeconds(-1); - - private static readonly Comparer SortMidiEventTick - = Comparer.Create((x, y) - => x.Tick.CompareTo(y.Tick)); - - public override void Update(float delta) - { - if (!IsMidiOpen && !IsInputOpen) return; - - var now = _gameTiming.RealTime; - var oneSecAGo = now.Add(OneSecAgo); - - if (_lastMeasured <= oneSecAGo) - { - _lastMeasured = now; - _sentWithinASec = 0; - } - - if (_midiEventBuffer.Count == 0) return; - - var max = _respectMidiLimits ? - Math.Min(_instrumentSystem.MaxMidiEventsPerBatch, _instrumentSystem.MaxMidiEventsPerSecond - _sentWithinASec) - : _midiEventBuffer.Count; - - if (max <= 0) - { - // hit event/sec limit, have to lag the batch or drop events - return; - } - - // fix cross-fade events generating retroactive events - // also handle any significant backlog of events after midi finished - - _midiEventBuffer.Sort(SortMidiEventTick); - var bufferTicks = IsRendererAlive && _renderer!.Status != MidiRendererStatus.None - ? _renderer.SequencerTimeScale * .2f - : 0; - var bufferedTick = IsRendererAlive - ? _renderer!.SequencerTick - bufferTicks - : int.MaxValue; - - var events = _midiEventBuffer - .TakeWhile(x => x.Tick < bufferedTick) - .Take(max) - .ToArray(); - - var eventCount = events.Length; - - if (eventCount == 0) return; - -#pragma warning disable 618 - SendNetworkMessage(new InstrumentMidiEventMessage(events)); -#pragma warning restore 618 - - _sentWithinASec += eventCount; - - _midiEventBuffer.RemoveRange(0, eventCount); - } - - } + [ViewVariables] + public int PlayerTick => Renderer?.PlayerTick ?? 0; + public void PlaybackEndedInvoke() => OnMidiPlaybackEnded?.Invoke(); } diff --git a/Content.Client/Instruments/InstrumentSystem.cs b/Content.Client/Instruments/InstrumentSystem.cs index 7e987b380d..0240df877b 100644 --- a/Content.Client/Instruments/InstrumentSystem.cs +++ b/Content.Client/Instruments/InstrumentSystem.cs @@ -1,29 +1,234 @@ -using Content.Shared; +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; using Content.Shared.CCVar; +using Content.Shared.Instruments; +using Content.Shared.Physics; using JetBrains.Annotations; +using Robust.Client.Audio.Midi; +using Robust.Shared.Audio.Midi; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Network; using Robust.Shared.Timing; +using SharpFont; namespace Content.Client.Instruments { [UsedImplicitly] - public class InstrumentSystem : EntitySystem + public class InstrumentSystem : SharedInstrumentSystem { + [Dependency] private readonly IClientNetManager _netManager = default!; + [Dependency] private readonly IMidiManager _midiManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; + public readonly TimeSpan OneSecAgo = TimeSpan.FromSeconds(-1); + + public readonly Comparer SortMidiEventTick + = Comparer.Create((x, y) + => x.Tick.CompareTo(y.Tick)); + + public int MaxMidiEventsPerBatch { get; private set; } + public int MaxMidiEventsPerSecond { get; private set; } + public override void Initialize() { base.Initialize(); _cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true); _cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true); + + SubscribeNetworkEvent(OnMidiEventRx); + SubscribeNetworkEvent(OnMidiStart); + SubscribeNetworkEvent(OnMidiStop); + + SubscribeLocalEvent(OnShutdown); } - public int MaxMidiEventsPerBatch { get; private set; } - public int MaxMidiEventsPerSecond { get; private set; } + public override void Shutdown() + { + base.Shutdown(); + + _cfg.UnsubValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged); + _cfg.UnsubValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged); + } + + private void OnShutdown(EntityUid uid, InstrumentComponent component, ComponentShutdown args) + { + EndRenderer(uid, false, component); + } + + public override void SetupRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (component is not InstrumentComponent instrument || instrument.IsRendererAlive) + return; + + instrument.SequenceDelay = 0; + instrument.SequenceStartTick = 0; + _midiManager.OcclusionCollisionMask = (int) CollisionGroup.Impassable; + instrument.Renderer = _midiManager.GetNewRenderer(); + + if (instrument.Renderer != null) + { + UpdateRenderer(uid, instrument); + instrument.Renderer.OnMidiPlayerFinished += () => + { + instrument.PlaybackEndedInvoke(); + EndRenderer(uid, fromStateChange, instrument); + }; + } + + if (!fromStateChange) + { + RaiseNetworkEvent(new InstrumentStartMidiEvent(uid)); + } + } + + public void UpdateRenderer(EntityUid uid, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument) || instrument.Renderer == null) + return; + + instrument.Renderer.MidiBank = instrument.InstrumentBank; + instrument.Renderer.MidiProgram = instrument.InstrumentProgram; + instrument.Renderer.TrackingEntity = instrument.Owner; + instrument.Renderer.DisablePercussionChannel = !instrument.AllowPercussion; + instrument.Renderer.DisableProgramChangeEvent = !instrument.AllowProgramChange; + instrument.Renderer.LoopMidi = instrument.LoopMidi; + instrument.DirtyRenderer = false; + } + + public override void EndRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (component is not InstrumentComponent instrument) + return; + + if (instrument.IsInputOpen) + { + CloseInput(uid, fromStateChange, instrument); + return; + } + + if (instrument.IsMidiOpen) + { + CloseMidi(uid, fromStateChange, instrument); + return; + } + + instrument.Renderer?.StopAllNotes(); + + var renderer = instrument.Renderer; + + // We dispose of the synth two seconds from now to allow the last notes to stop from playing. + instrument.Owner.SpawnTimer(2000, () => { renderer?.Dispose(); }); + instrument.Renderer = null; + instrument.MidiEventBuffer.Clear(); + + if (!fromStateChange && _netManager.IsConnected) + { + RaiseNetworkEvent(new InstrumentStopMidiEvent(uid)); + } + } + + public void SetPlayerTick(EntityUid uid, int playerTick, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument)) + return; + + if (instrument.Renderer == null || instrument.Renderer.Status != MidiRendererStatus.File) + return; + + instrument.MidiEventBuffer.Clear(); + + instrument.Renderer.PlayerTick = playerTick; + var tick = instrument.Renderer.SequencerTick; + + // We add a "all notes off" message. + for (byte i = 0; i < 16; i++) + { + instrument.MidiEventBuffer.Add(new MidiEvent() + { + Tick = tick, Type = 176, + Control = 123, Velocity = 0, Channel = i, + }); + } + + // Now we add a Reset All Controllers message. + instrument.MidiEventBuffer.Add(new MidiEvent() + { + Tick = tick, Type = 176, + Control = 121, Value = 0, + }); + } + + public bool OpenInput(EntityUid uid, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument, false)) + return false; + + SetupRenderer(uid, false, instrument); + + if (instrument.Renderer != null && instrument.Renderer.OpenInput()) + { + instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add; + return true; + } + + return false; + } + + public bool OpenMidi(EntityUid uid, ReadOnlySpan data, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument)) + return false; + + SetupRenderer(uid, false, instrument); + + if (instrument.Renderer == null || !instrument.Renderer.OpenMidi(data)) + { + return false; + } + + instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add; + return true; + } + + public bool CloseInput(EntityUid uid, bool fromStateChange, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument)) + return false; + + if (instrument.Renderer == null || !instrument.Renderer.CloseInput()) + { + return false; + } + + EndRenderer(uid, fromStateChange, instrument); + return true; + } + + public bool CloseMidi(EntityUid uid, bool fromStateChange, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument)) + return false; + + if (instrument.Renderer == null || !instrument.Renderer.CloseMidi()) + { + return false; + } + + EndRenderer(uid, fromStateChange, instrument); + return true; + } private void OnMaxMidiEventsPerSecondChanged(int obj) { @@ -35,6 +240,73 @@ namespace Content.Client.Instruments MaxMidiEventsPerBatch = obj; } + private void OnMidiEventRx(InstrumentMidiEventEvent midiEv) + { + var uid = midiEv.Uid; + + if (!EntityManager.TryGetComponent(uid, out InstrumentComponent? instrument)) + return; + + var renderer = instrument.Renderer; + + if (renderer != null) + { + // If we're the ones sending the MidiEvents, we ignore this message. + if (instrument.IsInputOpen || instrument.IsMidiOpen) + return; + } + else + { + // if we haven't started or finished some sequence + if (instrument.SequenceStartTick == 0) + { + // we may have arrived late + SetupRenderer(uid, true, instrument); + } + + // might be our own notes after we already finished playing + return; + } + + if (instrument.SequenceStartTick <= 0) + { + instrument.SequenceStartTick = midiEv.MidiEvent.Min(x => x.Tick) - 1; + } + + var sqrtLag = MathF.Sqrt(_netManager.ServerChannel!.Ping / 1000f); + var delay = (uint) (renderer.SequencerTimeScale * (.2 + sqrtLag)); + var delta = delay - instrument.SequenceStartTick; + + instrument.SequenceDelay = Math.Max(instrument.SequenceDelay, delta); + + var currentTick = renderer.SequencerTick; + + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < midiEv.MidiEvent.Length; i++) + { + var ev = midiEv.MidiEvent[i]; + var scheduled = ev.Tick + instrument.SequenceDelay; + + if (scheduled <= currentTick) + { + instrument.SequenceDelay += currentTick - ev.Tick; + scheduled = ev.Tick + instrument.SequenceDelay; + } + + instrument.Renderer?.ScheduleMidiEvent(ev, scheduled, true); + } + } + + private void OnMidiStart(InstrumentStartMidiEvent ev) + { + SetupRenderer(ev.Uid, true); + } + + private void OnMidiStop(InstrumentStopMidiEvent ev) + { + EndRenderer(ev.Uid, true); + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -44,9 +316,61 @@ namespace Content.Client.Instruments return; } - foreach (var instrumentComponent in EntityManager.EntityQuery(true)) + foreach (var instrument in EntityManager.EntityQuery(true)) { - instrumentComponent.Update(frameTime); + if (instrument.DirtyRenderer && instrument.Renderer != null) + UpdateRenderer(instrument.OwnerUid, instrument); + + if (!instrument.IsMidiOpen && !instrument.IsInputOpen) + return; + + var now = _gameTiming.RealTime; + var oneSecAGo = now.Add(OneSecAgo); + + if (instrument.LastMeasured <= oneSecAGo) + { + instrument.LastMeasured = now; + instrument.SentWithinASec = 0; + } + + if (instrument.MidiEventBuffer.Count == 0) return; + + var max = instrument.RespectMidiLimits ? + Math.Min(MaxMidiEventsPerBatch, MaxMidiEventsPerSecond - instrument.SentWithinASec) + : instrument.MidiEventBuffer.Count; + + if (max <= 0) + { + // hit event/sec limit, have to lag the batch or drop events + return; + } + + // fix cross-fade events generating retroactive events + // also handle any significant backlog of events after midi finished + + instrument.MidiEventBuffer.Sort(SortMidiEventTick); + var bufferTicks = instrument.IsRendererAlive && instrument.Renderer!.Status != MidiRendererStatus.None + ? instrument.Renderer.SequencerTimeScale * .2f + : 0; + var bufferedTick = instrument.IsRendererAlive + ? instrument.Renderer!.SequencerTick - bufferTicks + : int.MaxValue; + + var events = instrument.MidiEventBuffer + .TakeWhile(x => x.Tick < bufferedTick) + .Take(max) + .ToArray(); + + var eventCount = events.Length; + + if (eventCount == 0) + return; + + RaiseNetworkEvent(new InstrumentMidiEventEvent(instrument.OwnerUid, events)); + + instrument.SentWithinASec += eventCount; + + instrument.MidiEventBuffer.RemoveRange(0, eventCount); } } } diff --git a/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs b/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs index 7950a83cbe..98631bba8e 100644 --- a/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs +++ b/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs @@ -9,6 +9,7 @@ using Robust.Client.UserInterface; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Containers; +using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Timing; @@ -98,7 +99,8 @@ namespace Content.Client.Instruments.UI // While we're waiting, load it into memory. await Task.WhenAll(Timer.Delay(100), file.CopyToAsync(memStream)); - if (!_owner.Instrument?.OpenMidi(memStream.GetBuffer().AsSpan(0, (int) memStream.Length)) ?? true) + if (_owner.Instrument is not {} instrument + || !EntitySystem.Get().OpenMidi(instrument.OwnerUid, memStream.GetBuffer().AsSpan(0, (int) memStream.Length), instrument)) return; MidiPlaybackSetButtonsDisabled(false); @@ -108,16 +110,19 @@ namespace Content.Client.Instruments.UI private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj) { + var instrumentSystem = EntitySystem.Get(); + if (obj.Pressed) { if (!PlayCheck()) return; MidiStopButtonOnPressed(null); - _owner.Instrument?.OpenInput(); + if(_owner.Instrument is {} instrument) + instrumentSystem.OpenInput(instrument.OwnerUid, instrument); } - else - _owner.Instrument?.CloseInput(); + else if(_owner.Instrument is {} instrument) + instrumentSystem.CloseInput(instrument.OwnerUid, false, instrument); } private bool PlayCheck() @@ -149,28 +154,35 @@ namespace Content.Client.Instruments.UI private void MidiStopButtonOnPressed(ButtonEventArgs? obj) { MidiPlaybackSetButtonsDisabled(true); - _owner.Instrument?.CloseMidi(); + + if (_owner.Instrument is not { } instrument) + return; + + EntitySystem.Get().CloseMidi(instrument.OwnerUid, false, instrument); } private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj) { - if (_owner.Instrument != null) - _owner.Instrument.LoopMidi = obj.Pressed; + if (_owner.Instrument == null) + return; + + _owner.Instrument.LoopMidi = obj.Pressed; + _owner.Instrument.DirtyRenderer = true; } private void PlaybackSliderSeek(Range _) { // Do not seek while still grabbing. - if (PlaybackSlider.Grabbed || _owner.Instrument == null) return; + if (PlaybackSlider.Grabbed || _owner.Instrument is not {} instrument) return; - _owner.Instrument.PlayerTick = (int)Math.Ceiling((double) PlaybackSlider.Value); + EntitySystem.Get().SetPlayerTick(instrument.OwnerUid, (int)Math.Ceiling(PlaybackSlider.Value), instrument); } private void PlaybackSliderKeyUp(GUIBoundKeyEventArgs args) { - if (args.Function != EngineKeyFunctions.UIClick || _owner.Instrument == null) return; + if (args.Function != EngineKeyFunctions.UIClick || _owner.Instrument is not {} instrument) return; - _owner.Instrument.PlayerTick = (int)Math.Ceiling((double) PlaybackSlider.Value); + EntitySystem.Get().SetPlayerTick(instrument.OwnerUid, (int)Math.Ceiling(PlaybackSlider.Value), instrument); } protected override void FrameUpdate(FrameEventArgs args) diff --git a/Content.Server/Instruments/InstrumentComponent.cs b/Content.Server/Instruments/InstrumentComponent.cs index 56d97f5c6f..67e1b0f3c3 100644 --- a/Content.Server/Instruments/InstrumentComponent.cs +++ b/Content.Server/Instruments/InstrumentComponent.cs @@ -1,254 +1,30 @@ -using System; -using System.Linq; -using Content.Server.Stunnable; -using Content.Server.Stunnable.Components; using Content.Server.UserInterface; -using Content.Shared.ActionBlocker; -using Content.Shared.Hands; using Content.Shared.Instruments; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Standing; -using Content.Shared.Stunnable; -using Content.Shared.Throwing; using Robust.Server.GameObjects; using Robust.Server.Player; -using Robust.Shared.Enums; using Robust.Shared.GameObjects; -using Robust.Shared.Localization; -using Robust.Shared.Network; -using Robust.Shared.Players; -using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; -namespace Content.Server.Instruments +namespace Content.Server.Instruments; + +[RegisterComponent, ComponentReference(typeof(SharedInstrumentComponent))] +public sealed class InstrumentComponent : SharedInstrumentComponent { + [ViewVariables] + public float Timer = 0f; - [RegisterComponent] - public class InstrumentComponent - : SharedInstrumentComponent - { - private InstrumentSystem _instrumentSystem = default!; + [ViewVariables] + public int BatchesDropped = 0; - [ViewVariables] - private bool _playing = false; + [ViewVariables] + public int LaggedBatches = 0; - [ViewVariables] - private float _timer = 0f; + [ViewVariables] + public int MidiEventCount = 0; - [ViewVariables] - private int _batchesDropped = 0; - - [ViewVariables] - private int _laggedBatches = 0; - - [ViewVariables] - private uint _lastSequencerTick = 0; - - [ViewVariables] - private int _midiEventCount = 0; - - [DataField("program")] - private byte _instrumentProgram = 1; - [DataField("bank")] - private byte _instrumentBank; - [DataField("allowPercussion")] - private bool _allowPercussion; - [DataField("allowProgramChange")] - private bool _allowProgramChange; - [DataField("respectMidiLimits")] - private bool _respectMidiLimits = true; - - public override byte InstrumentProgram { get => _instrumentProgram; - set - { - _instrumentProgram = value; - Dirty(); - } - } - - public override byte InstrumentBank { get => _instrumentBank; - set - { - _instrumentBank = value; - Dirty(); - } - } - - public override bool AllowPercussion { get => _allowPercussion; - set - { - _allowPercussion = value; - Dirty(); - } - } - - public override bool AllowProgramChange { get => _allowProgramChange; - set - { - _allowProgramChange = value; - Dirty(); - } - } - - public override bool RespectMidiLimits { get => _respectMidiLimits; - set - { - _respectMidiLimits = value; - Dirty(); - } - } - - public IPlayerSession? InstrumentPlayer => Owner.GetComponentOrNull()?.CurrentSingleUser; - - /// - /// Whether the instrument is currently playing or not. - /// - [ViewVariables] - public bool Playing - { - get => _playing; - set - { - _playing = value; - Dirty(); - } - } - - [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(InstrumentUiKey.Key); - - protected override void Initialize() - { - base.Initialize(); - - _instrumentSystem = EntitySystem.Get(); - } - - public override ComponentState GetComponentState(ICommonSession player) - { - return new InstrumentState(Playing, InstrumentProgram, InstrumentBank, AllowPercussion, AllowProgramChange, RespectMidiLimits, _lastSequencerTick); - } - - [Obsolete("Component Messages are deprecated, use Entity Events instead.")] - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, channel, session); - - var maxMidiLaggedBatches = _instrumentSystem.MaxMidiLaggedBatches; - var maxMidiEventsPerSecond = _instrumentSystem.MaxMidiEventsPerSecond; - var maxMidiEventsPerBatch = _instrumentSystem.MaxMidiEventsPerBatch; - - switch (message) - { - case InstrumentMidiEventMessage midiEventMsg: - if (!Playing || session != InstrumentPlayer || InstrumentPlayer == null) return; - - var send = true; - - var minTick = midiEventMsg.MidiEvent.Min(x => x.Tick); - if (_lastSequencerTick > minTick) - { - _laggedBatches++; - - if (_respectMidiLimits) - { - if (_laggedBatches == (int) (maxMidiLaggedBatches * (1 / 3d) + 1)) - { - InstrumentPlayer.AttachedEntity?.PopupMessage( - Loc.GetString("instrument-component-finger-cramps-light-message")); - } else if (_laggedBatches == (int) (maxMidiLaggedBatches * (2 / 3d) + 1)) - { - InstrumentPlayer.AttachedEntity?.PopupMessage( - Loc.GetString("instrument-component-finger-cramps-serious-message")); - } - } - - if (_laggedBatches > maxMidiLaggedBatches) - { - send = false; - } - } - - if (++_midiEventCount > maxMidiEventsPerSecond - || midiEventMsg.MidiEvent.Length > maxMidiEventsPerBatch) - { - _batchesDropped++; - - send = false; - } - - if (send || !_respectMidiLimits) - { -#pragma warning disable 618 - SendNetworkMessage(midiEventMsg); -#pragma warning restore 618 - } - - var maxTick = midiEventMsg.MidiEvent.Max(x => x.Tick); - _lastSequencerTick = Math.Max(maxTick, minTick); - break; - case InstrumentStartMidiMessage startMidi: - if (session != InstrumentPlayer) - break; - Playing = true; - break; - case InstrumentStopMidiMessage stopMidi: - if (session != InstrumentPlayer) - break; - Playing = false; - Clean(); - break; - } - } - - public void Clean() - { - if (Playing) - { -#pragma warning disable 618 - SendNetworkMessage(new InstrumentStopMidiMessage()); -#pragma warning restore 618 - } - Playing = false; - _lastSequencerTick = 0; - _batchesDropped = 0; - _laggedBatches = 0; - } - - public override void Update(float delta) - { - base.Update(delta); - - var maxMidiLaggedBatches = _instrumentSystem.MaxMidiLaggedBatches; - var maxMidiBatchDropped = _instrumentSystem.MaxMidiBatchesDropped; - - if ((_batchesDropped >= maxMidiBatchDropped - || _laggedBatches >= maxMidiLaggedBatches) - && InstrumentPlayer != null && _respectMidiLimits) - { - var mob = InstrumentPlayer.AttachedEntity; - - // Just in case - Clean(); - UserInterface?.CloseAll(); - - if (mob != null) - { - EntitySystem.Get().TryParalyze(mob.Uid, TimeSpan.FromSeconds(1)); - - Owner.PopupMessage(mob, "instrument-component-finger-cramps-max-message"); - } - } - - _timer += delta; - if (_timer < 1) return; - - _timer = 0f; - _midiEventCount = 0; - _laggedBatches = 0; - _batchesDropped = 0; - } - - } + public IPlayerSession? InstrumentPlayer => + Owner.GetComponentOrNull()?.CurrentSingleUser + ?? Owner.GetComponentOrNull()?.PlayerSession; + [ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(InstrumentUiKey.Key); } diff --git a/Content.Server/Instruments/InstrumentSystem.CVars.cs b/Content.Server/Instruments/InstrumentSystem.CVars.cs new file mode 100644 index 0000000000..9ffa8bb851 --- /dev/null +++ b/Content.Server/Instruments/InstrumentSystem.CVars.cs @@ -0,0 +1,47 @@ +using Content.Shared.CCVar; + +namespace Content.Server.Instruments; + +public partial class InstrumentSystem +{ + public int MaxMidiEventsPerSecond { get; private set; } + public int MaxMidiEventsPerBatch { get; private set; } + public int MaxMidiBatchesDropped { get; private set; } + public int MaxMidiLaggedBatches { get; private set; } + + private void InitializeCVars() + { + _cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true); + _cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true); + _cfg.OnValueChanged(CCVars.MaxMidiBatchesDropped, OnMaxMidiBatchesDroppedChanged, true); + _cfg.OnValueChanged(CCVars.MaxMidiLaggedBatches, OnMaxMidiLaggedBatchesChanged, true); + } + + private void ShutdownCVars() + { + _cfg.UnsubValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged); + _cfg.UnsubValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged); + _cfg.UnsubValueChanged(CCVars.MaxMidiBatchesDropped, OnMaxMidiBatchesDroppedChanged); + _cfg.UnsubValueChanged(CCVars.MaxMidiLaggedBatches, OnMaxMidiLaggedBatchesChanged); + } + + private void OnMaxMidiLaggedBatchesChanged(int obj) + { + MaxMidiLaggedBatches = obj; + } + + private void OnMaxMidiBatchesDroppedChanged(int obj) + { + MaxMidiBatchesDropped = obj; + } + + private void OnMaxMidiEventsPerBatchChanged(int obj) + { + MaxMidiEventsPerBatch = obj; + } + + private void OnMaxMidiEventsPerSecondChanged(int obj) + { + MaxMidiEventsPerSecond = obj; + } +} diff --git a/Content.Server/Instruments/InstrumentSystem.cs b/Content.Server/Instruments/InstrumentSystem.cs index 049a9c93d6..c1e6b16dd9 100644 --- a/Content.Server/Instruments/InstrumentSystem.cs +++ b/Content.Server/Instruments/InstrumentSystem.cs @@ -1,68 +1,177 @@ -using Content.Shared; -using Content.Shared.CCVar; +using System; +using System.Linq; +using Content.Server.Stunnable; using Content.Server.UserInterface; +using Content.Shared.Instruments; +using Content.Shared.Popups; using JetBrains.Annotations; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Localization; -namespace Content.Server.Instruments +namespace Content.Server.Instruments; + +[UsedImplicitly] +public sealed partial class InstrumentSystem : SharedInstrumentSystem { - [UsedImplicitly] - internal sealed class InstrumentSystem : EntitySystem + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly StunSystem _stunSystem = default!; + + public override void Initialize() { - [Dependency] private readonly IConfigurationManager _cfg = default!; + base.Initialize(); - public override void Initialize() + InitializeCVars(); + + SubscribeNetworkEvent(OnMidiEventRx); + SubscribeNetworkEvent(OnMidiStart); + SubscribeNetworkEvent(OnMidiStop); + + SubscribeLocalEvent(InstrumentNeedsClean); + } + + private void OnMidiStart(InstrumentStartMidiEvent msg, EntitySessionEventArgs args) + { + var uid = msg.Uid; + + if (!EntityManager.TryGetComponent(uid, out InstrumentComponent? instrument)) + return; + + if (args.SenderSession != instrument.InstrumentPlayer) + return; + + instrument.Playing = true; + instrument.Dirty(); + } + + private void OnMidiStop(InstrumentStopMidiEvent msg, EntitySessionEventArgs args) + { + var uid = msg.Uid; + + if (!EntityManager.TryGetComponent(uid, out InstrumentComponent? instrument)) + return; + + if (args.SenderSession != instrument.InstrumentPlayer) + return; + + Clean(uid, instrument); + } + + public override void Shutdown() + { + base.Shutdown(); + + ShutdownCVars(); + } + + public void Clean(EntityUid uid, InstrumentComponent? instrument = null) + { + if (!Resolve(uid, ref instrument)) + return; + + if (instrument.Playing) { - base.Initialize(); - - _cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, true); - _cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true); - _cfg.OnValueChanged(CCVars.MaxMidiBatchesDropped, OnMaxMidiBatchesDroppedChanged, true); - _cfg.OnValueChanged(CCVars.MaxMidiLaggedBatches, OnMaxMidiLaggedBatchesChanged, true); - - SubscribeLocalEvent(InstrumentNeedsClean); + RaiseNetworkEvent(new InstrumentStopMidiEvent(uid)); } - public int MaxMidiEventsPerSecond { get; private set; } - public int MaxMidiEventsPerBatch { get; private set; } - public int MaxMidiBatchesDropped { get; private set; } - public int MaxMidiLaggedBatches { get; private set; } + instrument.Playing = false; + instrument.LastSequencerTick = 0; + instrument.BatchesDropped = 0; + instrument.LaggedBatches = 0; + instrument.Dirty(); + } - private void OnMaxMidiLaggedBatchesChanged(int obj) + private void InstrumentNeedsClean(EntityUid uid, InstrumentComponent component, ActivatableUIPlayerChangedEvent ev) + { + Clean(uid, component); + } + + private void OnMidiEventRx(InstrumentMidiEventEvent msg, EntitySessionEventArgs args) + { + var uid = msg.Uid; + + if (!EntityManager.TryGetComponent(uid, out InstrumentComponent? instrument)) + return; + + if (!instrument.Playing || args.SenderSession != instrument.InstrumentPlayer || instrument.InstrumentPlayer == null) + return; + + var send = true; + + var minTick = msg.MidiEvent.Min(x => x.Tick); + if (instrument.LastSequencerTick > minTick) { - MaxMidiLaggedBatches = obj; - } + instrument.LaggedBatches++; - private void OnMaxMidiBatchesDroppedChanged(int obj) - { - MaxMidiBatchesDropped = obj; - } - - private void OnMaxMidiEventsPerBatchChanged(int obj) - { - MaxMidiEventsPerBatch = obj; - } - - private void OnMaxMidiEventsPerSecondChanged(int obj) - { - MaxMidiEventsPerSecond = obj; - } - - private void InstrumentNeedsClean(EntityUid uid, InstrumentComponent component, ActivatableUIPlayerChangedEvent ev) - { - component.Clean(); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - foreach (var component in EntityManager.EntityQuery(true)) + if (instrument.RespectMidiLimits) { - component.Update(frameTime); + if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (1 / 3d) + 1)) + { + instrument.InstrumentPlayer.AttachedEntity?.PopupMessage( + Loc.GetString("instrument-component-finger-cramps-light-message")); + } else if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (2 / 3d) + 1)) + { + instrument.InstrumentPlayer.AttachedEntity?.PopupMessage( + Loc.GetString("instrument-component-finger-cramps-serious-message")); + } } + + if (instrument.LaggedBatches > MaxMidiLaggedBatches) + { + send = false; + } + } + + if (++instrument.MidiEventCount > MaxMidiEventsPerSecond + || msg.MidiEvent.Length > MaxMidiEventsPerBatch) + { + instrument.BatchesDropped++; + + send = false; + } + + if (send || !instrument.RespectMidiLimits) + { + RaiseNetworkEvent(msg); + } + + var maxTick = msg.MidiEvent.Max(x => x.Tick); + instrument.LastSequencerTick = Math.Max(maxTick, minTick); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var instrument in EntityManager.EntityQuery(true)) + { + if ((instrument.BatchesDropped >= MaxMidiBatchesDropped + || instrument.LaggedBatches >= MaxMidiLaggedBatches) + && instrument.InstrumentPlayer != null && instrument.RespectMidiLimits) + { + var mob = instrument.InstrumentPlayer.AttachedEntity; + + // Just in case + Clean(instrument.OwnerUid); + instrument.UserInterface?.CloseAll(); + + if (mob != null) + { + _stunSystem.TryParalyze(mob.Uid, TimeSpan.FromSeconds(1)); + + instrument.Owner.PopupMessage(mob, "instrument-component-finger-cramps-max-message"); + } + } + + instrument.Timer += frameTime; + if (instrument.Timer < 1) + return; + + instrument.Timer = 0f; + instrument.MidiEventCount = 0; + instrument.LaggedBatches = 0; + instrument.BatchesDropped = 0; } } } diff --git a/Content.Shared/Instruments/SharedInstrumentComponent.cs b/Content.Shared/Instruments/SharedInstrumentComponent.cs index 477f5cf5e4..f7c171442e 100644 --- a/Content.Shared/Instruments/SharedInstrumentComponent.cs +++ b/Content.Shared/Instruments/SharedInstrumentComponent.cs @@ -1,99 +1,112 @@ using System; +using Robust.Shared.Analyzers; using Robust.Shared.Audio.Midi; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; -namespace Content.Shared.Instruments +namespace Content.Shared.Instruments; + +[NetworkedComponent, Friend(typeof(SharedInstrumentSystem))] +public class SharedInstrumentComponent : Component { - [NetworkedComponent()] - public class SharedInstrumentComponent : Component + public override string Name => "Instrument"; + + [ViewVariables] + public bool Playing { get; set; } + + [ViewVariables] + public uint LastSequencerTick { get; set; } + + [DataField("program")] + public byte InstrumentProgram { get; set; } + + [DataField("bank")] + public byte InstrumentBank { get; set; } + + [DataField("allowPercussion")] + public bool AllowPercussion { get; set; } + + [DataField("allowProgramChange")] + public bool AllowProgramChange { get ; set; } + + [DataField("respectMidiLimits")] + public bool RespectMidiLimits { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public bool DirtyRenderer { get; set; } +} + + +/// +/// This message is sent to the client to completely stop midi input and midi playback. +/// +[Serializable, NetSerializable] +public class InstrumentStopMidiEvent : EntityEventArgs +{ + public EntityUid Uid { get; } + + public InstrumentStopMidiEvent(EntityUid uid) { - public override string Name => "Instrument"; - - [ViewVariables(VVAccess.ReadWrite)] - public virtual byte InstrumentProgram { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual byte InstrumentBank { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual bool AllowPercussion { get; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual bool AllowProgramChange { get ; set; } - - [ViewVariables(VVAccess.ReadWrite)] - public virtual bool RespectMidiLimits { get; set; } - - public virtual void Update(float delta) - { - } - } - - - /// - /// This message is sent to the client to completely stop midi input and midi playback. - /// - [Serializable, NetSerializable] -#pragma warning disable 618 - public class InstrumentStopMidiMessage : ComponentMessage -#pragma warning restore 618 - { - } - - /// - /// This message is sent to the client to start the synth. - /// - [Serializable, NetSerializable] -#pragma warning disable 618 - public class InstrumentStartMidiMessage : ComponentMessage -#pragma warning restore 618 - { - - } - - /// - /// This message carries a MidiEvent to be played on clients. - /// - [Serializable, NetSerializable] -#pragma warning disable 618 - public class InstrumentMidiEventMessage : ComponentMessage -#pragma warning restore 618 - { - public MidiEvent[] MidiEvent; - - public InstrumentMidiEventMessage(MidiEvent[] midiEvent) - { - MidiEvent = midiEvent; - } - } - - [Serializable, NetSerializable] - public class InstrumentState : ComponentState - { - public bool Playing { get; } - public byte InstrumentProgram { get; } - public byte InstrumentBank { get; } - public bool AllowPercussion { get; } - public bool AllowProgramChange { get; } - public bool RespectMidiLimits { get; } - - public InstrumentState(bool playing, byte instrumentProgram, byte instrumentBank, bool allowPercussion, bool allowProgramChange, bool respectMidiLimits, uint sequencerTick = 0) - { - Playing = playing; - InstrumentProgram = instrumentProgram; - InstrumentBank = instrumentBank; - AllowPercussion = allowPercussion; - AllowProgramChange = allowProgramChange; - RespectMidiLimits = respectMidiLimits; - } - } - - [NetSerializable, Serializable] - public enum InstrumentUiKey - { - Key, + Uid = uid; } } + +/// +/// This message is sent to the client to start the synth. +/// +[Serializable, NetSerializable] +public class InstrumentStartMidiEvent : EntityEventArgs +{ + public EntityUid Uid { get; } + + public InstrumentStartMidiEvent(EntityUid uid) + { + Uid = uid; + } +} + +/// +/// This message carries a MidiEvent to be played on clients. +/// +[Serializable, NetSerializable] +public class InstrumentMidiEventEvent : EntityEventArgs +{ + public EntityUid Uid { get; } + public MidiEvent[] MidiEvent { get; } + + public InstrumentMidiEventEvent(EntityUid uid, MidiEvent[] midiEvent) + { + Uid = uid; + MidiEvent = midiEvent; + } +} + +[Serializable, NetSerializable] +public class InstrumentState : ComponentState +{ + public bool Playing { get; } + public byte InstrumentProgram { get; } + public byte InstrumentBank { get; } + public bool AllowPercussion { get; } + public bool AllowProgramChange { get; } + public bool RespectMidiLimits { get; } + + public InstrumentState(bool playing, byte instrumentProgram, byte instrumentBank, bool allowPercussion, bool allowProgramChange, bool respectMidiLimits, uint sequencerTick = 0) + { + Playing = playing; + InstrumentProgram = instrumentProgram; + InstrumentBank = instrumentBank; + AllowPercussion = allowPercussion; + AllowProgramChange = allowProgramChange; + RespectMidiLimits = respectMidiLimits; + } +} + +[NetSerializable, Serializable] +public enum InstrumentUiKey +{ + Key, +} diff --git a/Content.Shared/Instruments/SharedInstrumentSystem.cs b/Content.Shared/Instruments/SharedInstrumentSystem.cs new file mode 100644 index 0000000000..5d2b5985f8 --- /dev/null +++ b/Content.Shared/Instruments/SharedInstrumentSystem.cs @@ -0,0 +1,50 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; + +namespace Content.Shared.Instruments; + +public abstract class SharedInstrumentSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + } + + public virtual void SetupRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? instrument = null) + { } + + public virtual void EndRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? instrument = null) + { } + + private void OnGetState(EntityUid uid, SharedInstrumentComponent instrument, ref ComponentGetState args) + { + args.State = + new InstrumentState(instrument.Playing, instrument.InstrumentProgram, instrument.InstrumentBank, + instrument.AllowPercussion, instrument.AllowProgramChange, instrument.RespectMidiLimits, instrument.LastSequencerTick); + } + + private void OnHandleState(EntityUid uid, SharedInstrumentComponent instrument, ref ComponentHandleState args) + { + if (args.Current is not InstrumentState state) + return; + + if (state.Playing) + { + SetupRenderer(uid, true, instrument); + } + else + { + EndRenderer(uid, true, instrument); + } + + instrument.Playing = state.Playing; + instrument.AllowPercussion = state.AllowPercussion; + instrument.AllowProgramChange = state.AllowProgramChange; + instrument.InstrumentBank = state.InstrumentBank; + instrument.InstrumentProgram = state.InstrumentProgram; + instrument.DirtyRenderer = true; + } +}