Merge remote-tracking branch 'space-station-14/master' into ed-25-05-2024-upstream

# Conflicts:
#	Content.Server/Atmos/Components/FlammableComponent.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
This commit is contained in:
Ed
2024-05-25 18:49:49 +03:00
123 changed files with 1291 additions and 1076 deletions

View File

@@ -91,7 +91,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
ClearAlerts?.Invoke(this, EventArgs.Empty);
}
public void AlertClicked(AlertType alertType)
public void AlertClicked(ProtoId<AlertPrototype> alertType)
{
RaiseNetworkEvent(new ClickAlertEvent(alertType));
}

View File

@@ -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<Vector2i, GasOverlayChunk> 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;
}

View File

@@ -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<Vector2i, DecalChunk> 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)

View File

@@ -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!;

View File

@@ -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);

View File

@@ -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()

View File

@@ -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<StorageComponent>(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;
}

View File

@@ -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)

View File

@@ -5,9 +5,27 @@ namespace Content.Client.Message;
public static class RichTextLabelExt
{
/// <summary>
/// Sets the labels markup.
/// </summary>
/// <remarks>
/// Invalid markup will cause exceptions to be thrown. Don't use this for user input!
/// </remarks>
public static RichTextLabel SetMarkup(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkup(markup));
return label;
}
/// <summary>
/// Sets the labels markup.<br/>
/// Uses <c>FormatedMessage.FromMarkupPermissive</c> which treats invalid markup as text.
/// </summary>
public static RichTextLabel SetMarkupPermissive(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkupPermissive(markup));
return label;
}
}

View File

@@ -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<Vector2i, int[]> modifiedChunks;
Dictionary<NetEntity, NavMapBeacon> 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;
}

View File

@@ -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<RevenantComponent> ent, ref UpdateAlertSpriteEvent args)
{
if (args.Alert.AlertType != AlertType.Essence)
if (args.Alert.ID != ent.Comp.EssenceAlert)
return;
var sprite = args.SpriteViewEnt.Comp;

View File

@@ -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;

View File

@@ -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<GameplayS
SyncAlerts();
}
private void OnAlertPressed(object? sender, AlertType e)
private void OnAlertPressed(object? sender, ProtoId<AlertPrototype> e)
{
_alertsSystem?.AlertClicked(e);
}

View File

@@ -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<AlertKey, AlertState> alertStates)
public void SyncControls(AlertsSystem alertsSystem,
AlertOrderPrototype? alertOrderPrototype,
IReadOnlyDictionary<AlertKey,
AlertState> 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<AlertType>? AlertPressed;
public event EventHandler<ProtoId<AlertPrototype>>? AlertPressed;
private bool SyncRemoveControls(IReadOnlyDictionary<AlertKey, AlertState> 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);
}
}

View File

@@ -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<IEntityManager>();

View File

@@ -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));
});

View File

@@ -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<IEntityManager>();
var alertsSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<AlertsSystem>();
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<TransformComponent>(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);

View File

@@ -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
/// </summary>
[DataField("vowCooldown")]
public TimeSpan VowCooldown = TimeSpan.FromMinutes(5);
[DataField]
public ProtoId<AlertPrototype> VowAlert = "VowOfSilence";
[DataField]
public ProtoId<AlertPrototype> VowBrokenAlert = "VowBroken";
}
}

View File

@@ -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<MutedComponent>(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<MutedComponent>(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<MutedComponent>(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);
}
}

View File

@@ -40,13 +40,13 @@ namespace Content.Server.Alert.Commands
var alertType = args[0];
var alertsSystem = _e.System<AlertsSystem>();
if (!alertsSystem.TryGet(Enum.Parse<AlertType>(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);
}
}
}

View File

@@ -41,7 +41,7 @@ namespace Content.Server.Alert.Commands
var alertType = args[0];
var severity = args[1];
var alertsSystem = _e.System<AlertsSystem>();
if (!alertsSystem.TryGet(Enum.Parse<AlertType>(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);
}
}
}

View File

@@ -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<AlertPrototype> HighPressureAlert = "HighPressure";
[DataField]
public ProtoId<AlertPrototype> LowPressureAlert = "LowPressure";
[DataField]
public ProtoId<AlertCategoryPrototype> PressureAlertCategory = "Pressure";
}
}

View File

@@ -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<AlertPrototype> FireAlert = "Fire";
/// <summary>
/// CrystallPunk fireplace fuel
/// </summary>

View File

@@ -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;
}
}

View File

@@ -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)
{

View File

@@ -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
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan StatusTime;
[DataField]
public ProtoId<AlertPrototype> BleedingAlert = "Bleed";
}
}

View File

@@ -1,3 +1,6 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
namespace Content.Server.Body.Components
{
/// <summary>
@@ -18,5 +21,8 @@ namespace Content.Server.Body.Components
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(3);
[DataField]
public ProtoId<AlertPrototype> InternalsAlert = "Internals";
}
}

