diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..8554c97ee8
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,40 @@
+# Space Station 14 Code of Conduct
+
+Space Station 14's staff and community is made up volunteers from all over the world, working on every aspect of the project - including development, teaching, and hosting integral tools.
+
+Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to all levels of the project, from commenters to contributors to staff.
+
+This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
+
+This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here.
+
+If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com).
+
+- **Be friendly and patient.**
+- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
+- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and contributors, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. We have contributors of all skill levels, some even making their first foray into a new field with this project, so keep that in mind when discussing someone's work.
+- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Space Station 14 community should be respectful when dealing with other members as well as with people outside the Space Station 14 community. Assume contributions to the project, even those that do not end up being included, are made in good faith.
+- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
+ - Violent threats or language directed against another person.
+ - Discriminatory jokes and language.
+ - Posting sexually explicit or violent material.
+ - Posting (or threatening to post) other people's personally identifying information ("doxing").
+ - Personal insults, especially those using racist or sexist terms.
+ - Unwelcome sexual attention.
+ - Advocating for, or encouraging, any of the above behavior.
+ - Repeated harassment of others. In general, if someone asks you to stop, then stop.
+- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Space Station 14 is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Space Station 14 comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to make mistakes and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
+
+Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
+
+## On Comunity Moderation
+
+Deviating from the Code of Conduct on the Github repository may result in moderative actions taken by project Maintainers. This can involve your content being edited or deleted, and may result in a temporary or permanent block from the repository.
+
+This is to ensure Space Station 14 is a healthy community in which contributors feel encouraged and empowered to contribute, and to give you as a member of this community a chance to reflect on how you are interacting with it. While outright offensive and bigoted content will *always* be unacceptable on the repository, Maintainers are at liberty to take moderative actions against more ambiguous content that fail to provide constructive criticism, or that provides constructive criticism in a non-constructive manner. Examples of this include using hyperbole, bringing up PRs/changes unrelated to the discussion at hand, hostile tone, off-topic comments, creating PRs/Issues for the sole purpose of causing discussions, skirting the line of acceptable behavior, etc. Disagreeing with content or each other is fine and appreciated, but only as long as it's done with respect and in a constructive manner.
+
+Maintainers are expected to adhere to the guidelines as listed in the [Github Moderation Guidelines](https://docs.spacestation14.com/en/general-development/github-moderation-guidelines.html), though may deviate should they feel it's in the best interest of the community. If you believe you had an action incorrectly applied against you, you are encouraged to contact staff via [Discord](https://discord.ss14.io/) or [the forums](https://forum.spacestation14.com/), [appeal your Github ban](https://forum.spacestation14.com/c/ban-appeals/appeals-github/38), or make a [staff complaint](https://forum.spacestation14.com/t/staff-complaint-instructions-and-info/31).
+
+## Attribution
+
+This Code of Conduct is an edited version of the [Django Code of Conduct](https://www.djangoproject.com/conduct/), licensed under CC BY 3.0, for the Space Station 14 Github repository.
diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index 2bb71bb59d..de788234e5 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -47,7 +47,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
- public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
+ public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;
diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml
new file mode 100644
index 0000000000..84d581487d
--- /dev/null
+++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
new file mode 100644
index 0000000000..da68653ce5
--- /dev/null
+++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
@@ -0,0 +1,449 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.Access.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class GroupedAccessLevelChecklist : BoxContainer
+{
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+ private bool _isMonotone;
+ private string? _labelStyleClass;
+
+ // Access data
+ private HashSet> _accessGroups = new();
+ private HashSet> _accessLevels = new();
+ private HashSet> _activeAccessLevels = new();
+
+ // Button groups
+ private readonly ButtonGroup _accessGroupsButtons = new();
+
+ // Temp values
+ private int _accessGroupTabIndex = 0;
+ private bool _canInteract = false;
+ private List _accessLevelsForTab = new();
+ private readonly List _accessLevelEntries = new();
+ private readonly Dictionary> _groupedAccessLevels = new();
+
+ // Events
+ public event Action>, bool>? OnAccessLevelsChangedEvent;
+
+ ///
+ /// Creates a UI control for changing access levels.
+ /// Access levels are organized under a list of tabs by their associated access group.
+ ///
+ public GroupedAccessLevelChecklist()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ private void ArrangeAccessControls()
+ {
+ // Create a list of known access groups with which to populate the UI
+ _groupedAccessLevels.Clear();
+
+ foreach (var accessGroup in _accessGroups)
+ {
+ if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
+ continue;
+
+ _groupedAccessLevels.Add(accessGroupProto, new());
+ }
+
+ // Ensure that the 'general' access group is added to handle
+ // misc. access levels that aren't associated with any group
+ if (_protoManager.TryIndex("General", out var generalAccessProto))
+ _groupedAccessLevels.TryAdd(generalAccessProto, new());
+
+ // Assign known access levels with their associated groups
+ foreach (var accessLevel in _accessLevels)
+ {
+ if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
+ continue;
+
+ var assigned = false;
+
+ foreach (var (accessGroup, accessLevels) in _groupedAccessLevels)
+ {
+ if (!accessGroup.Tags.Contains(accessLevelProto.ID))
+ continue;
+
+ assigned = true;
+ _groupedAccessLevels[accessGroup].Add(accessLevelProto);
+ }
+
+ if (!assigned && generalAccessProto != null)
+ _groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
+ }
+
+ // Remove access groups that have no assigned access levels
+ foreach (var (group, accessLevels) in _groupedAccessLevels)
+ {
+ if (accessLevels.Count == 0)
+ _groupedAccessLevels.Remove(group);
+ }
+ }
+
+ private bool TryRebuildAccessGroupControls()
+ {
+ AccessGroupList.DisposeAllChildren();
+ AccessLevelChecklist.DisposeAllChildren();
+
+ // No access level prototypes were assigned to any of the access level groups.
+ // Either the turret controller has no assigned access levels or their names were invalid.
+ if (_groupedAccessLevels.Count == 0)
+ return false;
+
+ // Reorder the access groups alphabetically
+ var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
+
+ // Add group access buttons to the UI
+ foreach (var accessGroup in orderedAccessGroups)
+ {
+ var accessGroupButton = CreateAccessGroupButton();
+
+ // Button styling
+ if (_groupedAccessLevels.Count > 1)
+ {
+ if (AccessGroupList.ChildCount == 0)
+ accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft);
+ else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
+ accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight);
+ else
+ accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth);
+ }
+
+ accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup);
+
+ // Label text and styling
+ if (_labelStyleClass != null)
+ accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass);
+
+ var accessLevelPrototypes = _groupedAccessLevels[accessGroup];
+ var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x))
+ ? "»"
+ : accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x))
+ ? "›"
+ : " ";
+
+ var text = Loc.GetString(
+ "turret-controls-window-access-group-label",
+ ("prefix", prefix),
+ ("label", accessGroup.GetAccessGroupName())
+ );
+
+ accessGroupButton.Text = text;
+
+ // Button events
+ accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent());
+
+ AccessGroupList.AddChild(accessGroupButton);
+ }
+
+ // Adjust the current tab index so it remains in range
+ if (_accessGroupTabIndex >= _groupedAccessLevels.Count)
+ _accessGroupTabIndex = _groupedAccessLevels.Count - 1;
+
+ return true;
+ }
+
+ ///
+ /// Rebuilds the checkbox list for the access level controls.
+ ///
+ public void RebuildAccessLevelsControls()
+ {
+ AccessLevelChecklist.DisposeAllChildren();
+ _accessLevelEntries.Clear();
+
+ // No access level prototypes were assigned to any of the access level groups
+ // Either turret controller has no assigned access levels, or their names were invalid
+ if (_groupedAccessLevels.Count == 0)
+ return;
+
+ // Reorder the access groups alphabetically
+ var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
+
+ // Get the access levels associated with the current tab
+ var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex];
+ _accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto];
+ _accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList();
+
+ // Add an 'all' checkbox as the first child of the list if it has more than one access level
+ // Toggling this checkbox on will mark all other boxes below it on/off
+ var allCheckBox = CreateAccessLevelCheckbox();
+ allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox");
+
+ if (_labelStyleClass != null)
+ allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
+
+ // Add the 'all' checkbox events
+ allCheckBox.OnPressed += args =>
+ {
+ SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed);
+
+ var accessLevels = new HashSet>();
+
+ foreach (var accessLevel in _accessLevelsForTab)
+ {
+ accessLevels.Add(accessLevel);
+ }
+
+ OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed);
+ };
+
+ AccessLevelChecklist.AddChild(allCheckBox);
+
+ // Hide the 'all' checkbox if the tab has only one access level
+ var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
+
+ allCheckBox.Visible = allCheckBoxVisible;
+ allCheckBox.Disabled = !_canInteract;
+
+ // Add any remaining missing access level buttons to the UI
+ foreach (var accessLevel in _accessLevelsForTab)
+ {
+ // Create the entry
+ var accessLevelEntry = new AccessLevelEntry(_isMonotone);
+
+ accessLevelEntry.AccessLevel = accessLevel;
+ accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
+ accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel);
+ accessLevelEntry.CheckBox.Disabled = !_canInteract;
+
+ if (_labelStyleClass != null)
+ accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
+
+ // Set the checkbox linkage lines
+ var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1);
+
+ var lines = new List<(Vector2, Vector2)>
+ {
+ (new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)),
+ (new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)),
+ };
+
+ accessLevelEntry.UpdateCheckBoxLink(lines);
+ accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible;
+ accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White;
+
+ // Add checkbox events
+ accessLevelEntry.CheckBox.OnPressed += args =>
+ {
+ // If the checkbox and its siblings are checked, check the 'all' checkbox too
+ allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
+
+ OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed);
+ };
+
+ AccessLevelChecklist.AddChild(accessLevelEntry);
+ _accessLevelEntries.Add(accessLevelEntry);
+ }
+
+ // Press the 'all' checkbox if all others are pressed
+ allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
+ }
+
+ private bool AreAllCheckBoxesPressed(IEnumerable checkBoxes)
+ {
+ foreach (var checkBox in checkBoxes)
+ {
+ if (!checkBox.Pressed)
+ return false;
+ }
+
+ return true;
+ }
+
+ private void SetCheckBoxPressedState(List accessLevelEntries, bool pressed)
+ {
+ foreach (var accessLevelEntry in accessLevelEntries)
+ {
+ accessLevelEntry.CheckBox.Pressed = pressed;
+ }
+ }
+
+
+ ///
+ /// Provides the UI with a list of access groups using which list of tabs should be populated.
+ ///
+ public void SetAccessGroups(HashSet> accessGroups)
+ {
+ _accessGroups = accessGroups;
+
+ ArrangeAccessControls();
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Provides the UI with a list of access levels with which it can populate the currently selected tab.
+ ///
+ public void SetAccessLevels(HashSet> accessLevels)
+ {
+ _accessLevels = accessLevels;
+
+ ArrangeAccessControls();
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Sets which access level checkboxes should be marked on the UI.
+ ///
+ public void SetActiveAccessLevels(HashSet> activeAccessLevels)
+ {
+ _activeAccessLevels = activeAccessLevels;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Sets whether the local player can interact with the checkboxes.
+ ///
+ public void SetLocalPlayerAccessibility(bool canInteract)
+ {
+ _canInteract = canInteract;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Sets whether the UI should use monotone buttons and checkboxes.
+ ///
+ public void SetMonotone(bool monotone)
+ {
+ _isMonotone = monotone;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Applies the specified style to the labels on the UI buttons and checkboxes.
+ ///
+ public void SetLabelStyleClass(string? styleClass)
+ {
+ _labelStyleClass = styleClass;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ private void OnAccessGroupChanged(int newTabIndex)
+ {
+ if (newTabIndex == _accessGroupTabIndex)
+ return;
+
+ _accessGroupTabIndex = newTabIndex;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ private Button CreateAccessGroupButton()
+ {
+ var button = _isMonotone ? new MonotoneButton() : new Button();
+
+ button.ToggleMode = true;
+ button.Group = _accessGroupsButtons;
+ button.Label.HorizontalAlignment = HAlignment.Left;
+
+ return button;
+ }
+
+ private CheckBox CreateAccessLevelCheckbox()
+ {
+ var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox();
+
+ checkbox.Margin = new Thickness(0, 0, 0, 3);
+ checkbox.ToggleMode = true;
+ checkbox.ReservesSpace = false;
+
+ return checkbox;
+ }
+
+ private sealed class AccessLevelEntry : BoxContainer
+ {
+ public ProtoId AccessLevel;
+ public readonly CheckBox CheckBox;
+ public readonly LineRenderer CheckBoxLink;
+
+ public AccessLevelEntry(bool monotone)
+ {
+ HorizontalExpand = true;
+
+ CheckBoxLink = new LineRenderer
+ {
+ SetWidth = 22,
+ VerticalExpand = true,
+ Margin = new Thickness(0, -1),
+ ReservesSpace = false,
+ };
+
+ AddChild(CheckBoxLink);
+
+ CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox();
+ CheckBox.ToggleMode = true;
+ CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f);
+
+ AddChild(CheckBox);
+ }
+
+ public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
+ {
+ CheckBoxLink.Lines = lines;
+ }
+ }
+
+ private sealed class LineRenderer : Control
+ {
+ ///
+ /// List of lines to render (their start and end x-y coordinates).
+ /// Position (0,0) is the top left corner of the control and
+ /// position (1,1) is the bottom right corner.
+ ///
+ ///
+ /// The color of the lines is inherited from the control.
+ ///
+ public List<(Vector2, Vector2)> Lines;
+
+ public LineRenderer()
+ {
+ Lines = new List<(Vector2, Vector2)>();
+ }
+
+ public LineRenderer(List<(Vector2, Vector2)> lines)
+ {
+ Lines = lines;
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ foreach (var line in Lines)
+ {
+ var start = PixelPosition +
+ new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y);
+
+ var end = PixelPosition +
+ new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y);
+
+ handle.DrawLine(start, end, ActualModulateSelf);
+ }
+ }
+ }
+}
diff --git a/Content.Client/Actions/ActionEvents.cs b/Content.Client/Actions/ActionEvents.cs
index 2fdf25c976..73bee331be 100644
--- a/Content.Client/Actions/ActionEvents.cs
+++ b/Content.Client/Actions/ActionEvents.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Actions.Components;
+using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
+
namespace Content.Client.Actions;
///
@@ -7,3 +10,17 @@ public sealed class FillActionSlotEvent : EntityEventArgs
{
public EntityUid? Action;
}
+
+///
+/// Client-side event used to attempt to trigger a targeted action.
+/// This only gets raised if the has .
+/// Handlers must set Handled to true, then if the action has been performed,
+/// i.e. a target is found, then FoundTarget must be set to true.
+///
+[ByRefEvent]
+public record struct ActionTargetAttemptEvent(
+ PointerInputCmdArgs Input,
+ Entity User,
+ ActionComponent Action,
+ bool Handled = false,
+ bool FoundTarget = false);
diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs
index 31350a6a5d..91baa3b1a9 100644
--- a/Content.Client/Actions/ActionsSystem.cs
+++ b/Content.Client/Actions/ActionsSystem.cs
@@ -1,18 +1,23 @@
using System.IO;
using System.Linq;
using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
using Content.Shared.Charges.Systems;
+using Content.Shared.Mapping;
+using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@@ -25,6 +30,7 @@ namespace Content.Client.Actions
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -38,131 +44,67 @@ namespace Content.Client.Actions
public event Action>? AssignSlot;
private readonly List _removed = new();
- private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
+ private readonly List> _added = new();
+
+ public static readonly EntProtoId MappingEntityAction = "BaseMappingEntityAction";
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnPlayerAttached);
SubscribeLocalEvent(OnPlayerDetached);
- SubscribeLocalEvent(HandleComponentState);
+ SubscribeLocalEvent(OnHandleState);
- SubscribeLocalEvent(OnInstantHandleState);
- SubscribeLocalEvent(OnEntityTargetHandleState);
- SubscribeLocalEvent(OnWorldTargetHandleState);
- SubscribeLocalEvent(OnEntityWorldTargetHandleState);
+ SubscribeLocalEvent(OnActionAutoHandleState);
+
+ SubscribeLocalEvent(OnEntityTargetAttempt);
+ SubscribeLocalEvent(OnWorldTargetAttempt);
}
- private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
- {
- if (args.Current is not InstantActionComponentState state)
- return;
- BaseHandleState(uid, component, state);
+ private void OnActionAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ UpdateAction(ent);
}
- private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
+ public override void UpdateAction(Entity ent)
{
- if (args.Current is not EntityTargetActionComponentState state)
- return;
-
- component.Whitelist = state.Whitelist;
- component.Blacklist = state.Blacklist;
- component.CanTargetSelf = state.CanTargetSelf;
- BaseHandleState(uid, component, state);
- }
-
- private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
- {
- if (args.Current is not WorldTargetActionComponentState state)
- return;
-
- BaseHandleState(uid, component, state);
- }
-
- private void OnEntityWorldTargetHandleState(EntityUid uid,
- EntityWorldTargetActionComponent component,
- ref ComponentHandleState args)
- {
- if (args.Current is not EntityWorldTargetActionComponentState state)
- return;
-
- component.Whitelist = state.Whitelist;
- component.CanTargetSelf = state.CanTargetSelf;
- BaseHandleState(uid, component, state);
- }
-
- private void BaseHandleState(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
- {
- // TODO ACTIONS use auto comp states
- component.Icon = state.Icon;
- component.IconOn = state.IconOn;
- component.IconColor = state.IconColor;
- component.OriginalIconColor = state.OriginalIconColor;
- component.DisabledIconColor = state.DisabledIconColor;
- component.Keywords.Clear();
- component.Keywords.UnionWith(state.Keywords);
- component.Enabled = state.Enabled;
- component.Toggled = state.Toggled;
- component.Cooldown = state.Cooldown;
- component.UseDelay = state.UseDelay;
- component.Container = EnsureEntity(state.Container, uid);
- component.EntityIcon = EnsureEntity(state.EntityIcon, uid);
- component.CheckCanInteract = state.CheckCanInteract;
- component.CheckConsciousness = state.CheckConsciousness;
- component.ClientExclusive = state.ClientExclusive;
- component.Priority = state.Priority;
- component.AttachedEntity = EnsureEntity(state.AttachedEntity, uid);
- component.RaiseOnUser = state.RaiseOnUser;
- component.RaiseOnAction = state.RaiseOnAction;
- component.AutoPopulate = state.AutoPopulate;
- component.Temporary = state.Temporary;
- component.ItemIconStyle = state.ItemIconStyle;
- component.Sound = state.Sound;
-
- UpdateAction(uid, component);
- }
-
- public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
- {
- if (!ResolveActionData(actionId, ref action))
- return;
-
// TODO: Decouple this.
- action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
-
- base.UpdateAction(actionId, action);
- if (_playerManager.LocalEntity != action.AttachedEntity)
+ ent.Comp.IconColor = _sharedCharges.GetCurrentCharges(ent.Owner) == 0 ? ent.Comp.DisabledIconColor : ent.Comp.OriginalIconColor;
+ base.UpdateAction(ent);
+ if (_playerManager.LocalEntity != ent.Comp.AttachedEntity)
return;
ActionsUpdated?.Invoke();
}
- private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
+ private void OnHandleState(Entity ent, ref ComponentHandleState args)
{
if (args.Current is not ActionsComponentState state)
return;
+ var (uid, comp) = ent;
_added.Clear();
_removed.Clear();
var stateEnts = EnsureEntitySet(state.Actions, uid);
- foreach (var act in component.Actions)
+ foreach (var act in comp.Actions)
{
if (!stateEnts.Contains(act) && !IsClientSide(act))
_removed.Add(act);
}
- component.Actions.ExceptWith(_removed);
+ comp.Actions.ExceptWith(_removed);
foreach (var actionId in stateEnts)
{
if (!actionId.IsValid())
continue;
- if (!component.Actions.Add(actionId))
+ if (!comp.Actions.Add(actionId))
continue;
- TryGetActionData(actionId, out var action);
- _added.Add((actionId, action));
+ if (GetAction(actionId) is {} action)
+ _added.Add(action);
}
if (_playerManager.LocalEntity != uid)
@@ -177,47 +119,46 @@ namespace Content.Client.Actions
foreach (var action in _added)
{
- OnActionAdded?.Invoke(action.Item1);
+ OnActionAdded?.Invoke(action);
}
ActionsUpdated?.Invoke();
}
- public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
+ public static int ActionComparer(Entity a, Entity b)
{
- var priorityA = a.Item2?.Priority ?? 0;
- var priorityB = b.Item2?.Priority ?? 0;
+ var priorityA = a.Comp?.Priority ?? 0;
+ var priorityB = b.Comp?.Priority ?? 0;
if (priorityA != priorityB)
return priorityA - priorityB;
- priorityA = a.Item2?.Container?.Id ?? 0;
- priorityB = b.Item2?.Container?.Id ?? 0;
+ priorityA = a.Comp?.Container?.Id ?? 0;
+ priorityB = b.Comp?.Container?.Id ?? 0;
return priorityA - priorityB;
}
- protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
- BaseActionComponent action)
+ protected override void ActionAdded(Entity performer, Entity action)
{
- if (_playerManager.LocalEntity != performer)
+ if (_playerManager.LocalEntity != performer.Owner)
return;
- OnActionAdded?.Invoke(actionId);
+ OnActionAdded?.Invoke(action);
ActionsUpdated?.Invoke();
}
- protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+ protected override void ActionRemoved(Entity performer, Entity action)
{
- if (_playerManager.LocalEntity != performer)
+ if (_playerManager.LocalEntity != performer.Owner)
return;
- OnActionRemoved?.Invoke(actionId);
+ OnActionRemoved?.Invoke(action);
ActionsUpdated?.Invoke();
}
- public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
+ public IEnumerable> GetClientActions()
{
if (_playerManager.LocalEntity is not { } user)
- return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
+ return Enumerable.Empty>();
return GetActions(user);
}
@@ -254,24 +195,23 @@ namespace Content.Client.Actions
CommandBinds.Unregister();
}
- public void TriggerAction(EntityUid actionId, BaseActionComponent action)
+ public void TriggerAction(Entity action)
{
- if (_playerManager.LocalEntity is not { } user ||
- !TryComp(user, out ActionsComponent? actions))
- {
- return;
- }
-
- if (action is not InstantActionComponent instantAction)
+ if (_playerManager.LocalEntity is not { } user)
return;
- if (action.ClientExclusive)
+ // TODO: unhardcode this somehow
+
+ if (!HasComp(action))
+ return;
+
+ if (action.Comp.ClientExclusive)
{
- PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
+ PerformAction(user, action);
}
else
{
- var request = new RequestPerformActionEvent(GetNetEntity(actionId));
+ var request = new RequestPerformActionEvent(GetNetEntity(action));
EntityManager.RaisePredictiveEvent(request);
}
}
@@ -295,39 +235,137 @@ namespace Content.Client.Actions
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
+ var actions = EnsureComp(user);
+
ClearAssignments?.Invoke();
var assignments = new List();
-
foreach (var entry in sequence.Sequence)
{
if (entry is not MappingDataNode map)
continue;
- if (!map.TryGet("action", out var actionNode))
- continue;
-
- var action = _serialization.Read(actionNode, notNullableOverride: true);
- var actionId = Spawn();
- AddComp(actionId, action);
- AddActionDirect(user, actionId);
-
- if (map.TryGet("name", out var nameNode))
- _metaData.SetEntityName(actionId, nameNode.Value);
-
if (!map.TryGet("assignments", out var assignmentNode))
continue;
- var nodeAssignments = _serialization.Read>(assignmentNode, notNullableOverride: true);
-
- foreach (var index in nodeAssignments)
+ var actionId = EntityUid.Invalid;
+ if (map.TryGet("action", out var actionNode))
{
- var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
- assignments.Add(assignment);
+ var id = new EntProtoId(actionNode.Value);
+ actionId = Spawn(id);
}
+ else if (map.TryGet("entity", out var entityNode))
+ {
+ var id = new EntProtoId(entityNode.Value);
+ var proto = _proto.Index(id);
+ actionId = Spawn(MappingEntityAction);
+ SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id));
+ SetEvent(actionId, new StartPlacementActionEvent()
+ {
+ PlacementOption = "SnapgridCenter",
+ EntityType = id
+ });
+ _metaData.SetEntityName(actionId, proto.Name);
+ }
+ else if (map.TryGet("tileId", out var tileNode))
+ {
+ var id = new ProtoId(tileNode.Value);
+ var proto = _proto.Index(id);
+ actionId = Spawn(MappingEntityAction);
+ if (proto.Sprite is {} sprite)
+ SetIcon(actionId, new SpriteSpecifier.Texture(sprite));
+ SetEvent(actionId, new StartPlacementActionEvent()
+ {
+ PlacementOption = "AlignTileAny",
+ TileId = id
+ });
+ _metaData.SetEntityName(actionId, Loc.GetString(proto.Name));
+ }
+ else
+ {
+ Log.Error($"Mapping actions from {path} had unknown action data!");
+ continue;
+ }
+
+ AddActionDirect((user, actions), actionId);
+ }
+ }
+
+ private void OnWorldTargetAttempt(Entity ent, ref ActionTargetAttemptEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ args.Handled = true;
+
+ var (uid, comp) = ent;
+ var action = args.Action;
+ var coords = args.Input.Coordinates;
+ var user = args.User;
+
+ if (!ValidateWorldTarget(user, coords, ent))
+ return;
+
+ // optionally send the clicked entity too, if it matches its whitelist etc
+ // this is the actual entity-world targeting magic
+ EntityUid? targetEnt = null;
+ if (TryComp(ent, out var entity) &&
+ args.Input.EntityUid != null &&
+ ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
+ {
+ targetEnt = args.Input.EntityUid;
}
- AssignSlot?.Invoke(assignments);
+ if (action.ClientExclusive)
+ {
+ // TODO: abstract away from single event or maybe just RaiseLocalEvent?
+ if (comp.Event is {} ev)
+ {
+ ev.Target = coords;
+ ev.Entity = targetEnt;
+ }
+
+ PerformAction((user, user.Comp), (uid, action));
+ }
+ else
+ RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(targetEnt), GetNetCoordinates(coords)));
+
+ args.FoundTarget = true;
+ }
+
+ private void OnEntityTargetAttempt(Entity ent, ref ActionTargetAttemptEvent args)
+ {
+ if (args.Handled || args.Input.EntityUid is not { Valid: true } entity)
+ return;
+
+ // let world target component handle it
+ var (uid, comp) = ent;
+ if (comp.Event is not {} ev)
+ {
+ DebugTools.Assert(HasComp(ent), $"Action {ToPrettyString(ent)} requires WorldTargetActionComponent for entity-world targeting");
+ return;
+ }
+
+ args.Handled = true;
+
+ var action = args.Action;
+ var user = args.User;
+
+ if (!ValidateEntityTarget(user, entity, ent))
+ return;
+
+ if (action.ClientExclusive)
+ {
+ ev.Target = entity;
+
+ PerformAction((user, user.Comp), (uid, action));
+ }
+ else
+ {
+ RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(entity)));
+ }
+
+ args.FoundTarget = true;
}
public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
diff --git a/Content.Client/Administration/UI/Tabs/RoundTab.xaml b/Content.Client/Administration/UI/Tabs/RoundTab.xaml
index 2c8a400ecd..36c06cab76 100644
--- a/Content.Client/Administration/UI/Tabs/RoundTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/RoundTab.xaml
@@ -1,13 +1,13 @@
-
-
-
-
+
+
+
+
diff --git a/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs b/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs
index 28073bc91d..70f12bb393 100644
--- a/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs
@@ -1,10 +1,24 @@
using Robust.Client.AutoGenerated;
+using Robust.Client.Console;
using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Tabs
{
[GenerateTypedNameReferences]
public sealed partial class RoundTab : Control
{
+ [Dependency] private readonly IClientConsoleHost _console = default!;
+
+ public RoundTab()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ StartRound.OnPressed += _ => _console.ExecuteCommand("startround");
+ EndRound.OnPressed += _ => _console.ExecuteCommand("endround");
+ RestartRound.OnPressed += _ => _console.ExecuteCommand("restartround");
+ RestartRoundNow.OnPressed += _ => _console.ExecuteCommand("restartroundnow");
+ }
}
}
diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
index b998405835..80c186f7fd 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml
@@ -1,11 +1,12 @@
-
+
diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
index 24b92e42ce..7a70e42d06 100644
--- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs
@@ -1,5 +1,6 @@
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
+using Robust.Client.Console;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
@@ -10,6 +11,7 @@ namespace Content.Client.Administration.UI.Tabs
public sealed partial class ServerTab : Control
{
[Dependency] private readonly IConfigurationManager _config = default!;
+ [Dependency] private readonly IClientConsoleHost _console = default!;
public ServerTab()
{
@@ -18,6 +20,8 @@ namespace Content.Client.Administration.UI.Tabs
_config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true);
_config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true);
+
+ ServerShutdownButton.OnPressed += _ => _console.ExecuteCommand("shutdown");
}
private void OocEnabledChanged(bool value)
diff --git a/Content.Client/Atmos/AlignAtmosPipeLayers.cs b/Content.Client/Atmos/AlignAtmosPipeLayers.cs
new file mode 100644
index 0000000000..1bf3310a6c
--- /dev/null
+++ b/Content.Client/Atmos/AlignAtmosPipeLayers.cs
@@ -0,0 +1,203 @@
+using Content.Client.Construction;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.Construction.Prototypes;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Placement;
+using Robust.Client.Placement.Modes;
+using Robust.Client.Utility;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using System.Numerics;
+using static Robust.Client.Placement.PlacementManager;
+
+namespace Content.Client.Atmos;
+
+///
+/// Allows users to place atmos pipes on different layers depending on how the mouse cursor is positioned within a grid tile.
+///
+///
+/// This placement mode is not on the engine because it is content specific.
+///
+public sealed class AlignAtmosPipeLayers : SnapgridCenter
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+
+ private readonly SharedMapSystem _mapSystem;
+ private readonly SharedTransformSystem _transformSystem;
+ private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem;
+ private readonly SpriteSystem _spriteSystem;
+
+ private const float SearchBoxSize = 2f;
+ private EntityCoordinates _unalignedMouseCoords = default;
+ private const float MouseDeadzoneRadius = 0.25f;
+
+ private Color _guideColor = new Color(0, 0, 0.5785f);
+ private const float GuideRadius = 0.1f;
+ private const float GuideOffset = 0.21875f;
+
+ public AlignAtmosPipeLayers(PlacementManager pMan) : base(pMan)
+ {
+ IoCManager.InjectDependencies(this);
+
+ _mapSystem = _entityManager.System();
+ _transformSystem = _entityManager.System();
+ _pipeLayersSystem = _entityManager.System();
+ _spriteSystem = _entityManager.System();
+ }
+
+ ///
+ public override void Render(in OverlayDrawArgs args)
+ {
+ var gridUid = _entityManager.System().GetGrid(MouseCoords);
+
+ if (gridUid == null || Grid == null)
+ return;
+
+ // Draw guide circles for each pipe layer if we are not in line/grid placing mode
+ if (pManager.PlacementType == PlacementTypes.None)
+ {
+ var gridRotation = _transformSystem.GetWorldRotation(gridUid.Value);
+ var worldPosition = _mapSystem.LocalToWorld(gridUid.Value, Grid, MouseCoords.Position);
+ var direction = (_eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
+ var multi = (direction == Direction.North || direction == Direction.South) ? -1f : 1f;
+
+ args.WorldHandle.DrawCircle(worldPosition, GuideRadius, _guideColor);
+ args.WorldHandle.DrawCircle(worldPosition + gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
+ args.WorldHandle.DrawCircle(worldPosition - gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor);
+ }
+
+ base.Render(args);
+ }
+
+ ///
+ public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
+ {
+ _unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
+ base.AlignPlacementMode(mouseScreen);
+
+ // Exit early if we are in line/grid placing mode
+ if (pManager.PlacementType != PlacementTypes.None)
+ return;
+
+ MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
+
+ var gridId = _transformSystem.GetGrid(MouseCoords);
+
+ if (!_entityManager.TryGetComponent(gridId, out var mapGrid))
+ return;
+
+ var gridRotation = _transformSystem.GetWorldRotation(gridId.Value);
+ CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
+
+ float tileSize = mapGrid.TileSize;
+ GridDistancing = tileSize;
+
+ MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
+ CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
+
+ // Calculate the position of the mouse cursor with respect to the center of the tile to determine which layer to use
+ var mouseCoordsDiff = _unalignedMouseCoords.Position - MouseCoords.Position;
+ var layer = AtmosPipeLayer.Primary;
+
+ if (mouseCoordsDiff.Length() > MouseDeadzoneRadius)
+ {
+ // Determine the direction of the mouse is relative to the center of the tile, adjusting for the player eye and grid rotation
+ var direction = (new Angle(mouseCoordsDiff) + _eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir();
+ layer = (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary;
+ }
+
+ // Update the construction menu placer
+ if (pManager.Hijack != null)
+ UpdateHijackedPlacer(layer, mouseScreen);
+
+ // Otherwise update the debug placer
+ else
+ UpdatePlacer(layer);
+ }
+
+ private void UpdateHijackedPlacer(AtmosPipeLayer layer, ScreenCoordinates mouseScreen)
+ {
+ // Try to get alternative prototypes from the construction prototype
+ var constructionSystem = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentConstructionSystem;
+ var altPrototypes = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.AlternativePrototypes;
+
+ if (constructionSystem == null || altPrototypes == null || (int)layer >= altPrototypes.Length)
+ return;
+
+ var newProtoId = altPrototypes[(int)layer];
+
+ if (!_protoManager.TryIndex(newProtoId, out var newProto))
+ return;
+
+ if (newProto.Type != ConstructionType.Structure)
+ {
+ pManager.Clear();
+ return;
+ }
+
+ if (newProto.ID == (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.ID)
+ return;
+
+ // Start placing
+ pManager.BeginPlacing(new PlacementInformation()
+ {
+ IsTile = false,
+ PlacementOption = newProto.PlacementMode,
+ }, new ConstructionPlacementHijack(constructionSystem, newProto));
+
+ if (pManager.CurrentMode is AlignAtmosPipeLayers { } newMode)
+ newMode.RefreshGrid(mouseScreen);
+
+ // Update construction guide
+ constructionSystem.GetGuide(newProto);
+ }
+
+ private void UpdatePlacer(AtmosPipeLayer layer)
+ {
+ // Try to get alternative prototypes from the entity atmos pipe layer component
+ if (pManager.CurrentPermission?.EntityType == null)
+ return;
+
+ if (!_protoManager.TryIndex(pManager.CurrentPermission.EntityType, out var currentProto))
+ return;
+
+ if (!currentProto.TryGetComponent(out var atmosPipeLayers, _entityManager.ComponentFactory))
+ return;
+
+ if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId))
+ return;
+
+ if (_protoManager.TryIndex(newProtoId, out var newProto))
+ {
+ // Update the placed prototype
+ pManager.CurrentPermission.EntityType = newProtoId;
+
+ // Update the appearance of the ghost sprite
+ if (newProto.TryGetComponent(out var sprite, _entityManager.ComponentFactory))
+ {
+ var textures = new List();
+
+ foreach (var spriteLayer in sprite.AllLayers)
+ {
+ if (spriteLayer.ActualRsi?.Path != null && spriteLayer.RsiState.Name != null)
+ textures.Add(_spriteSystem.RsiStateLike(new SpriteSpecifier.Rsi(spriteLayer.ActualRsi.Path, spriteLayer.RsiState.Name)));
+ }
+
+ pManager.CurrentTextures = textures;
+ }
+ }
+ }
+
+ private void RefreshGrid(ScreenCoordinates mouseScreen)
+ {
+ base.AlignPlacementMode(mouseScreen);
+ }
+}
diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs
index c23ebb6435..20902722ff 100644
--- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs
+++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs
@@ -17,6 +17,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
public int? FocusNetId = null;
private const int ChunkSize = 4;
+ private const float ScaleModifier = 4f;
+
+ private readonly float[] _layerFraction = { 0.5f, 0.75f, 0.25f };
+ private const float LineThickness = 0.05f;
private readonly Color _basePipeNetColor = Color.LightGray;
private readonly Color _unfocusedPipeNetColor = Color.DimGray;
@@ -95,23 +99,23 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
foreach (var chunkedLine in atmosPipeNetwork)
{
var leftTop = ScalePosition(new Vector2
- (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
- Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
+ (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
+ Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
- offset);
var rightTop = ScalePosition(new Vector2
- (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
- Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
+ (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
+ Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness)
- offset);
var leftBottom = ScalePosition(new Vector2
- (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
- Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
+ (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness,
+ Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
- offset);
var rightBottom = ScalePosition(new Vector2
- (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
- Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
+ (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness,
+ Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness)
- offset);
if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV))
@@ -142,7 +146,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
if (chunks == null || grid == null)
return decodedOutput;
- // Clear stale look up table values
+ // Clear stale look up table values
_horizLines.Clear();
_horizLinesReversed.Clear();
_vertLines.Clear();
@@ -158,7 +162,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
{
var list = new List();
- foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData)
+ foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData)
{
// Determine the correct coloration for the pipe
var color = Color.FromHex(hexColor) * _basePipeNetColor;
@@ -191,6 +195,9 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
_vertLinesReversed[color] = vertLinesReversed;
}
+ var layerFraction = _layerFraction[(int)layer];
+ var origin = new Vector2(grid.TileSize * layerFraction, -grid.TileSize * layerFraction);
+
// Loop over the chunk
for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++)
{
@@ -208,21 +215,22 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
// Calculate the draw point offsets
var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
- new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+ new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 1f) : origin;
var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
- new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+ new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 0f) : origin;
var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
- new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+ new Vector2(grid.TileSize * 1f, -grid.TileSize * layerFraction) : origin;
var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ?
- new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f);
+ new Vector2(grid.TileSize * 0f, -grid.TileSize * layerFraction) : origin;
- // Since we can have pipe lines that have a length of a half tile,
- // double the vectors and convert to vector2i so we can merge them
- AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed);
- AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed);
+ // Scale up the vectors and convert to vector2i so we can merge them
+ AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, ScaleModifier),
+ ConvertVector2ToVector2i(tile + horizLineTerminus, ScaleModifier), horizLines, horizLinesReversed);
+ AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, ScaleModifier),
+ ConvertVector2ToVector2i(tile + vertLineTerminus, ScaleModifier), vertLines, vertLinesReversed);
}
}
}
@@ -235,7 +243,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
foreach (var (origin, terminal) in horizLines)
decodedOutput.Add(new AtmosMonitoringConsoleLine
- (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
+ (ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
}
foreach (var (color, vertLines) in _vertLines)
@@ -245,7 +253,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
foreach (var (origin, terminal) in vertLines)
decodedOutput.Add(new AtmosMonitoringConsoleLine
- (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB));
+ (ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB));
}
return decodedOutput;
diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs
index bfbb05d2ab..6a4967e4a4 100644
--- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs
+++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs
@@ -15,7 +15,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args)
{
- Dictionary> modifiedChunks;
+ Dictionary> modifiedChunks;
Dictionary atmosDevices;
switch (args.Current)
@@ -54,7 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
foreach (var (origin, chunk) in modifiedChunks)
{
var newChunk = new AtmosPipeChunk(origin);
- newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk);
+ newChunk.AtmosPipeData = new Dictionary(chunk);
component.AtmosPipeChunks[origin] = newChunk;
}
diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs
index e25c3af9e9..1a084ea73b 100644
--- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs
+++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs
@@ -13,6 +13,7 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Numerics;
namespace Content.Client.Atmos.Consoles;
@@ -33,6 +34,8 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
private ProtoId _navMapConsoleProtoId = "NavMapConsole";
private ProtoId _gasPipeSensorProtoId = "GasPipeSensor";
+ private readonly Vector2[] _pipeLayerOffsets = { new Vector2(0f, 0f), new Vector2(0.25f, 0.25f), new Vector2(-0.25f, -0.25f) };
+
public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner)
{
RobustXamlLoader.Load(this);
@@ -53,7 +56,7 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
consoleCoords = xform.Coordinates;
NavMap.MapUid = xform.GridUid;
- // Assign station name
+ // Assign station name
if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData))
stationName = stationMetaData.EntityName;
@@ -238,6 +241,10 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow
var blinks = proto.Blinks || _focusEntity == metaData.NetEntity;
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
+
+ if (proto.Placement == NavMapBlipPlacement.Offset && metaData.PipeLayer > 0)
+ coords = coords.Offset(_pipeLayerOffsets[(int)metaData.PipeLayer]);
+
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale);
NavMap.TrackedEntities[metaData.NetEntity] = blip;
}
diff --git a/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs
index 2029cb9be5..1a12c3967b 100644
--- a/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs
+++ b/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs
@@ -1,6 +1,7 @@
using Content.Client.SubFloor;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -8,7 +9,7 @@ using Robust.Client.GameObjects;
namespace Content.Client.Atmos.EntitySystems;
[UsedImplicitly]
-public sealed class AtmosPipeAppearanceSystem : EntitySystem
+public sealed partial class AtmosPipeAppearanceSystem : SharedAtmosPipeAppearanceSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
@@ -26,26 +27,37 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
if (!TryComp(uid, out SpriteComponent? sprite))
return;
+ var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out _);
+
foreach (var layerKey in Enum.GetValues())
{
- var layer = _sprite.LayerMapReserve((uid, sprite), layerKey);
- _sprite.LayerSetRsi((uid, sprite), layer, component.Sprite.RsiPath);
- _sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite.RsiState);
- _sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey));
+ for (byte i = 0; i < numberOfPipeLayers; i++)
+ {
+ var layerName = layerKey.ToString() + i.ToString();
+ var layer = _sprite.LayerMapReserve((uid, sprite), layerName);
+ _sprite.LayerSetRsi((uid, sprite), layer, component.Sprite[i].RsiPath);
+ _sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite[i].RsiState);
+ _sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey));
+ }
}
}
- private void HideAllPipeConnection(Entity entity)
+ private void HideAllPipeConnection(Entity entity, AtmosPipeLayersComponent? atmosPipeLayers, int numberOfPipeLayers)
{
var sprite = entity.Comp;
foreach (var layerKey in Enum.GetValues())
{
- if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerKey, out var key, false))
- continue;
+ for (byte i = 0; i < numberOfPipeLayers; i++)
+ {
+ var layerName = layerKey.ToString() + i.ToString();
- var layer = sprite[key];
- layer.Visible = false;
+ if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerName, out var key, false))
+ continue;
+
+ var layer = sprite[key];
+ layer.Visible = false;
+ }
}
}
@@ -61,33 +73,45 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
return;
}
- if (!_appearance.TryGetData(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component))
+ var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out var atmosPipeLayers);
+
+ if (!_appearance.TryGetData(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component))
{
- HideAllPipeConnection((uid, args.Sprite));
+ HideAllPipeConnection((uid, args.Sprite), atmosPipeLayers, numberOfPipeLayers);
return;
}
if (!_appearance.TryGetData(uid, PipeColorVisuals.Color, out var color, args.Component))
color = Color.White;
- // transform connected directions to local-coordinates
- var connectedDirections = worldConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation);
-
- foreach (var layerKey in Enum.GetValues())
+ for (byte i = 0; i < numberOfPipeLayers; i++)
{
- if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerKey, out var key, false))
- continue;
+ // Extract the cardinal pipe orientations for the current pipe layer
+ // '15' is the four bit mask that is used to extract the pipe orientations of interest from 'worldConnectedDirections'
+ // Fun fact: a collection of four bits is called a 'nibble'! They aren't natively supported :(
+ var pipeLayerConnectedDirections = (PipeDirection)(15 & (worldConnectedDirections >> (PipeDirectionHelpers.PipeDirections * i)));
- var layer = args.Sprite[key];
- var dir = (PipeDirection)layerKey;
- var visible = connectedDirections.HasDirection(dir);
+ // Transform the connected directions to local-coordinates
+ var connectedDirections = pipeLayerConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation);
- layer.Visible &= visible;
+ foreach (var layerKey in Enum.GetValues())
+ {
+ var layerName = layerKey.ToString() + i.ToString();
- if (!visible)
- continue;
+ if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerName, out var key, false))
+ continue;
- layer.Color = color;
+ var layer = args.Sprite[key];
+ var dir = (PipeDirection)layerKey;
+ var visible = connectedDirections.HasDirection(dir);
+
+ layer.Visible &= visible;
+
+ if (!visible)
+ continue;
+
+ layer.Color = color;
+ }
}
}
diff --git a/Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
new file mode 100644
index 0000000000..f560e0b833
--- /dev/null
+++ b/Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs
@@ -0,0 +1,56 @@
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.EntitySystems;
+using Robust.Client.GameObjects;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Reflection;
+using Robust.Shared.Serialization.TypeSerializers.Implementations;
+using Robust.Shared.Utility;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+///
+/// The system responsible for updating the appearance of layered gas pipe
+///
+public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly IReflectionManager _reflection = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly SpriteSystem _sprite = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAppearanceChange);
+ }
+
+ private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent ev)
+ {
+ if (!TryComp(ent, out var sprite))
+ return;
+
+ if (_appearance.TryGetData(ent, AtmosPipeLayerVisuals.Sprite, out var spriteRsi) &&
+ _resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / spriteRsi, out RSIResource? resource))
+ {
+ _sprite.SetBaseRsi((ent, sprite), resource.RSI);
+ }
+
+ if (_appearance.TryGetData>(ent, AtmosPipeLayerVisuals.SpriteLayers, out var pipeState))
+ {
+ foreach (var (layerKey, rsiPath) in pipeState)
+ {
+ if (TryParseKey(layerKey, out var @enum))
+ _sprite.LayerSetRsi((ent, sprite), @enum, new ResPath(rsiPath));
+ else
+ _sprite.LayerSetRsi((ent, sprite), layerKey, new ResPath(rsiPath));
+ }
+ }
+ }
+
+ private bool TryParseKey(string keyString, [NotNullWhen(true)] out Enum? @enum)
+ {
+ return _reflection.TryParseEnumReference(keyString, out @enum);
+ }
+}
diff --git a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs
index bd221eb4bf..431a598678 100644
--- a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs
+++ b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs
@@ -11,7 +11,6 @@ namespace Content.Client.Atmos.EntitySystems;
///
public sealed class FireVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly PointLightSystem _lights = default!;
public override void Initialize()
@@ -33,9 +32,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem(uid, out var sprite) &&
- _sprite.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false))
+ SpriteSystem.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false))
{
- _sprite.RemoveLayer((uid, sprite), layer);
+ SpriteSystem.RemoveLayer((uid, sprite), layer);
}
}
@@ -44,11 +43,11 @@ public sealed class FireVisualizerSystem : VisualizerSystem(uid, out var sprite) || !TryComp(uid, out AppearanceComponent? appearance))
return;
- _sprite.LayerMapReserve((uid, sprite), FireVisualLayers.Fire);
- _sprite.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false);
+ SpriteSystem.LayerMapReserve((uid, sprite), FireVisualLayers.Fire);
+ SpriteSystem.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false);
sprite.LayerSetShader(FireVisualLayers.Fire, "unshaded");
if (component.Sprite != null)
- _sprite.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite));
+ SpriteSystem.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite));
UpdateAppearance(uid, component, sprite, appearance);
}
@@ -61,12 +60,12 @@ public sealed class FireVisualizerSystem : VisualizerSystem(uid, FireVisuals.OnFire, out var onFire, appearance);
AppearanceSystem.TryGetData(uid, FireVisuals.FireStacks, out var fireStacks, appearance);
- _sprite.LayerSetVisible((uid, sprite), index, onFire);
+ SpriteSystem.LayerSetVisible((uid, sprite), index, onFire);
if (!onFire)
{
@@ -80,9 +79,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem component.FireStackAlternateState && !string.IsNullOrEmpty(component.AlternateState))
- _sprite.LayerSetRsiState((uid, sprite), index, component.AlternateState);
+ SpriteSystem.LayerSetRsiState((uid, sprite), index, component.AlternateState);
else
- _sprite.LayerSetRsiState((uid, sprite), index, component.NormalState);
+ SpriteSystem.LayerSetRsiState((uid, sprite), index, component.NormalState);
component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default));
var light = EnsureComp(component.LightEntity.Value);
diff --git a/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs b/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs
index 6d3a373759..472e1e3d34 100644
--- a/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs
+++ b/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs
@@ -7,11 +7,9 @@ namespace Content.Client.Atmos.Monitor;
public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsComponent component, ref AppearanceChangeEvent args)
{
- if (args.Sprite == null || !_sprite.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false))
+ if (args.Sprite == null || !SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false))
return;
if (!args.AppearanceData.TryGetValue(PowerDeviceVisuals.Powered, out var poweredObject) ||
@@ -24,8 +22,8 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem
public sealed class PortableScrubberSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, PortableScrubberVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -20,15 +18,15 @@ public sealed class PortableScrubberSystem : VisualizerSystem(uid, PortableScrubberVisuals.IsRunning, out var isRunning, args.Component))
{
var runningState = isRunning ? component.RunningState : component.IdleState;
- _sprite.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState);
var fullState = isFull ? component.FullState : component.ReadyState;
- _sprite.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState);
}
if (AppearanceSystem.TryGetData(uid, PortableScrubberVisuals.IsDraining, out var isDraining, args.Component))
{
- _sprite.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining);
}
}
}
diff --git a/Content.Client/Botany/PlantHolderVisualizerSystem.cs b/Content.Client/Botany/PlantHolderVisualizerSystem.cs
index 7050abb39f..bd5b5e56dc 100644
--- a/Content.Client/Botany/PlantHolderVisualizerSystem.cs
+++ b/Content.Client/Botany/PlantHolderVisualizerSystem.cs
@@ -7,8 +7,6 @@ namespace Content.Client.Botany;
public sealed class PlantHolderVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -20,8 +18,8 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem(uid, out var sprite))
return;
- _sprite.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant);
- _sprite.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false);
+ SpriteSystem.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant);
+ SpriteSystem.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false);
}
protected override void OnAppearanceChange(EntityUid uid, PlantHolderVisualsComponent component, ref AppearanceChangeEvent args)
@@ -34,12 +32,12 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem(uid, TypingIndicatorVisuals.State, out var state);
- _sprite.LayerSetVisible((uid, args.Sprite), layer, state != TypingIndicatorState.None);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, state != TypingIndicatorState.None);
switch (state)
{
case TypingIndicatorState.Idle:
- _sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.IdleState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.IdleState);
break;
case TypingIndicatorState.Typing:
- _sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState);
break;
}
}
diff --git a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs
index 00a29563c1..06bc790696 100644
--- a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs
+++ b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs
@@ -11,7 +11,6 @@ namespace Content.Client.Chemistry.Visualizers;
public sealed class FoamVisualizerSystem : VisualizerSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -72,7 +71,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem(uid, out var sprite))
- _sprite.SetVisible((uid, sprite), false);
+ SpriteSystem.SetVisible((uid, sprite), false);
}
}
diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
index 2baef6a535..6fc07785b2 100644
--- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
+++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
@@ -16,7 +16,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem(uid, SolutionContainerVisuals.BaseOverride,
out var baseOverride,
@@ -80,35 +79,35 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem 0)
{
- _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
maxFillLevels = reagentProto.MetamorphicMaxFillLevels;
fillBaseName = reagentProto.MetamorphicFillBaseName;
changeColor = reagentProto.MetamorphicChangeColor;
fillSprite = sprite;
}
else
- _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false);
if (hasOverlay)
- _sprite.LayerSetVisible((uid, args.Sprite), overlayLayer, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, false);
}
else
{
- _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
if (hasOverlay)
- _sprite.LayerSetVisible((uid, args.Sprite), overlayLayer, true);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, true);
if (component.MetamorphicDefaultSprite != null)
- _sprite.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite);
+ SpriteSystem.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite);
}
}
}
}
else
{
- _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
}
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, maxFillLevels + 1);
@@ -120,25 +119,25 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem(uid, SolutionContainerVisuals.Color, out var color, args.Component))
- _sprite.LayerSetColor((uid, args.Sprite), fillLayer, color);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, color);
else
- _sprite.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
}
else
{
if (component.EmptySpriteName == null)
- _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false);
else
{
- _sprite.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName);
if (changeColor)
- _sprite.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor);
else
- _sprite.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
}
}
diff --git a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs
index bde6a4b99a..74fcf48849 100644
--- a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs
+++ b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs
@@ -13,16 +13,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
- private static readonly SlotFlags[] IgnoredSlots =
- {
- SlotFlags.All,
- SlotFlags.PREVENTEQUIP,
- SlotFlags.NONE
- };
- private static readonly SlotFlags[] Slots = Enum.GetValues().Except(IgnoredSlots).ToArray();
-
- private readonly Dictionary> _data = new();
-
public override void Initialize()
{
base.Initialize();
@@ -61,49 +51,4 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
borderColor.AccentVColor = otherBorderColor.AccentVColor;
}
}
-
- ///
- /// Get a list of valid chameleon targets for these slots.
- ///
- public IEnumerable GetValidTargets(SlotFlags slot)
- {
- var set = new HashSet();
- foreach (var availableSlot in _data.Keys)
- {
- if (slot.HasFlag(availableSlot))
- {
- set.UnionWith(_data[availableSlot]);
- }
- }
- return set;
- }
-
- private void PrepareAllVariants()
- {
- _data.Clear();
- var prototypes = _proto.EnumeratePrototypes();
-
- foreach (var proto in prototypes)
- {
- // check if this is valid clothing
- if (!IsValidTarget(proto))
- continue;
- if (!proto.TryGetComponent(out ClothingComponent? item, Factory))
- continue;
-
- // sort item by their slot flags
- // one item can be placed in several buckets
- foreach (var slot in Slots)
- {
- if (!item.Slots.HasFlag(slot))
- continue;
-
- if (!_data.ContainsKey(slot))
- {
- _data.Add(slot, new List());
- }
- _data[slot].Add(proto.ID);
- }
- }
- }
}
diff --git a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs
index bd86ffbec0..876f300e50 100644
--- a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs
+++ b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs
@@ -42,7 +42,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
var targets = _chameleon.GetValidTargets(st.Slot);
if (st.RequiredTag != null)
{
- var newTargets = new List();
+ var newTargets = new List();
foreach (var target in targets)
{
if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))
diff --git a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs
index bd45be4510..c6dce10776 100644
--- a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs
+++ b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs
@@ -19,8 +19,8 @@ public sealed partial class ChameleonMenu : DefaultWindow
private readonly SpriteSystem _sprite;
public event Action? OnIdSelected;
- private IEnumerable _possibleIds = Enumerable.Empty();
- private string? _selectedId;
+ private IEnumerable _possibleIds = [];
+ private EntProtoId? _selectedId;
private string _searchFilter = "";
public ChameleonMenu()
@@ -32,7 +32,7 @@ public sealed partial class ChameleonMenu : DefaultWindow
Search.OnTextChanged += OnSearchEntered;
}
- public void UpdateState(IEnumerable possibleIds, string? selectedId)
+ public void UpdateState(IEnumerable possibleIds, string? selectedId)
{
_possibleIds = possibleIds;
_selectedId = selectedId;
@@ -57,7 +57,7 @@ public sealed partial class ChameleonMenu : DefaultWindow
if (!_prototypeManager.TryIndex(id, out EntityPrototype? proto))
continue;
- var lowId = id.ToLowerInvariant();
+ var lowId = id.Id.ToLowerInvariant();
var lowName = proto.Name.ToLowerInvariant();
if (!lowId.Contains(searchFilterLow) && !lowName.Contains(_searchFilter))
continue;
diff --git a/Content.Client/Construction/ConstructionPlacementHijack.cs b/Content.Client/Construction/ConstructionPlacementHijack.cs
index e6a8e0f1f0..79112a8f8e 100644
--- a/Content.Client/Construction/ConstructionPlacementHijack.cs
+++ b/Content.Client/Construction/ConstructionPlacementHijack.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using Content.Shared.Construction.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Placement;
@@ -13,6 +13,9 @@ namespace Content.Client.Construction
private readonly ConstructionSystem _constructionSystem;
private readonly ConstructionPrototype? _prototype;
+ public ConstructionSystem? CurrentConstructionSystem { get { return _constructionSystem; } }
+ public ConstructionPrototype? CurrentPrototype { get { return _prototype; } }
+
public override bool CanRotate { get; }
public ConstructionPlacementHijack(ConstructionSystem constructionSystem, ConstructionPrototype? prototype)
diff --git a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
index 2d94034bb9..1b83f5ed03 100644
--- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
+++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs
@@ -131,7 +131,7 @@ namespace Content.Client.ContextMenu.UI
{
if (!Menus.TryPeek(out var topMenu))
{
- Logger.Error("Context Menu: Mouse entered menu without any open menus?");
+ Log.Error("Context Menu: Mouse entered menu without any open menus?");
return;
}
@@ -181,7 +181,7 @@ namespace Content.Client.ContextMenu.UI
{
if (!Menus.TryPeek(out var topMenu))
{
- Logger.Error("Context Menu: Attempting to open sub menu without any open menus?");
+ Log.Error("Context Menu: Attempting to open sub menu without any open menus?");
return;
}
diff --git a/Content.Client/ContextMenu/UI/EntityMenuUIController.cs b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs
index bda831394d..e0a88300db 100644
--- a/Content.Client/ContextMenu/UI/EntityMenuUIController.cs
+++ b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs
@@ -306,7 +306,7 @@ namespace Content.Client.ContextMenu.UI
// find the element associated with this entity
if (!Elements.TryGetValue(entity, out var element))
{
- Logger.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent(entity).EntityName} ({entity})");
+ Log.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent(entity).EntityName} ({entity})");
return;
}
diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs
index b38e70f6cc..de866ca9a4 100644
--- a/Content.Client/Damage/DamageVisualsSystem.cs
+++ b/Content.Client/Damage/DamageVisualsSystem.cs
@@ -27,7 +27,6 @@ namespace Content.Client.Damage;
public sealed class DamageVisualsSystem : VisualizerSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -229,7 +228,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem
private void AddDamageLayerToSprite(Entity spriteEnt, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null)
{
- var newLayer = _sprite.AddLayer(
+ var newLayer = SpriteSystem.AddLayer(
spriteEnt,
new SpriteSpecifier.Rsi(
new(sprite.Sprite), state
),
index
);
- _sprite.LayerMapSet(spriteEnt, mapKey, newLayer);
+ SpriteSystem.LayerMapSet(spriteEnt, mapKey, newLayer);
if (sprite.Color != null)
- _sprite.LayerSetColor(spriteEnt, newLayer, Color.FromHex(sprite.Color));
- _sprite.LayerSetVisible(spriteEnt, newLayer, false);
+ SpriteSystem.LayerSetColor(spriteEnt, newLayer, Color.FromHex(sprite.Color));
+ SpriteSystem.LayerSetVisible(spriteEnt, newLayer, false);
}
protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent damageVisComp, ref AppearanceChangeEvent args)
@@ -410,7 +409,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem spriteEnt, DamageVisualsComponent damageVisComp, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold)
{
- _sprite.LayerMapTryGet(spriteEnt.AsNullable(), key, out var spriteLayer, false);
+ SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), key, out var spriteLayer, false);
var visibility = spriteEnt.Comp[spriteLayer].Visible;
- _sprite.RemoveLayer(spriteEnt.AsNullable(), spriteLayer);
+ SpriteSystem.RemoveLayer(spriteEnt.AsNullable(), spriteLayer);
if (threshold == FixedPoint2.Zero) // these should automatically be invisible
threshold = damageVisComp.Thresholds[1];
- spriteLayer = _sprite.AddLayer(
+ spriteLayer = SpriteSystem.AddLayer(
spriteEnt.AsNullable(),
new SpriteSpecifier.Rsi(
new(sprite.Sprite),
$"{statePrefix}_{threshold}"
),
spriteLayer);
- _sprite.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer);
- _sprite.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility);
+ SpriteSystem.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer);
+ SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility);
// this is somewhat iffy since it constantly reallocates
damageVisComp.TopMostLayerKey = key;
}
@@ -610,7 +609,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem
private void UpdateOverlay(Entity spriteEnt, FixedPoint2 threshold)
{
- _sprite.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false);
+ SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false);
UpdateDamageLayerState(spriteEnt,
spriteLayer,
@@ -690,7 +689,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, DamageStateVisualsComponent component, ref AppearanceChangeEvent args)
{
var sprite = args.Sprite;
@@ -25,18 +23,18 @@ public sealed class DamageStateVisualizerSystem : VisualizerSystem (int)DrawDepth.DeadMobs)
{
component.OriginalDrawDepth = sprite.DrawDepth;
- _sprite.SetDrawDepth((uid, sprite), (int)DrawDepth.DeadMobs);
+ SpriteSystem.SetDrawDepth((uid, sprite), (int)DrawDepth.DeadMobs);
}
}
else if (component.OriginalDrawDepth != null)
{
- _sprite.SetDrawDepth((uid, sprite), component.OriginalDrawDepth.Value);
+ SpriteSystem.SetDrawDepth((uid, sprite), component.OriginalDrawDepth.Value);
component.OriginalDrawDepth = null;
}
}
diff --git a/Content.Client/Decals/DecalPlacementSystem.cs b/Content.Client/Decals/DecalPlacementSystem.cs
index a4495042c6..db00534a38 100644
--- a/Content.Client/Decals/DecalPlacementSystem.cs
+++ b/Content.Client/Decals/DecalPlacementSystem.cs
@@ -2,6 +2,7 @@ using System.Numerics;
using Content.Client.Actions;
using Content.Client.Decals.Overlays;
using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
using Content.Shared.Decals;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -21,9 +22,12 @@ public sealed class DecalPlacementSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
+ public static readonly EntProtoId DecalAction = "BaseMappingDecalAction";
+
private string? _decalId;
private Color _decalColor = Color.White;
private Angle _decalAngle = Angle.Zero;
@@ -152,19 +156,12 @@ public sealed class DecalPlacementSystem : EntitySystem
Cleanable = _cleanable,
};
- var actionId = Spawn(null);
- AddComp(actionId, new WorldTargetActionComponent
- {
- // non-unique actions may be considered duplicates when saving/loading.
- Icon = decalProto.Sprite,
- Repeat = true,
- ClientExclusive = true,
- CheckCanAccess = false,
- CheckCanInteract = false,
- Range = -1,
- Event = actionEvent,
- IconColor = _decalColor,
- });
+ var actionId = Spawn(DecalAction);
+ var action = Comp(actionId);
+ var ent = (actionId, action);
+ _actions.SetEvent(actionId, actionEvent);
+ _actions.SetIcon(ent, decalProto.Sprite);
+ _actions.SetIconColor(ent, _decalColor);
_metaData.SetEntityName(actionId, $"{_decalId} ({_decalColor.ToHex()}, {(int) _decalAngle.Degrees})");
diff --git a/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs b/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs
index 7fc81219f7..65b70727c0 100644
--- a/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs
+++ b/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs
@@ -11,7 +11,6 @@ namespace Content.Client.Electrocution;
public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem
{
[Dependency] private readonly IPlayerManager _playerMan = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
@@ -59,7 +58,7 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem(uid, ElectrifiedVisuals.IsElectrified, out var electrified, appearanceComp))
continue;
- _sprite.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, electrified);
+ SpriteSystem.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, electrified);
}
}
@@ -70,7 +69,7 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem();
while (electrifiedQuery.MoveNext(out var uid, out _, out _, out var spriteComp))
{
- _sprite.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, false);
+ SpriteSystem.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, false);
}
}
@@ -84,6 +83,6 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem(player));
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), ElectrifiedLayers.HUD, electrified && HasComp(player));
}
}
diff --git a/Content.Client/Ensnaring/Components/EnsnaringComponent.cs b/Content.Client/Ensnaring/Components/EnsnaringComponent.cs
deleted file mode 100644
index 63c8d0dfbe..0000000000
--- a/Content.Client/Ensnaring/Components/EnsnaringComponent.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-using Content.Shared.Ensnaring.Components;
-
-
diff --git a/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
index d0b39abb37..fee397b27d 100644
--- a/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs
@@ -28,12 +28,15 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private readonly TagSystem _tagSystem;
private readonly ExamineSystem _examineSystem;
private readonly GuidebookSystem _guidebookSystem;
+ private readonly ISawmill _sawmill;
+
public bool Interactive;
public Entity? Sprite => View.Entity == null || View.Sprite == null
@@ -53,6 +56,7 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
_tagSystem = _systemManager.GetEntitySystem();
_examineSystem = _systemManager.GetEntitySystem();
_guidebookSystem = _systemManager.GetEntitySystem();
+ _sawmill = _logManager.GetSawmill("guidebook.entity");
MouseFilter = MouseFilterMode.Stop;
}
@@ -135,7 +139,7 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag
{
if (!args.TryGetValue("Entity", out var proto))
{
- Logger.Error("Entity embed tag is missing entity prototype argument");
+ _sawmill.Error("Entity embed tag is missing entity prototype argument");
control = null;
return false;
}
diff --git a/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs
index da93fb46fd..d6f20d5a25 100644
--- a/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs
@@ -24,7 +24,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILogManager _logManager = default!;
- private ISawmill _sawmill = default!;
+ private readonly ISawmill _sawmill = default!;
public IPrototype? RepresentedPrototype { get; private set; }
@@ -34,7 +34,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
IoCManager.InjectDependencies(this);
MouseFilter = MouseFilterMode.Stop;
- _sawmill = _logManager.GetSawmill("guidemicrowaveembed");
+ _sawmill = _logManager.GetSawmill("guidebook.microwave");
}
public GuideMicrowaveEmbed(string recipe) : this()
diff --git a/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs b/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs
index 098e99459c..7c2a0ecfe1 100644
--- a/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs
+++ b/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs
@@ -15,12 +15,16 @@ namespace Content.Client.Guidebook.Controls;
[UsedImplicitly]
public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTag
{
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
+ private readonly ISawmill _sawmill;
+
public GuideMicrowaveGroupEmbed()
{
Orientation = LayoutOrientation.Vertical;
IoCManager.InjectDependencies(this);
+ _sawmill = _logManager.GetSawmill("guidebook.microwave_group");
MouseFilter = MouseFilterMode.Stop;
}
@@ -34,7 +38,7 @@ public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTa
control = null;
if (!args.TryGetValue("Group", out var group))
{
- Logger.Error("Microwave group embed tag is missing group argument");
+ _sawmill.Error("Microwave group embed tag is missing group argument");
return false;
}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
index 78cd765bdb..29569e40e6 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs
@@ -25,9 +25,11 @@ namespace Content.Client.Guidebook.Controls;
public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl
{
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private readonly ChemistryGuideDataSystem _chemistryGuideData;
+ private readonly ISawmill _sawmill;
public IPrototype? RepresentedPrototype { get; private set; }
@@ -35,6 +37,7 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _sawmill = _logManager.GetSawmill("guidebook.reagent");
_chemistryGuideData = _systemManager.GetEntitySystem();
MouseFilter = MouseFilterMode.Stop;
}
@@ -64,13 +67,13 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
control = null;
if (!args.TryGetValue("Reagent", out var id))
{
- Logger.Error("Reagent embed tag is missing reagent prototype argument");
+ _sawmill.Error("Reagent embed tag is missing reagent prototype argument");
return false;
}
if (!_prototype.TryIndex(id, out var reagent))
{
- Logger.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype");
+ _sawmill.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype");
return false;
}
diff --git a/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs
index 0c9356eccb..5373034b42 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs
@@ -17,12 +17,16 @@ namespace Content.Client.Guidebook.Controls;
[UsedImplicitly, GenerateTypedNameReferences]
public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag
{
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
+ private readonly ISawmill _sawmill;
+
public GuideReagentGroupEmbed()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _sawmill = _logManager.GetSawmill("guidebook.reagent_group");
MouseFilter = MouseFilterMode.Stop;
}
@@ -42,7 +46,7 @@ public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag
control = null;
if (!args.TryGetValue("Group", out var group))
{
- Logger.Error("Reagent group embed tag is missing group argument");
+ _sawmill.Error("Reagent group embed tag is missing group argument");
return false;
}
diff --git a/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs
index 88d264cb05..a01f2a8f17 100644
--- a/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs
@@ -17,12 +17,16 @@ namespace Content.Client.Guidebook.Controls;
[UsedImplicitly, GenerateTypedNameReferences]
public sealed partial class GuideTechDisciplineEmbed : BoxContainer, IDocumentTag
{
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
+ private readonly ISawmill _sawmill;
+
public GuideTechDisciplineEmbed()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _sawmill = _logManager.GetSawmill("guidebook.tech_discipline");
MouseFilter = MouseFilterMode.Stop;
}
@@ -42,7 +46,7 @@ public sealed partial class GuideTechDisciplineEmbed : BoxContainer, IDocumentTa
control = null;
if (!args.TryGetValue("Discipline", out var group))
{
- Logger.Error("Technology discipline embed tag is missing discipline argument");
+ _sawmill.Error("Technology discipline embed tag is missing discipline argument");
return false;
}
diff --git a/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs
index d61cc2d961..7d205f7cea 100644
--- a/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs
@@ -22,10 +22,12 @@ namespace Content.Client.Guidebook.Controls;
public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, ISearchableControl
{
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private readonly ResearchSystem _research;
private readonly SpriteSystem _sprite;
+ private readonly ISawmill _sawmill;
public GuideTechnologyEmbed()
{
@@ -33,6 +35,7 @@ public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, I
IoCManager.InjectDependencies(this);
_research = _systemManager.GetEntitySystem();
_sprite = _systemManager.GetEntitySystem();
+ _sawmill = _logManager.GetSawmill("guidebook.technology");
MouseFilter = MouseFilterMode.Stop;
}
@@ -61,13 +64,13 @@ public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, I
control = null;
if (!args.TryGetValue("Technology", out var id))
{
- Logger.Error("Technology embed tag is missing technology prototype argument");
+ _sawmill.Error("Technology embed tag is missing technology prototype argument");
return false;
}
if (!_prototype.TryIndex(id, out var technology))
{
- Logger.Error($"Specified technology prototype \"{id}\" is not a valid technology prototype");
+ _sawmill.Error($"Specified technology prototype \"{id}\" is not a valid technology prototype");
return false;
}
diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
index 8cc4c491f4..716e3e2034 100644
--- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
@@ -35,7 +35,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- _sawmill = Logger.GetSawmill("Guidebook");
+ _sawmill = Logger.GetSawmill("guidebook");
Tree.OnSelectedItemChanged += OnSelectionChanged;
@@ -232,7 +232,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA
{
// TODO GUIDEBOOK Maybe allow duplicate entries?
// E.g., for adding medicine under both chemicals & the chemist job
- Logger.Error($"Adding duplicate guide entry: {id}");
+ _sawmill.Error($"Adding duplicate guide entry: {id}");
return null;
}
diff --git a/Content.Client/Implants/ChameleonControllerSystem.cs b/Content.Client/Implants/ChameleonControllerSystem.cs
new file mode 100644
index 0000000000..7db4b37ef2
--- /dev/null
+++ b/Content.Client/Implants/ChameleonControllerSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Implants;
+
+namespace Content.Client.Implants;
+
+public sealed partial class ChameleonControllerSystem : SharedChameleonControllerSystem;
diff --git a/Content.Client/Implants/UI/ChameleonControllerBoundUserInterface.cs b/Content.Client/Implants/UI/ChameleonControllerBoundUserInterface.cs
new file mode 100644
index 0000000000..42b891ff50
--- /dev/null
+++ b/Content.Client/Implants/UI/ChameleonControllerBoundUserInterface.cs
@@ -0,0 +1,49 @@
+using Content.Shared.Clothing;
+using Content.Shared.Implants;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles;
+using Content.Shared.Timing;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Implants.UI;
+
+[UsedImplicitly]
+public sealed class ChameleonControllerBoundUserInterface : BoundUserInterface
+{
+ private readonly UseDelaySystem _delay;
+
+ [ViewVariables]
+ private ChameleonControllerMenu? _menu;
+
+ public ChameleonControllerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ _delay = EntMan.System();
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = this.CreateWindow();
+ _menu.OnJobSelected += OnJobSelected;
+ }
+
+ private void OnJobSelected(ProtoId outfit)
+ {
+ if (!EntMan.TryGetComponent(Owner, out var useDelayComp))
+ return;
+
+ if (!_delay.TryResetDelay((Owner, useDelayComp), true))
+ return;
+
+ SendMessage(new ChameleonControllerSelectedOutfitMessage(outfit));
+
+ if (!_delay.TryGetDelayInfo((Owner, useDelayComp), out var delay) || _menu == null)
+ return;
+
+ _menu._lockedUntil = DateTime.Now.Add(delay.Length);
+ _menu.UpdateGrid(true);
+ }
+}
diff --git a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml
new file mode 100644
index 0000000000..39322a2991
--- /dev/null
+++ b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
new file mode 100644
index 0000000000..a41e2e9293
--- /dev/null
+++ b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
@@ -0,0 +1,157 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Roles;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Implants;
+using Content.Shared.StatusIcon;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Implants.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class ChameleonControllerMenu : FancyWindow
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ private readonly SpriteSystem _sprite;
+ private readonly JobSystem _job;
+
+ // List of all the job protos that you can select!
+ private IEnumerable _outfits;
+
+ // Lock the UI until this time
+ public DateTime? _lockedUntil;
+
+ private static readonly ProtoId UnknownIcon = "JobIconUnknown";
+ private static readonly LocId UnknownDepartment = "department-Unknown";
+
+ public event Action>? OnJobSelected;
+
+ public ChameleonControllerMenu()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _sprite = _entityManager.System();
+ _job = _entityManager.System();
+
+ _outfits = _prototypeManager.EnumeratePrototypes();
+
+ UpdateGrid();
+ }
+
+ ///
+ /// Fill the grid with the correct job icons and buttons.
+ ///
+ /// Set to true to disable all the buttons.
+ public void UpdateGrid(bool disabled = false)
+ {
+ Grid.RemoveAllChildren();
+
+ // Dictionary to easily put outfits in departments.
+ // Department name -> UI element holding that department.
+ var departments = new Dictionary();
+
+ departments.Add(UnknownDepartment, CreateDepartment(UnknownDepartment));
+
+ // Go through every outfit and add them to the correct department.
+ foreach (var outfit in _outfits)
+ {
+ _prototypeManager.TryIndex(outfit.Job, out var jobProto);
+
+ var name = outfit.LoadoutName ?? outfit.Name ?? jobProto?.Name ?? "Prototype has no name or job.";
+
+ var jobIconId = outfit.Icon ?? jobProto?.Icon ?? UnknownIcon;
+ var jobIconProto = _prototypeManager.Index(jobIconId);
+
+ var outfitButton = CreateOutfitButton(disabled, name, jobIconProto, outfit.ID);
+
+ if (outfit.Job != null && _job.TryGetLowestWeightDepartment(outfit.Job, out var departmentPrototype))
+ {
+ if (!departments.ContainsKey(departmentPrototype.Name))
+ departments.Add(departmentPrototype.Name, CreateDepartment(departmentPrototype.Name));
+
+ departments[departmentPrototype.Name].AddChild(outfitButton);
+ }
+ else
+ {
+ departments[UnknownDepartment].AddChild(outfitButton);
+ }
+ }
+
+ // Sort the departments by their weight.
+ var departmentList = departments.ToList();
+ departmentList.Sort((a, b) => a.Value.ChildCount.CompareTo(b.Value.ChildCount));
+
+ // Actually add the departments to the window.
+ foreach (var department in departmentList)
+ {
+ Grid.AddChild(department.Value);
+ }
+ }
+
+ private BoxContainer CreateDepartment(string name)
+ {
+ var departmentContainer = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ };
+ departmentContainer.AddChild(new Label
+ {
+ Text = Loc.GetString(name),
+ });
+
+ return departmentContainer;
+ }
+
+ private BoxContainer CreateOutfitButton(bool disabled, string name, JobIconPrototype jobIconProto, ProtoId outfitProto)
+ {
+ var outfitButton = new BoxContainer();
+
+ var button = new Button
+ {
+ HorizontalExpand = true,
+ StyleClasses = {StyleBase.ButtonSquare},
+ ToolTip = Loc.GetString(name),
+ Text = Loc.GetString(name),
+ Margin = new Thickness(0, 0, 15, 0),
+ Disabled = disabled,
+ };
+
+ var jobIconTexture = new TextureRect
+ {
+ Texture = _sprite.Frame0(jobIconProto.Icon),
+ TextureScale = new Vector2(2.5f, 2.5f),
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ Margin = new Thickness(0, 0, 5, 0),
+ };
+
+ outfitButton.AddChild(jobIconTexture);
+ outfitButton.AddChild(button);
+
+ button.OnPressed += _ => JobButtonPressed(outfitProto);
+
+ return outfitButton;
+ }
+
+ private void JobButtonPressed(ProtoId outfit)
+ {
+ OnJobSelected?.Invoke(outfit);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (_lockedUntil == null || DateTime.Now < _lockedUntil)
+ return;
+
+ _lockedUntil = null;
+ UpdateGrid();
+ }
+}
diff --git a/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs b/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs
index 11ccc86550..fcbba97a9c 100644
--- a/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs
+++ b/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs
@@ -6,8 +6,6 @@ namespace Content.Client.Light.EntitySystems;
public sealed class EmergencyLightSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, EmergencyLightComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -16,13 +14,13 @@ public sealed class EmergencyLightSystem : VisualizerSystem(uid, EmergencyLightVisuals.On, out var on, args.Component))
on = false;
- _sprite.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, !on);
- _sprite.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, on);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, !on);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, on);
if (AppearanceSystem.TryGetData(uid, EmergencyLightVisuals.Color, out var color, args.Component))
{
- _sprite.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, color);
- _sprite.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, color);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, color);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, color);
}
}
}
diff --git a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs
index bc4c85cca7..e47430677d 100644
--- a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs
+++ b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs
@@ -10,7 +10,6 @@ public sealed class ExpendableLightSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -18,13 +16,13 @@ public sealed class LightBulbSystem : VisualizerSystem
switch (state)
{
case LightBulbState.Normal:
- _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState);
break;
case LightBulbState.Broken:
- _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState);
break;
case LightBulbState.Burned:
- _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState);
break;
}
}
@@ -32,7 +30,7 @@ public sealed class LightBulbSystem : VisualizerSystem
// also update sprites color
if (AppearanceSystem.TryGetData(uid, LightBulbVisuals.Color, out var color, args.Component))
{
- _sprite.SetColor((uid, args.Sprite), color);
+ SpriteSystem.SetColor((uid, args.Sprite), color);
}
}
}
diff --git a/Content.Client/Light/HandheldLightSystem.cs b/Content.Client/Light/HandheldLightSystem.cs
index d25b28756f..2a5aa949ff 100644
--- a/Content.Client/Light/HandheldLightSystem.cs
+++ b/Content.Client/Light/HandheldLightSystem.cs
@@ -44,7 +44,7 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem
return;
}
- if (!_appearance.TryGetData(uid, ToggleableLightVisuals.Enabled, out var enabled, args.Component))
+ if (!_appearance.TryGetData(uid, ToggleableVisuals.Enabled, out var enabled, args.Component))
{
return;
}
diff --git a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs
index ba83e69af7..7ed1fb2dde 100644
--- a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs
+++ b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs
@@ -11,7 +11,6 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem(uid, out var light))
{
- _sprite.LayerSetColor((uid, args.Sprite), PoweredLightLayers.Glow, light.Color);
+ SpriteSystem.LayerSetColor((uid, args.Sprite), PoweredLightLayers.Glow, light.Color);
}
- _sprite.LayerSetVisible((uid, args.Sprite), PoweredLightLayers.Glow, state == PoweredLightState.On);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PoweredLightLayers.Glow, state == PoweredLightState.On);
}
SetBlinkingAnimation(
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index cea8248e9f..121e8dbe71 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -32,7 +32,6 @@ public sealed class LobbyUIController : UIController, IOnStateEntered
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, LockVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null
@@ -22,14 +20,14 @@ public sealed class LockVisualizerSystem : VisualizerSystem(uid, StorageVisuals.Open, out var open, args.Component))
{
- _sprite.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, !open);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, !open);
}
else if (!(bool)unlockedStateExist!)
- _sprite.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, locked);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, locked);
if (!open && (bool)unlockedStateExist!)
{
- _sprite.LayerSetRsiState((uid, args.Sprite), LockVisualLayers.Lock, locked ? comp.StateLocked : comp.StateUnlocked);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), LockVisualLayers.Lock, locked ? comp.StateLocked : comp.StateUnlocked);
}
}
}
diff --git a/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs
new file mode 100644
index 0000000000..8fe4f3d386
--- /dev/null
+++ b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs
@@ -0,0 +1,14 @@
+namespace Content.Client.Machines.Components;
+
+///
+/// Component attached to all multipart machine ghosts
+/// Intended for client side usage only, but used on prototypes.
+///
+[RegisterComponent]
+public sealed partial class MultipartMachineGhostComponent : Component
+{
+ ///
+ /// Machine this particular ghost is linked to.
+ ///
+ public EntityUid? LinkedMachine = null;
+}
diff --git a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
new file mode 100644
index 0000000000..4919a5e8f2
--- /dev/null
+++ b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs
@@ -0,0 +1,109 @@
+using Content.Client.Examine;
+using Content.Client.Machines.Components;
+using Content.Shared.Machines.Components;
+using Content.Shared.Machines.EntitySystems;
+using Robust.Client.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Spawners;
+
+namespace Content.Client.Machines.EntitySystems;
+
+///
+/// Client side handling of multipart machines.
+/// Handles client side examination events to show the expected layout of the machine
+/// based on the origin of the main entity.
+///
+public sealed class MultipartMachineSystem : SharedMultipartMachineSystem
+{
+ private readonly EntProtoId _ghostPrototype = "MultipartMachineGhost";
+ private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180);
+
+ [Dependency] private readonly SpriteSystem _sprite = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly ISerializationManager _serialization= default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMachineExamined);
+ SubscribeLocalEvent(OnHandleState);
+ SubscribeLocalEvent(OnGhostDespawned);
+ }
+
+ ///
+ /// Handles spawning several ghost sprites to show where the different parts of the machine
+ /// should go and the rotations they're expected to have.
+ /// Can only show one set of ghost parts at a time and their location depends on the current map/grid
+ /// location of the origin machine.
+ ///
+ /// Entity/Component that has been inspected.
+ /// Args for the event.
+ private void OnMachineExamined(Entity ent, ref ClientExaminedEvent args)
+ {
+ if (ent.Comp.Ghosts.Count != 0)
+ {
+ // Already showing some part ghosts
+ return;
+ }
+
+ foreach (var part in ent.Comp.Parts.Values)
+ {
+ if (part.Entity.HasValue)
+ continue;
+
+ var entityCoords = new EntityCoordinates(ent.Owner, part.Offset);
+ var ghostEnt = Spawn(_ghostPrototype, entityCoords);
+
+ if (!XformQuery.TryGetComponent(ghostEnt, out var xform))
+ break;
+
+ xform.LocalRotation = part.Rotation;
+
+ Comp(ghostEnt).LinkedMachine = ent;
+
+ ent.Comp.Ghosts.Add(ghostEnt);
+
+ if (part.GhostProto == null)
+ continue;
+
+ var entProto = _prototype.Index(part.GhostProto.Value);
+ if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite)
+ return;
+
+ var ghostSprite = EnsureComp(ghostEnt);
+ _serialization.CopyTo(protoSprite, ref ghostSprite, notNullableOverride: true);
+
+ _sprite.SetColor((ghostEnt, ghostSprite), _partiallyTransparent);
+
+ _metaData.SetEntityName(ghostEnt, entProto.Name);
+ _metaData.SetEntityDescription(ghostEnt, entProto.Description);
+ }
+ }
+
+ private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ foreach (var part in ent.Comp.Parts.Values)
+ {
+ part.Entity = part.NetEntity.HasValue ? EnsureEntity(part.NetEntity.Value, ent) : null;
+ }
+ }
+
+ ///
+ /// Handles when a ghost part despawns after its short lifetime.
+ /// Will attempt to remove itself from the list of known ghost entities in the main multipart
+ /// machine component.
+ ///
+ /// Ghost entity that has been despawned.
+ /// Args for the event.
+ private void OnGhostDespawned(Entity ent, ref TimedDespawnEvent args)
+ {
+ if (!TryComp(ent.Comp.LinkedMachine, out var machine))
+ return;
+
+ machine.Ghosts.Remove(ent);
+ }
+}
diff --git a/Content.Client/Mapping/MappingSystem.cs b/Content.Client/Mapping/MappingSystem.cs
index 80189fbdfc..627977a526 100644
--- a/Content.Client/Mapping/MappingSystem.cs
+++ b/Content.Client/Mapping/MappingSystem.cs
@@ -4,8 +4,8 @@ using Content.Shared.Mapping;
using Content.Shared.Maps;
using Robust.Client.Placement;
using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
-using static Robust.Shared.Utility.SpriteSpecifier;
namespace Content.Client.Mapping;
@@ -14,16 +14,10 @@ public sealed partial class MappingSystem : EntitySystem
[Dependency] private readonly IPlacementManager _placementMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileMan = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
- ///
- /// The icon to use for space tiles.
- ///
- private readonly SpriteSpecifier _spaceIcon = new Texture(new ("Tiles/cropped_parallax.png"));
-
- ///
- /// The icon to use for entity-eraser.
- ///
- private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
+ public static readonly EntProtoId SpawnAction = "BaseMappingSpawnAction";
+ public static readonly EntProtoId EraserAction = "ActionMappingEraser";
public override void Initialize()
{
@@ -38,90 +32,46 @@ public sealed partial class MappingSystem : EntitySystem
/// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd
/// prefer if it were to function more like DecalPlacementSystem.
///
- private void OnFillActionSlot(FillActionSlotEvent ev)
+ private void OnFillActionSlot(FillActionSlotEvent args)
{
if (!_placementMan.IsActive)
return;
- if (ev.Action != null)
+ if (args.Action != null)
return;
- var actionEvent = new StartPlacementActionEvent();
- ITileDefinition? tileDef = null;
-
- if (_placementMan.CurrentPermission != null)
+ if (_placementMan.CurrentPermission is {} permission)
{
- actionEvent.EntityType = _placementMan.CurrentPermission.EntityType;
- actionEvent.PlacementOption = _placementMan.CurrentPermission.PlacementOption;
+ var ev = new StartPlacementActionEvent()
+ {
+ EntityType = permission.EntityType,
+ PlacementOption = permission.PlacementOption,
+ };
+ var action = Spawn(SpawnAction);
if (_placementMan.CurrentPermission.IsTile)
{
- tileDef = _tileMan[_placementMan.CurrentPermission.TileType];
- actionEvent.TileId = tileDef.ID;
+ if (_tileMan[_placementMan.CurrentPermission.TileType] is not ContentTileDefinition tileDef)
+ return;
+
+ if (!tileDef.MapAtmosphere && tileDef.Sprite is {} sprite)
+ _actions.SetIcon(action, new SpriteSpecifier.Texture(sprite));
+ ev.TileId = tileDef.ID;
+ _metaData.SetEntityName(action, Loc.GetString(tileDef.Name));
}
+ else if (permission.EntityType is {} id)
+ {
+ _actions.SetIcon(action, new SpriteSpecifier.EntityPrototype(id));
+ _metaData.SetEntityName(action, id);
+ }
+
+ _actions.SetEvent(action, ev);
+ args.Action = action;
}
else if (_placementMan.Eraser)
{
- actionEvent.Eraser = true;
+ args.Action = Spawn(EraserAction);
}
- else
- return;
-
- InstantActionComponent action;
- string name;
-
- if (tileDef != null)
- {
- if (tileDef is not ContentTileDefinition contentTileDef)
- return;
-
- var tileIcon = contentTileDef.MapAtmosphere
- ? _spaceIcon
- : new Texture(contentTileDef.Sprite!.Value);
-
- action = new InstantActionComponent
- {
- ClientExclusive = true,
- CheckCanInteract = false,
- Event = actionEvent,
- Icon = tileIcon
- };
-
- name = Loc.GetString(tileDef.Name);
- }
- else if (actionEvent.Eraser)
- {
- action = new InstantActionComponent
- {
- ClientExclusive = true,
- CheckCanInteract = false,
- Event = actionEvent,
- Icon = _deleteIcon,
- };
-
- name = Loc.GetString("action-name-mapping-erase");
- }
- else
- {
- if (string.IsNullOrWhiteSpace(actionEvent.EntityType))
- return;
-
- action = new InstantActionComponent
- {
- ClientExclusive = true,
- CheckCanInteract = false,
- Event = actionEvent,
- Icon = new EntityPrototype(actionEvent.EntityType),
- };
-
- name = actionEvent.EntityType;
- }
-
- var actionId = Spawn(null);
- AddComp(actionId, action);
- _metaData.SetEntityName(actionId, name);
-
- ev.Action = actionId;
}
private void OnStartPlacementAction(StartPlacementActionEvent args)
diff --git a/Content.Client/Movement/Systems/JetpackSystem.cs b/Content.Client/Movement/Systems/JetpackSystem.cs
index 6810bb24cc..bf80ed4252 100644
--- a/Content.Client/Movement/Systems/JetpackSystem.cs
+++ b/Content.Client/Movement/Systems/JetpackSystem.cs
@@ -34,10 +34,6 @@ public sealed class JetpackSystem : SharedJetpackSystem
{
Appearance.TryGetData(uid, JetpackVisuals.Enabled, out var enabled, args.Component);
- var state = "icon" + (enabled ? "-on" : "");
- if (args.Sprite != null)
- _sprite.LayerSetRsiState((uid, args.Sprite), 0, state);
-
if (TryComp(uid, out var clothing))
_clothing.SetEquippedPrefix(uid, enabled ? "on" : null, clothing);
}
diff --git a/Content.Client/Options/UI/OptionColorSlider.xaml b/Content.Client/Options/UI/OptionColorSlider.xaml
new file mode 100644
index 0000000000..4f5f082350
--- /dev/null
+++ b/Content.Client/Options/UI/OptionColorSlider.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/Options/UI/OptionColorSlider.xaml.cs b/Content.Client/Options/UI/OptionColorSlider.xaml.cs
new file mode 100644
index 0000000000..6f8f46a3b4
--- /dev/null
+++ b/Content.Client/Options/UI/OptionColorSlider.xaml.cs
@@ -0,0 +1,31 @@
+using Content.Client.Options.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Options.UI;
+
+///
+/// Standard UI control used for color sliders in the options menu. Intended for use with .
+///
+///
+[GenerateTypedNameReferences]
+public sealed partial class OptionColorSlider : Control
+{
+ ///
+ /// The text describing what this slider affects.
+ ///
+ public string? Title
+ {
+ get => TitleLabel.Text;
+ set => TitleLabel.Text = value;
+ }
+
+ ///
+ /// The example text showing the current color of the slider.
+ ///
+ public string? Example
+ {
+ get => ExampleLabel.Text;
+ set => ExampleLabel.Text = value;
+ }
+}
diff --git a/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs b/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs
index 31dd9897f4..ad262f94a2 100644
--- a/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs
+++ b/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs
@@ -121,6 +121,19 @@ public sealed partial class OptionsTabControlRow : Control
return AddOption(new OptionSliderFloatCVar(this, _cfg, cVar, slider, min, max, scale, FormatPercent));
}
+ ///
+ /// Add a color slider option, backed by a simple string CVar.
+ ///
+ /// The CVar represented by the slider.
+ /// The UI control for the option.
+ /// The option instance backing the added option.
+ public OptionColorSliderCVar AddOptionColorSlider(
+ CVarDef cVar,
+ OptionColorSlider slider)
+ {
+ return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider));
+ }
+
///
/// Add a slider option, backed by a simple integer CVar.
///
@@ -518,6 +531,58 @@ public sealed class OptionSliderFloatCVar : BaseOptionCVar
}
}
+///
+/// Implementation of a CVar option that simply corresponds with a string .
+///
+///
+public sealed class OptionColorSliderCVar : BaseOptionCVar
+{
+ private readonly OptionColorSlider _slider;
+
+ protected override string Value
+ {
+ get => _slider.Slider.Color.ToHex();
+ set
+ {
+ _slider.Slider.Color = Color.FromHex(value);
+ UpdateLabelColor();
+ }
+ }
+
+ ///
+ /// Creates a new instance of this type.
+ ///
+ ///
+ ///
+ /// It is generally more convenient to call overloads on
+ /// such as instead of instantiating this type directly.
+ ///
+ ///
+ /// The control row that owns this option.
+ /// The configuration manager to get and set values from.
+ /// The CVar that is being controlled by this option.
+ /// The UI control for the option.
+ public OptionColorSliderCVar(
+ OptionsTabControlRow controller,
+ IConfigurationManager cfg,
+ CVarDef cVar,
+ OptionColorSlider slider) : base(controller, cfg, cVar)
+ {
+ _slider = slider;
+
+ slider.Slider.OnColorChanged += _ =>
+ {
+ ValueChanged();
+ UpdateLabelColor();
+ };
+ }
+
+ private void UpdateLabelColor()
+ {
+ _slider.ExampleLabel.FontColorOverride = Color.FromHex(Value);
+ }
+}
+
///
/// Implementation of a CVar option that simply corresponds with an integer .
///
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
index 5041b498a0..41fac83c59 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
@@ -14,6 +14,10 @@
+
+
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
index f87cda746c..0c88eae50d 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
@@ -20,6 +20,8 @@ public sealed partial class AccessibilityTab : Control
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
+ Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
+ Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
diff --git a/Content.Client/Overlays/BlackAndWhiteOverlay.cs b/Content.Client/Overlays/BlackAndWhiteOverlay.cs
index aae2b63acf..785d1dad7f 100644
--- a/Content.Client/Overlays/BlackAndWhiteOverlay.cs
+++ b/Content.Client/Overlays/BlackAndWhiteOverlay.cs
@@ -1,5 +1,4 @@
using Robust.Client.Graphics;
-using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
@@ -7,9 +6,7 @@ namespace Content.Client.Overlays;
public sealed partial class BlackAndWhiteOverlay : Overlay
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
@@ -22,17 +19,6 @@ public sealed partial class BlackAndWhiteOverlay : Overlay
ZIndex = 10; // draw this over the DamageOverlay, RainbowOverlay etc.
}
- protected override bool BeforeDraw(in OverlayDrawArgs args)
- {
- if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
- return false;
-
- if (args.Viewport.Eye != eyeComp.Eye)
- return false;
-
- return true;
- }
-
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture == null)
diff --git a/Content.Client/Overlays/BlackAndWhiteOverlaySystem.cs b/Content.Client/Overlays/BlackAndWhiteOverlaySystem.cs
index 09c282d10a..7f5cd33a1f 100644
--- a/Content.Client/Overlays/BlackAndWhiteOverlaySystem.cs
+++ b/Content.Client/Overlays/BlackAndWhiteOverlaySystem.cs
@@ -1,7 +1,6 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
using Robust.Client.Graphics;
-using Robust.Client.Player;
namespace Content.Client.Overlays;
diff --git a/Content.Client/Overlays/NoirOverlay.cs b/Content.Client/Overlays/NoirOverlay.cs
new file mode 100644
index 0000000000..d2a6cbe8b3
--- /dev/null
+++ b/Content.Client/Overlays/NoirOverlay.cs
@@ -0,0 +1,33 @@
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+public sealed partial class NoirOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ public override bool RequestScreenTexture => true;
+ private readonly ShaderInstance _noirShader;
+
+ public NoirOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _noirShader = _prototypeManager.Index("Noir").InstanceUnique();
+ ZIndex = 9; // draw this over the DamageOverlay, RainbowOverlay etc, but before the black and white shader
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture == null)
+ return;
+
+ var handle = args.WorldHandle;
+ _noirShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+ handle.UseShader(_noirShader);
+ handle.DrawRect(args.WorldBounds, Color.White);
+ handle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Overlays/NoirOverlaySystem.cs b/Content.Client/Overlays/NoirOverlaySystem.cs
new file mode 100644
index 0000000000..d51a323419
--- /dev/null
+++ b/Content.Client/Overlays/NoirOverlaySystem.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Inventory.Events;
+using Content.Shared.Overlays;
+using Robust.Client.Graphics;
+
+namespace Content.Client.Overlays;
+
+public sealed partial class NoirOverlaySystem : EquipmentHudSystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+
+ private NoirOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _overlay = new();
+ }
+
+ protected override void UpdateInternal(RefreshEquipmentHudEvent component)
+ {
+ base.UpdateInternal(component);
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ protected override void DeactivateInternal()
+ {
+ base.DeactivateInternal();
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/PDA/PdaVisualizerSystem.cs b/Content.Client/PDA/PdaVisualizerSystem.cs
index 1c132de586..0d7685d6ae 100644
--- a/Content.Client/PDA/PdaVisualizerSystem.cs
+++ b/Content.Client/PDA/PdaVisualizerSystem.cs
@@ -6,20 +6,19 @@ namespace Content.Client.PDA;
public sealed class PdaVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
protected override void OnAppearanceChange(EntityUid uid, PdaVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (AppearanceSystem.TryGetData(uid, PdaVisuals.PdaType, out var pdaType, args.Component))
- _sprite.LayerSetRsiState((uid, args.Sprite), PdaVisualLayers.Base, pdaType);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PdaVisualLayers.Base, pdaType);
if (AppearanceSystem.TryGetData(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
- _sprite.LayerSetVisible((uid, args.Sprite), PdaVisualLayers.Flashlight, isFlashlightOn);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PdaVisualLayers.Flashlight, isFlashlightOn);
if (AppearanceSystem.TryGetData(uid, PdaVisuals.IdCardInserted, out var isCardInserted, args.Component))
- _sprite.LayerSetVisible((uid, args.Sprite), PdaVisualLayers.IdLight, isCardInserted);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PdaVisualLayers.IdLight, isCardInserted);
}
public enum PdaVisualLayers : byte
diff --git a/Content.Client/Paper/EnvelopeSystem.cs b/Content.Client/Paper/EnvelopeSystem.cs
index 49bd36349c..0e9dac6415 100644
--- a/Content.Client/Paper/EnvelopeSystem.cs
+++ b/Content.Client/Paper/EnvelopeSystem.cs
@@ -5,8 +5,6 @@ namespace Content.Client.Paper;
public sealed class EnvelopeSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -23,9 +21,9 @@ public sealed class EnvelopeSystem : VisualizerSystem
if (!Resolve(ent.Owner, ref sprite))
return;
- _sprite.LayerSetVisible((ent.Owner, sprite), EnvelopeVisualLayers.Open, ent.Comp.State == EnvelopeComponent.EnvelopeState.Open);
- _sprite.LayerSetVisible((ent.Owner, sprite), EnvelopeVisualLayers.Sealed, ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed);
- _sprite.LayerSetVisible((ent.Owner, sprite), EnvelopeVisualLayers.Torn, ent.Comp.State == EnvelopeComponent.EnvelopeState.Torn);
+ SpriteSystem.LayerSetVisible((ent.Owner, sprite), EnvelopeVisualLayers.Open, ent.Comp.State == EnvelopeComponent.EnvelopeState.Open);
+ SpriteSystem.LayerSetVisible((ent.Owner, sprite), EnvelopeVisualLayers.Sealed, ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed);
+ SpriteSystem.LayerSetVisible((ent.Owner, sprite), EnvelopeVisualLayers.Torn, ent.Comp.State == EnvelopeComponent.EnvelopeState.Torn);
}
public enum EnvelopeVisualLayers : byte
diff --git a/Content.Client/Paper/UI/PaperVisualizerSystem.cs b/Content.Client/Paper/UI/PaperVisualizerSystem.cs
index b8e429b64b..eb8489b7cb 100644
--- a/Content.Client/Paper/UI/PaperVisualizerSystem.cs
+++ b/Content.Client/Paper/UI/PaperVisualizerSystem.cs
@@ -6,26 +6,24 @@ namespace Content.Client.Paper.UI;
public sealed class PaperVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, PaperVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (AppearanceSystem.TryGetData(uid, PaperVisuals.Status, out var writingStatus, args.Component))
- _sprite.LayerSetVisible((uid, args.Sprite), PaperVisualLayers.Writing, writingStatus == PaperStatus.Written);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PaperVisualLayers.Writing, writingStatus == PaperStatus.Written);
if (AppearanceSystem.TryGetData(uid, PaperVisuals.Stamp, out var stampState, args.Component))
{
if (stampState != string.Empty)
{
- _sprite.LayerSetRsiState((uid, args.Sprite), PaperVisualLayers.Stamp, stampState);
- _sprite.LayerSetVisible((uid, args.Sprite), PaperVisualLayers.Stamp, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PaperVisualLayers.Stamp, stampState);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PaperVisualLayers.Stamp, true);
}
else
{
- _sprite.LayerSetVisible((uid, args.Sprite), PaperVisualLayers.Stamp, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PaperVisualLayers.Stamp, false);
}
}
diff --git a/Content.Client/ParticleAccelerator/ParticleAcceleratorPartVisualizerSystem.cs b/Content.Client/ParticleAccelerator/ParticleAcceleratorPartVisualizerSystem.cs
index 0f27da360c..0e98c1a292 100644
--- a/Content.Client/ParticleAccelerator/ParticleAcceleratorPartVisualizerSystem.cs
+++ b/Content.Client/ParticleAccelerator/ParticleAcceleratorPartVisualizerSystem.cs
@@ -5,14 +5,12 @@ namespace Content.Client.ParticleAccelerator;
public sealed class ParticleAcceleratorPartVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, ParticleAcceleratorPartVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
- if (!_sprite.LayerMapTryGet((uid, args.Sprite), ParticleAcceleratorVisualLayers.Unlit, out var index, false))
+ if (!SpriteSystem.LayerMapTryGet((uid, args.Sprite), ParticleAcceleratorVisualLayers.Unlit, out var index, false))
return;
if (!AppearanceSystem.TryGetData(uid, ParticleAcceleratorVisuals.VisualState, out var state, args.Component))
@@ -22,12 +20,12 @@ public sealed class ParticleAcceleratorPartVisualizerSystem : VisualizerSystem
-
+
diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
index 8b21e7d94b..cc5016c4a7 100644
--- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
+++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.xaml.cs
@@ -268,6 +268,7 @@ public sealed class PASegmentControl : Control
private RSI? _rsi;
public string BaseState { get; set; } = "control_box";
+ public bool DefaultVisible { get; set; } = false;
public PASegmentControl()
{
@@ -283,12 +284,14 @@ public sealed class PASegmentControl : Control
_rsi = IoCManager.Resolve().GetResource($"/Textures/Structures/Power/Generation/PA/{BaseState}.rsi").RSI;
MinSize = _rsi.Size;
_base.Texture = _rsi["completed"].Frame0;
+
+ SetVisible(DefaultVisible);
+ _unlit.Visible = DefaultVisible;
}
public void SetPowerState(ParticleAcceleratorUIState state, bool exists)
{
- _base.ShaderOverride = exists ? null : _greyScaleShader;
- _base.ModulateSelfOverride = exists ? null : new Color(127, 127, 127);
+ SetVisible(exists);
if (!state.Enabled || !exists)
{
@@ -319,4 +322,23 @@ public sealed class PASegmentControl : Control
_unlit.Texture = rState.Frame0;
}
+
+ ///
+ /// Adds/Removes the shading to the part in the control menu based on the
+ /// input state.
+ ///
+ /// True if the part exists, false otherwise
+ private void SetVisible(bool state)
+ {
+ if (state)
+ {
+ _base.ShaderOverride = null;
+ _base.ModulateSelfOverride = null;
+ }
+ else
+ {
+ _base.ShaderOverride = _greyScaleShader;
+ _base.ModulateSelfOverride = new Color(127, 127, 127);
+ }
+ }
}
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index ca04c93e74..2fe5c18fe0 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -1,5 +1,6 @@
using Content.Shared.Alert;
using Content.Shared.CCVar;
+using Content.Shared.Friction;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
diff --git a/Content.Client/Power/APC/ApcVisualizerSystem.cs b/Content.Client/Power/APC/ApcVisualizerSystem.cs
index 1bb7fa5363..0000055e3a 100644
--- a/Content.Client/Power/APC/ApcVisualizerSystem.cs
+++ b/Content.Client/Power/APC/ApcVisualizerSystem.cs
@@ -6,7 +6,6 @@ namespace Content.Client.Power.APC;
public sealed class ApcVisualizerSystem : VisualizerSystem
{
[Dependency] private readonly SharedPointLightSystem _lights = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
protected override void OnAppearanceChange(EntityUid uid, ApcVisualsComponent comp, ref AppearanceChangeEvent args)
{
@@ -14,8 +13,8 @@ public sealed class ApcVisualizerSystem : VisualizerSystem
return;
// get the mapped layer index of the first lock layer and the first channel layer
- var lockIndicatorOverlayStart = _sprite.LayerMapGet((uid, args.Sprite), ApcVisualLayers.InterfaceLock);
- var channelIndicatorOverlayStart = _sprite.LayerMapGet((uid, args.Sprite), ApcVisualLayers.Equipment);
+ var lockIndicatorOverlayStart = SpriteSystem.LayerMapGet((uid, args.Sprite), ApcVisualLayers.InterfaceLock);
+ var channelIndicatorOverlayStart = SpriteSystem.LayerMapGet((uid, args.Sprite), ApcVisualLayers.Equipment);
// Handle APC screen overlay:
if (!AppearanceSystem.TryGetData(uid, ApcVisuals.ChargeState, out var chargeState, args.Component))
@@ -23,7 +22,7 @@ public sealed class ApcVisualizerSystem : VisualizerSystem
if (chargeState >= 0 && chargeState < ApcChargeState.NumStates)
{
- _sprite.LayerSetRsiState((uid, args.Sprite), ApcVisualLayers.ChargeState, $"{comp.ScreenPrefix}-{comp.ScreenSuffixes[(sbyte)chargeState]}");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), ApcVisualLayers.ChargeState, $"{comp.ScreenPrefix}-{comp.ScreenSuffixes[(sbyte)chargeState]}");
// LockState does nothing currently. The backend doesn't exist.
if (AppearanceSystem.TryGetData(uid, ApcVisuals.LockState, out var lockStates, args.Component))
@@ -32,8 +31,8 @@ public sealed class ApcVisualizerSystem : VisualizerSystem
{
var layer = (byte)lockIndicatorOverlayStart + i;
var lockState = (sbyte)((lockStates >> (i << (sbyte)ApcLockState.LogWidth)) & (sbyte)ApcLockState.All);
- _sprite.LayerSetRsiState((uid, args.Sprite), layer, $"{comp.LockPrefix}{i}-{comp.LockSuffixes[lockState]}");
- _sprite.LayerSetVisible((uid, args.Sprite), layer, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, $"{comp.LockPrefix}{i}-{comp.LockSuffixes[lockState]}");
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, true);
}
}
@@ -44,8 +43,8 @@ public sealed class ApcVisualizerSystem : VisualizerSystem
{
var layer = (byte)channelIndicatorOverlayStart + i;
var channelState = (sbyte)((channelStates >> (i << (sbyte)ApcChannelState.LogWidth)) & (sbyte)ApcChannelState.All);
- _sprite.LayerSetRsiState((uid, args.Sprite), layer, $"{comp.ChannelPrefix}{i}-{comp.ChannelSuffixes[channelState]}");
- _sprite.LayerSetVisible((uid, args.Sprite), layer, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, $"{comp.ChannelPrefix}{i}-{comp.ChannelSuffixes[channelState]}");
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, true);
}
}
@@ -57,16 +56,16 @@ public sealed class ApcVisualizerSystem : VisualizerSystem
else
{
/// Overrides all of the lock and channel indicators.
- _sprite.LayerSetRsiState((uid, args.Sprite), ApcVisualLayers.ChargeState, comp.EmaggedScreenState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), ApcVisualLayers.ChargeState, comp.EmaggedScreenState);
for (var i = 0; i < comp.LockIndicators; ++i)
{
var layer = (byte)lockIndicatorOverlayStart + i;
- _sprite.LayerSetVisible((uid, args.Sprite), layer, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, false);
}
for (var i = 0; i < comp.ChannelIndicators; ++i)
{
var layer = (byte)channelIndicatorOverlayStart + i;
- _sprite.LayerSetVisible((uid, args.Sprite), layer, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, false);
}
if (TryComp(uid, out var light))
diff --git a/Content.Client/Power/SMES/SmesSystem.cs b/Content.Client/Power/SMES/SmesSystem.cs
index ac4b51547e..de0a5363cb 100644
--- a/Content.Client/Power/SMES/SmesSystem.cs
+++ b/Content.Client/Power/SMES/SmesSystem.cs
@@ -6,8 +6,6 @@ namespace Content.Client.Power.SMES;
public sealed class SmesVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, SmesComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -15,12 +13,12 @@ public sealed class SmesVisualizerSystem : VisualizerSystem
if (!AppearanceSystem.TryGetData(uid, SmesVisuals.LastChargeLevel, out var level, args.Component) || level == 0)
{
- _sprite.LayerSetVisible((uid, args.Sprite), SmesVisualLayers.Charge, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), SmesVisualLayers.Charge, false);
}
else
{
- _sprite.LayerSetVisible((uid, args.Sprite), SmesVisualLayers.Charge, true);
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Charge, $"{comp.ChargeOverlayPrefix}{level}");
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), SmesVisualLayers.Charge, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Charge, $"{comp.ChargeOverlayPrefix}{level}");
}
if (!AppearanceSystem.TryGetData(uid, SmesVisuals.LastChargeState, out var state, args.Component))
@@ -29,16 +27,16 @@ public sealed class SmesVisualizerSystem : VisualizerSystem
switch (state)
{
case ChargeState.Still:
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Input, $"{comp.InputOverlayPrefix}0");
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Output, $"{comp.OutputOverlayPrefix}1");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Input, $"{comp.InputOverlayPrefix}0");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Output, $"{comp.OutputOverlayPrefix}1");
break;
case ChargeState.Charging:
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Input, $"{comp.InputOverlayPrefix}1");
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Output, $"{comp.OutputOverlayPrefix}1");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Input, $"{comp.InputOverlayPrefix}1");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Output, $"{comp.OutputOverlayPrefix}1");
break;
case ChargeState.Discharging:
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Input, $"{comp.InputOverlayPrefix}0");
- _sprite.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Output, $"{comp.OutputOverlayPrefix}2");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Input, $"{comp.InputOverlayPrefix}0");
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), SmesVisualLayers.Output, $"{comp.OutputOverlayPrefix}2");
break;
}
}
diff --git a/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs b/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs
index 852cd61905..c76e25b7af 100644
--- a/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs
+++ b/Content.Client/PowerCell/PowerChargerVisualizerSystem.cs
@@ -5,8 +5,6 @@ namespace Content.Client.PowerCell;
public sealed class PowerChargerVisualizerSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, PowerChargerVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -16,22 +14,22 @@ public sealed class PowerChargerVisualizerSystem : VisualizerSystem(uid, CellVisual.Occupied, out var occupied, args.Component) && occupied)
{
// TODO: don't throw if it doesn't have a full state
- _sprite.LayerSetRsiState((uid, args.Sprite), PowerChargerVisualLayers.Base, comp.OccupiedState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerChargerVisualLayers.Base, comp.OccupiedState);
}
else
{
- _sprite.LayerSetRsiState((uid, args.Sprite), PowerChargerVisualLayers.Base, comp.EmptyState);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerChargerVisualLayers.Base, comp.EmptyState);
}
// Update lighting
if (AppearanceSystem.TryGetData(uid, CellVisual.Light, out var status, args.Component)
&& comp.LightStates.TryGetValue(status, out var lightState))
{
- _sprite.LayerSetRsiState((uid, args.Sprite), PowerChargerVisualLayers.Light, lightState);
- _sprite.LayerSetVisible((uid, args.Sprite), PowerChargerVisualLayers.Light, true);
+ SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerChargerVisualLayers.Light, lightState);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PowerChargerVisualLayers.Light, true);
}
else
- _sprite.LayerSetVisible((uid, args.Sprite), PowerChargerVisualLayers.Light, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), PowerChargerVisualLayers.Light, false);
}
}
diff --git a/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs b/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs
index 9ec24fae0e..d6e36dd0ad 100644
--- a/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs
+++ b/Content.Client/Radiation/Overlays/RadiationPulseOverlay.cs
@@ -59,7 +59,7 @@ namespace Content.Client.Radiation.Overlays
shd?.SetParameter("positionInput", tempCoords);
shd?.SetParameter("range", instance.Range);
var life = (_gameTiming.RealTime - instance.Start).TotalSeconds / instance.Duration;
- shd?.SetParameter("life", (float) life);
+ shd?.SetParameter("life", (float)life);
// There's probably a very good reason not to do this.
// Oh well!
diff --git a/Content.Client/Rootable/RootableSystem.cs b/Content.Client/Rootable/RootableSystem.cs
new file mode 100644
index 0000000000..33e68ae594
--- /dev/null
+++ b/Content.Client/Rootable/RootableSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Rootable;
+
+namespace Content.Client.Rootable;
+
+public sealed class RootableSystem : SharedRootableSystem;
diff --git a/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs b/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs
index 1832f61c82..6eb2944ea9 100644
--- a/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs
+++ b/Content.Client/Salvage/UI/SalvageExpeditionConsoleBoundUserInterface.cs
@@ -20,11 +20,15 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
+ private readonly ISawmill _sawmill;
+
public SalvageExpeditionConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
+ _sawmill = _logManager.GetSawmill("salvage.expedition.console");
}
protected override void Open()
@@ -111,7 +115,7 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
string LogAndReturnDefaultFactionDescription(string faction)
{
- Logger.Error($"Description is null or white space for SalvageFactionPrototype: {faction}");
+ _sawmill.Error($"Description is null or white space for SalvageFactionPrototype: {faction}");
return Loc.GetString(_protoManager.Index(faction).ID);
}
@@ -150,7 +154,7 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
string LogAndReturnDefaultBiomDescription(string biome)
{
- Logger.Error($"Description is null or white space for SalvageBiomeModPrototype: {biome}");
+ _sawmill.Error($"Description is null or white space for SalvageBiomeModPrototype: {biome}");
return Loc.GetString(_protoManager.Index(biome).ID);
}
diff --git a/Content.Client/Shuttles/ThrusterSystem.cs b/Content.Client/Shuttles/ThrusterSystem.cs
index 4205879b16..5ec5476258 100644
--- a/Content.Client/Shuttles/ThrusterSystem.cs
+++ b/Content.Client/Shuttles/ThrusterSystem.cs
@@ -8,8 +8,6 @@ namespace Content.Client.Shuttles;
///
public sealed class ThrusterSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
///
/// Updates whether or not the thruster is visibly active/thrusting.
///
@@ -19,7 +17,7 @@ public sealed class ThrusterSystem : VisualizerSystem
|| !AppearanceSystem.TryGetData(uid, ThrusterVisualState.State, out var state, args.Component))
return;
- _sprite.LayerSetVisible((uid, args.Sprite), ThrusterVisualLayers.ThrustOn, state);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), ThrusterVisualLayers.ThrustOn, state);
SetThrusting(
uid,
state && AppearanceSystem.TryGetData(uid, ThrusterVisualState.Thrusting, out var thrusting, args.Component) && thrusting,
@@ -32,14 +30,14 @@ public sealed class ThrusterSystem : VisualizerSystem
///
private void SetThrusting(EntityUid uid, bool value, SpriteComponent sprite)
{
- if (_sprite.LayerMapTryGet((uid, sprite), ThrusterVisualLayers.Thrusting, out var thrustingLayer, false))
+ if (SpriteSystem.LayerMapTryGet((uid, sprite), ThrusterVisualLayers.Thrusting, out var thrustingLayer, false))
{
- _sprite.LayerSetVisible((uid, sprite), thrustingLayer, value);
+ SpriteSystem.LayerSetVisible((uid, sprite), thrustingLayer, value);
}
- if (_sprite.LayerMapTryGet((uid, sprite), ThrusterVisualLayers.ThrustingUnshaded, out var unshadedLayer, false))
+ if (SpriteSystem.LayerMapTryGet((uid, sprite), ThrusterVisualLayers.ThrustingUnshaded, out var unshadedLayer, false))
{
- _sprite.LayerSetVisible((uid, sprite), unshadedLayer, value);
+ SpriteSystem.LayerSetVisible((uid, sprite), unshadedLayer, value);
}
}
}
diff --git a/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs b/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs
index 4753b308aa..301de2bf04 100644
--- a/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs
+++ b/Content.Client/Storage/Systems/StorageContainerVisualsSystem.cs
@@ -8,8 +8,6 @@ namespace Content.Client.Storage.Systems;
///
public sealed class StorageContainerVisualsSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, StorageContainerVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -23,7 +21,7 @@ public sealed class StorageContainerVisualsSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
public override void Initialize()
{
base.Initialize();
@@ -25,7 +23,7 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem(uid, out var sprite))
return;
- _sprite.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
+ SpriteSystem.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
protected override void OnAppearanceChange(EntityUid uid, EntityStorageVisualsComponent comp, ref AppearanceChangeEvent args)
@@ -35,41 +33,41 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem().TryGetActionData(actionId, out var action) &&
- action.Icon != null)
- {
- texture = spriteSys.Frame0(action.Icon);
- }
+ if (_entityManager.System().GetAction(actionId)?.Comp?.Icon is {} icon)
+ texture = spriteSys.Frame0(icon);
}
var listingInStock = GetListingPriceString(listing);
diff --git a/Content.Client/TextScreen/TextScreenSystem.cs b/Content.Client/TextScreen/TextScreenSystem.cs
index eaa0c5e0b7..0e6406a38c 100644
--- a/Content.Client/TextScreen/TextScreenSystem.cs
+++ b/Content.Client/TextScreen/TextScreenSystem.cs
@@ -27,7 +27,6 @@ namespace Content.Client.TextScreen;
public sealed class TextScreenSystem : VisualizerSystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
///
/// Contains char/state Key/Value pairs.
@@ -90,11 +89,11 @@ public sealed class TextScreenSystem : VisualizerSystem(uid);
@@ -190,7 +189,7 @@ public sealed class TextScreenSystem : VisualizerSystem pairs.Value != null))
- _sprite.LayerSetRsiState((uid, sprite), key, state);
+ SpriteSystem.LayerSetRsiState((uid, sprite), key, state);
}
public override void Update(float frameTime)
diff --git a/Content.Client/Toggleable/ToggleableLightVisualsComponent.cs b/Content.Client/Toggleable/ToggleableLightVisualsComponent.cs
deleted file mode 100644
index c42d5a7faa..0000000000
--- a/Content.Client/Toggleable/ToggleableLightVisualsComponent.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using Content.Shared.Hands.Components;
-
-namespace Content.Client.Toggleable;
-
-///
-/// Component that handles the toggling the visuals of some light emitting entity.
-///
-///
-/// This will toggle the visibility of layers on an entity's sprite, the in-hand visuals, and the clothing/equipment
-/// visuals. This will modify the color of any attached point lights.
-///
-[RegisterComponent]
-public sealed partial class ToggleableLightVisualsComponent : Component
-{
- ///
- /// Sprite layer that will have its visibility toggled when this item is toggled.
- ///
- [DataField("spriteLayer")]
- public string? SpriteLayer = "light";
-
- ///
- /// Layers to add to the sprite of the player that is holding this entity (while the component is toggled on).
- ///
- [DataField("inhandVisuals")]
- public Dictionary> InhandVisuals = new();
-
- ///
- /// Layers to add to the sprite of the player that is wearing this entity (while the component is toggled on).
- ///
- [DataField("clothingVisuals")]
- public Dictionary> ClothingVisuals = new();
-}
diff --git a/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs b/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
deleted file mode 100644
index 28144486c3..0000000000
--- a/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-using Content.Client.Clothing;
-using Content.Client.Items.Systems;
-using Content.Shared.Clothing;
-using Content.Shared.Hands;
-using Content.Shared.Inventory;
-using Content.Shared.Item;
-using Content.Shared.Toggleable;
-using Robust.Client.GameObjects;
-using Robust.Shared.Utility;
-using System.Linq;
-
-namespace Content.Client.Toggleable;
-
-public sealed class ToggleableLightVisualsSystem : VisualizerSystem
-{
- [Dependency] private readonly SharedItemSystem _itemSys = default!;
- [Dependency] private readonly SharedPointLightSystem _lights = default!;
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnGetHeldVisuals, after: new[] { typeof(ItemSystem) });
- SubscribeLocalEvent(OnGetEquipmentVisuals, after: new[] { typeof(ClientClothingSystem) });
- }
-
- protected override void OnAppearanceChange(EntityUid uid, ToggleableLightVisualsComponent component, ref AppearanceChangeEvent args)
- {
- if (!AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Enabled, out var enabled, args.Component))
- return;
-
- var modulate = AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Color, out var color, args.Component);
-
- // Update the item's sprite
- if (args.Sprite != null && component.SpriteLayer != null && _sprite.LayerMapTryGet((uid, args.Sprite), component.SpriteLayer, out var layer, false))
- {
- _sprite.LayerSetVisible((uid, args.Sprite), layer, enabled);
- if (modulate)
- _sprite.LayerSetColor((uid, args.Sprite), layer, color);
- }
-
- // Update any point-lights
- if (TryComp(uid, out PointLightComponent? light))
- {
- DebugTools.Assert(!light.NetSyncEnabled, "light visualizers require point lights without net-sync");
- _lights.SetEnabled(uid, enabled, light);
- if (enabled && modulate)
- {
- _lights.SetColor(uid, color, light);
- }
- }
-
- // update clothing & in-hand visuals.
- _itemSys.VisualsChanged(uid);
- }
-
- ///
- /// Add the unshaded light overlays to any clothing sprites.
- ///
- private void OnGetEquipmentVisuals(EntityUid uid, ToggleableLightVisualsComponent component, GetEquipmentVisualsEvent args)
- {
- if (!TryComp(uid, out AppearanceComponent? appearance)
- || !AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Enabled, out var enabled, appearance)
- || !enabled)
- return;
-
- if (!TryComp(args.Equipee, out InventoryComponent? inventory))
- return;
- List? layers = null;
-
- // attempt to get species specific data
- if (inventory.SpeciesId != null)
- component.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers);
-
- // No species specific data. Try to default to generic data.
- if (layers == null && !component.ClothingVisuals.TryGetValue(args.Slot, out layers))
- return;
-
- var modulate = AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Color, out var color, appearance);
-
- var i = 0;
- foreach (var layer in layers)
- {
- var key = layer.MapKeys?.FirstOrDefault();
- if (key == null)
- {
- key = i == 0 ? $"{args.Slot}-toggle" : $"{args.Slot}-toggle-{i}";
- i++;
- }
-
- if (modulate)
- layer.Color = color;
-
- args.Layers.Add((key, layer));
- }
- }
-
- private void OnGetHeldVisuals(EntityUid uid, ToggleableLightVisualsComponent component, GetInhandVisualsEvent args)
- {
- if (!TryComp(uid, out AppearanceComponent? appearance)
- || !AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Enabled, out var enabled, appearance)
- || !enabled)
- return;
-
- if (!component.InhandVisuals.TryGetValue(args.Location, out var layers))
- return;
-
- var modulate = AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Color, out var color, appearance);
-
- var i = 0;
- var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}-toggle";
- foreach (var layer in layers)
- {
- var key = layer.MapKeys?.FirstOrDefault();
- if (key == null)
- {
- key = i == 0 ? defaultKey : $"{defaultKey}-{i}";
- i++;
- }
-
- if (modulate)
- layer.Color = color;
-
- args.Layers.Add((key, layer));
- }
- }
-}
diff --git a/Content.Client/Toggleable/ToggleableVisualsComponent.cs b/Content.Client/Toggleable/ToggleableVisualsComponent.cs
new file mode 100644
index 0000000000..cab8650466
--- /dev/null
+++ b/Content.Client/Toggleable/ToggleableVisualsComponent.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Hands.Components;
+
+namespace Content.Client.Toggleable;
+
+///
+/// Component that handles toggling the visuals of an entity, including layers on an entity's sprite,
+/// the in-hand visuals, and the clothing/equipment visuals.
+///
+///
+[RegisterComponent]
+public sealed partial class ToggleableVisualsComponent : Component
+{
+ ///
+ /// Sprite layer that will have its visibility toggled when this item is toggled.
+ ///
+ [DataField(required: true)]
+ public string? SpriteLayer;
+
+ ///
+ /// Layers to add to the sprite of the player that is holding this entity (while the component is toggled on).
+ ///
+ [DataField]
+ public Dictionary> InhandVisuals = new();
+
+ ///
+ /// Layers to add to the sprite of the player that is wearing this entity (while the component is toggled on).
+ ///
+ [DataField]
+ public Dictionary> ClothingVisuals = new();
+}
diff --git a/Content.Client/Toggleable/ToggleableVisualsSystem.cs b/Content.Client/Toggleable/ToggleableVisualsSystem.cs
new file mode 100644
index 0000000000..3aacd9a4fa
--- /dev/null
+++ b/Content.Client/Toggleable/ToggleableVisualsSystem.cs
@@ -0,0 +1,140 @@
+using System.Linq;
+using Content.Client.Clothing;
+using Content.Client.Items.Systems;
+using Content.Shared.Clothing;
+using Content.Shared.Hands;
+using Content.Shared.Inventory;
+using Content.Shared.Item;
+using Content.Shared.Light.Components;
+using Content.Shared.Toggleable;
+using Robust.Client.GameObjects;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Toggleable;
+
+///
+/// Implements the behavior of by reacting to
+/// , for the sprite directly; for the
+/// in-hand visuals; and for the clothing visuals.
+///
+///
+public sealed class ToggleableVisualsSystem : VisualizerSystem
+{
+ [Dependency] private readonly SharedItemSystem _item = default!;
+ [Dependency] private readonly SharedPointLightSystem _pointLight = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnGetHeldVisuals,
+ after: [typeof(ItemSystem)]);
+ SubscribeLocalEvent(OnGetEquipmentVisuals,
+ after: [typeof(ClientClothingSystem)]);
+ }
+
+ protected override void OnAppearanceChange(EntityUid uid,
+ ToggleableVisualsComponent component,
+ ref AppearanceChangeEvent args)
+ {
+ if (!AppearanceSystem.TryGetData(uid, ToggleableVisuals.Enabled, out var enabled, args.Component))
+ return;
+
+ var modulateColor =
+ AppearanceSystem.TryGetData(uid, ToggleableVisuals.Color, out var color, args.Component);
+
+ // Update the item's sprite
+ if (args.Sprite != null && component.SpriteLayer != null &&
+ SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.SpriteLayer, out var layer, false))
+ {
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, enabled);
+ if (modulateColor)
+ SpriteSystem.LayerSetColor((uid, args.Sprite), component.SpriteLayer, color);
+ }
+
+ // If there's a `ItemTogglePointLightComponent` that says to apply the color to attached lights, do so.
+ if (TryComp(uid, out var toggleLights) &&
+ TryComp(uid, out PointLightComponent? light))
+ {
+ DebugTools.Assert(!light.NetSyncEnabled,
+ $"{typeof(ItemTogglePointLightComponent)} requires point lights without net-sync");
+ _pointLight.SetEnabled(uid, enabled, light);
+ if (modulateColor && toggleLights.ToggleableVisualsColorModulatesLights)
+ {
+ _pointLight.SetColor(uid, color, light);
+ }
+ }
+
+ // update clothing & in-hand visuals.
+ _item.VisualsChanged(uid);
+ }
+
+ private void OnGetEquipmentVisuals(EntityUid uid,
+ ToggleableVisualsComponent component,
+ GetEquipmentVisualsEvent args)
+ {
+ if (!TryComp(uid, out AppearanceComponent? appearance)
+ || !AppearanceSystem.TryGetData(uid, ToggleableVisuals.Enabled, out var enabled, appearance)
+ || !enabled)
+ return;
+
+ if (!TryComp(args.Equipee, out InventoryComponent? inventory))
+ return;
+ List? layers = null;
+
+ // attempt to get species specific data
+ if (inventory.SpeciesId != null)
+ component.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers);
+
+ // No species specific data. Try to default to generic data.
+ if (layers == null && !component.ClothingVisuals.TryGetValue(args.Slot, out layers))
+ return;
+
+ var modulateColor = AppearanceSystem.TryGetData(uid, ToggleableVisuals.Color, out var color, appearance);
+
+ var i = 0;
+ foreach (var layer in layers)
+ {
+ var key = layer.MapKeys?.FirstOrDefault();
+ if (key == null)
+ {
+ key = i == 0 ? $"{args.Slot}-toggle" : $"{args.Slot}-toggle-{i}";
+ i++;
+ }
+
+ if (modulateColor)
+ layer.Color = color;
+
+ args.Layers.Add((key, layer));
+ }
+ }
+
+ private void OnGetHeldVisuals(EntityUid uid, ToggleableVisualsComponent component, GetInhandVisualsEvent args)
+ {
+ if (!TryComp(uid, out AppearanceComponent? appearance)
+ || !AppearanceSystem.TryGetData(uid, ToggleableVisuals.Enabled, out var enabled, appearance)
+ || !enabled)
+ return;
+
+ if (!component.InhandVisuals.TryGetValue(args.Location, out var layers))
+ return;
+
+ var modulateColor = AppearanceSystem.TryGetData(uid, ToggleableVisuals.Color, out var color, appearance);
+
+ var i = 0;
+ var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}-toggle";
+ foreach (var layer in layers)
+ {
+ var key = layer.MapKeys?.FirstOrDefault();
+ if (key == null)
+ {
+ key = i == 0 ? defaultKey : $"{defaultKey}-{i}";
+ i++;
+ }
+
+ if (modulateColor)
+ layer.Color = color;
+
+ args.Layers.Add((key, layer));
+ }
+ }
+}
diff --git a/Content.Client/TurretController/DeployableTurretControllerSystem.cs b/Content.Client/TurretController/DeployableTurretControllerSystem.cs
new file mode 100644
index 0000000000..c3b305f821
--- /dev/null
+++ b/Content.Client/TurretController/DeployableTurretControllerSystem.cs
@@ -0,0 +1,9 @@
+using Content.Shared.TurretController;
+
+namespace Content.Client.TurretController;
+
+///
+public sealed class DeployableTurretControllerSystem : SharedDeployableTurretControllerSystem
+{
+
+}
diff --git a/Content.Client/TurretController/TurretControllerWindow.xaml b/Content.Client/TurretController/TurretControllerWindow.xaml
new file mode 100644
index 0000000000..5f4af68f91
--- /dev/null
+++ b/Content.Client/TurretController/TurretControllerWindow.xaml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/TurretController/TurretControllerWindow.xaml.cs b/Content.Client/TurretController/TurretControllerWindow.xaml.cs
new file mode 100644
index 0000000000..2f6d81ff70
--- /dev/null
+++ b/Content.Client/TurretController/TurretControllerWindow.xaml.cs
@@ -0,0 +1,202 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using Content.Shared.TurretController;
+using Content.Shared.Turrets;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Numerics;
+
+namespace Content.Client.TurretController;
+
+[GenerateTypedNameReferences]
+public sealed partial class TurretControllerWindow : BaseWindow
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IResourceCache _cache = default!;
+
+ private readonly AccessReaderSystem _accessReaderSystem;
+
+ private EntityUid? _owner;
+
+ // Button groups
+ private readonly ButtonGroup _armamentButtons = new();
+
+ // Events
+ public event Action>, bool>? OnAccessLevelsChangedEvent;
+ public event Action? OnArmamentSettingChangedEvent;
+
+ // Colors
+ private static readonly Dictionary ThemeColors = new()
+ {
+ [TurretArmamentSetting.Safe] = Color.FromHex("#33e633"),
+ [TurretArmamentSetting.Stun] = Color.FromHex("#dfb827"),
+ [TurretArmamentSetting.Lethal] = Color.FromHex("#da2a2a")
+ };
+
+ public TurretControllerWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _accessReaderSystem = _entManager.System();
+
+ CloseButton.OnPressed += _ => Close();
+
+ // Set up armament buttons
+ SafeButton.OnToggled += args => OnArmamentButtonPressed(SafeButton, TurretArmamentSetting.Safe);
+ StunButton.OnToggled += args => OnArmamentButtonPressed(StunButton, TurretArmamentSetting.Stun);
+ LethalButton.OnToggled += args => OnArmamentButtonPressed(LethalButton, TurretArmamentSetting.Lethal);
+
+ SafeButton.Group = _armamentButtons;
+ StunButton.Group = _armamentButtons;
+ LethalButton.Group = _armamentButtons;
+
+ SafeButton.Label.AddStyleClass("ConsoleText");
+ StunButton.Label.AddStyleClass("ConsoleText");
+ LethalButton.Label.AddStyleClass("ConsoleText");
+
+ // Set up access configuration buttons
+ AccessConfiguration.SetMonotone(true);
+ AccessConfiguration.SetLabelStyleClass("ConsoleText");
+ AccessConfiguration.OnAccessLevelsChangedEvent += OnAccessLevelsChanged;
+
+ // Override footer font
+ var smallFont = _cache.NotoStack(size: 8);
+ Footer.FontOverride = smallFont;
+ }
+
+ private void OnAccessLevelsChanged(HashSet> accessLevels, bool isPressed)
+ {
+ OnAccessLevelsChangedEvent?.Invoke(accessLevels, isPressed);
+ }
+
+ private void OnArmamentButtonPressed(MonotoneButton pressedButton, TurretArmamentSetting setting)
+ {
+ UpdateTheme(setting);
+ OnArmamentSettingChangedEvent?.Invoke(setting);
+ }
+
+ private void Initialize()
+ {
+ RefreshLinkedTurrets(new());
+
+ if (_entManager.TryGetComponent(_owner, out var turretController))
+ {
+ AccessConfiguration.SetAccessGroups(turretController.AccessGroups);
+ AccessConfiguration.SetAccessLevels(turretController.AccessLevels);
+ UpdateTheme((TurretArmamentSetting)turretController.ArmamentState);
+ }
+
+ if (_entManager.TryGetComponent(_owner, out var turretTargetSettings))
+ {
+ RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
+ }
+ }
+
+ public void SetOwner(EntityUid owner)
+ {
+ _owner = owner;
+
+ Initialize();
+ }
+
+ private void UpdateTheme(TurretArmamentSetting setting)
+ {
+ var setPressedOn = setting switch
+ {
+ TurretArmamentSetting.Safe => SafeButton,
+ TurretArmamentSetting.Stun => StunButton,
+ TurretArmamentSetting.Lethal => LethalButton,
+ _ => throw new NotImplementedException(),
+ };
+ setPressedOn.Pressed = true;
+
+ var canInteract = IsLocalPlayerAllowedToInteract();
+
+ SafeButton.Disabled = !SafeButton.Pressed && !canInteract;
+ StunButton.Disabled = !StunButton.Pressed && !canInteract;
+ LethalButton.Disabled = !LethalButton.Pressed && !canInteract;
+
+ ContentsContainer.Modulate = ThemeColors[setting];
+ }
+
+ public void UpdateState(DeployableTurretControllerBoundInterfaceState state)
+ {
+ if (_entManager.TryGetComponent(_owner, out var turretController))
+ UpdateTheme((TurretArmamentSetting)turretController.ArmamentState);
+
+ if (_entManager.TryGetComponent(_owner, out var turretTargetSettings))
+ RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
+
+ RefreshLinkedTurrets(state.TurretStateByAddress);
+ }
+
+ public void RefreshLinkedTurrets(Dictionary turretStates)
+ {
+ var turretCount = turretStates.Count;
+ var hasTurrets = turretCount > 0;
+
+ NoLinkedTurretsText.Visible = !hasTurrets;
+ LinkedTurretsContainer.Visible = hasTurrets;
+
+ LinkedTurretsContainer.RemoveAllChildren();
+
+ foreach (var (address, state) in turretStates)
+ {
+ var text = Loc.GetString(
+ "turret-controls-window-turret-status",
+ ("device", address),
+ ("status", Loc.GetString(state))
+ );
+
+ var label = new Label
+ {
+ Text = text,
+ HorizontalAlignment = HAlignment.Left,
+ Margin = new Thickness(10f, 0f, 10f, 0f),
+ HorizontalExpand = true,
+ SetHeight = 20f,
+ };
+
+ label.AddStyleClass("ConsoleText");
+
+ LinkedTurretsContainer.AddChild(label);
+ }
+
+ TurretStatusHeader.Text = Loc.GetString("turret-controls-window-turret-status-label", ("count", turretCount));
+ }
+
+ public void RefreshAccessControls(HashSet> exemptAccessLevels)
+ {
+ AccessConfiguration.SetActiveAccessLevels(exemptAccessLevels);
+ AccessConfiguration.SetLocalPlayerAccessibility(IsLocalPlayerAllowedToInteract());
+ }
+
+ protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
+ {
+ return DragMode.Move;
+ }
+
+ private bool IsLocalPlayerAllowedToInteract()
+ {
+ if (_owner == null || _playerManager.LocalSession?.AttachedEntity == null)
+ return false;
+
+ return _accessReaderSystem.IsAllowed(_playerManager.LocalSession.AttachedEntity.Value, _owner.Value);
+ }
+
+ public enum TurretArmamentSetting
+ {
+ Safe = -1,
+ Stun = 0,
+ Lethal = 1,
+ }
+}
diff --git a/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs b/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs
new file mode 100644
index 0000000000..ab1635b8d8
--- /dev/null
+++ b/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Access;
+using Content.Shared.TurretController;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.TurretController;
+
+public sealed class TurretControllerWindowBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+ [ViewVariables]
+ private TurretControllerWindow? _window;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.SetOwner(Owner);
+ _window.OpenCentered();
+
+ _window.OnAccessLevelsChangedEvent += OnAccessLevelChanged;
+ _window.OnArmamentSettingChangedEvent += OnArmamentSettingChanged;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not DeployableTurretControllerBoundInterfaceState { } castState)
+ return;
+
+ _window?.UpdateState(castState);
+ }
+
+ private void OnAccessLevelChanged(HashSet> accessLevels, bool enabled)
+ {
+ SendPredictedMessage(new DeployableTurretExemptAccessLevelChangedMessage(accessLevels, enabled));
+ }
+
+ private void OnArmamentSettingChanged(TurretControllerWindow.TurretArmamentSetting setting)
+ {
+ SendPredictedMessage(new DeployableTurretArmamentSettingChangedMessage((int)setting));
+ }
+}
diff --git a/Content.Client/Turrets/DeployableTurretSystem.cs b/Content.Client/Turrets/DeployableTurretSystem.cs
index 5e84b1e01a..05cacba6f1 100644
--- a/Content.Client/Turrets/DeployableTurretSystem.cs
+++ b/Content.Client/Turrets/DeployableTurretSystem.cs
@@ -84,9 +84,6 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste
if (_animation.HasRunningAnimation(ent, animPlayer, DeployableTurretComponent.AnimationKey))
return;
- if (state == ent.Comp.VisualState)
- return;
-
var targetState = state & DeployableTurretState.Deployed;
var destinationState = ent.Comp.VisualState & DeployableTurretState.Deployed;
diff --git a/Content.Client/UserInterface/Controls/MonotoneButton.cs b/Content.Client/UserInterface/Controls/MonotoneButton.cs
index 7271ee7de7..b19a2c640f 100644
--- a/Content.Client/UserInterface/Controls/MonotoneButton.cs
+++ b/Content.Client/UserInterface/Controls/MonotoneButton.cs
@@ -7,7 +7,7 @@ namespace Content.Client.UserInterface.Controls;
///
/// A button intended for use with a monotone color palette
///
-public sealed class MonotoneButton : ContainerButton
+public sealed class MonotoneButton : Button
{
///
/// Specifies the color of the label text when the button is pressed.
@@ -15,43 +15,9 @@ public sealed class MonotoneButton : ContainerButton
[ViewVariables]
public Color AltTextColor { set; get; } = new Color(0.2f, 0.2f, 0.2f);
- ///
- /// The label that holds the button text.
- ///
- public Label Label { get; }
-
- ///
- /// The text displayed by the button.
- ///
- [PublicAPI, ViewVariables]
- public string? Text { get => Label.Text; set => Label.Text = value; }
-
- ///
- /// How to align the text inside the button.
- ///
- [PublicAPI, ViewVariables]
- public AlignMode TextAlign { get => Label.Align; set => Label.Align = value; }
-
- ///
- /// If true, the button will allow shrinking and clip text
- /// to prevent the text from going outside the bounds of the button.
- /// If false, the minimum size will always fit the contained text.
- ///
- [PublicAPI, ViewVariables]
- public bool ClipText
- {
- get => Label.ClipText;
- set => Label.ClipText = value;
- }
-
public MonotoneButton()
{
- Label = new Label
- {
- StyleClasses = { StyleClassButton }
- };
-
- AddChild(Label);
+ RemoveStyleClass("button");
UpdateAppearance();
}
diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
index 8d512c23ab..dc62dbe897 100644
--- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
+++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
@@ -12,6 +12,7 @@ using Content.Client.UserInterface.Systems.Actions.Widgets;
using Content.Client.UserInterface.Systems.Actions.Windows;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Input;
using Robust.Client.GameObjects;
@@ -162,142 +163,33 @@ public sealed class ActionUIController : UIController, IOnStateChanged(user, out var comp))
return false;
- if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
- baseAction is not BaseTargetActionComponent action)
+ if (_actionsSystem.GetAction(actionId) is not {} action ||
+ !EntityManager.TryGetComponent(action, out var target))
{
return false;
}
// Is the action currently valid?
- if (!action.Enabled
- || action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
+ if (!_actionsSystem.ValidAction(action))
{
// The user is targeting with this action, but it is not valid. Maybe mark this click as
// handled and prevent further interactions.
- return !action.InteractOnMiss;
+ return !target.InteractOnMiss;
}
- switch (action)
+ var ev = new ActionTargetAttemptEvent(args, (user, comp), action);
+ EntityManager.EventBus.RaiseLocalEvent(action, ref ev);
+ if (!ev.Handled)
{
- case WorldTargetActionComponent mapTarget:
- return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
-
- case EntityTargetActionComponent entTarget:
- return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
-
- case EntityWorldTargetActionComponent entMapTarget:
- return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.InteractOnMiss;
-
- default:
- Logger.Error($"Unknown targeting action: {actionId.GetType()}");
- return false;
- }
- }
-
- private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, WorldTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
- {
- if (_actionsSystem == null)
- return false;
-
- var coords = args.Coordinates;
-
- if (!_actionsSystem.ValidateWorldTarget(user, coords, (actionId, action)))
- {
- // Invalid target.
- if (action.DeselectOnMiss)
- StopTargeting();
-
+ Log.Error($"Action {EntityManager.ToPrettyString(actionId)} did not handle ActionTargetAttemptEvent!");
return false;
}
- if (action.ClientExclusive)
- {
- if (action.Event != null)
- {
- action.Event.Target = coords;
- }
-
- _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
- }
- else
- EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetCoordinates(coords)));
-
- if (!action.Repeat)
- StopTargeting();
-
- return true;
- }
-
- private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, EntityTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
- {
- if (_actionsSystem == null)
- return false;
-
- var entity = args.EntityUid;
-
- if (!_actionsSystem.ValidateEntityTarget(user, entity, (actionId, action)))
- {
- if (action.DeselectOnMiss)
- StopTargeting();
-
- return false;
- }
-
- if (action.ClientExclusive)
- {
- if (action.Event != null)
- {
- action.Event.Target = entity;
- }
-
- _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
- }
- else
- EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid)));
-
- if (!action.Repeat)
- StopTargeting();
-
- return true;
- }
-
- private bool TryTargetEntityWorld(in PointerInputCmdArgs args,
- EntityUid actionId,
- EntityWorldTargetActionComponent action,
- EntityUid user,
- ActionsComponent actionComp)
- {
- if (_actionsSystem == null)
- return false;
-
- var entity = args.EntityUid;
- var coords = args.Coordinates;
-
- if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action)))
- {
- if (action.DeselectOnMiss)
- StopTargeting();
-
- return false;
- }
-
- if (action.ClientExclusive)
- {
- if (action.Event != null)
- {
- action.Event.Entity = entity;
- action.Event.Coords = coords;
- }
-
- _actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
- }
- else
- EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords)));
-
- if (!action.Repeat)
+ // stop targeting when needed
+ if (ev.FoundTarget ? !target.Repeat : target.DeselectOnMiss)
StopTargeting();
return true;
@@ -305,36 +197,26 @@ public sealed class ActionUIController : UIController, IOnStateChanged(action, out var target))
+ ToggleTargeting((action, action, target));
else
- _actionsSystem?.TriggerAction(actionId.Value, baseAction);
+ _actionsSystem?.TriggerAction(action);
}
private void OnActionAdded(EntityUid actionId)
{
- if (_actionsSystem == null ||
- !_actionsSystem.TryGetActionData(actionId, out var action))
- {
+ if (_actionsSystem?.GetAction(actionId) is not {} action)
return;
- }
+ // TODO: event
// if the action is toggled when we add it, start targeting
- if (action is BaseTargetActionComponent targetAction && action.Toggled)
- StartTargeting(actionId, targetAction);
+ if (action.Comp.Toggled && EntityManager.TryGetComponent(actionId, out var target))
+ StartTargeting((action, action, target));
- if (_actions.Contains(actionId))
+ if (_actions.Contains(action))
return;
- _actions.Add(actionId);
+ _actions.Add(action);
}
private void OnActionRemoved(EntityUid actionId)
@@ -437,15 +317,16 @@ public sealed class ActionUIController : UIController, IOnStateChanged ent, Filters filter)
{
+ var (uid, comp) = ent;
return filter switch
{
- Filters.Enabled => action.Enabled,
- Filters.Item => action.Container != null && action.Container != _playerManager.LocalEntity,
- Filters.Innate => action.Container == null || action.Container == _playerManager.LocalEntity,
- Filters.Instant => action is InstantActionComponent,
- Filters.Targeted => action is BaseTargetActionComponent,
+ Filters.Enabled => comp.Enabled,
+ Filters.Item => comp.Container != null && comp.Container != _playerManager.LocalEntity,
+ Filters.Innate => comp.Container == null || comp.Container == _playerManager.LocalEntity,
+ Filters.Instant => EntityManager.HasComponent(uid),
+ Filters.Targeted => EntityManager.HasComponent(uid),
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
};
}
@@ -456,7 +337,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged actions)
+ private void PopulateActions(IEnumerable> actions)
{
if (_window is not { Disposed: false, IsOpen: true })
return;
@@ -478,7 +359,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged
{
- if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action.Comp, filter)))
+ if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action, filter)))
return false;
if (action.Comp.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
return true;
- var name = EntityManager.GetComponent(action.Id).EntityName;
+ var name = EntityManager.GetComponent(action).EntityName;
if (name.Contains(search, StringComparison.OrdinalIgnoreCase))
return true;
@@ -581,7 +462,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged(action, out var target))
{
- _actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
+ _actionsSystem?.TriggerAction(action);
return;
}
@@ -714,7 +593,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged
- private void ToggleTargeting(EntityUid actionId, BaseTargetActionComponent action)
+ private void ToggleTargeting(Entity ent)
{
- if (SelectingTargetFor == actionId)
+ if (SelectingTargetFor == ent)
{
StopTargeting();
return;
}
- StartTargeting(actionId, action);
+ StartTargeting(ent);
}
///
/// Puts us in targeting mode, where we need to pick either a target point or entity
///
- private void StartTargeting(EntityUid actionId, BaseTargetActionComponent action)
+ private void StartTargeting(Entity ent)
{
+ var (uid, action, target) = ent;
+
// If we were targeting something else we should stop
StopTargeting();
- SelectingTargetFor = actionId;
+ SelectingTargetFor = uid;
// TODO inform the server
- action.Toggled = true;
+ _actionsSystem?.SetToggled(uid, true);
// override "held-item" overlay
var provider = action.Container;
- if (action.TargetingIndicator && _overlays.TryGetOverlay(out var handOverlay))
+ if (target.TargetingIndicator && _overlays.TryGetOverlay(out var handOverlay))
{
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Container != null)
{
@@ -940,7 +821,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged(uid, out var entity))
return;
Func? predicate = null;
- var attachedEnt = entityAction.AttachedEntity;
+ var attachedEnt = action.AttachedEntity;
- if (!entityAction.CanTargetSelf)
+ if (!entity.CanTargetSelf)
predicate = e => e != attachedEnt;
- var range = entityAction.CheckCanAccess ? action.Range : -1;
+ var range = target.CheckCanAccess ? target.Range : -1;
_interactionOutline?.SetEnabled(false);
- _targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, entityAction.Blacklist, null);
+ _targetOutline?.Enable(range, target.CheckCanAccess, predicate, entity.Whitelist, entity.Blacklist, null);
}
///
@@ -974,11 +855,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged? Action { get; private set; }
public bool Locked { get; set; }
public event Action? ActionPressed;
@@ -193,7 +193,7 @@ public sealed class ActionButton : Control, IEntityControl
private Control? SupplyTooltip(Control sender)
{
- if (!_entities.TryGetComponent(ActionId, out MetaDataComponent? metadata))
+ if (!_entities.TryGetComponent(Action, out MetaDataComponent? metadata))
return null;
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
@@ -201,14 +201,14 @@ public sealed class ActionButton : Control, IEntityControl
FormattedMessage? chargesText = null;
// 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(ActionId, out LimitedChargesComponent? actionCharges))
+ if (_entities.TryGetComponent(Action, out LimitedChargesComponent? actionCharges))
{
- var charges = _sharedChargesSys.GetCurrentCharges((ActionId.Value, actionCharges, null));
+ var charges = _sharedChargesSys.GetCurrentCharges((Action.Value, actionCharges, null));
chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
- if (_entities.TryGetComponent(ActionId, out AutoRechargeComponent? autoRecharge))
+ if (_entities.TryGetComponent(Action, out AutoRechargeComponent? autoRecharge))
{
- var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((ActionId.Value, actionCharges, autoRecharge));
+ var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((Action.Value, actionCharges, autoRecharge));
chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
}
}
@@ -223,7 +223,7 @@ public sealed class ActionButton : Control, IEntityControl
private void UpdateItemIcon()
{
- if (_action is not {EntityIcon: { } entity} ||
+ if (Action?.Comp is not {EntityIcon: { } entity} ||
!_entities.HasComponent(entity))
{
_bigItemSpriteView.Visible = false;
@@ -233,7 +233,7 @@ public sealed class ActionButton : Control, IEntityControl
}
else
{
- switch (_action.ItemIconStyle)
+ switch (Action?.Comp.ItemIconStyle)
{
case ItemActionIconStyle.BigItem:
_bigItemSpriteView.Visible = true;
@@ -259,17 +259,17 @@ public sealed class ActionButton : Control, IEntityControl
private void SetActionIcon(Texture? texture)
{
- if (_action == null || texture == null)
+ if (Action?.Comp is not {} action || texture == null)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
- else if (_action.EntityIcon != null && _action.ItemIconStyle == ItemActionIconStyle.BigItem)
+ else if (action.EntityIcon != null && action.ItemIconStyle == ItemActionIconStyle.BigItem)
{
_smallActionIcon.Texture = texture;
- _smallActionIcon.Modulate = _action.IconColor;
+ _smallActionIcon.Modulate = action.IconColor;
_smallActionIcon.Visible = true;
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
@@ -277,7 +277,7 @@ public sealed class ActionButton : Control, IEntityControl
else
{
_bigActionIcon.Texture = texture;
- _bigActionIcon.Modulate = _action.IconColor;
+ _bigActionIcon.Modulate = action.IconColor;
_bigActionIcon.Visible = true;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
@@ -289,7 +289,7 @@ public sealed class ActionButton : Control, IEntityControl
UpdateItemIcon();
UpdateBackground();
- if (_action == null)
+ if (Action is not {} action)
{
SetActionIcon(null);
return;
@@ -297,29 +297,27 @@ public sealed class ActionButton : Control, IEntityControl
_controller ??= UserInterfaceManager.GetUIController();
_spriteSys ??= _entities.System();
- if ((_controller.SelectingTargetFor == ActionId || _action.Toggled))
+ var icon = action.Comp.Icon;
+ if (_controller.SelectingTargetFor == action || action.Comp.Toggled)
{
- if (_action.IconOn != null)
- SetActionIcon(_spriteSys.Frame0(_action.IconOn));
- else if (_action.Icon != null)
- SetActionIcon(_spriteSys.Frame0(_action.Icon));
- else
- SetActionIcon(null);
+ if (action.Comp.IconOn is {} iconOn)
+ icon = iconOn;
- if (_action.BackgroundOn != null)
- _buttonBackgroundTexture = _spriteSys.Frame0(_action.BackgroundOn);
+ if (action.Comp.BackgroundOn is {} background)
+ _buttonBackgroundTexture = _spriteSys.Frame0(background);
}
else
{
- SetActionIcon(_action.Icon != null ? _spriteSys.Frame0(_action.Icon) : null);
_buttonBackgroundTexture = Theme.ResolveTexture("SlotBackground");
}
+
+ SetActionIcon(icon != null ? _spriteSys.Frame0(icon) : null);
}
public void UpdateBackground()
{
_controller ??= UserInterfaceManager.GetUIController();
- if (_action != null ||
+ if (Action != null ||
_controller.IsDragging && GetPositionInParent() == Parent?.ChildCount - 1)
{
Button.Texture = _buttonBackgroundTexture;
@@ -333,9 +331,7 @@ public sealed class ActionButton : Control, IEntityControl
public bool TryReplaceWith(EntityUid actionId, ActionsSystem system)
{
if (Locked)
- {
return false;
- }
UpdateData(actionId, system);
return true;
@@ -343,16 +339,15 @@ public sealed class ActionButton : Control, IEntityControl
public void UpdateData(EntityUid? actionId, ActionsSystem system)
{
- ActionId = actionId;
- system.TryGetActionData(actionId, out _action);
- Label.Visible = actionId != null;
+ Action = system.GetAction(actionId);
+
+ Label.Visible = Action != null;
UpdateIcons();
}
public void ClearData()
{
- ActionId = null;
- _action = null;
+ Action = null;
Cooldown.Visible = false;
Cooldown.Progress = 1;
Label.Visible = false;
@@ -365,19 +360,15 @@ public sealed class ActionButton : Control, IEntityControl
UpdateBackground();
- Cooldown.Visible = _action != null && _action.Cooldown != null;
- if (_action == null)
+ Cooldown.Visible = Action?.Comp.Cooldown != null;
+ if (Action?.Comp is not {} action)
return;
- if (_action.Cooldown != null)
- {
- Cooldown.FromTime(_action.Cooldown.Value.Start, _action.Cooldown.Value.End);
- }
+ if (action.Cooldown is {} cooldown)
+ Cooldown.FromTime(cooldown.Start, cooldown.End);
- if (ActionId != null && _toggled != _action.Toggled)
- {
- _toggled = _action.Toggled;
- }
+ if (_toggled != action.Toggled)
+ _toggled = action.Toggled;
}
protected override void MouseEntered()
@@ -404,7 +395,7 @@ public sealed class ActionButton : Control, IEntityControl
public void Depress(GUIBoundKeyEventArgs args, bool depress)
{
// action can still be toggled if it's allowed to stay selected
- if (_action is not {Enabled: true})
+ if (Action?.Comp is not {Enabled: true})
return;
_depressed = depress;
@@ -414,17 +405,17 @@ public sealed class ActionButton : Control, IEntityControl
public void DrawModeChanged()
{
_controller ??= UserInterfaceManager.GetUIController();
- HighlightRect.Visible = _beingHovered && (_action != null || _controller.IsDragging);
+ HighlightRect.Visible = _beingHovered && (Action != null || _controller.IsDragging);
// always show the normal empty button style if no action in this slot
- if (_action == null)
+ if (Action?.Comp is not {} action)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
// show a hover only if the action is usable or another action is being dragged on top of this
- if (_beingHovered && (_controller.IsDragging || _action!.Enabled))
+ if (_beingHovered && (_controller.IsDragging || action.Enabled))
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
}
@@ -439,16 +430,16 @@ public sealed class ActionButton : Control, IEntityControl
}
// if it's toggled on, always show the toggled on style (currently same as depressed style)
- if (_action.Toggled || _controller.SelectingTargetFor == ActionId)
+ if (action.Toggled || _controller.SelectingTargetFor == Action?.Owner)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
- SetOnlyStylePseudoClass(_action.IconOn != null
+ SetOnlyStylePseudoClass(action.IconOn != null
? ContainerButton.StylePseudoClassNormal
: ContainerButton.StylePseudoClassPressed);
return;
}
- if (!_action.Enabled)
+ if (!action.Enabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;
@@ -457,5 +448,5 @@ public sealed class ActionButton : Control, IEntityControl
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
}
- EntityUid? IEntityControl.UiEntity => ActionId;
+ EntityUid? IEntityControl.UiEntity => Action;
}
diff --git a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
index ebfec38275..1b22f9460a 100644
--- a/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
+++ b/Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
@@ -48,6 +48,8 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged "c.s.go.es.bwoink";
+
public override void Initialize()
{
base.Initialize();
@@ -129,7 +131,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged, IOnStateExited, IOnSystemChanged
{
[Dependency] private readonly IEntityManager _ent = default!;
- [Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
[UISystemDependency] private readonly SpriteSystem _sprite = default!;
- private ISawmill _sawmill = default!;
-
public override void Initialize()
{
base.Initialize();
- _sawmill = _logMan.GetSawmill("character");
-
SubscribeNetworkEvent(OnRoleTypeChanged);
}
@@ -222,7 +217,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered
+/// A partial class of ChatUIController that handles the saving and loading of highlights for the chatbox.
+/// It also makes use of the CharacterInfoSystem to optionally generate highlights based on the character's info.
+///
+public sealed partial class ChatUIController : IOnSystemChanged
+{
+ [UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
+
+ ///
+ /// The list of words to be highlighted in the chatbox.
+ ///
+ private List _highlights = new();
+
+ ///
+ /// The string holding the hex color used to highlight words.
+ ///
+ private string? _highlightsColor;
+
+ private bool _autoFillHighlightsEnabled;
+
+ ///
+ /// The boolean that keeps track of the 'OnCharacterUpdated' event, whenever it's a player attaching or opening the character info panel.
+ ///
+ private bool _charInfoIsAttach = false;
+
+ public event Action? HighlightsUpdated;
+
+ private void InitializeHighlights()
+ {
+ _config.OnValueChanged(CCVars.ChatAutoFillHighlights, (value) => { _autoFillHighlightsEnabled = value; }, true);
+
+ _config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
+
+ // Load highlights if any were saved.
+ string highlights = _config.GetCVar(CCVars.ChatHighlights);
+
+ if (!string.IsNullOrEmpty(highlights))
+ {
+ UpdateHighlights(highlights, true);
+ }
+ }
+
+ public void OnSystemLoaded(CharacterInfoSystem system)
+ {
+ system.OnCharacterUpdate += OnCharacterUpdated;
+ }
+
+ public void OnSystemUnloaded(CharacterInfoSystem system)
+ {
+ system.OnCharacterUpdate -= OnCharacterUpdated;
+ }
+
+ private void UpdateAutoFillHighlights()
+ {
+ if (!_autoFillHighlightsEnabled)
+ return;
+
+ // If auto highlights are enabled generate a request for new character info
+ // that will be used to determine the highlights.
+ _charInfoIsAttach = true;
+ _characterInfo.RequestCharacterInfo();
+ }
+
+ public void UpdateHighlights(string newHighlights, bool firstLoad = false)
+ {
+ // Do nothing if the provided highlights are the same as the old ones and it is not the first time.
+ if (!firstLoad && _config.GetCVar(CCVars.ChatHighlights).Equals(newHighlights, StringComparison.CurrentCultureIgnoreCase))
+ return;
+
+ _config.SetCVar(CCVars.ChatHighlights, newHighlights);
+ _config.SaveToFile();
+
+ _highlights.Clear();
+
+ // We first subdivide the highlights based on newlines to prevent replacing
+ // a valid "\n" tag and adding it to the final regex.
+ string[] splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ for (int i = 0; i < splittedHighlights.Length; i++)
+ {
+ // Replace every "\" character with a "\\" to prevent "\n", "\0", etc...
+ string keyword = splittedHighlights[i].Replace(@"\", @"\\");
+
+ // Escape the keyword to prevent special characters like "(" and ")" to be considered valid regex.
+ keyword = Regex.Escape(keyword);
+
+ // 1. Since the "["s in WrappedMessage are already sanitized, add 2 extra "\"s
+ // to make sure it matches the literal "\" before the square bracket.
+ keyword = keyword.Replace(@"\[", @"\\\[");
+
+ // If present, replace the double quotes at the edges with tags
+ // that make sure the words to match are separated by spaces or punctuation.
+ // NOTE: The reason why we don't use \b tags is that \b doesn't match reverse slash characters "\" so
+ // a pre-sanitized (see 1.) string like "\[test]" wouldn't get picked up by the \b.
+ if (keyword.Count(c => (c == '"')) > 0)
+ {
+ // Matches the last double quote character.
+ keyword = Regex.Replace(keyword, "\"$", "(?!\\w)");
+ // When matching for the first double quote character we also consider the possibility
+ // of the double quote being preceded by a @ character.
+ keyword = Regex.Replace(keyword, "^\"|(?<=^@)\"", "(? y.Length.CompareTo(x.Length));
+ }
+
+ private void OnCharacterUpdated(CharacterData data)
+ {
+ // If _charInfoIsAttach is false then the opening of the character panel was the one
+ // to generate the event, dismiss it.
+ if (!_charInfoIsAttach)
+ return;
+
+ var (_, job, _, _, entityName) = data;
+
+ // Mark this entity's name as our character name for the "UpdateHighlights" function.
+ string newHighlights = "@" + entityName;
+
+ // Subdivide the character's name based on spaces or hyphens so that every word gets highlighted.
+ if (newHighlights.Count(c => (c == ' ' || c == '-')) == 1)
+ newHighlights = newHighlights.Replace("-", "\n@").Replace(" ", "\n@");
+
+ // If the character has a name with more than one hyphen assume it is a lizard name and extract the first and
+ // last name eg. "Eats-The-Food" -> "@Eats" "@Food"
+ if (newHighlights.Count(c => c == '-') > 1)
+ newHighlights = newHighlights.Split('-')[0] + "\n@" + newHighlights.Split('-')[^1];
+
+ // Convert the job title to kebab-case and use it as a key for the loc file.
+ string jobKey = job.Replace(' ', '-').ToLower();
+
+ if (Loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
+ newHighlights += '\n' + jobMatches.Replace(", ", "\n");
+
+ UpdateHighlights(newHighlights);
+ HighlightsUpdated?.Invoke(newHighlights);
+ _charInfoIsAttach = false;
+ }
+}
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
index 1ccba1ae20..13c6ff4a2a 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
@@ -41,9 +41,10 @@ using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+
namespace Content.Client.UserInterface.Systems.Chat;
-public sealed class ChatUIController : UIController
+public sealed partial class ChatUIController : UIController
{
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IChatManager _manager = default!;
@@ -240,6 +241,7 @@ public sealed class ChatUIController : UIController
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
+ InitializeHighlights();
}
public void OnScreenLoad()
@@ -426,6 +428,8 @@ public sealed class ChatUIController : UIController
private void OnAttachedChanged(EntityUid uid)
{
UpdateChannelPermissions();
+
+ UpdateAutoFillHighlights();
}
private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
@@ -825,6 +829,12 @@ public sealed class ChatUIController : UIController
msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name")));
}
+ // Color any words chosen by the client.
+ foreach (var highlight in _highlights)
+ {
+ msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, highlight, "color", _highlightsColor);
+ }
+
// Color any codewords for minds that have roles that use them
if (_player.LocalUser != null && _mindSystem != null && _roleCodewordSystem != null)
{
diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml
index 459c44eee2..2e4a057a05 100644
--- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml
+++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml
@@ -1,10 +1,22 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
index 33a82db335..100607d36e 100644
--- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
@@ -1,4 +1,7 @@
using Content.Shared.Chat;
+using Content.Shared.CCVar;
+using Robust.Shared.Utility;
+using Robust.Shared.Configuration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -29,10 +32,24 @@ public sealed partial class ChannelFilterPopup : Popup
private readonly Dictionary _filterStates = new();
public event Action? OnChannelFilter;
+ public event Action? OnNewHighlights;
public ChannelFilterPopup()
{
RobustXamlLoader.Load(this);
+
+ HighlightButton.OnPressed += HighlightsEntered;
+ // Add a placeholder text to the highlights TextEdit.
+ HighlightEdit.Placeholder = new Rope.Leaf(Loc.GetString("hud-chatbox-highlights-placeholder"));
+
+ // Load highlights if any were saved.
+ var cfg = IoCManager.Resolve();
+ string highlights = cfg.GetCVar(CCVars.ChatHighlights);
+
+ if (!string.IsNullOrEmpty(highlights))
+ {
+ UpdateHighlights(highlights);
+ }
}
public bool IsActive(ChatChannel channel)
@@ -92,12 +109,22 @@ public sealed partial class ChannelFilterPopup : Popup
}
}
+ public void UpdateHighlights(string highlights)
+ {
+ HighlightEdit.TextRope = new Rope.Leaf(highlights);
+ }
+
private void CheckboxPressed(ButtonEventArgs args)
{
var checkbox = (ChannelFilterCheckbox) args.Button;
OnChannelFilter?.Invoke(checkbox.Channel, checkbox.Pressed);
}
+ private void HighlightsEntered(ButtonEventArgs _args)
+ {
+ OnNewHighlights?.Invoke(Rope.Collapse(HighlightEdit.TextRope));
+ }
+
public void UpdateUnread(ChatChannel channel, int? unread)
{
if (_filterStates.TryGetValue(channel, out var checkbox))
diff --git a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
index 068cf4e87e..289c9fa164 100644
--- a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
@@ -38,9 +38,10 @@ public partial class ChatBox : UIWidget
ChatInput.Input.OnFocusExit += OnFocusExit;
ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect;
ChatInput.FilterButton.Popup.OnChannelFilter += OnChannelFilter;
-
+ ChatInput.FilterButton.Popup.OnNewHighlights += OnNewHighlights;
_controller = UserInterfaceManager.GetUIController();
_controller.MessageAdded += OnMessageAdded;
+ _controller.HighlightsUpdated += OnHighlightsUpdated;
_controller.RegisterChat(this);
}
@@ -67,6 +68,11 @@ public partial class ChatBox : UIWidget
AddLine(msg.WrappedMessage, color);
}
+ private void OnHighlightsUpdated(string highlights)
+ {
+ ChatInput.FilterButton.Popup.UpdateHighlights(highlights);
+ }
+
private void OnChannelSelect(ChatSelectChannel channel)
{
_controller.UpdateSelectedChannel(this);
@@ -97,6 +103,11 @@ public partial class ChatBox : UIWidget
}
}
+ private void OnNewHighlights(string highlighs)
+ {
+ _controller.UpdateHighlights(highlighs);
+ }
+
public void AddLine(string message, Color color)
{
var formatted = new FormattedMessage(3);
diff --git a/Content.Client/UserInterface/Systems/Guidebook/GuidebookUIController.cs b/Content.Client/UserInterface/Systems/Guidebook/GuidebookUIController.cs
index 702b0a52ac..95653d2ffa 100644
--- a/Content.Client/UserInterface/Systems/Guidebook/GuidebookUIController.cs
+++ b/Content.Client/UserInterface/Systems/Guidebook/GuidebookUIController.cs
@@ -227,7 +227,7 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered]
private const string DefaultRuleset = "DefaultRuleset";
public ProtoId RulesEntryId = DefaultRuleset;
+ protected override string SawmillName => "rules";
+
public override void Initialize()
{
base.Initialize();
- _sawmill = _logMan.GetSawmill("rules");
_netManager.RegisterNetMessage();
_netManager.RegisterNetMessage(OnRulesInformationMessage);
@@ -94,7 +93,7 @@ public sealed class InfoUIController : UIController, IOnStateExited(DefaultRuleset);
- _sawmill.Error($"Couldn't find the following prototype: {RulesEntryId}. Falling back to {DefaultRuleset}, please check that the server has the rules set up correctly");
+ Log.Error($"Couldn't find the following prototype: {RulesEntryId}. Falling back to {DefaultRuleset}, please check that the server has the rules set up correctly");
return guideEntryPrototype;
}
diff --git a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
index 1b872484ae..e66ce2ae6c 100644
--- a/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
+++ b/Content.Client/UserInterface/Systems/Inventory/InventoryUIController.cs
@@ -243,7 +243,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, WiresVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
- var layer = _sprite.LayerMapReserve((uid, args.Sprite), WiresVisualLayers.MaintenancePanel);
+ var layer = SpriteSystem.LayerMapReserve((uid, args.Sprite), WiresVisualLayers.MaintenancePanel);
if (args.AppearanceData.TryGetValue(WiresVisuals.MaintenancePanelState, out var panelStateObject) &&
panelStateObject is bool panelState)
{
- _sprite.LayerSetVisible((uid, args.Sprite), layer, panelState);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, panelState);
}
else
{
//Mainly for spawn window
- _sprite.LayerSetVisible((uid, args.Sprite), layer, false);
+ SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, false);
}
}
}
diff --git a/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs b/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
index 030d1a364b..5e60086987 100644
--- a/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
+++ b/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
@@ -5,8 +5,6 @@ namespace Content.Client.Xenoarchaeology.XenoArtifacts;
public sealed class RandomArtifactSpriteSystem : VisualizerSystem
{
- [Dependency] private readonly SpriteSystem _sprite = default!;
-
protected override void OnAppearanceChange(EntityUid uid, RandomArtifactSpriteComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -25,24 +23,24 @@ public sealed class RandomArtifactSpriteSystem : VisualizerSystem();
+ var cQuery = cEntMan.GetEntityQuery();
var sActions = sActionSystem.GetActions(serverEnt).Where(
- x => x.Comp is InstantActionComponent act && act.Event?.GetType() == evType).ToArray();
+ ent => sQuery.CompOrNull(ent)?.Event?.GetType() == evType).ToArray();
var cActions = cActionSystem.GetActions(clientEnt).Where(
- x => x.Comp is InstantActionComponent act && act.Event?.GetType() == evType).ToArray();
+ ent => cQuery.CompOrNull(ent)?.Event?.GetType() == evType).ToArray();
Assert.That(sActions.Length, Is.EqualTo(1));
Assert.That(cActions.Length, Is.EqualTo(1));
- var sAct = sActions[0].Comp;
- var cAct = cActions[0].Comp;
+ var sAct = sActions[0];
+ var cAct = cActions[0];
- Assert.That(sAct, Is.Not.Null);
- Assert.That(cAct, Is.Not.Null);
+ Assert.That(sAct.Comp, Is.Not.Null);
+ Assert.That(cAct.Comp, Is.Not.Null);
// Finally, these two actions are not the same object
// required, because integration tests do not respect the [NonSerialized] attribute and will simply events by reference.
- Assert.That(ReferenceEquals(sAct, cAct), Is.False);
- Assert.That(ReferenceEquals(sAct.BaseEvent, cAct.BaseEvent), Is.False);
+ Assert.That(ReferenceEquals(sAct.Comp, cAct.Comp), Is.False);
+ Assert.That(ReferenceEquals(sQuery.GetComponent(sAct).Event, cQuery.GetComponent(cAct).Event), Is.False);
await pair.CleanReturnAsync();
}
diff --git a/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs b/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs
index cff2b487ab..6f15a6c916 100644
--- a/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs
+++ b/Content.IntegrationTests/Tests/Atmos/ConstantsTest.cs
@@ -1,32 +1,53 @@
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
-using Robust.Shared.GameObjects;
+using Content.Shared.Atmos.Prototypes;
-namespace Content.IntegrationTests.Tests.Atmos
-{
- [TestFixture]
- [TestOf(typeof(Atmospherics))]
- public sealed class ConstantsTest
- {/*
- [Test]
- public async Task TotalGasesTest()
+namespace Content.IntegrationTests.Tests.Atmos;
+
+[TestOf(typeof(Atmospherics))]
+public sealed class ConstantsTest
+{/*
+ [Test]
+ public async Task TotalGasesTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+ var entityManager = server.EntMan;
+ var protoManager = server.ProtoMan;
+
+ await server.WaitPost(() =>
{
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
- var entityManager = server.ResolveDependency();
+ var atmosSystem = entityManager.System();
- await server.WaitPost(() =>
+ Assert.Multiple(() =>
{
- var atmosSystem = entityManager.System();
+ // adding new gases needs a few changes in the code, so make sure this is done everywhere
+ var gasProtos = protoManager.EnumeratePrototypes().ToList();
- Assert.Multiple(() =>
+ // number of gas prototypes
+ Assert.That(gasProtos, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
+ $"Number of GasPrototypes is not equal to TotalNumberOfGases.");
+ // number of gas prototypes used in the atmos system
+ Assert.That(atmosSystem.Gases.Count(), Is.EqualTo(Atmospherics.TotalNumberOfGases),
+ $"AtmosSystem.Gases is not equal to TotalNumberOfGases.");
+ // enum mapping gases to their Id
+ Assert.That(Enum.GetValues(), Has.Length.EqualTo(Atmospherics.TotalNumberOfGases),
+ $"Gas enum size is not equal to TotalNumberOfGases.");
+ // localized abbreviations for UI purposes
+ Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
+ $"GasAbbreviations size is not equal to TotalNumberOfGases.");
+
+ // the ID for each gas has to be a number from 0 to TotalNumberOfGases-1
+ foreach (var gas in gasProtos)
{
- Assert.That(atmosSystem.Gases.Count(), Is.EqualTo(Atmospherics.TotalNumberOfGases));
- Assert.That(Enum.GetValues(typeof(Gas)), Has.Length.EqualTo(Atmospherics.TotalNumberOfGases));
- });
+ var validInteger = int.TryParse(gas.ID, out var number);
+ Assert.That(validInteger, Is.True, $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
+ Assert.That(number, Is.InRange(0, Atmospherics.TotalNumberOfGases - 1), $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
+ }
});
- await pair.CleanReturnAsync();
- }*/
- }
+ });
+ await pair.CleanReturnAsync();
+ }*/
}
+
diff --git a/Content.IntegrationTests/Tests/Chameleon/ChameleonJobLoadoutTest.cs b/Content.IntegrationTests/Tests/Chameleon/ChameleonJobLoadoutTest.cs
new file mode 100644
index 0000000000..da061f052a
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Chameleon/ChameleonJobLoadoutTest.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Text;
+using Content.Client.Implants;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Clothing;
+using Content.Shared.Implants;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Chameleon;
+
+///
+/// Ensures all round "round start jobs" have an associated chameleon loadout.
+///
+public sealed class ChameleonJobLoadoutTest : InteractionTest
+{
+ private readonly List> JobBlacklist =
+ [
+
+ ];
+
+ [Test]
+ public async Task CheckAllJobs()
+ {
+ var alljobs = ProtoMan.EnumeratePrototypes();
+
+ // Job -> number of references
+ Dictionary, int> validJobs = new();
+
+ // Only add stuff that actually has clothing! We don't want stuff like AI or borgs.
+ foreach (var job in alljobs)
+ {
+ if (!IsProbablyRoundStartJob(job) || JobBlacklist.Contains(job.ID))
+ continue;
+
+ validJobs.Add(job.ID, 0);
+ }
+
+ var chameleons = ProtoMan.EnumeratePrototypes();
+
+ foreach (var chameleon in chameleons)
+ {
+ if (chameleon.Job == null || !validJobs.ContainsKey(chameleon.Job.Value))
+ continue;
+
+ validJobs[chameleon.Job.Value] += 1;
+ }
+
+ var errorMessage = new StringBuilder();
+ errorMessage.AppendLine("The following job(s) have no chameleon prototype(s):");
+ var invalid = false;
+
+ // All round start jobs have a chameleon loadout
+ foreach (var job in validJobs)
+ {
+ if (job.Value != 0)
+ continue;
+
+ errorMessage.AppendLine(job.Key + " has no chameleonOutfit prototype.");
+ invalid = true;
+ }
+
+ if (!invalid)
+ return;
+
+ Assert.Fail(errorMessage.ToString());
+ }
+
+ ///
+ /// Best guess at what a "round start" job is.
+ ///
+ private bool IsProbablyRoundStartJob(JobPrototype job)
+ {
+ return job.StartingGear != null && ProtoMan.HasIndex(LoadoutSystem.GetJobPrototype(job.ID));
+ }
+
+}
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
index 06874f39ed..491bf6b510 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
@@ -18,7 +18,7 @@ public sealed class MachineConstruction : InteractionTest
ClientAssertPrototype(Unfinished, Target);
await Interact(Wrench, Cable);
AssertPrototype(MachineFrame);
- await Interact(ProtolatheBoard, Bin1, Bin1, Manipulator1, Manipulator1, Beaker, Beaker, Screw);
+ await Interact(ProtolatheBoard, Manipulator1, Manipulator1, Manipulator1, Manipulator1, Beaker, Beaker, Screw);
AssertPrototype(Protolathe);
}
@@ -36,8 +36,7 @@ public sealed class MachineConstruction : InteractionTest
(Steel, 5),
(Cable, 1),
(Beaker, 2),
- (Manipulator1, 2),
- (Bin1, 2),
+ (Manipulator1, 4),
(ProtolatheBoard, 1));
}
@@ -52,7 +51,7 @@ public sealed class MachineConstruction : InteractionTest
// Change it into an autolathe
await InteractUsing("AutolatheMachineCircuitboard");
AssertPrototype(MachineFrame);
- await Interact(Bin1, Bin1, Bin1, Manipulator1, Glass, Screw);
+ await Interact(Manipulator1, Manipulator1, Manipulator1, Manipulator1, Glass, Screw);
AssertPrototype("Autolathe");
}
}
diff --git a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs
index 87ef41fe96..bf47768274 100644
--- a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs
+++ b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs
@@ -33,6 +33,7 @@ public sealed class AbsorbentTest
id: {AbsorbentDummyId}
components:
- type: Absorbent
+ useAbsorberSolution: true
- type: SolutionContainerManager
solutions:
absorbed:
@@ -94,7 +95,7 @@ public sealed class AbsorbentTest
refillable = entityManager.SpawnEntity(RefillableDummyId, coordinates);
entityManager.TryGetComponent(absorbent, out component);
- solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSoln, out var absorbentSolution);
+ solutionContainerSystem.TryGetSolution(absorbent, component.SolutionName, out var absorbentSoln, out var absorbentSolution);
solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSoln, out var refillableSolution);
// Arrange
@@ -152,7 +153,7 @@ public sealed class AbsorbentTest
refillable = entityManager.SpawnEntity(SmallRefillableDummyId, coordinates);
entityManager.TryGetComponent(absorbent, out component);
- solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSoln, out var absorbentSolution);
+ solutionContainerSystem.TryGetSolution(absorbent, component.SolutionName, out var absorbentSoln, out var absorbentSolution);
solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSoln, out var refillableSolution);
// Arrange
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index aa58b30bf3..5db5d91d0d 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -29,8 +29,6 @@ public abstract partial class InteractionTest
protected const string Rod = "MetalRod";
// Parts
- protected const string Bin1 = "MatterBinStockPart";
- protected const string Cap1 = "CapacitorStockPart";
protected const string Manipulator1 = "MicroManipulatorStockPart";
protected const string Battery1 = "PowerCellSmall";
protected const string Battery4 = "PowerCellHyper";
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index a61f7c3ec8..0f9e9b5ebe 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -791,7 +791,7 @@ public abstract partial class InteractionTest
gridUid = gridEnt;
gridComp = gridEnt.Comp;
var gridXform = SEntMan.GetComponent(gridUid);
- Transform.SetWorldPosition(gridXform, pos.Position);
+ Transform.SetWorldPosition((gridUid, gridXform), pos.Position);
MapSystem.SetTile((gridUid, gridComp), SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
if (!MapMan.TryFindGridAt(pos, out _, out _))
diff --git a/Content.IntegrationTests/Tests/Power/StationPowerTests.cs b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
new file mode 100644
index 0000000000..c9f9498750
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Power/StationPowerTests.cs
@@ -0,0 +1,105 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.GameTicking;
+using Content.Server.Maps;
+using Content.Server.Power.Components;
+using Content.Server.Power.NodeGroups;
+using Content.Server.Power.Pow3r;
+using Content.Shared.NodeContainer;
+using Robust.Shared.EntitySerialization;
+
+namespace Content.IntegrationTests.Tests.Power;
+
+[Explicit]
+public sealed class StationPowerTests
+{
+ ///
+ /// How long the station should be able to survive on stored power if nothing is changed from round start.
+ ///
+ private const float MinimumPowerDurationSeconds = 10 * 60;
+
+ private static readonly string[] GameMaps =
+ [
+ "Fland",
+ "Meta",
+ "Packed",
+ "Omega",
+ "Bagel",
+ "Box",
+ "Core",
+ "Marathon",
+ "Saltern",
+ "Reach",
+ "Train",
+ "Oasis",
+ "Gate",
+ "Amber",
+ "Loop",
+ "Plasma",
+ "Elkridge",
+ "Convex",
+ "Relic",
+ ];
+
+ [Test, TestCaseSource(nameof(GameMaps))]
+ public async Task TestStationStartingPowerWindow(string mapProtoId)
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ Dirty = true,
+ });
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var protoMan = server.ProtoMan;
+ var ticker = entMan.System();
+
+ // Load the map
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(protoMan.TryIndex(mapProtoId, out var mapProto));
+ var opts = DeserializationOptions.Default with { InitializeMaps = true };
+ ticker.LoadGameMap(mapProto, out var mapId, opts);
+ });
+
+ // Let powernet set up
+ await server.WaitRunTicks(1);
+
+ // Find the power network with the greatest stored charge in its batteries.
+ // This keeps backup SMESes out of the calculation.
+ var networks = new Dictionary