diff --git a/Content.Client/Commands/DebugCommands.cs b/Content.Client/Commands/DebugCommands.cs index 8fb0e75bb4..715dd99549 100644 --- a/Content.Client/Commands/DebugCommands.cs +++ b/Content.Client/Commands/DebugCommands.cs @@ -4,6 +4,7 @@ using Content.Shared.GameObjects.Components.Markers; using Robust.Client.Interfaces.Console; using Robust.Client.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; @@ -48,8 +49,7 @@ namespace Content.Client.Commands public bool Execute(IDebugConsole console, params string[] args) { - IoCManager.Resolve() - .GetEntitySystem() + EntitySystem.Get() .EnableAll ^= true; return false; diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 2676cf26a2..935db4b5ad 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -148,6 +148,14 @@ namespace Content.Client "RandomPottedPlant", "CommunicationsConsole", "BarSign", + "DroppedBodyPart", + "DroppedMechanism", + "BodyManager", + "Stunnable", + "SolarPanel", + "BodyScanner", + "Stunbaton", + "EmergencyClosetFill" }; foreach (var ignoreName in registerIgnore) @@ -239,7 +247,6 @@ namespace Content.Client IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); _baseClient.RunLevelChanged += (sender, args) => { diff --git a/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs index 359dbeb282..6556989340 100644 --- a/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs +++ b/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs @@ -30,7 +30,7 @@ namespace Content.Client.GameObjects.Components.Chemistry //Handle net updates public override void HandleComponentState(ComponentState curState, ComponentState nextState) { - var cast = (InjectorComponentState) curState; + var cast = (InjectorComponentState) curState; if (cast != null) { CurrentVolume = cast.CurrentVolume; @@ -64,7 +64,7 @@ namespace Content.Client.GameObjects.Components.Chemistry { return; } - + _parent._uiUpdateNeeded = false; //Update current volume and injector state diff --git a/Content.Client/GameObjects/Components/EmergencyLightComponent.cs b/Content.Client/GameObjects/Components/EmergencyLightComponent.cs new file mode 100644 index 0000000000..3bf8879d02 --- /dev/null +++ b/Content.Client/GameObjects/Components/EmergencyLightComponent.cs @@ -0,0 +1,45 @@ +using System; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Client.GameObjects.Components.Animations; +using Robust.Shared.Animations; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.Components +{ + [RegisterComponent] + public class EmergencyLightComponent : Component + { + public override string Name => "EmergencyLight"; + + protected override void Startup() + { + base.Startup(); + + var animation = new Animation + { + Length = TimeSpan.FromSeconds(4), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(PointLightComponent), + InterpolationMode = AnimationInterpolationMode.Linear, + Property = nameof(PointLightComponent.Rotation), + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.Zero, 0), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(1080), 4) + } + } + } + }; + + var playerComponent = Owner.EnsureComponent(); + playerComponent.Play(animation, "emergency"); + + playerComponent.AnimationCompleted += s => playerComponent.Play(animation, s); + } + } +} 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/Components/Kitchen/MicrowaveMenu.cs b/Content.Client/GameObjects/Components/Kitchen/MicrowaveMenu.cs index d2160d1c4d..b2f62fbcc5 100644 --- a/Content.Client/GameObjects/Components/Kitchen/MicrowaveMenu.cs +++ b/Content.Client/GameObjects/Components/Kitchen/MicrowaveMenu.cs @@ -137,10 +137,11 @@ namespace Content.Client.GameObjects.Components.Kitchen { Text = (index <= 0 ? 1 : index).ToString(), TextAlign = Label.AlignMode.Center, + ToggleMode = true, Group = CookTimeButtonGroup, }; CookTimeButtonVbox.AddChild(newButton); - newButton.OnPressed += args => + newButton.OnToggled += args => { OnCookTimeSelected?.Invoke(args); _cookTimeInfoLabel.Text = $"{Loc.GetString("COOK TIME")}: {VisualCookTime}"; diff --git a/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs b/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs index 46a31f069f..cfd7e09596 100644 --- a/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs +++ b/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs @@ -1,6 +1,5 @@ using System; using Content.Shared.GameObjects.Components.Mobs; -using Microsoft.CodeAnalysis.Completion; using Robust.Client.Animations; using Robust.Client.GameObjects; using Robust.Client.GameObjects.Components.Animations; diff --git a/Content.Client/GameObjects/Components/Sound/SoundComponent.cs b/Content.Client/GameObjects/Components/Sound/SoundComponent.cs index 03b5f088d3..94bfcf22ff 100644 --- a/Content.Client/GameObjects/Components/Sound/SoundComponent.cs +++ b/Content.Client/GameObjects/Components/Sound/SoundComponent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Content.Shared.GameObjects.Components.Sound; using Robust.Client.GameObjects.EntitySystems; using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Random; @@ -9,6 +10,7 @@ using Robust.Shared.IoC; using Robust.Shared.Players; using Robust.Shared.Serialization; using Robust.Shared.Timers; +using Robust.Shared.Utility; namespace Content.Client.GameObjects.Components.Sound { @@ -54,7 +56,7 @@ namespace Content.Client.GameObjects.Components.Sound Timer.Spawn((int) schedule.Delay + (_random.Next((int) schedule.RandomDelay)),() => { if (!schedule.Play) return; // We make sure this hasn't changed. - if (_audioSystem == null) _audioSystem = IoCManager.Resolve().GetEntitySystem(); + if (_audioSystem == null) _audioSystem = EntitySystem.Get(); _audioStreams.Add(schedule,_audioSystem.Play(schedule.Filename, Owner, schedule.AudioParams)); if (schedule.Times == 0) return; @@ -87,7 +89,7 @@ namespace Content.Client.GameObjects.Components.Sound public override void Initialize() { base.Initialize(); - IoCManager.Resolve().TryGetEntitySystem(out _audioSystem); + EntitySystem.TryGet(out _audioSystem); } public override void ExposeData(ObjectSerializer serializer) diff --git a/Content.Client/GameObjects/Components/Storage/StorageVisualizer2D.cs b/Content.Client/GameObjects/Components/Storage/StorageVisualizer2D.cs index 16269d4357..b64b5b974e 100644 --- a/Content.Client/GameObjects/Components/Storage/StorageVisualizer2D.cs +++ b/Content.Client/GameObjects/Components/Storage/StorageVisualizer2D.cs @@ -1,6 +1,7 @@ using Content.Shared.GameObjects.Components.Storage; using Robust.Client.GameObjects; using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; @@ -8,6 +9,7 @@ namespace Content.Client.GameObjects.Components.Storage { public sealed class StorageVisualizer2D : AppearanceVisualizer { + private string _stateBase; private string _stateOpen; private string _stateClosed; @@ -15,7 +17,12 @@ namespace Content.Client.GameObjects.Components.Storage { base.LoadData(node); - if (node.TryGetNode("state_open", out var child)) + if (node.TryGetNode("state", out var child)) + { + _stateBase = child.AsString(); + } + + if (node.TryGetNode("state_open", out child)) { _stateOpen = child.AsString(); } @@ -26,6 +33,19 @@ namespace Content.Client.GameObjects.Components.Storage } } + public override void InitializeEntity(IEntity entity) + { + if (!entity.TryGetComponent(out ISpriteComponent sprite)) + { + return; + } + + if (_stateBase != null) + { + sprite.LayerSetState(0, _stateBase); + } + } + public override void OnChangeData(AppearanceComponent component) { base.OnChangeData(component); @@ -36,7 +56,9 @@ namespace Content.Client.GameObjects.Components.Storage } component.TryGetData(StorageVisuals.Open, out bool open); - sprite.LayerSetState(StorageVisualLayers.Door, open ? _stateOpen : _stateClosed); + sprite.LayerSetState(StorageVisualLayers.Door, open + ? _stateOpen ?? $"{_stateBase}_open" + : _stateClosed ?? $"{_stateBase}_door"); } } 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.Client/GameObjects/EntitySystems/VerbSystem.cs b/Content.Client/GameObjects/EntitySystems/VerbSystem.cs index 7b91aaedaa..53b7cf87af 100644 --- a/Content.Client/GameObjects/EntitySystems/VerbSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/VerbSystem.cs @@ -1,25 +1,37 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; using Content.Client.State; +using Content.Client.UserInterface; +using Content.Client.Utility; using Content.Shared.GameObjects; using Content.Shared.GameObjects.EntitySystemMessages; using Content.Shared.Input; using JetBrains.Annotations; using Robust.Client.GameObjects.EntitySystems; +using Robust.Client.Graphics; +using Robust.Client.Graphics.Drawing; +using Robust.Client.Interfaces.GameObjects.Components; using Robust.Client.Interfaces.Input; +using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.State; +using Robust.Client.Interfaces.UserInterface; using Robust.Client.Player; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.Utility; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Input; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Utility; +using Timer = Robust.Shared.Timers.Timer; namespace Content.Client.GameObjects.EntitySystems { @@ -31,11 +43,21 @@ namespace Content.Client.GameObjects.EntitySystems [Dependency] private readonly IEntityManager _entityManager; [Dependency] private readonly IPlayerManager _playerManager; [Dependency] private readonly IInputManager _inputManager; + [Dependency] private readonly IItemSlotManager _itemSlotManager; + [Dependency] private readonly IGameTiming _gameTiming; + [Dependency] private readonly IUserInterfaceManager _userInterfaceManager; + [Dependency] private readonly IResourceCache _resourceCache; #pragma warning restore 649 - private VerbPopup _currentPopup; + private EntityList _currentEntityList; + private VerbPopup _currentVerbListRoot; + private VerbPopup _currentGroupList; + private EntityUid _currentEntity; + private bool IsAnyContextMenuOpen => _currentEntityList != null || _currentVerbListRoot != null; + + public override void Initialize() { base.Initialize(); @@ -51,29 +73,28 @@ namespace Content.Client.GameObjects.EntitySystems public void OpenContextMenu(IEntity entity, ScreenCoordinates screenCoordinates) { - if (_currentPopup != null) + if (_currentVerbListRoot != null) { - CloseContextMenu(); + CloseVerbMenu(); } _currentEntity = entity.Uid; - _currentPopup = new VerbPopup(); - _currentPopup.UserInterfaceManager.ModalRoot.AddChild(_currentPopup); - _currentPopup.OnPopupHide += CloseContextMenu; + _currentVerbListRoot = new VerbPopup(); + _userInterfaceManager.ModalRoot.AddChild(_currentVerbListRoot); + _currentVerbListRoot.OnPopupHide += CloseVerbMenu; - _currentPopup.List.AddChild(new Label {Text = "Waiting on Server..."}); + _currentVerbListRoot.List.AddChild(new Label {Text = "Waiting on Server..."}); RaiseNetworkEvent(new VerbSystemMessages.RequestVerbsMessage(_currentEntity)); - var size = _currentPopup.List.CombinedMinimumSize; - var box = UIBox2.FromDimensions(screenCoordinates.Position, size); - _currentPopup.Open(box); + var box = UIBox2.FromDimensions(screenCoordinates.Position, (1, 1)); + _currentVerbListRoot.Open(box); } private bool OnOpenContextMenu(in PointerInputCmdHandler.PointerInputCmdArgs args) { - if (_currentPopup != null) + if (IsAnyContextMenuOpen) { - CloseContextMenu(); + CloseAllMenus(); return true; } @@ -89,20 +110,29 @@ namespace Content.Client.GameObjects.EntitySystems return false; } - _currentPopup = new VerbPopup(); - _currentPopup.OnPopupHide += CloseContextMenu; - foreach (var entity in entities) + _currentEntityList = new EntityList(); + _currentEntityList.OnPopupHide += CloseAllMenus; + for (var i = 0; i < entities.Count; i++) { - var button = new Button {Text = entity.Name}; - _currentPopup.List.AddChild(button); - button.OnPressed += _ => OnContextButtonPressed(entity); + if (i != 0) + { + _currentEntityList.List.AddChild(new PanelContainer + { + CustomMinimumSize = (0, 2), + PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#333")} + }); + } + + var entity = entities[i]; + + _currentEntityList.List.AddChild(new EntityButton(this, entity)); } - _currentPopup.UserInterfaceManager.ModalRoot.AddChild(_currentPopup); + _userInterfaceManager.ModalRoot.AddChild(_currentEntityList); - var size = _currentPopup.List.CombinedMinimumSize; + var size = _currentEntityList.List.CombinedMinimumSize; var box = UIBox2.FromDimensions(args.ScreenCoordinates.Position, size); - _currentPopup.Open(box); + _currentEntityList.Open(box); return true; } @@ -119,28 +149,31 @@ namespace Content.Client.GameObjects.EntitySystems return; } - DebugTools.AssertNotNull(_currentPopup); + DebugTools.AssertNotNull(_currentVerbListRoot); - var buttons = new Dictionary>(); + var buttons = new Dictionary>(); + var groupIcons = new Dictionary(); - var vBox = _currentPopup.List; + var vBox = _currentVerbListRoot.List; vBox.DisposeAllChildren(); + + // Local variable so that scope capture ensures this is the correct value. + var curEntity = _currentEntity; + foreach (var data in msg.Verbs) { - var button = new Button {Text = data.Text, Disabled = !data.Available}; - if (data.Available) + var list = buttons.GetOrNew(data.Category); + + if (data.CategoryIcon != null && !groupIcons.ContainsKey(data.Category)) { - button.OnPressed += _ => - { - RaiseNetworkEvent(new VerbSystemMessages.UseVerbMessage(_currentEntity, data.Key)); - CloseContextMenu(); - }; + groupIcons.Add(data.Category, data.CategoryIcon); } - if(!buttons.ContainsKey(data.Category)) - buttons[data.Category] = new List