View File

@@ -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.
/// </summary>
[DataField]
public AlertType Alert = AlertType.LowOxygen;
public ProtoId<AlertPrototype> Alert = "LowOxygen";
}

View File

@@ -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;

View File

@@ -144,12 +144,12 @@ public sealed class InternalsSystem : EntitySystem
private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
{
_alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
{
_alerts.ClearAlert(ent, AlertType.Internals);
_alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
}
private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
@@ -159,7 +159,7 @@ public sealed class InternalsSystem : EntitySystem
var gasTank = Comp<GasTankComponent>(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<InternalsComponent> 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<InternalsComponent> 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<InternalsComponent> 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;
}

View File

@@ -10,8 +10,8 @@ public sealed partial class AdjustAlert : ReagentEffect
/// <summary>
/// The specific Alert that will be adjusted
/// </summary>
[DataField("alertType", required: true)]
public AlertType Type;
[DataField(required: true)]
public ProtoId<AlertPrototype> AlertType;
/// <summary>
/// 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<FirelockComponent, GetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, DoorStateChangedEvent>(OnUpdateState);
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
SubscribeLocalEvent<FirelockComponent, AtmosAlarmEvent>(OnAtmosAlarm);
// Visuals
SubscribeLocalEvent<FirelockComponent, MapInitEvent>(UpdateVisuals);
SubscribeLocalEvent<FirelockComponent, ComponentStartup>(UpdateVisuals);
SubscribeLocalEvent<FirelockComponent, PowerChangedEvent>(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<AirtightComponent>();
var appearanceQuery = GetEntityQuery<AppearanceComponent>();
@@ -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<AirtightComponent>();
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<AirtightComponent>();
@@ -234,17 +121,17 @@ namespace Content.Server.Doors.Systems
return (false, false);
}
if (!TryComp(xform.ParentUid, out GridAtmosphereComponent? gridAtmosphere))
if (!HasComp<GridAtmosphereComponent>(xform.ParentUid))
return (false, false);
var grid = Comp<MapGridComponent>(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;

View File

@@ -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);
}
}

View File

@@ -7,31 +7,6 @@ using Robust.Shared.Player;
namespace Content.Server.Interaction
{
/// <summary>
/// Governs interactions during clicking on entities
/// </summary>
[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;
}

View File

@@ -102,20 +102,23 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
/// </summary>
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);
}
}

View File

@@ -141,7 +141,7 @@ public sealed partial class RevenantSystem : EntitySystem
if (TryComp<StoreComponent>(uid, out var store))
_store.UpdateUserInterface(uid, uid, store);
_alerts.ShowAlert(uid, AlertType.Essence);
_alerts.ShowAlert(uid, component.EssenceAlert);
if (component.Essence <= 0)
{

View File

@@ -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);

View File

@@ -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<PowerCellDrawComponent>(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<BorgChassisComponent> 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);
}
/// <summary>

View File

@@ -28,7 +28,9 @@ public sealed class ImmovableRodRule : StationEventSystem<ImmovableRodRuleCompon
if (proto.TryGetComponent<ImmovableRodComponent>(out var rod) && proto.TryGetComponent<TimedDespawnComponent>(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();

View File

@@ -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
/// </summary>
[DataField]
public bool TakingDamage = false;
[DataField]
public ProtoId<AlertPrototype> HotAlert = "Hot";
[DataField]
public ProtoId<AlertPrototype> ColdAlert = "Cold";
}

View File

@@ -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<AlertCategoryPrototype>]
public const string TemperatureAlertCategory = "Temperature";
public override void Initialize()
{
SubscribeLocalEvent<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
@@ -184,13 +188,13 @@ public sealed class TemperatureSystem : EntitySystem
private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args)
{
AlertType type;
ProtoId<AlertPrototype> type;
float threshold;
float idealTemp;
if (!TryComp<TemperatureComponent>(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;
}
}

View File

@@ -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<EntityUid> targets, EntityUid? user, TransformComponent targetXform)

View File

@@ -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<WorldTargetActionComponent> action)

View File

@@ -1,20 +0,0 @@
namespace Content.Shared.Alert;
/// <summary>
/// Every category of alert. Corresponds to category field in alert prototypes defined in YML
/// </summary>
public enum AlertCategory
{
Pressure,
Temperature,
Breathing,
Buckled,
Health,
Internals,
Stamina,
Piloting,
Hunger,
Thirst,
Toxins,
Battery
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Alert;
/// <summary>
/// This is a prototype for a category for marking alerts as mutually exclusive.
/// </summary>
[Prototype]
public sealed partial class AlertCategoryPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
}

View File

