Merge remote-tracking branch 'space-wizards/master'

# Conflicts:
#	Content.Server/Access/Systems/AgentIDCardSystem.cs
#	Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs
#	Content.Server/GameTicking/Rules/ThiefRuleSystem.cs
#	Content.Server/Medical/SuitSensors/SuitSensorSystem.cs
#	Content.Server/Zombies/ZombieSystem.cs
#	Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
#	Content.Shared/Standing/StandingStateComponent.cs
#	Content.Shared/Standing/StandingStateSystem.cs
#	Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs
#	Content.Shared/Stunnable/SharedStunSystem.cs
#	Resources/Locale/en-US/_strings/game-ticking/game-presets/preset-changeling.ftl
#	Resources/Locale/en-US/_strings/ghost/observer-role.ftl
#	Resources/Locale/en-US/_strings/robotics/borg_modules.ftl
#	Resources/Prototypes/Entities/Clothing/Ears/specific.yml
#	Resources/Prototypes/Entities/Clothing/Eyes/specific.yml
#	Resources/Prototypes/Entities/Clothing/Hands/specific.yml
#	Resources/Prototypes/Entities/Clothing/Head/specific.yml
#	Resources/Prototypes/Entities/Clothing/Neck/specific.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml
#	Resources/Prototypes/Entities/Clothing/Shoes/specific.yml
#	Resources/Prototypes/Entities/Clothing/Uniforms/specific.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml
#	Resources/Prototypes/Entities/Objects/Materials/parts.yml
#	Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
This commit is contained in:
Vigers Ray 2025-08-13 14:13:30 +03:00
commit 3cf56cd5b3
217 changed files with 3422 additions and 2454 deletions

View file

@ -21,7 +21,7 @@ namespace Content.Client.Actions.UI
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null)
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
@ -52,17 +52,6 @@ namespace Content.Client.Actions.UI
vbox.AddChild(description);
}
if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString()))
{
var chargesLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipActionCharges }
};
chargesLabel.SetMessage(charges);
vbox.AddChild(chargesLabel);
}
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,

View file

@ -1,36 +1,46 @@
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Client.GameObjects;
namespace Content.Client.Light.Visualizers;
namespace Content.Client.Light.EntitySystems;
public sealed class LightBulbSystem : VisualizerSystem<LightBulbComponent>
public sealed class LightBulbSystem : SharedLightBulbSystem
{
protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LightBulbComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
// update sprite state
if (AppearanceSystem.TryGetData<LightBulbState>(uid, LightBulbVisuals.State, out var state, args.Component))
if (_appearance.TryGetData<LightBulbState>(uid, LightBulbVisuals.State, out var state, args.Component))
{
switch (state)
{
case LightBulbState.Normal:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
break;
case LightBulbState.Broken:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
break;
case LightBulbState.Burned:
SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
_sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
break;
}
}
// also update sprites color
if (AppearanceSystem.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
if (_appearance.TryGetData<Color>(uid, LightBulbVisuals.Color, out var color, args.Component))
{
SpriteSystem.SetColor((uid, args.Sprite), color);
_sprite.SetColor((uid, args.Sprite), color);
}
}
}

View file

@ -0,0 +1,5 @@
using Content.Shared.Light.EntitySystems;
namespace Content.Client.Light.EntitySystems;
public sealed class PoweredLightSystem : SharedPoweredLightSystem;

View file

@ -0,0 +1,5 @@
using Content.Shared.Medical.SuitSensors;
namespace Content.Client.Medical.SuitSensors;
public sealed class SuitSensorSystem : SharedSuitSensorSystem;

View file

@ -5,4 +5,5 @@ namespace Content.Client.Power.Components;
[RegisterComponent]
public sealed partial class ApcPowerReceiverComponent : SharedApcPowerReceiverComponent
{
public override float Load { get; set; }
}

View file

@ -28,6 +28,8 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
public EntityUid Entity;
private bool _allowBorgControl = true;
public RoboticsConsoleWindow()
{
RobustXamlLoader.Load(this);
@ -72,6 +74,7 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
public void UpdateState(RoboticsConsoleState state)
{
_cyborgs = state.Cyborgs;
_allowBorgControl = state.AllowBorgControl;
// clear invalid selection
if (_selected is {} selected && !_cyborgs.ContainsKey(selected))
@ -85,8 +88,8 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
PopulateData();
var locked = _lock.IsLocked(Entity);
DangerZone.Visible = !locked;
LockedMessage.Visible = locked;
DangerZone.Visible = !locked && _allowBorgControl;
LockedMessage.Visible = locked && _allowBorgControl; // Only show if locked AND control is allowed
}
private void PopulateCyborgs()
@ -120,11 +123,19 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
BorgSprite.Texture = _sprite.Frame0(data.ChassisSprite!);
var batteryColor = data.Charge switch {
< 0.2f => "red",
< 0.4f => "orange",
< 0.6f => "yellow",
< 0.8f => "green",
_ => "blue"
< 0.2f => "#FF6C7F", // red
< 0.4f => "#EF973C", // orange
< 0.6f => "#E8CB2D", // yellow
< 0.8f => "#30CC19", // green
_ => "#00D3B8" // cyan
};
var hpPercentColor = data.HpPercent switch {
< 0.2f => "#FF6C7F", // red
< 0.4f => "#EF973C", // orange
< 0.6f => "#E8CB2D", // yellow
< 0.8f => "#30CC19", // green
_ => "#00D3B8" // cyan
};
var text = new FormattedMessage();
@ -132,12 +143,14 @@ public sealed partial class RoboticsConsoleWindow : FancyWindow
text.AddMarkupOrThrow(Loc.GetString("robotics-console-designation"));
text.AddText($" {data.Name}\n"); // prevent players trolling by naming borg [color=red]satan[/color]
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-battery", ("charge", (int)(data.Charge * 100f)), ("color", batteryColor))}\n");
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-hp", ("hp", (int)(data.HpPercent * 100f)), ("color", hpPercentColor))}\n");
text.AddMarkupOrThrow($"{Loc.GetString("robotics-console-brain", ("brain", data.HasBrain))}\n");
text.AddMarkupOrThrow(Loc.GetString("robotics-console-modules", ("count", data.ModuleCount)));
BorgInfo.SetMessage(text);
// how the turntables
DisableButton.Disabled = !(data.HasBrain && data.CanDisable);
DisableButton.Disabled = !_allowBorgControl || !(data.HasBrain && data.CanDisable);
DestroyButton.Disabled = !_allowBorgControl;
}
protected override void FrameUpdate(FrameEventArgs args)

View file

