diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs index 223bf7876a..359c8957f9 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -91,7 +91,7 @@ public sealed class ClientAlertsSystem : AlertsSystem ClearAlerts?.Invoke(this, EventArgs.Empty); } - public void AlertClicked(AlertType alertType) + public void AlertClicked(ProtoId alertType) { RaiseNetworkEvent(new ClickAlertEvent(alertType)); } diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs index 78185ce6b0..86cf0a9eb8 100644 --- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -1,4 +1,5 @@ using Content.Client.Atmos.Overlays; +using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Atmos.EntitySystems; using JetBrains.Annotations; @@ -36,28 +37,38 @@ namespace Content.Client.Atmos.EntitySystems private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args) { - if (args.Current is not GasTileOverlayState state) - return; + Dictionary modifiedChunks; - // is this a delta or full state? - if (!state.FullState) + switch (args.Current) { - foreach (var index in comp.Chunks.Keys) + // is this a delta or full state? + case GasTileOverlayDeltaState delta: { - if (!state.AllChunks!.Contains(index)) - comp.Chunks.Remove(index); + modifiedChunks = delta.ModifiedChunks; + foreach (var index in comp.Chunks.Keys) + { + if (!delta.AllChunks.Contains(index)) + comp.Chunks.Remove(index); + } + + break; } - } - else - { - foreach (var index in comp.Chunks.Keys) + case GasTileOverlayState state: { - if (!state.Chunks.ContainsKey(index)) - comp.Chunks.Remove(index); + modifiedChunks = state.Chunks; + foreach (var index in comp.Chunks.Keys) + { + if (!state.Chunks.ContainsKey(index)) + comp.Chunks.Remove(index); + } + + break; } + default: + return; } - foreach (var (index, data) in state.Chunks) + foreach (var (index, data) in modifiedChunks) { comp.Chunks[index] = data; } diff --git a/Content.Client/Decals/DecalSystem.cs b/Content.Client/Decals/DecalSystem.cs index 901ab270fb..41e5f39c28 100644 --- a/Content.Client/Decals/DecalSystem.cs +++ b/Content.Client/Decals/DecalSystem.cs @@ -56,34 +56,43 @@ namespace Content.Client.Decals private void OnHandleState(EntityUid gridUid, DecalGridComponent gridComp, ref ComponentHandleState args) { - if (args.Current is not DecalGridState state) - return; - // is this a delta or full state? _removedChunks.Clear(); + Dictionary modifiedChunks; - if (!state.FullState) + switch (args.Current) { - foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + case DecalGridDeltaState delta: { - if (!state.AllChunks!.Contains(key)) - _removedChunks.Add(key); + modifiedChunks = delta.ModifiedChunks; + foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + { + if (!delta.AllChunks.Contains(key)) + _removedChunks.Add(key); + } + + break; } - } - else - { - foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + case DecalGridState state: { - if (!state.Chunks.ContainsKey(key)) - _removedChunks.Add(key); + modifiedChunks = state.Chunks; + foreach (var key in gridComp.ChunkCollection.ChunkCollection.Keys) + { + if (!state.Chunks.ContainsKey(key)) + _removedChunks.Add(key); + } + + break; } + default: + return; } if (_removedChunks.Count > 0) RemoveChunks(gridUid, gridComp, _removedChunks); - if (state.Chunks.Count > 0) - UpdateChunks(gridUid, gridComp, state.Chunks); + if (modifiedChunks.Count > 0) + UpdateChunks(gridUid, gridComp, modifiedChunks); } private void OnChunkUpdate(DecalChunkUpdateEvent ev) diff --git a/Content.Client/Doors/FirelockSystem.cs b/Content.Client/Doors/FirelockSystem.cs index cfd84a4713..f64b4c8e52 100644 --- a/Content.Client/Doors/FirelockSystem.cs +++ b/Content.Client/Doors/FirelockSystem.cs @@ -1,9 +1,10 @@ using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; using Robust.Client.GameObjects; namespace Content.Client.Doors; -public sealed class FirelockSystem : EntitySystem +public sealed class FirelockSystem : SharedFirelockSystem { [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 45db4efa53..125ec62e49 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -239,8 +239,10 @@ namespace Content.Client.Examine if (knowTarget) { - var itemName = FormattedMessage.RemoveMarkup(Identity.Name(target, EntityManager, player)); - var labelMessage = FormattedMessage.FromMarkup($"[bold]{itemName}[/bold]"); + // TODO: FormattedMessage.RemoveMarkupPermissive + // var itemName = FormattedMessage.RemoveMarkupPermissive(Identity.Name(target, EntityManager, player)); + var itemName = FormattedMessage.FromMarkupPermissive(Identity.Name(target, EntityManager, player)).ToString(); + var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]"); var label = new RichTextLabel(); label.SetMessage(labelMessage); hBox.AddChild(label); diff --git a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs index 2a846ff708..0f5729f55b 100644 --- a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs +++ b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs @@ -37,14 +37,8 @@ namespace Content.Client.Instruments.UI protected override void ReceiveMessage(BoundUserInterfaceMessage message) { - switch (message) - { - case InstrumentBandResponseBuiMessage bandRx: - _bandMenu?.Populate(bandRx.Nearby, EntMan); - break; - default: - break; - } + if (message is InstrumentBandResponseBuiMessage bandRx) + _bandMenu?.Populate(bandRx.Nearby, EntMan); } protected override void Open() diff --git a/Content.Client/Interactable/InteractionSystem.cs b/Content.Client/Interactable/InteractionSystem.cs index 0af8830e9a..ff0a607920 100644 --- a/Content.Client/Interactable/InteractionSystem.cs +++ b/Content.Client/Interactable/InteractionSystem.cs @@ -4,24 +4,6 @@ using Robust.Shared.Containers; namespace Content.Client.Interactable { - public sealed class InteractionSystem : SharedInteractionSystem - { - [Dependency] private readonly SharedContainerSystem _container = default!; - - public override bool CanAccessViaStorage(EntityUid user, EntityUid target) - { - if (!EntityManager.EntityExists(target)) - return false; - - if (!_container.TryGetContainingContainer(target, out var container)) - return false; - - if (!HasComp(container.Owner)) - return false; - - // we don't check if the user can access the storage entity itself. This should be handed by the UI system. - // Need to return if UI is open or not - return true; - } - } + // TODO Remove Shared prefix + public sealed class InteractionSystem : SharedInteractionSystem; } diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs index 7f98e3e0c3..5e068f1e9c 100644 --- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs +++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs @@ -76,7 +76,7 @@ public sealed partial class ArticleEditorPanel : Control TextEditPanel.Visible = !_preview; PreviewPanel.Visible = _preview; - PreviewLabel.SetMarkup(Rope.Collapse(ContentField.TextRope)); + PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope)); } private void OnCancel(BaseButton.ButtonEventArgs eventArgs) diff --git a/Content.Client/Message/RichTextLabelExt.cs b/Content.Client/Message/RichTextLabelExt.cs index ab6d17bf44..7ff6390764 100644 --- a/Content.Client/Message/RichTextLabelExt.cs +++ b/Content.Client/Message/RichTextLabelExt.cs @@ -5,9 +5,27 @@ namespace Content.Client.Message; public static class RichTextLabelExt { + + + /// + /// Sets the labels markup. + /// + /// + /// Invalid markup will cause exceptions to be thrown. Don't use this for user input! + /// public static RichTextLabel SetMarkup(this RichTextLabel label, string markup) { label.SetMessage(FormattedMessage.FromMarkup(markup)); return label; } + + /// + /// Sets the labels markup.
+ /// Uses FormatedMessage.FromMarkupPermissive which treats invalid markup as text. + ///
+ public static RichTextLabel SetMarkupPermissive(this RichTextLabel label, string markup) + { + label.SetMessage(FormattedMessage.FromMarkupPermissive(markup)); + return label; + } } diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs index e33bc5d329..9aeb792a42 100644 --- a/Content.Client/Pinpointer/NavMapSystem.cs +++ b/Content.Client/Pinpointer/NavMapSystem.cs @@ -14,27 +14,40 @@ public sealed partial class NavMapSystem : SharedNavMapSystem private void OnHandleState(EntityUid uid, NavMapComponent component, ref ComponentHandleState args) { - if (args.Current is not NavMapComponentState state) - return; + Dictionary modifiedChunks; + Dictionary beacons; - if (!state.FullState) + switch (args.Current) { - foreach (var index in component.Chunks.Keys) + case NavMapDeltaState delta: { - if (!state.AllChunks!.Contains(index)) - component.Chunks.Remove(index); + modifiedChunks = delta.ModifiedChunks; + beacons = delta.Beacons; + foreach (var index in component.Chunks.Keys) + { + if (!delta.AllChunks!.Contains(index)) + component.Chunks.Remove(index); + } + + break; } - } - else - { - foreach (var index in component.Chunks.Keys) + case NavMapState state: { - if (!state.Chunks.ContainsKey(index)) - component.Chunks.Remove(index); + modifiedChunks = state.Chunks; + beacons = state.Beacons; + foreach (var index in component.Chunks.Keys) + { + if (!state.Chunks.ContainsKey(index)) + component.Chunks.Remove(index); + } + + break; } + default: + return; } - foreach (var (origin, chunk) in state.Chunks) + foreach (var (origin, chunk) in modifiedChunks) { var newChunk = new NavMapChunk(origin); Array.Copy(chunk, newChunk.TileData, chunk.Length); @@ -42,7 +55,7 @@ public sealed partial class NavMapSystem : SharedNavMapSystem } component.Beacons.Clear(); - foreach (var (nuid, beacon) in state.Beacons) + foreach (var (nuid, beacon) in beacons) { component.Beacons[nuid] = beacon; } diff --git a/Content.Client/Revenant/RevenantSystem.cs b/Content.Client/Revenant/RevenantSystem.cs index 49d29d8a5f..e050fe35aa 100644 --- a/Content.Client/Revenant/RevenantSystem.cs +++ b/Content.Client/Revenant/RevenantSystem.cs @@ -1,5 +1,4 @@ using Content.Client.Alerts; -using Content.Shared.Alert; using Content.Shared.Revenant; using Content.Shared.Revenant.Components; using Robust.Client.GameObjects; @@ -42,7 +41,7 @@ public sealed class RevenantSystem : EntitySystem private void OnUpdateAlert(Entity ent, ref UpdateAlertSpriteEvent args) { - if (args.Alert.AlertType != AlertType.Essence) + if (args.Alert.ID != ent.Comp.EssenceAlert) return; var sprite = args.SpriteViewEnt.Comp; diff --git a/Content.Client/Tips/TippyUIController.cs b/Content.Client/Tips/TippyUIController.cs index 67c3ee45a7..2cc694d97d 100644 --- a/Content.Client/Tips/TippyUIController.cs +++ b/Content.Client/Tips/TippyUIController.cs @@ -175,7 +175,7 @@ public sealed class TippyUIController : UIController sprite.LayerSetVisible("hiding", false); } sprite.Rotation = 0; - tippy.Label.SetMarkup(_currentMessage.Msg); + tippy.Label.SetMarkupPermissive(_currentMessage.Msg); tippy.Label.Visible = false; tippy.LabelPanel.Visible = false; tippy.Visible = true; diff --git a/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs b/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs index 3b85972a9b..5c19512038 100644 --- a/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs +++ b/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs @@ -7,6 +7,7 @@ using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; +using Robust.Shared.Prototypes; namespace Content.Client.UserInterface.Systems.Alerts; @@ -43,7 +44,7 @@ public sealed class AlertsUIController : UIController, IOnStateEntered e) { _alertsSystem?.AlertClicked(e); } diff --git a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs index a1a494c47b..d6a79a81c4 100644 --- a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs +++ b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs @@ -4,6 +4,7 @@ using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Input; +using Robust.Shared.Prototypes; namespace Content.Client.UserInterface.Systems.Alerts.Widgets; @@ -21,8 +22,10 @@ public sealed partial class AlertsUI : UIWidget RobustXamlLoader.Load(this); } - public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, - IReadOnlyDictionary alertStates) + public void SyncControls(AlertsSystem alertsSystem, + AlertOrderPrototype? alertOrderPrototype, + IReadOnlyDictionary alertStates) { // remove any controls with keys no longer present if (SyncRemoveControls(alertStates)) @@ -46,7 +49,7 @@ public sealed partial class AlertsUI : UIWidget _alertControls.Clear(); } - public event EventHandler? AlertPressed; + public event EventHandler>? AlertPressed; private bool SyncRemoveControls(IReadOnlyDictionary alertStates) { @@ -88,7 +91,7 @@ public sealed partial class AlertsUI : UIWidget } if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) && - existingAlertControl.Alert.AlertType == newAlert.AlertType) + existingAlertControl.Alert.ID == newAlert.ID) { // key is the same, simply update the existing control severity / cooldown existingAlertControl.SetSeverity(alertState.Severity); @@ -155,6 +158,6 @@ public sealed partial class AlertsUI : UIWidget if (args.Event.Function != EngineKeyFunctions.UIClick) return; - AlertPressed?.Invoke(this, control.Alert.AlertType); + AlertPressed?.Invoke(this, control.Alert.ID); } } diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs index d3b1fb4722..54af64122b 100644 --- a/Content.IntegrationTests/Tests/EntityTest.cs +++ b/Content.IntegrationTests/Tests/EntityTest.cs @@ -350,8 +350,12 @@ namespace Content.IntegrationTests.Tests "DebrisFeaturePlacerController", // Above. "LoadedChunk", // Worldgen chunk loading malding. "BiomeSelection", // Whaddya know, requires config. + "ActivatableUI", // Requires enum key }; + // TODO TESTS + // auto ignore any components that have a "required" data field. + await using var pair = await PoolManager.GetServerClient(); var server = pair.Server; var entityManager = server.ResolveDependency(); diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs index 1da77ac558..ef4e6326cd 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs @@ -5,7 +5,6 @@ using Content.Shared.Alert; using Robust.Client.UserInterface; using Robust.Server.Player; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs { @@ -45,8 +44,8 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs Assert.That(alerts, Is.Not.Null); var alertCount = alerts.Count; - alertsSystem.ShowAlert(playerUid, AlertType.Debug1); - alertsSystem.ShowAlert(playerUid, AlertType.Debug2); + alertsSystem.ShowAlert(playerUid, "Debug1"); + alertsSystem.ShowAlert(playerUid, "Debug2"); Assert.That(alerts, Has.Count.EqualTo(alertCount + 2)); }); @@ -87,14 +86,14 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs // we should be seeing 3 alerts - our health, and the 2 debug alerts, in a specific order. Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(3)); var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c); - var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray(); - var expectedIDs = new[] { AlertType.HumanHealth, AlertType.Debug1, AlertType.Debug2 }; + var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray(); + var expectedIDs = new[] { "HumanHealth", "Debug1", "Debug2" }; Assert.That(alertIDs, Is.SupersetOf(expectedIDs)); }); await server.WaitAssertion(() => { - alertsSystem.ClearAlert(playerUid, AlertType.Debug1); + alertsSystem.ClearAlert(playerUid, "Debug1"); }); await pair.RunTicksSync(5); @@ -104,8 +103,8 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs // we should be seeing 2 alerts now because one was cleared Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(2)); var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c); - var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray(); - var expectedIDs = new[] { AlertType.HumanHealth, AlertType.Debug2 }; + var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray(); + var expectedIDs = new[] { "HumanHealth", "Debug2" }; Assert.That(alertIDs, Is.SupersetOf(expectedIDs)); }); diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs index 0ad198d6ef..74641126ae 100644 --- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs +++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs @@ -1,5 +1,6 @@ using Content.Server.Gravity; using Content.Shared.Alert; +using Content.Shared.Gravity; using Robust.Shared.GameObjects; namespace Content.IntegrationTests.Tests.Gravity @@ -38,6 +39,7 @@ namespace Content.IntegrationTests.Tests.Gravity var entityManager = server.ResolveDependency(); var alertsSystem = server.ResolveDependency().GetEntitySystem(); + var weightlessAlert = SharedGravitySystem.WeightlessAlert; EntityUid human = default; @@ -56,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Gravity await server.WaitAssertion(() => { // No gravity without a gravity generator - Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless)); + Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert)); generatorUid = entityManager.SpawnEntity("WeightlessGravityGeneratorDummy", entityManager.GetComponent(human).Coordinates); }); @@ -66,7 +68,7 @@ namespace Content.IntegrationTests.Tests.Gravity await server.WaitAssertion(() => { - Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless), Is.False); + Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert), Is.False); // This should kill gravity entityManager.DeleteEntity(generatorUid); @@ -76,7 +78,7 @@ namespace Content.IntegrationTests.Tests.Gravity await server.WaitAssertion(() => { - Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless)); + Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert)); }); await pair.RunTicksSync(10); diff --git a/Content.Server/Abilities/Mime/MimePowersComponent.cs b/Content.Server/Abilities/Mime/MimePowersComponent.cs index fd4fc2c2af..d56644ed19 100644 --- a/Content.Server/Abilities/Mime/MimePowersComponent.cs +++ b/Content.Server/Abilities/Mime/MimePowersComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Alert; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -47,5 +48,12 @@ namespace Content.Server.Abilities.Mime /// [DataField("vowCooldown")] public TimeSpan VowCooldown = TimeSpan.FromMinutes(5); + + [DataField] + public ProtoId VowAlert = "VowOfSilence"; + + [DataField] + public ProtoId VowBrokenAlert = "VowBroken"; + } } diff --git a/Content.Server/Abilities/Mime/MimePowersSystem.cs b/Content.Server/Abilities/Mime/MimePowersSystem.cs index 629fe99344..f3bf6e703f 100644 --- a/Content.Server/Abilities/Mime/MimePowersSystem.cs +++ b/Content.Server/Abilities/Mime/MimePowersSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Popups; -using Content.Server.Speech.Muting; using Content.Shared.Actions; using Content.Shared.Actions.Events; using Content.Shared.Alert; @@ -54,7 +53,7 @@ namespace Content.Server.Abilities.Mime private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args) { EnsureComp(uid); - _alertsSystem.ShowAlert(uid, AlertType.VowOfSilence); + _alertsSystem.ShowAlert(uid, component.VowAlert); _actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid); } @@ -115,8 +114,8 @@ namespace Content.Server.Abilities.Mime mimePowers.VowBroken = true; mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown; RemComp(uid); - _alertsSystem.ClearAlert(uid, AlertType.VowOfSilence); - _alertsSystem.ShowAlert(uid, AlertType.VowBroken); + _alertsSystem.ClearAlert(uid, mimePowers.VowAlert); + _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert); _actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity); } @@ -138,8 +137,8 @@ namespace Content.Server.Abilities.Mime mimePowers.ReadyToRepent = false; mimePowers.VowBroken = false; AddComp(uid); - _alertsSystem.ClearAlert(uid, AlertType.VowBroken); - _alertsSystem.ShowAlert(uid, AlertType.VowOfSilence); + _alertsSystem.ClearAlert(uid, mimePowers.VowAlert); + _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert); _actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid); } } diff --git a/Content.Server/Alert/Commands/ClearAlert.cs b/Content.Server/Alert/Commands/ClearAlert.cs index 73a6ca52c7..2e317de754 100644 --- a/Content.Server/Alert/Commands/ClearAlert.cs +++ b/Content.Server/Alert/Commands/ClearAlert.cs @@ -40,13 +40,13 @@ namespace Content.Server.Alert.Commands var alertType = args[0]; var alertsSystem = _e.System(); - if (!alertsSystem.TryGet(Enum.Parse(alertType), out var alert)) + if (!alertsSystem.TryGet(alertType, out var alert)) { shell.WriteLine("unrecognized alertType " + alertType); return; } - alertsSystem.ClearAlert(attachedEntity, alert.AlertType); + alertsSystem.ClearAlert(attachedEntity, alert.ID); } } } diff --git a/Content.Server/Alert/Commands/ShowAlert.cs b/Content.Server/Alert/Commands/ShowAlert.cs index f37ab23f2f..cae24ff336 100644 --- a/Content.Server/Alert/Commands/ShowAlert.cs +++ b/Content.Server/Alert/Commands/ShowAlert.cs @@ -41,7 +41,7 @@ namespace Content.Server.Alert.Commands var alertType = args[0]; var severity = args[1]; var alertsSystem = _e.System(); - if (!alertsSystem.TryGet(Enum.Parse(alertType), out var alert)) + if (!alertsSystem.TryGet(alertType, out var alert)) { shell.WriteLine("unrecognized alertType " + alertType); return; @@ -53,7 +53,7 @@ namespace Content.Server.Alert.Commands } short? severity1 = sevint == -1 ? null : sevint; - alertsSystem.ShowAlert(attachedEntity, alert.AlertType, severity1, null); + alertsSystem.ShowAlert(attachedEntity, alert.ID, severity1, null); } } } diff --git a/Content.Server/Atmos/Components/BarotraumaComponent.cs b/Content.Server/Atmos/Components/BarotraumaComponent.cs index 4e29699872..d261c5ab03 100644 --- a/Content.Server/Atmos/Components/BarotraumaComponent.cs +++ b/Content.Server/Atmos/Components/BarotraumaComponent.cs @@ -1,5 +1,7 @@ +using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; namespace Content.Server.Atmos.Components { @@ -46,5 +48,13 @@ namespace Content.Server.Atmos.Components [ViewVariables(VVAccess.ReadWrite)] public bool HasImmunity = false; + [DataField] + public ProtoId HighPressureAlert = "HighPressure"; + + [DataField] + public ProtoId LowPressureAlert = "LowPressure"; + + [DataField] + public ProtoId PressureAlertCategory = "Pressure"; } } diff --git a/Content.Server/Atmos/Components/FlammableComponent.cs b/Content.Server/Atmos/Components/FlammableComponent.cs index b02109d433..6f804e2c2b 100644 --- a/Content.Server/Atmos/Components/FlammableComponent.cs +++ b/Content.Server/Atmos/Components/FlammableComponent.cs @@ -1,5 +1,7 @@ +using Content.Shared.Alert; using Content.Shared.Damage; using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Prototypes; namespace Content.Server.Atmos.Components { @@ -78,6 +80,9 @@ namespace Content.Server.Atmos.Components [DataField, ViewVariables(VVAccess.ReadWrite)] public float FirestackFade = -0.1f; + [DataField] + public ProtoId FireAlert = "Fire"; + /// /// CrystallPunk fireplace fuel /// diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs index 98a5ffa70a..ec508790ba 100644 --- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs +++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs @@ -245,7 +245,7 @@ namespace Content.Server.Atmos.EntitySystems _adminLogger.Add(LogType.Barotrauma, $"{ToPrettyString(uid):entity} started taking low pressure damage"); } - _alertsSystem.ShowAlert(uid, AlertType.LowPressure, 2); + _alertsSystem.ShowAlert(uid, barotrauma.LowPressureAlert, 2); } else if (pressure >= Atmospherics.HazardHighPressure) { @@ -260,7 +260,7 @@ namespace Content.Server.Atmos.EntitySystems _adminLogger.Add(LogType.Barotrauma, $"{ToPrettyString(uid):entity} started taking high pressure damage"); } - _alertsSystem.ShowAlert(uid, AlertType.HighPressure, 2); + _alertsSystem.ShowAlert(uid, barotrauma.HighPressureAlert, 2); } else { @@ -275,13 +275,13 @@ namespace Content.Server.Atmos.EntitySystems switch (pressure) { case <= Atmospherics.WarningLowPressure: - _alertsSystem.ShowAlert(uid, AlertType.LowPressure, 1); + _alertsSystem.ShowAlert(uid, barotrauma.LowPressureAlert, 1); break; case >= Atmospherics.WarningHighPressure: - _alertsSystem.ShowAlert(uid, AlertType.HighPressure, 1); + _alertsSystem.ShowAlert(uid, barotrauma.HighPressureAlert, 1); break; default: - _alertsSystem.ClearAlertCategory(uid, AlertCategory.Pressure); + _alertsSystem.ClearAlertCategory(uid, barotrauma.PressureAlertCategory); break; } } diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 390bcab463..78b2402f4b 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -427,11 +427,11 @@ namespace Content.Server.Atmos.EntitySystems if (!flammable.OnFire) { - _alertsSystem.ClearAlert(uid, AlertType.Fire); + _alertsSystem.ClearAlert(uid, flammable.FireAlert); continue; } - _alertsSystem.ShowAlert(uid, AlertType.Fire); + _alertsSystem.ShowAlert(uid, flammable.FireAlert); if (flammable.FireStacks > 0) { diff --git a/Content.Server/Body/Components/BloodstreamComponent.cs b/Content.Server/Body/Components/BloodstreamComponent.cs index 1d8aa9ffd3..a6d2afab21 100644 --- a/Content.Server/Body/Components/BloodstreamComponent.cs +++ b/Content.Server/Body/Components/BloodstreamComponent.cs @@ -1,5 +1,6 @@ using Content.Server.Body.Systems; using Content.Server.Chemistry.EntitySystems; +using Content.Shared.Alert; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage; @@ -171,5 +172,8 @@ namespace Content.Server.Body.Components /// [ViewVariables(VVAccess.ReadWrite)] public TimeSpan StatusTime; + + [DataField] + public ProtoId BleedingAlert = "Bleed"; } } diff --git a/Content.Server/Body/Components/InternalsComponent.cs b/Content.Server/Body/Components/InternalsComponent.cs index 18caab8dcf..098f178921 100644 --- a/Content.Server/Body/Components/InternalsComponent.cs +++ b/Content.Server/Body/Components/InternalsComponent.cs @@ -1,3 +1,6 @@ +using Content.Shared.Alert; +using Robust.Shared.Prototypes; + namespace Content.Server.Body.Components { /// @@ -18,5 +21,8 @@ namespace Content.Server.Body.Components [ViewVariables(VVAccess.ReadWrite)] [DataField] public TimeSpan Delay = TimeSpan.FromSeconds(3); + + [DataField] + public ProtoId InternalsAlert = "Internals"; } } diff --git a/Content.Server/Body/Components/LungComponent.cs b/Content.Server/Body/Components/LungComponent.cs index 46600b3020..72af4d9e63 100644 --- a/Content.Server/Body/Components/LungComponent.cs +++ b/Content.Server/Body/Components/LungComponent.cs @@ -1,8 +1,8 @@ -using Content.Server.Atmos; using Content.Server.Body.Systems; using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Chemistry.Components; +using Robust.Shared.Prototypes; namespace Content.Server.Body.Components; @@ -33,5 +33,5 @@ public sealed partial class LungComponent : Component /// The type of gas this lung needs. Used only for the breathing alerts, not actual metabolism. /// [DataField] - public AlertType Alert = AlertType.LowOxygen; + public ProtoId Alert = "LowOxygen"; } diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index ddeb154e79..f961307fc6 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -392,11 +392,11 @@ public sealed class BloodstreamSystem : EntitySystem component.BleedAmount = Math.Clamp(component.BleedAmount, 0, component.MaxBleedAmount); if (component.BleedAmount == 0) - _alertsSystem.ClearAlert(uid, AlertType.Bleed); + _alertsSystem.ClearAlert(uid, component.BleedingAlert); else { var severity = (short) Math.Clamp(Math.Round(component.BleedAmount, MidpointRounding.ToZero), 0, 10); - _alertsSystem.ShowAlert(uid, AlertType.Bleed, severity); + _alertsSystem.ShowAlert(uid, component.BleedingAlert, severity); } return true; diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs index 8afd1c767f..c1e1de2baa 100644 --- a/Content.Server/Body/Systems/InternalsSystem.cs +++ b/Content.Server/Body/Systems/InternalsSystem.cs @@ -144,12 +144,12 @@ public sealed class InternalsSystem : EntitySystem private void OnInternalsStartup(Entity ent, ref ComponentStartup args) { - _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent)); + _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); } private void OnInternalsShutdown(Entity ent, ref ComponentShutdown args) { - _alerts.ClearAlert(ent, AlertType.Internals); + _alerts.ClearAlert(ent, ent.Comp.InternalsAlert); } private void OnInhaleLocation(Entity ent, ref InhaleLocationEvent args) @@ -159,7 +159,7 @@ public sealed class InternalsSystem : EntitySystem var gasTank = Comp(ent.Comp.GasTankEntity!.Value); args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume); // TODO: Should listen to gas tank updates instead I guess? - _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent)); + _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); } } public void DisconnectBreathTool(Entity ent) @@ -173,7 +173,7 @@ public sealed class InternalsSystem : EntitySystem DisconnectTank(ent); } - _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent)); + _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); } public void ConnectBreathTool(Entity ent, EntityUid toolEntity) @@ -184,7 +184,7 @@ public sealed class InternalsSystem : EntitySystem } ent.Comp.BreathToolEntity = toolEntity; - _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent)); + _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); } public void DisconnectTank(InternalsComponent? component) @@ -196,7 +196,7 @@ public sealed class InternalsSystem : EntitySystem _gasTank.DisconnectFromInternals((component.GasTankEntity.Value, tank)); component.GasTankEntity = null; - _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component)); + _alerts.ShowAlert(component.Owner, component.InternalsAlert, GetSeverity(component)); } public bool TryConnectTank(Entity ent, EntityUid tankEntity) @@ -208,7 +208,7 @@ public sealed class InternalsSystem : EntitySystem _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank)); ent.Comp.GasTankEntity = tankEntity; - _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent)); + _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent)); return true; } diff --git a/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs b/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs index 8d475570ad..40858176bd 100644 --- a/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs +++ b/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs @@ -10,8 +10,8 @@ public sealed partial class AdjustAlert : ReagentEffect /// /// The specific Alert that will be adjusted /// - [DataField("alertType", required: true)] - public AlertType Type; + [DataField(required: true)] + public ProtoId AlertType; /// /// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately. @@ -42,7 +42,7 @@ public sealed partial class AdjustAlert : ReagentEffect if (Clear && Time <= 0) { - alertSys.ClearAlert(args.SolutionEntity, Type); + alertSys.ClearAlert(args.SolutionEntity, AlertType); } else { @@ -52,7 +52,7 @@ public sealed partial class AdjustAlert : ReagentEffect if ((ShowCooldown || Clear) && Time > 0) cooldown = (timing.CurTime, timing.CurTime + TimeSpan.FromSeconds(Time)); - alertSys.ShowAlert(args.SolutionEntity, Type, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown); + alertSys.ShowAlert(args.SolutionEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown); } } diff --git a/Content.Server/Clothing/MagbootsSystem.cs b/Content.Server/Clothing/MagbootsSystem.cs index f12558389e..3838ad168d 100644 --- a/Content.Server/Clothing/MagbootsSystem.cs +++ b/Content.Server/Clothing/MagbootsSystem.cs @@ -29,11 +29,11 @@ public sealed class MagbootsSystem : SharedMagbootsSystem if (state) { - _alerts.ShowAlert(parent, AlertType.Magboots); + _alerts.ShowAlert(parent, component.MagbootsAlert); } else { - _alerts.ClearAlert(parent, AlertType.Magboots); + _alerts.ClearAlert(parent, component.MagbootsAlert); } } diff --git a/Content.Server/Configurable/ConfigurationSystem.cs b/Content.Server/Configurable/ConfigurationSystem.cs index 2683bf4e09..5f5f1ef7d1 100644 --- a/Content.Server/Configurable/ConfigurationSystem.cs +++ b/Content.Server/Configurable/ConfigurationSystem.cs @@ -24,6 +24,7 @@ public sealed class ConfigurationSystem : EntitySystem private void OnInteractUsing(EntityUid uid, ConfigurationComponent component, InteractUsingEvent args) { + // TODO use activatable ui system if (args.Handled) return; diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index 3d4c8a4ec5..e2b8b5829d 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -1,67 +1,54 @@ using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Shuttles.Components; -using Content.Shared.Access.Systems; using Content.Shared.Atmos; using Content.Shared.Atmos.Monitor; using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; -using Content.Shared.Popups; -using Content.Shared.Prying.Components; using Robust.Shared.Map.Components; namespace Content.Server.Doors.Systems { - public sealed class FirelockSystem : EntitySystem + public sealed class FirelockSystem : SharedFirelockSystem { - [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoorSystem _doorSystem = default!; [Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!; [Dependency] private readonly AtmosphereSystem _atmosSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; + [Dependency] private readonly SharedMapSystem _mapping = default!; - private static float _visualUpdateInterval = 0.5f; - private float _accumulatedFrameTime; + private const int UpdateInterval = 30; + private int _accumulatedTicks; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnBeforeDoorOpened); - SubscribeLocalEvent(OnDoorGetPryTimeModifier); - SubscribeLocalEvent(OnUpdateState); - - SubscribeLocalEvent(OnBeforeDoorAutoclose); SubscribeLocalEvent(OnAtmosAlarm); - // Visuals - SubscribeLocalEvent(UpdateVisuals); - SubscribeLocalEvent(UpdateVisuals); SubscribeLocalEvent(PowerChanged); + } private void PowerChanged(EntityUid uid, FirelockComponent component, ref PowerChangedEvent args) { // TODO this should REALLLLY not be door specific appearance thing. _appearance.SetData(uid, DoorVisuals.Powered, args.Powered); + component.Powered = args.Powered; + Dirty(uid, component); } - #region Visuals - private void UpdateVisuals(EntityUid uid, FirelockComponent component, EntityEventArgs args) => UpdateVisuals(uid, component); - public override void Update(float frameTime) { - _accumulatedFrameTime += frameTime; - if (_accumulatedFrameTime < _visualUpdateInterval) + _accumulatedTicks += 1; + if (_accumulatedTicks < UpdateInterval) return; - _accumulatedFrameTime -= _visualUpdateInterval; + _accumulatedTicks = 0; var airtightQuery = GetEntityQuery(); var appearanceQuery = GetEntityQuery(); @@ -84,107 +71,13 @@ namespace Content.Server.Doors.Systems { var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery); _appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); + firelock.Temperature = fire; + firelock.Pressure = pressure; + Dirty(uid, firelock); } } } - private void UpdateVisuals(EntityUid uid, - FirelockComponent? firelock = null, - DoorComponent? door = null, - AirtightComponent? airtight = null, - AppearanceComponent? appearance = null, - TransformComponent? xform = null) - { - if (!Resolve(uid, ref door, ref appearance, false)) - return; - - // only bother to check pressure on doors that are some variation of closed. - if (door.State != DoorState.Closed - && door.State != DoorState.Welded - && door.State != DoorState.Denying) - { - _appearance.SetData(uid, DoorVisuals.ClosedLights, false, appearance); - return; - } - - var query = GetEntityQuery(); - if (!Resolve(uid, ref firelock, ref airtight, ref appearance, ref xform, false) || !query.Resolve(uid, ref airtight, false)) - return; - - var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, query); - _appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); - } - #endregion - - public bool EmergencyPressureStop(EntityUid uid, FirelockComponent? firelock = null, DoorComponent? door = null) - { - if (!Resolve(uid, ref firelock, ref door)) - return false; - - if (door.State == DoorState.Open) - { - if (_doorSystem.TryClose(uid, door)) - { - return _doorSystem.OnPartialClose(uid, door); - } - } - return false; - } - - private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args) - { - // Give the Door remote the ability to force a firelock open even if it is holding back dangerous gas - var overrideAccess = (args.User != null) && _accessReaderSystem.IsAllowed(args.User.Value, uid); - - if (!this.IsPowered(uid, EntityManager) || (!overrideAccess && IsHoldingPressureOrFire(uid, component))) - args.Cancel(); - } - - private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args) - { - var state = CheckPressureAndFire(uid, component); - - if (state.Fire) - { - _popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-fire-message"), - uid, args.User, PopupType.MediumCaution); - } - else if (state.Pressure) - { - _popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-pressure-message"), - uid, args.User, PopupType.MediumCaution); - } - - if (state.Fire || state.Pressure) - args.PryTimeModifier *= component.LockedPryTimeModifier; - } - - private void OnUpdateState(EntityUid uid, FirelockComponent component, DoorStateChangedEvent args) - { - var ev = new BeforeDoorAutoCloseEvent(); - RaiseLocalEvent(uid, ev); - UpdateVisuals(uid, component, args); - if (ev.Cancelled) - { - return; - } - - _doorSystem.SetNextStateChange(uid, component.AutocloseDelay); - } - - private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args) - { - if (!this.IsPowered(uid, EntityManager)) - args.Cancel(); - - // Make firelocks autoclose, but only if the last alarm type it - // remembers was a danger. This is to prevent people from - // flooding hallways with endless bad air/fire. - if (component.AlarmAutoClose && - (_atmosAlarmable.TryGetHighestAlert(uid, out var alarm) && alarm != AtmosAlarmType.Danger || alarm == null)) - args.Cancel(); - } - private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosAlarmEvent args) { if (!this.IsPowered(uid, EntityManager)) @@ -204,12 +97,6 @@ namespace Content.Server.Doors.Systems } } - public bool IsHoldingPressureOrFire(EntityUid uid, FirelockComponent firelock) - { - var result = CheckPressureAndFire(uid, firelock); - return result.Pressure || result.Fire; - } - public (bool Pressure, bool Fire) CheckPressureAndFire(EntityUid uid, FirelockComponent firelock) { var query = GetEntityQuery(); @@ -234,17 +121,17 @@ namespace Content.Server.Doors.Systems return (false, false); } - if (!TryComp(xform.ParentUid, out GridAtmosphereComponent? gridAtmosphere)) + if (!HasComp(xform.ParentUid)) return (false, false); var grid = Comp(xform.ParentUid); - var pos = grid.CoordinatesToTile(xform.Coordinates); + var pos = _mapping.CoordinatesToTile(xform.ParentUid, grid, xform.Coordinates); var minPressure = float.MaxValue; var maxPressure = float.MinValue; var minTemperature = float.MaxValue; var maxTemperature = float.MinValue; - bool holdingFire = false; - bool holdingPressure = false; + var holdingFire = false; + var holdingPressure = false; // We cannot simply use `_atmosSystem.GetAdjacentTileMixtures` because of how the `includeBlocked` option // works, we want to ignore the firelock's blocking, while including blockers on other tiles. @@ -284,7 +171,7 @@ namespace Content.Server.Doors.Systems { // Is there some airtight entity blocking this direction? If yes, don't include this direction in the // pressure differential - if (HasAirtightBlocker(grid.GetAnchoredEntities(adjacentPos), dir.GetOpposite(), airtightQuery)) + if (HasAirtightBlocker(_mapping.GetAnchoredEntities(xform.ParentUid, grid, adjacentPos), dir.GetOpposite(), airtightQuery)) continue; var p = gas.Pressure; diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs index 105a8f9720..51d3242ce4 100644 --- a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs +++ b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs @@ -163,8 +163,8 @@ public sealed partial class EnsnareableSystem public void UpdateAlert(EntityUid target, EnsnareableComponent component) { if (!component.IsEnsnared) - _alerts.ClearAlert(target, AlertType.Ensnared); + _alerts.ClearAlert(target, component.EnsnaredAlert); else - _alerts.ShowAlert(target, AlertType.Ensnared); + _alerts.ShowAlert(target, component.EnsnaredAlert); } } diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 4eac7e9ef1..9ac82b2185 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -7,31 +7,6 @@ using Robust.Shared.Player; namespace Content.Server.Interaction { - /// - /// Governs interactions during clicking on entities - /// - [UsedImplicitly] - public sealed partial class InteractionSystem : SharedInteractionSystem - { - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; - - public override bool CanAccessViaStorage(EntityUid user, EntityUid target) - { - if (Deleted(target)) - return false; - - if (!_container.TryGetContainingContainer(target, out var container)) - return false; - - if (!TryComp(container.Owner, out StorageComponent? storage)) - return false; - - if (storage.Container?.ID != container.ID) - return false; - - // we don't check if the user can access the storage entity itself. This should be handed by the UI system. - return _uiSystem.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, user); - } - } + // TODO Remove Shared prefix + public sealed class InteractionSystem : SharedInteractionSystem; } diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs index 1dfaf4f339..0c1e88653f 100644 --- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs +++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs @@ -102,20 +102,23 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem /// public void SetSuitPowerAlert(EntityUid uid, SpaceNinjaComponent? comp = null) { - if (!Resolve(uid, ref comp, false) || comp.Deleted || comp.Suit == null) + if (!Resolve(uid, ref comp, false)) + return; + + if (comp.Deleted || comp.Suit == null) { - _alerts.ClearAlert(uid, AlertType.SuitPower); + _alerts.ClearAlert(uid, comp.SuitPowerAlert); return; } if (GetNinjaBattery(uid, out _, out var battery)) { var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8); - _alerts.ShowAlert(uid, AlertType.SuitPower, (short) severity); + _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity); } else { - _alerts.ClearAlert(uid, AlertType.SuitPower); + _alerts.ClearAlert(uid, comp.SuitPowerAlert); } } diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index 86be70c41f..c390432f3a 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -141,7 +141,7 @@ public sealed partial class RevenantSystem : EntitySystem if (TryComp(uid, out var store)) _store.UpdateUserInterface(uid, uid, store); - _alerts.ShowAlert(uid, AlertType.Essence); + _alerts.ShowAlert(uid, component.EssenceAlert); if (component.Essence <= 0) { diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs index 2b5769881d..7a19fd13b2 100644 --- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs @@ -317,7 +317,7 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem component.SubscribedPilots.Add(entity); - _alertsSystem.ShowAlert(entity, AlertType.PilotingShuttle); + _alertsSystem.ShowAlert(entity, pilotComponent.PilotingAlert); pilotComponent.Console = uid; ActionBlockerSystem.UpdateCanMove(entity); @@ -339,7 +339,7 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem if (!helm.SubscribedPilots.Remove(pilotUid)) return; - _alertsSystem.ClearAlert(pilotUid, AlertType.PilotingShuttle); + _alertsSystem.ClearAlert(pilotUid, pilotComponent.PilotingAlert); _popup.PopupEntity(Loc.GetString("shuttle-pilot-end"), pilotUid, pilotUid); diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index ceab044d4c..97adfd00eb 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -84,7 +84,7 @@ public sealed partial class BorgSystem : SharedBorgSystem private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args) { - UpdateBatteryAlert(uid); + UpdateBatteryAlert((uid, component)); _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); } @@ -183,7 +183,7 @@ public sealed partial class BorgSystem : SharedBorgSystem private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, PowerCellChangedEvent args) { - UpdateBatteryAlert(uid); + UpdateBatteryAlert((uid, component)); if (!TryComp(uid, out var draw)) return; @@ -256,12 +256,12 @@ public sealed partial class BorgSystem : SharedBorgSystem args.Cancel(); } - private void UpdateBatteryAlert(EntityUid uid, PowerCellSlotComponent? slotComponent = null) + private void UpdateBatteryAlert(Entity ent, PowerCellSlotComponent? slotComponent = null) { - if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery, slotComponent)) + if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery, slotComponent)) { - _alerts.ClearAlert(uid, AlertType.BorgBattery); - _alerts.ShowAlert(uid, AlertType.BorgBatteryNone); + _alerts.ClearAlert(ent, ent.Comp.BatteryAlert); + _alerts.ShowAlert(ent, ent.Comp.NoBatteryAlert); return; } @@ -269,13 +269,13 @@ public sealed partial class BorgSystem : SharedBorgSystem // we make sure 0 only shows if they have absolutely no battery. // also account for floating point imprecision - if (chargePercent == 0 && _powerCell.HasDrawCharge(uid, cell: slotComponent)) + if (chargePercent == 0 && _powerCell.HasDrawCharge(ent, cell: slotComponent)) { chargePercent = 1; } - _alerts.ClearAlert(uid, AlertType.BorgBatteryNone); - _alerts.ShowAlert(uid, AlertType.BorgBattery, chargePercent); + _alerts.ClearAlert(ent, ent.Comp.NoBatteryAlert); + _alerts.ShowAlert(ent, ent.Comp.BatteryAlert, chargePercent); } /// diff --git a/Content.Server/StationEvents/Events/ImmovableRodRule.cs b/Content.Server/StationEvents/Events/ImmovableRodRule.cs index cacb839cd3..aa193f2f4c 100644 --- a/Content.Server/StationEvents/Events/ImmovableRodRule.cs +++ b/Content.Server/StationEvents/Events/ImmovableRodRule.cs @@ -28,7 +28,9 @@ public sealed class ImmovableRodRule : StationEventSystem(out var rod) && proto.TryGetComponent(out var despawn)) { - TryFindRandomTile(out _, out _, out _, out var targetCoords); + if (!TryFindRandomTile(out _, out _, out _, out var targetCoords)) + return; + var speed = RobustRandom.NextFloat(rod.MinSpeed, rod.MaxSpeed); var angle = RobustRandom.NextAngle(); var direction = angle.ToVec(); diff --git a/Content.Server/Temperature/Components/TemperatureComponent.cs b/Content.Server/Temperature/Components/TemperatureComponent.cs index ec00a570f9..3bfa12f269 100644 --- a/Content.Server/Temperature/Components/TemperatureComponent.cs +++ b/Content.Server/Temperature/Components/TemperatureComponent.cs @@ -1,7 +1,9 @@ using Content.Server.Temperature.Systems; +using Content.Shared.Alert; using Content.Shared.Atmos; using Content.Shared.Damage; using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; namespace Content.Server.Temperature.Components; @@ -78,4 +80,10 @@ public sealed partial class TemperatureComponent : Component /// [DataField] public bool TakingDamage = false; + + [DataField] + public ProtoId HotAlert = "Hot"; + + [DataField] + public ProtoId ColdAlert = "Cold"; } diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs index 01a5dcb274..78401cfa9f 100644 --- a/Content.Server/Temperature/Systems/TemperatureSystem.cs +++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Inventory; using Content.Shared.Rejuvenate; using Content.Shared.Temperature; using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; namespace Content.Server.Temperature.Systems; @@ -33,6 +34,9 @@ public sealed class TemperatureSystem : EntitySystem private float _accumulatedFrametime; + [ValidatePrototypeId] + public const string TemperatureAlertCategory = "Temperature"; + public override void Initialize() { SubscribeLocalEvent(EnqueueDamage); @@ -184,13 +188,13 @@ public sealed class TemperatureSystem : EntitySystem private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args) { - AlertType type; + ProtoId type; float threshold; float idealTemp; if (!TryComp(uid, out var temperature)) { - _alerts.ClearAlertCategory(uid, AlertCategory.Temperature); + _alerts.ClearAlertCategory(uid, TemperatureAlertCategory); return; } @@ -207,12 +211,12 @@ public sealed class TemperatureSystem : EntitySystem if (args.CurrentTemperature <= idealTemp) { - type = AlertType.Cold; + type = temperature.ColdAlert; threshold = temperature.ColdDamageThreshold; } else { - type = AlertType.Hot; + type = temperature.HotAlert; threshold = temperature.HeatDamageThreshold; } @@ -234,7 +238,7 @@ public sealed class TemperatureSystem : EntitySystem break; case > 0.66f: - _alerts.ClearAlertCategory(uid, AlertCategory.Temperature); + _alerts.ClearAlertCategory(uid, TemperatureAlertCategory); break; } } diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 2612e99ec9..190a2d0263 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -187,15 +187,10 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem if (session is { } pSession) { (targetCoordinates, targetLocalAngle) = _lag.GetCoordinatesAngle(target, pSession); - } - else - { - var xform = Transform(target); - targetCoordinates = xform.Coordinates; - targetLocalAngle = xform.LocalRotation; + return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range); } - return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range); + return Interaction.InRangeUnobstructed(user, target, range); } protected override void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform) diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 30687c9322..c92d67ba81 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -502,13 +502,7 @@ public abstract class SharedActionsSystem : EntitySystem return distance <= action.Range; } - if (_interactionSystem.InRangeUnobstructed(user, target, range: action.Range) - && _containerSystem.IsInSameOrParentContainer(user, target)) - { - return true; - } - - return _interactionSystem.CanAccessViaStorage(user, target); + return _interactionSystem.InRangeAndAccessible(user, target, range: action.Range); } public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity action) diff --git a/Content.Shared/Alert/AlertCategory.cs b/Content.Shared/Alert/AlertCategory.cs deleted file mode 100644 index 7450f585a4..0000000000 --- a/Content.Shared/Alert/AlertCategory.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Content.Shared.Alert; - -/// -/// Every category of alert. Corresponds to category field in alert prototypes defined in YML -/// -public enum AlertCategory -{ - Pressure, - Temperature, - Breathing, - Buckled, - Health, - Internals, - Stamina, - Piloting, - Hunger, - Thirst, - Toxins, - Battery -} diff --git a/Content.Shared/Alert/AlertCategoryPrototype.cs b/Content.Shared/Alert/AlertCategoryPrototype.cs new file mode 100644 index 0000000000..7c7d047521 --- /dev/null +++ b/Content.Shared/Alert/AlertCategoryPrototype.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Alert; + +/// +/// This is a prototype for a category for marking alerts as mutually exclusive. +/// +[Prototype] +public sealed partial class AlertCategoryPrototype : IPrototype +{ + /// + [IdDataField] + public string ID { get; } = default!; +} diff --git a/Content.Shared/Alert/AlertKey.cs b/Content.Shared/Alert/AlertKey.cs index c784af4cd4..c5c3a7643e 100644 --- a/Content.Shared/Alert/AlertKey.cs +++ b/Content.Shared/Alert/AlertKey.cs @@ -1,5 +1,5 @@ -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; namespace Content.Shared.Alert; @@ -11,13 +11,13 @@ namespace Content.Shared.Alert; [Serializable, NetSerializable] public struct AlertKey { - public AlertType? AlertType { get; private set; } = Alert.AlertType.Error; - public readonly AlertCategory? AlertCategory; + public ProtoId? AlertType { get; private set; } = default!; + public readonly ProtoId? AlertCategory; /// NOTE: if the alert has a category you must pass the category for this to work /// properly as a key. I.e. if the alert has a category and you pass only the alert type, and you /// compare this to another AlertKey that has both the category and the same alert type, it will not consider them equal. - public AlertKey(AlertType? alertType, AlertCategory? alertCategory) + public AlertKey(ProtoId? alertType, ProtoId? alertCategory) { AlertCategory = alertCategory; AlertType = alertType; @@ -49,7 +49,7 @@ public struct AlertKey /// alert category, must not be null /// An alert key for the provided alert category. This must only be used for /// queries and never storage, as it is lacking an alert type. - public static AlertKey ForCategory(AlertCategory category) + public static AlertKey ForCategory(ProtoId category) { return new(null, category); } diff --git a/Content.Shared/Alert/AlertOrderPrototype.cs b/Content.Shared/Alert/AlertOrderPrototype.cs index 8279d592b4..af4241a27e 100644 --- a/Content.Shared/Alert/AlertOrderPrototype.cs +++ b/Content.Shared/Alert/AlertOrderPrototype.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Alert /// /// Defines the order of alerts so they show up in a consistent order. /// - [Prototype("alertOrder")] + [Prototype] [DataDefinition] public sealed partial class AlertOrderPrototype : IPrototype, IComparer { @@ -15,7 +15,7 @@ namespace Content.Shared.Alert [IdDataField] public string ID { get; private set; } = default!; - [DataField("order")] + [DataField] private (string type, string alert)[] Order { // why would paul do this to me. @@ -46,10 +46,10 @@ namespace Content.Shared.Alert switch (type) { case "alertType": - _typeToIdx[Enum.Parse(alert)] = i++; + _typeToIdx[alert] = i++; break; case "category": - _categoryToIdx[Enum.Parse(alert)] = i++; + _categoryToIdx[alert] = i++; break; default: throw new ArgumentException(); @@ -58,17 +58,17 @@ namespace Content.Shared.Alert } } - private readonly Dictionary _typeToIdx = new(); - private readonly Dictionary _categoryToIdx = new(); + private readonly Dictionary, int> _typeToIdx = new(); + private readonly Dictionary, int> _categoryToIdx = new(); private int GetOrderIndex(AlertPrototype alert) { - if (_typeToIdx.TryGetValue(alert.AlertType, out var idx)) + if (_typeToIdx.TryGetValue(alert.ID, out var idx)) { return idx; } if (alert.Category != null && - _categoryToIdx.TryGetValue((AlertCategory) alert.Category, out idx)) + _categoryToIdx.TryGetValue(alert.Category.Value, out idx)) { return idx; } @@ -78,20 +78,25 @@ namespace Content.Shared.Alert public int Compare(AlertPrototype? x, AlertPrototype? y) { - if ((x == null) && (y == null)) return 0; - if (x == null) return 1; - if (y == null) return -1; + if (x == null && y == null) + return 0; + if (x == null) + return 1; + if (y == null) + return -1; var idx = GetOrderIndex(x); var idy = GetOrderIndex(y); if (idx == -1 && idy == -1) { // break ties by type value // Must cast to int to avoid integer overflow when subtracting (enum's unsigned) - return (int)x.AlertType - (int)y.AlertType; + return string.Compare(x.ID, y.ID, StringComparison.InvariantCulture); } - if (idx == -1) return 1; - if (idy == -1) return -1; + if (idx == -1) + return 1; + if (idy == -1) + return -1; var result = idx - idy; // not strictly necessary (we don't care about ones that go at the same index) // but it makes the sort stable @@ -99,7 +104,7 @@ namespace Content.Shared.Alert { // break ties by type value // Must cast to int to avoid integer overflow when subtracting (enum's unsigned) - return (int)x.AlertType - (int)y.AlertType; + return string.Compare(x.ID, y.ID, StringComparison.InvariantCulture); } return result; diff --git a/Content.Shared/Alert/AlertPrototype.cs b/Content.Shared/Alert/AlertPrototype.cs index 248cc00ba4..f53da27c0d 100644 --- a/Content.Shared/Alert/AlertPrototype.cs +++ b/Content.Shared/Alert/AlertPrototype.cs @@ -1,120 +1,116 @@ using Robust.Shared.Prototypes; using Robust.Shared.Utility; -namespace Content.Shared.Alert +namespace Content.Shared.Alert; + +/// +/// An alert popup with associated icon, tooltip, and other data. +/// +[Prototype] +public sealed partial class AlertPrototype : IPrototype { /// - /// An alert popup with associated icon, tooltip, and other data. + /// Type of alert, no 2 alert prototypes should have the same one. /// - [Prototype("alert")] - public sealed partial class AlertPrototype : IPrototype + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// List of icons to use for this alert. Each entry corresponds to a different severity level, starting from the + /// minimum and incrementing upwards. If severities are not supported, the first entry is used. + /// + [DataField(required: true)] + public List Icons = new(); + + /// + /// An entity used for displaying the in the UI control. + /// + [DataField] + public EntProtoId AlertViewEntity = "AlertSpriteView"; + + /// + /// Name to show in tooltip window. Accepts formatting. + /// + [DataField] + public string Name { get; private set; } = string.Empty; + + /// + /// Description to show in tooltip window. Accepts formatting. + /// + [DataField] + public string Description { get; private set; } = string.Empty; + + /// + /// Category the alert belongs to. Only one alert of a given category + /// can be shown at a time. If one is shown while another is already being shown, + /// it will be replaced. This can be useful for categories of alerts which should naturally + /// replace each other and are mutually exclusive, for example lowpressure / highpressure, + /// hot / cold. If left unspecified, the alert will not replace or be replaced by any other alerts. + /// + [DataField] + public ProtoId? Category { get; private set; } + + /// + /// Key which is unique w.r.t category semantics (alerts with same category have equal keys, + /// alerts with no category have different keys). + /// + public AlertKey AlertKey => new(ID, Category); + + /// + /// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state. + /// + public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity; + + [DataField("minSeverity")] private short _minSeverity = 1; + + /// + /// Maximum severity level supported by this state. -1 (default) indicates + /// no severity levels are supported by the state. + /// + [DataField] + public short MaxSeverity = -1; + + /// + /// Indicates whether this state support severity levels + /// + public bool SupportsSeverity => MaxSeverity != -1; + + /// + /// Defines what to do when the alert is clicked. + /// This will always be null on clientside. + /// + [DataField(serverOnly: true)] + public IAlertClick? OnClick { get; private set; } + + /// severity level, if supported by this alert + /// the icon path to the texture for the provided severity level + public SpriteSpecifier GetIcon(short? severity = null) { - [ViewVariables] - string IPrototype.ID => AlertType.ToString(); + var minIcons = SupportsSeverity + ? MaxSeverity - MinSeverity + : 1; - /// - /// Type of alert, no 2 alert prototypes should have the same one. - /// - [IdDataField] - public AlertType AlertType { get; private set; } + if (Icons.Count < minIcons) + throw new InvalidOperationException($"Insufficient number of icons given for alert {ID}"); - /// - /// List of icons to use for this alert. Each entry corresponds to a different severity level, starting from the - /// minimum and incrementing upwards. If severities are not supported, the first entry is used. - /// - [DataField("icons", required: true)] - public List Icons = new(); + if (!SupportsSeverity) + return Icons[0]; - /// - /// An entity used for displaying the in the UI control. - /// - [DataField] - public EntProtoId AlertViewEntity = "AlertSpriteView"; - - /// - /// Name to show in tooltip window. Accepts formatting. - /// - [DataField("name")] - public string Name { get; private set; } = ""; - - /// - /// Description to show in tooltip window. Accepts formatting. - /// - [DataField("description")] - public string Description { get; private set; } = ""; - - /// - /// Category the alert belongs to. Only one alert of a given category - /// can be shown at a time. If one is shown while another is already being shown, - /// it will be replaced. This can be useful for categories of alerts which should naturally - /// replace each other and are mutually exclusive, for example lowpressure / highpressure, - /// hot / cold. If left unspecified, the alert will not replace or be replaced by any other alerts. - /// - [DataField("category")] - public AlertCategory? Category { get; private set; } - - /// - /// Key which is unique w.r.t category semantics (alerts with same category have equal keys, - /// alerts with no category have different keys). - /// - public AlertKey AlertKey => new(AlertType, Category); - - /// - /// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state. - /// - public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity; - - [DataField("minSeverity")] private short _minSeverity = 1; - - /// - /// Maximum severity level supported by this state. -1 (default) indicates - /// no severity levels are supported by the state. - /// - [DataField("maxSeverity")] - public short MaxSeverity = -1; - - /// - /// Indicates whether this state support severity levels - /// - public bool SupportsSeverity => MaxSeverity != -1; - - /// - /// Defines what to do when the alert is clicked. - /// This will always be null on clientside. - /// - [DataField("onClick", serverOnly: true)] - public IAlertClick? OnClick { get; private set; } - - /// severity level, if supported by this alert - /// the icon path to the texture for the provided severity level - public SpriteSpecifier GetIcon(short? severity = null) + if (severity == null) { - var minIcons = SupportsSeverity - ? MaxSeverity - MinSeverity - : 1; - - if (Icons.Count < minIcons) - throw new InvalidOperationException($"Insufficient number of icons given for alert {AlertType}"); - - if (!SupportsSeverity) - return Icons[0]; - - if (severity == null) - { - throw new ArgumentException($"No severity specified but this alert ({AlertKey}) has severity.", nameof(severity)); - } - - if (severity < MinSeverity) - { - throw new ArgumentOutOfRangeException(nameof(severity), $"Severity below minimum severity in {AlertKey}."); - } - - if (severity > MaxSeverity) - { - throw new ArgumentOutOfRangeException(nameof(severity), $"Severity above maximum severity in {AlertKey}."); - } - - return Icons[severity.Value - _minSeverity]; + throw new ArgumentException($"No severity specified but this alert ({AlertKey}) has severity.", nameof(severity)); } + + if (severity < MinSeverity) + { + throw new ArgumentOutOfRangeException(nameof(severity), $"Severity below minimum severity in {AlertKey}."); + } + + if (severity > MaxSeverity) + { + throw new ArgumentOutOfRangeException(nameof(severity), $"Severity above maximum severity in {AlertKey}."); + } + + return Icons[severity.Value - _minSeverity]; } } diff --git a/Content.Shared/Alert/AlertState.cs b/Content.Shared/Alert/AlertState.cs index effd952203..d6309f6b42 100644 --- a/Content.Shared/Alert/AlertState.cs +++ b/Content.Shared/Alert/AlertState.cs @@ -1,3 +1,4 @@ +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Alert; @@ -9,5 +10,5 @@ public struct AlertState public (TimeSpan, TimeSpan)? Cooldown; public bool AutoRemove; public bool ShowCooldown; - public AlertType Type; + public ProtoId Type; } diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs deleted file mode 100644 index b989b8d4b6..0000000000 --- a/Content.Shared/Alert/AlertType.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Content.Shared.Alert -{ - /// - /// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML - /// NOTE: Using byte for a compact encoding when sending this in messages, can upgrade - /// to ushort - /// - public enum AlertType : byte - { - Error, - LowOxygen, - LowNitrogen, - LowPressure, - HighPressure, - Fire, - Cold, - Hot, - Weightless, - Stun, - Handcuffed, - Ensnared, - Buckled, - HumanCrit, - HumanDead, - HumanHealth, - BorgBattery, - BorgBatteryNone, - PilotingShuttle, - Peckish, - Starving, - Thirsty, - Parched, - Stamina, - Pulled, - Pulling, - Magboots, - Internals, - Toxins, - Muted, - VowOfSilence, - VowBroken, - Essence, - Corporeal, - Bleed, - Pacified, - Debug1, - Debug2, - Debug3, - Debug4, - Debug5, - Debug6, - SuitPower, - BorgHealth, - BorgCrit, - BorgDead, - Deflecting - } - -} diff --git a/Content.Shared/Alert/AlertsSystem.cs b/Content.Shared/Alert/AlertsSystem.cs index 5b888e30c4..83c6fcd0dd 100644 --- a/Content.Shared/Alert/AlertsSystem.cs +++ b/Content.Shared/Alert/AlertsSystem.cs @@ -11,7 +11,7 @@ public abstract class AlertsSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _timing = default!; - private FrozenDictionary _typeToAlert = default!; + private FrozenDictionary, AlertPrototype> _typeToAlert = default!; public IReadOnlyDictionary? GetActiveAlerts(EntityUid euid) { @@ -20,23 +20,23 @@ public abstract class AlertsSystem : EntitySystem : null; } - public short GetSeverityRange(AlertType alertType) + public short GetSeverityRange(ProtoId alertType) { var minSeverity = _typeToAlert[alertType].MinSeverity; return (short)MathF.Max(minSeverity,_typeToAlert[alertType].MaxSeverity - minSeverity); } - public short GetMaxSeverity(AlertType alertType) + public short GetMaxSeverity(ProtoId alertType) { return _typeToAlert[alertType].MaxSeverity; } - public short GetMinSeverity(AlertType alertType) + public short GetMinSeverity(ProtoId alertType) { return _typeToAlert[alertType].MinSeverity; } - public bool IsShowingAlert(EntityUid euid, AlertType alertType) + public bool IsShowingAlert(EntityUid euid, ProtoId alertType) { if (!EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent)) return false; @@ -51,7 +51,7 @@ public abstract class AlertsSystem : EntitySystem } /// true iff an alert of the indicated alert category is currently showing - public bool IsShowingAlertCategory(EntityUid euid, AlertCategory alertCategory) + public bool IsShowingAlertCategory(EntityUid euid, ProtoId alertCategory) { return EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent) && alertsComponent.Alerts.ContainsKey(AlertKey.ForCategory(alertCategory)); @@ -78,7 +78,7 @@ public abstract class AlertsSystem : EntitySystem /// be erased if there is currently a cooldown for the alert) /// if true, the alert will be removed at the end of the cooldown /// if true, the cooldown will be visibly shown over the alert icon - public void ShowAlert(EntityUid euid, AlertType alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null, bool autoRemove = false, bool showCooldown = true ) + public void ShowAlert(EntityUid euid, ProtoId alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null, bool autoRemove = false, bool showCooldown = true ) { // This should be handled as part of networking. if (_timing.ApplyingState) @@ -131,7 +131,7 @@ public abstract class AlertsSystem : EntitySystem /// /// Clear the alert with the given category, if one is currently showing. /// - public void ClearAlertCategory(EntityUid euid, AlertCategory category) + public void ClearAlertCategory(EntityUid euid, ProtoId category) { if(!TryComp(euid, out AlertsComponent? alertsComponent)) return; @@ -150,7 +150,7 @@ public abstract class AlertsSystem : EntitySystem /// /// Clear the alert of the given type if it is currently showing. /// - public void ClearAlert(EntityUid euid, AlertType alertType) + public void ClearAlert(EntityUid euid, ProtoId alertType) { if (_timing.ApplyingState) return; @@ -286,13 +286,13 @@ public abstract class AlertsSystem : EntitySystem protected virtual void LoadPrototypes() { - var dict = new Dictionary(); + var dict = new Dictionary, AlertPrototype>(); foreach (var alert in _prototypeManager.EnumeratePrototypes()) { - if (!dict.TryAdd(alert.AlertType, alert)) + if (!dict.TryAdd(alert.ID, alert)) { Log.Error("Found alert with duplicate alertType {0} - all alerts must have" + - " a unique alertType, this one will be skipped", alert.AlertType); + " a unique alertType, this one will be skipped", alert.ID); } } @@ -303,7 +303,7 @@ public abstract class AlertsSystem : EntitySystem /// Tries to get the alert of the indicated type /// /// true if found - public bool TryGet(AlertType alertType, [NotNullWhen(true)] out AlertPrototype? alert) + public bool TryGet(ProtoId alertType, [NotNullWhen(true)] out AlertPrototype? alert) { return _typeToAlert.TryGetValue(alertType, out alert); } diff --git a/Content.Shared/Alert/ClickAlertEvent.cs b/Content.Shared/Alert/ClickAlertEvent.cs index fe7ca97e4c..43dd086b56 100644 --- a/Content.Shared/Alert/ClickAlertEvent.cs +++ b/Content.Shared/Alert/ClickAlertEvent.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Serialization; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; namespace Content.Shared.Alert; @@ -8,9 +9,9 @@ namespace Content.Shared.Alert; [Serializable, NetSerializable] public sealed class ClickAlertEvent : EntityEventArgs { - public readonly AlertType Type; + public readonly ProtoId Type; - public ClickAlertEvent(AlertType alertType) + public ClickAlertEvent(ProtoId alertType) { Type = alertType; } diff --git a/Content.Shared/Atmos/Components/GasTileOverlayComponent.cs b/Content.Shared/Atmos/Components/GasTileOverlayComponent.cs index e72a1d6758..2c3149b11a 100644 --- a/Content.Shared/Atmos/Components/GasTileOverlayComponent.cs +++ b/Content.Shared/Atmos/Components/GasTileOverlayComponent.cs @@ -1,7 +1,6 @@ using Robust.Shared.GameStates; using Robust.Shared.Serialization; using Robust.Shared.Timing; -using Robust.Shared.Utility; namespace Content.Shared.Atmos.Components; @@ -24,55 +23,47 @@ public sealed partial class GasTileOverlayComponent : Component public GameTick ForceTick { get; set; } } +[Serializable, NetSerializable] +public sealed class GasTileOverlayState(Dictionary chunks) : ComponentState +{ + public readonly Dictionary Chunks = chunks; +} [Serializable, NetSerializable] -public sealed class GasTileOverlayState : ComponentState, IComponentDeltaState +public sealed class GasTileOverlayDeltaState( + Dictionary modifiedChunks, + HashSet allChunks) + : ComponentState, IComponentDeltaState { - public readonly Dictionary Chunks; - public bool FullState => AllChunks == null; + public readonly Dictionary ModifiedChunks = modifiedChunks; + public readonly HashSet AllChunks = allChunks; - // required to infer deleted/missing chunks for delta states - public HashSet? AllChunks; - - public GasTileOverlayState(Dictionary chunks) + public void ApplyToFullState(GasTileOverlayState state) { - Chunks = chunks; - } - - public void ApplyToFullState(IComponentState fullState) - { - DebugTools.Assert(!FullState); - var state = (GasTileOverlayState) fullState; - DebugTools.Assert(state.FullState); - foreach (var key in state.Chunks.Keys) { - if (!AllChunks!.Contains(key)) + if (!AllChunks.Contains(key)) state.Chunks.Remove(key); } - foreach (var (chunk, data) in Chunks) + foreach (var (chunk, data) in ModifiedChunks) { state.Chunks[chunk] = new(data); } } - public IComponentState CreateNewFullState(IComponentState fullState) + public GasTileOverlayState CreateNewFullState(GasTileOverlayState state) { - DebugTools.Assert(!FullState); - var state = (GasTileOverlayState) fullState; - DebugTools.Assert(state.FullState); + var chunks = new Dictionary(AllChunks.Count); - var chunks = new Dictionary(state.Chunks.Count); - - foreach (var (chunk, data) in Chunks) + foreach (var (chunk, data) in ModifiedChunks) { chunks[chunk] = new(data); } foreach (var (chunk, data) in state.Chunks) { - if (AllChunks!.Contains(chunk)) + if (AllChunks.Contains(chunk)) chunks.TryAdd(chunk, new(data)); } diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs index f468724db3..8e7dfdedaf 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs @@ -55,7 +55,7 @@ namespace Content.Shared.Atmos.EntitySystems data[index] = chunk; } - args.State = new GasTileOverlayState(data) { AllChunks = new(component.Chunks.Keys) }; + args.State = new GasTileOverlayDeltaState(data, new(component.Chunks.Keys)); } public static Vector2i GetGasChunkIndices(Vector2i indices) diff --git a/Content.Shared/Buckle/Components/StrapComponent.cs b/Content.Shared/Buckle/Components/StrapComponent.cs index 72c92ebf84..9a19cea0c9 100644 --- a/Content.Shared/Buckle/Components/StrapComponent.cs +++ b/Content.Shared/Buckle/Components/StrapComponent.cs @@ -3,6 +3,7 @@ using Content.Shared.Alert; using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Buckle.Components; @@ -115,7 +116,7 @@ public sealed partial class StrapComponent : Component /// [DataField] [ViewVariables(VVAccess.ReadWrite)] - public AlertType BuckledAlertType = AlertType.Buckled; + public ProtoId BuckledAlertType = "Buckled"; /// /// The sum of the sizes of all the buckled entities in this strap diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index 0d67473ffe..00040211e3 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -40,6 +40,9 @@ public abstract partial class SharedBuckleSystem SubscribeLocalEvent(OnBuckleUpdateCanMove); } + [ValidatePrototypeId] + public const string BuckledAlertCategory = "Buckled"; + private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) { UpdateBuckleStatus(uid, component); @@ -61,7 +64,7 @@ public abstract partial class SharedBuckleSystem return; var strapPosition = Transform(strapUid).Coordinates; - if (ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance)) + if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance)) return; TryUnbuckle(uid, uid, true, component); @@ -165,7 +168,7 @@ public abstract partial class SharedBuckleSystem } else { - _alerts.ClearAlertCategory(uid, AlertCategory.Buckled); + _alerts.ClearAlertCategory(uid, BuckledAlertCategory); } } diff --git a/Content.Shared/Clothing/MagbootsComponent.cs b/Content.Shared/Clothing/MagbootsComponent.cs index 0d0d59f89f..0d074ff38b 100644 --- a/Content.Shared/Clothing/MagbootsComponent.cs +++ b/Content.Shared/Clothing/MagbootsComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Alert; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -16,4 +17,7 @@ public sealed partial class MagbootsComponent : Component [DataField("on"), AutoNetworkedField] public bool On; + + [DataField] + public ProtoId MagbootsAlert = "Magboots"; } diff --git a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs index 6d94c087af..a927e1a697 100644 --- a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs +++ b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs @@ -7,7 +7,6 @@ using Content.Shared.Interaction.Events; using Content.Shared.Popups; using Content.Shared.Throwing; using Content.Shared.Weapons.Ranged.Events; -using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Timing; namespace Content.Shared.CombatMode.Pacification; @@ -109,7 +108,7 @@ public sealed class PacificationSystem : EntitySystem _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false); } - _alertsSystem.ShowAlert(uid, AlertType.Pacified); + _alertsSystem.ShowAlert(uid, component.PacifiedAlert); } private void OnShutdown(EntityUid uid, PacifiedComponent component, ComponentShutdown args) @@ -121,7 +120,7 @@ public sealed class PacificationSystem : EntitySystem _combatSystem.SetCanDisarm(uid, true, combatMode); _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, true); - _alertsSystem.ClearAlert(uid, AlertType.Pacified); + _alertsSystem.ClearAlert(uid, component.PacifiedAlert); } private void OnBeforeThrow(Entity ent, ref BeforeThrowEvent args) diff --git a/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs b/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs index 464ef77885..96081e5dc6 100644 --- a/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs +++ b/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs @@ -1,4 +1,6 @@ +using Content.Shared.Alert; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; namespace Content.Shared.CombatMode.Pacification; @@ -42,4 +44,6 @@ public sealed partial class PacifiedComponent : Component [DataField] public EntityUid? LastAttackedEntity = null; + [DataField] + public ProtoId PacifiedAlert = "Pacified"; } diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index 914b34d3c1..2e3f9ed461 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -197,6 +197,7 @@ namespace Content.Shared.Containers.ItemSlots if (!EntityManager.TryGetComponent(args.User, out HandsComponent? hands)) return; + var slots = new List(); foreach (var slot in itemSlots.Slots.Values) { if (!slot.InsertOnInteract) @@ -205,10 +206,20 @@ namespace Content.Shared.Containers.ItemSlots if (!CanInsert(uid, args.Used, args.User, slot, swap: slot.Swap, popup: args.User)) continue; - // Drop the held item onto the floor. Return if the user cannot drop. - if (!_handsSystem.TryDrop(args.User, args.Used, handsComp: hands)) - return; + slots.Add(slot); + } + if (slots.Count == 0) + return; + + // Drop the held item onto the floor. Return if the user cannot drop. + if (!_handsSystem.TryDrop(args.User, args.Used, handsComp: hands)) + return; + + slots.Sort(SortEmpty); + + foreach (var slot in slots) + { if (slot.Item != null) _handsSystem.TryPickupAnyHand(args.User, slot.Item.Value, handsComp: hands); @@ -333,6 +344,65 @@ namespace Content.Shared.Containers.ItemSlots Insert(uid, slot, held, user, excludeUserAudio: excludeUserAudio); return true; } + + /// + /// Tries to insert an item into any empty slot. + /// + /// The entity that has the item slots. + /// The item to be inserted. + /// The entity performing the interaction. + /// + /// If true, will exclude the user when playing sound. Does nothing client-side. + /// Useful for predicted interactions + /// + /// False if failed to insert item + public bool TryInsertEmpty(Entity ent, EntityUid item, EntityUid? user, bool excludeUserAudio = false) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + var slots = new List(); + foreach (var slot in ent.Comp.Slots.Values) + { + if (slot.ContainerSlot?.ContainedEntity != null) + continue; + + if (CanInsert(ent, item, user, slot)) + slots.Add(slot); + } + + if (slots.Count == 0) + return false; + + if (user != null && _handsSystem.IsHolding(user.Value, item)) + { + if (!_handsSystem.TryDrop(user.Value, item)) + return false; + } + + slots.Sort(SortEmpty); + + foreach (var slot in slots) + { + if (TryInsert(ent, slot, item, user, excludeUserAudio: excludeUserAudio)) + return true; + } + + return false; + } + + private static int SortEmpty(ItemSlot a, ItemSlot b) + { + var aEnt = a.ContainerSlot?.ContainedEntity; + var bEnt = b.ContainerSlot?.ContainedEntity; + if (aEnt == null && bEnt == null) + return a.Priority.CompareTo(b.Priority); + + if (aEnt == null) + return -1; + + return 1; + } #endregion #region Eject diff --git a/Content.Shared/Cuffs/Components/CuffableComponent.cs b/Content.Shared/Cuffs/Components/CuffableComponent.cs index 5da6fa41a5..4ddfe1b53e 100644 --- a/Content.Shared/Cuffs/Components/CuffableComponent.cs +++ b/Content.Shared/Cuffs/Components/CuffableComponent.cs @@ -1,6 +1,8 @@ +using Content.Shared.Alert; using Content.Shared.Damage; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; @@ -39,6 +41,9 @@ public sealed partial class CuffableComponent : Component /// [DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)] public bool CanStillInteract = true; + + [DataField] + public ProtoId CuffedAlert = "Handcuffed"; } [Serializable, NetSerializable] diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index 0077f5a358..f0f9a94983 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -172,9 +172,9 @@ namespace Content.Shared.Cuffs _actionBlocker.UpdateCanMove(uid); if (component.CanStillInteract) - _alerts.ClearAlert(uid, AlertType.Handcuffed); + _alerts.ClearAlert(uid, component.CuffedAlert); else - _alerts.ShowAlert(uid, AlertType.Handcuffed); + _alerts.ShowAlert(uid, component.CuffedAlert); var ev = new CuffedStateChangeEvent(); RaiseLocalEvent(uid, ref ev); diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index 5a2fba4970..14c3f6d9f5 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -1,4 +1,6 @@ +using Content.Shared.Alert; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Damage.Components; @@ -51,4 +53,7 @@ public sealed partial class StaminaComponent : Component [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField] [AutoPausedField] public TimeSpan NextUpdate = TimeSpan.Zero; + + [DataField] + public ProtoId StaminaAlert = "Stamina"; } diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs index 840b2e0431..1f9a7f1dd8 100644 --- a/Content.Shared/Damage/Systems/StaminaSystem.cs +++ b/Content.Shared/Damage/Systems/StaminaSystem.cs @@ -79,8 +79,7 @@ public sealed partial class StaminaSystem : EntitySystem { RemCompDeferred(uid); } - - SetStaminaAlert(uid); + _alerts.ClearAlert(uid, component.StaminaAlert); } private void OnStartup(EntityUid uid, StaminaComponent component, ComponentStartup args) @@ -204,13 +203,10 @@ public sealed partial class StaminaSystem : EntitySystem private void SetStaminaAlert(EntityUid uid, StaminaComponent? component = null) { if (!Resolve(uid, ref component, false) || component.Deleted) - { - _alerts.ClearAlert(uid, AlertType.Stamina); return; - } var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, component.CritThreshold - component.StaminaDamage), component.CritThreshold, 7); - _alerts.ShowAlert(uid, AlertType.Stamina, (short) severity); + _alerts.ShowAlert(uid, component.StaminaAlert, (short) severity); } /// diff --git a/Content.Shared/Decals/DecalGridComponent.cs b/Content.Shared/Decals/DecalGridComponent.cs index 8ac05cb280..67a9c03769 100644 --- a/Content.Shared/Decals/DecalGridComponent.cs +++ b/Content.Shared/Decals/DecalGridComponent.cs @@ -62,46 +62,37 @@ namespace Content.Shared.Decals } [Serializable, NetSerializable] - public sealed class DecalGridState : ComponentState, IComponentDeltaState + public sealed class DecalGridState(Dictionary chunks) : ComponentState { - public Dictionary Chunks; - public bool FullState => AllChunks == null; + public Dictionary Chunks = chunks; + } - // required to infer deleted/missing chunks for delta states - public HashSet? AllChunks; + [Serializable, NetSerializable] + public sealed class DecalGridDeltaState(Dictionary modifiedChunks, HashSet allChunks) + : ComponentState, IComponentDeltaState + { + public Dictionary ModifiedChunks = modifiedChunks; + public HashSet AllChunks = allChunks; - public DecalGridState(Dictionary chunks) + public void ApplyToFullState(DecalGridState state) { - Chunks = chunks; - } - - public void ApplyToFullState(IComponentState fullState) - { - DebugTools.Assert(!FullState); - var state = (DecalGridState) fullState; - DebugTools.Assert(state.FullState); - foreach (var key in state.Chunks.Keys) { if (!AllChunks!.Contains(key)) state.Chunks.Remove(key); } - foreach (var (chunk, data) in Chunks) + foreach (var (chunk, data) in ModifiedChunks) { state.Chunks[chunk] = new(data); } } - public IComponentState CreateNewFullState(IComponentState fullState) + public DecalGridState CreateNewFullState(DecalGridState state) { - DebugTools.Assert(!FullState); - var state = (DecalGridState) fullState; - DebugTools.Assert(state.FullState); - var chunks = new Dictionary(state.Chunks.Count); - foreach (var (chunk, data) in Chunks) + foreach (var (chunk, data) in ModifiedChunks) { chunks[chunk] = new(data); } diff --git a/Content.Shared/Decals/SharedDecalSystem.cs b/Content.Shared/Decals/SharedDecalSystem.cs index 0665ccbf84..0a2349ea29 100644 --- a/Content.Shared/Decals/SharedDecalSystem.cs +++ b/Content.Shared/Decals/SharedDecalSystem.cs @@ -49,7 +49,7 @@ namespace Content.Shared.Decals data[index] = chunk; } - args.State = new DecalGridState(data) { AllChunks = new(component.ChunkCollection.ChunkCollection.Keys) }; + args.State = new DecalGridDeltaState(data, new(component.ChunkCollection.ChunkCollection.Keys)); } private void OnGridInitialize(GridInitializeEvent msg) diff --git a/Content.Shared/Doors/Components/FirelockComponent.cs b/Content.Shared/Doors/Components/FirelockComponent.cs index 97e57185ca..3f7d6c3f70 100644 --- a/Content.Shared/Doors/Components/FirelockComponent.cs +++ b/Content.Shared/Doors/Components/FirelockComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Doors.Components; +using Robust.Shared.GameStates; namespace Content.Shared.Doors.Components { @@ -7,9 +7,11 @@ namespace Content.Shared.Doors.Components /// auto-closing on depressurization, air/fire alarm interactions, and preventing normal door functions when /// retaining pressure.. /// - [RegisterComponent] + [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class FirelockComponent : Component { + #region Settings + /// /// Pry time modifier to be used when the firelock is currently closed due to fire or pressure. /// @@ -17,8 +19,6 @@ namespace Content.Shared.Doors.Components [DataField("lockedPryTimeModifier"), ViewVariables(VVAccess.ReadWrite)] public float LockedPryTimeModifier = 1.5f; - [DataField("autocloseDelay")] public TimeSpan AutocloseDelay = TimeSpan.FromSeconds(3f); - /// /// Maximum pressure difference before the firelock will refuse to open, in kPa. /// @@ -39,5 +39,47 @@ namespace Content.Shared.Doors.Components /// [DataField("alarmAutoClose"), ViewVariables(VVAccess.ReadWrite)] public bool AlarmAutoClose = true; + + /// + /// The cooldown duration before a firelock can automatically close due to a hazardous environment after it has + /// been pried open. Measured in seconds. + /// + [DataField] + public TimeSpan EmergencyCloseCooldownDuration = TimeSpan.FromSeconds(2); + + #endregion + + #region Set by system + + /// + /// When the firelock will be allowed to automatically close again due to a hazardous environment. + /// + [DataField] + public TimeSpan? EmergencyCloseCooldown; + + /// + /// Whether the firelock can open, or is locked due to its environment. + /// + public bool IsLocked => Pressure || Temperature; + + /// + /// Whether the firelock is holding back a hazardous pressure. + /// + [DataField, AutoNetworkedField] + public bool Pressure; + + /// + /// Whether the firelock is holding back extreme temperatures. + /// + [DataField, AutoNetworkedField] + public bool Temperature; + + /// + /// Whether the airlock is powered. + /// + [DataField, AutoNetworkedField] + public bool Powered; + + #endregion } } diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index b58b7b265e..20456c1477 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -6,7 +6,6 @@ using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.Doors.Components; using Content.Shared.Emag.Systems; -using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Physics; using Content.Shared.Popups; @@ -466,7 +465,7 @@ public abstract partial class SharedDoorSystem : EntitySystem door.Partial = true; - // Make sure no entity waled into the airlock when it started closing. + // Make sure no entity walked into the airlock when it started closing. if (!CanClose(uid, door)) { door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo; diff --git a/Content.Shared/Doors/Systems/SharedFirelockSystem.cs b/Content.Shared/Doors/Systems/SharedFirelockSystem.cs new file mode 100644 index 0000000000..47a29a4ba8 --- /dev/null +++ b/Content.Shared/Doors/Systems/SharedFirelockSystem.cs @@ -0,0 +1,110 @@ +using Content.Shared.Access.Systems; +using Content.Shared.Doors.Components; +using Content.Shared.Popups; +using Content.Shared.Prying.Components; +using Robust.Shared.Timing; + +namespace Content.Shared.Doors.Systems; + +public abstract class SharedFirelockSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedDoorSystem _doorSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() + { + base.Initialize(); + + // Access/Prying + SubscribeLocalEvent(OnBeforeDoorOpened); + SubscribeLocalEvent(OnDoorGetPryTimeModifier); + SubscribeLocalEvent(OnAfterPried); + + // Visuals + SubscribeLocalEvent(UpdateVisuals); + SubscribeLocalEvent(UpdateVisuals); + } + + public bool EmergencyPressureStop(EntityUid uid, FirelockComponent? firelock = null, DoorComponent? door = null) + { + if (!Resolve(uid, ref firelock, ref door)) + return false; + + if (door.State != DoorState.Open + || firelock.EmergencyCloseCooldown != null + && _gameTiming.CurTime < firelock.EmergencyCloseCooldown) + return false; + + if (!_doorSystem.TryClose(uid, door)) + return false; + + return _doorSystem.OnPartialClose(uid, door); + } + + #region Access/Prying + + private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args) + { + // Give the Door remote the ability to force a firelock open even if it is holding back dangerous gas + var overrideAccess = (args.User != null) && _accessReaderSystem.IsAllowed(args.User.Value, uid); + + if (!component.Powered || (!overrideAccess && component.IsLocked)) + args.Cancel(); + } + + private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args) + { + if (component.Temperature) + { + _popupSystem.PopupClient(Loc.GetString("firelock-component-is-holding-fire-message"), + uid, args.User, PopupType.MediumCaution); + } + else if (component.Pressure) + { + _popupSystem.PopupClient(Loc.GetString("firelock-component-is-holding-pressure-message"), + uid, args.User, PopupType.MediumCaution); + } + + if (component.IsLocked) + args.PryTimeModifier *= component.LockedPryTimeModifier; + } + + private void OnAfterPried(EntityUid uid, FirelockComponent component, ref PriedEvent args) + { + component.EmergencyCloseCooldown = _gameTiming.CurTime + component.EmergencyCloseCooldownDuration; + } + + #endregion + + #region Visuals + + private void UpdateVisuals(EntityUid uid, FirelockComponent component, EntityEventArgs args) => UpdateVisuals(uid, component); + + private void UpdateVisuals(EntityUid uid, + FirelockComponent? firelock = null, + DoorComponent? door = null, + AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref door, ref appearance, false)) + return; + + // only bother to check pressure on doors that are some variation of closed. + if (door.State != DoorState.Closed + && door.State != DoorState.Welded + && door.State != DoorState.Denying) + { + _appearance.SetData(uid, DoorVisuals.ClosedLights, false, appearance); + return; + } + + if (!Resolve(uid, ref firelock, ref appearance, false)) + return; + + _appearance.SetData(uid, DoorVisuals.ClosedLights, firelock.IsLocked, appearance); + } + + #endregion +} diff --git a/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs b/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs index 553f6df1c7..2536fac4ed 100644 --- a/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs +++ b/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs @@ -1,5 +1,7 @@ +using Content.Shared.Alert; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Ensnaring.Components; @@ -40,6 +42,9 @@ public sealed partial class EnsnareableComponent : Component [DataField("state")] public string? State; + + [DataField] + public ProtoId EnsnaredAlert = "Ensnared"; } [Serializable, NetSerializable] diff --git a/Content.Shared/Gravity/SharedGravitySystem.cs b/Content.Shared/Gravity/SharedGravitySystem.cs index 100d2ee74f..df13be51fd 100644 --- a/Content.Shared/Gravity/SharedGravitySystem.cs +++ b/Content.Shared/Gravity/SharedGravitySystem.cs @@ -17,6 +17,9 @@ namespace Content.Shared.Gravity [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly InventorySystem _inventory = default!; + [ValidatePrototypeId] + public const string WeightlessAlert = "Weightless"; + public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, TransformComponent? xform = null) { Resolve(uid, ref body, false); @@ -93,11 +96,11 @@ namespace Content.Shared.Gravity if (!ev.HasGravity) { - _alerts.ShowAlert(uid, AlertType.Weightless); + _alerts.ShowAlert(uid, WeightlessAlert); } else { - _alerts.ClearAlert(uid, AlertType.Weightless); + _alerts.ClearAlert(uid, WeightlessAlert); } } } @@ -106,11 +109,11 @@ namespace Content.Shared.Gravity { if (IsWeightless(ev.Euid)) { - _alerts.ShowAlert(ev.Euid, AlertType.Weightless); + _alerts.ShowAlert(ev.Euid, WeightlessAlert); } else { - _alerts.ClearAlert(ev.Euid, AlertType.Weightless); + _alerts.ClearAlert(ev.Euid, WeightlessAlert); } } @@ -118,11 +121,11 @@ namespace Content.Shared.Gravity { if (IsWeightless(uid)) { - _alerts.ShowAlert(uid, AlertType.Weightless); + _alerts.ShowAlert(uid, WeightlessAlert); } else { - _alerts.ClearAlert(uid, AlertType.Weightless); + _alerts.ClearAlert(uid, WeightlessAlert); } } diff --git a/Content.Shared/Interaction/Components/BypassInteractionChecksComponent.cs b/Content.Shared/Interaction/Components/BypassInteractionChecksComponent.cs new file mode 100644 index 0000000000..ca0ff96315 --- /dev/null +++ b/Content.Shared/Interaction/Components/BypassInteractionChecksComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Interaction.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BypassInteractionChecksComponent : Component; diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index c82a749755..3324ce5b9b 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -1,11 +1,10 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; -using Content.Shared.Administration; using Content.Shared.Administration.Logs; -using Content.Shared.Administration.Managers; using Content.Shared.CombatMode; using Content.Shared.Database; +using Content.Shared.Ghost; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Input; @@ -15,12 +14,13 @@ using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Movement.Components; -using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Physics; using Content.Shared.Popups; +using Content.Shared.Storage; using Content.Shared.Tag; using Content.Shared.Timing; +using Content.Shared.UserInterface; using Content.Shared.Verbs; using Content.Shared.Wall; using JetBrains.Annotations; @@ -36,6 +36,7 @@ using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Timing; +using Robust.Shared.Utility; #pragma warning disable 618 @@ -50,12 +51,11 @@ namespace Content.Shared.Interaction [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly ISharedAdminManager _adminManager = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly SharedPhysicsSystem _sharedBroadphaseSystem = default!; + [Dependency] private readonly SharedPhysicsSystem _broadphase = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedVerbSystem _verbSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; @@ -64,6 +64,18 @@ namespace Content.Shared.Interaction [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + + private EntityQuery _ignoreUiRangeQuery; + private EntityQuery _fixtureQuery; + private EntityQuery _itemQuery; + private EntityQuery _physicsQuery; + private EntityQuery _handsQuery; + private EntityQuery _relayQuery; + private EntityQuery _combatQuery; + private EntityQuery _wallMountQuery; + private EntityQuery _delayQuery; + private EntityQuery _uiQuery; private const CollisionGroup InRangeUnobstructedMask = CollisionGroup.Impassable | CollisionGroup.InteractImpassable; @@ -76,6 +88,17 @@ namespace Content.Shared.Interaction public override void Initialize() { + _ignoreUiRangeQuery = GetEntityQuery(); + _fixtureQuery = GetEntityQuery(); + _itemQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); + _handsQuery = GetEntityQuery(); + _relayQuery = GetEntityQuery(); + _combatQuery = GetEntityQuery(); + _wallMountQuery = GetEntityQuery(); + _delayQuery = GetEntityQuery(); + _uiQuery = GetEntityQuery(); + SubscribeLocalEvent(HandleUserInterfaceRangeCheck); SubscribeLocalEvent(OnBoundInterfaceInteractAttempt); @@ -111,29 +134,57 @@ namespace Content.Shared.Interaction /// private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev) { - var user = ev.Actor; + _uiQuery.TryComp(ev.Target, out var uiComp); + if (!_actionBlockerSystem.CanInteract(ev.Actor, ev.Target)) + { + // We permit ghosts to open uis unless explicitly blocked + if (ev.Message is not OpenBoundInterfaceMessage || !HasComp(ev.Actor) || uiComp?.BlockSpectators == true) + { + ev.Cancel(); + return; + } + } - if (!_actionBlockerSystem.CanInteract(user, ev.Target)) + var range = _ui.GetUiRange(ev.Target, ev.UiKey); + + // As long as range>0, the UI frame updates should have auto-closed the UI if it is out of range. + DebugTools.Assert(range <= 0 || UiRangeCheck(ev.Actor, ev.Target, range)); + + if (range <= 0 && !IsAccessible(ev.Actor, ev.Target)) { ev.Cancel(); return; } - // Check if the bound entity is accessible. Note that we allow admins to ignore this restriction, so that - // they can fiddle with UI's that people can't normally interact with (e.g., placing things directly into - // other people's backpacks). - if (!_containerSystem.IsInSameOrParentContainer(user, ev.Target) - && !CanAccessViaStorage(user, ev.Target) - && !_adminManager.HasAdminFlag(user, AdminFlags.Admin)) + if (uiComp == null) + return; + + if (uiComp.SingleUser && uiComp.CurrentSingleUser != ev.Actor) { ev.Cancel(); return; } - if (!InRangeUnobstructed(user, ev.Target)) - { + if (!uiComp.RequireHands) + return; + + if (!_handsQuery.TryComp(ev.Actor, out var hands) || hands.Hands.Count == 0) ev.Cancel(); - } + } + + private bool UiRangeCheck(Entity user, Entity target, float range) + { + if (!Resolve(target, ref target.Comp)) + return false; + + if (user.Owner == target.Owner) + return true; + + // Fast check: if the user is the parent of the entity (e.g., holding it), we always assume that it is in range + if (target.Comp.ParentUid == user.Owner) + return true; + + return InRangeAndAccessible(user, target, range) || _ignoreUiRangeQuery.HasComp(user); } /// @@ -190,10 +241,7 @@ namespace Content.Shared.Interaction if (!InRangeUnobstructed(userEntity.Value, uid, popup: true)) return false; - if (!TryComp(uid, out PullableComponent? pull)) - return false; - - _pullSystem.TogglePull(uid, userEntity.Value, pull); + _pullSystem.TogglePull(uid, userEntity.Value); return false; } @@ -269,7 +317,7 @@ namespace Content.Shared.Interaction public bool CombatModeCanHandInteract(EntityUid user, EntityUid? target) { // Always allow attack in these cases - if (target == null || !TryComp(user, out var hands) || hands.ActiveHand?.HeldEntity is not null) + if (target == null || !_handsQuery.TryComp(user, out var hands) || hands.ActiveHand?.HeldEntity is not null) return false; // Only eat input if: @@ -277,7 +325,7 @@ namespace Content.Shared.Interaction // - Target doesn't cancel should-interact event // This is intended to allow items to be picked up in combat mode, // but to also allow items to force attacks anyway (like mobs which are items, e.g. mice) - if (!HasComp(target)) + if (!_itemQuery.HasComp(target)) return false; var combatEv = new CombatModeShouldHandInteractEvent(); @@ -307,7 +355,7 @@ namespace Content.Shared.Interaction bool checkAccess = true, bool checkCanUse = true) { - if (TryComp(user, out var relay) && relay.RelayEntity is not null) + if (_relayQuery.TryComp(user, out var relay) && relay.RelayEntity is not null) { // TODO this needs to be handled better. This probably bypasses many complex can-interact checks in weird roundabout ways. if (_actionBlockerSystem.CanInteract(user, target)) @@ -321,7 +369,7 @@ namespace Content.Shared.Interaction if (target != null && Deleted(target.Value)) return; - if (!altInteract && TryComp(user, out var combatMode) && combatMode.IsInCombatMode) + if (!altInteract && _combatQuery.TryComp(user, out var combatMode) && combatMode.IsInCombatMode) { if (!CombatModeCanHandInteract(user, target)) return; @@ -343,10 +391,7 @@ namespace Content.Shared.Interaction // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // Also checks if the item is accessible via some storage UI (e.g., open backpack) - if (checkAccess - && target != null - && !_containerSystem.IsInSameOrParentContainer(user, target.Value) - && !CanAccessViaStorage(user, target.Value)) + if (checkAccess && target != null && !IsAccessible(user, target.Value)) return; var inRangeUnobstructed = target == null @@ -354,7 +399,7 @@ namespace Content.Shared.Interaction : !checkAccess || InRangeUnobstructed(user, target.Value); // permits interactions with wall mounted entities // Does the user have hands? - if (!TryComp(user, out var hands) || hands.ActiveHand == null) + if (!_handsQuery.TryComp(user, out var hands) || hands.ActiveHand == null) { var ev = new InteractNoHandEvent(user, target, coordinates); RaiseLocalEvent(user, ev); @@ -494,7 +539,7 @@ namespace Content.Shared.Interaction predicate ??= _ => false; var ray = new CollisionRay(origin.Position, dir.Normalized(), collisionMask); - var rayResults = _sharedBroadphaseSystem.IntersectRayWithPredicate(origin.MapId, ray, dir.Length(), predicate.Invoke, false).ToList(); + var rayResults = _broadphase.IntersectRayWithPredicate(origin.MapId, ray, dir.Length(), predicate.Invoke, false).ToList(); if (rayResults.Count == 0) return dir.Length(); @@ -557,23 +602,29 @@ namespace Content.Shared.Interaction } var ray = new CollisionRay(origin.Position, dir.Normalized(), (int) collisionMask); - var rayResults = _sharedBroadphaseSystem.IntersectRayWithPredicate(origin.MapId, ray, length, predicate.Invoke, false).ToList(); + var rayResults = _broadphase.IntersectRayWithPredicate(origin.MapId, ray, length, predicate.Invoke, false).ToList(); return rayResults.Count == 0; } public bool InRangeUnobstructed( - EntityUid origin, - EntityUid other, + Entity origin, + Entity other, float range = InteractionRange, CollisionGroup collisionMask = InRangeUnobstructedMask, Ignored? predicate = null, bool popup = false) { - if (!TryComp(other, out TransformComponent? otherXform)) + if (!Resolve(other, ref other.Comp)) return false; - return InRangeUnobstructed(origin, other, otherXform.Coordinates, otherXform.LocalRotation, range, collisionMask, predicate, + return InRangeUnobstructed(origin, + other, + other.Comp.Coordinates, + other.Comp.LocalRotation, + range, + collisionMask, + predicate, popup); } @@ -605,8 +656,8 @@ namespace Content.Shared.Interaction /// True if the two points are within a given range without being obstructed. /// public bool InRangeUnobstructed( - EntityUid origin, - EntityUid other, + Entity origin, + Entity other, EntityCoordinates otherCoordinates, Angle otherAngle, float range = InteractionRange, @@ -614,10 +665,10 @@ namespace Content.Shared.Interaction Ignored? predicate = null, bool popup = false) { - Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false); + Ignored combinedPredicate = e => e == origin.Owner || (predicate?.Invoke(e) ?? false); var inRange = true; MapCoordinates originPos = default; - var targetPos = otherCoordinates.ToMap(EntityManager, _transform); + var targetPos = _transform.ToMapCoordinates(otherCoordinates); Angle targetRot = default; // So essentially: @@ -627,23 +678,30 @@ namespace Content.Shared.Interaction // Alternatively we could check centre distances first though // that means we wouldn't be able to easily check overlap interactions. if (range > 0f && - TryComp(origin, out var fixtureA) && + _fixtureQuery.TryComp(origin, out var fixtureA) && // These fixture counts are stuff that has the component but no fixtures for (e.g. buttons). // At least until they get removed. fixtureA.FixtureCount > 0 && - TryComp(other, out var fixtureB) && + _fixtureQuery.TryComp(other, out var fixtureB) && fixtureB.FixtureCount > 0 && - TryComp(origin, out TransformComponent? xformA)) + Resolve(origin, ref origin.Comp)) { - var (worldPosA, worldRotA) = xformA.GetWorldPositionRotation(); + var (worldPosA, worldRotA) = origin.Comp.GetWorldPositionRotation(); var xfA = new Transform(worldPosA, worldRotA); var parentRotB = _transform.GetWorldRotation(otherCoordinates.EntityId); var xfB = new Transform(targetPos.Position, parentRotB + otherAngle); // Different map or the likes. - if (!_sharedBroadphaseSystem.TryGetNearest(origin, other, - out _, out _, out var distance, - xfA, xfB, fixtureA, fixtureB)) + if (!_broadphase.TryGetNearest( + origin, + other, + out _, + out _, + out var distance, + xfA, + xfB, + fixtureA, + fixtureB)) { inRange = false; } @@ -665,15 +723,15 @@ namespace Content.Shared.Interaction else { // We'll still do the raycast from the centres but we'll bump the range as we know they're in range. - originPos = _transform.GetMapCoordinates(origin, xform: xformA); + originPos = _transform.GetMapCoordinates(origin, xform: origin.Comp); range = (originPos.Position - targetPos.Position).Length(); } } // No fixtures, e.g. wallmounts. else { - originPos = _transform.GetMapCoordinates(origin); - var otherParent = Transform(other).ParentUid; + originPos = _transform.GetMapCoordinates(origin, origin); + var otherParent = (other.Comp ?? Transform(other)).ParentUid; targetRot = otherParent.IsValid() ? Transform(otherParent).LocalRotation + otherAngle : otherAngle; } @@ -724,13 +782,13 @@ namespace Content.Shared.Interaction { HashSet ignored = new(); - if (HasComp(target) && TryComp(target, out PhysicsComponent? physics) && physics.CanCollide) + if (_itemQuery.HasComp(target) && _physicsQuery.TryComp(target, out var physics) && physics.CanCollide) { // If the target is an item, we ignore any colliding entities. Currently done so that if items get stuck // inside of walls, users can still pick them up. - ignored.UnionWith(_sharedBroadphaseSystem.GetEntitiesIntersectingBody(target, (int) collisionMask, false, physics)); + ignored.UnionWith(_broadphase.GetEntitiesIntersectingBody(target, (int) collisionMask, false, physics)); } - else if (TryComp(target, out WallMountComponent? wallMount)) + else if (_wallMountQuery.TryComp(target, out var wallMount)) { // wall-mount exemptions may be restricted to a specific angle range.da @@ -748,13 +806,7 @@ namespace Content.Shared.Interaction ignored.UnionWith(grid.GetAnchoredEntities(targetCoords)); } - Ignored combinedPredicate = e => - { - return e == target - || (predicate?.Invoke(e) ?? false) - || ignored.Contains(e); - }; - + Ignored combinedPredicate = e => e == target || (predicate?.Invoke(e) ?? false) || ignored.Contains(e); return combinedPredicate; } @@ -951,10 +1003,8 @@ namespace Content.Shared.Interaction bool checkUseDelay = true, bool checkAccess = true) { - UseDelayComponent? delayComponent = null; - if (checkUseDelay - && TryComp(used, out delayComponent) - && _useDelay.IsDelayed((used, delayComponent))) + _delayQuery.TryComp(used, out var delayComponent); + if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent))) return false; if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, used)) @@ -965,11 +1015,11 @@ namespace Content.Shared.Interaction // Check if interacted entity is in the same container, the direct child, or direct parent of the user. // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) - if (checkAccess && !_containerSystem.IsInSameOrParentContainer(user, used) && !CanAccessViaStorage(user, used)) + if (checkAccess && !IsAccessible(user, used)) return false; // Does the user have hands? - if (!HasComp(user)) + if (!_handsQuery.HasComp(user)) return false; var activateMsg = new ActivateInWorldEvent(user, used); @@ -979,7 +1029,9 @@ namespace Content.Shared.Interaction DoContactInteraction(user, used, activateMsg); // Still need to call this even without checkUseDelay in case this gets relayed from Activate. - _useDelay.TryResetDelay(used, component: delayComponent); + if (delayComponent != null) + _useDelay.TryResetDelay(used, component: delayComponent); + if (!activateMsg.WasLogged) _adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}"); return true; @@ -1000,11 +1052,8 @@ namespace Content.Shared.Interaction bool checkCanInteract = true, bool checkUseDelay = true) { - UseDelayComponent? delayComponent = null; - - if (checkUseDelay - && TryComp(used, out delayComponent) - && _useDelay.IsDelayed((used, delayComponent))) + _delayQuery.TryComp(used, out var delayComponent); + if (checkUseDelay && delayComponent != null && _useDelay.IsDelayed((used, delayComponent))) return true; // if the item is on cooldown, we consider this handled. if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, used)) @@ -1066,11 +1115,60 @@ namespace Content.Shared.Interaction } #endregion + /// + /// Check if a user can access a target (stored in the same containers) and is in range without obstructions. + /// + public bool InRangeAndAccessible( + Entity user, + Entity target, + float range = InteractionRange, + CollisionGroup collisionMask = InRangeUnobstructedMask, + Ignored? predicate = null) + { + if (user == target) + return true; + + if (!Resolve(user, ref user.Comp)) + return false; + + if (!Resolve(target, ref target.Comp)) + return false; + + return IsAccessible(user, target) && InRangeUnobstructed(user, target, range, collisionMask, predicate); + } + + /// + /// Check if a user can access a target or if they are stored in different containers. + /// + public bool IsAccessible(Entity user, Entity target) + { + if (_containerSystem.IsInSameOrParentContainer(user, target, out _, out var container)) + return true; + + return container != null && CanAccessViaStorage(user, target, container); + } + /// /// If a target is in range, but not in the same container as the user, it may be inside of a backpack. This /// checks if the user can access the item in these situations. /// - public abstract bool CanAccessViaStorage(EntityUid user, EntityUid target); + public bool CanAccessViaStorage(EntityUid user, EntityUid target) + { + if (!_containerSystem.TryGetContainingContainer(target, out var container)) + return false; + + return CanAccessViaStorage(user, target, container); + } + + /// + public bool CanAccessViaStorage(EntityUid user, EntityUid target, BaseContainer container) + { + if (StorageComponent.ContainerId != container.ID) + return false; + + // we don't check if the user can access the storage entity itself. This should be handed by the UI system. + return _ui.IsUiOpen(target, StorageComponent.StorageUiKey.Key, user); + } /// /// Checks whether an entity currently equipped by another player is accessible to some user. This shouldn't @@ -1151,19 +1249,15 @@ namespace Content.Shared.Interaction RaiseLocalEvent(uidB.Value, new ContactInteractionEvent(uidA)); } + private void HandleUserInterfaceRangeCheck(ref BoundUserInterfaceCheckRangeEvent ev) { if (ev.Result == BoundUserInterfaceRangeResult.Fail) return; - if (InRangeUnobstructed(ev.Actor, ev.Target, ev.Data.InteractionRange)) - { - ev.Result = BoundUserInterfaceRangeResult.Pass; - } - else - { - ev.Result = BoundUserInterfaceRangeResult.Fail; - } + ev.Result = UiRangeCheck(ev.Actor!, ev.Target, ev.Data.InteractionRange) + ? BoundUserInterfaceRangeResult.Pass + : BoundUserInterfaceRangeResult.Fail; } } diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index d55632b104..26cca051ba 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -210,11 +210,7 @@ public abstract partial class InventorySystem return false; // Can the actor reach the item? - if (_interactionSystem.InRangeUnobstructed(actor, itemUid) && _containerSystem.IsInSameOrParentContainer(actor, itemUid)) - return true; - - // Is the item in an open storage UI, i.e., is the user quick-equipping from an open backpack? - if (_interactionSystem.CanAccessViaStorage(actor, itemUid)) + if (_interactionSystem.InRangeAndAccessible(actor, itemUid)) return true; // Is the actor currently stripping the target? Here we could check if the actor has the stripping UI open, but diff --git a/Content.Shared/Inventory/InventorySystem.Slots.cs b/Content.Shared/Inventory/InventorySystem.Slots.cs index 19831278b0..228b788722 100644 --- a/Content.Shared/Inventory/InventorySystem.Slots.cs +++ b/Content.Shared/Inventory/InventorySystem.Slots.cs @@ -75,7 +75,7 @@ public partial class InventorySystem : EntitySystem if (TryGetSlotEntity(uid, ev.Slot, out var entityUid) && TryComp(entityUid, out var storageComponent)) { - _storageSystem.OpenStorageUI(entityUid.Value, uid, storageComponent); + _storageSystem.OpenStorageUI(entityUid.Value, uid, storageComponent, false); } } diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index 4cd94c8d3f..457fdb8c25 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Storage; using Content.Shared.Storage.Components; using Content.Shared.Verbs; using Content.Shared.Wires; @@ -46,11 +47,13 @@ public sealed class LockSystem : EntitySystem SubscribeLocalEvent(OnDoAfterLock); SubscribeLocalEvent(OnDoAfterUnlock); SubscribeLocalEvent(OnBeforeDoorOpened); //CrystallPunk Lock System Adapt + SubscribeLocalEvent(OnStorageInteractAttempt); SubscribeLocalEvent(OnLockToggleAttempt); SubscribeLocalEvent(OnAttemptChangePanel); SubscribeLocalEvent(OnUnanchorAttempt); } + private void OnStartup(EntityUid uid, LockComponent lockComp, ComponentStartup args) { _appearanceSystem.SetData(uid, LockVisuals.Locked, lockComp.Locked); @@ -324,6 +327,12 @@ public sealed class LockSystem : EntitySystem TryUnlock(uid, args.User, skipDoAfter: true); } + private void OnStorageInteractAttempt(Entity ent, ref StorageInteractAttemptEvent args) + { + if (ent.Comp.Locked) + args.Cancelled = true; + } + private void OnLockToggleAttempt(Entity ent, ref LockToggleAttemptEvent args) { if (args.Cancelled) diff --git a/Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs b/Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs index 91059d60bf..433ad6b4fc 100644 --- a/Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs +++ b/Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Humanoid.Markings; using Content.Shared.Interaction; using Content.Shared.UserInterface; using Robust.Shared.Serialization; +using Robust.Shared.Utility; namespace Content.Shared.MagicMirror; @@ -21,10 +22,13 @@ public abstract class SharedMagicMirrorSystem : EntitySystem private void OnMirrorRangeCheck(EntityUid uid, MagicMirrorComponent component, ref BoundUserInterfaceCheckRangeEvent args) { - if (!Exists(component.Target) || !_interaction.InRangeUnobstructed(uid, component.Target.Value)) - { + if (args.Result == BoundUserInterfaceRangeResult.Fail) + return; + + DebugTools.Assert(component.Target != null && Exists(component.Target)); + + if (!_interaction.InRangeUnobstructed(uid, component.Target.Value)) args.Result = BoundUserInterfaceRangeResult.Fail; - } } private void OnBeforeUIOpen(Entity ent, ref BeforeActivatableUIOpenEvent args) diff --git a/Content.Shared/Mobs/Components/MobThresholdsComponent.cs b/Content.Shared/Mobs/Components/MobThresholdsComponent.cs index e97d3672a2..0e37cf9b10 100644 --- a/Content.Shared/Mobs/Components/MobThresholdsComponent.cs +++ b/Content.Shared/Mobs/Components/MobThresholdsComponent.cs @@ -2,6 +2,7 @@ using Content.Shared.Alert; using Content.Shared.FixedPoint; using Content.Shared.Mobs.Systems; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Mobs.Components; @@ -24,13 +25,16 @@ public sealed partial class MobThresholdsComponent : Component /// Used for alternate health alerts (silicons, for example) /// [DataField("stateAlertDict")] - public Dictionary StateAlertDict = new() + public Dictionary> StateAlertDict = new() { - {MobState.Alive, AlertType.HumanHealth}, - {MobState.Critical, AlertType.HumanCrit}, - {MobState.Dead, AlertType.HumanDead}, + {MobState.Alive, "HumanHealth"}, + {MobState.Critical, "HumanCrit"}, + {MobState.Dead, "HumanDead"}, }; + [DataField] + public ProtoId HealthAlertCategory = "Health"; + /// /// Whether or not this entity should display damage overlays (robots don't feel pain, black out etc.) /// @@ -53,19 +57,19 @@ public sealed class MobThresholdsComponentState : ComponentState public MobState CurrentThresholdState; - public Dictionary StateAlertDict = new() - { - {MobState.Alive, AlertType.HumanHealth}, - {MobState.Critical, AlertType.HumanCrit}, - {MobState.Dead, AlertType.HumanDead}, - }; + public Dictionary> StateAlertDict; public bool ShowOverlays; public bool AllowRevives; - public MobThresholdsComponentState(Dictionary unsortedThresholds, bool triggersAlerts, MobState currentThresholdState, - Dictionary stateAlertDict, bool showOverlays, bool allowRevives) + public MobThresholdsComponentState(Dictionary unsortedThresholds, + bool triggersAlerts, + MobState currentThresholdState, + Dictionary> stateAlertDict, + bool showOverlays, + bool allowRevives) { UnsortedThresholds = unsortedThresholds; TriggersAlerts = triggersAlerts; diff --git a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs index 59d9fb4c23..b11de9eac5 100644 --- a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs +++ b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs @@ -431,7 +431,7 @@ public sealed class MobThresholdSystem : EntitySystem private void MobThresholdShutdown(EntityUid target, MobThresholdsComponent component, ComponentShutdown args) { if (component.TriggersAlerts) - _alerts.ClearAlertCategory(target, AlertCategory.Health); + _alerts.ClearAlertCategory(target, component.HealthAlertCategory); } private void OnUpdateMobState(EntityUid target, MobThresholdsComponent component, ref UpdateMobStateEvent args) diff --git a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs index db889e7e3b..100cd9d6d3 100644 --- a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs +++ b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs @@ -1,4 +1,6 @@ +using Content.Shared.Alert; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; namespace Content.Shared.Movement.Pulling.Components; @@ -36,4 +38,7 @@ public sealed partial class PullableComponent : Component [Access(typeof(Systems.PullingSystem), Other = AccessPermissions.ReadExecute)] [AutoNetworkedField, DataField] public bool PrevFixedRotation; + + [DataField] + public ProtoId PulledAlert = "Pulled"; } diff --git a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs index 1fc9b731bd..061ec13ed2 100644 --- a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs +++ b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs @@ -1,5 +1,7 @@ -using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Alert; +using Content.Shared.Movement.Pulling.Systems; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Movement.Pulling.Components; @@ -38,4 +40,7 @@ public sealed partial class PullerComponent : Component /// [DataField] public bool NeedsHands = true; + + [DataField] + public ProtoId PullingAlert = "Pulling"; } diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 81b2fee569..3de71172c7 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -223,7 +223,7 @@ public sealed class PullingSystem : EntitySystem if (TryComp(oldPuller, out var pullerComp)) { var pullerUid = oldPuller.Value; - _alertsSystem.ClearAlert(pullerUid, AlertType.Pulling); + _alertsSystem.ClearAlert(pullerUid, pullerComp.PullingAlert); pullerComp.Pulling = null; Dirty(oldPuller.Value, pullerComp); @@ -237,7 +237,7 @@ public sealed class PullingSystem : EntitySystem } - _alertsSystem.ClearAlert(pullableUid, AlertType.Pulled); + _alertsSystem.ClearAlert(pullableUid, pullableComp.PulledAlert); } public bool IsPulled(EntityUid uid, PullableComponent? component = null) @@ -362,14 +362,17 @@ public sealed class PullingSystem : EntitySystem return !startPull.Cancelled && !getPulled.Cancelled; } - public bool TogglePull(EntityUid pullableUid, EntityUid pullerUid, PullableComponent pullable) + public bool TogglePull(Entity pullable, EntityUid pullerUid) { - if (pullable.Puller == pullerUid) + if (!Resolve(pullable, ref pullable.Comp, false)) + return false; + + if (pullable.Comp.Puller == pullerUid) { - return TryStopPull(pullableUid, pullable); + return TryStopPull(pullable, pullable.Comp); } - return TryStartPull(pullerUid, pullableUid, pullableComp: pullable); + return TryStartPull(pullerUid, pullable, pullableComp: pullable); } public bool TogglePull(EntityUid pullerUid, PullerComponent puller) @@ -377,7 +380,7 @@ public sealed class PullingSystem : EntitySystem if (!TryComp(puller.Pulling, out var pullable)) return false; - return TogglePull(puller.Pulling.Value, pullerUid, pullable); + return TogglePull((puller.Pulling.Value, pullable), pullerUid); } public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, @@ -460,8 +463,8 @@ public sealed class PullingSystem : EntitySystem // Messaging var message = new PullStartedMessage(pullerUid, pullableUid); - _alertsSystem.ShowAlert(pullerUid, AlertType.Pulling); - _alertsSystem.ShowAlert(pullableUid, AlertType.Pulled); + _alertsSystem.ShowAlert(pullerUid, pullerComp.PullingAlert); + _alertsSystem.ShowAlert(pullableUid, pullableComp.PulledAlert); RaiseLocalEvent(pullerUid, message); RaiseLocalEvent(pullableUid, message); diff --git a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs index 0f3bff265c..91c816df5c 100644 --- a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs +++ b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Alert; using Content.Shared.Ninja.Systems; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -53,4 +54,7 @@ public sealed partial class SpaceNinjaComponent : Component /// [DataField] public EntProtoId SpiderChargeObjective = "SpiderChargeObjective"; + + [DataField] + public ProtoId SuitPowerAlert = "SuitPower"; } diff --git a/Content.Shared/Nutrition/Components/HungerComponent.cs b/Content.Shared/Nutrition/Components/HungerComponent.cs index 9ac82ba283..79d895ddae 100644 --- a/Content.Shared/Nutrition/Components/HungerComponent.cs +++ b/Content.Shared/Nutrition/Components/HungerComponent.cs @@ -2,6 +2,7 @@ using Content.Shared.Alert; using Content.Shared.Damage; using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; @@ -65,15 +66,18 @@ public sealed partial class HungerComponent : Component /// /// A dictionary relating hunger thresholds to corresponding alerts. /// - [DataField("hungerThresholdAlerts", customTypeSerializer: typeof(DictionarySerializer))] + [DataField("hungerThresholdAlerts")] [AutoNetworkedField] - public Dictionary HungerThresholdAlerts = new() + public Dictionary> HungerThresholdAlerts = new() { - { HungerThreshold.Peckish, AlertType.Peckish }, - { HungerThreshold.Starving, AlertType.Starving }, - { HungerThreshold.Dead, AlertType.Starving } + { HungerThreshold.Peckish, "Peckish" }, + { HungerThreshold.Starving, "Starving" }, + { HungerThreshold.Dead, "Starving" } }; + [DataField] + public ProtoId HungerAlertCategory = "Hunger"; + /// /// A dictionary relating HungerThreshold to how much they modify . /// diff --git a/Content.Shared/Nutrition/Components/ThirstComponent.cs b/Content.Shared/Nutrition/Components/ThirstComponent.cs index 731346401f..f3ac881361 100644 --- a/Content.Shared/Nutrition/Components/ThirstComponent.cs +++ b/Content.Shared/Nutrition/Components/ThirstComponent.cs @@ -1,6 +1,7 @@ using Content.Shared.Alert; using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Nutrition.Components; @@ -56,11 +57,14 @@ public sealed partial class ThirstComponent : Component {ThirstThreshold.Dead, 0.0f}, }; - public static readonly Dictionary ThirstThresholdAlertTypes = new() + [DataField] + public ProtoId ThirstyCategory = "Thirst"; + + public static readonly Dictionary> ThirstThresholdAlertTypes = new() { - {ThirstThreshold.Thirsty, AlertType.Thirsty}, - {ThirstThreshold.Parched, AlertType.Parched}, - {ThirstThreshold.Dead, AlertType.Parched}, + {ThirstThreshold.Thirsty, "Thirsty"}, + {ThirstThreshold.Parched, "Parched"}, + {ThirstThreshold.Dead, "Parched"}, }; } diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs index 4de4e4d5fe..bff15f06ff 100644 --- a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs @@ -61,7 +61,7 @@ public sealed class HungerSystem : EntitySystem private void OnShutdown(EntityUid uid, HungerComponent component, ComponentShutdown args) { - _alerts.ClearAlertCategory(uid, AlertCategory.Hunger); + _alerts.ClearAlertCategory(uid, component.HungerAlertCategory); } private void OnRefreshMovespeed(EntityUid uid, HungerComponent component, RefreshMovementSpeedModifiersEvent args) @@ -142,7 +142,7 @@ public sealed class HungerSystem : EntitySystem } else { - _alerts.ClearAlertCategory(uid, AlertCategory.Hunger); + _alerts.ClearAlertCategory(uid, component.HungerAlertCategory); } if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier)) diff --git a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs index 8ea7d9140c..205d8d6cde 100644 --- a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs @@ -165,7 +165,7 @@ public sealed class ThirstSystem : EntitySystem } else { - _alerts.ClearAlertCategory(uid, AlertCategory.Thirst); + _alerts.ClearAlertCategory(uid, component.ThirstyCategory); } switch (component.CurrentThirstThreshold) diff --git a/Content.Shared/Pinpointer/SharedNavMapSystem.cs b/Content.Shared/Pinpointer/SharedNavMapSystem.cs index ffe81c2d0e..7c12321b5d 100644 --- a/Content.Shared/Pinpointer/SharedNavMapSystem.cs +++ b/Content.Shared/Pinpointer/SharedNavMapSystem.cs @@ -96,7 +96,7 @@ public abstract class SharedNavMapSystem : EntitySystem chunks.Add(origin, chunk.TileData); } - args.State = new NavMapComponentState(chunks, component.Beacons); + args.State = new NavMapState(chunks, component.Beacons); return; } @@ -109,12 +109,7 @@ public abstract class SharedNavMapSystem : EntitySystem chunks.Add(origin, chunk.TileData); } - args.State = new NavMapComponentState(chunks, component.Beacons) - { - // TODO NAVMAP cache a single AllChunks hashset in the component. - // Or maybe just only send them if a chunk gets removed. - AllChunks = new(component.Chunks.Keys), - }; + args.State = new NavMapDeltaState(chunks, component.Beacons, new(component.Chunks.Keys)); } #endregion @@ -122,32 +117,35 @@ public abstract class SharedNavMapSystem : EntitySystem #region: System messages [Serializable, NetSerializable] - protected sealed class NavMapComponentState( + protected sealed class NavMapState( Dictionary chunks, Dictionary beacons) - : ComponentState, IComponentDeltaState + : ComponentState { public Dictionary Chunks = chunks; public Dictionary Beacons = beacons; + } - // Required to infer deleted/missing chunks for delta states - public HashSet? AllChunks; + [Serializable, NetSerializable] + protected sealed class NavMapDeltaState( + Dictionary modifiedChunks, + Dictionary beacons, + HashSet allChunks) + : ComponentState, IComponentDeltaState + { + public Dictionary ModifiedChunks = modifiedChunks; + public Dictionary Beacons = beacons; + public HashSet AllChunks = allChunks; - public bool FullState => AllChunks == null; - - public void ApplyToFullState(IComponentState fullState) + public void ApplyToFullState(NavMapState state) { - DebugTools.Assert(!FullState); - var state = (NavMapComponentState) fullState; - DebugTools.Assert(state.FullState); - foreach (var key in state.Chunks.Keys) { if (!AllChunks!.Contains(key)) state.Chunks.Remove(key); } - foreach (var (index, data) in Chunks) + foreach (var (index, data) in ModifiedChunks) { if (!state.Chunks.TryGetValue(index, out var stateValue)) state.Chunks[index] = stateValue = new int[data.Length]; @@ -162,12 +160,8 @@ public abstract class SharedNavMapSystem : EntitySystem } } - public IComponentState CreateNewFullState(IComponentState fullState) + public NavMapState CreateNewFullState(NavMapState state) { - DebugTools.Assert(!FullState); - var state = (NavMapComponentState) fullState; - DebugTools.Assert(state.FullState); - var chunks = new Dictionary(state.Chunks.Count); foreach (var (index, data) in state.Chunks) { @@ -176,13 +170,13 @@ public abstract class SharedNavMapSystem : EntitySystem var newData = chunks[index] = new int[ArraySize]; - if (Chunks.TryGetValue(index, out var updatedData)) + if (ModifiedChunks.TryGetValue(index, out var updatedData)) Array.Copy(newData, updatedData, ArraySize); else Array.Copy(newData, data, ArraySize); } - return new NavMapComponentState(chunks, new(Beacons)); + return new NavMapState(chunks, new(Beacons)); } } diff --git a/Content.Shared/Prying/Components/PryUnpoweredComponent.cs b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs index f0e61dc968..b6b69a2577 100644 --- a/Content.Shared/Prying/Components/PryUnpoweredComponent.cs +++ b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs @@ -8,4 +8,6 @@ namespace Content.Shared.Prying.Components; [RegisterComponent, NetworkedComponent] public sealed partial class PryUnpoweredComponent : Component { + [DataField] + public float PryModifier = 0.1f; } diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs index ab87585c70..372c89c9ae 100644 --- a/Content.Shared/Prying/Systems/PryingSystem.cs +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -93,17 +93,17 @@ public sealed class PryingSystem : EntitySystem id = null; // We don't care about displaying a message if no tool was used. - if (!CanPry(target, user, out _)) + if (!TryComp(target, out var unpoweredComp) || !CanPry(target, user, out _, unpoweredComp: unpoweredComp)) // If we have reached this point we want the event that caused this // to be marked as handled. return true; // hand-prying is much slower - var modifier = CompOrNull(user)?.SpeedModifier ?? 0.1f; + var modifier = CompOrNull(user)?.SpeedModifier ?? unpoweredComp.PryModifier; return StartPry(target, user, null, modifier, out id); } - private bool CanPry(EntityUid target, EntityUid user, out string? message, PryingComponent? comp = null) + private bool CanPry(EntityUid target, EntityUid user, out string? message, PryingComponent? comp = null, PryUnpoweredComponent? unpoweredComp = null) { BeforePryEvent canev; @@ -113,7 +113,7 @@ public sealed class PryingSystem : EntitySystem } else { - if (!TryComp(target, out _)) + if (!Resolve(target, ref unpoweredComp)) { message = null; return false; diff --git a/Content.Shared/Revenant/Components/RevenantComponent.cs b/Content.Shared/Revenant/Components/RevenantComponent.cs index 947c1a4b3f..d7fb28ef13 100644 --- a/Content.Shared/Revenant/Components/RevenantComponent.cs +++ b/Content.Shared/Revenant/Components/RevenantComponent.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Shared.Alert; using Content.Shared.FixedPoint; using Content.Shared.Store; using Content.Shared.Whitelist; @@ -200,6 +201,9 @@ public sealed partial class RevenantComponent : Component public EntityWhitelist? MalfunctionBlacklist; #endregion + [DataField] + public ProtoId EssenceAlert = "Essence"; + #region Visualizer [DataField("state")] public string State = "idle"; diff --git a/Content.Shared/Shuttles/Components/PilotComponent.cs b/Content.Shared/Shuttles/Components/PilotComponent.cs index 1a6927cf81..cb42db4436 100644 --- a/Content.Shared/Shuttles/Components/PilotComponent.cs +++ b/Content.Shared/Shuttles/Components/PilotComponent.cs @@ -1,7 +1,9 @@ using System.Numerics; +using Content.Shared.Alert; using Content.Shared.Movement.Systems; using Robust.Shared.GameStates; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Shuttles.Components @@ -32,6 +34,9 @@ namespace Content.Shared.Shuttles.Components [ViewVariables] public ShuttleButtons HeldButtons = ShuttleButtons.None; + [DataField] + public ProtoId PilotingAlert = "PilotingShuttle"; + public override bool SendOnlyToOwner => true; } } diff --git a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs index 71d3a7bd16..e1776873da 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs @@ -1,6 +1,8 @@ -using Content.Shared.Whitelist; +using Content.Shared.Alert; +using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; namespace Content.Shared.Silicons.Borgs.Components; @@ -76,6 +78,12 @@ public sealed partial class BorgChassisComponent : Component [DataField("noMindState")] public string NoMindState = string.Empty; #endregion + + [DataField] + public ProtoId BatteryAlert = "BorgBattery"; + + [DataField] + public ProtoId NoBatteryAlert = "BorgBatteryNone"; } [Serializable, NetSerializable] diff --git a/Content.Shared/StatusEffect/StatusEffectPrototype.cs b/Content.Shared/StatusEffect/StatusEffectPrototype.cs index ae9e26879e..8b1f84e4d8 100644 --- a/Content.Shared/StatusEffect/StatusEffectPrototype.cs +++ b/Content.Shared/StatusEffect/StatusEffectPrototype.cs @@ -10,7 +10,7 @@ namespace Content.Shared.StatusEffect public string ID { get; private set; } = default!; [DataField("alert")] - public AlertType? Alert { get; private set; } + public ProtoId? Alert { get; private set; } /// /// Whether a status effect should be able to apply to any entity, diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index f3e3e12bd8..9806077f9b 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -219,7 +219,7 @@ namespace Content.Shared.StatusEffect /// This is mostly for stuns, since Stun and Knockdown share an alert key. Other times this pretty much /// will not be useful. /// - private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, AlertType alert, StatusEffectsComponent status) + private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, ProtoId alert, StatusEffectsComponent status) { (TimeSpan, TimeSpan)? maxCooldown = null; foreach (var kvp in status.ActiveEffects) diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index a55f21f2e2..66f0e7963c 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -9,12 +9,12 @@ using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Containers.ItemSlots; using Content.Shared.Destructible; using Content.Shared.DoAfter; -using Content.Shared.Ghost; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Implants.Components; using Content.Shared.Input; using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; using Content.Shared.Inventory; using Content.Shared.Item; using Content.Shared.Lock; @@ -42,7 +42,6 @@ public abstract class SharedStorageSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] protected readonly IRobustRandom Random = default!; - [Dependency] private readonly ISharedAdminManager _admin = default!; [Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!; [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; @@ -251,17 +250,8 @@ public abstract class SharedStorageSystem : EntitySystem private void AddUiVerb(EntityUid uid, StorageComponent component, GetVerbsEvent args) { - var silent = false; - if (!args.CanAccess || !args.CanInteract || TryComp(uid, out var lockComponent) && lockComponent.Locked) - { - // we allow admins to open the storage anyways - if (!_admin.HasAdminFlag(args.User, AdminFlags.Admin)) - return; - - silent = true; - } - - silent |= HasComp(args.User); + if (!CanInteract(args.User, (uid, component), args.CanAccess && args.CanInteract)) + return; // Does this player currently have the storage UI open? var uiOpen = _ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User); @@ -276,7 +266,7 @@ public abstract class SharedStorageSystem : EntitySystem } else { - OpenStorageUI(uid, args.User, component, silent); + OpenStorageUI(uid, args.User, component); } } }; @@ -300,13 +290,16 @@ public abstract class SharedStorageSystem : EntitySystem /// Opens the storage UI for an entity /// /// The entity to open the UI for - public void OpenStorageUI(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = false) + public void OpenStorageUI(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = true) { if (!Resolve(uid, ref storageComp, false)) return; // prevent spamming bag open / honkerton honk sound silent |= TryComp(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay)); + if (!CanInteract(entity, (uid, storageComp), silent: silent)) + return; + if (!silent) { if (!_ui.IsUiOpen(uid, StorageComponent.StorageUiKey.Key)) @@ -328,7 +321,7 @@ public abstract class SharedStorageSystem : EntitySystem var entities = component.Container.ContainedEntities; - if (entities.Count == 0 || TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked) + if (entities.Count == 0 || !CanInteract(args.User, (uid, component))) return; // if the target is storage, add a verb to transfer storage. @@ -339,7 +332,7 @@ public abstract class SharedStorageSystem : EntitySystem { Text = Loc.GetString("storage-component-transfer-verb"), IconEntity = GetNetEntity(args.Using), - Act = () => TransferEntities(uid, args.Target, args.User, component, lockComponent, targetStorage, targetLock) + Act = () => TransferEntities(uid, args.Target, args.User, component, null, targetStorage, targetLock) }; args.Verbs.Add(verb); @@ -352,7 +345,7 @@ public abstract class SharedStorageSystem : EntitySystem /// true if inserted, false otherwise private void OnInteractUsing(EntityUid uid, StorageComponent storageComp, InteractUsingEvent args) { - if (args.Handled || !storageComp.ClickInsert || TryComp(uid, out LockComponent? lockComponent) && lockComponent.Locked) + if (args.Handled || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert, false)) return; if (HasComp(uid)) @@ -373,7 +366,7 @@ public abstract class SharedStorageSystem : EntitySystem /// private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args) { - if (args.Handled || TryComp(uid, out var lockComponent) && lockComponent.Locked) + if (args.Handled || !CanInteract(args.User, (uid, storageComp), storageComp.ClickInsert)) return; // Toggle @@ -383,7 +376,7 @@ public abstract class SharedStorageSystem : EntitySystem } else { - OpenStorageUI(uid, args.User, storageComp); + OpenStorageUI(uid, args.User, storageComp, false); } args.Handled = true; @@ -397,7 +390,7 @@ public abstract class SharedStorageSystem : EntitySystem if (args.Handled) return; - OpenStorageUI(uid, args.Performer, storageComp); + OpenStorageUI(uid, args.Performer, storageComp, false); args.Handled = true; } @@ -1092,7 +1085,7 @@ public abstract class SharedStorageSystem : EntitySystem /// true if inserted, false otherwise public bool PlayerInsertEntityInWorld(Entity uid, EntityUid player, EntityUid toInsert) { - if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid)) + if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid.Owner)) return false; if (!Insert(uid, toInsert, out _, user: player, uid.Comp)) @@ -1408,7 +1401,7 @@ public abstract class SharedStorageSystem : EntitySystem } /// - /// Checks if a storage's UI is open by anyone when locked, and closes it unless they're an admin. + /// Checks if a storage's UI is open by anyone when locked, and closes it. /// private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockToggledEvent args) { @@ -1418,11 +1411,8 @@ public abstract class SharedStorageSystem : EntitySystem // Gets everyone looking at the UI foreach (var actor in _ui.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList()) { - if (_admin.HasAdminFlag(actor, AdminFlags.Admin)) - continue; - - // And closes it unless they're an admin - _ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor); + if (!CanInteract(actor, (uid, component))) + _ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor); } } @@ -1462,7 +1452,7 @@ public abstract class SharedStorageSystem : EntitySystem if (!_ui.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt)) { - OpenStorageUI(storageEnt.Value, playerEnt); + OpenStorageUI(storageEnt.Value, playerEnt, silent: false); } else { @@ -1477,6 +1467,20 @@ public abstract class SharedStorageSystem : EntitySystem #endif } + private bool CanInteract(EntityUid user, Entity storage, bool canInteract = true, bool silent = true) + { + if (HasComp(user)) + return true; + + if (!canInteract) + return false; + + var ev = new StorageInteractAttemptEvent(silent); + RaiseLocalEvent(storage, ref ev); + + return !ev.Cancelled; + } + /// /// Plays a clientside pickup animation for the specified uid. /// diff --git a/Content.Shared/Storage/StorageComponent.cs b/Content.Shared/Storage/StorageComponent.cs index e712d167e5..0ea1baeb40 100644 --- a/Content.Shared/Storage/StorageComponent.cs +++ b/Content.Shared/Storage/StorageComponent.cs @@ -5,7 +5,6 @@ using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; -using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -236,6 +235,9 @@ namespace Content.Shared.Storage } } + [ByRefEvent] + public record struct StorageInteractAttemptEvent(bool Silent, bool Cancelled = false); + [NetSerializable] [Serializable] public enum StorageVisuals : byte diff --git a/Content.Shared/UserInterface/ActivatableUIComponent.cs b/Content.Shared/UserInterface/ActivatableUIComponent.cs index 3f83816b7d..93f05acac0 100644 --- a/Content.Shared/UserInterface/ActivatableUIComponent.cs +++ b/Content.Shared/UserInterface/ActivatableUIComponent.cs @@ -57,7 +57,7 @@ namespace Content.Shared.UserInterface /// [ViewVariables(VVAccess.ReadWrite)] [DataField] - public bool AllowSpectator = true; + public bool BlockSpectators; /// /// Whether the item must be in the user's currently selected/active hand. diff --git a/Content.Shared/UserInterface/ActivatableUISystem.cs b/Content.Shared/UserInterface/ActivatableUISystem.cs index a6d27ac545..c1822c4ee3 100644 --- a/Content.Shared/UserInterface/ActivatableUISystem.cs +++ b/Content.Shared/UserInterface/ActivatableUISystem.cs @@ -8,7 +8,7 @@ using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Popups; using Content.Shared.Verbs; -using Robust.Shared.Containers; +using Robust.Shared.Utility; namespace Content.Shared.UserInterface; @@ -19,15 +19,12 @@ public sealed partial class ActivatableUISystem : EntitySystem [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly SharedInteractionSystem _interaction = default!; - - private readonly List _toClose = new(); public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnUseInHand); SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnInteractUsing); @@ -37,28 +34,24 @@ public sealed partial class ActivatableUISystem : EntitySystem SubscribeLocalEvent>(GetActivationVerb); SubscribeLocalEvent>(GetVerb); - // TODO ActivatableUI - // Add UI-user component, and listen for user container changes. - // I.e., should lose a computer UI if a player gets shut into a locker. - SubscribeLocalEvent(OnGotInserted); - SubscribeLocalEvent(OnGotRemoved); - - SubscribeLocalEvent(OnBoundInterfaceInteractAttempt); SubscribeLocalEvent(OnActionPerform); InitializePower(); } - private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev) + private void OnStartup(Entity ent, ref ComponentStartup args) { - if (!TryComp(ev.Target, out ActivatableUIComponent? comp)) + if (ent.Comp.Key == null) + { + Log.Error($"Missing UI Key for entity: {ToPrettyString(ent)}"); return; + } - if (!comp.RequireHands) - return; - - if (!TryComp(ev.Actor, out HandsComponent? hands) || hands.Hands.Count == 0) - ev.Cancel(); + // TODO BUI + // set interaction range to zero to avoid constant range checks. + // + // if (ent.Comp.InHandsOnly && _uiSystem.TryGetInterfaceData(ent.Owner, ent.Comp.Key, out var data)) + // data.InteractionRange = 0; } private void OnActionPerform(EntityUid uid, UserInterfaceComponent component, OpenUiActionEvent args) @@ -77,9 +70,10 @@ public sealed partial class ActivatableUISystem : EntitySystem args.Verbs.Add(new ActivationVerb { - // TODO VERBS add "open UI" icon Act = () => InteractUI(args.User, uid, component), - Text = Loc.GetString(component.VerbText) + Text = Loc.GetString(component.VerbText), + // TODO VERB ICON find a better icon + Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), }); } @@ -90,9 +84,10 @@ public sealed partial class ActivatableUISystem : EntitySystem args.Verbs.Add(new Verb { - // TODO VERBS add "open UI" icon Act = () => InteractUI(args.User, uid, component), - Text = Loc.GetString(component.VerbText) + Text = Loc.GetString(component.VerbText), + // TODO VERB ICON find a better icon + Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), }); } @@ -119,7 +114,7 @@ public sealed partial class ActivatableUISystem : EntitySystem } } - return args.CanInteract || component.AllowSpectator && HasComp(args.User); + return args.CanInteract || HasComp(args.User) && !component.BlockSpectators; } private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args) @@ -191,7 +186,7 @@ public sealed partial class ActivatableUISystem : EntitySystem return true; } - if (!_blockerSystem.CanInteract(user, uiEntity) && (!aui.AllowSpectator || !HasComp(user))) + if (!_blockerSystem.CanInteract(user, uiEntity) && (!HasComp(user) || aui.BlockSpectators)) return false; if (aui.RequireHands) @@ -286,47 +281,4 @@ public sealed partial class ActivatableUISystem : EntitySystem if (ent.Comp.RequireHands && ent.Comp.InHandsOnly) CloseAll(ent, ent); } - - private void OnGotInserted(Entity ent, ref EntGotInsertedIntoContainerMessage args) - { - CheckAccess((ent, ent)); - } - - private void OnGotRemoved(Entity ent, ref EntGotRemovedFromContainerMessage args) - { - CheckAccess((ent, ent)); - } - - public void CheckAccess(Entity ent) - { - if (!Resolve(ent, ref ent.Comp)) - return; - - if (ent.Comp.Key == null) - { - Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(ent)}"); - return; - } - - foreach (var user in _uiSystem.GetActors(ent.Owner, ent.Comp.Key)) - { - if (!_container.IsInSameOrParentContainer(user, ent) - && !_interaction.CanAccessViaStorage(user, ent)) - { - _toClose.Add(user); - continue; - - } - - if (!_interaction.InRangeUnobstructed(user, ent)) - _toClose.Add(user); - } - - foreach (var user in _toClose) - { - _uiSystem.CloseUi(ent.Owner, ent.Comp.Key, user); - } - - _toClose.Clear(); - } } diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs index 60714aea8f..e78fe98f4c 100644 --- a/Content.Shared/Verbs/SharedVerbSystem.cs +++ b/Content.Shared/Verbs/SharedVerbSystem.cs @@ -72,19 +72,7 @@ namespace Content.Shared.Verbs extraCategories = new(); // accessibility checks - bool canAccess = false; - if (force || target == user) - canAccess = true; - else if (_interactionSystem.InRangeUnobstructed(user, target)) - { - // Note that being in a container does not count as an obstruction for InRangeUnobstructed - // Therefore, we need extra checks to ensure the item is actually accessible: - if (ContainerSystem.IsInSameOrParentContainer(user, target)) - canAccess = true; - else - // the item might be in a backpack that the user has open - canAccess = _interactionSystem.CanAccessViaStorage(user, target); - } + var canAccess = force || _interactionSystem.InRangeAndAccessible(user, target); // A large number of verbs need to check action blockers. Instead of repeatedly having each system individually // call ActionBlocker checks, just cache it for the verb request. diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 98b9bed6a4..9f4d2cddce 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -174,49 +174,39 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem private void OnLightAttack(LightAttackEvent msg, EntitySessionEventArgs args) { - var user = args.SenderSession.AttachedEntity; - - if (user == null) + if (args.SenderSession.AttachedEntity is not {} user) return; - if (!TryGetWeapon(user.Value, out var weaponUid, out var weapon) || + if (!TryGetWeapon(user, out var weaponUid, out var weapon) || weaponUid != GetEntity(msg.Weapon)) { return; } - AttemptAttack(args.SenderSession.AttachedEntity!.Value, weaponUid, weapon, msg, args.SenderSession); + AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } private void OnHeavyAttack(HeavyAttackEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity == null) - { + if (args.SenderSession.AttachedEntity is not {} user) return; - } - if (!TryGetWeapon(args.SenderSession.AttachedEntity.Value, out var weaponUid, out var weapon) || + if (!TryGetWeapon(user, out var weaponUid, out var weapon) || weaponUid != GetEntity(msg.Weapon)) { return; } - AttemptAttack(args.SenderSession.AttachedEntity.Value, weaponUid, weapon, msg, args.SenderSession); + AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } private void OnDisarmAttack(DisarmAttackEvent msg, EntitySessionEventArgs args) { - if (args.SenderSession.AttachedEntity == null) - { + if (args.SenderSession.AttachedEntity is not {} user) return; - } - if (!TryGetWeapon(args.SenderSession.AttachedEntity.Value, out var weaponUid, out var weapon)) - { - return; - } - - AttemptAttack(args.SenderSession.AttachedEntity.Value, weaponUid, weapon, msg, args.SenderSession); + if (TryGetWeapon(user, out var weaponUid, out var weapon)) + AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession); } /// @@ -343,23 +333,32 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem if (!CombatMode.IsInCombatMode(user)) return false; + EntityUid? target = null; switch (attack) { case LightAttackEvent light: - var lightTarget = GetEntity(light.Target); + if (light.Target != null && !TryGetEntity(light.Target, out target)) + { + // Target was lightly attacked & deleted. + return false; + } - if (!Blocker.CanAttack(user, lightTarget, (weaponUid, weapon))) + if (!Blocker.CanAttack(user, target, (weaponUid, weapon))) return false; // Can't self-attack if you're the weapon - if (weaponUid == lightTarget) + if (weaponUid == target) return false; break; case DisarmAttackEvent disarm: - var disarmTarget = GetEntity(disarm.Target); + if (disarm.Target != null && !TryGetEntity(disarm.Target, out target)) + { + // Target was lightly attacked & deleted. + return false; + } - if (!Blocker.CanAttack(user, disarmTarget, (weaponUid, weapon), true)) + if (!Blocker.CanAttack(user, target, (weaponUid, weapon), true)) return false; break; default: diff --git a/Content.Shared/Weapons/Reflect/ReflectSystem.cs b/Content.Shared/Weapons/Reflect/ReflectSystem.cs index 36dbedb4cb..03ad97edff 100644 --- a/Content.Shared/Weapons/Reflect/ReflectSystem.cs +++ b/Content.Shared/Weapons/Reflect/ReflectSystem.cs @@ -42,6 +42,9 @@ public sealed class ReflectSystem : EntitySystem [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly AlertsSystem _alerts = default!; + [ValidatePrototypeId] + private const string DeflectingAlert = "Deflecting"; + public override void Initialize() { base.Initialize(); @@ -296,11 +299,11 @@ public sealed class ReflectSystem : EntitySystem private void EnableAlert(EntityUid alertee) { - _alerts.ShowAlert(alertee, AlertType.Deflecting); + _alerts.ShowAlert(alertee, DeflectingAlert); } private void DisableAlert(EntityUid alertee) { - _alerts.ClearAlert(alertee, AlertType.Deflecting); + _alerts.ClearAlert(alertee, DeflectingAlert); } } diff --git a/Content.Tests/Shared/Alert/AlertManagerTests.cs b/Content.Tests/Shared/Alert/AlertManagerTests.cs index 2d5f6af5a7..c57df63d5b 100644 --- a/Content.Tests/Shared/Alert/AlertManagerTests.cs +++ b/Content.Tests/Shared/Alert/AlertManagerTests.cs @@ -1,6 +1,5 @@ using System.IO; using Content.Client.Alerts; -using Content.Server.Alert; using Content.Shared.Alert; using NUnit.Framework; using Robust.Shared.GameObjects; @@ -45,15 +44,15 @@ namespace Content.Tests.Shared.Alert prototypeManager.Initialize(); prototypeManager.LoadFromStream(new StringReader(PROTOTYPES)); - Assert.That(alertsSystem.TryGet(AlertType.LowPressure, out var lowPressure)); - Assert.That(lowPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png")))); - Assert.That(alertsSystem.TryGet(AlertType.HighPressure, out var highPressure)); - Assert.That(highPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png")))); + Assert.That(alertsSystem.TryGet("LowPressure", out var lowPressure)); + Assert.That(lowPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png")))); + Assert.That(alertsSystem.TryGet("HighPressure", out var highPressure)); + Assert.That(highPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png")))); - Assert.That(alertsSystem.TryGet(AlertType.LowPressure, out lowPressure)); - Assert.That(lowPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png")))); - Assert.That(alertsSystem.TryGet(AlertType.HighPressure, out highPressure)); - Assert.That(highPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png")))); + Assert.That(alertsSystem.TryGet("LowPressure", out lowPressure)); + Assert.That(lowPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png")))); + Assert.That(alertsSystem.TryGet("HighPressure", out highPressure)); + Assert.That(highPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png")))); } } } diff --git a/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs b/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs index 56f76d46a9..efcd59acbb 100644 --- a/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs +++ b/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs @@ -85,24 +85,24 @@ namespace Content.Tests.Shared.Alert var alerts = prototypeManager.EnumeratePrototypes(); // ensure they sort according to our expected criteria - var expectedOrder = new List(); - expectedOrder.Add(AlertType.Handcuffed); - expectedOrder.Add(AlertType.Ensnared); - expectedOrder.Add(AlertType.HighPressure); + var expectedOrder = new List(); + expectedOrder.Add("Handcuffed"); + expectedOrder.Add("Ensnared"); + expectedOrder.Add("HighPressure"); // stuff with only category + same category ordered by enum value - expectedOrder.Add(AlertType.Peckish); - expectedOrder.Add(AlertType.Hot); - expectedOrder.Add(AlertType.Stun); - expectedOrder.Add(AlertType.LowPressure); - expectedOrder.Add(AlertType.Cold); - // stuff at end of list ordered by enum value - expectedOrder.Add(AlertType.Weightless); - expectedOrder.Add(AlertType.PilotingShuttle); + expectedOrder.Add("Peckish"); + expectedOrder.Add("Hot"); + expectedOrder.Add("Stun"); + expectedOrder.Add("LowPressure"); + expectedOrder.Add("Cold"); + // stuff at end of list ordered by ID + expectedOrder.Add("PilotingShuttle"); + expectedOrder.Add("Weightless"); var actual = alerts.ToList(); actual.Sort(alertOrder); - Assert.That(actual.Select(a => a.AlertType).ToList(), Is.EqualTo(expectedOrder)); + Assert.That(actual.Select(a => a.ID).ToList(), Is.EqualTo(expectedOrder)); } } } diff --git a/Content.Tests/Shared/Alert/AlertPrototypeTests.cs b/Content.Tests/Shared/Alert/AlertPrototypeTests.cs index d95acb8aff..43ae98b452 100644 --- a/Content.Tests/Shared/Alert/AlertPrototypeTests.cs +++ b/Content.Tests/Shared/Alert/AlertPrototypeTests.cs @@ -39,9 +39,9 @@ namespace Content.Tests.Shared.Alert [Test] public void TestAlertKey() { - Assert.That(new AlertKey(AlertType.HumanHealth, null), Is.Not.EqualTo(AlertKey.ForCategory(AlertCategory.Health))); - Assert.That((new AlertKey(null, AlertCategory.Health)), Is.EqualTo(AlertKey.ForCategory(AlertCategory.Health))); - Assert.That((new AlertKey(AlertType.Buckled, AlertCategory.Health)), Is.EqualTo(AlertKey.ForCategory(AlertCategory.Health))); + Assert.That(new AlertKey("HumanHealth", null), Is.Not.EqualTo(AlertKey.ForCategory("Health"))); + Assert.That((new AlertKey(null, "Health")), Is.EqualTo(AlertKey.ForCategory("Health"))); + Assert.That((new AlertKey("Buckled", "Health")), Is.EqualTo(AlertKey.ForCategory("Health"))); } [TestCase(0, "/Textures/Interface/Alerts/Human/human.rsi/human0.png")] diff --git a/Content.Tests/Shared/Alert/ServerAlertsComponentTests.cs b/Content.Tests/Shared/Alert/ServerAlertsComponentTests.cs index 47ae3ef74a..bcc32e13de 100644 --- a/Content.Tests/Shared/Alert/ServerAlertsComponentTests.cs +++ b/Content.Tests/Shared/Alert/ServerAlertsComponentTests.cs @@ -15,6 +15,9 @@ namespace Content.Tests.Shared.Alert public sealed class ServerAlertsComponentTests : ContentUnitTest { const string PROTOTYPES = @" +- type: alertCategory + id: Pressure + - type: alert id: LowPressure category: Pressure @@ -49,10 +52,10 @@ namespace Content.Tests.Shared.Alert var alertsComponent = new AlertsComponent(); alertsComponent = IoCManager.InjectDependencies(alertsComponent); - Assert.That(entManager.System().TryGet(AlertType.LowPressure, out var lowpressure)); - Assert.That(entManager.System().TryGet(AlertType.HighPressure, out var highpressure)); + Assert.That(entManager.System().TryGet("LowPressure", out var lowpressure)); + Assert.That(entManager.System().TryGet("HighPressure", out var highpressure)); - entManager.System().ShowAlert(alertsComponent.Owner, AlertType.LowPressure, null, null); + entManager.System().ShowAlert(alertsComponent.Owner, "LowPressure"); var getty = new ComponentGetState(); entManager.EventBus.RaiseComponentEvent(alertsComponent, getty); @@ -60,17 +63,17 @@ namespace Content.Tests.Shared.Alert var alertState = (AlertsComponent.AlertsComponent_AutoState) getty.State!; Assert.That(alertState, Is.Not.Null); Assert.That(alertState.Alerts.Count, Is.EqualTo(1)); - Assert.That(alertState.Alerts.ContainsKey(lowpressure.AlertKey)); + Assert.That(alertState.Alerts.ContainsKey(lowpressure!.AlertKey)); - entManager.System().ShowAlert(alertsComponent.Owner, AlertType.HighPressure, null, null); + entManager.System().ShowAlert(alertsComponent.Owner, "HighPressure"); // Lazy entManager.EventBus.RaiseComponentEvent(alertsComponent, getty); alertState = (AlertsComponent.AlertsComponent_AutoState) getty.State!; Assert.That(alertState.Alerts.Count, Is.EqualTo(1)); - Assert.That(alertState.Alerts.ContainsKey(highpressure.AlertKey)); + Assert.That(alertState.Alerts.ContainsKey(highpressure!.AlertKey)); - entManager.System().ClearAlertCategory(alertsComponent.Owner, AlertCategory.Pressure); + entManager.System().ClearAlertCategory(alertsComponent.Owner, "Pressure"); entManager.EventBus.RaiseComponentEvent(alertsComponent, getty); alertState = (AlertsComponent.AlertsComponent_AutoState) getty.State!; diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d20cf44cdb..61b8e7ec78 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,36 +1,4 @@ Entries: -- author: MACMAN2003 - changes: - - message: Diagonal windows no longer space you when in a pressurized environment. - type: Fix - id: 6112 - time: '2024-03-08T07:50:34.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25926 -- author: mryikes - changes: - - message: New Nuke Detonation Song "Clearly Nuclear". - type: Add - id: 6113 - time: '2024-03-09T01:44:54.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25765 -- author: Plykiya - changes: - - message: Pre-filled syringes start in inject mode now. - type: Tweak - id: 6114 - time: '2024-03-09T10:19:03.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25881 -- author: EmoGarbage404 - changes: - - message: Added a new "colorblind friendly" toggle in the accessibility menu. This - allows you to toggle between a standard and colorblind-friendly palette for - things like progress bars and the medical HUD. - type: Add - - message: The medical HUD is now bright, even in low light levels. - type: Tweak - id: 6115 - time: '2024-03-09T11:43:20.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25318 - author: metalgearsloth changes: - message: Fix NPC mouse movement. @@ -3870,3 +3838,36 @@ id: 6611 time: '2024-05-22T07:52:58.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/28200 +- author: nikthechampiongr + changes: + - message: Firelocks can now be pried by hand. + type: Tweak + - message: Firelocks will now not automatically close for 2 seconds after you pry + them open. + type: Tweak + - message: Firelocks are now properly predicted. + type: Fix + id: 6612 + time: '2024-05-22T11:16:20.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26582 +- author: blueDev2 + changes: + - message: Watermelon slices are now considered fruit + type: Fix + id: 6613 + time: '2024-05-23T20:27:45.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/28226 +- author: ElectroJr + changes: + - message: Ghosts can once again open paper & other UIs + type: Fix + id: 6614 + time: '2024-05-24T05:03:03.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27999 +- author: nikthechampiongr + changes: + - message: Firelocks will no longer randomly pulse closing lights. + type: Fix + id: 6615 + time: '2024-05-24T14:44:42.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/28227 diff --git a/Resources/Maps/atlas.yml b/Resources/Maps/atlas.yml index cfd5f3b5ac..cb7dc8e778 100644 --- a/Resources/Maps/atlas.yml +++ b/Resources/Maps/atlas.yml @@ -89,7 +89,7 @@ entities: version: 6 0,1: ind: 0,1 - tiles: AwAAAAAAAwAAAAAAAwAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAAwAAAAAAAwAAAAAAAwAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAAwAAAAAAAwAAAAAAAwAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAHQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAALAAAAAAAeQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAHQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAHQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAPgAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeAAAAAAAPgAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAAAAAAAAAPgAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAAAAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAALgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeQAAAAAAeQAAAAAAeAAAAAAAeAAAAAAAeAAAAAAAAAAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeQAAAAAAWQAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeQAAAAAAWQAAAAAA + tiles: AwAAAAAAAwAAAAAAAwAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAAwAAAAAAAwAAAAAAAwAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAAwAAAAAAAwAAAAAAAwAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAHQAAAAAAeQAAAAAAWQAAAAAAWQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAUAAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAHQAAAAAAeQAAAAAAeQAAAAAAWQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAALAAAAAAAeQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAHQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAdgAAAAAAHQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAATQAAAAAATQAAAAAATQAAAAAATQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAPgAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeAAAAAAAPgAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAAAAAAAAAPgAAAAAAdgAAAAAAdgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAAAAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAALgAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeQAAAAAAeQAAAAAAeAAAAAAAeAAAAAAAeAAAAAAAAAAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAeQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeQAAAAAAWQAAAAAAAAAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeQAAAAAAWQAAAAAA version: 6 1,1: ind: 1,1 @@ -3360,6 +3360,13 @@ entities: - type: Transform pos: -44.5,-15.5 parent: 30 +- proto: AirlockArmoryGlassLocked + entities: + - uid: 1448 + components: + - type: Transform + pos: 9.5,14.5 + parent: 30 - proto: AirlockAtmosphericsGlassLocked entities: - uid: 241 @@ -3940,6 +3947,9 @@ entities: - type: Transform pos: 18.5,-6.5 parent: 30 + - type: DeviceLinkSource + lastSignals: + DoorStatus: True - uid: 2282 components: - type: Transform @@ -4053,12 +4063,6 @@ entities: parent: 30 - proto: AirlockHeadOfSecurityGlassLocked entities: - - uid: 1448 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 9.5,14.5 - parent: 30 - uid: 7378 components: - type: Transform @@ -4239,6 +4243,9 @@ entities: - type: Transform pos: -6.5,17.5 parent: 30 + - type: DeviceLinkSource + lastSignals: + DoorStatus: True - proto: AirlockMedicalGlass entities: - uid: 2408 @@ -7082,7 +7089,7 @@ entities: - uid: 2074 components: - type: Transform - pos: -10.5,8.5 + pos: -7.5,8.5 parent: 30 - uid: 2075 components: @@ -23899,14 +23906,6 @@ entities: rot: 1.5707963267948966 rad pos: -44.5,-16.5 parent: 30 -- proto: EncryptionKeyCargo - entities: - - uid: 8041 - components: - - type: Transform - parent: 8035 - - type: Physics - canCollide: False - proto: EncryptionKeyCommand entities: - uid: 6716 @@ -23931,14 +23930,6 @@ entities: parent: 6972 - type: Physics canCollide: False -- proto: EncryptionKeyMedical - entities: - - uid: 7916 - components: - - type: Transform - parent: 7915 - - type: Physics - canCollide: False - proto: EncryptionKeyScience entities: - uid: 7393 @@ -38622,6 +38613,14 @@ entities: rot: -1.5707963267948966 rad pos: -11.5,-9.5 parent: 30 +- proto: HydroponicsToolClippers + entities: + - uid: 7916 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: 2.368904,17.565664 + parent: 30 - proto: HydroponicsToolHatchet entities: - uid: 1498 @@ -41743,14 +41742,6 @@ entities: parent: 30 - type: ApcPowerReceiver powerLoad: 0 - - uid: 7438 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: -30.5,-11.5 - parent: 30 - - type: ApcPowerReceiver - powerLoad: 0 - uid: 7439 components: - type: Transform @@ -41773,6 +41764,12 @@ entities: rot: 3.141592653589793 rad pos: 3.5,-15.5 parent: 30 + - uid: 8041 + components: + - type: Transform + rot: 3.141592653589793 rad + pos: -31.5,-11.5 + parent: 30 - uid: 8112 components: - type: Transform @@ -46316,6 +46313,14 @@ entities: - type: Transform pos: 10.5,15.5 parent: 30 +- proto: SpawnMobMonkeyPunpun + entities: + - uid: 8035 + components: + - type: Transform + rot: 1.5707963267948966 rad + pos: -18.5,3.5 + parent: 30 - proto: SpawnMobMouse entities: - uid: 1570 @@ -48692,48 +48697,20 @@ entities: showEnts: False occludes: True ents: [] -- proto: TelecomServerFilled +- proto: TelecomServerFilledCargo + entities: + - uid: 7438 + components: + - type: Transform + pos: -48.5,6.5 + parent: 30 +- proto: TelecomServerFilledMedical entities: - uid: 7915 components: - type: Transform pos: -47.5,6.5 parent: 30 - - type: ContainerContainer - containers: - key_slots: !type:Container - showEnts: False - occludes: True - ents: - - 7916 - machine_board: !type:Container - showEnts: False - occludes: True - ents: [] - machine_parts: !type:Container - showEnts: False - occludes: True - ents: [] - - uid: 8035 - components: - - type: Transform - pos: -48.5,6.5 - parent: 30 - - type: ContainerContainer - containers: - key_slots: !type:Container - showEnts: False - occludes: True - ents: - - 8041 - machine_board: !type:Container - showEnts: False - occludes: True - ents: [] - machine_parts: !type:Container - showEnts: False - occludes: True - ents: [] - proto: ToiletDirtyWater entities: - uid: 7674 diff --git a/Resources/Prototypes/Alerts/categories.yml b/Resources/Prototypes/Alerts/categories.yml new file mode 100644 index 0000000000..2365422ed9 --- /dev/null +++ b/Resources/Prototypes/Alerts/categories.yml @@ -0,0 +1,35 @@ +- type: alertCategory + id: Pressure + +- type: alertCategory + id: Temperature + +- type: alertCategory + id: Breathing + +- type: alertCategory + id: Buckled + +- type: alertCategory + id: Health + +- type: alertCategory + id: Internals + +- type: alertCategory + id: Stamina + +- type: alertCategory + id: Piloting + +- type: alertCategory + id: Hunger + +- type: alertCategory + id: Thirst + +- type: alertCategory + id: Toxins + +- type: alertCategory + id: Battery diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_service.yml b/Resources/Prototypes/Catalog/Cargo/cargo_service.yml index ebcd9dfc5e..d2ca08e116 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_service.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_service.yml @@ -173,7 +173,7 @@ icon: sprite: Clothing/Head/Helmets/janitor_bombsuit.rsi state: icon - product: CrateJanitorExplosive + product: ClosetJanitorBombFilled cost: 1000 category: cargoproduct-category-name-service group: market diff --git a/Resources/Prototypes/Catalog/Fills/Crates/service.yml b/Resources/Prototypes/Catalog/Fills/Crates/service.yml index 35e66ac4d3..5b16b91f8b 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/service.yml @@ -321,16 +321,3 @@ prob: 0.1 - id: ShardGlassPlasma prob: 0.1 - -- type: entity - id: CrateJanitorExplosive - parent: ClosetJanitorBomb - name: janitorial bomb suit crate - description: Supplies a bomb suit for cleaning up any explosive compounds, buy one today! - components: - - type: StorageFill - contents: - - id: ClothingOuterSuitJanitorBomb - amount: 1 - - id: ClothingHeadHelmetJanitorBombSuit - amount: 1 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml index 945ae0dd7b..6d0577a16e 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml @@ -130,7 +130,7 @@ - type: entity id: ClosetJanitorBombFilled parent: ClosetJanitorBomb - suffix: Filled + suffix: DO NOT MAP, Filled components: - type: StorageFill contents: diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 8171ec0053..b294729e07 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -90,6 +90,7 @@ - type: InventorySlots - type: Loadout prototypes: [ MobAghostGear ] + - type: BypassInteractionChecks - type: entity id: ActionAGhostShowSolar diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index 1bd895829b..888e4e4e35 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -1647,6 +1647,9 @@ reagents: - ReagentId: JuiceWatermelon Quantity: 4 + - type: Tag + tags: + - Fruit - type: entity name: grapes diff --git a/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml b/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml index 684260b737..122ff42eb2 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/Instruments/base_instruments.yml @@ -29,7 +29,7 @@ components: - type: Instrument - type: ActivatableUI - allowSpectator: false # otherwise they can play client-side music + blockSpectators: true # otherwise they can play client-side music inHandsOnly: false singleUser: true requireHands: true diff --git a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml index fcdb432dce..860db862ae 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml @@ -108,6 +108,8 @@ - type: DoorBolt - type: AccessReader access: [ [ "Engineering" ] ] + - type: PryUnpowered + pryModifier: 0.5 - type: entity id: Firelock diff --git a/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml b/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml index 05cbb60ed3..3764f13591 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml @@ -15,7 +15,9 @@ mask: - TabletopMachineMask layer: - - TabletopMachineLayer + - Impassable + - MidImpassable + - LowImpassable hard: false - type: ApcPowerReceiver powerLoad: 300 diff --git a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml index d78960030d..fe4eb14518 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml @@ -58,7 +58,6 @@ - TabletopMachineMask layer: - TabletopMachineLayer - hard: false - type: Sprite sprite: Structures/Machines/microwave.rsi drawdepth: SmallObjects diff --git a/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml b/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml index 9894a2691f..d6e7333313 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml @@ -32,7 +32,6 @@ - TabletopMachineMask layer: - TabletopMachineLayer - hard: false - type: Sprite sprite: Structures/Machines/juicer.rsi drawdepth: SmallObjects diff --git a/Resources/migration.yml b/Resources/migration.yml index 71e6b9f49c..028601e0dc 100644 --- a/Resources/migration.yml +++ b/Resources/migration.yml @@ -355,3 +355,6 @@ DrinkBottleGoldschlager: DrinkBottleGildlager # 2024-05-14 soda_dispenser: SodaDispenser chem_master: ChemMaster + +# 2024-05-21 +CrateJanitorExplosive: ClosetJanitorBombFilled diff --git a/RobustToolbox b/RobustToolbox index ec794ce4e4..796abe1230 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit ec794ce4e4693069d3b3ebf7a88ead5ff2f860e0 +Subproject commit 796abe1230554c31daffbfa6559a0a4bcf50b1af