@@ -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<AlertPrototype>? AlertType { get; private set; } = default!;
public readonly ProtoId<AlertCategoryPrototype>? 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<AlertPrototype>? alertType, ProtoId<AlertCategoryPrototype>? alertCategory)
{
AlertCategory = alertCategory;
AlertType = alertType;
@@ -49,7 +49,7 @@ public struct AlertKey
/// <param name="category">alert category, must not be null</param>
/// <returns>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.</returns>
public static AlertKey ForCategory(AlertCategory category)
public static AlertKey ForCategory(ProtoId<AlertCategoryPrototype> category)
{
return new(null, category);
}

View File

@@ -7,7 +7,7 @@ namespace Content.Shared.Alert
/// <summary>
/// Defines the order of alerts so they show up in a consistent order.
/// </summary>
[Prototype("alertOrder")]
[Prototype]
[DataDefinition]
public sealed partial class AlertOrderPrototype : IPrototype, IComparer<AlertPrototype>
{
@@ -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<AlertType>(alert)] = i++;
_typeToIdx[alert] = i++;
break;
case "category":
_categoryToIdx[Enum.Parse<AlertCategory>(alert)] = i++;
_categoryToIdx[alert] = i++;
break;
default:
throw new ArgumentException();
@@ -58,17 +58,17 @@ namespace Content.Shared.Alert
}
}
private readonly Dictionary<AlertType, int> _typeToIdx = new();
private readonly Dictionary<AlertCategory, int> _categoryToIdx = new();
private readonly Dictionary<ProtoId<AlertPrototype>, int> _typeToIdx = new();
private readonly Dictionary<ProtoId<AlertCategoryPrototype>, 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;

View File

@@ -1,120 +1,116 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Alert
namespace Content.Shared.Alert;
/// <summary>
/// An alert popup with associated icon, tooltip, and other data.
/// </summary>
[Prototype]
public sealed partial class AlertPrototype : IPrototype
{
/// <summary>
/// An alert popup with associated icon, tooltip, and other data.
/// Type of alert, no 2 alert prototypes should have the same one.
/// </summary>
[Prototype("alert")]
public sealed partial class AlertPrototype : IPrototype
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// 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.
/// </summary>
[DataField(required: true)]
public List<SpriteSpecifier> Icons = new();
/// <summary>
/// An entity used for displaying the <see cref="Icons"/> in the UI control.
/// </summary>
[DataField]
public EntProtoId AlertViewEntity = "AlertSpriteView";
/// <summary>
/// Name to show in tooltip window. Accepts formatting.
/// </summary>
[DataField]
public string Name { get; private set; } = string.Empty;
/// <summary>
/// Description to show in tooltip window. Accepts formatting.
/// </summary>
[DataField]
public string Description { get; private set; } = string.Empty;
/// <summary>
/// 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.
/// </summary>
[DataField]
public ProtoId<AlertCategoryPrototype>? Category { get; private set; }
/// <summary>
/// Key which is unique w.r.t category semantics (alerts with same category have equal keys,
/// alerts with no category have different keys).
/// </summary>
public AlertKey AlertKey => new(ID, Category);
/// <summary>
/// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state.
/// </summary>
public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity;
[DataField("minSeverity")] private short _minSeverity = 1;
/// <summary>
/// Maximum severity level supported by this state. -1 (default) indicates
/// no severity levels are supported by the state.
/// </summary>
[DataField]
public short MaxSeverity = -1;
/// <summary>
/// Indicates whether this state support severity levels
/// </summary>
public bool SupportsSeverity => MaxSeverity != -1;
/// <summary>
/// Defines what to do when the alert is clicked.
/// This will always be null on clientside.
/// </summary>
[DataField(serverOnly: true)]
public IAlertClick? OnClick { get; private set; }
/// <param name="severity">severity level, if supported by this alert</param>
/// <returns>the icon path to the texture for the provided severity level</returns>
public SpriteSpecifier GetIcon(short? severity = null)
{
[ViewVariables]
string IPrototype.ID => AlertType.ToString();
var minIcons = SupportsSeverity
? MaxSeverity - MinSeverity
: 1;
/// <summary>
/// Type of alert, no 2 alert prototypes should have the same one.
/// </summary>
[IdDataField]
public AlertType AlertType { get; private set; }
if (Icons.Count < minIcons)
throw new InvalidOperationException($"Insufficient number of icons given for alert {ID}");
/// <summary>
/// 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.
/// </summary>
[DataField("icons", required: true)]
public List<SpriteSpecifier> Icons = new();
if (!SupportsSeverity)
return Icons[0];
/// <summary>
/// An entity used for displaying the <see cref="Icons"/> in the UI control.
/// </summary>
[DataField]
public EntProtoId AlertViewEntity = "AlertSpriteView";
/// <summary>
/// Name to show in tooltip window. Accepts formatting.
/// </summary>
[DataField("name")]
public string Name { get; private set; } = "";
/// <summary>
/// Description to show in tooltip window. Accepts formatting.
/// </summary>
[DataField("description")]
public string Description { get; private set; } = "";
/// <summary>
/// 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.
/// </summary>
[DataField("category")]
public AlertCategory? Category { get; private set; }
/// <summary>
/// Key which is unique w.r.t category semantics (alerts with same category have equal keys,
/// alerts with no category have different keys).
/// </summary>
public AlertKey AlertKey => new(AlertType, Category);
/// <summary>
/// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state.
/// </summary>
public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity;
[DataField("minSeverity")] private short _minSeverity = 1;
/// <summary>
/// Maximum severity level supported by this state. -1 (default) indicates
/// no severity levels are supported by the state.
/// </summary>
[DataField("maxSeverity")]
public short MaxSeverity = -1;
/// <summary>
/// Indicates whether this state support severity levels
/// </summary>
public bool SupportsSeverity => MaxSeverity != -1;
/// <summary>
/// Defines what to do when the alert is clicked.
/// This will always be null on clientside.
/// </summary>
[DataField("onClick", serverOnly: true)]
public IAlertClick? OnClick { get; private set; }
/// <param name="severity">severity level, if supported by this alert</param>
/// <returns>the icon path to the texture for the provided severity level</returns>
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];
}
}

