CatStation/Content.Server/Instruments/InstrumentSystem.cs
Dmitry 25c5e59248
Upstream (#3172)
Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Victor Shen <71985089+Vexerot@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kirus59 <145689588+Kirus59@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Stomf <5dorkydorks@gmail.com>
Co-authored-by: drakewill-CRL <46307022+drakewill-CRL@users.noreply.github.com>
Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com>
Co-authored-by: abadaba695 <spacestation13thingy@gmail.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Thinbug <101073555+Thinbug0@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: ActiveMammmoth <140334666+ActiveMammmoth@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Gentleman-Bird <dcgreen406@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: BIGZi0348 <svalker0348@gmail.com>
Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: Matthew Herber <32679887+happyrobot33@users.noreply.github.com>
Co-authored-by: Ertanic <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: eoineoineoin <helloworld@eoinrul.es>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: nikitosych <boriszyn@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: YoungThug <ramialanbagy@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: Vladislav Suchkov <20380250+murolem@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>
Co-authored-by: 3nderall <101940324+3nderall@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: J <billsmith116@gmail.com>
Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Spanky <scott@wearejacob.com>
Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: B_Kirill <153602297+B-Kirill@users.noreply.github.com>
Co-authored-by: archee1 <archee3@hotmail.co.uk>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: Krunklehorn <42424291+Krunklehorn@users.noreply.github.com>
Co-authored-by: OnyxTheBrave <131422822+OnyxTheBrave@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Zalycon <84675130+Zalycon@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: Dmitry <57028746+dimm00n@users.noreply.github.com>
2025-05-24 11:00:17 +03:00

455 lines
15 KiB
C#

using Content.Server.Administration;
using Content.Server.Interaction;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Administration;
using Content.Shared.Examine;
using Content.Shared.Instruments;
using Content.Shared.Instruments.UI;
using Content.Shared.Physics;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Instruments;
[UsedImplicitly]
public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly StunSystem _stuns = default!;
[Dependency] private readonly UserInterfaceSystem _bui = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
private const float MaxInstrumentBandRange = 10f;
// Band Requests are queued and delayed both to avoid metagaming and to prevent spamming it, since it's expensive.
private const float BandRequestDelay = 1.0f;
private TimeSpan _bandRequestTimer = TimeSpan.Zero;
private readonly List<InstrumentBandRequestBuiMessage> _bandRequestQueue = new();
public override void Initialize()
{
base.Initialize();
InitializeCVars();
SubscribeNetworkEvent<InstrumentMidiEventEvent>(OnMidiEventRx);
SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
{
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
subs.Event<BoundUIOpenedEvent>(OnBoundUIOpened);
subs.Event<InstrumentBandRequestBuiMessage>(OnBoundUIRequestBands);
});
SubscribeLocalEvent<InstrumentComponent, ComponentGetState>(OnStrumentGetState);
_conHost.RegisterCommand("addtoband", AddToBandCommand);
}
private void OnStrumentGetState(EntityUid uid, InstrumentComponent component, ref ComponentGetState args)
{
args.State = new InstrumentComponentState()
{
Playing = component.Playing,
InstrumentProgram = component.InstrumentProgram,
InstrumentBank = component.InstrumentBank,
AllowPercussion = component.AllowPercussion,
AllowProgramChange = component.AllowProgramChange,
RespectMidiLimits = component.RespectMidiLimits,
Master = GetNetEntity(component.Master),
FilteredChannels = component.FilteredChannels
};
}
[AdminCommand(AdminFlags.Fun)]
private void AddToBandCommand(IConsoleShell shell, string _, string[] args)
{
if (!NetEntity.TryParse(args[0], out var firstUidNet) || !TryGetEntity(firstUidNet, out var firstUid))
{
shell.WriteError($"Cannot parse first Uid");
return;
}
if (!NetEntity.TryParse(args[1], out var secondUidNet) || !TryGetEntity(secondUidNet, out var secondUid))
{
shell.WriteError($"Cannot parse second Uid");
return;
}
if (!HasComp<ActiveInstrumentComponent>(secondUid))
{
shell.WriteError($"Puppet instrument is not active!");
return;
}
var otherInstrument = Comp<InstrumentComponent>(secondUid.Value);
otherInstrument.Playing = true;
otherInstrument.Master = firstUid;
Dirty(secondUid.Value, otherInstrument);
}
private void OnMidiStart(InstrumentStartMidiEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
return;
instrument.Playing = true;
Dirty(uid, instrument);
}
private void OnMidiStop(InstrumentStopMidiEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
return;
Clean(uid, instrument);
}
private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
var master = GetEntity(msg.Master);
if (!HasComp<ActiveInstrumentComponent>(uid))
return;
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
return;
if (master != null)
{
if (!HasComp<ActiveInstrumentComponent>(master))
return;
if (!TryComp<InstrumentComponent>(master, out var masterInstrument) || masterInstrument.Master != null)
return;
instrument.Master = master;
instrument.FilteredChannels.SetAll(false);
instrument.Playing = true;
Dirty(uid, instrument);
return;
}
// Cleanup when disabling master...
if (master == null && instrument.Master != null)
{
Clean(uid, instrument);
}
}
private void OnMidiSetFilteredChannel(InstrumentSetFilteredChannelEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
return;
if (msg.Channel == RobustMidiEvent.PercussionChannel && !instrument.AllowPercussion)
return;
instrument.FilteredChannels[msg.Channel] = msg.Value;
if (msg.Value)
{
// Prevent stuck notes when turning off a channel... Shrimple.
RaiseNetworkEvent(new InstrumentMidiEventEvent(msg.Uid, new []{RobustMidiEvent.AllNotesOff((byte)msg.Channel, 0)}));
}
Dirty(uid, instrument);
}
private void OnBoundUIClosed(EntityUid uid, InstrumentComponent component, BoundUIClosedEvent args)
{
if (HasComp<ActiveInstrumentComponent>(uid)
&& !_bui.IsUiOpen(uid, args.UiKey))
{
RemComp<ActiveInstrumentComponent>(uid);
}
Clean(uid, component);
}
private void OnBoundUIOpened(EntityUid uid, InstrumentComponent component, BoundUIOpenedEvent args)
{
EnsureComp<ActiveInstrumentComponent>(uid);
Clean(uid, component);
}
private void OnBoundUIRequestBands(EntityUid uid, InstrumentComponent component, InstrumentBandRequestBuiMessage args)
{
foreach (var request in _bandRequestQueue)
{
// Prevent spamming requests for the same entity.
if (request.Entity == args.Entity)
return;
}
_bandRequestQueue.Add(args);
}
public (NetEntity, string)[] GetBands(EntityUid uid)
{
var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
if (Deleted(uid))
return Array.Empty<(NetEntity, string)>();
var list = new ValueList<(NetEntity, string)>();
var instrumentQuery = EntityManager.GetEntityQuery<InstrumentComponent>();
if (!TryComp(uid, out InstrumentComponent? originInstrument)
|| originInstrument.InstrumentPlayer is not {} originPlayer)
return Array.Empty<(NetEntity, string)>();
// It's probably faster to get all possible active instruments than all entities in range
var activeEnumerator = EntityManager.EntityQueryEnumerator<ActiveInstrumentComponent>();
while (activeEnumerator.MoveNext(out var entity, out _))
{
if (entity == uid)
continue;
// Don't grab puppet instruments.
if (!instrumentQuery.TryGetComponent(entity, out var instrument) || instrument.Master != null)
continue;
// We want to use the instrument player's name.
if (instrument.InstrumentPlayer is not {} playerUid)
continue;
// Maybe a bit expensive but oh well GetBands is queued and has a timer anyway.
// Make sure the instrument is visible
if (!_examineSystem.InRangeUnOccluded(uid, entity, MaxInstrumentBandRange, e => e == playerUid || e == originPlayer))
continue;
if (!metadataQuery.TryGetComponent(playerUid, out var playerMetadata)
|| !metadataQuery.TryGetComponent(entity, out var metadata))
continue;
list.Add((GetNetEntity(entity), $"{playerMetadata.EntityName} - {metadata.EntityName}"));
}
return list.ToArray();
}
public void Clean(EntityUid uid, InstrumentComponent? instrument = null)
{
if (!Resolve(uid, ref instrument))
return;
if (instrument.Playing)
{
var netUid = GetNetEntity(uid);
// Reset puppet instruments too.
RaiseNetworkEvent(new InstrumentMidiEventEvent(netUid, new[]{RobustMidiEvent.SystemReset(0)}));
RaiseNetworkEvent(new InstrumentStopMidiEvent(netUid));
}
instrument.Playing = false;
instrument.Master = null;
instrument.FilteredChannels.SetAll(false);
instrument.LastSequencerTick = 0;
instrument.BatchesDropped = 0;
instrument.LaggedBatches = 0;
Dirty(uid, instrument);
}
private void OnMidiEventRx(InstrumentMidiEventEvent msg, EntitySessionEventArgs args)
{
var uid = GetEntity(msg.Uid);
if (!TryComp(uid, out InstrumentComponent? instrument))
return;
if (!instrument.Playing
|| args.SenderSession.AttachedEntity != instrument.InstrumentPlayer
|| instrument.InstrumentPlayer == null
|| args.SenderSession.AttachedEntity is not { } attached)
{
return;
}
var send = true;
var minTick = uint.MaxValue;
var maxTick = uint.MinValue;
for (var i = 0; i < msg.MidiEvent.Length; i++)
{
var tick = msg.MidiEvent[i].Tick;
if (tick < minTick)
minTick = tick;
if (tick > maxTick)
maxTick = tick;
}
if (instrument.LastSequencerTick > minTick)
{
instrument.LaggedBatches++;
if (instrument.RespectMidiLimits)
{
if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (1 / 3d) + 1))
{
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-light-message"),
uid, attached, PopupType.SmallCaution);
}
else if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (2 / 3d) + 1))
{
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-serious-message"),
uid, attached, PopupType.MediumCaution);
}
}
if (instrument.LaggedBatches > MaxMidiLaggedBatches)
{
send = false;
}
}
if (++instrument.MidiEventCount > MaxMidiEventsPerSecond
|| msg.MidiEvent.Length > MaxMidiEventsPerBatch)
{
instrument.BatchesDropped++;
send = false;
}
instrument.LastSequencerTick = Math.Max(maxTick, minTick);
if (send || !instrument.RespectMidiLimits)
{
RaiseNetworkEvent(msg);
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_bandRequestQueue.Count > 0 && _bandRequestTimer < _timing.RealTime)
{
_bandRequestTimer = _timing.RealTime.Add(TimeSpan.FromSeconds(BandRequestDelay));
foreach (var request in _bandRequestQueue)
{
var entity = GetEntity(request.Entity);
var nearby = GetBands(entity);
_bui.ServerSendUiMessage(entity, request.UiKey, new InstrumentBandResponseBuiMessage(nearby), request.Actor);
}
_bandRequestQueue.Clear();
}
var activeQuery = EntityManager.GetEntityQuery<ActiveInstrumentComponent>();
var transformQuery = EntityManager.GetEntityQuery<TransformComponent>();
var query = AllEntityQuery<ActiveInstrumentComponent, InstrumentComponent>();
while (query.MoveNext(out var uid, out _, out var instrument))
{
if (instrument.Master is {} master)
{
if (Deleted(master))
{
Clean(uid, instrument);
}
var masterActive = activeQuery.CompOrNull(master);
if (masterActive == null)
{
Clean(uid, instrument);
}
var trans = transformQuery.GetComponent(uid);
var masterTrans = transformQuery.GetComponent(master);
if (!_transform.InRange(masterTrans.Coordinates, trans.Coordinates, 10f)
)
{
Clean(uid, instrument);
}
}
if (instrument.RespectMidiLimits &&
(instrument.BatchesDropped >= MaxMidiBatchesDropped
|| instrument.LaggedBatches >= MaxMidiLaggedBatches))
{
if (instrument.InstrumentPlayer is {Valid: true} mob)
{
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
uid, mob, PopupType.LargeCaution);
}
// Just in case
Clean(uid);
_bui.CloseUi(uid, InstrumentUiKey.Key);
}
instrument.Timer += frameTime;
if (instrument.Timer < 1)
continue;
instrument.Timer = 0f;
instrument.MidiEventCount = 0;
instrument.LaggedBatches = 0;
instrument.BatchesDropped = 0;
}
}
public void ToggleInstrumentUi(EntityUid uid, EntityUid actor, InstrumentComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
_bui.TryToggleUi(uid, InstrumentUiKey.Key, actor);
}
public override bool ResolveInstrument(EntityUid uid, ref SharedInstrumentComponent? component)
{
if (component is not null)
return true;
TryComp<InstrumentComponent>(uid, out var localComp);
component = localComp;
return component != null;
}
}