InstrumentSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. using Content.Server.Administration;
  2. using Content.Server.Interaction;
  3. using Content.Server.Popups;
  4. using Content.Server.Stunnable;
  5. using Content.Shared.Administration;
  6. using Content.Shared.Examine;
  7. using Content.Shared.Instruments;
  8. using Content.Shared.Instruments.UI;
  9. using Content.Shared.Physics;
  10. using Content.Shared.Popups;
  11. using JetBrains.Annotations;
  12. using Robust.Server.GameObjects;
  13. using Robust.Shared.Audio.Midi;
  14. using Robust.Shared.Collections;
  15. using Robust.Shared.Configuration;
  16. using Robust.Shared.Console;
  17. using Robust.Shared.GameStates;
  18. using Robust.Shared.Player;
  19. using Robust.Shared.Timing;
  20. namespace Content.Server.Instruments;
  21. [UsedImplicitly]
  22. public sealed partial class InstrumentSystem : SharedInstrumentSystem
  23. {
  24. [Dependency] private readonly IGameTiming _timing = default!;
  25. [Dependency] private readonly IConsoleHost _conHost = default!;
  26. [Dependency] private readonly IConfigurationManager _cfg = default!;
  27. [Dependency] private readonly StunSystem _stuns = default!;
  28. [Dependency] private readonly UserInterfaceSystem _bui = default!;
  29. [Dependency] private readonly PopupSystem _popup = default!;
  30. [Dependency] private readonly TransformSystem _transform = default!;
  31. [Dependency] private readonly ExamineSystemShared _examineSystem = default!;
  32. private const float MaxInstrumentBandRange = 10f;
  33. // Band Requests are queued and delayed both to avoid metagaming and to prevent spamming it, since it's expensive.
  34. private const float BandRequestDelay = 1.0f;
  35. private TimeSpan _bandRequestTimer = TimeSpan.Zero;
  36. private readonly List<InstrumentBandRequestBuiMessage> _bandRequestQueue = new();
  37. public override void Initialize()
  38. {
  39. base.Initialize();
  40. InitializeCVars();
  41. SubscribeNetworkEvent<InstrumentMidiEventEvent>(OnMidiEventRx);
  42. SubscribeNetworkEvent<InstrumentStartMidiEvent>(OnMidiStart);
  43. SubscribeNetworkEvent<InstrumentStopMidiEvent>(OnMidiStop);
  44. SubscribeNetworkEvent<InstrumentSetMasterEvent>(OnMidiSetMaster);
  45. SubscribeNetworkEvent<InstrumentSetFilteredChannelEvent>(OnMidiSetFilteredChannel);
  46. Subs.BuiEvents<InstrumentComponent>(InstrumentUiKey.Key, subs =>
  47. {
  48. subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
  49. subs.Event<BoundUIOpenedEvent>(OnBoundUIOpened);
  50. subs.Event<InstrumentBandRequestBuiMessage>(OnBoundUIRequestBands);
  51. });
  52. SubscribeLocalEvent<InstrumentComponent, ComponentGetState>(OnStrumentGetState);
  53. _conHost.RegisterCommand("addtoband", AddToBandCommand);
  54. }
  55. private void OnStrumentGetState(EntityUid uid, InstrumentComponent component, ref ComponentGetState args)
  56. {
  57. args.State = new InstrumentComponentState()
  58. {
  59. Playing = component.Playing,
  60. InstrumentProgram = component.InstrumentProgram,
  61. InstrumentBank = component.InstrumentBank,
  62. AllowPercussion = component.AllowPercussion,
  63. AllowProgramChange = component.AllowProgramChange,
  64. RespectMidiLimits = component.RespectMidiLimits,
  65. Master = GetNetEntity(component.Master),
  66. FilteredChannels = component.FilteredChannels
  67. };
  68. }
  69. [AdminCommand(AdminFlags.Fun)]
  70. private void AddToBandCommand(IConsoleShell shell, string _, string[] args)
  71. {
  72. if (!NetEntity.TryParse(args[0], out var firstUidNet) || !TryGetEntity(firstUidNet, out var firstUid))
  73. {
  74. shell.WriteError($"Cannot parse first Uid");
  75. return;
  76. }
  77. if (!NetEntity.TryParse(args[1], out var secondUidNet) || !TryGetEntity(secondUidNet, out var secondUid))
  78. {
  79. shell.WriteError($"Cannot parse second Uid");
  80. return;
  81. }
  82. if (!HasComp<ActiveInstrumentComponent>(secondUid))
  83. {
  84. shell.WriteError($"Puppet instrument is not active!");
  85. return;
  86. }
  87. var otherInstrument = Comp<InstrumentComponent>(secondUid.Value);
  88. otherInstrument.Playing = true;
  89. otherInstrument.Master = firstUid;
  90. Dirty(secondUid.Value, otherInstrument);
  91. }
  92. private void OnMidiStart(InstrumentStartMidiEvent msg, EntitySessionEventArgs args)
  93. {
  94. var uid = GetEntity(msg.Uid);
  95. if (!TryComp(uid, out InstrumentComponent? instrument))
  96. return;
  97. if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
  98. return;
  99. instrument.Playing = true;
  100. Dirty(uid, instrument);
  101. }
  102. private void OnMidiStop(InstrumentStopMidiEvent msg, EntitySessionEventArgs args)
  103. {
  104. var uid = GetEntity(msg.Uid);
  105. if (!TryComp(uid, out InstrumentComponent? instrument))
  106. return;
  107. if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
  108. return;
  109. Clean(uid, instrument);
  110. }
  111. private void OnMidiSetMaster(InstrumentSetMasterEvent msg, EntitySessionEventArgs args)
  112. {
  113. var uid = GetEntity(msg.Uid);
  114. var master = GetEntity(msg.Master);
  115. if (!HasComp<ActiveInstrumentComponent>(uid))
  116. return;
  117. if (!TryComp(uid, out InstrumentComponent? instrument))
  118. return;
  119. if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
  120. return;
  121. if (master != null)
  122. {
  123. if (!HasComp<ActiveInstrumentComponent>(master))
  124. return;
  125. if (!TryComp<InstrumentComponent>(master, out var masterInstrument) || masterInstrument.Master != null)
  126. return;
  127. instrument.Master = master;
  128. instrument.FilteredChannels.SetAll(false);
  129. instrument.Playing = true;
  130. Dirty(uid, instrument);
  131. return;
  132. }
  133. // Cleanup when disabling master...
  134. if (master == null && instrument.Master != null)
  135. {
  136. Clean(uid, instrument);
  137. }
  138. }
  139. private void OnMidiSetFilteredChannel(InstrumentSetFilteredChannelEvent msg, EntitySessionEventArgs args)
  140. {
  141. var uid = GetEntity(msg.Uid);
  142. if (!TryComp(uid, out InstrumentComponent? instrument))
  143. return;
  144. if (args.SenderSession.AttachedEntity != instrument.InstrumentPlayer)
  145. return;
  146. if (msg.Channel == RobustMidiEvent.PercussionChannel && !instrument.AllowPercussion)
  147. return;
  148. instrument.FilteredChannels[msg.Channel] = msg.Value;
  149. if (msg.Value)
  150. {
  151. // Prevent stuck notes when turning off a channel... Shrimple.
  152. RaiseNetworkEvent(new InstrumentMidiEventEvent(msg.Uid, new []{RobustMidiEvent.AllNotesOff((byte)msg.Channel, 0)}));
  153. }
  154. Dirty(uid, instrument);
  155. }
  156. private void OnBoundUIClosed(EntityUid uid, InstrumentComponent component, BoundUIClosedEvent args)
  157. {
  158. if (HasComp<ActiveInstrumentComponent>(uid)
  159. && !_bui.IsUiOpen(uid, args.UiKey))
  160. {
  161. RemComp<ActiveInstrumentComponent>(uid);
  162. }
  163. Clean(uid, component);
  164. }
  165. private void OnBoundUIOpened(EntityUid uid, InstrumentComponent component, BoundUIOpenedEvent args)
  166. {
  167. EnsureComp<ActiveInstrumentComponent>(uid);
  168. Clean(uid, component);
  169. }
  170. private void OnBoundUIRequestBands(EntityUid uid, InstrumentComponent component, InstrumentBandRequestBuiMessage args)
  171. {
  172. foreach (var request in _bandRequestQueue)
  173. {
  174. // Prevent spamming requests for the same entity.
  175. if (request.Entity == args.Entity)
  176. return;
  177. }
  178. _bandRequestQueue.Add(args);
  179. }
  180. public (NetEntity, string)[] GetBands(EntityUid uid)
  181. {
  182. var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
  183. if (Deleted(uid, metadataQuery))
  184. return Array.Empty<(NetEntity, string)>();
  185. var list = new ValueList<(NetEntity, string)>();
  186. var instrumentQuery = EntityManager.GetEntityQuery<InstrumentComponent>();
  187. if (!TryComp(uid, out InstrumentComponent? originInstrument)
  188. || originInstrument.InstrumentPlayer is not {} originPlayer)
  189. return Array.Empty<(NetEntity, string)>();
  190. // It's probably faster to get all possible active instruments than all entities in range
  191. var activeEnumerator = EntityManager.EntityQueryEnumerator<ActiveInstrumentComponent>();
  192. while (activeEnumerator.MoveNext(out var entity, out _))
  193. {
  194. if (entity == uid)
  195. continue;
  196. // Don't grab puppet instruments.
  197. if (!instrumentQuery.TryGetComponent(entity, out var instrument) || instrument.Master != null)
  198. continue;
  199. // We want to use the instrument player's name.
  200. if (instrument.InstrumentPlayer is not {} playerUid)
  201. continue;
  202. // Maybe a bit expensive but oh well GetBands is queued and has a timer anyway.
  203. // Make sure the instrument is visible
  204. if (!_examineSystem.InRangeUnOccluded(uid, entity, MaxInstrumentBandRange, e => e == playerUid || e == originPlayer))
  205. continue;
  206. if (!metadataQuery.TryGetComponent(playerUid, out var playerMetadata)
  207. || !metadataQuery.TryGetComponent(entity, out var metadata))
  208. continue;
  209. list.Add((GetNetEntity(entity), $"{playerMetadata.EntityName} - {metadata.EntityName}"));
  210. }
  211. return list.ToArray();
  212. }
  213. public void Clean(EntityUid uid, InstrumentComponent? instrument = null)
  214. {
  215. if (!Resolve(uid, ref instrument))
  216. return;
  217. if (instrument.Playing)
  218. {
  219. var netUid = GetNetEntity(uid);
  220. // Reset puppet instruments too.
  221. RaiseNetworkEvent(new InstrumentMidiEventEvent(netUid, new[]{RobustMidiEvent.SystemReset(0)}));
  222. RaiseNetworkEvent(new InstrumentStopMidiEvent(netUid));
  223. }
  224. instrument.Playing = false;
  225. instrument.Master = null;
  226. instrument.FilteredChannels.SetAll(false);
  227. instrument.LastSequencerTick = 0;
  228. instrument.BatchesDropped = 0;
  229. instrument.LaggedBatches = 0;
  230. Dirty(uid, instrument);
  231. }
  232. private void OnMidiEventRx(InstrumentMidiEventEvent msg, EntitySessionEventArgs args)
  233. {
  234. var uid = GetEntity(msg.Uid);
  235. if (!TryComp(uid, out InstrumentComponent? instrument))
  236. return;
  237. if (!instrument.Playing
  238. || args.SenderSession.AttachedEntity != instrument.InstrumentPlayer
  239. || instrument.InstrumentPlayer == null
  240. || args.SenderSession.AttachedEntity is not { } attached)
  241. {
  242. return;
  243. }
  244. var send = true;
  245. var minTick = uint.MaxValue;
  246. var maxTick = uint.MinValue;
  247. for (var i = 0; i < msg.MidiEvent.Length; i++)
  248. {
  249. var tick = msg.MidiEvent[i].Tick;
  250. if (tick < minTick)
  251. minTick = tick;
  252. if (tick > maxTick)
  253. maxTick = tick;
  254. }
  255. if (instrument.LastSequencerTick > minTick)
  256. {
  257. instrument.LaggedBatches++;
  258. if (instrument.RespectMidiLimits)
  259. {
  260. if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (1 / 3d) + 1))
  261. {
  262. _popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-light-message"),
  263. uid, attached, PopupType.SmallCaution);
  264. }
  265. else if (instrument.LaggedBatches == (int) (MaxMidiLaggedBatches * (2 / 3d) + 1))
  266. {
  267. _popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-serious-message"),
  268. uid, attached, PopupType.MediumCaution);
  269. }
  270. }
  271. if (instrument.LaggedBatches > MaxMidiLaggedBatches)
  272. {
  273. send = false;
  274. }
  275. }
  276. if (++instrument.MidiEventCount > MaxMidiEventsPerSecond
  277. || msg.MidiEvent.Length > MaxMidiEventsPerBatch)
  278. {
  279. instrument.BatchesDropped++;
  280. send = false;
  281. }
  282. instrument.LastSequencerTick = Math.Max(maxTick, minTick);
  283. if (send || !instrument.RespectMidiLimits)
  284. {
  285. RaiseNetworkEvent(msg);
  286. }
  287. }
  288. public override void Update(float frameTime)
  289. {
  290. base.Update(frameTime);
  291. if (_bandRequestQueue.Count > 0 && _bandRequestTimer < _timing.RealTime)
  292. {
  293. _bandRequestTimer = _timing.RealTime.Add(TimeSpan.FromSeconds(BandRequestDelay));
  294. foreach (var request in _bandRequestQueue)
  295. {
  296. var entity = GetEntity(request.Entity);
  297. var nearby = GetBands(entity);
  298. _bui.ServerSendUiMessage(entity, request.UiKey, new InstrumentBandResponseBuiMessage(nearby), request.Actor);
  299. }
  300. _bandRequestQueue.Clear();
  301. }
  302. var activeQuery = EntityManager.GetEntityQuery<ActiveInstrumentComponent>();
  303. var metadataQuery = EntityManager.GetEntityQuery<MetaDataComponent>();
  304. var transformQuery = EntityManager.GetEntityQuery<TransformComponent>();
  305. var query = AllEntityQuery<ActiveInstrumentComponent, InstrumentComponent>();
  306. while (query.MoveNext(out var uid, out _, out var instrument))
  307. {
  308. if (instrument.Master is {} master)
  309. {
  310. if (Deleted(master, metadataQuery))
  311. {
  312. Clean(uid, instrument);
  313. }
  314. var masterActive = activeQuery.CompOrNull(master);
  315. if (masterActive == null)
  316. {
  317. Clean(uid, instrument);
  318. }
  319. var trans = transformQuery.GetComponent(uid);
  320. var masterTrans = transformQuery.GetComponent(master);
  321. if (!_transform.InRange(masterTrans.Coordinates, trans.Coordinates, 10f)
  322. )
  323. {
  324. Clean(uid, instrument);
  325. }
  326. }
  327. if (instrument.RespectMidiLimits &&
  328. (instrument.BatchesDropped >= MaxMidiBatchesDropped
  329. || instrument.LaggedBatches >= MaxMidiLaggedBatches))
  330. {
  331. if (instrument.InstrumentPlayer is {Valid: true} mob)
  332. {
  333. _stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
  334. _popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
  335. uid, mob, PopupType.LargeCaution);
  336. }
  337. // Just in case
  338. Clean(uid);
  339. _bui.CloseUi(uid, InstrumentUiKey.Key);
  340. }
  341. instrument.Timer += frameTime;
  342. if (instrument.Timer < 1)
  343. continue;
  344. instrument.Timer = 0f;
  345. instrument.MidiEventCount = 0;
  346. instrument.LaggedBatches = 0;
  347. instrument.BatchesDropped = 0;
  348. }
  349. }
  350. public void ToggleInstrumentUi(EntityUid uid, EntityUid actor, InstrumentComponent? component = null)
  351. {
  352. if (!Resolve(uid, ref component))
  353. return;
  354. _bui.TryToggleUi(uid, InstrumentUiKey.Key, actor);
  355. }
  356. public override bool ResolveInstrument(EntityUid uid, ref SharedInstrumentComponent? component)
  357. {
  358. if (component is not null)
  359. return true;
  360. TryComp<InstrumentComponent>(uid, out var localComp);
  361. component = localComp;
  362. return component != null;
  363. }
  364. }