View File

@@ -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<AlertPrototype> Type;
}

View File

@@ -1,59 +0,0 @@
namespace Content.Shared.Alert
{
/// <summary>
/// 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
/// </summary>
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
}
}

View File

@@ -11,7 +11,7 @@ public abstract class AlertsSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private FrozenDictionary<AlertType, AlertPrototype> _typeToAlert = default!;
private FrozenDictionary<ProtoId<AlertPrototype>, AlertPrototype> _typeToAlert = default!;
public IReadOnlyDictionary<AlertKey, AlertState>? GetActiveAlerts(EntityUid euid)
{
@@ -20,23 +20,23 @@ public abstract class AlertsSystem : EntitySystem
: null;
}
public short GetSeverityRange(AlertType alertType)
public short GetSeverityRange(ProtoId<AlertPrototype> alertType)
{
var minSeverity = _typeToAlert[alertType].MinSeverity;
return (short)MathF.Max(minSeverity,_typeToAlert[alertType].MaxSeverity - minSeverity);
}
public short GetMaxSeverity(AlertType alertType)
public short GetMaxSeverity(ProtoId<AlertPrototype> alertType)
{
return _typeToAlert[alertType].MaxSeverity;
}
public short GetMinSeverity(AlertType alertType)
public short GetMinSeverity(ProtoId<AlertPrototype> alertType)
{
return _typeToAlert[alertType].MinSeverity;
}
public bool IsShowingAlert(EntityUid euid, AlertType alertType)
public bool IsShowingAlert(EntityUid euid, ProtoId<AlertPrototype> alertType)
{
if (!EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent))
return false;
@@ -51,7 +51,7 @@ public abstract class AlertsSystem : EntitySystem
}
/// <returns>true iff an alert of the indicated alert category is currently showing</returns>
public bool IsShowingAlertCategory(EntityUid euid, AlertCategory alertCategory)
public bool IsShowingAlertCategory(EntityUid euid, ProtoId<AlertCategoryPrototype> 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)</param>
/// <param name="autoRemove">if true, the alert will be removed at the end of the cooldown</param>
/// <param name="showCooldown">if true, the cooldown will be visibly shown over the alert icon</param>
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<AlertPrototype> 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
/// <summary>
/// Clear the alert with the given category, if one is currently showing.
/// </summary>
public void ClearAlertCategory(EntityUid euid, AlertCategory category)
public void ClearAlertCategory(EntityUid euid, ProtoId<AlertCategoryPrototype> category)
{
if(!TryComp(euid, out AlertsComponent? alertsComponent))
return;
@@ -150,7 +150,7 @@ public abstract class AlertsSystem : EntitySystem
/// <summary>
/// Clear the alert of the given type if it is currently showing.
/// </summary>
public void ClearAlert(EntityUid euid, AlertType alertType)
public void ClearAlert(EntityUid euid, ProtoId<AlertPrototype> alertType)
{
if (_timing.ApplyingState)
return;
@@ -286,13 +286,13 @@ public abstract class AlertsSystem : EntitySystem
protected virtual void LoadPrototypes()
{
var dict = new Dictionary<AlertType, AlertPrototype>();
var dict = new Dictionary<ProtoId<AlertPrototype>, AlertPrototype>();
foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
{
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
/// </summary>
/// <returns>true if found</returns>
public bool TryGet(AlertType alertType, [NotNullWhen(true)] out AlertPrototype? alert)
public bool TryGet(ProtoId<AlertPrototype> alertType, [NotNullWhen(true)] out AlertPrototype? alert)
{
return _typeToAlert.TryGetValue(alertType, out alert);
}

View File

@@ -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<AlertPrototype> Type;
public ClickAlertEvent(AlertType alertType)
public ClickAlertEvent(ProtoId<AlertPrototype> alertType)
{
Type = alertType;
}

View File

@@ -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<Vector2i, GasOverlayChunk> chunks) : ComponentState
{
public readonly Dictionary<Vector2i, GasOverlayChunk> Chunks = chunks;
}
[Serializable, NetSerializable]
public sealed class GasTileOverlayState : ComponentState, IComponentDeltaState
public sealed class GasTileOverlayDeltaState(
Dictionary<Vector2i, GasOverlayChunk> modifiedChunks,
HashSet<Vector2i> allChunks)
: ComponentState, IComponentDeltaState<GasTileOverlayState>
{
public readonly Dictionary<Vector2i, GasOverlayChunk> Chunks;
public bool FullState => AllChunks == null;
public readonly Dictionary<Vector2i, GasOverlayChunk> ModifiedChunks = modifiedChunks;
public readonly HashSet<Vector2i> AllChunks = allChunks;
// required to infer deleted/missing chunks for delta states
public HashSet<Vector2i>? AllChunks;
public GasTileOverlayState(Dictionary<Vector2i, GasOverlayChunk> 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<Vector2i, GasOverlayChunk>(AllChunks.Count);
var chunks = new Dictionary<Vector2i, GasOverlayChunk>(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));
}

View File

@@ -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)

View File

@@ -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
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public AlertType BuckledAlertType = AlertType.Buckled;
public ProtoId<AlertPrototype> BuckledAlertType = "Buckled";
/// <summary>
/// The sum of the sizes of all the buckled entities in this strap

View File

@@ -40,6 +40,9 @@ public abstract partial class SharedBuckleSystem
SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
}
[ValidatePrototypeId<AlertCategoryPrototype>]
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);
}
}

