Make instruments ECS (#5516)

This commit is contained in:
Vera Aguilera Puerto
2021-11-28 01:47:36 +01:00
committed by GitHub
parent f5c3b1935b
commit 47a19f94d4
8 changed files with 770 additions and 859 deletions

View File

@@ -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;
/// <summary>
/// Called when a midi song stops playing.
/// </summary>
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;
/// <summary>
/// A queue of MidiEvents to be sent to the server.
/// </summary>
[ViewVariables]
public readonly List<MidiEvent> MidiEventBuffer = new();
[DataField("bank")]
private byte _instrumentBank;
/// <summary>
/// Whether a midi song will loop or not.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool LoopMidi { get; set; } = false;
private uint _sequenceDelay;
/// <summary>
/// Whether this instrument is handheld or not.
/// </summary>
[ViewVariables]
[DataField("handheld")]
public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent.
private uint _sequenceStartTick;
/// <summary>
/// Whether there's a midi song being played or not.
/// </summary>
[ViewVariables]
public bool IsMidiOpen => Renderer?.Status == MidiRendererStatus.File;
[DataField("allowPercussion")]
private bool _allowPercussion;
/// <summary>
/// Whether the midi renderer is listening for midi input or not.
/// </summary>
[ViewVariables]
public bool IsInputOpen => Renderer?.Status == MidiRendererStatus.Input;
[DataField("allowProgramChange")]
private bool _allowProgramChange;
/// <summary>
/// Whether the midi renderer is alive or not.
/// </summary>
[ViewVariables]
public bool IsRendererAlive => Renderer != null;
[DataField("respectMidiLimits")]
private bool _respectMidiLimits = true;
[ViewVariables]
public int PlayerTotalTick => Renderer?.PlayerTotalTick ?? 0;
/// <summary>
/// A queue of MidiEvents to be sent to the server.
/// </summary>
[ViewVariables]
private readonly List<MidiEvent> _midiEventBuffer = new();
/// <summary>
/// Whether a midi song will loop or not.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool LoopMidi
{
get => _renderer?.LoopMidi ?? false;
set
{
if (_renderer != null)
{
_renderer.LoopMidi = value;
}
}
}
/// <summary>
/// Changes the instrument the midi renderer will play.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public override byte InstrumentProgram
{
get => _instrumentProgram;
set
{
_instrumentProgram = value;
if (_renderer != null)
{
_renderer.MidiProgram = _instrumentProgram;
}
}
}
/// <summary>
/// Changes the instrument bank the midi renderer will use.
/// </summary>
[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;
}
}
}
/// <summary>
/// Whether this instrument is handheld or not.
/// </summary>
[ViewVariables]
[DataField("handheld")]
public bool Handheld { get; set; } // TODO: Replace this by simply checking if the entity has an ItemComponent.
/// <summary>
/// Whether there's a midi song being played or not.
/// </summary>
[ViewVariables]
public bool IsMidiOpen => _renderer?.Status == MidiRendererStatus.File;
/// <summary>
/// Whether the midi renderer is listening for midi input or not.
/// </summary>
[ViewVariables]
public bool IsInputOpen => _renderer?.Status == MidiRendererStatus.Input;
/// <summary>
/// Whether the midi renderer is alive or not.
/// </summary>
[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<InstrumentSystem>();
}
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<INetManager>().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;
}
/// <inheritdoc cref="MidiRenderer.OpenInput"/>
public bool OpenInput()
{
SetupRenderer();
if (_renderer != null && _renderer.OpenInput())
{
_renderer.OnMidiEvent += RendererOnMidiEvent;
return true;
}
return false;
}
/// <inheritdoc cref="MidiRenderer.CloseInput"/>
public bool CloseInput(bool fromStateChange = false)
{
if (_renderer == null || !_renderer.CloseInput())
{
return false;
}
EndRenderer(fromStateChange);
return true;
}
public bool OpenMidi(ReadOnlySpan<byte> data)
{
SetupRenderer();
if (_renderer == null || !_renderer.OpenMidi(data))
{
return false;
}
_renderer.OnMidiEvent += RendererOnMidiEvent;
return true;
}
/// <inheritdoc cref="MidiRenderer.CloseMidi"/>
public bool CloseMidi(bool fromStateChange = false)
{
if (_renderer == null || !_renderer.CloseMidi())
{
return false;
}
EndRenderer(fromStateChange);
return true;
}
/// <summary>
/// Called whenever the renderer receives a midi event.
/// </summary>
/// <param name="midiEvent">The received midi event</param>
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<MidiEvent> SortMidiEventTick
= Comparer<MidiEvent>.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();
}

View File

@@ -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<MidiEvent> SortMidiEventTick
= Comparer<MidiEvent>.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<InstrumentMidiEventEvent>(OnMidiEventRx);
SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(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<byte> 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<InstrumentComponent>(true))
foreach (var instrument in EntityManager.EntityQuery<InstrumentComponent>(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);
}
}
}

View File

@@ -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<InstrumentSystem>().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<InstrumentSystem>();
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<InstrumentSystem>().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<InstrumentSystem>().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<InstrumentSystem>().SetPlayerTick(instrument.OwnerUid, (int)Math.Ceiling(PlaybackSlider.Value), instrument);
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -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<ActivatableUIComponent>()?.CurrentSingleUser;
/// <summary>
/// Whether the instrument is currently playing or not.
/// </summary>
[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<InstrumentSystem>();
}
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<StunSystem>().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<ActivatableUIComponent>()?.CurrentSingleUser
?? Owner.GetComponentOrNull<ActorComponent>()?.PlayerSession;
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(InstrumentUiKey.Key);
}

View File

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

View File

@@ -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<InstrumentMidiEventEvent>(OnMidiEventRx);
SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
SubscribeLocalEvent<InstrumentComponent, ActivatableUIPlayerChangedEvent>(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<InstrumentComponent, ActivatableUIPlayerChangedEvent>(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<InstrumentComponent>(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<InstrumentComponent>(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;
}
}
}

View File

@@ -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; }
}
/// <summary>
/// This message is sent to the client to completely stop midi input and midi playback.
/// </summary>
[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)
{
}
}
/// <summary>
/// This message is sent to the client to completely stop midi input and midi playback.
/// </summary>
[Serializable, NetSerializable]
#pragma warning disable 618
public class InstrumentStopMidiMessage : ComponentMessage
#pragma warning restore 618
{
}
/// <summary>
/// This message is sent to the client to start the synth.
/// </summary>
[Serializable, NetSerializable]
#pragma warning disable 618
public class InstrumentStartMidiMessage : ComponentMessage
#pragma warning restore 618
{
}
/// <summary>
/// This message carries a MidiEvent to be played on clients.
/// </summary>
[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;
}
}
/// <summary>
/// This message is sent to the client to start the synth.
/// </summary>
[Serializable, NetSerializable]
public class InstrumentStartMidiEvent : EntityEventArgs
{
public EntityUid Uid { get; }
public InstrumentStartMidiEvent(EntityUid uid)
{
Uid = uid;
}
}
/// <summary>
/// This message carries a MidiEvent to be played on clients.
/// </summary>
[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,
}

View File

@@ -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<SharedInstrumentComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<SharedInstrumentComponent, ComponentHandleState>(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;
}
}