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