View File

@@ -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<AlertPrototype> MagbootsAlert = "Magboots";
}

View File

@@ -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<PacifiedComponent> ent, ref BeforeThrowEvent args)

View File

@@ -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<AlertPrototype> PacifiedAlert = "Pacified";
}

View File

@@ -197,6 +197,7 @@ namespace Content.Shared.Containers.ItemSlots
if (!EntityManager.TryGetComponent(args.User, out HandsComponent? hands))
return;
var slots = new List<ItemSlot>();
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;
}
/// <summary>
/// Tries to insert an item into any empty slot.
/// </summary>
/// <param name="ent">The entity that has the item slots.</param>
/// <param name="item">The item to be inserted.</param>
/// <param name="user">The entity performing the interaction.</param>
/// <param name="excludeUserAudio">
/// If true, will exclude the user when playing sound. Does nothing client-side.
/// Useful for predicted interactions
/// </param>
/// <returns>False if failed to insert item</returns>
public bool TryInsertEmpty(Entity<ItemSlotsComponent?> ent, EntityUid item, EntityUid? user, bool excludeUserAudio = false)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
var slots = new List<ItemSlot>();
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

View File

@@ -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
/// </summary>
[DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)]
public bool CanStillInteract = true;
[DataField]
public ProtoId<AlertPrototype> CuffedAlert = "Handcuffed";
}
[Serializable, NetSerializable]

View File

@@ -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);

View File

@@ -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<AlertPrototype> StaminaAlert = "Stamina";
}

View File

@@ -79,8 +79,7 @@ public sealed partial class StaminaSystem : EntitySystem
{
RemCompDeferred<ActiveStaminaComponent>(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);
}
/// <summary>

View File