@ -40,6 +40,8 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
private readonly HashSet<DockingPortState> _drawnDocks = new();
private readonly Dictionary<DockingPortState, Button> _dockButtons = new();
private readonly Color _fallbackHighlightedColor = Color.Magenta;
/// <summary>
/// Store buttons for every other dock
/// </summary>
@ -213,11 +215,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (HighlightedDock == dock.Entity)
{
otherDockColor = Color.ToSrgb(Color.Magenta);
otherDockColor = Color.ToSrgb(dock.HighlightedColor);
}
else
{
otherDockColor = Color.ToSrgb(Color.Purple);
otherDockColor = Color.ToSrgb(dock.Color);
}
/*
@ -311,7 +313,7 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
ScalePosition(Vector2.Transform(new Vector2(-0.5f, 0.5f), rotation)),
ScalePosition(Vector2.Transform(new Vector2(0.5f, -0.5f), rotation)));
var dockColor = Color.Magenta;
var dockColor = _viewedState?.HighlightedColor ?? _fallbackHighlightedColor;
var connectionColor = Color.Pink;
handle.DrawRect(ourDockConnection, connectionColor.WithAlpha(0.2f));

View file

@ -308,7 +308,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
-dockRadius * UIScale,
(Size.X + dockRadius) * UIScale,
(Size.Y + dockRadius) * UIScale);
if (_docks.TryGetValue(nent, out var docks))
{
foreach (var state in docks)
@ -321,7 +321,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
continue;
}
var color = Color.ToSrgb(Color.Magenta);
var color = Color.ToSrgb(state.HighlightedColor);
var verts = new[]
{

View file

@ -28,22 +28,8 @@ namespace Content.Client.Stack
base.SetCount(uid, amount, component);
if (component.Lingering &&
TryComp<SpriteComponent>(uid, out var sprite))
{
// tint the stack gray and make it transparent if it's lingering.
var color = component.Count == 0 && component.Lingering
? Color.DarkGray.WithAlpha(0.65f)
: Color.White;
for (var i = 0; i < sprite.AllLayers.Count(); i++)
{
_sprite.LayerSetColor((uid, sprite), i, color);
}
}
// TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call.
if (component.Count <= 0 && !component.Lingering)
if (component.Count <= 0)
{
Xform.DetachEntity(uid, Transform(uid));
return;

View file

@ -1,9 +1,11 @@
using System.Numerics;
using Content.Client.Cooldown;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls
{
@ -20,6 +22,7 @@ namespace Content.Client.UserInterface.Controls
public CooldownGraphic CooldownDisplay { get; }
private SpriteView SpriteView { get; }
private EntityPrototypeView ProtoView { get; }
public EntityUid? Entity => SpriteView.Entity;
@ -141,6 +144,13 @@ namespace Content.Client.UserInterface.Controls
SetSize = new Vector2(DefaultButtonSize, DefaultButtonSize),
OverrideDirection = Direction.South
});
AddChild(ProtoView = new EntityPrototypeView
{
Visible = false,
Scale = new Vector2(2, 2),
SetSize = new Vector2(DefaultButtonSize, DefaultButtonSize),
OverrideDirection = Direction.South
});
AddChild(HoverSpriteView = new SpriteView
{
@ -209,12 +219,35 @@ namespace Content.Client.UserInterface.Controls
HoverSpriteView.SetEntity(null);
}
/// <summary>
/// Causes the control to display a placeholder prototype, optionally faded
/// </summary>
public void SetEntity(EntityUid? ent)
{
SpriteView.SetEntity(ent);
SpriteView.Visible = true;
ProtoView.Visible = false;
UpdateButtonTexture();
}
/// <summary>
/// Causes the control to display a placeholder prototype, optionally faded
/// </summary>
public void SetPrototype(EntProtoId? proto, bool fade)
{
ProtoView.SetPrototype(proto);
SpriteView.Visible = false;
ProtoView.Visible = true;
UpdateButtonTexture();
if (ProtoView.Entity is not { } ent || !fade)
return;
var sprites = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
sprites.SetColor((ent.Owner, ent.Comp1), Color.DarkGray.WithAlpha(0.65f));
}
private void UpdateButtonTexture()
{
var fullTexture = Theme.ResolveTextureOrNull(_fullButtonTexturePath);

View file

@ -3,12 +3,12 @@ using Content.Client.Actions;
using Content.Client.Actions.UI;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Examine;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
@ -23,9 +23,9 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls;
public sealed class ActionButton : Control, IEntityControl
{
private IEntityManager _entities;
private IPlayerManager _player;
private SpriteSystem? _spriteSys;
private ActionUIController? _controller;
private SharedChargesSystem _sharedChargesSys;
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
@ -67,8 +67,8 @@ public sealed class ActionButton : Control, IEntityControl
// TODO why is this constructor so slooooow. The rest of the code is fine
_entities = entities;
_player = IoCManager.Resolve<IPlayerManager>();
_spriteSys = spriteSys;
_sharedChargesSys = _entities.System<SharedChargesSystem>();
_controller = controller;
MouseFilter = MouseFilterMode.Pass;
@ -197,23 +197,17 @@ public sealed class ActionButton : Control, IEntityControl
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
FormattedMessage? chargesText = null;
var desc = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
// TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
if (_entities.TryGetComponent(Action, out LimitedChargesComponent? actionCharges))
{
var charges = _sharedChargesSys.GetCurrentCharges((Action.Value, actionCharges, null));
chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
if (_player.LocalEntity is null)
return null;
if (_entities.TryGetComponent(Action, out AutoRechargeComponent? autoRecharge))
{
var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((Action.Value, actionCharges, autoRecharge));
chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
}
}
var ev = new ExaminedEvent(desc, Action.Value, _player.LocalEntity.Value, true, !desc.IsEmpty);
_entities.EventBus.RaiseLocalEvent(Action.Value.Owner, ev);
return new ActionAlertTooltip(name, decr, charges: chargesText);
var newDesc = ev.GetTotalMessage();
return new ActionAlertTooltip(name, newDesc);
}
protected override void ControlFocusExited()

View file

@ -12,6 +12,7 @@ using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Input;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@ -73,7 +74,8 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
{
if (entity.Owner != _player.LocalEntity)
return;
AddHand(name, location);
if (_handsSystem.TryGetHand((entity.Owner, entity.Comp), name, out var hand))
AddHand(name, hand.Value);
}
private void OnRemoveHand(Entity<HandsComponent> entity, string name)
@ -139,7 +141,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_playerHandsComponent = handsComp;
foreach (var (name, hand) in handsComp.Comp.Hands)
{
var handButton = AddHand(name, hand.Location);
var handButton = AddHand(name, hand);
if (_handsSystem.TryGetHeldItem(handsComp.AsNullable(), name, out var held) &&
_entities.TryGetComponent(held, out VirtualItemComponent? virt))
@ -147,11 +149,25 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
else
else if (held != null)
{
handButton.SetEntity(held);
handButton.Blocked = false;
}
else
{
if (hand.EmptyRepresentative is { } representative)
{
// placeholder, view it
SetRepresentative(handButton, representative);
}
else
{
// otherwise empty
handButton.SetEntity(null);
}
handButton.Blocked = false;
}
}
if (handsComp.Comp.ActiveHandId == null)
@ -159,6 +175,11 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
SetActiveHand(handsComp.Comp.ActiveHandId);
}
private void SetRepresentative(HandButton handButton, EntProtoId prototype)
{
handButton.SetPrototype(prototype, true);
}
private void HandBlocked(string handName)
{
if (!_handLookup.TryGetValue(handName, out var hand))
@ -203,7 +224,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
hand.Blocked = false;
}
UpdateHandStatus(hand, entity);
if (_playerHandsComponent != null &&
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), name, out var handData))
{
UpdateHandStatus(hand, entity, handData);
}
}
private void OnItemRemoved(string name, EntityUid entity)
@ -212,8 +238,19 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (hand == null)
return;
if (_playerHandsComponent != null &&
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), name, out var handData))
{
UpdateHandStatus(hand, null, handData);
if (handData?.EmptyRepresentative is { } representative)
{
SetRepresentative(hand, representative);
return;
}
}
hand.SetEntity(null);
UpdateHandStatus(hand, null);
}
private HandsContainer GetFirstAvailableContainer()
@ -276,13 +313,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (foldedLocation == HandUILocation.Left)
{
_statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(heldEnt);
HandsGui.UpdatePanelEntityLeft(heldEnt, hand.Value);
}
else
{
// Middle or right
_statusHandRight = handControl;
HandsGui.UpdatePanelEntityRight(heldEnt);
HandsGui.UpdatePanelEntityRight(heldEnt, hand.Value);
}
HandsGui.SetHighlightHand(foldedLocation);
@ -295,9 +332,9 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
return handControl;
}
private HandButton AddHand(string handName, HandLocation location)
private HandButton AddHand(string handName, Hand hand)
{
var button = new HandButton(handName, location);
var button = new HandButton(handName, hand.Location);
button.StoragePressed += StorageActivate;
button.Pressed += HandPressed;
var uiLocation = location.GetUILocation(); // 🌟Starlight🌟
@ -319,10 +356,16 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
GetFirstAvailableContainer().AddButton(button);
}
if (hand.EmptyRepresentative is { } representative)
{
SetRepresentative(button, representative);
}
UpdateHandStatus(button, null, hand);
// If we don't have a status for this hand type yet, set it.
// This means we have status filled by default in most scenarios,
// otherwise the user'd need to switch hands to "activate" the hands the first time.
if (location.GetUILocation() == HandUILocation.Left)
if (hand.Location.GetUILocation() == HandUILocation.Left)
_statusHandLeft ??= button;
else
_statusHandRight ??= button;
@ -486,12 +529,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
}
}
private void UpdateHandStatus(HandButton hand, EntityUid? entity)
private void UpdateHandStatus(HandButton hand, EntityUid? entity, Hand? handData)
{
if (hand == _statusHandLeft)
HandsGui?.UpdatePanelEntityLeft(entity);
HandsGui?.UpdatePanelEntityLeft(entity, handData);
if (hand == _statusHandRight)
HandsGui?.UpdatePanelEntityRight(entity);
HandsGui?.UpdatePanelEntityRight(entity, handData);
}
}

View file

@ -19,14 +19,14 @@ public sealed partial class HotbarGui : UIWidget
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
}
public void UpdatePanelEntityLeft(EntityUid? entity)
public void UpdatePanelEntityLeft(EntityUid? entity, Hand? hand)
{
StatusPanelLeft.Update(entity);
StatusPanelLeft.Update(entity, hand);
}
public void UpdatePanelEntityRight(EntityUid? entity)
public void UpdatePanelEntityRight(EntityUid? entity, Hand? hand)
{
StatusPanelRight.Update(entity);
StatusPanelRight.Update(entity, hand);
}
public void SetHighlightHand(HandUILocation? hand)

View file

@ -17,6 +17,7 @@ public sealed partial class ItemStatusPanel : Control
[Dependency] private readonly IEntityManager _entityManager = default!;
[ViewVariables] private EntityUid? _entity;
[ViewVariables] private Hand? _hand;
// Tracked so we can re-run SetSide() if the theme changes.
private HandUILocation _side;
@ -101,29 +102,45 @@ public sealed partial class ItemStatusPanel : Control
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateItemName();
UpdateItemName(_hand);
}
public void Update(EntityUid? entity)
public void Update(EntityUid? entity, Hand? hand)
{
ItemNameLabel.Visible = entity != null;
NoItemLabel.Visible = entity == null;
if (entity == _entity && hand == _hand)
return;
_hand = hand;
if (entity == null)
{
ItemNameLabel.Text = "";
ClearOldStatus();
_entity = null;
if (hand?.EmptyLabel is { } label)
{
ItemNameLabel.Visible = true;
NoItemLabel.Visible = false;
ItemNameLabel.Text = Loc.GetString(label);
}
else
{
ItemNameLabel.Visible = false;
NoItemLabel.Visible = true;
ItemNameLabel.Text = "";
}
return;
}
if (entity != _entity)
{
_entity = entity.Value;
BuildNewEntityStatus();
ItemNameLabel.Visible = true;
NoItemLabel.Visible = false;
UpdateItemName();
}
_entity = entity.Value;
BuildNewEntityStatus();
UpdateItemName(hand);
}
public void UpdateHighlight(bool highlight)
@ -131,14 +148,14 @@ public sealed partial class ItemStatusPanel : Control
HighlightPanel.Visible = highlight;
}
private void UpdateItemName()
private void UpdateItemName(Hand? hand)
{
if (_entity == null)
return;
if (!_entityManager.TryGetComponent<MetaDataComponent>(_entity, out var meta) || meta.Deleted)
{
Update(null);
Update(null, hand);
return;
}

View file

@ -5,4 +5,4 @@
// https://github.com/dotnet/runtime/issues/107197
// So we can't really parallelize integration tests harder either until the runtime fixes that,
// *or* we fix serv3 to not spam expression trees.
[assembly: LevelOfParallelism(3)]
[assembly: LevelOfParallelism(2)]

View file

@ -11,7 +11,6 @@ using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
@ -22,6 +21,7 @@ using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.NukeOps;
using Content.Shared.Pinpointer;
using Content.Shared.Roles.Components;
using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;

View file

@ -3,7 +3,6 @@ using System.Linq;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@ -11,7 +10,7 @@ using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Players;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.Components;
using Robust.Server.Console;
using Robust.Server.GameObjects;
using Robust.Server.Player;

View file

@ -1,7 +1,5 @@
using System.Linq;
using Content.Server.Roles;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Reflection;

View file

@ -14,6 +14,7 @@ using Content.Shared._Sunrise.Biocode;
using Content.Server.Implants;
using Content.Shared.Implants;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.PDA;
namespace Content.Server.Access.Systems
@ -26,6 +27,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChameleonClothingSystem _chameleon = default!;
[Dependency] private readonly ChameleonControllerSystem _chamController = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly BiocodeSystem _biocodeSystem = default!;
public override void Initialize()
@ -81,7 +83,8 @@ namespace Content.Server.Access.Systems
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) ||
!TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
return;
// Sunrise-Start

View file

@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.Forensics;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
using Content.Server.Mind;
@ -21,6 +20,7 @@ using Content.Shared.PDA;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Roles.Jobs;
using Content.Shared.StationRecords;
using Content.Shared.Throwing;

View file

@ -276,8 +276,8 @@ namespace Content.Server.Construction
if(!insertStep.EntityValid(insert, EntityManager, Factory))
return HandleResult.False;
// Unremovable items can't be inserted, unless they are a lingering stack
if(HasComp<UnremoveableComponent>(insert) && (!TryComp<StackComponent>(insert, out var comp) || !comp.Lingering))
// Unremovable items can't be inserted
if(HasComp<UnremoveableComponent>(insert))
return HandleResult.False;
// If we're only testing whether this step would be handled by the given event, then we're done.

View file

@ -1,5 +1,4 @@
using Content.Server.Administration.Logs;
using Content.Server.Light.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
@ -16,6 +15,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Jittering;
using Content.Shared.Light.Components;
using Content.Shared.Maps;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;

View file

@ -3,7 +3,6 @@ using System.Numerics;
using Content.Server.Announcements;
using Content.Server.Discord;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.Roles;
using Content.Server.Shuttles.Components;
@ -13,6 +12,7 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
using Content.Shared.Roles.Components;
using JetBrains.Annotations;
using Prometheus;
using Robust.Shared.Asynchronous;
@ -20,7 +20,6 @@ using Robust.Shared.Audio;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;

View file

@ -4,6 +4,7 @@ using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.Station.Systems;
using Content.Shared.Localizations;
using Content.Shared.Roles.Components;
using Robust.Server.GameObjects;
namespace Content.Server.GameTicking.Rules;

View file

@ -17,6 +17,7 @@ using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Nuke;
using Content.Shared.NukeOps;
using Content.Shared.Roles.Components;
using Content.Shared.Store;
using Content.Shared.Tag;
using Content.Shared.Zombies;

View file

@ -25,6 +25,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Roles.Components;
using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
@ -204,7 +205,10 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
{
role.Value.Comp2.ConvertedCount++;
Dirty(role.Value.Owner, role.Value.Comp2);
}
}
}

View file

@ -6,6 +6,7 @@ using Content.Server.Shuttles.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles.Components;
using Content.Shared.Survivor.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;

View file

@ -2,6 +2,7 @@ using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Roles;
using Content.Shared.Humanoid;
using Content.Shared.Roles.Components;
using Content.Shared.NPC.Systems;
namespace Content.Server.GameTicking.Rules;

View file

@ -1,19 +1,16 @@
using Content.Server.Administration.Logs;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
using Content.Shared.PDA;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.RoleCodeword;
using Robust.Shared.Prototypes;

View file

@ -13,6 +13,7 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Zombies;
using Robust.Shared.Player;
using Robust.Shared.Timing;

View file

@ -1,12 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Ghost;
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
[RegisterComponent]
public sealed partial class ObserverRoleComponent : BaseMindRoleComponent
{
public string Name => Loc.GetString("observer-role-name");
}

View file

@ -1,15 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a ghostrole.
/// It also holds the name for the round end display
/// </summary>
[RegisterComponent]
public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
{
//TODO does anything still use this? It gets populated by GhostRolesystem but I don't see anything ever reading it
[DataField] public string? Name;
}

View file

@ -565,9 +565,6 @@ public sealed class GhostRoleSystem : EntitySystem
_mindSystem.TransferTo(newMind, mob);
_roleSystem.MindAddRoles(newMind.Owner, role.MindRoles, newMind.Comp);
if (_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
markerRole.Value.Comp2.Name = role.RoleName;
}
/// <summary>

View file

@ -1,87 +1,5 @@
using Content.Server.Light.Components;
using Content.Shared.Destructible;
using Content.Shared.Light.Components;
using Content.Shared.Throwing;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Content.Shared.Light.EntitySystems;
namespace Content.Server.Light.EntitySystems
{
public sealed class LightBulbSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
namespace Content.Server.Light.EntitySystems;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LightBulbComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<LightBulbComponent, LandEvent>(HandleLand);
SubscribeLocalEvent<LightBulbComponent, BreakageEventArgs>(OnBreak);
}
private void OnInit(EntityUid uid, LightBulbComponent bulb, ComponentInit args)
{
// update default state of bulbs
SetColor(uid, bulb.Color, bulb);
SetState(uid, bulb.State, bulb);
}
private void HandleLand(EntityUid uid, LightBulbComponent bulb, ref LandEvent args)
{
PlayBreakSound(uid, bulb);
SetState(uid, LightBulbState.Broken, bulb);
}
private void OnBreak(EntityUid uid, LightBulbComponent component, BreakageEventArgs args)
{
SetState(uid, LightBulbState.Broken, component);
}
/// <summary>
/// Set a new color for a light bulb and raise event about change
/// </summary>
public void SetColor(EntityUid uid, Color color, LightBulbComponent? bulb = null)
{
if (!Resolve(uid, ref bulb))
return;
bulb.Color = color;
UpdateAppearance(uid, bulb);
}
/// <summary>
/// Set a new state for a light bulb (broken, burned) and raise event about change
/// </summary>
public void SetState(EntityUid uid, LightBulbState state, LightBulbComponent? bulb = null)
{
if (!Resolve(uid, ref bulb))
return;
bulb.State = state;
UpdateAppearance(uid, bulb);
}
public void PlayBreakSound(EntityUid uid, LightBulbComponent? bulb = null)
{
if (!Resolve(uid, ref bulb))
return;
_audio.PlayPvs(bulb.BreakSound, uid);
}
private void UpdateAppearance(EntityUid uid, LightBulbComponent? bulb = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref bulb, ref appearance, logMissing: false))
return;
// try to update appearance and color
_appearance.SetData(uid, LightBulbVisuals.State, bulb.State, appearance);
_appearance.SetData(uid, LightBulbVisuals.Color, bulb.Color, appearance);
}
}
}
public sealed class LightBulbSystem : SharedLightBulbSystem;

View file

@ -1,440 +1,64 @@
using Content.Server.DeviceLinking.Systems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Server.Ghost;
using Content.Server.Light.Components;
using Content.Server.Power.Components;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.DoAfter;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage.Components;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power;
using Content.Shared.Light.EntitySystems;
namespace Content.Server.Light.EntitySystems
namespace Content.Server.Light.EntitySystems;
/// <summary>
/// System for the PoweredLightComponents
/// </summary>
public sealed class PoweredLightSystem : SharedPoweredLightSystem
{
/// <summary>
/// System for the PoweredLightComponents
/// </summary>
public sealed class PoweredLightSystem : EntitySystem
public override void Initialize()
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!;
[Dependency] private readonly LightBulbSystem _bulbSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PointLightSystem _pointLight = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly DamageOnInteractSystem _damageOnInteractSystem = default!;
base.Initialize();
SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
private static readonly TimeSpan ThunkDelay = TimeSpan.FromSeconds(2);
public const string LightBulbContainer = "light_bulb";
SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
public override void Initialize()
SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)
{
if (light.IgnoreGhostsBoo)
return;
// check cooldown first to prevent abuse
var time = GameTiming.CurTime;
if (light.LastGhostBlink != null)
{
base.Initialize();
SubscribeLocalEvent<PoweredLightComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<PoweredLightComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PoweredLightComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<PoweredLightComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
SubscribeLocalEvent<PoweredLightComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<PoweredLightComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<PoweredLightComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<PoweredLightComponent, PoweredLightDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<PoweredLightComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnInit(EntityUid uid, PoweredLightComponent light, ComponentInit args)
{
light.LightBulbContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, LightBulbContainer);
_signalSystem.EnsureSinkPorts(uid, light.OnPort, light.OffPort, light.TogglePort);
}
private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent args)
{
// TODO: Use ContainerFill dog
if (light.HasLampOnSpawn != null)
{
var entity = Spawn(light.HasLampOnSpawn, Comp<TransformComponent>(uid).Coordinates);
_containerSystem.Insert(entity, light.LightBulbContainer);
}
// need this to update visualizers
UpdateLight(uid, light);
}
private void OnInteractUsing(EntityUid uid, PoweredLightComponent component, InteractUsingEvent args)
{
if (args.Handled)
if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown)
return;
args.Handled = InsertBulb(uid, args.Used, component);
}
private void OnInteractHand(EntityUid uid, PoweredLightComponent light, InteractHandEvent args)
light.LastGhostBlink = time;
ToggleBlinkingLight(uid, light, true);
uid.SpawnTimer(light.GhostBlinkingTime, () =>
{
if (args.Handled)
return;
ToggleBlinkingLight(uid, light, false);
});
// check if light has bulb to eject
var bulbUid = GetBulb(uid, light);
if (bulbUid == null)
return;
args.Handled = true;
}
var userUid = args.User;
//removing a broken/burned bulb, so allow instant removal
if(TryComp<LightBulbComponent>(bulbUid.Value, out var bulb) && bulb.State != LightBulbState.Normal)
{
args.Handled = EjectBulb(uid, userUid, light) != null;
return;
}
// removing a working bulb, so require a delay
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, userUid, light.EjectBulbDelay, new PoweredLightDoAfterEvent(), uid, target: uid)
{
BreakOnMove = true,
BreakOnDamage = true,
});
args.Handled = true;
}
#region Bulb Logic API
/// <summary>
/// Inserts the bulb if possible.
/// </summary>
/// <returns>True if it could insert it, false if it couldn't.</returns>
public bool InsertBulb(EntityUid uid, EntityUid bulbUid, PoweredLightComponent? light = null)
private void OnMapInit(EntityUid uid, PoweredLightComponent light, MapInitEvent args)
{
// TODO: Use ContainerFill dog
if (light.HasLampOnSpawn != null)
{
if (!Resolve(uid, ref light))
return false;
// check if light already has bulb
if (GetBulb(uid, light) != null)
return false;
// check if bulb fits
if (!TryComp(bulbUid, out LightBulbComponent? lightBulb))
return false;
if (lightBulb.Type != light.BulbType)
return false;
// try to insert bulb in container
if (!_containerSystem.Insert(bulbUid, light.LightBulbContainer))
return false;
UpdateLight(uid, light);
return true;
var entity = EntityManager.SpawnEntity(light.HasLampOnSpawn, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
ContainerSystem.Insert(entity, light.LightBulbContainer);
}
// need this to update visualizers
UpdateLight(uid, light);
}
/// <summary>
/// Ejects the bulb to a mob's hand if possible.
/// </summary>
/// <returns>Bulb uid if it was successfully ejected, null otherwise</returns>
public EntityUid? EjectBulb(EntityUid uid, EntityUid? userUid = null, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return null;
// check if light has bulb
if (GetBulb(uid, light) is not { Valid: true } bulb)
return null;
// try to remove bulb from container
if (!_containerSystem.Remove(bulb, light.LightBulbContainer))
return null;
// try to place bulb in hands
_handsSystem.PickupOrDrop(userUid, bulb);
UpdateLight(uid, light);
return bulb;
}
/// <summary>
/// Replaces the spawned prototype of a pre-mapinit powered light with a different variant.
/// </summary>
public bool ReplaceSpawnedPrototype(Entity<PoweredLightComponent> light, string bulb)
{
if (light.Comp.LightBulbContainer.ContainedEntity != null)
return false;
if (LifeStage(light.Owner) >= EntityLifeStage.MapInitialized)
return false;
light.Comp.HasLampOnSpawn = bulb;
return true;
}
/// <summary>
/// Try to replace current bulb with a new one
/// If succeed old bulb just drops on floor
/// </summary>
public bool ReplaceBulb(EntityUid uid, EntityUid bulb, PoweredLightComponent? light = null)
{
EjectBulb(uid, null, light);
return InsertBulb(uid, bulb, light);
}
/// <summary>
/// Try to get light bulb inserted in powered light
/// </summary>
/// <returns>Bulb uid if it exist, null otherwise</returns>
public EntityUid? GetBulb(EntityUid uid, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return null;
return light.LightBulbContainer.ContainedEntity;
}
/// <summary>
/// Try to break bulb inside light fixture
/// </summary>
public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light, false))
return false;
// if we aren't mapinited,
// just null the spawned bulb
if (LifeStage(uid) < EntityLifeStage.MapInitialized)
{
light.HasLampOnSpawn = null;
return true;
}
// check bulb state
var bulbUid = GetBulb(uid, light);
if (bulbUid == null || !TryComp(bulbUid.Value, out LightBulbComponent? lightBulb))
return false;
if (lightBulb.State == LightBulbState.Broken)
return false;
// break it
_bulbSystem.SetState(bulbUid.Value, LightBulbState.Broken, lightBulb);
_bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb);
UpdateLight(uid, light);
return true;
}
#endregion
private void UpdateLight(EntityUid uid,
PoweredLightComponent? light = null,
ApcPowerReceiverComponent? powerReceiver = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref light, ref powerReceiver, false))
return;
// Optional component.
Resolve(uid, ref appearance, false);
// check if light has bulb
var bulbUid = GetBulb(uid, light);
if (bulbUid == null || !TryComp(bulbUid.Value, out LightBulbComponent? lightBulb))
{
SetLight(uid, false, light: light);
powerReceiver.Load = 0;
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Empty, appearance);
return;
}
switch (lightBulb.State)
{
case LightBulbState.Normal:
if (powerReceiver.Powered && light.On)
{
SetLight(uid, true, lightBulb.Color, light, lightBulb.LightRadius, lightBulb.LightEnergy, lightBulb.LightSoftness);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.On, appearance);
var time = _gameTiming.CurTime;
if (time > light.LastThunk + ThunkDelay)
{
light.LastThunk = time;
_audio.PlayPvs(light.TurnOnSound, uid, light.TurnOnSound.Params.AddVolume(-10f));
}
}
else
{
SetLight(uid, false, light: light);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Off, appearance);
}
break;
case LightBulbState.Broken:
SetLight(uid, false, light: light);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Broken, appearance);
break;
case LightBulbState.Burned:
SetLight(uid, false, light: light);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Burned, appearance);
break;
}
powerReceiver.Load = (light.On && lightBulb.State == LightBulbState.Normal) ? lightBulb.PowerUse : 0;
}
/// <summary>
/// Destroy the light bulb if the light took any damage.
/// </summary>
public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
{
// Was it being repaired, or did it take damage?
if (args.DamageIncreased)
{
// Eventually, this logic should all be done by this (or some other) system, not a component.
TryDestroyBulb(uid, component);
}
}
private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)
{
if (light.IgnoreGhostsBoo)
return;
// check cooldown first to prevent abuse
var time = _gameTiming.CurTime;
if (light.LastGhostBlink != null)
{
if (time <= light.LastGhostBlink + light.GhostBlinkingCooldown)
return;
}
light.LastGhostBlink = time;
ToggleBlinkingLight(uid, light, true);
uid.SpawnTimer(light.GhostBlinkingTime, () =>
{
ToggleBlinkingLight(uid, light, false);
});
args.Handled = true;
}
private void OnPowerChanged(EntityUid uid, PoweredLightComponent component, ref PowerChangedEvent args)
{
// TODO: Power moment
var metadata = MetaData(uid);
if (metadata.EntityPaused || TerminatingOrDeleted(uid, metadata))
return;
UpdateLight(uid, component);
}
public void ToggleBlinkingLight(EntityUid uid, PoweredLightComponent light, bool isNowBlinking)
{
if (light.IsBlinking == isNowBlinking)
return;
light.IsBlinking = isNowBlinking;
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PoweredLightVisuals.Blinking, isNowBlinking, appearance);
}
private void OnSignalReceived(EntityUid uid, PoweredLightComponent component, ref SignalReceivedEvent args)
{
if (args.Port == component.OffPort)
SetState(uid, false, component);
else if (args.Port == component.OnPort)
SetState(uid, true, component);
else if (args.Port == component.TogglePort)
ToggleLight(uid, component);
}
/// <summary>
/// Turns the light on or of when receiving a <see cref="DeviceNetworkConstants.CmdSetState"/> command.
/// The light is turned on or of according to the <see cref="DeviceNetworkConstants.StateEnabled"/> value
/// </summary>
private void OnPacketReceived(EntityUid uid, PoweredLightComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command) || command != DeviceNetworkConstants.CmdSetState) return;
if (!args.Data.TryGetValue(DeviceNetworkConstants.StateEnabled, out bool enabled)) return;
SetState(uid, enabled, component);
}
private void SetLight(EntityUid uid, bool value, Color? color = null, PoweredLightComponent? light = null, float? radius = null, float? energy = null, float? softness = null)
{
if (!Resolve(uid, ref light))
return;
light.CurrentLit = value;
_ambientSystem.SetAmbience(uid, value);
if (TryComp(uid, out PointLightComponent? pointLight))
{
_pointLight.SetEnabled(uid, value, pointLight);
if (color != null)
_pointLight.SetColor(uid, color.Value, pointLight);
if (radius != null)
_pointLight.SetRadius(uid, (float) radius, pointLight);
if (energy != null)
_pointLight.SetEnergy(uid, (float) energy, pointLight);
if (softness != null)
_pointLight.SetSoftness(uid, (float) softness, pointLight);
}
// light bulbs burn your hands!
if (TryComp<DamageOnInteractComponent>(uid, out var damageOnInteractComp))
_damageOnInteractSystem.SetIsDamageActiveTo((uid, damageOnInteractComp), value);
}
public void ToggleLight(EntityUid uid, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return;
light.On = !light.On;
UpdateLight(uid, light);
}
public void SetState(EntityUid uid, bool state, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return;
light.On = state;
UpdateLight(uid, light);
}
private void OnDoAfter(EntityUid uid, PoweredLightComponent component, DoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
EjectBulb(args.Args.Target.Value, args.Args.User, component);
args.Handled = true;
}
private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
{
if (TryDestroyBulb(uid, component))
args.Affected = true;
}
private void OnEmpPulse(EntityUid uid, PoweredLightComponent component, ref EmpPulseEvent args)
{
if (TryDestroyBulb(uid, component))
args.Affected = true;
}
}

View file

@ -1,65 +1,25 @@
using System.Numerics;
using Content.Server.Access.Systems;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Server.Medical.CrewMonitoring;
using Content.Server.Popups;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Clothing;
using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Medical.SuitSensors;
using Robust.Shared.Timing;
namespace Content.Server.Medical.SuitSensors;
public sealed class SuitSensorSystem : EntitySystem
public sealed class SuitSensorSystem : SharedSuitSensorSystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly IdCardSystem _idCardSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SuitSensorComponent, ClothingGotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<SuitSensorComponent, ClothingGotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
SubscribeLocalEvent<SuitSensorComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<SuitSensorComponent, EmpDisabledRemoved>(OnEmpFinished);
SubscribeLocalEvent<SuitSensorComponent, SuitSensorChangeDoAfterEvent>(OnSuitSensorDoAfter);
}
public override void Update(float frameTime)
@ -78,14 +38,13 @@ public sealed class SuitSensorSystem : EntitySystem
if (curTime < sensor.NextUpdate)
continue;
if (!CheckSensorAssignedStation(uid, sensor))
if (!CheckSensorAssignedStation((uid, sensor)))
continue;
// TODO: This would cause imprecision at different tick rates.
sensor.NextUpdate = curTime + sensor.UpdateRate;
sensor.NextUpdate += sensor.UpdateRate;
// get sensor status
var status = GetSensorState(uid, sensor);
var status = GetSensorState((uid, sensor));
if (status == null)
continue;
@ -112,406 +71,21 @@ public sealed class SuitSensorSystem : EntitySystem
}
}
/// <summary>
/// Checks whether the sensor is assigned to a station or not
/// and tries to assign an unassigned sensor to a station if it's currently on a grid
/// </summary>
/// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
private bool CheckSensorAssignedStation(EntityUid uid, SuitSensorComponent sensor)
{
if (!sensor.StationId.HasValue && Transform(uid).GridUid == null)
return false;
sensor.StationId = _stationSystem.GetOwningStation(uid);
return sensor.StationId.HasValue;
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent ev)
{
// If the player spawns in arrivals then the grid underneath them may not be appropriate.
// in which case we'll just use the station spawn code told us they are attached to and set all of their
// sensors.
var sensorQuery = GetEntityQuery<SuitSensorComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
RecursiveSensor(ev.Mob, ev.Station, sensorQuery, xformQuery);
}
private void RecursiveSensor(EntityUid uid, EntityUid stationUid, EntityQuery<SuitSensorComponent> sensorQuery, EntityQuery<TransformComponent> xformQuery)
{
var xform = xformQuery.GetComponent(uid);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (sensorQuery.TryGetComponent(child, out var sensor))
{
sensor.StationId = stationUid;
}
RecursiveSensor(child, stationUid, sensorQuery, xformQuery);
}
}
private void OnMapInit(EntityUid uid, SuitSensorComponent component, MapInitEvent args)
{
// Fallback
component.StationId ??= _stationSystem.GetOwningStation(uid);
// generate random mode
if (component.RandomMode)
{
//make the sensor mode favor higher levels, except coords.
var modesDist = new[]
{
SuitSensorMode.SensorOff,
SuitSensorMode.SensorBinary, SuitSensorMode.SensorBinary,
SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals,
SuitSensorMode.SensorCords, SuitSensorMode.SensorCords
};
component.Mode = _random.Pick(modesDist);
}
}
private void OnEquipped(EntityUid uid, SuitSensorComponent component, ref ClothingGotEquippedEvent args)
{
component.User = args.Wearer;
}
private void OnUnequipped(EntityUid uid, SuitSensorComponent component, ref ClothingGotUnequippedEvent args)
{
component.User = null;
}
private void OnExamine(EntityUid uid, SuitSensorComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
string msg;
switch (component.Mode)
{
case SuitSensorMode.SensorOff:
msg = "suit-sensor-examine-off";
break;
case SuitSensorMode.SensorBinary:
msg = "suit-sensor-examine-binary";
break;
case SuitSensorMode.SensorVitals:
msg = "suit-sensor-examine-vitals";
break;
case SuitSensorMode.SensorCords:
msg = "suit-sensor-examine-cords";
break;
default:
return;
}
args.PushMarkup(Loc.GetString(msg));
}
private void OnVerb(EntityUid uid, SuitSensorComponent component, GetVerbsEvent<Verb> args)
{
// check if user can change sensor
if (component.ControlsLocked)
return;
// standard interaction checks
if (!args.CanInteract || args.Hands == null)
return;
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
return;
// check if target is incapacitated (cuffed, dead, etc)
if (component.User != null && args.User != component.User && _actionBlocker.CanInteract(component.User.Value, null))
return;
args.Verbs.UnionWith(new[]
{
CreateVerb(uid, component, args.User, SuitSensorMode.SensorOff),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorBinary),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorVitals),
CreateVerb(uid, component, args.User, SuitSensorMode.SensorCords)
});
}
private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = args.Container.Owner;
}
private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args)
{
if (args.Container.ID != component.ActivationContainer)
return;
component.User = null;
}
private void OnEmpPulse(EntityUid uid, SuitSensorComponent component, ref EmpPulseEvent args)
private void OnEmpPulse(Entity<SuitSensorComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
component.PreviousMode = component.Mode;
SetSensor((uid, component), SuitSensorMode.SensorOff, null);
ent.Comp.PreviousMode = ent.Comp.Mode;
SetSensor(ent.AsNullable(), SuitSensorMode.SensorOff, null);
component.PreviousControlsLocked = component.ControlsLocked;
component.ControlsLocked = true;
ent.Comp.PreviousControlsLocked = ent.Comp.ControlsLocked;
ent.Comp.ControlsLocked = true;
}
private void OnEmpFinished(EntityUid uid, SuitSensorComponent component, ref EmpDisabledRemoved args)
private void OnEmpFinished(Entity<SuitSensorComponent> ent, ref EmpDisabledRemoved args)
{
SetSensor((uid, component), component.PreviousMode, null);
component.ControlsLocked = component.PreviousControlsLocked;
}
private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode)
{
return new Verb()
{
Text = GetModeName(mode),
Disabled = component.Mode == mode,
Priority = -(int) mode, // sort them in descending order
Category = VerbCategory.SetSensor,
Act = () => TrySetSensor((uid, component), mode, userUid)
};
}
private string GetModeName(SuitSensorMode mode)
{
string name;
switch (mode)
{
case SuitSensorMode.SensorOff:
name = "suit-sensor-mode-off";
break;
case SuitSensorMode.SensorBinary:
name = "suit-sensor-mode-binary";
break;
case SuitSensorMode.SensorVitals:
name = "suit-sensor-mode-vitals";
break;
case SuitSensorMode.SensorCords:
name = "suit-sensor-mode-cords";
break;
default:
return "";
}
return Loc.GetString(name);
}
public void TrySetSensor(Entity<SuitSensorComponent> sensors, SuitSensorMode mode, EntityUid userUid)
{
var comp = sensors.Comp;
if (!Resolve(sensors, ref comp))
return;
if (comp.User == null || userUid == comp.User)
SetSensor(sensors, mode, userUid);
else
{
var doAfterEvent = new SuitSensorChangeDoAfterEvent(mode);
var doAfterArgs = new DoAfterArgs(EntityManager, userUid, comp.SensorsTime, doAfterEvent, sensors)
{
BreakOnMove = true,
BreakOnDamage = true
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
}
}
private void OnSuitSensorDoAfter(Entity<SuitSensorComponent> sensors, ref SuitSensorChangeDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
SetSensor(sensors, args.Mode, args.User);
}
public void SetSensor(Entity<SuitSensorComponent> sensors, SuitSensorMode mode, EntityUid? userUid = null)
{
var comp = sensors.Comp;
comp.Mode = mode;
if (userUid != null)
{
var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
_popupSystem.PopupEntity(msg, sensors, userUid.Value);
}
}
/// <summary>
/// Set all suit sensors on the equipment someone is wearing to the specified mode.
/// </summary>
public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots = SlotFlags.All )
{
// iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator(target, slots);
while (slotEnumerator.NextItem(out var item, out _))
{
if (TryComp<SuitSensorComponent>(item, out var sensorComp))
SetSensor((item, sensorComp), mode);
}
}
public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null)
{
if (!Resolve(uid, ref sensor, ref transform))
return null;
// check if sensor is enabled and worn by user
if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || !HasComp<MobStateComponent>(sensor.User) || transform.GridUid == null)
return null;
var sensorTransform = Transform(uid);
var sensorMapId = sensorTransform.MapID;
// try to get mobs id from ID slot
var userName = Loc.GetString("suit-sensor-component-unknown-name");
var userJob = Loc.GetString("suit-sensor-component-unknown-job");
var userJobIcon = "JobIconNoId";
var userJobDepartments = new List<string>();
if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card))
{
if (card.Comp.FullName != null)
userName = card.Comp.FullName;
if (card.Comp.LocalizedJobTitle != null)
userJob = card.Comp.LocalizedJobTitle;
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
}
// get health mob state
var isAlive = false;
if (TryComp(sensor.User.Value, out MobStateComponent? mobState))
isAlive = !_mobStateSystem.IsDead(sensor.User.Value, mobState);
// get mob total damage
var totalDamage = 0;
if (TryComp<DamageableComponent>(sensor.User.Value, out var damageable))
totalDamage = damageable.TotalDamage.Int();
// Get mob total damage crit threshold
int? totalDamageThreshold = null;
if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
totalDamageThreshold = critThreshold.Value.Int();
// finally, form suit sensor status
var status = new SuitSensorStatus(GetNetEntity(sensor.User.Value), GetNetEntity(uid), userName, userJob, userJobIcon, userJobDepartments, sensorMapId);
switch (sensor.Mode)
{
case SuitSensorMode.SensorBinary:
status.IsAlive = isAlive;
break;
case SuitSensorMode.SensorVitals:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
status.TotalDamageThreshold = totalDamageThreshold;
break;
case SuitSensorMode.SensorCords:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
status.TotalDamageThreshold = totalDamageThreshold;
EntityCoordinates coordinates;
var xformQuery = GetEntityQuery<TransformComponent>();
if (transform.GridUid != null)
{
coordinates = new EntityCoordinates(transform.GridUid.Value,
Vector2.Transform(_transform.GetWorldPosition(transform, xformQuery),
_transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery)));
}
else if (transform.MapUid != null)
{
coordinates = new EntityCoordinates(transform.MapUid.Value,
_transform.GetWorldPosition(transform, xformQuery));
}
else
{
coordinates = EntityCoordinates.Invalid;
}
status.Coordinates = GetNetCoordinates(coordinates);
break;
}
return status;
}
/// <summary>
/// Serialize create a device network package from the suit sensors status.
/// </summary>
public NetworkPayload SuitSensorToPacket(SuitSensorStatus status)
{
var payload = new NetworkPayload()
{
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
[SuitSensorConstants.NET_NAME] = status.Name,
[SuitSensorConstants.NET_JOB] = status.Job,
[SuitSensorConstants.NET_JOB_ICON] = status.JobIcon,
[SuitSensorConstants.NET_JOB_DEPARTMENTS] = status.JobDepartments,
[SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive,
[SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid,
[SuitSensorConstants.NET_OWNER_UID] = status.OwnerUid,
};
if (status.TotalDamage != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
if (status.TotalDamageThreshold != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, status.TotalDamageThreshold);
if (status.Coordinates != null)
payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
if (status.MapId != null)
payload.Add(SuitSensorConstants.MAP_ID, status.MapId);
return payload;
}
/// <summary>
/// Try to create the suit sensors status from the device network message
/// </summary>
public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload)
{
// check command
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return null;
if (command != DeviceNetworkConstants.CmdUpdatedState)
return null;
// check name, job and alive
if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_ICON, out string? jobIcon)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_DEPARTMENTS, out List<string>? jobDepartments)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_OWNER_UID, out NetEntity ownerUid)) return null;
// try get total damage and cords (optionals)
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, out int? totalDamageThreshold);
payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
payload.TryGetValue(SuitSensorConstants.MAP_ID, out MapId? mapId);
var status = new SuitSensorStatus(ownerUid, suitSensorUid, name, job, jobIcon, jobDepartments, mapId)
{
IsAlive = isAlive.Value,
TotalDamage = totalDamage,
TotalDamageThreshold = totalDamageThreshold,
Coordinates = coords,
MapId = mapId,
};
return status;
SetSensor(ent.AsNullable(), ent.Comp.PreviousMode, null);
ent.Comp.ControlsLocked = ent.Comp.PreviousControlsLocked;
}
}

View file

@ -7,6 +7,7 @@ using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Roles.Components;
using Robust.Shared.Containers;
namespace Content.Server.Mindshield;

View file

@ -1,10 +1,10 @@
using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Sticky;
using Content.Shared.Trigger;

View file

@ -1,8 +1,8 @@
using Content.Server.Objectives.Components;
using Content.Server.Roles;
using Content.Shared.Objectives.Components;
using Content.Shared.Ninja.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Warps;
using Robust.Shared.Random;

View file

@ -14,9 +14,12 @@ namespace Content.Server.Power.Components
/// <summary>
/// Amount of charge this needs from an APC per second to function.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("powerLoad")]
public float Load { get => NetworkLoad.DesiredPower; set => NetworkLoad.DesiredPower = value; }
public override float Load
{
get => NetworkLoad.DesiredPower;
set => NetworkLoad.DesiredPower = value;
}
public ApcPowerProviderComponent? Provider = null;

View file

@ -198,6 +198,7 @@ public sealed class GeneratorSystem : SharedGeneratorSystem
generator.On = on;
UpdateState(uid, generator);
Dirty(uid, generator);
}
public override void Update(float frameTime)

View file

@ -42,8 +42,6 @@ public sealed class PortableGeneratorSystem : SharedPortableGeneratorSystem
SubscribeLocalEvent<PortableGeneratorComponent, PortableGeneratorStartMessage>(GeneratorStartMessage);
SubscribeLocalEvent<PortableGeneratorComponent, PortableGeneratorStopMessage>(GeneratorStopMessage);
SubscribeLocalEvent<PortableGeneratorComponent, PortableGeneratorSwitchOutputMessage>(GeneratorSwitchOutputMessage);
SubscribeLocalEvent<FuelGeneratorComponent, SwitchPowerCheckEvent>(OnSwitchPowerCheck);
}
private void GeneratorSwitchOutputMessage(EntityUid uid, PortableGeneratorComponent component, PortableGeneratorSwitchOutputMessage args)
@ -195,12 +193,6 @@ public sealed class PortableGeneratorSystem : SharedPortableGeneratorSystem
}
}
private void OnSwitchPowerCheck(EntityUid uid, FuelGeneratorComponent comp, ref SwitchPowerCheckEvent args)
{
if (comp.On)
args.DisableMessage = Loc.GetString("fuel-generator-verb-disable-on");
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<PortableGeneratorComponent, FuelGeneratorComponent, PowerSupplierComponent>();

View file

@ -1,5 +1,4 @@
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Nodes;
@ -7,9 +6,7 @@ using Content.Shared.NodeContainer;
using Content.Shared.Power;
using Content.Shared.Power.Generator;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Utility;
namespace Content.Server.Power.Generator;
@ -26,47 +23,9 @@ public sealed class PowerSwitchableSystem : SharedPowerSwitchableSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerSwitchableComponent, GetVerbsEvent<InteractionVerb>>(GetVerbs);
}
private void GetVerbs(EntityUid uid, PowerSwitchableComponent comp, GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
var voltage = VoltageColor(GetNextVoltage(uid, comp));
var msg = Loc.GetString("power-switchable-switch-voltage", ("voltage", voltage));
InteractionVerb verb = new()
{
Act = () =>
{
// don't need to check it again since if its disabled server wont let the verb act
Cycle(uid, args.User, comp);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/zap.svg.192dpi.png")),
Text = msg
};
var ev = new SwitchPowerCheckEvent();
RaiseLocalEvent(uid, ref ev);
if (ev.DisableMessage != null)
{
verb.Message = ev.DisableMessage;
verb.Disabled = true;
}
args.Verbs.Add(verb);
}
/// <summary>
/// Cycles voltage then updates nodes and optionally power supplier to match it.
/// </summary>
public void Cycle(EntityUid uid, EntityUid user, PowerSwitchableComponent? comp = null)
// TODO: Prediction
/// <inheritdoc/>
public override void Cycle(EntityUid uid, EntityUid user, PowerSwitchableComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
@ -115,10 +74,3 @@ public sealed class PowerSwitchableSystem : SharedPowerSwitchableSystem
_useDelay.TryResetDelay((uid, useDelay));
}
}
/// <summary>
/// Raised on a <see cref="PowerSwitchableComponent"/> to see if its verb should work.
/// If <see cref="DisableMessage"/> is non-null, the verb is disabled with that as the message.
/// </summary>
[ByRefEvent]
public record struct SwitchPowerCheckEvent(string? DisableMessage = null);

View file

@ -21,6 +21,7 @@ using Content.Shared.DoAfter;
using Content.Shared.Emag.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Light.Components;
using Content.Shared.Maps;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;

View file

@ -95,6 +95,9 @@ public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
private void OnDisable(Entity<RoboticsConsoleComponent> ent, ref RoboticsConsoleDisableMessage args)
{
if (!ent.Comp.AllowBorgControl)
return;
if (_lock.IsLocked(ent.Owner))
return;
@ -112,6 +115,9 @@ public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
private void OnDestroy(Entity<RoboticsConsoleComponent> ent, ref RoboticsConsoleDestroyMessage args)
{
if (!ent.Comp.AllowBorgControl)
return;
if (_lock.IsLocked(ent.Owner))
return;
@ -139,7 +145,7 @@ public sealed class RoboticsConsoleSystem : SharedRoboticsConsoleSystem
private void UpdateUserInterface(Entity<RoboticsConsoleComponent> ent)
{
var state = new RoboticsConsoleState(ent.Comp.Cyborgs);
var state = new RoboticsConsoleState(ent.Comp.Cyborgs, ent.Comp.AllowBorgControl);
_ui.SetUiState(ent.Owner, RoboticsConsoleUiKey.Key, state);
}
}

View file

@ -1,12 +0,0 @@
using Content.Server.Dragon;
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a space dragon.
/// </summary>
[RegisterComponent, Access(typeof(DragonSystem))]
public sealed partial class DragonRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,12 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are an initial infected.
/// </summary>
[RegisterComponent]
public sealed partial class InitialInfectedRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,11 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a space ninja.
/// </summary>
[RegisterComponent]
public sealed partial class NinjaRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,11 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a nuke operative.
/// </summary>
[RegisterComponent]
public sealed partial class NukeopsRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,17 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a paradox clone.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxCloneRoleComponent : BaseMindRoleComponent
{
/// <summary>
/// Name modifer applied to the player when they turn into a ghost.
/// Needed to be able to keep the original and the clone apart in dead chat.
/// </summary>
[DataField]
public LocId? NameModifier = "paradox-clone-ghost-name-modifier";
}

View file

@ -1,7 +1,7 @@
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
namespace Content.Server.Roles;

View file

@ -3,6 +3,7 @@ using Content.Shared.Administration;
using Content.Shared.Players;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.Components;
using Robust.Server.Player;
using Robust.Shared.Console;

View file

@ -1,16 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a Revolutionary.
/// </summary>
[RegisterComponent]
public sealed partial class RevolutionaryRoleComponent : BaseMindRoleComponent
{
/// <summary>
/// For headrevs, how many people you have converted.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public uint ConvertedCount = 0;
}

View file

@ -1,13 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Adds a briefing to the character info menu, does nothing else.
/// </summary>
[RegisterComponent]
public sealed partial class RoleBriefingComponent : BaseMindRoleComponent
{
[DataField]
public string Briefing;
}

View file

@ -1,3 +1,5 @@
using Content.Shared.Roles.Components;
namespace Content.Server.Roles;
public sealed class RoleBriefingSystem : EntitySystem

View file

@ -1,11 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a hacked borg.
/// </summary>
[RegisterComponent]
public sealed partial class SubvertedSiliconRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,9 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Adds to a mind role ent to tag they're a Survivor
/// </summary>
[RegisterComponent]
public sealed partial class SurvivorRoleComponent : BaseMindRoleComponent;

View file

@ -1,11 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a thief.
/// </summary>
[RegisterComponent]
public sealed partial class ThiefRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,11 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a syndicate traitor.
/// </summary>
[RegisterComponent]
public sealed partial class TraitorRoleComponent : BaseMindRoleComponent
{
}

View file

@ -1,7 +0,0 @@
namespace Content.Server.Roles;
/// <summary>
/// Mind role to tag entities that they're a Wizard
/// </summary>
[RegisterComponent]
public sealed partial class WizardRoleComponent : Component;

View file

@ -1,11 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a zombie.
/// </summary>
[RegisterComponent]
public sealed partial class ZombieRoleComponent : BaseMindRoleComponent
{
}

View file

@ -235,6 +235,8 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
_xformQuery.TryGetComponent(comp.DockedWith, out var otherDockXform) ?
GetNetEntity(otherDockXform.GridUid) :
null,
Color = comp.RadarColor,
HighlightedColor = comp.HighlightedRadarColor
};
gridDocks.Add(state);

View file

@ -249,7 +249,7 @@ public sealed partial class ShuttleSystem
if (direction.LengthSquared() > minsq)
{
_stuns.TryUpdateKnockdownDuration(uid, knockdownTime);
_stuns.TryCrawling(uid, knockdownTime);
_throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false);
}
else

View file

@ -1,7 +1,7 @@
using Content.Server.Roles;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Containers;

View file

@ -52,7 +52,7 @@ public sealed partial class BorgSystem
private void OnProvideItemStartup(EntityUid uid, ItemBorgModuleComponent component, ComponentStartup args)
{
component.ProvidedContainer = Container.EnsureContainer<Container>(uid, component.ProvidedContainerId);
Container.EnsureContainer<Container>(uid, component.HoldingContainer);
}
private void OnSelectableInstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleInstalledEvent args)
@ -187,43 +187,43 @@ public sealed partial class BorgSystem
if (!TryComp<HandsComponent>(chassis, out var hands))
return;
var xform = Transform(chassis);
foreach (var itemProto in component.Items)
{
EntityUid item;
if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container))
return;
if (!component.ItemsCreated)
var xform = Transform(chassis);
for (var i = 0; i < component.Hands.Count; i++)
{
var hand = component.Hands[i];
var handId = $"{uid}-hand-{i}";
_hands.AddHand((chassis, hands), handId, hand.Hand);
EntityUid? item = null;
if (component.StoredItems is not null)
{
if (component.StoredItems.TryGetValue(handId, out var storedItem))
{
item = storedItem;
_container.Remove(storedItem, container, force: true);
}
}
else if (hand.Item is { } itemProto)
{
item = Spawn(itemProto, xform.Coordinates);
}
else
if (item is { } pickUp)
{
item = component.ProvidedContainer.ContainedEntities
.FirstOrDefault(ent => Prototype(ent)?.ID == itemProto.Id);
if (!item.IsValid())
_hands.DoPickup(chassis, handId, pickUp, hands);
if (!hand.ForceRemovable && hand.Hand.Whitelist == null && hand.Hand.Blacklist == null)
{
Log.Debug($"no items found: {component.ProvidedContainer.ContainedEntities.Count}");
continue;
EnsureComp<UnremoveableComponent>(pickUp);
}
_container.Remove(item, component.ProvidedContainer, force: true);
}
if (!item.IsValid())
{
Log.Debug("no valid item");
continue;
}
var handId = $"{uid}-item{component.HandCounter}";
component.HandCounter++;
_hands.AddHand((chassis, hands), handId, HandLocation.Middle);
_hands.DoPickup(chassis, handId, item, hands);
EnsureComp<UnremoveableComponent>(item);
component.ProvidedItems.Add(handId, item);
}
component.ItemsCreated = true;
Dirty(uid, component);
}
private void RemoveProvidedItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null)
@ -234,27 +234,33 @@ public sealed partial class BorgSystem
if (!TryComp<HandsComponent>(chassis, out var hands))
return;
if (TerminatingOrDeleted(uid))
{
foreach (var (hand, item) in component.ProvidedItems)
{
QueueDel(item);
_hands.RemoveHand(chassis, hand);
}
component.ProvidedItems.Clear();
if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container))
return;
}
foreach (var (handId, item) in component.ProvidedItems)
if (TerminatingOrDeleted(uid))
return;
component.StoredItems ??= new();
for (var i = 0; i < component.Hands.Count; i++)
{
if (LifeStage(item) <= EntityLifeStage.MapInitialized)
var handId = $"{uid}-hand-{i}";
if (_hands.TryGetHeldItem(chassis, handId, out var held))
{
RemComp<UnremoveableComponent>(item);
_container.Insert(item, component.ProvidedContainer);
RemComp<UnremoveableComponent>(held.Value);
_container.Insert(held.Value, container);
component.StoredItems[handId] = held.Value;
}
else
{
component.StoredItems.Remove(handId);
}
_hands.RemoveHand(chassis, handId);
}
component.ProvidedItems.Clear();
Dirty(uid, component);
}
/// <summary>
@ -286,8 +292,8 @@ public sealed partial class BorgSystem
if (!TryComp<ItemBorgModuleComponent>(containedModuleUid, out var containedItemModuleComp))
continue;
if (containedItemModuleComp.Items.Count == itemModuleComp.Items.Count &&
containedItemModuleComp.Items.All(itemModuleComp.Items.Contains))
if (containedItemModuleComp.Hands.Count == itemModuleComp.Hands.Count &&
containedItemModuleComp.Hands.All(itemModuleComp.Hands.Contains))
{
if (user != null)
Popup.PopupEntity(Loc.GetString("borg-module-duplicate"), uid, user.Value);

View file

@ -1,4 +1,9 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DeviceNetwork;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Components;
using Content.Shared.Popups;
using Content.Shared.Robotics;
@ -14,6 +19,9 @@ namespace Content.Server.Silicons.Borgs;
public sealed partial class BorgSystem
{
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
private void InitializeTransponder()
{
@ -28,7 +36,7 @@ public sealed partial class BorgSystem
var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
{
if (comp.NextDisable is {} nextDisable && now >= nextDisable)
if (comp.NextDisable is { } nextDisable && now >= nextDisable)
DoDisable((uid, comp, chassis, meta));
if (now < comp.NextBroadcast)
@ -38,13 +46,17 @@ public sealed partial class BorgSystem
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
charge = battery.CurrentCharge / battery.MaxCharge;
var hasBrain = chassis.BrainEntity != null && !comp.FakeDisabled;
var hpPercent = CalcHP(uid);
// checks if it has a brain and if the brain is not a empty MMI (gives false anyway if the fake disable is true)
var hasBrain = CheckBrain(chassis.BrainEntity) && !comp.FakeDisabled;
var canDisable = comp.NextDisable == null && !comp.FakeDisabling;
var data = new CyborgControlData(
comp.Sprite,
comp.Name,
meta.EntityName,
charge,
hpPercent,
chassis.ModuleCount,
hasBrain,
canDisable);
@ -75,7 +87,7 @@ public sealed partial class BorgSystem
return;
}
if (ent.Comp2.BrainEntity is not {} brain)
if (ent.Comp2.BrainEntity is not { } brain)
return;
var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent, ent.Comp3)));
@ -161,4 +173,40 @@ public sealed partial class BorgSystem
{
ent.Comp.Name = name;
}
/// <summary>
/// Returns a ratio between 0 and 1, 1 when they have no damage and 0 whenever they are crit (or more damaged)
/// </summary>
private float CalcHP(EntityUid uid)
{
if (!TryComp<DamageableComponent>(uid, out var damageable))
return 1;
if (!_mobState.IsAlive(uid))
return 0;
if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var threshold))
{
Log.Error($"Borg({ToPrettyString(uid)}), doesn't have critical threshold.");
return 1;
}
return 1 - ((FixedPoint2)(damageable.TotalDamage / threshold)).Float();
}
/// <summary>
/// Returns true if the borg has a brain
/// </summary>
private bool CheckBrain(EntityUid? brainEntity)
{
if (brainEntity == null)
return false;
// if the brainEntity.Value has the component MMIComponent then it is a MMI,
// in that case it trys to get the "brain" of the MMI, if it is null the MMI is empty and so it returns false
if (TryComp<MMIComponent>(brainEntity.Value, out var mmi) && _itemSlotsSystem.GetItemOrNull(brainEntity.Value, mmi.BrainSlotId) == null)
return false;
return true;
}
}

View file

@ -4,7 +4,6 @@ using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.Radio.Components;
using Content.Server.Roles;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.Chat;
@ -13,9 +12,9 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Silicons.Laws;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;

View file

@ -33,7 +33,7 @@ namespace Content.Server.Stack
base.SetCount(uid, amount, component);
// Queue delete stack if count reaches zero.
if (component.Count <= 0 && !component.Lingering)
if (component.Count <= 0)
QueueDel(uid);
}

View file

@ -8,6 +8,7 @@ using Content.Shared.Radio.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Light.Components;
namespace Content.Server.StationEvents.Events;

View file

@ -1,58 +1,66 @@
namespace Content.Server.Stunnable.Components
using Content.Server.Stunnable.Systems;
namespace Content.Server.Stunnable.Components;
/// <summary>
/// Adds stun when it collides with an entity
/// </summary>
[RegisterComponent, Access(typeof(StunOnCollideSystem))]
public sealed partial class StunOnCollideComponent : Component
{
// TODO: Can probably predict this.
/// <summary>
/// Adds stun when it collides with an entity
/// How long we are stunned for
/// </summary>
[RegisterComponent, Access(typeof(StunOnCollideSystem))]
public sealed partial class StunOnCollideComponent : Component
{
// TODO: Can probably predict this.
[DataField]
public TimeSpan StunAmount;
/// <summary>
/// How long we are stunned for
/// </summary>
[DataField]
public TimeSpan StunAmount;
/// <summary>
/// How long we are knocked down for
/// </summary>
[DataField]
public TimeSpan KnockdownAmount;
/// <summary>
/// How long we are knocked down for
/// </summary>
[DataField]
public TimeSpan KnockdownAmount;
/// <summary>
/// How long we are slowed down for
/// </summary>
[DataField]
public TimeSpan SlowdownAmount;
/// <summary>
/// How long we are slowed down for
/// </summary>
[DataField]
public TimeSpan SlowdownAmount;
/// <summary>
/// Multiplier for a mob's walking speed
/// </summary>
[DataField]
public float WalkSpeedModifier = 1f;
/// <summary>
/// Multiplier for a mob's walking speed
/// </summary>
[DataField]
public float WalkSpeedModifier = 1f;
/// <summary>
/// Multiplier for a mob's sprinting speed
/// </summary>
[DataField]
public float SprintSpeedModifier = 1f;
/// <summary>
/// Multiplier for a mob's sprinting speed
/// </summary>
[DataField]
public float SprintSpeedModifier = 1f;
/// <summary>
/// Refresh Stun or Slowdown on hit
/// </summary>
[DataField]
public bool Refresh = true;
/// <summary>
/// Refresh Stun or Slowdown on hit
/// </summary>
[DataField]
public bool Refresh = true;
/// <summary>
/// Should the entity try and stand automatically after being knocked down?
/// </summary>
[DataField]
public bool AutoStand = true;
/// <summary>
/// Should the entity try and stand automatically after being knocked down?
/// </summary>
[DataField]
public bool AutoStand = true;
/// <summary>
/// Should the entity drop their items upon first being knocked down?
/// </summary>
[DataField]
public bool Drop = true;
/// <summary>
/// Fixture we track for the collision.
/// </summary>
[DataField("fixture")] public string FixtureID = "projectile";
}
/// <summary>
/// Fixture we track for the collision.
/// </summary>
[DataField("fixture")] public string FixtureID = "projectile";
}

View file

@ -4,47 +4,60 @@ using JetBrains.Annotations;
using Content.Shared.Throwing;
using Robust.Shared.Physics.Events;
namespace Content.Server.Stunnable
namespace Content.Server.Stunnable.Systems;
[UsedImplicitly]
internal sealed class StunOnCollideSystem : EntitySystem
{
[UsedImplicitly]
internal sealed class StunOnCollideSystem : EntitySystem
[Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
public override void Initialize()
{
[Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
base.Initialize();
public override void Initialize()
SubscribeLocalEvent<StunOnCollideComponent, StartCollideEvent>(HandleCollide);
SubscribeLocalEvent<StunOnCollideComponent, ThrowDoHitEvent>(HandleThrow);
}
private void TryDoCollideStun(Entity<StunOnCollideComponent> ent, EntityUid target)
{
_stunSystem.TryKnockdown(target, ent.Comp.KnockdownAmount, ent.Comp.Refresh, ent.Comp.AutoStand, ent.Comp.Drop);
if (ent.Comp.Refresh)
{
base.Initialize();
SubscribeLocalEvent<StunOnCollideComponent, StartCollideEvent>(HandleCollide);
SubscribeLocalEvent<StunOnCollideComponent, ThrowDoHitEvent>(HandleThrow);
}
private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target)
{
_stunSystem.TryUpdateStunDuration(target, component.StunAmount);
_stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand, force: true);
_stunSystem.TryUpdateStunDuration(target, ent.Comp.StunAmount);
_movementMod.TryUpdateMovementSpeedModDuration(
target,
MovementModStatusSystem.TaserSlowdown,
component.SlowdownAmount,
component.WalkSpeedModifier,
component.SprintSpeedModifier
ent.Comp.SlowdownAmount,
ent.Comp.WalkSpeedModifier,
ent.Comp.SprintSpeedModifier
);
}
private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
else
{
if (args.OurFixtureId != component.FixtureID)
return;
TryDoCollideStun(uid, component, args.OtherEntity);
}
private void HandleThrow(EntityUid uid, StunOnCollideComponent component, ThrowDoHitEvent args)
{
TryDoCollideStun(uid, component, args.Target);
_stunSystem.TryAddStunDuration(target, ent.Comp.StunAmount);
_movementMod.TryAddMovementSpeedModDuration(
target,
MovementModStatusSystem.TaserSlowdown,
ent.Comp.SlowdownAmount,
ent.Comp.WalkSpeedModifier,
ent.Comp.SprintSpeedModifier
);
}
}
private void HandleCollide(Entity<StunOnCollideComponent> ent, ref StartCollideEvent args)
{
if (args.OurFixtureId != ent.Comp.FixtureID)
return;
TryDoCollideStun(ent, args.OtherEntity);
}
private void HandleThrow(Entity<StunOnCollideComponent> ent, ref ThrowDoHitEvent args)
{
TryDoCollideStun(ent, args.Target);
}
}

View file

@ -1,12 +1,12 @@
using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Server.Roles;
using Content.Server.Thief.Components;
using Content.Shared.Examine;
using Content.Shared.Foldable;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Thief.Systems;

View file

@ -0,0 +1,53 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Trigger;
using Content.Shared.Trigger.Components.Effects;
namespace Content.Server.Trigger.Systems;
/// <summary>
/// Trigger system for adding or removing fire stacks from an entity with <see cref="FlammableComponent"/>.
/// </summary>
/// <seealso cref="IgniteOnTriggerSystem"/>
public sealed class FireStackOnTriggerSystem : EntitySystem
{
[Dependency] private readonly FlammableSystem _flame = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FireStackOnTriggerComponent, TriggerEvent>(OnTriggerFlame);
SubscribeLocalEvent<ExtinguishOnTriggerComponent, TriggerEvent>(OnTriggerExtinguish);
}
private void OnTriggerFlame(Entity<FireStackOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key))
return;
var target = ent.Comp.TargetUser ? args.User : ent.Owner;
if (target == null)
return;
_flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite);
args.Handled = true;
}
private void OnTriggerExtinguish(Entity<ExtinguishOnTriggerComponent> ent, ref TriggerEvent args)
{
if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key))
return;
var target = ent.Comp.TargetUser ? args.User : ent.Owner;
if (target == null)
return;
_flame.Extinguish(target.Value);
args.Handled = true;
}
}

View file

@ -8,6 +8,7 @@ namespace Content.Server.Trigger.Systems;
/// <summary>
/// Handles igniting when triggered and stopping ignition after the delay.
/// </summary>
/// <seealso cref="FireStackOnTriggerSystem"/>
public sealed class IgniteOnTriggerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;

View file

@ -5,11 +5,13 @@ using Content.Shared.Chat;
using Content.Shared.Clothing;
using Content.Shared.Database;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.Popups;
using Content.Shared.Preferences;
using Content.Shared.Speech;
using Content.Shared.VoiceMask;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Server.VoiceMask;
@ -22,6 +24,8 @@ public sealed partial class VoiceMaskSystem : EntitySystem
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
// CCVar.
private int _maxNameLength;
@ -30,6 +34,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName);
SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb);
SubscribeLocalEvent<VoiceMaskComponent, ClothingGotEquippedEvent>(OnEquip);
@ -47,6 +52,14 @@ public sealed partial class VoiceMaskSystem : EntitySystem
args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb;
}
private void OnLockToggled(Entity<VoiceMaskComponent> ent, ref LockToggledEvent args)
{
if (args.Locked)
_actions.RemoveAction(ent.Comp.ActionEntity);
else if (_container.TryGetContainingContainer(ent.Owner, out var container))
_actions.AddAction(container.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action, ent);
}
#region User inputs from UI
private void OnChangeVerb(Entity<VoiceMaskComponent> entity, ref VoiceMaskChangeVerbMessage msg)
{
@ -81,6 +94,9 @@ public sealed partial class VoiceMaskSystem : EntitySystem
#region UI
private void OnEquip(EntityUid uid, VoiceMaskComponent component, ClothingGotEquippedEvent args)
{
if (_lock.IsLocked(uid))
return;
_actions.AddAction(args.Wearer, ref component.ActionEntity, component.Action, uid);
EnsureComp<VoiceMaskerComponent>(args.Wearer, out var maskerComponent);
maskerComponent.VoiceId = component.VoiceId;

View file

@ -1,6 +1,6 @@
using Content.Server.Ghost;
using Content.Server.Light.Components;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Light.Components;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Random;

View file

@ -9,7 +9,6 @@ using Content.Server.Emoting.Systems;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Pinpointer;
using Content.Server.Speech.EntitySystems;
using Content.Server.Roles;
using Content.Shared.Anomaly.Components;
using Content.Shared.Armor;
using Content.Shared.Bed.Sleep;
@ -25,6 +24,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Melee.Events;

View file

@ -75,7 +75,21 @@ public partial struct ReagentId : IEquatable<ReagentId>
public override int GetHashCode()
{
return HashCode.Combine(Prototype, Data);
// We need to make sure we take the hash code of Data by value in order
// for hashed key lookups to work properly
var hash = 17;
unchecked
{
if (Data?.Count != 0)
{
foreach (var data in Data ?? [])
{
hash = hash * 23 + data.GetHashCode();
}
}
}
return HashCode.Combine(Prototype, hash);
}
public string ToString(FixedPoint2 quantity)

View file

@ -59,7 +59,7 @@ public partial struct ReagentQuantity : IEquatable<ReagentQuantity>
public bool Equals(ReagentQuantity other)
{
return Quantity != other.Quantity && Reagent.Equals(other.Reagent);
return Quantity == other.Quantity && Reagent.Equals(other.Reagent);
}
public override bool Equals(object? obj)

View file

@ -5,6 +5,7 @@ using Content.Shared.Contraband;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
using Content.Shared.Lock;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Robust.Shared.Prototypes;
@ -24,6 +25,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] protected readonly IGameTiming _timing = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly BiocodeSystem _biocodeSystem = default!;
private static readonly SlotFlags[] IgnoredSlots =
@ -170,7 +172,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
private void OnVerb(Entity<ChameleonClothingComponent> ent, ref GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || ent.Comp.User != args.User)
if (!args.CanAccess || !args.CanInteract || _lock.IsLocked(ent.Owner))
return;
// Can't pass args from a ref event inside of lambdas

View file

@ -33,6 +33,12 @@ public sealed partial class MachineBoardComponent : Component
public EntProtoId Prototype;
}
/// <summary>
/// Marker component for any item that's machine board-like without necessarily being a MachineBoardComponent
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class CircuitboardComponent : Component;
[DataDefinition, Serializable]
public partial struct GenericPartInfo
{

View file

@ -184,22 +184,22 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
}
}
// Whether the user and the target are too far apart.
// Whether the user and the target are too far apart or they are inaccessible.
if (args.Target != null)
{
if (args.DistanceThreshold != null)
{
if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value, args.DistanceThreshold.Value))
if (!_interaction.InRangeAndAccessible(args.User, args.Target.Value, args.DistanceThreshold.Value))
return true;
}
}
// Whether the distance between the tool and the user has grown too much.
// Whether the distance between the tool and the user has grown too much or they became inaccessible.
if (args.Used != null)
{
if (args.DistanceThreshold != null)
{
if (!_interaction.InRangeUnobstructed(args.User,
if (!_interaction.InRangeAndAccessible(args.User,
args.Used.Value,
args.DistanceThreshold.Value))
return true;

View file

@ -3,7 +3,7 @@ using Content.Shared.Localizations;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;

View file

@ -1,6 +1,8 @@
using Content.Shared.DisplacementMap;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Hands.Components;
@ -125,16 +127,45 @@ public sealed partial class HandsComponent : Component
public partial record struct Hand
{
[DataField]
public HandLocation Location = HandLocation.Right;
public HandLocation Location = HandLocation.Middle;
/// <summary>
/// The label to be displayed for this hand when it does not contain an entity
/// </summary>
[DataField]
public LocId? EmptyLabel;
/// <summary>
/// The prototype ID of a "representative" entity prototype for what this hand could hold, used in the UI.
/// It is not map-initted.
/// </summary>
[DataField]
public EntProtoId? EmptyRepresentative;
/// <summary>
/// What this hand is allowed to hold
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// What this hand is not allowed to hold
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
public Hand()
{
}
public Hand(HandLocation location)
public Hand(HandLocation location, LocId? emptyLabel = null, EntProtoId? emptyRepresentative = null, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
Location = location;
EmptyLabel = emptyLabel;
EmptyRepresentative = emptyRepresentative;
Whitelist = whitelist;
Blacklist = blacklist;
}
}

View file

@ -217,6 +217,9 @@ public abstract partial class SharedHandsSystem
if (checkActionBlocker && !_actionBlocker.CanPickup(uid, entity))
return false;
if (!CheckWhitelists((uid, handsComp), handId, entity))
return false;
if (ContainerSystem.TryGetContainingContainer((entity, null, null), out var container))
{
if (!ContainerSystem.CanRemove(entity, container))

View file

@ -0,0 +1,15 @@
using Content.Shared.Hands.Components;
using Content.Shared.Whitelist;
namespace Content.Shared.Hands.EntitySystems;
public abstract partial class SharedHandsSystem
{
private bool CheckWhitelists(Entity<HandsComponent?> ent, string handId, EntityUid toTest)
{
if (!TryGetHand(ent, handId, out var hand))
return false;
return _entityWhitelist.CheckBoth(toTest, hand.Value.Blacklist, hand.Value.Whitelist);
}
}

View file

@ -7,8 +7,10 @@ using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Hands.EntitySystems;
@ -23,6 +25,7 @@ public abstract partial class SharedHandsSystem
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
public event Action<Entity<HandsComponent>, string, HandLocation>? OnPlayerAddHand;
public event Action<Entity<HandsComponent>, string>? OnPlayerRemoveHand;
@ -66,9 +69,9 @@ public abstract partial class SharedHandsSystem
/// <summary>
/// Adds a hand with the given container id and supplied location to the specified entity.
/// </summary>
public void AddHand(Entity<HandsComponent?> ent, string handName, HandLocation handLocation)
public void AddHand(Entity<HandsComponent?> ent, string handName, HandLocation handLocation, LocId? emptyLabel = null, EntProtoId? emptyRepresentative = null, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
AddHand(ent, handName, new Hand(handLocation));
AddHand(ent, handName, new Hand(handLocation, emptyLabel, emptyRepresentative, whitelist, blacklist));
}
/// <summary>

View file

@ -131,7 +131,7 @@ public sealed class ItemToggleSystem : EntitySystem
if (ent.Comp.Activated)
{
var ev = new ItemToggleActivateAttemptEvent(args.User);
var ev = new ItemToggleDeactivateAttemptEvent(args.User);
RaiseLocalEvent(ent.Owner, ref ev);
if (ev.Cancelled)
@ -139,7 +139,7 @@ public sealed class ItemToggleSystem : EntitySystem
}
else
{
var ev = new ItemToggleDeactivateAttemptEvent(args.User);
var ev = new ItemToggleActivateAttemptEvent(args.User);
RaiseLocalEvent(ent.Owner, ref ev);
if (ev.Cancelled)

View file

@ -8,69 +8,61 @@ namespace Content.Shared.Light.Components;
/// Component that represents a light bulb. Can be broken, or burned, which turns them mostly useless.
/// TODO: Breaking and burning should probably be moved to another component eventually.
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class LightBulbComponent : Component
{
/// <summary>
/// The color of the lightbulb and the light it produces.
/// </summary>
[DataField("color")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public Color Color = Color.White;
/// <summary>
/// The type of lightbulb. Tube/bulb/etc...
/// </summary>
[DataField("bulb")]
[ViewVariables(VVAccess.ReadWrite)]
public LightBulbType Type = LightBulbType.Tube;
/// <summary>
/// The initial state of the lightbulb.
/// </summary>
[DataField("startingState")]
[DataField("startingState"), AutoNetworkedField]
public LightBulbState State = LightBulbState.Normal;
/// <summary>
/// The temperature the air around the lightbulb is exposed to when the lightbulb burns out.
/// </summary>
[DataField("BurningTemperature")]
[ViewVariables(VVAccess.ReadWrite)]
public int BurningTemperature = 1400;
/// <summary>
/// Relates to how bright the light produced by the lightbulb is.
/// </summary>
[DataField("lightEnergy")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float LightEnergy = 0.8f;
/// <summary>
/// The maximum radius of the point light source this light produces.
/// </summary>
[DataField("lightRadius")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float LightRadius = 10;
/// <summary>
/// Relates to the falloff constant of the light produced by the lightbulb.
/// </summary>
[DataField("lightSoftness")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float LightSoftness = 1;
/// <summary>
/// The amount of power used by the lightbulb when it's active.
/// </summary>
[DataField("PowerUse")]
[ViewVariables(VVAccess.ReadWrite)]
public int PowerUse = 60;
/// <summary>
/// The sound produced when the lightbulb breaks.
/// </summary>
[DataField("breakSound")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public SoundSpecifier BreakSound = new SoundCollectionSpecifier("GlassBreak", AudioParams.Default.WithVolume(-6f));
#region Appearance
@ -78,22 +70,19 @@ public sealed partial class LightBulbComponent : Component
/// <summary>
/// The sprite state used when the lightbulb is intact.
/// </summary>
[DataField("normalSpriteState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string NormalSpriteState = "normal";
/// <summary>
/// The sprite state used when the lightbulb is broken.
/// </summary>
[DataField("brokenSpriteState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string BrokenSpriteState = "broken";
/// <summary>
/// The sprite state used when the lightbulb is burned.
/// </summary>
[DataField("burnedSpriteState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string BurnedSpriteState = "burned";
#endregion Appearance

View file

@ -1,3 +1,4 @@
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Content.Shared.Storage;
using Robust.Shared.Audio;

View file

@ -1,80 +1,87 @@
using Content.Server.Light.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.DeviceLinking;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Light.Components
namespace Content.Shared.Light.Components
{
/// <summary>
/// Component that represents a wall light. It has a light bulb that can be replaced when broken.
/// </summary>
[RegisterComponent, Access(typeof(PoweredLightSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause, Access(typeof(SharedPoweredLightSystem))]
public sealed partial class PoweredLightComponent : Component
{
[DataField("burnHandSound")]
/*
* Stop adding more fields, use components or I will shed you.
*/
[DataField]
public SoundSpecifier BurnHandSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
[DataField("turnOnSound")]
[DataField]
public SoundSpecifier TurnOnSound = new SoundPathSpecifier("/Audio/Machines/light_tube_on.ogg");
[DataField("hasLampOnSpawn", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? HasLampOnSpawn = null;
// Should be using containerfill?
[DataField]
public EntProtoId? HasLampOnSpawn = null;
[DataField("bulb")]
public LightBulbType BulbType;
[DataField("on")]
[DataField, AutoNetworkedField]
public bool On = true;
[DataField("ignoreGhostsBoo")]
[DataField]
public bool IgnoreGhostsBoo;
[DataField("ghostBlinkingTime")]
[DataField]
public TimeSpan GhostBlinkingTime = TimeSpan.FromSeconds(10);
[DataField("ghostBlinkingCooldown")]
[DataField]
public TimeSpan GhostBlinkingCooldown = TimeSpan.FromSeconds(60);
[ViewVariables]
public ContainerSlot LightBulbContainer = default!;
[ViewVariables]
[AutoNetworkedField]
public bool CurrentLit;
[ViewVariables]
[DataField, AutoNetworkedField]
public bool IsBlinking;
[ViewVariables]
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan LastThunk;
[ViewVariables]
[DataField, AutoPausedField]
public TimeSpan? LastGhostBlink;
[DataField("onPort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string OnPort = "On";
[DataField]
public ProtoId<SinkPortPrototype> OnPort = "On";
[DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string OffPort = "Off";
[DataField]
public ProtoId<SinkPortPrototype> OffPort = "Off";
[DataField("togglePort", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string TogglePort = "Toggle";
[DataField]
public ProtoId<SinkPortPrototype> TogglePort = "Toggle";
/// <summary>
/// How long it takes to eject a bulb from this
/// </summary>
[DataField("ejectBulbDelay")]
[DataField]
public float EjectBulbDelay = 2;
/// <summary>
/// Shock damage done to a mob that hits the light with an unarmed attack
/// </summary>
[DataField("unarmedHitShock")]
[DataField]
public int UnarmedHitShock = 20;
/// <summary>
/// Stun duration applied to a mob that hits the light with an unarmed attack
/// </summary>
[DataField("unarmedHitStun")]
[DataField]
public TimeSpan UnarmedHitStun = TimeSpan.FromSeconds(5);
}
}

View file

@ -0,0 +1,84 @@
using Content.Shared.Destructible;
using Content.Shared.Light.Components;
using Content.Shared.Throwing;
using Robust.Shared.Audio.Systems;
namespace Content.Shared.Light.EntitySystems;
public abstract class SharedLightBulbSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LightBulbComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<LightBulbComponent, LandEvent>(HandleLand);
SubscribeLocalEvent<LightBulbComponent, BreakageEventArgs>(OnBreak);
}
private void OnInit(EntityUid uid, LightBulbComponent bulb, ComponentInit args)
{
// update default state of bulbs
SetColor(uid, bulb.Color, bulb);
SetState(uid, bulb.State, bulb);
}
private void HandleLand(EntityUid uid, LightBulbComponent bulb, ref LandEvent args)
{
PlayBreakSound(uid, bulb);
SetState(uid, LightBulbState.Broken, bulb);
}
private void OnBreak(EntityUid uid, LightBulbComponent component, BreakageEventArgs args)
{
SetState(uid, LightBulbState.Broken, component);
}
/// <summary>
/// Set a new color for a light bulb and raise event about change
/// </summary>
public void SetColor(EntityUid uid, Color color, LightBulbComponent? bulb = null)
{
if (!Resolve(uid, ref bulb) || bulb.Color.Equals(color))
return;
bulb.Color = color;
Dirty(uid, bulb);
UpdateAppearance(uid, bulb);
}
/// <summary>
/// Set a new state for a light bulb (broken, burned) and raise event about change
/// </summary>
public void SetState(EntityUid uid, LightBulbState state, LightBulbComponent? bulb = null)
{
if (!Resolve(uid, ref bulb) || bulb.State == state)
return;
bulb.State = state;
Dirty(uid, bulb);
UpdateAppearance(uid, bulb);
}
public void PlayBreakSound(EntityUid uid, LightBulbComponent? bulb = null, EntityUid? user = null)
{
if (!Resolve(uid, ref bulb))
return;
_audio.PlayPredicted(bulb.BreakSound, uid, user: user);
}
private void UpdateAppearance(EntityUid uid, LightBulbComponent? bulb = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref bulb, ref appearance, logMissing: false))
return;
// try to update appearance and color
_appearance.SetData(uid, LightBulbVisuals.State, bulb.State, appearance);
_appearance.SetData(uid, LightBulbVisuals.Color, bulb.Color, appearance);
}
}

View file

@ -0,0 +1,425 @@
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.DoAfter;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Light.Components;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.Light.EntitySystems;
public abstract class SharedPoweredLightSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly DamageOnInteractSystem _damageOnInteractSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedLightBulbSystem _bulbSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedDeviceLinkSystem _deviceLink = default!;
private static readonly TimeSpan ThunkDelay = TimeSpan.FromSeconds(2);
public const string LightBulbContainer = "light_bulb";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PoweredLightComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<PoweredLightComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<PoweredLightComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<PoweredLightComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<PoweredLightComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<PoweredLightComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<PoweredLightComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<PoweredLightComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<PoweredLightComponent, PoweredLightDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
}
private void OnInit(EntityUid uid, PoweredLightComponent light, ComponentInit args)
{
light.LightBulbContainer = ContainerSystem.EnsureContainer<ContainerSlot>(uid, LightBulbContainer);
_deviceLink.EnsureSinkPorts(uid, light.OnPort, light.OffPort, light.TogglePort);
}
private void OnRemoved(Entity<PoweredLightComponent> light, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != LightBulbContainer)
return;
UpdateLight(light, light);
}
private void OnInserted(Entity<PoweredLightComponent> light, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != LightBulbContainer)
return;
UpdateLight(light, light);
}
private void OnInteractUsing(EntityUid uid, PoweredLightComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
args.Handled = InsertBulb(uid, args.Used, component, user: args.User, playAnimation: true);
}
private void OnInteractHand(EntityUid uid, PoweredLightComponent light, InteractHandEvent args)
{
if (args.Handled)
return;
// check if light has bulb to eject
var bulbUid = GetBulb(uid, light);
if (bulbUid == null)
return;
var userUid = args.User;
//removing a broken/burned bulb, so allow instant removal
if (TryComp<LightBulbComponent>(bulbUid.Value, out var bulb) && bulb.State != LightBulbState.Normal)
{
args.Handled = EjectBulb(uid, userUid, light) != null;
return;
}
// removing a working bulb, so require a delay
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, userUid, light.EjectBulbDelay, new PoweredLightDoAfterEvent(), uid, target: uid)
{
BreakOnMove = true,
BreakOnDamage = true,
});
args.Handled = true;
}
private void OnSignalReceived(Entity<PoweredLightComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port == ent.Comp.OffPort)
SetState(ent, false, ent.Comp);
else if (args.Port == ent.Comp.OnPort)
SetState(ent, true, ent.Comp);
else if (args.Port == ent.Comp.TogglePort)
ToggleLight(ent, ent.Comp);
}
/// <summary>
/// Turns the light on or of when receiving a <see cref="DeviceNetworkConstants.CmdSetState"/> command.
/// The light is turned on or of according to the <see cref="DeviceNetworkConstants.StateEnabled"/> value
/// </summary>
private void OnPacketReceived(EntityUid uid, PoweredLightComponent component, DeviceNetworkPacketEvent args)
{
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command) || command != DeviceNetworkConstants.CmdSetState) return;
if (!args.Data.TryGetValue(DeviceNetworkConstants.StateEnabled, out bool enabled)) return;
SetState(uid, enabled, component);
}
/// <summary>
/// Inserts the bulb if possible.
/// </summary>
/// <returns>True if it could insert it, false if it couldn't.</returns>
public bool InsertBulb(EntityUid uid, EntityUid bulbUid, PoweredLightComponent? light = null, EntityUid? user = null, bool playAnimation = false)
{
if (!Resolve(uid, ref light))
return false;
// check if light already has bulb
if (GetBulb(uid, light) != null)
return false;
// check if bulb fits
if (!TryComp<LightBulbComponent>(bulbUid, out var lightBulb))
return false;
if (lightBulb.Type != light.BulbType)
return false;
// try to insert bulb in container
if (!ContainerSystem.Insert(bulbUid, light.LightBulbContainer))
return false;
if (playAnimation && TryComp(user, out TransformComponent? xform))
{
var itemXform = Transform(uid);
_storage.PlayPickupAnimation(bulbUid, xform.Coordinates, itemXform.Coordinates, itemXform.LocalRotation, user: user);
}
return true;
}
/// <summary>
/// Ejects the bulb to a mob's hand if possible.
/// </summary>
/// <returns>Bulb uid if it was successfully ejected, null otherwise</returns>
public EntityUid? EjectBulb(EntityUid uid, EntityUid? userUid = null, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return null;
// check if light has bulb
if (GetBulb(uid, light) is not { Valid: true } bulb)
return null;
// try to remove bulb from container
if (!ContainerSystem.Remove(bulb, light.LightBulbContainer))
return null;
// try to place bulb in hands
_handsSystem.PickupOrDrop(userUid, bulb);
return bulb;
}
/// <summary>
/// Replaces the spawned prototype of a pre-mapinit powered light with a different variant.
/// </summary>
public bool ReplaceSpawnedPrototype(Entity<PoweredLightComponent> light, string bulb)
{
if (light.Comp.LightBulbContainer.ContainedEntity != null)
return false;
if (LifeStage(light.Owner) >= EntityLifeStage.MapInitialized)
return false;
light.Comp.HasLampOnSpawn = bulb;
return true;
}
/// <summary>
/// Try to replace current bulb with a new one
/// If succeed old bulb just drops on floor
/// </summary>
public bool ReplaceBulb(EntityUid uid, EntityUid bulb, PoweredLightComponent? light = null)
{
EjectBulb(uid, null, light);
return InsertBulb(uid, bulb, light);
}
/// <summary>
/// Try to get light bulb inserted in powered light
/// </summary>
/// <returns>Bulb uid if it exist, null otherwise</returns>
public EntityUid? GetBulb(EntityUid uid, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return null;
return light.LightBulbContainer?.ContainedEntity;
}
/// <summary>
/// Try to break bulb inside light fixture
/// </summary>
public bool TryDestroyBulb(EntityUid uid, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light, false))
return false;
// if we aren't mapinited,
// just null the spawned bulb
if (LifeStage(uid) < EntityLifeStage.MapInitialized)
{
light.HasLampOnSpawn = null;
return true;
}
// check bulb state
var bulbUid = GetBulb(uid, light);
if (bulbUid == null || !EntityManager.TryGetComponent(bulbUid.Value, out LightBulbComponent? lightBulb))
return false;
if (lightBulb.State == LightBulbState.Broken)
return false;
// break it
_bulbSystem.SetState(bulbUid.Value, LightBulbState.Broken, lightBulb);
_bulbSystem.PlayBreakSound(bulbUid.Value, lightBulb);
UpdateLight(uid, light);
return true;
}
protected void UpdateLight(EntityUid uid,
PoweredLightComponent? light = null,
SharedApcPowerReceiverComponent? powerReceiver = null,
AppearanceComponent? appearance = null,
EntityUid? user = null)
{
// We don't do anything during state application on the client as if
// it's due to an entity spawn, we'd have to wait for component init to
// be able to do anything, despite the server having already sent us the
// state that we need. On the other hand, we still want this to run in
// prediction so we can, well, predict lights turning on.
if (GameTiming.ApplyingState)
return;
if (!Resolve(uid, ref light, false))
return;
if (!_receiver.ResolveApc(uid, ref powerReceiver))
return;
// Optional component.
Resolve(uid, ref appearance, false);
// check if light has bulb
var bulbUid = GetBulb(uid, light);
if (bulbUid == null || !TryComp<LightBulbComponent>(bulbUid.Value, out var lightBulb))
{
SetLight(uid, false, light: light);
powerReceiver.Load = 0;
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Empty, appearance);
return;
}
switch (lightBulb.State)
{
case LightBulbState.Normal:
if (powerReceiver.Powered && light.On)
{
SetLight(uid, true, lightBulb.Color, light, lightBulb.LightRadius, lightBulb.LightEnergy, lightBulb.LightSoftness);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.On, appearance);
var time = GameTiming.CurTime;
if (time > light.LastThunk + ThunkDelay)
{
light.LastThunk = time;
Dirty(uid, light);
_audio.PlayPredicted(light.TurnOnSound, uid, user: user, light.TurnOnSound.Params.AddVolume(-10f));
}
}
else
{
SetLight(uid, false, light: light);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Off, appearance);
}
break;
case LightBulbState.Broken:
SetLight(uid, false, light: light);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Broken, appearance);
break;
case LightBulbState.Burned:
SetLight(uid, false, light: light);
_appearance.SetData(uid, PoweredLightVisuals.BulbState, PoweredLightState.Burned, appearance);
break;
}
powerReceiver.Load = (light.On && lightBulb.State == LightBulbState.Normal) ? lightBulb.PowerUse : 0;
}
/// <summary>
/// Destroy the light bulb if the light took any damage.
/// </summary>
public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
{
// Was it being repaired, or did it take damage?
if (args.DamageIncreased)
{
// Eventually, this logic should all be done by this (or some other) system, not a component.
TryDestroyBulb(uid, component);
}
}
private void OnPowerChanged(EntityUid uid, PoweredLightComponent component, ref PowerChangedEvent args)
{
// TODO: Power moment
var metadata = MetaData(uid);
if (metadata.EntityPaused || TerminatingOrDeleted(uid, metadata))
return;
UpdateLight(uid, component);
}
public void ToggleBlinkingLight(EntityUid uid, PoweredLightComponent light, bool isNowBlinking)
{
if (light.IsBlinking == isNowBlinking)
return;
light.IsBlinking = isNowBlinking;
Dirty(uid, light);
if (!TryComp<AppearanceComponent>(uid, out var appearance))
return;
_appearance.SetData(uid, PoweredLightVisuals.Blinking, isNowBlinking, appearance);
}
private void SetLight(EntityUid uid, bool value, Color? color = null, PoweredLightComponent? light = null, float? radius = null, float? energy = null, float? softness = null)
{
if (!Resolve(uid, ref light))
return;
if (light.CurrentLit != value)
{
light.CurrentLit = value;
Dirty(uid, light);
}
_ambientSystem.SetAmbience(uid, value);
if (_pointLight.TryGetLight(uid, out var pointLight))
{
_pointLight.SetEnabled(uid, value, pointLight);
if (color != null)
_pointLight.SetColor(uid, color.Value, pointLight);
if (radius != null)
_pointLight.SetRadius(uid, (float)radius, pointLight);
if (energy != null)
_pointLight.SetEnergy(uid, (float)energy, pointLight);
if (softness != null)
_pointLight.SetSoftness(uid, (float)softness, pointLight);
}
// light bulbs burn your hands!
if (TryComp<DamageOnInteractComponent>(uid, out var damageOnInteractComp))
_damageOnInteractSystem.SetIsDamageActiveTo((uid, damageOnInteractComp), value);
}
public void ToggleLight(EntityUid uid, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return;
light.On = !light.On;
UpdateLight(uid, light);
}
public void SetState(EntityUid uid, bool state, PoweredLightComponent? light = null)
{
if (!Resolve(uid, ref light))
return;
light.On = state;
Dirty(uid, light);
UpdateLight(uid, light);
}
private void OnDoAfter(EntityUid uid, PoweredLightComponent component, DoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
EjectBulb(args.Args.Target.Value, args.Args.User, component);
args.Handled = true;
}
}

