diff --git a/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs b/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs index 4700292400..45bcce40a2 100644 --- a/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs +++ b/Content.Client/AME/UI/AMEControllerBoundUserInterface.cs @@ -11,7 +11,7 @@ namespace Content.Client.AME.UI { private AMEWindow? _window; - public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs index bb0f870f5a..b9880eb8b6 100644 --- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs +++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs @@ -10,7 +10,7 @@ namespace Content.Client.Access.UI { private AgentIDCardWindow? _window; - public AgentIDCardBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public AgentIDCardBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs index c5d0227e40..9eb989f715 100644 --- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs +++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs @@ -13,7 +13,7 @@ namespace Content.Client.Access.UI [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; - public IdCardConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public IdCardConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } @@ -59,7 +59,7 @@ namespace Content.Client.Access.UI _window?.UpdateState(castState); } - public void SubmitData(string newFullName, string newJobTitle, List newAccessList) + public void SubmitData(string newFullName, string newJobTitle, List newAccessList, string newJobPrototype) { if (newFullName.Length > MaxFullNameLength) newFullName = newFullName[..MaxFullNameLength]; @@ -70,7 +70,8 @@ namespace Content.Client.Access.UI SendMessage(new WriteToTargetIdMessage( newFullName, newJobTitle, - newAccessList)); + newAccessList, + newJobPrototype)); } } } diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs index 359ad44d52..e4594856f9 100644 --- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs +++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs @@ -26,8 +26,10 @@ namespace Content.Client.Access.UI private string? _lastFullName; private string? _lastJobTitle; + private string? _lastJobProto; - public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, List accessLevels) + public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, + List accessLevels) { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); @@ -101,6 +103,7 @@ namespace Content.Client.Access.UI } JobTitleLineEdit.Text = Loc.GetString(job.Name); + args.Button.SelectId(args.Id); ClearAllAccess(); @@ -181,17 +184,29 @@ namespace Content.Client.Access.UI } } + var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype); + if (jobIndex >= 0) + { + JobPresetOptionButton.SelectId(jobIndex); + } + _lastFullName = state.TargetIdFullName; _lastJobTitle = state.TargetIdJobTitle; + _lastJobProto = state.TargetIdJobPrototype; } private void SubmitData() { + // Don't send this if it isn't dirty. + var jobProtoDirty = _lastJobProto != null && + _jobPrototypeIds[JobPresetOptionButton.SelectedId] != _lastJobProto; + _owner.SubmitData( FullNameLineEdit.Text, JobTitleLineEdit.Text, // Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair - _accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList()); + _accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(), + jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty); } } } diff --git a/Content.Client/AirlockPainter/UI/AirlockPainterBoundUserInterface.cs b/Content.Client/AirlockPainter/UI/AirlockPainterBoundUserInterface.cs index ba3681cda7..12c8de926e 100644 --- a/Content.Client/AirlockPainter/UI/AirlockPainterBoundUserInterface.cs +++ b/Content.Client/AirlockPainter/UI/AirlockPainterBoundUserInterface.cs @@ -8,7 +8,7 @@ namespace Content.Client.AirlockPainter.UI private AirlockPainterWindow? _window; public List Styles = new(); - public AirlockPainterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public AirlockPainterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs index b1182199f7..cd74077c12 100644 --- a/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/BlockGameBoundUserInterface.cs @@ -8,7 +8,7 @@ namespace Content.Client.Arcade.UI { private BlockGameMenu? _menu; - public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs index c9f4d9b79b..bb1582d127 100644 --- a/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs +++ b/Content.Client/Arcade/UI/SpaceVillainArcadeBoundUserInterface.cs @@ -11,7 +11,7 @@ namespace Content.Client.Arcade.UI //public SharedSpaceVillainArcadeComponent SpaceVillainArcade; - public SpaceVillainArcadeBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public SpaceVillainArcadeBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { SendAction(PlayerAction.RequestData); } diff --git a/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs b/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs index 0bf37c9097..dbfbd74352 100644 --- a/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs +++ b/Content.Client/Atmos/Monitor/UI/AirAlarmBoundUserInterface.cs @@ -18,9 +18,9 @@ public sealed class AirAlarmBoundUserInterface : BoundUserInterface protected override void Open() { base.Open(); - + _window = new AirAlarmWindow(); - + if (State != null) UpdateState(State); _window.OpenCentered(); diff --git a/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs b/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs index fe91547f5a..c79fa03a38 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs @@ -6,7 +6,7 @@ namespace Content.Client.Atmos.UI { public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface { - public GasAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs index c1f4d5205d..ba9c7037ed 100644 --- a/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasCanisterBoundUserInterface.cs @@ -14,7 +14,7 @@ namespace Content.Client.Atmos.UI private GasCanisterWindow? _window; - public GasCanisterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasCanisterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs index c0c5944995..21600cc63b 100644 --- a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs @@ -18,7 +18,7 @@ namespace Content.Client.Atmos.UI private GasFilterWindow? _window; private const float MaxTransferRate = Atmospherics.MaxTransferRate; - public GasFilterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasFilterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs b/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs index 900b91fea2..2cd140ef55 100644 --- a/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs +++ b/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs @@ -19,7 +19,7 @@ namespace Content.Client.Atmos.UI private GasMixerWindow? _window; private const float MaxPressure = Atmospherics.MaxOutputPressure; - public GasMixerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasMixerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs index 75a129fad7..2c7302f056 100644 --- a/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasPressurePumpBoundUserInterface.cs @@ -19,7 +19,7 @@ namespace Content.Client.Atmos.UI private GasPressurePumpWindow? _window; private const float MaxPressure = Atmospherics.MaxOutputPressure; - public GasPressurePumpBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasPressurePumpBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs b/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs index eb809e6f7c..5eee70808a 100644 --- a/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasThermomachineBoundUserInterface.cs @@ -18,7 +18,7 @@ namespace Content.Client.Atmos.UI private float _minTemp = 0.0f; private float _maxTemp = 0.0f; - public GasThermomachineBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasThermomachineBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs b/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs index 5348416f07..e211a58fed 100644 --- a/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs +++ b/Content.Client/Atmos/UI/GasVolumePumpBoundUserInterface.cs @@ -16,7 +16,7 @@ namespace Content.Client.Atmos.UI private GasVolumePumpWindow? _window; private const float MaxTransferRate = Atmospherics.MaxTransferRate; - public GasVolumePumpBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public GasVolumePumpBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Audio/BackgroundAudioSystem.cs b/Content.Client/Audio/BackgroundAudioSystem.cs index f4cbc5310b..3897e8e5d5 100644 --- a/Content.Client/Audio/BackgroundAudioSystem.cs +++ b/Content.Client/Audio/BackgroundAudioSystem.cs @@ -14,6 +14,8 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using System.Threading; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; using Timer = Robust.Shared.Timing.Timer; namespace Content.Client.Audio @@ -44,7 +46,7 @@ namespace Content.Client.Audio /// /// What the ambience has been set to. /// - private SoundCollectionPrototype _currentCollection = default!; + private SoundCollectionPrototype? _currentCollection; private CancellationTokenSource _timerCancelTokenSource = new(); private SoundCollectionPrototype _spaceAmbience = default!; @@ -58,12 +60,22 @@ namespace Content.Client.Audio _spaceAmbience = _prototypeManager.Index("SpaceAmbienceBase"); _currentCollection = _stationAmbience; + // TOOD: Ideally audio loading streamed better / we have more robust audio but this is quite annoying + var cache = IoCManager.Resolve(); + + foreach (var audio in _spaceAmbience.PickFiles) + { + cache.GetResource(audio.ToString()); + } + _configManager.OnValueChanged(CCVars.AmbienceVolume, AmbienceCVarChanged); _configManager.OnValueChanged(CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged); _configManager.OnValueChanged(CCVars.StationAmbienceEnabled, StationAmbienceCVarChanged); _configManager.OnValueChanged(CCVars.SpaceAmbienceEnabled, SpaceAmbienceCVarChanged); + SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(EntParentChanged); + SubscribeLocalEvent(OnPlayerDetached); _stateManager.OnStateChanged += StateManagerOnStateChanged; @@ -73,10 +85,28 @@ namespace Content.Client.Audio _gameTicker.LobbyStatusUpdated += LobbySongReceived; } + private void OnPlayerAttached(PlayerAttachedEvent ev) + { + if (!TryComp(ev.Entity, out var xform)) + return; + + CheckAmbience(xform); + } + + private void OnPlayerDetached(PlayerDetachedEvent ev) + { + EndAmbience(); + } + public override void Shutdown() { base.Shutdown(); + _configManager.UnsubValueChanged(CCVars.AmbienceVolume, AmbienceCVarChanged); + _configManager.UnsubValueChanged(CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged); + _configManager.UnsubValueChanged(CCVars.StationAmbienceEnabled, StationAmbienceCVarChanged); + _configManager.UnsubValueChanged(CCVars.SpaceAmbienceEnabled, SpaceAmbienceCVarChanged); + _stateManager.OnStateChanged -= StateManagerOnStateChanged; _client.PlayerJoinedServer -= OnJoin; @@ -88,15 +118,12 @@ namespace Content.Client.Audio EndLobbyMusic(); } - private void EntParentChanged(ref EntParentChangedMessage message) + private void CheckAmbience(TransformComponent xform) { - if(_playMan.LocalPlayer is null || _playMan.LocalPlayer.ControlledEntity != message.Entity || - !_timing.IsFirstTimePredicted) return; - - // Check if we traversed to grid. - if (message.Transform.GridUid != null) + if (xform.GridUid != null) { - if (_currentCollection == _stationAmbience) return; + if (_currentCollection == _stationAmbience) + return; ChangeAmbience(_stationAmbience); } else @@ -105,6 +132,15 @@ namespace Content.Client.Audio } } + private void EntParentChanged(ref EntParentChangedMessage message) + { + if(_playMan.LocalPlayer is null || _playMan.LocalPlayer.ControlledEntity != message.Entity || + !_timing.IsFirstTimePredicted) return; + + // Check if we traversed to grid. + CheckAmbience(message.Transform); + } + private void ChangeAmbience(SoundCollectionPrototype newAmbience) { if (_currentCollection == newAmbience) return; @@ -172,7 +208,8 @@ namespace Content.Client.Audio private void StartAmbience() { EndAmbience(); - if (!CanPlayCollection(_currentCollection)) return; + if (_currentCollection == null || !CanPlayCollection(_currentCollection)) + return; _playingCollection = _currentCollection; var file = _robustRandom.Pick(_currentCollection.PickFiles).ToString(); _ambientStream = SoundSystem.Play(file, Filter.Local(), _ambientParams.WithVolume(_ambientParams.Volume + _configManager.GetCVar(CCVars.AmbienceVolume))); @@ -197,6 +234,9 @@ namespace Content.Client.Audio private void StationAmbienceCVarChanged(bool enabled) { + if (_currentCollection == null) + return; + if (enabled && _stateManager.CurrentState is GameScreen && _currentCollection.ID == _stationAmbience.ID) { StartAmbience(); @@ -209,6 +249,9 @@ namespace Content.Client.Audio private void SpaceAmbienceCVarChanged(bool enabled) { + if (_currentCollection == null) + return; + if (enabled && _stateManager.CurrentState is GameScreen && _currentCollection.ID == _spaceAmbience.ID) { StartAmbience(); diff --git a/Content.Client/Body/UI/BodyScannerBoundUserInterface.cs b/Content.Client/Body/UI/BodyScannerBoundUserInterface.cs index 2b84e8764e..e5aacdb31c 100644 --- a/Content.Client/Body/UI/BodyScannerBoundUserInterface.cs +++ b/Content.Client/Body/UI/BodyScannerBoundUserInterface.cs @@ -14,7 +14,7 @@ namespace Content.Client.Body.UI [ViewVariables] private BodyScannerDisplay? _display; - public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { } + public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } protected override void Open() { diff --git a/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs index 95f7e84291..83f07a3678 100644 --- a/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs @@ -39,7 +39,7 @@ namespace Content.Client.Cargo.BUI /// private CargoProductPrototype? _product; - public CargoOrderConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public CargoOrderConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs index 6dd3df5f6e..f10ef90fbe 100644 --- a/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoShuttleConsoleBoundUserInterface.cs @@ -11,7 +11,7 @@ public sealed class CargoShuttleConsoleBoundUserInterface : BoundUserInterface { private CargoShuttleMenu? _menu; - public CargoShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {} + public CargoShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {} protected override void Open() { diff --git a/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs b/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs index 20cd1bcd89..924f4093cb 100644 --- a/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs +++ b/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs @@ -23,7 +23,7 @@ namespace Content.Client.CharacterAppearance { private MagicMirrorWindow? _window; - public MagicMirrorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public MagicMirrorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs index 04b9cc67ed..6217ad2288 100644 --- a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs @@ -14,7 +14,7 @@ namespace Content.Client.Chemistry.UI { private ChemMasterWindow? _window; - public ChemMasterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public ChemMasterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs index b1b2ef0413..7f7b0bb630 100644 --- a/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/ReagentDispenserBoundUserInterface.cs @@ -17,7 +17,7 @@ namespace Content.Client.Chemistry.UI private ReagentDispenserWindow? _window; private ReagentDispenserBoundUserInterfaceState? _lastState; - public ReagentDispenserBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public ReagentDispenserBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs index fd501a8fa3..cca7310efd 100644 --- a/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs +++ b/Content.Client/Chemistry/UI/TransferAmountBoundUserInterface.cs @@ -27,7 +27,7 @@ namespace Content.Client.Chemistry.UI _window.OpenCentered(); } - public TransferAmountBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public TransferAmountBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs b/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs index 77b2d9e807..8ab2c34627 100644 --- a/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs +++ b/Content.Client/CloningConsole/UI/CloningConsoleBoundUserInterface.cs @@ -9,7 +9,7 @@ namespace Content.Client.CloningConsole.UI { private CloningConsoleWindow? _window; - public CloningConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public CloningConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs index 30cb4db8cf..f8dbfc0fe4 100644 --- a/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs +++ b/Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs @@ -26,7 +26,7 @@ namespace Content.Client.Communications.UI public int Countdown => _expectedCountdownTime == null ? 0 : Math.Max((int)_expectedCountdownTime.Value.Subtract(_gameTiming.CurTime).TotalSeconds, 0); private TimeSpan? _expectedCountdownTime; - public CommunicationsConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public CommunicationsConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Computer/ComputerBoundUserInterface.cs b/Content.Client/Computer/ComputerBoundUserInterface.cs index cb216fc76e..2dccd68d21 100644 --- a/Content.Client/Computer/ComputerBoundUserInterface.cs +++ b/Content.Client/Computer/ComputerBoundUserInterface.cs @@ -26,7 +26,7 @@ namespace Content.Client.Computer } // Alas, this constructor has to be copied to the subclass. :( - public ComputerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {} + public ComputerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {} protected override void UpdateState(BoundUserInterfaceState state) { @@ -58,7 +58,7 @@ namespace Content.Client.Computer [Virtual] public class ComputerBoundUserInterfaceBase : BoundUserInterface { - public ComputerBoundUserInterfaceBase(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {} + public ComputerBoundUserInterfaceBase(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {} public new void SendMessage(BoundUserInterfaceMessage msg) { diff --git a/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs b/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs index 0bbec64126..9ec62e01a0 100644 --- a/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs +++ b/Content.Client/Configurable/UI/ConfigurationBoundUserInterface.cs @@ -10,7 +10,7 @@ namespace Content.Client.Configurable.UI { public Regex? Validation { get; internal set; } - public ConfigurationBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public ConfigurationBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index f3b023e60d..ba786f27ea 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -167,7 +167,7 @@ namespace Content.Client.Construction.UI continue; } - if (!string.IsNullOrEmpty(category) && category != Loc.GetString("construction-presenter-category-all")) + if (!string.IsNullOrEmpty(category) && category != Loc.GetString("construction-category-all")) { if (recipe.Category != category) continue; @@ -191,11 +191,11 @@ namespace Content.Client.Construction.UI var uniqueCategories = new HashSet(); // hard-coded to show all recipes - uniqueCategories.Add(Loc.GetString("construction-presenter-category-all")); + uniqueCategories.Add(Loc.GetString("construction-category-all")); foreach (var prototype in _prototypeManager.EnumeratePrototypes()) { - var category = Loc.GetString(prototype.Category); + var category = prototype.Category; if (!string.IsNullOrEmpty(category)) uniqueCategories.Add(category); diff --git a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs index d31eb35db9..b6634ca813 100644 --- a/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs +++ b/Content.Client/Crayon/UI/CrayonBoundUserInterface.cs @@ -10,7 +10,7 @@ namespace Content.Client.Crayon.UI { public sealed class CrayonBoundUserInterface : BoundUserInterface { - public CrayonBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public CrayonBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/CrewManifest/CrewManifestUi.xaml.cs b/Content.Client/CrewManifest/CrewManifestUi.xaml.cs index d36fa3a2c0..5c7d371a09 100644 --- a/Content.Client/CrewManifest/CrewManifestUi.xaml.cs +++ b/Content.Client/CrewManifest/CrewManifestUi.xaml.cs @@ -131,11 +131,11 @@ public sealed partial class CrewManifestUi : DefaultWindow foreach (var entry in entries) { - var name = new Label() + var name = new RichTextLabel() { HorizontalExpand = true, - Text = entry.Name }; + name.SetMessage(entry.Name); var titleContainer = new BoxContainer() { @@ -143,10 +143,8 @@ public sealed partial class CrewManifestUi : DefaultWindow HorizontalExpand = true }; - var title = new Label() - { - Text = Loc.GetString(entry.JobTitle) - }; + var title = new RichTextLabel(); + title.SetMessage(Loc.GetString(entry.JobTitle)); if (rsi != null) diff --git a/Content.Client/Damage/DamageVisualizer.cs b/Content.Client/Damage/DamageVisualizer.cs deleted file mode 100644 index 2c8c528659..0000000000 --- a/Content.Client/Damage/DamageVisualizer.cs +++ /dev/null @@ -1,857 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Shared.Damage; -using Content.Shared.Damage.Prototypes; -using Content.Shared.FixedPoint; -using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Prototypes; -using Robust.Shared.Reflection; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; - -// apologies in advance for all the != null checks, -// my IDE wouldn't stop complaining about these - -namespace Content.Client.Damage -{ - /// - /// A simple visualizer for any entity with a DamageableComponent - /// to display the status of how damaged it is. - /// - /// Can either be an overlay for an entity, or target multiple - /// layers on the same entity. - /// - /// This can be disabled dynamically by passing into SetData, - /// key DamageVisualizerKeys.Disabled, value bool - /// (DamageVisualizerKeys lives in Content.Shared.Damage) - /// - /// Damage layers, if targetting layers, can also be dynamically - /// disabled if needed by passing into SetData, the name/enum - /// of the sprite layer, and then passing in a bool value - /// (true to enable, false to disable). - /// - public sealed class DamageVisualizer : AppearanceVisualizer - { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - private const string _name = "DamageVisualizer"; - /// - /// Damage thresholds between damage state changes. - /// - /// If there are any negative thresholds, or there is - /// less than one threshold, the visualizer is marked - /// as invalid. - /// - /// - /// A 'zeroth' threshold is automatically added, - /// and this list is automatically sorted for - /// efficiency beforehand. As such, the zeroth - /// threshold is not required - and negative - /// thresholds are automatically caught as - /// invalid. The zeroth threshold automatically - /// sets all layers to invisible, so a sprite - /// isn't required for it. - /// - [DataField("thresholds", required: true)] - private List _thresholds = new(); - - /// - /// Layers to target, by layerMapKey. - /// If a target layer map key is invalid - /// (in essence, undefined), then the target - /// layer is removed from the list for efficiency. - /// - /// If no layers are valid, then the visualizer - /// is marked as invalid. - /// - /// If this is not defined, however, the visualizer - /// instead adds an overlay to the sprite. - /// - /// - /// Layers can be disabled here by passing - /// the layer's name as a key to SetData, - /// and passing in a bool set to either 'false' - /// to disable it, or 'true' to enable it. - /// Setting the layer as disabled will make it - /// completely invisible. - /// - [DataField("targetLayers")] - private List? _targetLayers; - - /// - /// The actual sprites for every damage group - /// that the entity should display visually. - /// - /// This is keyed by a damage group identifier - /// (for example, Brute), and has a value - /// of a DamageVisualizerSprite (see below) - /// - [DataField("damageOverlayGroups")] - private readonly Dictionary? _damageOverlayGroups; - - /// - /// Sets if you want sprites to overlay the - /// entity when damaged, or if you would - /// rather have each target layer's state - /// replaced by a different state - /// within its RSI. - /// - /// This cannot be set to false if: - /// - There are no target layers - /// - There is no damage group - /// - [DataField("overlay")] - private readonly bool _overlay = true; - - /// - /// A single damage group to target. - /// This should only be defined if - /// overlay is set to false. - /// If this is defined with damageSprites, - /// this will be ignored. - /// - /// - /// This is here because otherwise, - /// you would need several permutations - /// of group sprites depending on - /// what kind of damage combination - /// you would want, on which threshold. - /// - [DataField("damageGroup")] - private readonly string? _damageGroup; - - /// - /// Set this if you want incoming damage to be - /// divided. - /// - /// - /// This is more useful if you have similar - /// damage sprites inbetween entities, - /// but with different damage thresholds - /// and you want to avoid duplicating - /// these sprites. - /// - [DataField("damageDivisor")] - private float _divisor = 1; - - /// - /// Set this to track all damage, instead of specific groups. - /// - /// - /// This will only work if you have damageOverlay - /// defined - otherwise, it will not work. - /// - [DataField("trackAllDamage")] - private readonly bool _trackAllDamage = false; - /// - /// This is the overlay sprite used, if _trackAllDamage is - /// enabled. Supports no complex per-group layering, - /// just an actually simple damage overlay. See - /// DamageVisualizerSprite for more information. - /// - [DataField("damageOverlay")] - private readonly DamageVisualizerSprite? _damageOverlay; - - // deals with the edge case of human damage visuals not - // being in color without making a Dict - /// The RSI path for the damage visualizer - /// group overlay. - /// - /// - /// States in here will require one of four - /// forms: - /// - /// If tracking damage groups: - /// - {base_state}_{group}_{threshold} if targetting - /// a static layer on a sprite (either as an - /// overlay or as a state change) - /// - DamageOverlay_{group}_{threshold} if not - /// targetting a layer on a sprite. - /// - /// If not tracking damage groups: - /// - {base_state}_{threshold} if it is targetting - /// a layer - /// - DamageOverlay_{threshold} if not targetting - /// a layer. - /// - [DataField("sprite", required: true)] - public readonly string Sprite = default!; - - /// - /// The color of this sprite overlay. - /// Supports only hexadecimal format. - /// - [DataField("color")] - public readonly string? Color; - } - - /// - /// Initializes an entity to be managed by this appearance controller. - /// DO NOT assume this is your only entity. Visualizers are shared. - /// - [Obsolete("Subscribe to your component being initialised instead.")] - public override void InitializeEntity(EntityUid entity) - { - base.InitializeEntity(entity); - - IoCManager.InjectDependencies(this); - - var damageData = _entityManager.EnsureComponent(entity); - VerifyVisualizerSetup(entity, damageData); - if (damageData.Valid) - InitializeVisualizer(entity, damageData); - } - - private void VerifyVisualizerSetup(EntityUid entity, DamageVisualizerDataComponent damageData) - { - if (_thresholds.Count < 1) - { - Logger.ErrorS(_name, $"Thresholds were invalid for entity {entity}. Thresholds: {_thresholds}"); - damageData.Valid = false; - return; - } - - if (_divisor == 0) - { - Logger.ErrorS(_name, $"Divisor for {entity} is set to zero."); - damageData.Valid = false; - return; - } - - if (_overlay) - { - if (_damageOverlayGroups == null && _damageOverlay == null) - { - Logger.ErrorS(_name, $"Enabled overlay without defined damage overlay sprites on {entity}."); - damageData.Valid = false; - return; - } - - if (_trackAllDamage && _damageOverlay == null) - { - Logger.ErrorS(_name, $"Enabled all damage tracking without a damage overlay sprite on {entity}."); - damageData.Valid = false; - return; - } - - if (!_trackAllDamage && _damageOverlay != null) - { - Logger.WarningS(_name, $"Disabled all damage tracking with a damage overlay sprite on {entity}."); - damageData.Valid = false; - return; - } - - - if (_trackAllDamage && _damageOverlayGroups != null) - { - Logger.WarningS(_name, $"Enabled all damage tracking with damage overlay groups on {entity}."); - damageData.Valid = false; - return; - } - } - else if (!_overlay) - { - if (_targetLayers == null) - { - Logger.ErrorS(_name, $"Disabled overlay without target layers on {entity}."); - damageData.Valid = false; - return; - } - - if (_damageOverlayGroups != null || _damageOverlay != null) - { - Logger.ErrorS(_name, $"Disabled overlay with defined damage overlay sprites on {entity}."); - damageData.Valid = false; - return; - } - - if (_damageGroup == null) - { - Logger.ErrorS(_name, $"Disabled overlay without defined damage group on {entity}."); - damageData.Valid = false; - return; - } - } - - if (_damageOverlayGroups != null && _damageGroup != null) - { - Logger.WarningS(_name, $"Damage overlay sprites and damage group are both defined on {entity}."); - } - - if (_damageOverlay != null && _damageGroup != null) - { - Logger.WarningS(_name, $"Damage overlay sprites and damage group are both defined on {entity}."); - } - } - - private void InitializeVisualizer(EntityUid entity, DamageVisualizerDataComponent damageData) - { - if (!_entityManager.TryGetComponent(entity, out SpriteComponent? spriteComponent) - || !_entityManager.TryGetComponent(entity, out var damageComponent) - || !_entityManager.HasComponent(entity)) - return; - - _thresholds.Add(FixedPoint2.Zero); - _thresholds.Sort(); - - if (_thresholds[0] != 0) - { - Logger.ErrorS(_name, $"Thresholds were invalid for entity {entity}. Thresholds: {_thresholds}"); - damageData.Valid = false; - return; - } - - // If the damage container on our entity's DamageableComponent - // is not null, we can try to check through its groups. - if (damageComponent.DamageContainerID != null - && _prototypeManager.TryIndex(damageComponent.DamageContainerID, out var damageContainer)) - { - // Are we using damage overlay sprites by group? - // Check if the container matches the supported groups, - // and start cacheing the last threshold. - if (_damageOverlayGroups != null) - { - foreach (string damageType in _damageOverlayGroups.Keys) - { - if (!damageContainer.SupportedGroups.Contains(damageType)) - { - Logger.ErrorS(_name, $"Damage key {damageType} was invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - damageData.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); - } - } - // Are we tracking a single damage group without overlay instead? - // See if that group is in our entity's damage container. - else if (!_overlay && _damageGroup != null) - { - if (!damageContainer.SupportedGroups.Contains(_damageGroup)) - { - Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - damageData.LastThresholdPerGroup.Add(_damageGroup, FixedPoint2.Zero); - } - } - // Ditto above, but instead we go through every group. - else // oh boy! time to enumerate through every single group! - { - var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes() - .Select((p, _) => p.ID) - .ToList(); - if (_damageOverlayGroups != null) - foreach (string damageType in _damageOverlayGroups.Keys) - { - if (!damagePrototypeIdList.Contains(damageType)) - { - Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - damageData.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); - } - else if (_damageGroup != null) - { - if (!damagePrototypeIdList.Contains(_damageGroup)) - { - Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - damageData.LastThresholdPerGroup.Add(_damageGroup, FixedPoint2.Zero); - } - } - - // If we're targetting any layers, and the amount of - // layers is greater than zero, we start reserving - // all the layers needed to track damage groups - // on the entity. - if (_targetLayers != null && _targetLayers.Count > 0) - { - // This should ensure that the layers we're targetting - // are valid for the visualizer's use. - // - // If the layer doesn't have a base state, or - // the layer key just doesn't exist, we skip it. - foreach (var key in _targetLayers) - { - if (!spriteComponent.LayerMapTryGet(key, out int index) - || spriteComponent.LayerGetState(index).ToString() == null) - { - Logger.WarningS(_name, $"Layer at key {key} was invalid for entity {entity}."); - continue; - } - - damageData.TargetLayerMapKeys.Add(key); - }; - - // Similar to damage overlay groups, if none of the targetted - // sprite layers could be used, we display an error and - // invalidate the visualizer without crashing. - if (damageData.TargetLayerMapKeys.Count == 0) - { - Logger.ErrorS(_name, $"Target layers were invalid for entity {entity}."); - damageData.Valid = false; - return; - } - - // Otherwise, we start reserving layers. Since the filtering - // loop above ensures that all of these layers are not null, - // and have valid state IDs, there should be no issues. - foreach (object layer in damageData.TargetLayerMapKeys) - { - int layerCount = spriteComponent.AllLayers.Count(); - int index = spriteComponent.LayerMapGet(layer); - string layerState = spriteComponent.LayerGetState(index)!.ToString()!; - - if (index + 1 != layerCount) - { - index += 1; - } - - damageData.LayerMapKeyStates.Add(layer, layerState); - - // If we're an overlay, and we're targetting groups, - // we reserve layers per damage group. - if (_overlay && _damageOverlayGroups != null) - { - foreach (var (group, sprite) in _damageOverlayGroups) - { - AddDamageLayerToSprite(spriteComponent, - sprite, - $"{layerState}_{group}_{_thresholds[1]}", - $"{layer}{group}", - index); - } - damageData.DisabledLayers.Add(layer, false); - } - // If we're not targetting groups, and we're still - // using an overlay, we instead just add a general - // overlay that reflects on how much damage - // was taken. - else if (_damageOverlay != null) - { - AddDamageLayerToSprite(spriteComponent, - _damageOverlay, - $"{layerState}_{_thresholds[1]}", - $"{layer}trackDamage", - index); - damageData.DisabledLayers.Add(layer, false); - } - } - } - // If we're not targetting layers, however, - // we should ensure that we instead - // reserve it as an overlay. - else - { - if (_damageOverlayGroups != null) - { - foreach (var (group, sprite) in _damageOverlayGroups) - { - AddDamageLayerToSprite(spriteComponent, - sprite, - $"DamageOverlay_{group}_{_thresholds[1]}", - $"DamageOverlay{group}"); - damageData.TopMostLayerKey = $"DamageOverlay{group}"; - } - } - else if (_damageOverlay != null) - { - AddDamageLayerToSprite(spriteComponent, - _damageOverlay, - $"DamageOverlay_{_thresholds[1]}", - "DamageOverlay"); - damageData.TopMostLayerKey = $"DamageOverlay"; - } - } - } - - /// - /// Adds a damage tracking layer to a given sprite component. - /// - private void AddDamageLayerToSprite(SpriteComponent spriteComponent, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null) - { - int newLayer = spriteComponent.AddLayer( - new SpriteSpecifier.Rsi( - new ResourcePath(sprite.Sprite), state - ), index); - spriteComponent.LayerMapSet(mapKey, newLayer); - if (sprite.Color != null) - spriteComponent.LayerSetColor(newLayer, Color.FromHex(sprite.Color)); - spriteComponent.LayerSetVisible(newLayer, false); - } - - [Obsolete("Subscribe to AppearanceChangeEvent instead.")] - public override void OnChangeData(AppearanceComponent component) - { - var entities = _entityManager; - if (!entities.TryGetComponent(component.Owner, out DamageVisualizerDataComponent? damageData)) - return; - - if (!damageData.Valid) - return; - - // If this was passed into the component, we update - // the data to ensure that the current disabled - // bool matches. - if (component.TryGetData(DamageVisualizerKeys.Disabled, out var disabledStatus)) - if (disabledStatus != damageData.Disabled) - damageData.Disabled = disabledStatus; - - if (damageData.Disabled) - return; - - HandleDamage(component, damageData); - } - - private void HandleDamage(AppearanceComponent component, DamageVisualizerDataComponent damageData) - { - var entities = _entityManager; - if (!entities.TryGetComponent(component.Owner, out SpriteComponent? spriteComponent) - || !entities.TryGetComponent(component.Owner, out DamageableComponent? damageComponent)) - return; - - if (_targetLayers != null && _damageOverlayGroups != null) - UpdateDisabledLayers(spriteComponent, component, damageData); - - if (_overlay && _damageOverlayGroups != null && _targetLayers == null) - CheckOverlayOrdering(spriteComponent, damageData); - - if (component.TryGetData(DamageVisualizerKeys.ForceUpdate, out bool update) - && update) - { - ForceUpdateLayers(damageComponent, spriteComponent, damageData); - return; - } - - if (_trackAllDamage) - { - UpdateDamageVisuals(damageComponent, spriteComponent, damageData); - } - else if (component.TryGetData(DamageVisualizerKeys.DamageUpdateGroups, out DamageVisualizerGroupData data)) - { - UpdateDamageVisuals(data.GroupList, damageComponent, spriteComponent, damageData); - } - } - - /// - /// Checks if any layers were disabled in the last - /// data update. Disabled layers mean that the - /// layer will no longer be visible, or obtain - /// any damage updates. - /// - private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualizerDataComponent damageData) - { - foreach (var layer in damageData.TargetLayerMapKeys) - { - bool? layerStatus = null; - if (component.TryGetData(layer, out var layerStateEnum)) - layerStatus = layerStateEnum; - - if (layerStatus == null) - continue; - - if (damageData.DisabledLayers[layer] != (bool) layerStatus) - { - damageData.DisabledLayers[layer] = (bool) layerStatus; - if (!_trackAllDamage && _damageOverlayGroups != null) - foreach (string damageGroup in _damageOverlayGroups!.Keys) - spriteComponent.LayerSetVisible($"{layer}{damageGroup}", damageData.DisabledLayers[layer]); - else if (_trackAllDamage) - spriteComponent.LayerSetVisible($"{layer}trackDamage", damageData.DisabledLayers[layer]); - } - } - } - - /// - /// Checks the overlay ordering on the current - /// sprite component, compared to the - /// data for the visualizer. If the top - /// most layer doesn't match, the sprite - /// layers are recreated and placed on top. - /// - private void CheckOverlayOrdering(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - if (spriteComponent[damageData.TopMostLayerKey] != spriteComponent[spriteComponent.AllLayers.Count() - 1]) - { - if (!_trackAllDamage && _damageOverlayGroups != null) - { - foreach (var (damageGroup, sprite) in _damageOverlayGroups) - { - FixedPoint2 threshold = damageData.LastThresholdPerGroup[damageGroup]; - ReorderOverlaySprite(spriteComponent, - damageData, - sprite, - $"DamageOverlay{damageGroup}", - $"DamageOverlay_{damageGroup}", - threshold); - } - } - else if (_trackAllDamage && _damageOverlay != null) - { - ReorderOverlaySprite(spriteComponent, - damageData, - _damageOverlay, - $"DamageOverlay", - $"DamageOverlay", - damageData.LastDamageThreshold); - } - } - } - - private void ReorderOverlaySprite(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold) - { - spriteComponent.LayerMapTryGet(key, out int spriteLayer); - bool visibility = spriteComponent[spriteLayer].Visible; - spriteComponent.RemoveLayer(spriteLayer); - if (threshold == FixedPoint2.Zero) // these should automatically be invisible - threshold = _thresholds[1]; - spriteLayer = spriteComponent.AddLayer( - new SpriteSpecifier.Rsi( - new ResourcePath(sprite.Sprite), - $"{statePrefix}_{threshold}" - ), - spriteLayer); - spriteComponent.LayerMapSet(key, spriteLayer); - spriteComponent.LayerSetVisible(spriteLayer, visibility); - // this is somewhat iffy since it constantly reallocates - damageData.TopMostLayerKey = key; - } - - /// - /// Updates damage visuals without tracking - /// any damage groups. - /// - private void UpdateDamageVisuals(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageData.LastDamageThreshold, out FixedPoint2 threshold)) - return; - - damageData.LastDamageThreshold = threshold; - - if (_targetLayers != null) - { - foreach (var layerMapKey in damageData.TargetLayerMapKeys) - UpdateTargetLayer(spriteComponent, damageData, layerMapKey, threshold); - } - else - { - UpdateOverlay(spriteComponent, threshold); - } - } - - /// - /// Updates damage visuals by damage group, - /// according to the list of damage groups - /// passed into it. - /// - private void UpdateDamageVisuals(List delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - foreach (var damageGroup in delta) - { - if (!_overlay && damageGroup != _damageGroup) - continue; - - if (!_prototypeManager.TryIndex(damageGroup, out var damageGroupPrototype) - || !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out FixedPoint2 damageTotal)) - continue; - - if (!damageData.LastThresholdPerGroup.TryGetValue(damageGroup, out FixedPoint2 lastThreshold) - || !CheckThresholdBoundary(damageTotal, lastThreshold, out FixedPoint2 threshold)) - continue; - - damageData.LastThresholdPerGroup[damageGroup] = threshold; - - if (_targetLayers != null) - { - foreach (var layerMapKey in damageData.TargetLayerMapKeys) - UpdateTargetLayer(spriteComponent, damageData, layerMapKey, damageGroup, threshold); - } - else - { - UpdateOverlay(spriteComponent, damageGroup, threshold); - } - } - - } - - /// - /// Checks if a threshold boundary was passed. - /// - private bool CheckThresholdBoundary(FixedPoint2 damageTotal, FixedPoint2 lastThreshold, out FixedPoint2 threshold) - { - threshold = FixedPoint2.Zero; - damageTotal = damageTotal / _divisor; - int thresholdIndex = _thresholds.BinarySearch(damageTotal); - - if (thresholdIndex < 0) - { - thresholdIndex = ~thresholdIndex; - threshold = _thresholds[thresholdIndex - 1]; - } - else - { - threshold = _thresholds[thresholdIndex]; - } - - if (threshold == lastThreshold) - return false; - - return true; - } - - /// - /// This is the entry point for - /// forcing an update on all damage layers. - /// Does different things depending on - /// the configuration of the visualizer. - /// - private void ForceUpdateLayers(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData) - { - if (_damageOverlayGroups != null) - { - UpdateDamageVisuals(_damageOverlayGroups.Keys.ToList(), damageComponent, spriteComponent, damageData); - } - else if (_damageGroup != null) - { - UpdateDamageVisuals(new List(){ _damageGroup }, damageComponent, spriteComponent, damageData); - } - else if (_damageOverlay != null) - { - UpdateDamageVisuals(damageComponent, spriteComponent, damageData); - } - } - - /// - /// Updates a target layer. Without a damage group passed in, - /// it assumes you're updating a layer that is tracking all - /// damage. - /// - private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, object layerMapKey, FixedPoint2 threshold) - { - if (_overlay && _damageOverlayGroups != null) - { - if (!damageData.DisabledLayers[layerMapKey]) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet($"{layerMapKey}trackDamage", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}", - threshold); - } - } - else if (!_overlay) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet(layerMapKey, out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}", - threshold); - } - } - - /// - /// Updates a target layer by damage group. - /// - private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, object layerMapKey, string damageGroup, FixedPoint2 threshold) - { - if (_overlay && _damageOverlayGroups != null) - { - if (_damageOverlayGroups.ContainsKey(damageGroup) && !damageData.DisabledLayers[layerMapKey]) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet($"{layerMapKey}{damageGroup}", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}_{damageGroup}", - threshold); - } - } - else if (!_overlay) - { - string layerState = damageData.LayerMapKeyStates[layerMapKey]; - spriteComponent.LayerMapTryGet(layerMapKey, out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"{layerState}_{damageGroup}", - threshold); - } - } - - /// - /// Updates an overlay that is tracking all damage. - /// - private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold) - { - spriteComponent.LayerMapTryGet($"DamageOverlay", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"DamageOverlay", - threshold); - } - - /// - /// Updates an overlay based on damage group. - /// - private void UpdateOverlay(SpriteComponent spriteComponent, string damageGroup, FixedPoint2 threshold) - { - if (_damageOverlayGroups != null) - { - if (_damageOverlayGroups.ContainsKey(damageGroup)) - { - spriteComponent.LayerMapTryGet($"DamageOverlay{damageGroup}", out int spriteLayer); - - UpdateDamageLayerState(spriteComponent, - spriteLayer, - $"DamageOverlay_{damageGroup}", - threshold); - } - } - } - - /// - /// Updates a layer on the sprite by what - /// prefix it has (calculated by whatever - /// function calls it), and what threshold - /// was passed into it. - /// - private void UpdateDamageLayerState(SpriteComponent spriteComponent, int spriteLayer, string statePrefix, FixedPoint2 threshold) - { - if (threshold == 0) - { - spriteComponent.LayerSetVisible(spriteLayer, false); - } - else - { - if (!spriteComponent[spriteLayer].Visible) - { - spriteComponent.LayerSetVisible(spriteLayer, true); - } - spriteComponent.LayerSetState(spriteLayer, $"{statePrefix}_{threshold}"); - } - } - } -} diff --git a/Content.Client/Damage/DamageVisualizerComponent.cs b/Content.Client/Damage/DamageVisualizerComponent.cs deleted file mode 100644 index 984cf0e2aa..0000000000 --- a/Content.Client/Damage/DamageVisualizerComponent.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Content.Shared.FixedPoint; -using Robust.Shared.GameObjects; - -namespace Content.Client.Damage -{ - // Stores all the data for a DamageVisualizer. - // - // Storing it inside of the AppearanceComponent's data - // dictionary was too messy, but at least we can - // store it in the entity itself as a separate, - // dynamically added component. - [RegisterComponent] - public sealed class DamageVisualizerDataComponent : Component - { - public List TargetLayerMapKeys = new(); - public bool Disabled = false; - public bool Valid = true; - public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero; - public Dictionary DisabledLayers = new(); - public Dictionary LayerMapKeyStates = new(); - public Dictionary LastThresholdPerGroup = new(); - public string TopMostLayerKey = default!; - } -} diff --git a/Content.Client/Damage/DamageVisualsComponent.cs b/Content.Client/Damage/DamageVisualsComponent.cs new file mode 100644 index 0000000000..1ccf7a5c11 --- /dev/null +++ b/Content.Client/Damage/DamageVisualsComponent.cs @@ -0,0 +1,161 @@ +using Content.Shared.FixedPoint; + +namespace Content.Client.Damage; + +[RegisterComponent] +public sealed class DamageVisualsComponent : Component +{ + /// + /// Damage thresholds between damage state changes. + /// + /// If there are any negative thresholds, or there is + /// less than one threshold, the visualizer is marked + /// as invalid. + /// + /// + /// A 'zeroth' threshold is automatically added, + /// and this list is automatically sorted for + /// efficiency beforehand. As such, the zeroth + /// threshold is not required - and negative + /// thresholds are automatically caught as + /// invalid. The zeroth threshold automatically + /// sets all layers to invisible, so a sprite + /// isn't required for it. + /// + [DataField("thresholds", required: true)] + public List Thresholds = new(); + + /// + /// Layers to target, by layerMapKey. + /// If a target layer map key is invalid + /// (in essence, undefined), then the target + /// layer is removed from the list for efficiency. + /// + /// If no layers are valid, then the visualizer + /// is marked as invalid. + /// + /// If this is not defined, however, the visualizer + /// instead adds an overlay to the sprite. + /// + /// + /// Layers can be disabled here by passing + /// the layer's name as a key to SetData, + /// and passing in a bool set to either 'false' + /// to disable it, or 'true' to enable it. + /// Setting the layer as disabled will make it + /// completely invisible. + /// + [DataField("targetLayers")] public List? TargetLayers; + + /// + /// The actual sprites for every damage group + /// that the entity should display visually. + /// + /// This is keyed by a damage group identifier + /// (for example, Brute), and has a value + /// of a DamageVisualizerSprite (see below) + /// + [DataField("damageOverlayGroups")] public readonly Dictionary? DamageOverlayGroups; + + /// + /// Sets if you want sprites to overlay the + /// entity when damaged, or if you would + /// rather have each target layer's state + /// replaced by a different state + /// within its RSI. + /// + /// This cannot be set to false if: + /// - There are no target layers + /// - There is no damage group + /// + [DataField("overlay")] public readonly bool Overlay = true; + + /// + /// A single damage group to target. + /// This should only be defined if + /// overlay is set to false. + /// If this is defined with damageSprites, + /// this will be ignored. + /// + /// + /// This is here because otherwise, + /// you would need several permutations + /// of group sprites depending on + /// what kind of damage combination + /// you would want, on which threshold. + /// + [DataField("damageGroup")] public readonly string? DamageGroup; + + /// + /// Set this if you want incoming damage to be + /// divided. + /// + /// + /// This is more useful if you have similar + /// damage sprites in between entities, + /// but with different damage thresholds + /// and you want to avoid duplicating + /// these sprites. + /// + [DataField("damageDivisor")] public float Divisor = 1; + + /// + /// Set this to track all damage, instead of specific groups. + /// + /// + /// This will only work if you have damageOverlay + /// defined - otherwise, it will not work. + /// + [DataField("trackAllDamage")] public readonly bool TrackAllDamage; + /// + /// This is the overlay sprite used, if _trackAllDamage is + /// enabled. Supports no complex per-group layering, + /// just an actually simple damage overlay. See + /// DamageVisualizerSprite for more information. + /// + [DataField("damageOverlay")] public readonly DamageVisualizerSprite? DamageOverlay; + + public readonly List TargetLayerMapKeys = new(); + public bool Disabled = false; + public bool Valid = true; + public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero; + public readonly Dictionary DisabledLayers = new(); + public readonly Dictionary LayerMapKeyStates = new(); + public readonly Dictionary LastThresholdPerGroup = new(); + public string TopMostLayerKey = default!; +} + +// deals with the edge case of human damage visuals not +// being in color without making a Dict + /// The RSI path for the damage visualizer + /// group overlay. + /// + /// + /// States in here will require one of four + /// forms: + /// + /// If tracking damage groups: + /// - {base_state}_{group}_{threshold} if targeting + /// a static layer on a sprite (either as an + /// overlay or as a state change) + /// - DamageOverlay_{group}_{threshold} if not + /// targeting a layer on a sprite. + /// + /// If not tracking damage groups: + /// - {base_state}_{threshold} if it is targeting + /// a layer + /// - DamageOverlay_{threshold} if not targeting + /// a layer. + /// + [DataField("sprite", required: true)] public readonly string Sprite = default!; + + /// + /// The color of this sprite overlay. + /// Supports only hexadecimal format. + /// + [DataField("color")] public readonly string? Color; +} diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs new file mode 100644 index 0000000000..adf245e4a5 --- /dev/null +++ b/Content.Client/Damage/DamageVisualsSystem.cs @@ -0,0 +1,698 @@ +using System.Linq; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Damage; + +/// +/// A simple visualizer for any entity with a DamageableComponent +/// to display the status of how damaged it is. +/// +/// Can either be an overlay for an entity, or target multiple +/// layers on the same entity. +/// +/// This can be disabled dynamically by passing into SetData, +/// key DamageVisualizerKeys.Disabled, value bool +/// (DamageVisualizerKeys lives in Content.Shared.Damage) +/// +/// Damage layers, if targeting layers, can also be dynamically +/// disabled if needed by passing into SetData, the name/enum +/// of the sprite layer, and then passing in a bool value +/// (true to enable, false to disable). +/// +public sealed class DamageVisualsSystem : VisualizerSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + private const string SawmillName = "DamageVisuals"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(InitializeEntity); + } + + private void InitializeEntity(EntityUid entity, DamageVisualsComponent comp, ComponentInit args) + { + VerifyVisualizerSetup(entity, comp); + + if (!comp.Valid) + { + RemCompDeferred(entity); + return; + } + + InitializeVisualizer(entity, comp); + } + + private void VerifyVisualizerSetup(EntityUid entity, DamageVisualsComponent damageVisComp) + { + if (damageVisComp.Thresholds.Count < 1) + { + Logger.ErrorS(SawmillName, $"Thresholds were invalid for entity {entity}. Thresholds: {damageVisComp.Thresholds}"); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.Divisor == 0) + { + Logger.ErrorS(SawmillName, $"Divisor for {entity} is set to zero."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.Overlay) + { + if (damageVisComp.DamageOverlayGroups == null && damageVisComp.DamageOverlay == null) + { + Logger.ErrorS(SawmillName, $"Enabled overlay without defined damage overlay sprites on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay == null) + { + Logger.ErrorS(SawmillName, $"Enabled all damage tracking without a damage overlay sprite on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay != null) + { + Logger.WarningS(SawmillName, $"Disabled all damage tracking with a damage overlay sprite on {entity}."); + damageVisComp.Valid = false; + return; + } + + + if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null) + { + Logger.WarningS(SawmillName, $"Enabled all damage tracking with damage overlay groups on {entity}."); + damageVisComp.Valid = false; + return; + } + } + else if (!damageVisComp.Overlay) + { + if (damageVisComp.TargetLayers == null) + { + Logger.ErrorS(SawmillName, $"Disabled overlay without target layers on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.DamageOverlayGroups != null || damageVisComp.DamageOverlay != null) + { + Logger.ErrorS(SawmillName, $"Disabled overlay with defined damage overlay sprites on {entity}."); + damageVisComp.Valid = false; + return; + } + + if (damageVisComp.DamageGroup == null) + { + Logger.ErrorS(SawmillName, $"Disabled overlay without defined damage group on {entity}."); + damageVisComp.Valid = false; + return; + } + } + + if (damageVisComp.DamageOverlayGroups != null && damageVisComp.DamageGroup != null) + { + Logger.WarningS(SawmillName, $"Damage overlay sprites and damage group are both defined on {entity}."); + } + + if (damageVisComp.DamageOverlay != null && damageVisComp.DamageGroup != null) + { + Logger.WarningS(SawmillName, $"Damage overlay sprites and damage group are both defined on {entity}."); + } + } + + private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damageVisComp) + { + if (!TryComp(entity, out SpriteComponent? spriteComponent) + || !TryComp(entity, out var damageComponent) + || !HasComp(entity)) + return; + + damageVisComp.Thresholds.Add(FixedPoint2.Zero); + damageVisComp.Thresholds.Sort(); + + if (damageVisComp.Thresholds[0] != 0) + { + Logger.ErrorS(SawmillName, $"Thresholds were invalid for entity {entity}. Thresholds: {damageVisComp.Thresholds}"); + damageVisComp.Valid = false; + return; + } + + // If the damage container on our entity's DamageableComponent + // is not null, we can try to check through its groups. + if (damageComponent.DamageContainerID != null + && _prototypeManager.TryIndex(damageComponent.DamageContainerID, out var damageContainer)) + { + // Are we using damage overlay sprites by group? + // Check if the container matches the supported groups, + // and start caching the last threshold. + if (damageVisComp.DamageOverlayGroups != null) + { + foreach (var damageType in damageVisComp.DamageOverlayGroups.Keys) + { + if (!damageContainer.SupportedGroups.Contains(damageType)) + { + Logger.ErrorS(SawmillName, $"Damage key {damageType} was invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + damageVisComp.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); + } + } + // Are we tracking a single damage group without overlay instead? + // See if that group is in our entity's damage container. + else if (!damageVisComp.Overlay && damageVisComp.DamageGroup != null) + { + if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup)) + { + Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + damageVisComp.LastThresholdPerGroup.Add(damageVisComp.DamageGroup, FixedPoint2.Zero); + } + } + // Ditto above, but instead we go through every group. + else // oh boy! time to enumerate through every single group! + { + var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes() + .Select((p, _) => p.ID) + .ToList(); + if (damageVisComp.DamageOverlayGroups != null) + { + foreach (var damageType in damageVisComp.DamageOverlayGroups.Keys) + { + if (!damagePrototypeIdList.Contains(damageType)) + { + Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + damageVisComp.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero); + } + } + else if (damageVisComp.DamageGroup != null) + { + if (!damagePrototypeIdList.Contains(damageVisComp.DamageGroup)) + { + Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + damageVisComp.LastThresholdPerGroup.Add(damageVisComp.DamageGroup, FixedPoint2.Zero); + } + } + + // If we're targeting any layers, and the amount of + // layers is greater than zero, we start reserving + // all the layers needed to track damage groups + // on the entity. + if (damageVisComp.TargetLayers is { Count: > 0 }) + { + // This should ensure that the layers we're targeting + // are valid for the visualizer's use. + // + // If the layer doesn't have a base state, or + // the layer key just doesn't exist, we skip it. + foreach (var key in damageVisComp.TargetLayers) + { + if (!spriteComponent.LayerMapTryGet(key, out var index) + || spriteComponent.LayerGetState(index).ToString() == null) + { + Logger.WarningS(SawmillName, $"Layer at key {key} was invalid for entity {entity}."); + continue; + } + + damageVisComp.TargetLayerMapKeys.Add(key); + } + + // Similar to damage overlay groups, if none of the targeted + // sprite layers could be used, we display an error and + // invalidate the visualizer without crashing. + if (damageVisComp.TargetLayerMapKeys.Count == 0) + { + Logger.ErrorS(SawmillName, $"Target layers were invalid for entity {entity}."); + damageVisComp.Valid = false; + return; + } + + // Otherwise, we start reserving layers. Since the filtering + // loop above ensures that all of these layers are not null, + // and have valid state IDs, there should be no issues. + foreach (object layer in damageVisComp.TargetLayerMapKeys) + { + var layerCount = spriteComponent.AllLayers.Count(); + var index = spriteComponent.LayerMapGet(layer); + var layerState = spriteComponent.LayerGetState(index).ToString()!; + + if (index + 1 != layerCount) + { + index += 1; + } + + damageVisComp.LayerMapKeyStates.Add(layer, layerState); + + // If we're an overlay, and we're targeting groups, + // we reserve layers per damage group. + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null) + { + foreach (var (group, sprite) in damageVisComp.DamageOverlayGroups) + { + AddDamageLayerToSprite(spriteComponent, + sprite, + $"{layerState}_{group}_{damageVisComp.Thresholds[1]}", + $"{layer}{group}", + index); + } + damageVisComp.DisabledLayers.Add(layer, false); + } + // If we're not targeting groups, and we're still + // using an overlay, we instead just add a general + // overlay that reflects on how much damage + // was taken. + else if (damageVisComp.DamageOverlay != null) + { + AddDamageLayerToSprite(spriteComponent, + damageVisComp.DamageOverlay, + $"{layerState}_{damageVisComp.Thresholds[1]}", + $"{layer}trackDamage", + index); + damageVisComp.DisabledLayers.Add(layer, false); + } + } + } + // If we're not targeting layers, however, + // we should ensure that we instead + // reserve it as an overlay. + else + { + if (damageVisComp.DamageOverlayGroups != null) + { + foreach (var (group, sprite) in damageVisComp.DamageOverlayGroups) + { + AddDamageLayerToSprite(spriteComponent, + sprite, + $"DamageOverlay_{group}_{damageVisComp.Thresholds[1]}", + $"DamageOverlay{group}"); + damageVisComp.TopMostLayerKey = $"DamageOverlay{group}"; + } + } + else if (damageVisComp.DamageOverlay != null) + { + AddDamageLayerToSprite(spriteComponent, + damageVisComp.DamageOverlay, + $"DamageOverlay_{damageVisComp.Thresholds[1]}", + "DamageOverlay"); + damageVisComp.TopMostLayerKey = $"DamageOverlay"; + } + } + } + + /// + /// Adds a damage tracking layer to a given sprite component. + /// + private void AddDamageLayerToSprite(SpriteComponent spriteComponent, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null) + { + var newLayer = spriteComponent.AddLayer( + new SpriteSpecifier.Rsi( + new ResourcePath(sprite.Sprite), state + ), index); + spriteComponent.LayerMapSet(mapKey, newLayer); + if (sprite.Color != null) + spriteComponent.LayerSetColor(newLayer, Color.FromHex(sprite.Color)); + spriteComponent.LayerSetVisible(newLayer, false); + } + + protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent damageVisComp, ref AppearanceChangeEvent args) + { + // how is this still here? + if (!damageVisComp.Valid) + return; + + // If this was passed into the component, we update + // the data to ensure that the current disabled + // bool matches. + if (args.Component.TryGetData(DamageVisualizerKeys.Disabled, out var disabledStatus)) + damageVisComp.Disabled = disabledStatus; + + if (damageVisComp.Disabled) + return; + + HandleDamage(args.Component, damageVisComp); + } + + private void HandleDamage(AppearanceComponent component, DamageVisualsComponent damageVisComp) + { + if (!TryComp(component.Owner, out SpriteComponent? spriteComponent) + || !TryComp(component.Owner, out DamageableComponent? damageComponent)) + return; + + if (damageVisComp.TargetLayers != null && damageVisComp.DamageOverlayGroups != null) + UpdateDisabledLayers(spriteComponent, component, damageVisComp); + + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null && damageVisComp.TargetLayers == null) + CheckOverlayOrdering(spriteComponent, damageVisComp); + + if (component.TryGetData(DamageVisualizerKeys.ForceUpdate, out var update) + && update) + { + ForceUpdateLayers(damageComponent, spriteComponent, damageVisComp); + return; + } + + if (damageVisComp.TrackAllDamage) + { + UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp); + } + else if (component.TryGetData(DamageVisualizerKeys.DamageUpdateGroups, out DamageVisualizerGroupData data)) + { + UpdateDamageVisuals(data.GroupList, damageComponent, spriteComponent, damageVisComp); + } + } + + /// + /// Checks if any layers were disabled in the last + /// data update. Disabled layers mean that the + /// layer will no longer be visible, or obtain + /// any damage updates. + /// + private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualsComponent damageVisComp) + { + foreach (var layer in damageVisComp.TargetLayerMapKeys) + { + bool? layerStatus = null; + if (component.TryGetData(layer, out var layerStateEnum)) + layerStatus = layerStateEnum; + + if (layerStatus == null) + continue; + + if (damageVisComp.DisabledLayers[layer] != (bool) layerStatus) + { + damageVisComp.DisabledLayers[layer] = (bool) layerStatus; + if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null) + { + foreach (var damageGroup in damageVisComp.DamageOverlayGroups!.Keys) + { + spriteComponent.LayerSetVisible($"{layer}{damageGroup}", damageVisComp.DisabledLayers[layer]); + } + } + else if (damageVisComp.TrackAllDamage) + spriteComponent.LayerSetVisible($"{layer}trackDamage", damageVisComp.DisabledLayers[layer]); + } + } + } + + /// + /// Checks the overlay ordering on the current + /// sprite component, compared to the + /// data for the visualizer. If the top + /// most layer doesn't match, the sprite + /// layers are recreated and placed on top. + /// + private void CheckOverlayOrdering(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + if (spriteComponent[damageVisComp.TopMostLayerKey] != spriteComponent[spriteComponent.AllLayers.Count() - 1]) + { + if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null) + { + foreach (var (damageGroup, sprite) in damageVisComp.DamageOverlayGroups) + { + var threshold = damageVisComp.LastThresholdPerGroup[damageGroup]; + ReorderOverlaySprite(spriteComponent, + damageVisComp, + sprite, + $"DamageOverlay{damageGroup}", + $"DamageOverlay_{damageGroup}", + threshold); + } + } + else if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay != null) + { + ReorderOverlaySprite(spriteComponent, + damageVisComp, + damageVisComp.DamageOverlay, + $"DamageOverlay", + $"DamageOverlay", + damageVisComp.LastDamageThreshold); + } + } + } + + private void ReorderOverlaySprite(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold) + { + spriteComponent.LayerMapTryGet(key, out var spriteLayer); + var visibility = spriteComponent[spriteLayer].Visible; + spriteComponent.RemoveLayer(spriteLayer); + if (threshold == FixedPoint2.Zero) // these should automatically be invisible + threshold = damageVisComp.Thresholds[1]; + spriteLayer = spriteComponent.AddLayer( + new SpriteSpecifier.Rsi( + new ResourcePath(sprite.Sprite), + $"{statePrefix}_{threshold}" + ), + spriteLayer); + spriteComponent.LayerMapSet(key, spriteLayer); + spriteComponent.LayerSetVisible(spriteLayer, visibility); + // this is somewhat iffy since it constantly reallocates + damageVisComp.TopMostLayerKey = key; + } + + /// + /// Updates damage visuals without tracking + /// any damage groups. + /// + private void UpdateDamageVisuals(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageVisComp.LastDamageThreshold, damageVisComp, out var threshold)) + return; + + damageVisComp.LastDamageThreshold = threshold; + + if (damageVisComp.TargetLayers != null) + { + foreach (var layerMapKey in damageVisComp.TargetLayerMapKeys) + { + UpdateTargetLayer(spriteComponent, damageVisComp, layerMapKey, threshold); + } + } + else + { + UpdateOverlay(spriteComponent, threshold); + } + } + + /// + /// Updates damage visuals by damage group, + /// according to the list of damage groups + /// passed into it. + /// + private void UpdateDamageVisuals(List delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + foreach (var damageGroup in delta) + { + if (!damageVisComp.Overlay && damageGroup != damageVisComp.DamageGroup) + continue; + + if (!_prototypeManager.TryIndex(damageGroup, out var damageGroupPrototype) + || !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal)) + continue; + + if (!damageVisComp.LastThresholdPerGroup.TryGetValue(damageGroup, out var lastThreshold) + || !CheckThresholdBoundary(damageTotal, lastThreshold, damageVisComp, out var threshold)) + continue; + + damageVisComp.LastThresholdPerGroup[damageGroup] = threshold; + + if (damageVisComp.TargetLayers != null) + { + foreach (var layerMapKey in damageVisComp.TargetLayerMapKeys) + { + UpdateTargetLayer(spriteComponent, damageVisComp, layerMapKey, damageGroup, threshold); + } + } + else + { + UpdateOverlay(spriteComponent, damageVisComp, damageGroup, threshold); + } + } + + } + + /// + /// Checks if a threshold boundary was passed. + /// + private bool CheckThresholdBoundary(FixedPoint2 damageTotal, FixedPoint2 lastThreshold, DamageVisualsComponent damageVisComp, out FixedPoint2 threshold) + { + threshold = FixedPoint2.Zero; + damageTotal = damageTotal / damageVisComp.Divisor; + var thresholdIndex = damageVisComp.Thresholds.BinarySearch(damageTotal); + + if (thresholdIndex < 0) + { + thresholdIndex = ~thresholdIndex; + threshold = damageVisComp.Thresholds[thresholdIndex - 1]; + } + else + { + threshold = damageVisComp.Thresholds[thresholdIndex]; + } + + if (threshold == lastThreshold) + return false; + + return true; + } + + /// + /// This is the entry point for + /// forcing an update on all damage layers. + /// Does different things depending on + /// the configuration of the visualizer. + /// + private void ForceUpdateLayers(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp) + { + if (damageVisComp.DamageOverlayGroups != null) + { + UpdateDamageVisuals(damageVisComp.DamageOverlayGroups.Keys.ToList(), damageComponent, spriteComponent, damageVisComp); + } + else if (damageVisComp.DamageGroup != null) + { + UpdateDamageVisuals(new List(){ damageVisComp.DamageGroup }, damageComponent, spriteComponent, damageVisComp); + } + else if (damageVisComp.DamageOverlay != null) + { + UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp); + } + } + + /// + /// Updates a target layer. Without a damage group passed in, + /// it assumes you're updating a layer that is tracking all + /// damage. + /// + private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, object layerMapKey, FixedPoint2 threshold) + { + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null) + { + if (!damageVisComp.DisabledLayers[layerMapKey]) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet($"{layerMapKey}trackDamage", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}", + threshold); + } + } + else if (!damageVisComp.Overlay) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet(layerMapKey, out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}", + threshold); + } + } + + /// + /// Updates a target layer by damage group. + /// + private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, object layerMapKey, string damageGroup, FixedPoint2 threshold) + { + if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null) + { + if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup) && !damageVisComp.DisabledLayers[layerMapKey]) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet($"{layerMapKey}{damageGroup}", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}_{damageGroup}", + threshold); + } + } + else if (!damageVisComp.Overlay) + { + var layerState = damageVisComp.LayerMapKeyStates[layerMapKey]; + spriteComponent.LayerMapTryGet(layerMapKey, out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"{layerState}_{damageGroup}", + threshold); + } + } + + /// + /// Updates an overlay that is tracking all damage. + /// + private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold) + { + spriteComponent.LayerMapTryGet($"DamageOverlay", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"DamageOverlay", + threshold); + } + + /// + /// Updates an overlay based on damage group. + /// + private void UpdateOverlay(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, string damageGroup, FixedPoint2 threshold) + { + if (damageVisComp.DamageOverlayGroups != null) + { + if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup)) + { + spriteComponent.LayerMapTryGet($"DamageOverlay{damageGroup}", out var spriteLayer); + + UpdateDamageLayerState(spriteComponent, + spriteLayer, + $"DamageOverlay_{damageGroup}", + threshold); + } + } + } + + /// + /// Updates a layer on the sprite by what + /// prefix it has (calculated by whatever + /// function calls it), and what threshold + /// was passed into it. + /// + private void UpdateDamageLayerState(SpriteComponent spriteComponent, int spriteLayer, string statePrefix, FixedPoint2 threshold) + { + if (threshold == 0) + { + spriteComponent.LayerSetVisible(spriteLayer, false); + } + else + { + if (!spriteComponent[spriteLayer].Visible) + { + spriteComponent.LayerSetVisible(spriteLayer, true); + } + spriteComponent.LayerSetState(spriteLayer, $"{statePrefix}_{threshold}"); + } + } +} diff --git a/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs b/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs index 207d332793..f883fe42d1 100644 --- a/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs +++ b/Content.Client/Disposal/UI/DisposalRouterBoundUserInterface.cs @@ -13,7 +13,7 @@ namespace Content.Client.Disposal.UI { private DisposalRouterWindow? _window; - public DisposalRouterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public DisposalRouterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs b/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs index 09d42372d9..6c2da1e4ff 100644 --- a/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs +++ b/Content.Client/Disposal/UI/DisposalTaggerBoundUserInterface.cs @@ -13,7 +13,7 @@ namespace Content.Client.Disposal.UI { private DisposalTaggerWindow? _window; - public DisposalTaggerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public DisposalTaggerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs b/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs index d9a31c534f..eaa2b9912e 100644 --- a/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs +++ b/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs @@ -20,7 +20,7 @@ namespace Content.Client.Disposal.UI public MailingUnitWindow? MailingUnitWindow; public DisposalUnitWindow? DisposalUnitWindow; - public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { } diff --git a/Content.Client/Doors/AirlockVisualizer.cs b/Content.Client/Doors/AirlockVisualizer.cs index 6769568f71..e2ff79362a 100644 --- a/Content.Client/Doors/AirlockVisualizer.cs +++ b/Content.Client/Doors/AirlockVisualizer.cs @@ -85,7 +85,7 @@ namespace Content.Client.Doors { var flickMaintenancePanel = new AnimationTrackSpriteFlick(); CloseAnimation.AnimationTracks.Add(flickMaintenancePanel); - flickMaintenancePanel.LayerKey = WiresVisualizer.WiresVisualLayers.MaintenancePanel; + flickMaintenancePanel.LayerKey = WiresVisualLayers.MaintenancePanel; flickMaintenancePanel.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("panel_closing", 0f)); } } @@ -109,7 +109,7 @@ namespace Content.Client.Doors { var flickMaintenancePanel = new AnimationTrackSpriteFlick(); OpenAnimation.AnimationTracks.Add(flickMaintenancePanel); - flickMaintenancePanel.LayerKey = WiresVisualizer.WiresVisualLayers.MaintenancePanel; + flickMaintenancePanel.LayerKey = WiresVisualLayers.MaintenancePanel; flickMaintenancePanel.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("panel_opening", 0f)); } } diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs index 398d01e578..d14a973304 100644 --- a/Content.Client/Doors/DoorSystem.cs +++ b/Content.Client/Doors/DoorSystem.cs @@ -27,9 +27,9 @@ public sealed class DoorSystem : SharedDoorSystem } // TODO AUDIO PREDICT see comments in server-side PlaySound() - protected override void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted) + protected override void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted) { if (GameTiming.InPrediction && GameTiming.IsFirstTimePredicted) - SoundSystem.Play(sound, Filter.Local(), uid, audioParams); + Audio.Play(soundSpecifier, Filter.Local(), uid, audioParams); } } diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 17f2f66bb5..78ba052e49 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -1,11 +1,9 @@ -using System; using Content.Client.Administration.Managers; using Content.Client.Changelog; using Content.Client.CharacterInterface; using Content.Client.Chat.Managers; using Content.Client.EscapeMenu; using Content.Client.Eui; -using Content.Client.Eye.Blinding; using Content.Client.Flash; using Content.Client.GhostKick; using Content.Client.HUD; @@ -14,40 +12,33 @@ using Content.Client.Input; using Content.Client.IoC; using Content.Client.Launcher; using Content.Client.MainMenu; -using Content.Client.MobState.Overlays; -using Content.Client.Parallax; using Content.Client.Parallax.Managers; using Content.Client.Players.PlayTimeTracking; using Content.Client.Preferences; using Content.Client.Radiation; -using Content.Client.Sandbox; using Content.Client.Screenshot; using Content.Client.Singularity; using Content.Client.Stylesheets; using Content.Client.Viewport; using Content.Client.Voting; -using Content.Shared.Actions; using Content.Shared.Administration; -using Content.Shared.Alert; using Content.Shared.AME; -using Content.Shared.Cargo.Components; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Dispenser; using Content.Shared.Gravity; using Content.Shared.Lathe; using Content.Shared.Markers; -using Content.Shared.Research.Components; -using Content.Shared.VendingMachines; -using Content.Shared.Wires; using Robust.Client; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.State; using Robust.Client.UserInterface; +#if FULL_RELEASE +using Robust.Shared; +using Robust.Shared.Configuration; +#endif using Robust.Shared.ContentPack; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -130,6 +121,11 @@ namespace Content.Client.Entry IoCManager.InjectDependencies(this); +#if FULL_RELEASE + // if FULL_RELEASE, because otherwise this breaks some integration tests. + IoCManager.Resolve().OverrideDefault(CVars.NetBufferSize, 2); +#endif + _escapeMenuOwner.Initialize(); _baseClient.PlayerJoinedServer += (_, _) => diff --git a/Content.Client/EscapeMenu/UI/Tabs/NetworkTab.xaml b/Content.Client/EscapeMenu/UI/Tabs/NetworkTab.xaml index 7d2741ab14..1b530fba8e 100644 --- a/Content.Client/EscapeMenu/UI/Tabs/NetworkTab.xaml +++ b/Content.Client/EscapeMenu/UI/Tabs/NetworkTab.xaml @@ -8,7 +8,7 @@