@@ -62,46 +62,37 @@ namespace Content.Shared.Decals
}
[Serializable, NetSerializable]
public sealed class DecalGridState : ComponentState, IComponentDeltaState
public sealed class DecalGridState(Dictionary<Vector2i, DecalChunk> chunks) : ComponentState
{
public Dictionary<Vector2i, DecalChunk> Chunks;
public bool FullState => AllChunks == null;
public Dictionary<Vector2i, DecalChunk> Chunks = chunks;
}
// required to infer deleted/missing chunks for delta states
public HashSet<Vector2i>? AllChunks;
[Serializable, NetSerializable]
public sealed class DecalGridDeltaState(Dictionary<Vector2i, DecalChunk> modifiedChunks, HashSet<Vector2i> allChunks)
: ComponentState, IComponentDeltaState<DecalGridState>
{
public Dictionary<Vector2i, DecalChunk> ModifiedChunks = modifiedChunks;
public HashSet<Vector2i> AllChunks = allChunks;
public DecalGridState(Dictionary<Vector2i, DecalChunk> 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<Vector2i, DecalChunk>(state.Chunks.Count);
foreach (var (chunk, data) in Chunks)
foreach (var (chunk, data) in ModifiedChunks)
{
chunks[chunk] = new(data);
}

View File

@@ -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)

View File

@@ -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..
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FirelockComponent : Component
{
#region Settings
/// <summary>
/// Pry time modifier to be used when the firelock is currently closed due to fire or pressure.
/// </summary>
@@ -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);
/// <summary>
/// Maximum pressure difference before the firelock will refuse to open, in kPa.
/// </summary>
@@ -39,5 +39,47 @@ namespace Content.Shared.Doors.Components
/// </summary>
[DataField("alarmAutoClose"), ViewVariables(VVAccess.ReadWrite)]
public bool AlarmAutoClose = true;
/// <summary>
/// The cooldown duration before a firelock can automatically close due to a hazardous environment after it has
/// been pried open. Measured in seconds.
/// </summary>
[DataField]
public TimeSpan EmergencyCloseCooldownDuration = TimeSpan.FromSeconds(2);
#endregion
#region Set by system
/// <summary>
/// When the firelock will be allowed to automatically close again due to a hazardous environment.
/// </summary>
[DataField]
public TimeSpan? EmergencyCloseCooldown;
/// <summary>
/// Whether the firelock can open, or is locked due to its environment.
/// </summary>
public bool IsLocked => Pressure || Temperature;
/// <summary>
/// Whether the firelock is holding back a hazardous pressure.
/// </summary>
[DataField, AutoNetworkedField]
public bool Pressure;
/// <summary>
/// Whether the firelock is holding back extreme temperatures.
/// </summary>
[DataField, AutoNetworkedField]
public bool Temperature;
/// <summary>
/// Whether the airlock is powered.
/// </summary>
[DataField, AutoNetworkedField]
public bool Powered;
#endregion
}
}

View File

@@ -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;

View File

@@ -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<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<FirelockComponent, GetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, PriedEvent>(OnAfterPried);
// Visuals
SubscribeLocalEvent<FirelockComponent, MapInitEvent>(UpdateVisuals);
SubscribeLocalEvent<FirelockComponent, ComponentStartup>(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
}

View File

@@ -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<AlertPrototype> EnsnaredAlert = "Ensnared";
}
[Serializable, NetSerializable]

View File

@@ -17,6 +17,9 @@ namespace Content.Shared.Gravity
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<AlertPrototype>]
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);
}
}

View File

@@ -0,0 +1,6 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Interaction.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class BypassInteractionChecksComponent : Component;

View File

