diff --git a/Content.Client/GameObjects/Components/Instruments/InstrumentComponent.cs b/Content.Client/GameObjects/Components/Instruments/InstrumentComponent.cs
index db85f689e1..bcceead7b9 100644
--- a/Content.Client/GameObjects/Components/Instruments/InstrumentComponent.cs
+++ b/Content.Client/GameObjects/Components/Instruments/InstrumentComponent.cs
@@ -1,17 +1,19 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Content.Shared.GameObjects.Components.Instruments;
using JetBrains.Annotations;
+using NFluidsynth;
using Robust.Shared.GameObjects;
using Robust.Client.Audio.Midi;
-using Robust.Shared.Audio.Midi;
-using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
+using Logger = Robust.Shared.Log.Logger;
+using MidiEvent = Robust.Shared.Audio.Midi.MidiEvent;
using Timer = Robust.Shared.Timers.Timer;
@@ -20,6 +22,8 @@ namespace Content.Client.GameObjects.Components.Instruments
[RegisterComponent]
public class InstrumentComponent : SharedInstrumentComponent
{
+ public const float TimeBetweenNetMessages = 1.0f;
+
///
/// Called when a midi song stops playing.
///
@@ -27,17 +31,22 @@ namespace Content.Client.GameObjects.Components.Instruments
#pragma warning disable 649
[Dependency] private IMidiManager _midiManager;
- [Dependency] private readonly IGameTiming _timing;
+ [Dependency] private readonly IGameTiming _gameTiming;
#pragma warning restore 649
[CanBeNull]
private IMidiRenderer _renderer;
- private int _instrumentProgram = 1;
+ private byte _instrumentProgram = 1;
+ private uint _syncSequencerTick;
///
/// A queue of MidiEvents to be sent to the server.
///
- private Queue _eventQueue = new Queue();
+ [ViewVariables]
+ private readonly Queue _midiQueue = new Queue();
+
+ [ViewVariables]
+ private float _timer = 0f;
///
/// Whether a midi song will loop or not.
@@ -59,7 +68,7 @@ namespace Content.Client.GameObjects.Components.Instruments
/// Changes the instrument the midi renderer will play.
///
[ViewVariables(VVAccess.ReadWrite)]
- public int InstrumentProgram
+ public byte InstrumentProgram
{
get => _instrumentProgram;
set
@@ -84,61 +93,102 @@ namespace Content.Client.GameObjects.Components.Instruments
[ViewVariables]
public bool IsInputOpen => _renderer?.Status == MidiRendererStatus.Input;
+ ///
+ /// Whether the midi renderer is alive or not.
+ ///
+ [ViewVariables]
+ public bool IsRendererAlive => _renderer != null;
+
public override void Initialize()
{
base.Initialize();
IoCManager.InjectDependencies(this);
+ }
+
+ protected void SetupRenderer()
+ {
+ if (IsRendererAlive)
+ return;
+
_renderer = _midiManager.GetNewRenderer();
if (_renderer != null)
{
_renderer.MidiProgram = _instrumentProgram;
_renderer.TrackingEntity = Owner;
- _renderer.OnMidiPlayerFinished += () => { OnMidiPlaybackEnded?.Invoke(); };
+ _renderer.OnMidiPlayerFinished += () => { OnMidiPlaybackEnded?.Invoke(); EndRenderer(); SendNetworkMessage(new InstrumentStopMidiMessage()); };
}
}
+ protected void EndRenderer()
+ {
+ if (IsInputOpen)
+ CloseInput();
+
+ if (IsMidiOpen)
+ CloseMidi();
+
+ _renderer?.StopAllNotes();
+
+ var renderer = _renderer;
+
+ // We dispose of the synth two seconds from now to allow the last notes to stop from playing.
+ Timer.Spawn(2000, () => { renderer?.Dispose(); });
+ _renderer = null;
+ _midiQueue.Clear();
+ }
+
protected override void Shutdown()
{
base.Shutdown();
- _renderer?.Dispose();
+ EndRenderer();
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
- serializer.DataField(ref _instrumentProgram, "program", 1);
+ serializer.DataField(ref _instrumentProgram, "program", (byte)1);
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession session = null)
{
base.HandleNetworkMessage(message, channel, session);
- if (_renderer == null)
- {
- return;
- }
-
switch (message)
{
case InstrumentMidiEventMessage midiEventMessage:
// If we're the ones sending the MidiEvents, we ignore this message.
- if (IsInputOpen || IsMidiOpen) break;
- Timer.Spawn((int) (500 + _timing.CurTime.TotalMilliseconds - midiEventMessage.Timestamp),
- () => _renderer.SendMidiEvent(midiEventMessage.MidiEvent));
- break;
-
- case InstrumentStopMidiMessage _:
- _renderer.StopAllNotes();
- if (IsInputOpen) CloseInput();
- if (IsMidiOpen) CloseMidi();
+ if (!IsRendererAlive || IsInputOpen || IsMidiOpen) break;
+ for (var i = 0; i < midiEventMessage.MidiEvent.Length; i++)
+ {
+ var ev = midiEventMessage.MidiEvent[i];
+ var delta = ((uint)TimeBetweenNetMessages*1250) + ev.Timestamp - _syncSequencerTick;
+ _renderer?.ScheduleMidiEvent(ev, delta, true);
+ }
break;
}
}
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ base.HandleComponentState(curState, nextState);
+ if (!(curState is InstrumentState state)) return;
+
+ if (state.Playing)
+ {
+ SetupRenderer();
+ _syncSequencerTick = state.SequencerTick;
+ }
+ else
+ EndRenderer();
+ }
+
///
public bool OpenInput()
{
+ SetupRenderer();
+ SendNetworkMessage(new InstrumentStartMidiMessage());
+
if (_renderer != null && _renderer.OpenInput())
{
_renderer.OnMidiEvent += RendererOnMidiEvent;
@@ -156,13 +206,17 @@ namespace Content.Client.GameObjects.Components.Instruments
return false;
}
- _renderer.OnMidiEvent -= RendererOnMidiEvent;
+ EndRenderer();
+ SendNetworkMessage(new InstrumentStopMidiMessage());
return true;
}
///
public bool OpenMidi(string filename)
{
+ SetupRenderer();
+ SendNetworkMessage(new InstrumentStartMidiMessage());
+
if (_renderer == null || !_renderer.OpenMidi(filename))
{
return false;
@@ -180,7 +234,8 @@ namespace Content.Client.GameObjects.Components.Instruments
return false;
}
- _renderer.OnMidiEvent -= RendererOnMidiEvent;
+ EndRenderer();
+ SendNetworkMessage(new InstrumentStopMidiMessage());
return true;
}
@@ -190,7 +245,29 @@ namespace Content.Client.GameObjects.Components.Instruments
/// The received midi event
private void RendererOnMidiEvent(MidiEvent midiEvent)
{
- SendNetworkMessage(new InstrumentMidiEventMessage(midiEvent, _timing.CurTime.TotalMilliseconds));
+ _midiQueue.Enqueue(midiEvent);
+ }
+
+ public override void Update(float delta)
+ {
+ if (!IsMidiOpen && !IsInputOpen)
+ return;
+
+ _timer -= delta;
+
+ if (_timer > 0f) return;
+
+ SendAllMidiMessages();
+ _timer = TimeBetweenNetMessages;
+ }
+
+ private void SendAllMidiMessages()
+ {
+ if (_midiQueue.Count == 0) return;
+ var events = _midiQueue.ToArray();
+ _midiQueue.Clear();
+
+ SendNetworkMessage(new InstrumentMidiEventMessage(events));
}
}
}
diff --git a/Content.Client/GameObjects/EntitySystems/InstrumentSystem.cs b/Content.Client/GameObjects/EntitySystems/InstrumentSystem.cs
new file mode 100644
index 0000000000..6303475c30
--- /dev/null
+++ b/Content.Client/GameObjects/EntitySystems/InstrumentSystem.cs
@@ -0,0 +1,25 @@
+using Content.Client.GameObjects.Components.Instruments;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
+
+namespace Content.Client.GameObjects.EntitySystems
+{
+ public class InstrumentSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ EntityQuery = new TypeEntityQuery(typeof(InstrumentComponent));
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var entity in RelevantEntities)
+ {
+ entity.GetComponent().Update(frameTime);
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Instruments/InstrumentComponent.cs b/Content.Server/GameObjects/Components/Instruments/InstrumentComponent.cs
index b6512b8ee6..5f07b3d8b0 100644
--- a/Content.Server/GameObjects/Components/Instruments/InstrumentComponent.cs
+++ b/Content.Server/GameObjects/Components/Instruments/InstrumentComponent.cs
@@ -1,15 +1,25 @@
+using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Interfaces;
+using Content.Server.Interfaces.GameObjects;
+using Content.Server.Mobs;
using Content.Shared.GameObjects.Components.Instruments;
+using NFluidsynth;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
+using Robust.Server.Player;
+using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
+using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
+using Logger = Robust.Shared.Log.Logger;
+using MidiEvent = Robust.Shared.Audio.Midi.MidiEvent;
namespace Content.Server.GameObjects.Components.Instruments
{
@@ -18,12 +28,35 @@ namespace Content.Server.GameObjects.Components.Instruments
public class InstrumentComponent : SharedInstrumentComponent,
IDropped, IHandSelected, IHandDeselected, IActivate, IUse, IThrown
{
+ [Dependency] private IServerNotifyManager _notifyManager;
+
+ // These 2 values are quite high for now, and this could be easily abused. Change this if people are abusing it.
+ public const int MaxMidiEventsPerSecond = 20;
+ public const int MaxMidiEventsPerBatch = 50;
+ public const int MaxMidiBatchDropped = 20;
+
///
/// The client channel currently playing the instrument, or null if there's none.
///
- private ICommonSession _instrumentPlayer;
+ [ViewVariables]
+ private IPlayerSession _instrumentPlayer;
private bool _handheld;
+ [ViewVariables]
+ private bool _playing = false;
+
+ [ViewVariables]
+ private float _timer = 0f;
+
+ [ViewVariables]
+ private int _batchesDropped = 0;
+
+ [ViewVariables]
+ private uint _lastSequencerTick = 0;
+
+ [ViewVariables]
+ private int _midiEventCount = 0;
+
[ViewVariables]
private BoundUserInterface _userInterface;
@@ -33,6 +66,43 @@ namespace Content.Server.GameObjects.Components.Instruments
[ViewVariables]
public bool Handheld => _handheld;
+ ///
+ /// Whether the instrument is currently playing or not.
+ ///
+ [ViewVariables]
+ public bool Playing
+ {
+ get => _playing;
+ set
+ {
+ _playing = value;
+ Dirty();
+ }
+ }
+
+ public IPlayerSession InstrumentPlayer
+ {
+ get => _instrumentPlayer;
+ private set
+ {
+ Playing = false;
+
+ if(_instrumentPlayer != null)
+ _instrumentPlayer.PlayerStatusChanged -= OnPlayerStatusChanged;
+
+ _instrumentPlayer = value;
+
+ if(value != null)
+ _instrumentPlayer.PlayerStatusChanged += OnPlayerStatusChanged;
+ }
+ }
+
+ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
+ {
+ if (e.NewStatus == SessionStatus.Disconnected)
+ InstrumentPlayer = null;
+ }
+
public override void Initialize()
{
base.Initialize();
@@ -46,31 +116,52 @@ namespace Content.Server.GameObjects.Components.Instruments
serializer.DataField(ref _handheld, "handheld", false);
}
+ public override ComponentState GetComponentState()
+ {
+ return new InstrumentState(Playing, _lastSequencerTick);
+ }
+
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession session = null)
{
base.HandleNetworkMessage(message, channel, session);
- // If the client that sent the message isn't the client playing this instrument, we ignore it.
- if (session != _instrumentPlayer) return;
switch (message)
{
case InstrumentMidiEventMessage midiEventMsg:
- SendNetworkMessage(midiEventMsg);
+ if (!Playing || session != _instrumentPlayer)
+ return;
+
+ if (++_midiEventCount <= MaxMidiEventsPerSecond &&
+ midiEventMsg.MidiEvent.Length < MaxMidiEventsPerBatch)
+ SendNetworkMessage(midiEventMsg);
+ else
+ _batchesDropped++; // Batch dropped!
+
+ _lastSequencerTick = midiEventMsg.MidiEvent[^1].Timestamp;
+ break;
+ case InstrumentStartMidiMessage startMidi:
+ Playing = true;
+ break;
+ case InstrumentStopMidiMessage stopMidi:
+ Playing = false;
+ _lastSequencerTick = 0;
break;
}
}
public void Dropped(DroppedEventArgs eventArgs)
{
+ Playing = false;
SendNetworkMessage(new InstrumentStopMidiMessage());
- _instrumentPlayer = null;
+ InstrumentPlayer = null;
_userInterface.CloseAll();
}
public void Thrown(ThrownEventArgs eventArgs)
{
+ Playing = false;
SendNetworkMessage(new InstrumentStopMidiMessage());
- _instrumentPlayer = null;
+ InstrumentPlayer = null;
_userInterface.CloseAll();
}
@@ -80,11 +171,12 @@ namespace Content.Server.GameObjects.Components.Instruments
if (session == null) return;
- _instrumentPlayer = session;
+ InstrumentPlayer = session;
}
public void HandDeselected(HandDeselectedEventArgs eventArgs)
{
+ Playing = false;
SendNetworkMessage(new InstrumentStopMidiMessage());
_userInterface.CloseAll();
}
@@ -94,10 +186,10 @@ namespace Content.Server.GameObjects.Components.Instruments
if (Handheld || !eventArgs.User.TryGetComponent(out IActorComponent actor))
return;
- if (_instrumentPlayer != null)
+ if (InstrumentPlayer != null)
return;
- _instrumentPlayer = actor.playerSession;
+ InstrumentPlayer = actor.playerSession;
OpenUserInterface(actor.playerSession);
}
@@ -106,17 +198,18 @@ namespace Content.Server.GameObjects.Components.Instruments
if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
return false;
- if(_instrumentPlayer == actor.playerSession)
+ if(InstrumentPlayer == actor.playerSession)
OpenUserInterface(actor.playerSession);
return false;
}
private void UserInterfaceOnClosed(IPlayerSession player)
{
- if (!Handheld && player == _instrumentPlayer)
+ if (!Handheld && player == InstrumentPlayer)
{
- _instrumentPlayer = null;
+ InstrumentPlayer = null;
SendNetworkMessage(new InstrumentStopMidiMessage());
+ Playing = false;
}
}
@@ -124,5 +217,35 @@ namespace Content.Server.GameObjects.Components.Instruments
{
_userInterface.Open(session);
}
+
+ public override void Update(float delta)
+ {
+ base.Update(delta);
+
+ if (_instrumentPlayer != null && !ActionBlockerSystem.CanInteract(_instrumentPlayer.AttachedEntity))
+ InstrumentPlayer = null;
+
+ if (_batchesDropped > MaxMidiBatchDropped && InstrumentPlayer != null)
+ {
+ _batchesDropped = 0;
+ var mob = InstrumentPlayer.AttachedEntity;
+
+ _userInterface.CloseAll();
+
+ if (mob.TryGetComponent(out StunnableComponent stun))
+ stun.Stun(1);
+ else
+ StandingStateHelper.DropAllItemsInHands(mob);
+
+ InstrumentPlayer = null;
+
+ _notifyManager.PopupMessage(Owner, mob, "Your fingers cramp up from playing!");
+ }
+
+ _timer += delta;
+ if (_timer < 1) return;
+ _timer = 0f;
+ _midiEventCount = 0;
+ }
}
}
diff --git a/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs b/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs
new file mode 100644
index 0000000000..a64f1fec01
--- /dev/null
+++ b/Content.Server/GameObjects/EntitySystems/InstrumentSystem.cs
@@ -0,0 +1,25 @@
+using Content.Server.GameObjects.Components.Instruments;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
+
+namespace Content.Server.GameObjects.EntitySystems
+{
+ public class InstrumentSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ EntityQuery = new TypeEntityQuery(typeof(InstrumentComponent));
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var entity in RelevantEntities)
+ {
+ entity.GetComponent().Update(frameTime);
+ }
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/Instruments/SharedInstrumentComponent.cs b/Content.Shared/GameObjects/Components/Instruments/SharedInstrumentComponent.cs
index aef625a057..ef94e31e86 100644
--- a/Content.Shared/GameObjects/Components/Instruments/SharedInstrumentComponent.cs
+++ b/Content.Shared/GameObjects/Components/Instruments/SharedInstrumentComponent.cs
@@ -1,4 +1,5 @@
using System;
+using Content.Shared.BodySystem;
using Robust.Shared.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
@@ -9,6 +10,10 @@ namespace Content.Shared.GameObjects.Components.Instruments
{
public override string Name => "Instrument";
public override uint? NetID => ContentNetIDs.INSTRUMENTS;
+
+ public virtual void Update(float delta)
+ {
+ }
}
@@ -20,19 +25,39 @@ namespace Content.Shared.GameObjects.Components.Instruments
{
}
+ ///
+ /// This message is sent to the client to start the synth.
+ ///
+ [Serializable, NetSerializable]
+ public class InstrumentStartMidiMessage : ComponentMessage
+ {
+
+ }
+
///
/// This message carries a MidiEvent to be played on clients.
///
[Serializable, NetSerializable]
public class InstrumentMidiEventMessage : ComponentMessage
{
- public MidiEvent MidiEvent;
- public double Timestamp;
+ public MidiEvent[] MidiEvent;
- public InstrumentMidiEventMessage(MidiEvent midiEvent, double timestamp)
+ public InstrumentMidiEventMessage(MidiEvent[] midiEvent)
{
MidiEvent = midiEvent;
- Timestamp = timestamp;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class InstrumentState : ComponentState
+ {
+ public bool Playing { get; }
+ public uint SequencerTick { get; }
+
+ public InstrumentState(bool playing, uint sequencerTick = 0) : base(ContentNetIDs.INSTRUMENTS)
+ {
+ Playing = playing;
+ SequencerTick = sequencerTick;
}
}
diff --git a/RobustToolbox b/RobustToolbox
index feb5050b32..5d3f573f3c 160000
--- a/RobustToolbox
+++ b/RobustToolbox
@@ -1 +1 @@
-Subproject commit feb5050b32499c5f9e1e61ceae63bf74f6797a27
+Subproject commit 5d3f573f3c0dce859d22247d05f1f106075e8645