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/Commands/ShowAccessReadersCommand.cs b/Content.Client/Access/Commands/ShowAccessReadersCommand.cs index cb6cb6cf6b..e26cca0fc2 100644 --- a/Content.Client/Access/Commands/ShowAccessReadersCommand.cs +++ b/Content.Client/Access/Commands/ShowAccessReadersCommand.cs @@ -4,39 +4,20 @@ using Robust.Shared.Console; namespace Content.Client.Access.Commands; -public sealed class ShowAccessReadersCommand : IConsoleCommand +public sealed class ShowAccessReadersCommand : LocalizedEntityCommands { - public string Command => "showaccessreaders"; + [Dependency] private readonly IOverlayManager _overlay = default!; + [Dependency] private readonly IResourceCache _cache = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; - public string Description => "Toggles showing access reader permissions on the map"; - public string Help => """ - Overlay Info: - -Disabled | The access reader is disabled - +Unrestricted | The access reader has no restrictions - +Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set) - +Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed - -Tag [Tag Name] | A tag that is not allowed (takes priority over other allows) - """; - public void Execute(IConsoleShell shell, string argStr, string[] args) + public override string Command => "showaccessreaders"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) { - var collection = IoCManager.Instance; + var existing = _overlay.RemoveOverlay(); + if (!existing) + _overlay.AddOverlay(new AccessOverlay(EntityManager, _cache, _xform)); - if (collection == null) - return; - - var overlay = collection.Resolve(); - - if (overlay.RemoveOverlay()) - { - shell.WriteLine($"Set access reader debug overlay to false"); - return; - } - - var entManager = collection.Resolve(); - var cache = collection.Resolve(); - var xform = entManager.System(); - - overlay.AddOverlay(new AccessOverlay(entManager, cache, xform)); - shell.WriteLine($"Set access reader debug overlay to true"); + shell.WriteLine(Loc.GetString($"cmd-showaccessreaders-status", ("status", !existing))); } } 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..23ff23997f 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,8 +30,8 @@ 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!; public event Action? OnActionAdded; @@ -38,131 +43,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 +118,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 +194,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 +234,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/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs index bb24da44e1..e280523e43 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs @@ -136,6 +136,7 @@ namespace Content.Client.Atmos.UI else { // oh shit of fuck its more than 4 this ui isn't gonna look pretty anymore + CDeviceMixes.RemoveAllChildren(); for (var i = 1; i < msg.NodeGasMixes.Length; i++) { GenerateGasDisplay(msg.NodeGasMixes[i], CDeviceMixes); diff --git a/Content.Client/Charges/ChargesSystem.cs b/Content.Client/Charges/ChargesSystem.cs index 2c7e0536cd..890ff207ac 100644 --- a/Content.Client/Charges/ChargesSystem.cs +++ b/Content.Client/Charges/ChargesSystem.cs @@ -25,16 +25,14 @@ public sealed class ChargesSystem : SharedChargesSystem while (query.MoveNext(out var uid, out var recharge, out var charges)) { - BaseActionComponent? actionComp = null; - - if (!_actions.ResolveActionData(uid, ref actionComp, logError: false)) + if (_actions.GetAction(uid, false) is not {} action) continue; var current = GetCurrentCharges((uid, charges, recharge)); if (!_lastCharges.TryGetValue(uid, out var last) || current != last) { - _actions.UpdateAction(uid, actionComp); + _actions.UpdateAction(action); } _tempLastCharges[uid] = current; diff --git a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs index bde6a4b99a..08153c3b3f 100644 --- a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs @@ -11,18 +11,6 @@ namespace Content.Client.Clothing.Systems; // All valid items for chameleon are calculated on client startup and stored in dictionary. 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 +49,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/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/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/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 2ff7cb6d61..ede0b0bcea 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -126,6 +126,8 @@ namespace Content.Client.Entry _prototypeManager.RegisterIgnore("nukeopsRole"); _prototypeManager.RegisterIgnore("stationGoal"); // Corvax-StationGoal _prototypeManager.RegisterIgnore("ghostRoleRaffleDecider"); + _prototypeManager.RegisterIgnore("codewordGenerator"); + _prototypeManager.RegisterIgnore("codewordFaction"); _componentFactory.GenerateNetIds(); _adminManager.Initialize(); 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 13ee0c87e7..677105df00 100644 --- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs @@ -31,7 +31,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; @@ -225,7 +225,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/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/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs index fff1b23ca1..a2b62fb36a 100644 --- a/Content.Client/Lobby/LobbyUIController.cs +++ b/Content.Client/Lobby/LobbyUIController.cs @@ -33,7 +33,6 @@ public sealed partial class LobbyUIController : UIController, IOnStateEntered +/// 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..c9e759e129 100644 --- a/Content.Client/Movement/Systems/JetpackSystem.cs +++ b/Content.Client/Movement/Systems/JetpackSystem.cs @@ -16,7 +16,6 @@ public sealed class JetpackSystem : SharedJetpackSystem [Dependency] private readonly ClothingSystem _clothing = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() { @@ -34,10 +33,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 @@ + +