@@ -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<IgnoreUIRangeComponent> _ignoreUiRangeQuery;
private EntityQuery<FixturesComponent> _fixtureQuery;
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<HandsComponent> _handsQuery;
private EntityQuery<InteractionRelayComponent> _relayQuery;
private EntityQuery<CombatModeComponent> _combatQuery;
private EntityQuery<WallMountComponent> _wallMountQuery;
private EntityQuery<UseDelayComponent> _delayQuery;
private EntityQuery<ActivatableUIComponent> _uiQuery;
private const CollisionGroup InRangeUnobstructedMask = CollisionGroup.Impassable | CollisionGroup.InteractImpassable;
@@ -76,6 +88,17 @@ namespace Content.Shared.Interaction
public override void Initialize()
{
_ignoreUiRangeQuery = GetEntityQuery<IgnoreUIRangeComponent>();
_fixtureQuery = GetEntityQuery<FixturesComponent>();
_itemQuery = GetEntityQuery<ItemComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_handsQuery = GetEntityQuery<HandsComponent>();
_relayQuery = GetEntityQuery<InteractionRelayComponent>();
_combatQuery = GetEntityQuery<CombatModeComponent>();
_wallMountQuery = GetEntityQuery<WallMountComponent>();
_delayQuery = GetEntityQuery<UseDelayComponent>();
_uiQuery = GetEntityQuery<ActivatableUIComponent>();
SubscribeLocalEvent<BoundUserInterfaceCheckRangeEvent>(HandleUserInterfaceRangeCheck);
SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
@@ -111,29 +134,57 @@ namespace Content.Shared.Interaction
/// </summary>
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<GhostComponent>(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<TransformComponent?> user, Entity<TransformComponent?> 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);
}
/// <summary>
@@ -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<HandsComponent>(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<ItemComponent>(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<InteractionRelayComponent>(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<CombatModeComponent>(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<HandsComponent>(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<TransformComponent?> origin,
Entity<TransformComponent?> 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.
/// </returns>
public bool InRangeUnobstructed(
EntityUid origin,
EntityUid other,
Entity<TransformComponent?> origin,
Entity<TransformComponent?> 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<FixturesComponent>(origin, out var fixtureA) &&
_fixtureQuery.TryComp(origin, out var fixtureA) &&
// These fixture counts are stuff that has the component but no fixtures for <reasons> (e.g. buttons).
// At least until they get removed.
fixtureA.FixtureCount > 0 &&
TryComp<FixturesComponent>(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<EntityUid> ignored = new();
if (HasComp<ItemComponent>(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<HandsComponent>(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
/// <summary>
/// Check if a user can access a target (stored in the same containers) and is in range without obstructions.
/// </summary>
public bool InRangeAndAccessible(
Entity<TransformComponent?> user,
Entity<TransformComponent?> 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);
}
/// <summary>
/// Check if a user can access a target or if they are stored in different containers.
/// </summary>
public bool IsAccessible(Entity<TransformComponent?> user, Entity<TransformComponent?> target)
{
if (_containerSystem.IsInSameOrParentContainer(user, target, out _, out var container))
return true;
return container != null && CanAccessViaStorage(user, target, container);
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <inheritdoc cref="CanAccessViaStorage(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid)"/>
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);
}
/// <summary>
/// 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;
}
}

View File

@@ -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

View File

@@ -75,7 +75,7 @@ public partial class InventorySystem : EntitySystem
if (TryGetSlotEntity(uid, ev.Slot, out var entityUid) && TryComp<StorageComponent>(entityUid, out var storageComponent))
{
_storageSystem.OpenStorageUI(entityUid.Value, uid, storageComponent);
_storageSystem.OpenStorageUI(entityUid.Value, uid, storageComponent, false);
}
}

View File

@@ -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<LockComponent, LockDoAfter>(OnDoAfterLock);
SubscribeLocalEvent<LockComponent, UnlockDoAfter>(OnDoAfterUnlock);
SubscribeLocalEvent<LockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened); //CrystallPunk Lock System Adapt
SubscribeLocalEvent<LockComponent, StorageInteractAttemptEvent>(OnStorageInteractAttempt);
SubscribeLocalEvent<LockedWiresPanelComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel);
SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(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<LockComponent> ent, ref StorageInteractAttemptEvent args)
{
if (ent.Comp.Locked)
args.Cancelled = true;
}
private void OnLockToggleAttempt(Entity<LockedWiresPanelComponent> ent, ref LockToggleAttemptEvent args)
{
if (args.Cancelled)

View File

@@ -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<MagicMirrorComponent> ent, ref BeforeActivatableUIOpenEvent args)

View File

@@ -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)
/// </summary>
[DataField("stateAlertDict")]
public Dictionary<MobState, AlertType> StateAlertDict = new()
public Dictionary<MobState, ProtoId<AlertPrototype>> 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<AlertCategoryPrototype> HealthAlertCategory = "Health";
/// <summary>
/// Whether or not this entity should display damage overlays (robots don't feel pain, black out etc.)
/// </summary>
@@ -53,19 +57,19 @@ public sealed class MobThresholdsComponentState : ComponentState
public MobState CurrentThresholdState;
public Dictionary<MobState, AlertType> StateAlertDict = new()
{
{MobState.Alive, AlertType.HumanHealth},
{MobState.Critical, AlertType.HumanCrit},
{MobState.Dead, AlertType.HumanDead},
};
public Dictionary<MobState, ProtoId<AlertPrototype>> StateAlertDict;
public bool ShowOverlays;
public bool AllowRevives;
public MobThresholdsComponentState(Dictionary<FixedPoint2, MobState> unsortedThresholds, bool triggersAlerts, MobState currentThresholdState,
Dictionary<MobState, AlertType> stateAlertDict, bool showOverlays, bool allowRevives)
public MobThresholdsComponentState(Dictionary<FixedPoint2, MobState> unsortedThresholds,
bool triggersAlerts,
MobState currentThresholdState,
Dictionary<MobState,
ProtoId<AlertPrototype>> stateAlertDict,
bool showOverlays,
bool allowRevives)
{
UnsortedThresholds = unsortedThresholds;
TriggersAlerts = triggersAlerts;

View File

@@ -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)

View File

@@ -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<AlertPrototype> PulledAlert = "Pulled";
}

View File

@@ -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
/// </summary>
[DataField]
public bool NeedsHands = true;
[DataField]
public ProtoId<AlertPrototype> PullingAlert = "Pulling";
}

View File

@@ -223,7 +223,7 @@ public sealed class PullingSystem : EntitySystem
if (TryComp<PullerComponent>(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<PullableComponent?> 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<PullableComponent>(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);

View File

@@ -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
/// </summary>
[DataField]
public EntProtoId SpiderChargeObjective = "SpiderChargeObjective";
[DataField]
public ProtoId<AlertPrototype> SuitPowerAlert = "SuitPower";
}

View File

@@ -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
/// <summary>
/// A dictionary relating hunger thresholds to corresponding alerts.
/// </summary>
[DataField("hungerThresholdAlerts", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, AlertType>))]
[DataField("hungerThresholdAlerts")]
[AutoNetworkedField]
public Dictionary<HungerThreshold, AlertType> HungerThresholdAlerts = new()
public Dictionary<HungerThreshold, ProtoId<AlertPrototype>> 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<AlertCategoryPrototype> HungerAlertCategory = "Hunger";
/// <summary>
/// A dictionary relating HungerThreshold to how much they modify <see cref="BaseDecayRate"/>.
/// </summary>

