Make instruments ECS (#5516)
This commit is contained in:
committed by
GitHub
parent
f5c3b1935b
commit
47a19f94d4
@@ -1,491 +1,71 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Content.Shared.Instruments;
|
using Content.Shared.Instruments;
|
||||||
using Content.Shared.Physics;
|
|
||||||
using Robust.Client;
|
|
||||||
using Robust.Client.Audio.Midi;
|
using Robust.Client.Audio.Midi;
|
||||||
using Robust.Shared.Audio.Midi;
|
using Robust.Shared.Audio.Midi;
|
||||||
using Robust.Shared.GameObjects;
|
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.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.Timing;
|
|
||||||
using Robust.Shared.ViewVariables;
|
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 IMidiRenderer? Renderer;
|
||||||
public class InstrumentComponent : SharedInstrumentComponent
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
public uint SequenceDelay;
|
||||||
/// Called when a midi song stops playing.
|
|
||||||
/// </summary>
|
|
||||||
public event Action? OnMidiPlaybackEnded;
|
|
||||||
|
|
||||||
[Dependency] private readonly IMidiManager _midiManager = default!;
|
public uint SequenceStartTick;
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
|
||||||
|
|
||||||
private IMidiRenderer? _renderer;
|
public TimeSpan LastMeasured = TimeSpan.MinValue;
|
||||||
|
|
||||||
private InstrumentSystem _instrumentSystem = default!;
|
public int SentWithinASec;
|
||||||
|
|
||||||
[DataField("program")]
|
/// <summary>
|
||||||
private byte _instrumentProgram = 1;
|
/// A queue of MidiEvents to be sent to the server.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public readonly List<MidiEvent> MidiEventBuffer = new();
|
||||||
|
|
||||||
[DataField("bank")]
|
/// <summary>
|
||||||
private byte _instrumentBank;
|
/// 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")]
|
/// <summary>
|
||||||
private bool _allowPercussion;
|
/// Whether the midi renderer is listening for midi input or not.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public bool IsInputOpen => Renderer?.Status == MidiRendererStatus.Input;
|
||||||
|
|
||||||
[DataField("allowProgramChange")]
|
/// <summary>
|
||||||
private bool _allowProgramChange;
|
/// Whether the midi renderer is alive or not.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public bool IsRendererAlive => Renderer != null;
|
||||||
|
|
||||||
[DataField("respectMidiLimits")]
|
[ViewVariables]
|
||||||
private bool _respectMidiLimits = true;
|
public int PlayerTotalTick => Renderer?.PlayerTotalTick ?? 0;
|
||||||
|
|
||||||
/// <summary>
|
[ViewVariables]
|
||||||
/// A queue of MidiEvents to be sent to the server.
|
public int PlayerTick => Renderer?.PlayerTick ?? 0;
|
||||||
/// </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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public void PlaybackEndedInvoke() => OnMidiPlaybackEnded?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.CCVar;
|
||||||
|
using Content.Shared.Instruments;
|
||||||
|
using Content.Shared.Physics;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Client.Audio.Midi;
|
||||||
|
using Robust.Shared.Audio.Midi;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
using SharpFont;
|
||||||
|
|
||||||
namespace Content.Client.Instruments
|
namespace Content.Client.Instruments
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[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 IGameTiming _gameTiming = default!;
|
||||||
[Dependency] private readonly IConfigurationManager _cfg = 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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true);
|
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true);
|
||||||
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerSecond, OnMaxMidiEventsPerSecondChanged, 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 override void Shutdown()
|
||||||
public int MaxMidiEventsPerSecond { get; private set; }
|
{
|
||||||
|
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)
|
private void OnMaxMidiEventsPerSecondChanged(int obj)
|
||||||
{
|
{
|
||||||
@@ -35,6 +240,73 @@ namespace Content.Client.Instruments
|
|||||||
MaxMidiEventsPerBatch = obj;
|
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)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
@@ -44,9 +316,61 @@ namespace Content.Client.Instruments
|
|||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Robust.Client.UserInterface;
|
|||||||
using Robust.Client.UserInterface.CustomControls;
|
using Robust.Client.UserInterface.CustomControls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Input;
|
using Robust.Shared.Input;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
@@ -98,7 +99,8 @@ namespace Content.Client.Instruments.UI
|
|||||||
// While we're waiting, load it into memory.
|
// While we're waiting, load it into memory.
|
||||||
await Task.WhenAll(Timer.Delay(100), file.CopyToAsync(memStream));
|
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;
|
return;
|
||||||
|
|
||||||
MidiPlaybackSetButtonsDisabled(false);
|
MidiPlaybackSetButtonsDisabled(false);
|
||||||
@@ -108,16 +110,19 @@ namespace Content.Client.Instruments.UI
|
|||||||
|
|
||||||
private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj)
|
private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj)
|
||||||
{
|
{
|
||||||
|
var instrumentSystem = EntitySystem.Get<InstrumentSystem>();
|
||||||
|
|
||||||
if (obj.Pressed)
|
if (obj.Pressed)
|
||||||
{
|
{
|
||||||
if (!PlayCheck())
|
if (!PlayCheck())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
MidiStopButtonOnPressed(null);
|
MidiStopButtonOnPressed(null);
|
||||||
_owner.Instrument?.OpenInput();
|
if(_owner.Instrument is {} instrument)
|
||||||
|
instrumentSystem.OpenInput(instrument.OwnerUid, instrument);
|
||||||
}
|
}
|
||||||
else
|
else if(_owner.Instrument is {} instrument)
|
||||||
_owner.Instrument?.CloseInput();
|
instrumentSystem.CloseInput(instrument.OwnerUid, false, instrument);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool PlayCheck()
|
private bool PlayCheck()
|
||||||
@@ -149,28 +154,35 @@ namespace Content.Client.Instruments.UI
|
|||||||
private void MidiStopButtonOnPressed(ButtonEventArgs? obj)
|
private void MidiStopButtonOnPressed(ButtonEventArgs? obj)
|
||||||
{
|
{
|
||||||
MidiPlaybackSetButtonsDisabled(true);
|
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)
|
private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj)
|
||||||
{
|
{
|
||||||
if (_owner.Instrument != null)
|
if (_owner.Instrument == null)
|
||||||
_owner.Instrument.LoopMidi = obj.Pressed;
|
return;
|
||||||
|
|
||||||
|
_owner.Instrument.LoopMidi = obj.Pressed;
|
||||||
|
_owner.Instrument.DirtyRenderer = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlaybackSliderSeek(Range _)
|
private void PlaybackSliderSeek(Range _)
|
||||||
{
|
{
|
||||||
// Do not seek while still grabbing.
|
// 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)
|
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)
|
protected override void FrameUpdate(FrameEventArgs args)
|
||||||
|
|||||||
@@ -1,254 +1,30 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using Content.Server.Stunnable;
|
|
||||||
using Content.Server.Stunnable.Components;
|
|
||||||
using Content.Server.UserInterface;
|
using Content.Server.UserInterface;
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Hands;
|
|
||||||
using Content.Shared.Instruments;
|
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.GameObjects;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Enums;
|
|
||||||
using Robust.Shared.GameObjects;
|
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;
|
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]
|
[ViewVariables]
|
||||||
public class InstrumentComponent
|
public int BatchesDropped = 0;
|
||||||
: SharedInstrumentComponent
|
|
||||||
{
|
|
||||||
private InstrumentSystem _instrumentSystem = default!;
|
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private bool _playing = false;
|
public int LaggedBatches = 0;
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
private float _timer = 0f;
|
public int MidiEventCount = 0;
|
||||||
|
|
||||||
[ViewVariables]
|
public IPlayerSession? InstrumentPlayer =>
|
||||||
private int _batchesDropped = 0;
|
Owner.GetComponentOrNull<ActivatableUIComponent>()?.CurrentSingleUser
|
||||||
|
?? Owner.GetComponentOrNull<ActorComponent>()?.PlayerSession;
|
||||||
[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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(InstrumentUiKey.Key);
|
||||||
}
|
}
|
||||||
|
|||||||
47
Content.Server/Instruments/InstrumentSystem.CVars.cs
Normal file
47
Content.Server/Instruments/InstrumentSystem.CVars.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +1,177 @@
|
|||||||
using Content.Shared;
|
using System;
|
||||||
using Content.Shared.CCVar;
|
using System.Linq;
|
||||||
|
using Content.Server.Stunnable;
|
||||||
using Content.Server.UserInterface;
|
using Content.Server.UserInterface;
|
||||||
|
using Content.Shared.Instruments;
|
||||||
|
using Content.Shared.Popups;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Localization;
|
||||||
|
|
||||||
namespace Content.Server.Instruments
|
namespace Content.Server.Instruments;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
internal sealed class InstrumentSystem : EntitySystem
|
[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();
|
RaiseNetworkEvent(new InstrumentStopMidiEvent(uid));
|
||||||
|
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int MaxMidiEventsPerSecond { get; private set; }
|
instrument.Playing = false;
|
||||||
public int MaxMidiEventsPerBatch { get; private set; }
|
instrument.LastSequencerTick = 0;
|
||||||
public int MaxMidiBatchesDropped { get; private set; }
|
instrument.BatchesDropped = 0;
|
||||||
public int MaxMidiLaggedBatches { get; private set; }
|
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)
|
if (instrument.RespectMidiLimits)
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,112 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Robust.Shared.Analyzers;
|
||||||
using Robust.Shared.Audio.Midi;
|
using Robust.Shared.Audio.Midi;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
using Robust.Shared.ViewVariables;
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
namespace Content.Shared.Instruments
|
namespace Content.Shared.Instruments;
|
||||||
|
|
||||||
|
[NetworkedComponent, Friend(typeof(SharedInstrumentSystem))]
|
||||||
|
public class SharedInstrumentComponent : Component
|
||||||
{
|
{
|
||||||
[NetworkedComponent()]
|
public override string Name => "Instrument";
|
||||||
public class SharedInstrumentComponent : Component
|
|
||||||
|
[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";
|
Uid = uid;
|
||||||
|
|
||||||
[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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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,
|
||||||
|
}
|
||||||
|
|||||||
50
Content.Shared/Instruments/SharedInstrumentSystem.cs
Normal file
50
Content.Shared/Instruments/SharedInstrumentSystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user