View file

@ -5,13 +5,19 @@ namespace Content.Shared.Lock;
/// <summary>
/// This is used for toggleable items that require the entity to have a lock in a certain state.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(LockSystem))]
public sealed partial class ItemToggleRequiresLockComponent : Component
{
/// <summary>
/// TRUE: the lock must be locked to toggle the item.
/// FALSE: the lock must be unlocked to toggle the item.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool RequireLocked;
/// <summary>
/// Popup text for when someone tries to toggle the item, but it's locked. If null, no popup will be shown.
/// </summary>
[DataField]
public LocId? LockedPopup = "lock-comp-generic-fail";
}

View file

@ -21,6 +21,18 @@ public sealed partial class LockComponent : Component
[AutoNetworkedField]
public bool Locked = true;
/// <summary>
/// If true, will show verbs to lock and unlock the item. Otherwise, it will not.
/// </summary>
[DataField, AutoNetworkedField]
public bool ShowLockVerbs = true;
/// <summary>
/// If true will show examine text.
/// </summary>
[DataField, AutoNetworkedField]
public bool ShowExamine = true;
/// <summary>
/// Whether or not the lock is locked by simply clicking.
/// </summary>
@ -50,7 +62,7 @@ public sealed partial class LockComponent : Component
/// The sound played when unlocked.
/// </summary>
[DataField("unlockingSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier UnlockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg")
public SoundSpecifier? UnlockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg")
{
Params = AudioParams.Default.WithVolume(-5f),
};
@ -59,7 +71,7 @@ public sealed partial class LockComponent : Component
/// The sound played when locked.
/// </summary>
[DataField("lockingSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier LockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg")
public SoundSpecifier? LockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg")
{
Params = AudioParams.Default.WithVolume(-5f)
};

View file

@ -29,12 +29,12 @@ public sealed class LockSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _sharedPopupSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
/// <inheritdoc />
public override void Initialize()
@ -55,8 +55,8 @@ public sealed class LockSystem : EntitySystem
SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel);
SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<ActivatableUIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<ActivatableUIRequiresLockComponent, LockToggledEvent>(LockToggled);
SubscribeLocalEvent<UIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<UIRequiresLockComponent, LockToggledEvent>(LockToggled);
SubscribeLocalEvent<ItemToggleRequiresLockComponent, ItemToggleActivateAttemptEvent>(OnActivateAttempt);
}
@ -97,6 +97,9 @@ public sealed class LockSystem : EntitySystem
private void OnExamined(EntityUid uid, LockComponent lockComp, ExaminedEvent args)
{
if (!lockComp.ShowExamine)
return;
args.PushText(Loc.GetString(lockComp.Locked
? "lock-comp-on-examined-is-locked"
: "lock-comp-on-examined-is-unlocked",
@ -246,6 +249,20 @@ public sealed class LockSystem : EntitySystem
return true;
}
/// <summary>
/// Toggle the lock to locked if unlocked, and unlocked if locked.
/// </summary>
/// <param name="uid">Entity to toggle the lock state of.</param>
/// <param name="user">The person trying to toggle the lock</param>
/// <param name="lockComp">Entities lock comp (will be resolved)</param>
public void ToggleLock(EntityUid uid, EntityUid? user, LockComponent? lockComp = null)
{
if (IsLocked((uid, lockComp)))
Unlock(uid, user, lockComp);
else
Lock(uid, user, lockComp);
}
/// <summary>
/// Returns true if the entity is locked.
/// Entities with no lock component are considered unlocked.
@ -304,11 +321,12 @@ public sealed class LockSystem : EntitySystem
private void AddToggleLockVerb(EntityUid uid, LockComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !args.CanComplexInteract)
if (!args.CanAccess || !args.CanInteract || !args.CanComplexInteract || !component.ShowLockVerbs)
return;
AlternativeVerb verb = new()
{
Disabled = !CanToggleLock(uid, args.User),
Act = component.Locked
? () => TryUnlock(uid, args.User, component)
: () => TryLock(uid, args.User, component),
@ -411,41 +429,54 @@ public sealed class LockSystem : EntitySystem
args.Cancel();
}
private void OnUIOpenAttempt(EntityUid uid, ActivatableUIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args)
private void OnUIOpenAttempt(EntityUid uid, UIRequiresLockComponent component, ActivatableUIOpenAttemptEvent args)
{
if (args.Cancelled)
return;
if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked != component.RequireLocked)
{
args.Cancel();
if (lockComp.Locked)
{
_sharedPopupSystem.PopupClient(Loc.GetString("entity-storage-component-locked-message"), uid, args.User);
}
if (!TryComp<LockComponent>(uid, out var lockComp) || lockComp.Locked == component.RequireLocked)
return;
_audio.PlayPredicted(component.AccessDeniedSound, uid, args.User);
args.Cancel();
if (lockComp.Locked && component.Popup != null)
{
_sharedPopupSystem.PopupClient(Loc.GetString(component.Popup), uid, args.User);
}
_audio.PlayPredicted(component.AccessDeniedSound, uid, args.User);
}
private void LockToggled(EntityUid uid, ActivatableUIRequiresLockComponent component, LockToggledEvent args)
private void LockToggled(EntityUid uid, UIRequiresLockComponent component, LockToggledEvent args)
{
if (!TryComp<LockComponent>(uid, out var lockComp) || lockComp.Locked == component.RequireLocked)
return;
_activatableUI.CloseAll(uid);
if (component.UserInterfaceKeys == null)
{
_ui.CloseUis(uid);
return;
}
foreach (var key in component.UserInterfaceKeys)
{
_ui.CloseUi(uid, key);
}
}
private void OnActivateAttempt(EntityUid uid, ItemToggleRequiresLockComponent component, ref ItemToggleActivateAttemptEvent args)
{
if (args.Cancelled)
return;
if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked != component.RequireLocked)
if (!TryComp<LockComponent>(uid, out var lockComp) || lockComp.Locked == component.RequireLocked)
return;
args.Cancelled = true;
if (lockComp.Locked && component.LockedPopup != null)
{
args.Cancelled = true;
if (lockComp.Locked)
_sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-generic-fail",
("target", Identity.Entity(uid, EntityManager))),
_sharedPopupSystem.PopupClient(Loc.GetString(component.LockedPopup,
("target", Identity.Entity(uid, EntityManager))),
uid,
args.User);
}

View file

@ -7,8 +7,15 @@ namespace Content.Shared.Lock;
/// This is used for activatable UIs that require the entity to have a lock in a certain state.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(LockSystem))]
public sealed partial class ActivatableUIRequiresLockComponent : Component
public sealed partial class UIRequiresLockComponent : Component
{
/// <summary>
/// UIs that are locked behind this component.
/// If null, will close all UIs.
/// </summary>
[DataField]
public List<Enum>? UserInterfaceKeys;
/// <summary>
/// TRUE: the lock must be locked to access the UI.
/// FALSE: the lock must be unlocked to access the UI.
@ -21,4 +28,7 @@ public sealed partial class ActivatableUIRequiresLockComponent : Component
/// </summary>
[DataField]
public SoundSpecifier? AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
[DataField]
public LocId? Popup = "entity-storage-component-locked-message";
}

View file

@ -0,0 +1,468 @@
using System.Numerics;
using Content.Shared.Access.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Clothing;
using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Station;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Medical.SuitSensors;
public abstract class SharedSuitSensorSystem : EntitySystem
{
[Dependency] private readonly SharedStationSystem _stationSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedIdCardSystem _idCardSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private EntityQuery<SuitSensorComponent> _sensorQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
SubscribeLocalEvent<SuitSensorComponent, ClothingGotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<SuitSensorComponent, ClothingGotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<SuitSensorComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<SuitSensorComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
SubscribeLocalEvent<SuitSensorComponent, SuitSensorChangeDoAfterEvent>(OnSuitSensorDoAfter);
_sensorQuery = GetEntityQuery<SuitSensorComponent>();
}
/// <summary>
/// Checks whether the sensor is assigned to a station or not
/// and tries to assign an unassigned sensor to a station if it's currently on a grid.
/// </summary>
/// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
public bool CheckSensorAssignedStation(Entity<SuitSensorComponent> sensor)
{
if (!sensor.Comp.StationId.HasValue && Transform(sensor.Owner).GridUid == null)
return false;
sensor.Comp.StationId = _stationSystem.GetOwningStation(sensor.Owner);
Dirty(sensor);
return sensor.Comp.StationId.HasValue;
}
private void OnMapInit(Entity<SuitSensorComponent> ent, ref MapInitEvent args)
{
// Fallback
ent.Comp.StationId ??= _stationSystem.GetOwningStation(ent.Owner);
// generate random mode
if (ent.Comp.RandomMode)
{
//make the sensor mode favor higher levels, except coords.
var modesDist = new[]
{
SuitSensorMode.SensorOff,
SuitSensorMode.SensorBinary, SuitSensorMode.SensorBinary,
SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals, SuitSensorMode.SensorVitals,
SuitSensorMode.SensorCords, SuitSensorMode.SensorCords
};
ent.Comp.Mode = _random.Pick(modesDist);
}
ent.Comp.NextUpdate = _timing.CurTime;
Dirty(ent);
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent ev)
{
// If the player spawns in arrivals then the grid underneath them may not be appropriate.
// in which case we'll just use the station spawn code told us they are attached to and set all of their
// sensors.
RecursiveSensor(ev.Mob, ev.Station);
}
private void RecursiveSensor(EntityUid uid, EntityUid stationUid)
{
var xform = Transform(uid);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
if (_sensorQuery.TryComp(child, out var sensor))
{
sensor.StationId = stationUid;
Dirty(child, sensor);
}
RecursiveSensor(child, stationUid);
}
}
private void OnEquipped(Entity<SuitSensorComponent> ent, ref ClothingGotEquippedEvent args)
{
ent.Comp.User = args.Wearer;
Dirty(ent);
}
private void OnUnequipped(Entity<SuitSensorComponent> ent, ref ClothingGotUnequippedEvent args)
{
ent.Comp.User = null;
Dirty(ent);
}
private void OnExamine(Entity<SuitSensorComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
string msg;
switch (ent.Comp.Mode)
{
case SuitSensorMode.SensorOff:
msg = "suit-sensor-examine-off";
break;
case SuitSensorMode.SensorBinary:
msg = "suit-sensor-examine-binary";
break;
case SuitSensorMode.SensorVitals:
msg = "suit-sensor-examine-vitals";
break;
case SuitSensorMode.SensorCords:
msg = "suit-sensor-examine-cords";
break;
default:
return;
}
args.PushMarkup(Loc.GetString(msg));
}
private void OnVerb(Entity<SuitSensorComponent> ent, ref GetVerbsEvent<Verb> args)
{
// check if user can change sensor
if (ent.Comp.ControlsLocked)
return;
// standard interaction checks
if (!args.CanInteract || args.Hands == null)
return;
if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target))
return;
// check if target is incapacitated (cuffed, dead, etc)
if (ent.Comp.User != null && args.User != ent.Comp.User && _actionBlocker.CanInteract(ent.Comp.User.Value, null))
return;
args.Verbs.UnionWith(new[]
{
CreateVerb(ent, args.User, SuitSensorMode.SensorOff),
CreateVerb(ent, args.User, SuitSensorMode.SensorBinary),
CreateVerb(ent, args.User, SuitSensorMode.SensorVitals),
CreateVerb(ent, args.User, SuitSensorMode.SensorCords)
});
}
private void OnInsert(Entity<SuitSensorComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
if (args.Container.ID != ent.Comp.ActivationContainer)
return;
ent.Comp.User = args.Container.Owner;
Dirty(ent);
}
private void OnRemove(Entity<SuitSensorComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
if (args.Container.ID != ent.Comp.ActivationContainer)
return;
ent.Comp.User = null;
Dirty(ent);
}
private Verb CreateVerb(Entity<SuitSensorComponent> ent, EntityUid userUid, SuitSensorMode mode)
{
return new Verb()
{
Text = GetModeName(mode),
Disabled = ent.Comp.Mode == mode,
Priority = -(int)mode, // sort them in descending order
Category = VerbCategory.SetSensor,
Act = () => TrySetSensor(ent.AsNullable(), mode, userUid)
};
}
public string GetModeName(SuitSensorMode mode)
{
string name;
switch (mode)
{
case SuitSensorMode.SensorOff:
name = "suit-sensor-mode-off";
break;
case SuitSensorMode.SensorBinary:
name = "suit-sensor-mode-binary";
break;
case SuitSensorMode.SensorVitals:
name = "suit-sensor-mode-vitals";
break;
case SuitSensorMode.SensorCords:
name = "suit-sensor-mode-cords";
break;
default:
return "";
}
return Loc.GetString(name);
}
/// <summary>
/// Attempts to set <see cref="SuitSensorComponent"/> mode of the entity to the selected in params.
/// Works instantly if the user is the player wearing the sensors and will start a DoAfter otherwise.
/// </summary>
/// <param name="sensors">Entity and its component that should be changed.</param>
/// <param name="mode">Selected mode</param>
/// <param name="userUid">userUid, when not equal to the <see cref="SuitSensorComponent.User"/>, creates doafter</param>
public bool TrySetSensor(Entity<SuitSensorComponent?> sensors, SuitSensorMode mode, EntityUid userUid)
{
if (!Resolve(sensors, ref sensors.Comp, false))
return false;
if (sensors.Comp.User == null || userUid == sensors.Comp.User)
SetSensor(sensors, mode, userUid);
else
{
var doAfterEvent = new SuitSensorChangeDoAfterEvent(mode);
var doAfterArgs = new DoAfterArgs(EntityManager, userUid, sensors.Comp.SensorsTime, doAfterEvent, sensors)
{
BreakOnMove = true,
BreakOnDamage = true
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
}
return true;
}
private void OnSuitSensorDoAfter(Entity<SuitSensorComponent> sensors, ref SuitSensorChangeDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
SetSensor(sensors.AsNullable(), args.Mode, args.User);
}
/// <summary>
/// Sets mode of the <see cref="SuitSensorComponent"/> of the chosen entity.
/// Makes popup when <param name="userUid"> not null
/// </summary>
/// <param name="sensors">Entity and it's component that should be changed</param>
/// <param name="mode">Selected mode</param>
/// <param name="userUid">uid, required for the popup</param>
public void SetSensor(Entity<SuitSensorComponent?> sensors, SuitSensorMode mode, EntityUid? userUid = null)
{
if (!Resolve(sensors, ref sensors.Comp, false))
return;
sensors.Comp.Mode = mode;
Dirty(sensors);
if (userUid != null)
{
var msg = Loc.GetString("suit-sensor-mode-state", ("mode", GetModeName(mode)));
_popupSystem.PopupClient(msg, sensors, userUid.Value);
}
}
/// <summary>
/// Set all suit sensors on the equipment someone is wearing to the specified mode.
/// </summary>
public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots = SlotFlags.All)
{
// iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator(target, slots);
while (slotEnumerator.NextItem(out var item, out _))
{
if (TryComp<SuitSensorComponent>(item, out var sensorComp))
SetSensor((item, sensorComp), mode);
}
}
/// <summary>
/// Attempts to get full <see cref="SuitSensorStatus"/> from the <see cref="SuitSensorComponent"/>
/// </summary>
/// <param name="uid">Entity to get status</param>
/// <returns>Full <see cref="SuitSensorStatus"/> of the chosen uid</returns>
public SuitSensorStatus? GetSensorState(Entity<SuitSensorComponent?, TransformComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return null;
var sensor = ent.Comp1;
var transform = ent.Comp2;
// check if sensor is enabled and worn by user
if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || !HasComp<MobStateComponent>(sensor.User) || transform.GridUid == null)
return null;
// try to get mobs id from ID slot
var userName = Loc.GetString("suit-sensor-component-unknown-name");
var userJob = Loc.GetString("suit-sensor-component-unknown-job");
var userJobIcon = "JobIconNoId";
var userJobDepartments = new List<string>();
if (_idCardSystem.TryFindIdCard(sensor.User.Value, out var card))
{
if (card.Comp.FullName != null)
userName = card.Comp.FullName;
if (card.Comp.LocalizedJobTitle != null)
userJob = card.Comp.LocalizedJobTitle;
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
}
// get health mob state
var isAlive = false;
if (TryComp(sensor.User.Value, out MobStateComponent? mobState))
isAlive = !_mobStateSystem.IsDead(sensor.User.Value, mobState);
// get mob total damage
var totalDamage = 0;
if (TryComp<DamageableComponent>(sensor.User.Value, out var damageable))
totalDamage = damageable.TotalDamage.Int();
// Get mob total damage crit threshold
int? totalDamageThreshold = null;
if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
totalDamageThreshold = critThreshold.Value.Int();
// finally, form suit sensor status
var status = new SuitSensorStatus(GetNetEntity(sensor.User.Value), GetNetEntity(ent.Owner), userName, userJob, userJobIcon, userJobDepartments);
switch (sensor.Mode)
{
case SuitSensorMode.SensorBinary:
status.IsAlive = isAlive;
break;
case SuitSensorMode.SensorVitals:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
status.TotalDamageThreshold = totalDamageThreshold;
break;
case SuitSensorMode.SensorCords:
status.IsAlive = isAlive;
status.TotalDamage = totalDamage;
status.TotalDamageThreshold = totalDamageThreshold;
EntityCoordinates coordinates;
var xformQuery = GetEntityQuery<TransformComponent>();
if (transform.GridUid != null)
{
coordinates = new EntityCoordinates(transform.GridUid.Value,
Vector2.Transform(_transform.GetWorldPosition(transform, xformQuery),
_transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery)));
}
else if (transform.MapUid != null)
{
coordinates = new EntityCoordinates(transform.MapUid.Value,
_transform.GetWorldPosition(transform, xformQuery));
}
else
{
coordinates = EntityCoordinates.Invalid;
}
status.Coordinates = GetNetCoordinates(coordinates);
break;
}
return status;
}
/// <summary>
/// Create a device network package from the suit sensors status.
/// </summary>
public NetworkPayload SuitSensorToPacket(SuitSensorStatus status)
{
var payload = new NetworkPayload()
{
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
[SuitSensorConstants.NET_NAME] = status.Name,
[SuitSensorConstants.NET_JOB] = status.Job,
[SuitSensorConstants.NET_JOB_ICON] = status.JobIcon,
[SuitSensorConstants.NET_JOB_DEPARTMENTS] = status.JobDepartments,
[SuitSensorConstants.NET_IS_ALIVE] = status.IsAlive,
[SuitSensorConstants.NET_SUIT_SENSOR_UID] = status.SuitSensorUid,
[SuitSensorConstants.NET_OWNER_UID] = status.OwnerUid,
};
if (status.TotalDamage != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
if (status.TotalDamageThreshold != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, status.TotalDamageThreshold);
if (status.Coordinates != null)
payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
return payload;
}
/// <summary>
/// Try to create the suit sensors status from the device network message.
/// </summary>
public SuitSensorStatus? PacketToSuitSensor(NetworkPayload payload)
{
// check command
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return null;
if (command != DeviceNetworkConstants.CmdUpdatedState)
return null;
// check name, job and alive
if (!payload.TryGetValue(SuitSensorConstants.NET_NAME, out string? name)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB, out string? job)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_ICON, out string? jobIcon)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_JOB_DEPARTMENTS, out List<string>? jobDepartments)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_IS_ALIVE, out bool? isAlive)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_SUIT_SENSOR_UID, out NetEntity suitSensorUid)) return null;
if (!payload.TryGetValue(SuitSensorConstants.NET_OWNER_UID, out NetEntity ownerUid)) return null;
// try get total damage and cords (optionals)
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE_THRESHOLD, out int? totalDamageThreshold);
payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out NetCoordinates? coords);
var status = new SuitSensorStatus(ownerUid, suitSensorUid, name, job, jobIcon, jobDepartments)
{
IsAlive = isAlive.Value,
TotalDamage = totalDamage,
TotalDamageThreshold = totalDamageThreshold,
Coordinates = coords,
};
return status;
}
}

View file

@ -1,14 +1,16 @@
using Content.Shared.Medical.SuitSensor;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Medical.SuitSensors;
namespace Content.Shared.Medical.SuitSensors;
/// <summary>
/// Tracking device, embedded in almost all uniforms and jumpsuits.
/// If enabled, will report to crew monitoring console owners position and status.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(SuitSensorSystem))]
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedSuitSensorSystem))]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class SuitSensorComponent : Component
{
/// <summary>
@ -20,7 +22,7 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// If true user can't change suit sensor mode
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool ControlsLocked = false;
/// <summary>
@ -32,7 +34,7 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// Current sensor mode. Can be switched by user verbs.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public SuitSensorMode Mode = SuitSensorMode.SensorOff;
/// <summary>
@ -56,7 +58,7 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// Current user that wears suit sensor. Null if nobody wearing it.
/// </summary>
[ViewVariables]
[DataField, AutoNetworkedField]
public EntityUid? User = null;
/// <summary>
@ -69,7 +71,7 @@ public sealed partial class SuitSensorComponent : Component
/// <summary>
/// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work.
/// </summary>
[DataField("station")]
[DataField("station"), AutoNetworkedField]
public EntityUid? StationId = null;
/// <summary>

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