View File

@@ -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<ThirstThreshold, AlertType> ThirstThresholdAlertTypes = new()
[DataField]
public ProtoId<AlertCategoryPrototype> ThirstyCategory = "Thirst";
public static readonly Dictionary<ThirstThreshold, ProtoId<AlertPrototype>> ThirstThresholdAlertTypes = new()
{
{ThirstThreshold.Thirsty, AlertType.Thirsty},
{ThirstThreshold.Parched, AlertType.Parched},
{ThirstThreshold.Dead, AlertType.Parched},
{ThirstThreshold.Thirsty, "Thirsty"},
{ThirstThreshold.Parched, "Parched"},
{ThirstThreshold.Dead, "Parched"},
};
}

View File

@@ -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))

View File

@@ -165,7 +165,7 @@ public sealed class ThirstSystem : EntitySystem
}
else
{
_alerts.ClearAlertCategory(uid, AlertCategory.Thirst);
_alerts.ClearAlertCategory(uid, component.ThirstyCategory);
}
switch (component.CurrentThirstThreshold)

View File

@@ -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<Vector2i, int[]> chunks,
Dictionary<NetEntity, NavMapBeacon> beacons)
: ComponentState, IComponentDeltaState
: ComponentState
{
public Dictionary<Vector2i, int[]> Chunks = chunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
}
// Required to infer deleted/missing chunks for delta states
public HashSet<Vector2i>? AllChunks;
[Serializable, NetSerializable]
protected sealed class NavMapDeltaState(
Dictionary<Vector2i, int[]> modifiedChunks,
Dictionary<NetEntity, NavMapBeacon> beacons,
HashSet<Vector2i> allChunks)
: ComponentState, IComponentDeltaState<NavMapState>
{
public Dictionary<Vector2i, int[]> ModifiedChunks = modifiedChunks;
public Dictionary<NetEntity, NavMapBeacon> Beacons = beacons;
public HashSet<Vector2i> 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<Vector2i, int[]>(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));
}
}

View File

@@ -8,4 +8,6 @@ namespace Content.Shared.Prying.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class PryUnpoweredComponent : Component
{
[DataField]
public float PryModifier = 0.1f;
}

View File

@@ -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<PryUnpoweredComponent>(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<PryingComponent>(user)?.SpeedModifier ?? 0.1f;
var modifier = CompOrNull<PryingComponent>(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<PryUnpoweredComponent>(target, out _))
if (!Resolve(target, ref unpoweredComp))
{
message = null;
return false;

View File

@@ -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<AlertPrototype> EssenceAlert = "Essence";
#region Visualizer
[DataField("state")]
public string State = "idle";

View File

@@ -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<AlertPrototype> PilotingAlert = "PilotingShuttle";
public override bool SendOnlyToOwner => true;
}
}

View File

@@ -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<AlertPrototype> BatteryAlert = "BorgBattery";
[DataField]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
}
[Serializable, NetSerializable]

View File

@@ -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<AlertPrototype>? Alert { get; private set; }
/// <summary>
/// Whether a status effect should be able to apply to any entity,

View File

@@ -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.
/// </remarks>
private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, AlertType alert, StatusEffectsComponent status)
private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, ProtoId<AlertPrototype> alert, StatusEffectsComponent status)
{
(TimeSpan, TimeSpan)? maxCooldown = null;
foreach (var kvp in status.ActiveEffects)

View File

@@ -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<ActivationVerb> args)
{
var silent = false;
if (!args.CanAccess || !args.CanInteract || TryComp<LockComponent>(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<GhostComponent>(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
/// </summary>
/// <param name="entity">The entity to open the UI for</param>
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<UseDelayComponent>(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
/// <returns>true if inserted, false otherwise</returns>
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<PlaceableSurfaceComponent>(uid))
@@ -373,7 +366,7 @@ public abstract class SharedStorageSystem : EntitySystem
/// </summary>
private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args)
{
if (args.Handled || TryComp<LockComponent>(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
/// <returns>true if inserted, false otherwise</returns>
public bool PlayerInsertEntityInWorld(Entity<StorageComponent?> 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
}
/// <summary>
/// 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.
/// </summary>
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<StorageComponent> storage, bool canInteract = true, bool silent = true)
{
if (HasComp<BypassInteractionChecksComponent>(user))
return true;
if (!canInteract)
return false;
var ev = new StorageInteractAttemptEvent(silent);
RaiseLocalEvent(storage, ref ev);
return !ev.Cancelled;
}
/// <summary>
/// Plays a clientside pickup animation for the specified uid.
/// </summary>

View File

@@ -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

View File

@@ -57,7 +57,7 @@ namespace Content.Shared.UserInterface
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool AllowSpectator = true;
public bool BlockSpectators;
/// <summary>
/// Whether the item must be in the user's currently selected/active hand.

Some files were not shown because too many files have changed in this diff Show More