diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..8554c97ee8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,40 @@ +# Space Station 14 Code of Conduct + +Space Station 14's staff and community is made up volunteers from all over the world, working on every aspect of the project - including development, teaching, and hosting integral tools. + +Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to all levels of the project, from commenters to contributors to staff. + +This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. + +This code of conduct applies specifically to the Github repositories and its spaces managed by the Space Station 14 project or Space Wizards Federation. Some spaces, such as the Space Station 14 Discord or the official Wizard's Den game servers, have their own rules but are in spirit equal to what may be found in here. + +If you believe someone is violating the code of conduct, we ask that you report it by contacting a Maintainer, Project Manager or Wizard staff member through [Discord](https://discord.ss14.io/), [the forums](https://forum.spacestation14.com/), or emailing [telecommunications@spacestation14.com](mailto:telecommunications@spacestation14.com). + +- **Be friendly and patient.** +- **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. +- **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and contributors, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. We have contributors of all skill levels, some even making their first foray into a new field with this project, so keep that in mind when discussing someone's work. +- **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Space Station 14 community should be respectful when dealing with other members as well as with people outside the Space Station 14 community. Assume contributions to the project, even those that do not end up being included, are made in good faith. +- **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: + - Violent threats or language directed against another person. + - Discriminatory jokes and language. + - Posting sexually explicit or violent material. + - Posting (or threatening to post) other people's personally identifying information ("doxing"). + - Personal insults, especially those using racist or sexist terms. + - Unwelcome sexual attention. + - Advocating for, or encouraging, any of the above behavior. + - Repeated harassment of others. In general, if someone asks you to stop, then stop. +- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Space Station 14 is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Space Station 14 comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to make mistakes and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. + +Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html). + +## On Comunity Moderation + +Deviating from the Code of Conduct on the Github repository may result in moderative actions taken by project Maintainers. This can involve your content being edited or deleted, and may result in a temporary or permanent block from the repository. + +This is to ensure Space Station 14 is a healthy community in which contributors feel encouraged and empowered to contribute, and to give you as a member of this community a chance to reflect on how you are interacting with it. While outright offensive and bigoted content will *always* be unacceptable on the repository, Maintainers are at liberty to take moderative actions against more ambiguous content that fail to provide constructive criticism, or that provides constructive criticism in a non-constructive manner. Examples of this include using hyperbole, bringing up PRs/changes unrelated to the discussion at hand, hostile tone, off-topic comments, creating PRs/Issues for the sole purpose of causing discussions, skirting the line of acceptable behavior, etc. Disagreeing with content or each other is fine and appreciated, but only as long as it's done with respect and in a constructive manner. + +Maintainers are expected to adhere to the guidelines as listed in the [Github Moderation Guidelines](https://docs.spacestation14.com/en/general-development/github-moderation-guidelines.html), though may deviate should they feel it's in the best interest of the community. If you believe you had an action incorrectly applied against you, you are encouraged to contact staff via [Discord](https://discord.ss14.io/) or [the forums](https://forum.spacestation14.com/), [appeal your Github ban](https://forum.spacestation14.com/c/ban-appeals/appeals-github/38), or make a [staff complaint](https://forum.spacestation14.com/t/staff-complaint-instructions-and-info/31). + +## Attribution + +This Code of Conduct is an edited version of the [Django Code of Conduct](https://www.djangoproject.com/conduct/), licensed under CC BY 3.0, for the Space Station 14 Github repository. diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index 2bb71bb59d..de788234e5 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -47,7 +47,7 @@ public class MapLoadBenchmark PoolManager.Shutdown(); } - public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"}; + public static readonly string[] MapsSource = { "Empty", "Saltern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"}; [ParamsSource(nameof(MapsSource))] public string Map; diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml new file mode 100644 index 0000000000..84d581487d --- /dev/null +++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs new file mode 100644 index 0000000000..da68653ce5 --- /dev/null +++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs @@ -0,0 +1,449 @@ +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Access; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; + +namespace Content.Client.Access.UI; + +[GenerateTypedNameReferences] +public sealed partial class GroupedAccessLevelChecklist : BoxContainer +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + private bool _isMonotone; + private string? _labelStyleClass; + + // Access data + private HashSet> _accessGroups = new(); + private HashSet> _accessLevels = new(); + private HashSet> _activeAccessLevels = new(); + + // Button groups + private readonly ButtonGroup _accessGroupsButtons = new(); + + // Temp values + private int _accessGroupTabIndex = 0; + private bool _canInteract = false; + private List _accessLevelsForTab = new(); + private readonly List _accessLevelEntries = new(); + private readonly Dictionary> _groupedAccessLevels = new(); + + // Events + public event Action>, bool>? OnAccessLevelsChangedEvent; + + /// + /// Creates a UI control for changing access levels. + /// Access levels are organized under a list of tabs by their associated access group. + /// + public GroupedAccessLevelChecklist() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } + + private void ArrangeAccessControls() + { + // Create a list of known access groups with which to populate the UI + _groupedAccessLevels.Clear(); + + foreach (var accessGroup in _accessGroups) + { + if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto)) + continue; + + _groupedAccessLevels.Add(accessGroupProto, new()); + } + + // Ensure that the 'general' access group is added to handle + // misc. access levels that aren't associated with any group + if (_protoManager.TryIndex("General", out var generalAccessProto)) + _groupedAccessLevels.TryAdd(generalAccessProto, new()); + + // Assign known access levels with their associated groups + foreach (var accessLevel in _accessLevels) + { + if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto)) + continue; + + var assigned = false; + + foreach (var (accessGroup, accessLevels) in _groupedAccessLevels) + { + if (!accessGroup.Tags.Contains(accessLevelProto.ID)) + continue; + + assigned = true; + _groupedAccessLevels[accessGroup].Add(accessLevelProto); + } + + if (!assigned && generalAccessProto != null) + _groupedAccessLevels[generalAccessProto].Add(accessLevelProto); + } + + // Remove access groups that have no assigned access levels + foreach (var (group, accessLevels) in _groupedAccessLevels) + { + if (accessLevels.Count == 0) + _groupedAccessLevels.Remove(group); + } + } + + private bool TryRebuildAccessGroupControls() + { + AccessGroupList.DisposeAllChildren(); + AccessLevelChecklist.DisposeAllChildren(); + + // No access level prototypes were assigned to any of the access level groups. + // Either the turret controller has no assigned access levels or their names were invalid. + if (_groupedAccessLevels.Count == 0) + return false; + + // Reorder the access groups alphabetically + var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList(); + + // Add group access buttons to the UI + foreach (var accessGroup in orderedAccessGroups) + { + var accessGroupButton = CreateAccessGroupButton(); + + // Button styling + if (_groupedAccessLevels.Count > 1) + { + if (AccessGroupList.ChildCount == 0) + accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft); + else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1)) + accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight); + else + accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth); + } + + accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup); + + // Label text and styling + if (_labelStyleClass != null) + accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass); + + var accessLevelPrototypes = _groupedAccessLevels[accessGroup]; + var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x)) + ? "»" + : accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x)) + ? "›" + : " "; + + var text = Loc.GetString( + "turret-controls-window-access-group-label", + ("prefix", prefix), + ("label", accessGroup.GetAccessGroupName()) + ); + + accessGroupButton.Text = text; + + // Button events + accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent()); + + AccessGroupList.AddChild(accessGroupButton); + } + + // Adjust the current tab index so it remains in range + if (_accessGroupTabIndex >= _groupedAccessLevels.Count) + _accessGroupTabIndex = _groupedAccessLevels.Count - 1; + + return true; + } + + /// + /// Rebuilds the checkbox list for the access level controls. + /// + public void RebuildAccessLevelsControls() + { + AccessLevelChecklist.DisposeAllChildren(); + _accessLevelEntries.Clear(); + + // No access level prototypes were assigned to any of the access level groups + // Either turret controller has no assigned access levels, or their names were invalid + if (_groupedAccessLevels.Count == 0) + return; + + // Reorder the access groups alphabetically + var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList(); + + // Get the access levels associated with the current tab + var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex]; + _accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto]; + _accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList(); + + // Add an 'all' checkbox as the first child of the list if it has more than one access level + // Toggling this checkbox on will mark all other boxes below it on/off + var allCheckBox = CreateAccessLevelCheckbox(); + allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox"); + + if (_labelStyleClass != null) + allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass); + + // Add the 'all' checkbox events + allCheckBox.OnPressed += args => + { + SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed); + + var accessLevels = new HashSet>(); + + foreach (var accessLevel in _accessLevelsForTab) + { + accessLevels.Add(accessLevel); + } + + OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed); + }; + + AccessLevelChecklist.AddChild(allCheckBox); + + // Hide the 'all' checkbox if the tab has only one access level + var allCheckBoxVisible = _accessLevelsForTab.Count > 1; + + allCheckBox.Visible = allCheckBoxVisible; + allCheckBox.Disabled = !_canInteract; + + // Add any remaining missing access level buttons to the UI + foreach (var accessLevel in _accessLevelsForTab) + { + // Create the entry + var accessLevelEntry = new AccessLevelEntry(_isMonotone); + + accessLevelEntry.AccessLevel = accessLevel; + accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName(); + accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel); + accessLevelEntry.CheckBox.Disabled = !_canInteract; + + if (_labelStyleClass != null) + accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass); + + // Set the checkbox linkage lines + var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1); + + var lines = new List<(Vector2, Vector2)> + { + (new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)), + (new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)), + }; + + accessLevelEntry.UpdateCheckBoxLink(lines); + accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible; + accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White; + + // Add checkbox events + accessLevelEntry.CheckBox.OnPressed += args => + { + // If the checkbox and its siblings are checked, check the 'all' checkbox too + allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox)); + + OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed); + }; + + AccessLevelChecklist.AddChild(accessLevelEntry); + _accessLevelEntries.Add(accessLevelEntry); + } + + // Press the 'all' checkbox if all others are pressed + allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox)); + } + + private bool AreAllCheckBoxesPressed(IEnumerable checkBoxes) + { + foreach (var checkBox in checkBoxes) + { + if (!checkBox.Pressed) + return false; + } + + return true; + } + + private void SetCheckBoxPressedState(List accessLevelEntries, bool pressed) + { + foreach (var accessLevelEntry in accessLevelEntries) + { + accessLevelEntry.CheckBox.Pressed = pressed; + } + } + + + /// + /// Provides the UI with a list of access groups using which list of tabs should be populated. + /// + public void SetAccessGroups(HashSet> accessGroups) + { + _accessGroups = accessGroups; + + ArrangeAccessControls(); + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Provides the UI with a list of access levels with which it can populate the currently selected tab. + /// + public void SetAccessLevels(HashSet> accessLevels) + { + _accessLevels = accessLevels; + + ArrangeAccessControls(); + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Sets which access level checkboxes should be marked on the UI. + /// + public void SetActiveAccessLevels(HashSet> activeAccessLevels) + { + _activeAccessLevels = activeAccessLevels; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Sets whether the local player can interact with the checkboxes. + /// + public void SetLocalPlayerAccessibility(bool canInteract) + { + _canInteract = canInteract; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Sets whether the UI should use monotone buttons and checkboxes. + /// + public void SetMonotone(bool monotone) + { + _isMonotone = monotone; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Applies the specified style to the labels on the UI buttons and checkboxes. + /// + public void SetLabelStyleClass(string? styleClass) + { + _labelStyleClass = styleClass; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + private void OnAccessGroupChanged(int newTabIndex) + { + if (newTabIndex == _accessGroupTabIndex) + return; + + _accessGroupTabIndex = newTabIndex; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + private Button CreateAccessGroupButton() + { + var button = _isMonotone ? new MonotoneButton() : new Button(); + + button.ToggleMode = true; + button.Group = _accessGroupsButtons; + button.Label.HorizontalAlignment = HAlignment.Left; + + return button; + } + + private CheckBox CreateAccessLevelCheckbox() + { + var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox(); + + checkbox.Margin = new Thickness(0, 0, 0, 3); + checkbox.ToggleMode = true; + checkbox.ReservesSpace = false; + + return checkbox; + } + + private sealed class AccessLevelEntry : BoxContainer + { + public ProtoId AccessLevel; + public readonly CheckBox CheckBox; + public readonly LineRenderer CheckBoxLink; + + public AccessLevelEntry(bool monotone) + { + HorizontalExpand = true; + + CheckBoxLink = new LineRenderer + { + SetWidth = 22, + VerticalExpand = true, + Margin = new Thickness(0, -1), + ReservesSpace = false, + }; + + AddChild(CheckBoxLink); + + CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox(); + CheckBox.ToggleMode = true; + CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f); + + AddChild(CheckBox); + } + + public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines) + { + CheckBoxLink.Lines = lines; + } + } + + private sealed class LineRenderer : Control + { + /// + /// List of lines to render (their start and end x-y coordinates). + /// Position (0,0) is the top left corner of the control and + /// position (1,1) is the bottom right corner. + /// + /// + /// The color of the lines is inherited from the control. + /// + public List<(Vector2, Vector2)> Lines; + + public LineRenderer() + { + Lines = new List<(Vector2, Vector2)>(); + } + + public LineRenderer(List<(Vector2, Vector2)> lines) + { + Lines = lines; + } + + protected override void Draw(DrawingHandleScreen handle) + { + foreach (var line in Lines) + { + var start = PixelPosition + + new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y); + + var end = PixelPosition + + new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y); + + handle.DrawLine(start, end, ActualModulateSelf); + } + } + } +} diff --git a/Content.Client/Actions/ActionEvents.cs b/Content.Client/Actions/ActionEvents.cs index 2fdf25c976..73bee331be 100644 --- a/Content.Client/Actions/ActionEvents.cs +++ b/Content.Client/Actions/ActionEvents.cs @@ -1,3 +1,6 @@ +using Content.Shared.Actions.Components; +using static Robust.Shared.Input.Binding.PointerInputCmdHandler; + namespace Content.Client.Actions; /// @@ -7,3 +10,17 @@ public sealed class FillActionSlotEvent : EntityEventArgs { public EntityUid? Action; } + +/// +/// Client-side event used to attempt to trigger a targeted action. +/// This only gets raised if the has . +/// Handlers must set Handled to true, then if the action has been performed, +/// i.e. a target is found, then FoundTarget must be set to true. +/// +[ByRefEvent] +public record struct ActionTargetAttemptEvent( + PointerInputCmdArgs Input, + Entity User, + ActionComponent Action, + bool Handled = false, + bool FoundTarget = false); diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 31350a6a5d..91baa3b1a9 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -1,18 +1,23 @@ using System.IO; using System.Linq; using Content.Shared.Actions; +using Content.Shared.Actions.Components; using Content.Shared.Charges.Systems; +using Content.Shared.Mapping; +using Content.Shared.Maps; using JetBrains.Annotations; using Robust.Client.Player; using Robust.Shared.ContentPack; using Robust.Shared.GameStates; using Robust.Shared.Input.Binding; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Timing; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; @@ -25,6 +30,7 @@ namespace Content.Client.Actions [Dependency] private readonly SharedChargesSystem _sharedCharges = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IResourceManager _resources = default!; [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; @@ -38,131 +44,67 @@ namespace Content.Client.Actions public event Action>? AssignSlot; private readonly List _removed = new(); - private readonly List<(EntityUid, BaseActionComponent?)> _added = new(); + private readonly List> _added = new(); + + public static readonly EntProtoId MappingEntityAction = "BaseMappingEntityAction"; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); - SubscribeLocalEvent(HandleComponentState); + SubscribeLocalEvent(OnHandleState); - SubscribeLocalEvent(OnInstantHandleState); - SubscribeLocalEvent(OnEntityTargetHandleState); - SubscribeLocalEvent(OnWorldTargetHandleState); - SubscribeLocalEvent(OnEntityWorldTargetHandleState); + SubscribeLocalEvent(OnActionAutoHandleState); + + SubscribeLocalEvent(OnEntityTargetAttempt); + SubscribeLocalEvent(OnWorldTargetAttempt); } - private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args) - { - if (args.Current is not InstantActionComponentState state) - return; - BaseHandleState(uid, component, state); + private void OnActionAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateAction(ent); } - private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args) + public override void UpdateAction(Entity ent) { - if (args.Current is not EntityTargetActionComponentState state) - return; - - component.Whitelist = state.Whitelist; - component.Blacklist = state.Blacklist; - component.CanTargetSelf = state.CanTargetSelf; - BaseHandleState(uid, component, state); - } - - private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args) - { - if (args.Current is not WorldTargetActionComponentState state) - return; - - BaseHandleState(uid, component, state); - } - - private void OnEntityWorldTargetHandleState(EntityUid uid, - EntityWorldTargetActionComponent component, - ref ComponentHandleState args) - { - if (args.Current is not EntityWorldTargetActionComponentState state) - return; - - component.Whitelist = state.Whitelist; - component.CanTargetSelf = state.CanTargetSelf; - BaseHandleState(uid, component, state); - } - - private void BaseHandleState(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent - { - // TODO ACTIONS use auto comp states - component.Icon = state.Icon; - component.IconOn = state.IconOn; - component.IconColor = state.IconColor; - component.OriginalIconColor = state.OriginalIconColor; - component.DisabledIconColor = state.DisabledIconColor; - component.Keywords.Clear(); - component.Keywords.UnionWith(state.Keywords); - component.Enabled = state.Enabled; - component.Toggled = state.Toggled; - component.Cooldown = state.Cooldown; - component.UseDelay = state.UseDelay; - component.Container = EnsureEntity(state.Container, uid); - component.EntityIcon = EnsureEntity(state.EntityIcon, uid); - component.CheckCanInteract = state.CheckCanInteract; - component.CheckConsciousness = state.CheckConsciousness; - component.ClientExclusive = state.ClientExclusive; - component.Priority = state.Priority; - component.AttachedEntity = EnsureEntity(state.AttachedEntity, uid); - component.RaiseOnUser = state.RaiseOnUser; - component.RaiseOnAction = state.RaiseOnAction; - component.AutoPopulate = state.AutoPopulate; - component.Temporary = state.Temporary; - component.ItemIconStyle = state.ItemIconStyle; - component.Sound = state.Sound; - - UpdateAction(uid, component); - } - - public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null) - { - if (!ResolveActionData(actionId, ref action)) - return; - // TODO: Decouple this. - action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor; - - base.UpdateAction(actionId, action); - if (_playerManager.LocalEntity != action.AttachedEntity) + ent.Comp.IconColor = _sharedCharges.GetCurrentCharges(ent.Owner) == 0 ? ent.Comp.DisabledIconColor : ent.Comp.OriginalIconColor; + base.UpdateAction(ent); + if (_playerManager.LocalEntity != ent.Comp.AttachedEntity) return; ActionsUpdated?.Invoke(); } - private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args) + private void OnHandleState(Entity ent, ref ComponentHandleState args) { if (args.Current is not ActionsComponentState state) return; + var (uid, comp) = ent; _added.Clear(); _removed.Clear(); var stateEnts = EnsureEntitySet(state.Actions, uid); - foreach (var act in component.Actions) + foreach (var act in comp.Actions) { if (!stateEnts.Contains(act) && !IsClientSide(act)) _removed.Add(act); } - component.Actions.ExceptWith(_removed); + comp.Actions.ExceptWith(_removed); foreach (var actionId in stateEnts) { if (!actionId.IsValid()) continue; - if (!component.Actions.Add(actionId)) + if (!comp.Actions.Add(actionId)) continue; - TryGetActionData(actionId, out var action); - _added.Add((actionId, action)); + if (GetAction(actionId) is {} action) + _added.Add(action); } if (_playerManager.LocalEntity != uid) @@ -177,47 +119,46 @@ namespace Content.Client.Actions foreach (var action in _added) { - OnActionAdded?.Invoke(action.Item1); + OnActionAdded?.Invoke(action); } ActionsUpdated?.Invoke(); } - public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b) + public static int ActionComparer(Entity a, Entity b) { - var priorityA = a.Item2?.Priority ?? 0; - var priorityB = b.Item2?.Priority ?? 0; + var priorityA = a.Comp?.Priority ?? 0; + var priorityB = b.Comp?.Priority ?? 0; if (priorityA != priorityB) return priorityA - priorityB; - priorityA = a.Item2?.Container?.Id ?? 0; - priorityB = b.Item2?.Container?.Id ?? 0; + priorityA = a.Comp?.Container?.Id ?? 0; + priorityB = b.Comp?.Container?.Id ?? 0; return priorityA - priorityB; } - protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp, - BaseActionComponent action) + protected override void ActionAdded(Entity performer, Entity action) { - if (_playerManager.LocalEntity != performer) + if (_playerManager.LocalEntity != performer.Owner) return; - OnActionAdded?.Invoke(actionId); + OnActionAdded?.Invoke(action); ActionsUpdated?.Invoke(); } - protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action) + protected override void ActionRemoved(Entity performer, Entity action) { - if (_playerManager.LocalEntity != performer) + if (_playerManager.LocalEntity != performer.Owner) return; - OnActionRemoved?.Invoke(actionId); + OnActionRemoved?.Invoke(action); ActionsUpdated?.Invoke(); } - public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions() + public IEnumerable> GetClientActions() { if (_playerManager.LocalEntity is not { } user) - return Enumerable.Empty<(EntityUid, BaseActionComponent)>(); + return Enumerable.Empty>(); return GetActions(user); } @@ -254,24 +195,23 @@ namespace Content.Client.Actions CommandBinds.Unregister(); } - public void TriggerAction(EntityUid actionId, BaseActionComponent action) + public void TriggerAction(Entity action) { - if (_playerManager.LocalEntity is not { } user || - !TryComp(user, out ActionsComponent? actions)) - { - return; - } - - if (action is not InstantActionComponent instantAction) + if (_playerManager.LocalEntity is not { } user) return; - if (action.ClientExclusive) + // TODO: unhardcode this somehow + + if (!HasComp(action)) + return; + + if (action.Comp.ClientExclusive) { - PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime); + PerformAction(user, action); } else { - var request = new RequestPerformActionEvent(GetNetEntity(actionId)); + var request = new RequestPerformActionEvent(GetNetEntity(action)); EntityManager.RaisePredictiveEvent(request); } } @@ -295,39 +235,137 @@ namespace Content.Client.Actions if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence) return; + var actions = EnsureComp(user); + ClearAssignments?.Invoke(); var assignments = new List(); - foreach (var entry in sequence.Sequence) { if (entry is not MappingDataNode map) continue; - if (!map.TryGet("action", out var actionNode)) - continue; - - var action = _serialization.Read(actionNode, notNullableOverride: true); - var actionId = Spawn(); - AddComp(actionId, action); - AddActionDirect(user, actionId); - - if (map.TryGet("name", out var nameNode)) - _metaData.SetEntityName(actionId, nameNode.Value); - if (!map.TryGet("assignments", out var assignmentNode)) continue; - var nodeAssignments = _serialization.Read>(assignmentNode, notNullableOverride: true); - - foreach (var index in nodeAssignments) + var actionId = EntityUid.Invalid; + if (map.TryGet("action", out var actionNode)) { - var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId); - assignments.Add(assignment); + var id = new EntProtoId(actionNode.Value); + actionId = Spawn(id); } + else if (map.TryGet("entity", out var entityNode)) + { + var id = new EntProtoId(entityNode.Value); + var proto = _proto.Index(id); + actionId = Spawn(MappingEntityAction); + SetIcon(actionId, new SpriteSpecifier.EntityPrototype(id)); + SetEvent(actionId, new StartPlacementActionEvent() + { + PlacementOption = "SnapgridCenter", + EntityType = id + }); + _metaData.SetEntityName(actionId, proto.Name); + } + else if (map.TryGet("tileId", out var tileNode)) + { + var id = new ProtoId(tileNode.Value); + var proto = _proto.Index(id); + actionId = Spawn(MappingEntityAction); + if (proto.Sprite is {} sprite) + SetIcon(actionId, new SpriteSpecifier.Texture(sprite)); + SetEvent(actionId, new StartPlacementActionEvent() + { + PlacementOption = "AlignTileAny", + TileId = id + }); + _metaData.SetEntityName(actionId, Loc.GetString(proto.Name)); + } + else + { + Log.Error($"Mapping actions from {path} had unknown action data!"); + continue; + } + + AddActionDirect((user, actions), actionId); + } + } + + private void OnWorldTargetAttempt(Entity ent, ref ActionTargetAttemptEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + + var (uid, comp) = ent; + var action = args.Action; + var coords = args.Input.Coordinates; + var user = args.User; + + if (!ValidateWorldTarget(user, coords, ent)) + return; + + // optionally send the clicked entity too, if it matches its whitelist etc + // this is the actual entity-world targeting magic + EntityUid? targetEnt = null; + if (TryComp(ent, out var entity) && + args.Input.EntityUid != null && + ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity))) + { + targetEnt = args.Input.EntityUid; } - AssignSlot?.Invoke(assignments); + if (action.ClientExclusive) + { + // TODO: abstract away from single event or maybe just RaiseLocalEvent? + if (comp.Event is {} ev) + { + ev.Target = coords; + ev.Entity = targetEnt; + } + + PerformAction((user, user.Comp), (uid, action)); + } + else + RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(targetEnt), GetNetCoordinates(coords))); + + args.FoundTarget = true; + } + + private void OnEntityTargetAttempt(Entity ent, ref ActionTargetAttemptEvent args) + { + if (args.Handled || args.Input.EntityUid is not { Valid: true } entity) + return; + + // let world target component handle it + var (uid, comp) = ent; + if (comp.Event is not {} ev) + { + DebugTools.Assert(HasComp(ent), $"Action {ToPrettyString(ent)} requires WorldTargetActionComponent for entity-world targeting"); + return; + } + + args.Handled = true; + + var action = args.Action; + var user = args.User; + + if (!ValidateEntityTarget(user, entity, ent)) + return; + + if (action.ClientExclusive) + { + ev.Target = entity; + + PerformAction((user, user.Comp), (uid, action)); + } + else + { + RaisePredictiveEvent(new RequestPerformActionEvent(GetNetEntity(uid), GetNetEntity(entity))); + } + + args.FoundTarget = true; } public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId); diff --git a/Content.Client/Administration/UI/Tabs/RoundTab.xaml b/Content.Client/Administration/UI/Tabs/RoundTab.xaml index 2c8a400ecd..36c06cab76 100644 --- a/Content.Client/Administration/UI/Tabs/RoundTab.xaml +++ b/Content.Client/Administration/UI/Tabs/RoundTab.xaml @@ -1,13 +1,13 @@  - - - - + + + + diff --git a/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs b/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs index 28073bc91d..70f12bb393 100644 --- a/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/RoundTab.xaml.cs @@ -1,10 +1,24 @@ using Robust.Client.AutoGenerated; +using Robust.Client.Console; using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; namespace Content.Client.Administration.UI.Tabs { [GenerateTypedNameReferences] public sealed partial class RoundTab : Control { + [Dependency] private readonly IClientConsoleHost _console = default!; + + public RoundTab() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + StartRound.OnPressed += _ => _console.ExecuteCommand("startround"); + EndRound.OnPressed += _ => _console.ExecuteCommand("endround"); + RestartRound.OnPressed += _ => _console.ExecuteCommand("restartround"); + RestartRoundNow.OnPressed += _ => _console.ExecuteCommand("restartroundnow"); + } } } diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml b/Content.Client/Administration/UI/Tabs/ServerTab.xaml index b998405835..80c186f7fd 100644 --- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml @@ -1,11 +1,12 @@  - + diff --git a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs index 24b92e42ce..7a70e42d06 100644 --- a/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ServerTab.xaml.cs @@ -1,5 +1,6 @@ using Content.Shared.CCVar; using Robust.Client.AutoGenerated; +using Robust.Client.Console; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; @@ -10,6 +11,7 @@ namespace Content.Client.Administration.UI.Tabs public sealed partial class ServerTab : Control { [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly IClientConsoleHost _console = default!; public ServerTab() { @@ -18,6 +20,8 @@ namespace Content.Client.Administration.UI.Tabs _config.OnValueChanged(CCVars.OocEnabled, OocEnabledChanged, true); _config.OnValueChanged(CCVars.LoocEnabled, LoocEnabledChanged, true); + + ServerShutdownButton.OnPressed += _ => _console.ExecuteCommand("shutdown"); } private void OocEnabledChanged(bool value) diff --git a/Content.Client/Atmos/AlignAtmosPipeLayers.cs b/Content.Client/Atmos/AlignAtmosPipeLayers.cs new file mode 100644 index 0000000000..1bf3310a6c --- /dev/null +++ b/Content.Client/Atmos/AlignAtmosPipeLayers.cs @@ -0,0 +1,203 @@ +using Content.Client.Construction; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Content.Shared.Construction.Prototypes; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Placement; +using Robust.Client.Placement.Modes; +using Robust.Client.Utility; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using System.Numerics; +using static Robust.Client.Placement.PlacementManager; + +namespace Content.Client.Atmos; + +/// +/// Allows users to place atmos pipes on different layers depending on how the mouse cursor is positioned within a grid tile. +/// +/// +/// This placement mode is not on the engine because it is content specific. +/// +public sealed class AlignAtmosPipeLayers : SnapgridCenter +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + + private readonly SharedMapSystem _mapSystem; + private readonly SharedTransformSystem _transformSystem; + private readonly SharedAtmosPipeLayersSystem _pipeLayersSystem; + private readonly SpriteSystem _spriteSystem; + + private const float SearchBoxSize = 2f; + private EntityCoordinates _unalignedMouseCoords = default; + private const float MouseDeadzoneRadius = 0.25f; + + private Color _guideColor = new Color(0, 0, 0.5785f); + private const float GuideRadius = 0.1f; + private const float GuideOffset = 0.21875f; + + public AlignAtmosPipeLayers(PlacementManager pMan) : base(pMan) + { + IoCManager.InjectDependencies(this); + + _mapSystem = _entityManager.System(); + _transformSystem = _entityManager.System(); + _pipeLayersSystem = _entityManager.System(); + _spriteSystem = _entityManager.System(); + } + + /// + public override void Render(in OverlayDrawArgs args) + { + var gridUid = _entityManager.System().GetGrid(MouseCoords); + + if (gridUid == null || Grid == null) + return; + + // Draw guide circles for each pipe layer if we are not in line/grid placing mode + if (pManager.PlacementType == PlacementTypes.None) + { + var gridRotation = _transformSystem.GetWorldRotation(gridUid.Value); + var worldPosition = _mapSystem.LocalToWorld(gridUid.Value, Grid, MouseCoords.Position); + var direction = (_eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir(); + var multi = (direction == Direction.North || direction == Direction.South) ? -1f : 1f; + + args.WorldHandle.DrawCircle(worldPosition, GuideRadius, _guideColor); + args.WorldHandle.DrawCircle(worldPosition + gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor); + args.WorldHandle.DrawCircle(worldPosition - gridRotation.RotateVec(new Vector2(multi * GuideOffset, GuideOffset)), GuideRadius, _guideColor); + } + + base.Render(args); + } + + /// + public override void AlignPlacementMode(ScreenCoordinates mouseScreen) + { + _unalignedMouseCoords = ScreenToCursorGrid(mouseScreen); + base.AlignPlacementMode(mouseScreen); + + // Exit early if we are in line/grid placing mode + if (pManager.PlacementType != PlacementTypes.None) + return; + + MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager); + + var gridId = _transformSystem.GetGrid(MouseCoords); + + if (!_entityManager.TryGetComponent(gridId, out var mapGrid)) + return; + + var gridRotation = _transformSystem.GetWorldRotation(gridId.Value); + CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords); + + float tileSize = mapGrid.TileSize; + GridDistancing = tileSize; + + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X, + CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y)); + + // Calculate the position of the mouse cursor with respect to the center of the tile to determine which layer to use + var mouseCoordsDiff = _unalignedMouseCoords.Position - MouseCoords.Position; + var layer = AtmosPipeLayer.Primary; + + if (mouseCoordsDiff.Length() > MouseDeadzoneRadius) + { + // Determine the direction of the mouse is relative to the center of the tile, adjusting for the player eye and grid rotation + var direction = (new Angle(mouseCoordsDiff) + _eyeManager.CurrentEye.Rotation + gridRotation + Math.PI / 2).GetCardinalDir(); + layer = (direction == Direction.North || direction == Direction.East) ? AtmosPipeLayer.Secondary : AtmosPipeLayer.Tertiary; + } + + // Update the construction menu placer + if (pManager.Hijack != null) + UpdateHijackedPlacer(layer, mouseScreen); + + // Otherwise update the debug placer + else + UpdatePlacer(layer); + } + + private void UpdateHijackedPlacer(AtmosPipeLayer layer, ScreenCoordinates mouseScreen) + { + // Try to get alternative prototypes from the construction prototype + var constructionSystem = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentConstructionSystem; + var altPrototypes = (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.AlternativePrototypes; + + if (constructionSystem == null || altPrototypes == null || (int)layer >= altPrototypes.Length) + return; + + var newProtoId = altPrototypes[(int)layer]; + + if (!_protoManager.TryIndex(newProtoId, out var newProto)) + return; + + if (newProto.Type != ConstructionType.Structure) + { + pManager.Clear(); + return; + } + + if (newProto.ID == (pManager.Hijack as ConstructionPlacementHijack)?.CurrentPrototype?.ID) + return; + + // Start placing + pManager.BeginPlacing(new PlacementInformation() + { + IsTile = false, + PlacementOption = newProto.PlacementMode, + }, new ConstructionPlacementHijack(constructionSystem, newProto)); + + if (pManager.CurrentMode is AlignAtmosPipeLayers { } newMode) + newMode.RefreshGrid(mouseScreen); + + // Update construction guide + constructionSystem.GetGuide(newProto); + } + + private void UpdatePlacer(AtmosPipeLayer layer) + { + // Try to get alternative prototypes from the entity atmos pipe layer component + if (pManager.CurrentPermission?.EntityType == null) + return; + + if (!_protoManager.TryIndex(pManager.CurrentPermission.EntityType, out var currentProto)) + return; + + if (!currentProto.TryGetComponent(out var atmosPipeLayers, _entityManager.ComponentFactory)) + return; + + if (!_pipeLayersSystem.TryGetAlternativePrototype(atmosPipeLayers, layer, out var newProtoId)) + return; + + if (_protoManager.TryIndex(newProtoId, out var newProto)) + { + // Update the placed prototype + pManager.CurrentPermission.EntityType = newProtoId; + + // Update the appearance of the ghost sprite + if (newProto.TryGetComponent(out var sprite, _entityManager.ComponentFactory)) + { + var textures = new List(); + + foreach (var spriteLayer in sprite.AllLayers) + { + if (spriteLayer.ActualRsi?.Path != null && spriteLayer.RsiState.Name != null) + textures.Add(_spriteSystem.RsiStateLike(new SpriteSpecifier.Rsi(spriteLayer.ActualRsi.Path, spriteLayer.RsiState.Name))); + } + + pManager.CurrentTextures = textures; + } + } + } + + private void RefreshGrid(ScreenCoordinates mouseScreen) + { + base.AlignPlacementMode(mouseScreen); + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs index c23ebb6435..20902722ff 100644 --- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs @@ -17,6 +17,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl public int? FocusNetId = null; private const int ChunkSize = 4; + private const float ScaleModifier = 4f; + + private readonly float[] _layerFraction = { 0.5f, 0.75f, 0.25f }; + private const float LineThickness = 0.05f; private readonly Color _basePipeNetColor = Color.LightGray; private readonly Color _unfocusedPipeNetColor = Color.DimGray; @@ -95,23 +99,23 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl foreach (var chunkedLine in atmosPipeNetwork) { var leftTop = ScalePosition(new Vector2 - (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, - Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) + (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness, + Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness) - offset); var rightTop = ScalePosition(new Vector2 - (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, - Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f) + (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness, + Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - LineThickness) - offset); var leftBottom = ScalePosition(new Vector2 - (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f, - Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) + (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - LineThickness, + Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness) - offset); var rightBottom = ScalePosition(new Vector2 - (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f, - Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f) + (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + LineThickness, + Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + LineThickness) - offset); if (!pipeVertexUVs.TryGetValue(chunkedLine.Color, out var pipeVertexUV)) @@ -142,7 +146,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl if (chunks == null || grid == null) return decodedOutput; - // Clear stale look up table values + // Clear stale look up table values _horizLines.Clear(); _horizLinesReversed.Clear(); _vertLines.Clear(); @@ -158,7 +162,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl { var list = new List(); - foreach (var ((netId, hexColor), atmosPipeData) in chunk.AtmosPipeData) + foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData) { // Determine the correct coloration for the pipe var color = Color.FromHex(hexColor) * _basePipeNetColor; @@ -191,6 +195,9 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl _vertLinesReversed[color] = vertLinesReversed; } + var layerFraction = _layerFraction[(int)layer]; + var origin = new Vector2(grid.TileSize * layerFraction, -grid.TileSize * layerFraction); + // Loop over the chunk for (var tileIdx = 0; tileIdx < ChunkSize * ChunkSize; tileIdx++) { @@ -208,21 +215,22 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl // Calculate the draw point offsets var vertLineOrigin = (atmosPipeData & northMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? - new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 1f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 1f) : origin; var vertLineTerminus = (atmosPipeData & southMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? - new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + new Vector2(grid.TileSize * layerFraction, -grid.TileSize * 0f) : origin; var horizLineOrigin = (atmosPipeData & eastMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? - new Vector2(grid.TileSize * 1f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + new Vector2(grid.TileSize * 1f, -grid.TileSize * layerFraction) : origin; var horizLineTerminus = (atmosPipeData & westMask << tileIdx * SharedNavMapSystem.Directions) > 0 ? - new Vector2(grid.TileSize * 0f, -grid.TileSize * 0.5f) : new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f); + new Vector2(grid.TileSize * 0f, -grid.TileSize * layerFraction) : origin; - // Since we can have pipe lines that have a length of a half tile, - // double the vectors and convert to vector2i so we can merge them - AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, 2), ConvertVector2ToVector2i(tile + horizLineTerminus, 2), horizLines, horizLinesReversed); - AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, 2), ConvertVector2ToVector2i(tile + vertLineTerminus, 2), vertLines, vertLinesReversed); + // Scale up the vectors and convert to vector2i so we can merge them + AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + horizLineOrigin, ScaleModifier), + ConvertVector2ToVector2i(tile + horizLineTerminus, ScaleModifier), horizLines, horizLinesReversed); + AddOrUpdateNavMapLine(ConvertVector2ToVector2i(tile + vertLineOrigin, ScaleModifier), + ConvertVector2ToVector2i(tile + vertLineTerminus, ScaleModifier), vertLines, vertLinesReversed); } } } @@ -235,7 +243,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl foreach (var (origin, terminal) in horizLines) decodedOutput.Add(new AtmosMonitoringConsoleLine - (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB)); + (ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB)); } foreach (var (color, vertLines) in _vertLines) @@ -245,7 +253,7 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl foreach (var (origin, terminal) in vertLines) decodedOutput.Add(new AtmosMonitoringConsoleLine - (ConvertVector2iToVector2(origin, 0.5f), ConvertVector2iToVector2(terminal, 0.5f), sRGB)); + (ConvertVector2iToVector2(origin, 1f / ScaleModifier), ConvertVector2iToVector2(terminal, 1f / ScaleModifier), sRGB)); } return decodedOutput; diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs index bfbb05d2ab..6a4967e4a4 100644 --- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleSystem.cs @@ -15,7 +15,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS private void OnHandleState(EntityUid uid, AtmosMonitoringConsoleComponent component, ref ComponentHandleState args) { - Dictionary> modifiedChunks; + Dictionary> modifiedChunks; Dictionary atmosDevices; switch (args.Current) @@ -54,7 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS foreach (var (origin, chunk) in modifiedChunks) { var newChunk = new AtmosPipeChunk(origin); - newChunk.AtmosPipeData = new Dictionary<(int, string), ulong>(chunk); + newChunk.AtmosPipeData = new Dictionary(chunk); component.AtmosPipeChunks[origin] = newChunk; } diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs index e25c3af9e9..1a084ea73b 100644 --- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleWindow.xaml.cs @@ -13,6 +13,7 @@ using Robust.Shared.Timing; using Robust.Shared.Utility; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Numerics; namespace Content.Client.Atmos.Consoles; @@ -33,6 +34,8 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow private ProtoId _navMapConsoleProtoId = "NavMapConsole"; private ProtoId _gasPipeSensorProtoId = "GasPipeSensor"; + private readonly Vector2[] _pipeLayerOffsets = { new Vector2(0f, 0f), new Vector2(0.25f, 0.25f), new Vector2(-0.25f, -0.25f) }; + public AtmosMonitoringConsoleWindow(AtmosMonitoringConsoleBoundUserInterface userInterface, EntityUid? owner) { RobustXamlLoader.Load(this); @@ -53,7 +56,7 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow consoleCoords = xform.Coordinates; NavMap.MapUid = xform.GridUid; - // Assign station name + // Assign station name if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData)) stationName = stationMetaData.EntityName; @@ -238,6 +241,10 @@ public sealed partial class AtmosMonitoringConsoleWindow : FancyWindow var blinks = proto.Blinks || _focusEntity == metaData.NetEntity; var coords = _entManager.GetCoordinates(metaData.NetCoordinates); + + if (proto.Placement == NavMapBlipPlacement.Offset && metaData.PipeLayer > 0) + coords = coords.Offset(_pipeLayerOffsets[(int)metaData.PipeLayer]); + var blip = new NavMapBlip(coords, _spriteSystem.Frame0(new SpriteSpecifier.Texture(texture)), color, blinks, proto.Selectable, proto.Scale); NavMap.TrackedEntities[metaData.NetEntity] = blip; } diff --git a/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs index 2029cb9be5..1a12c3967b 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosPipeAppearanceSystem.cs @@ -1,6 +1,7 @@ using Content.Client.SubFloor; using Content.Shared.Atmos; using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.Piping; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -8,7 +9,7 @@ using Robust.Client.GameObjects; namespace Content.Client.Atmos.EntitySystems; [UsedImplicitly] -public sealed class AtmosPipeAppearanceSystem : EntitySystem +public sealed partial class AtmosPipeAppearanceSystem : SharedAtmosPipeAppearanceSystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SpriteSystem _sprite = default!; @@ -26,26 +27,37 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem if (!TryComp(uid, out SpriteComponent? sprite)) return; + var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out _); + foreach (var layerKey in Enum.GetValues()) { - var layer = _sprite.LayerMapReserve((uid, sprite), layerKey); - _sprite.LayerSetRsi((uid, sprite), layer, component.Sprite.RsiPath); - _sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite.RsiState); - _sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey)); + for (byte i = 0; i < numberOfPipeLayers; i++) + { + var layerName = layerKey.ToString() + i.ToString(); + var layer = _sprite.LayerMapReserve((uid, sprite), layerName); + _sprite.LayerSetRsi((uid, sprite), layer, component.Sprite[i].RsiPath); + _sprite.LayerSetRsiState((uid, sprite), layer, component.Sprite[i].RsiState); + _sprite.LayerSetDirOffset((uid, sprite), layer, ToOffset(layerKey)); + } } } - private void HideAllPipeConnection(Entity entity) + private void HideAllPipeConnection(Entity entity, AtmosPipeLayersComponent? atmosPipeLayers, int numberOfPipeLayers) { var sprite = entity.Comp; foreach (var layerKey in Enum.GetValues()) { - if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerKey, out var key, false)) - continue; + for (byte i = 0; i < numberOfPipeLayers; i++) + { + var layerName = layerKey.ToString() + i.ToString(); - var layer = sprite[key]; - layer.Visible = false; + if (!_sprite.LayerMapTryGet(entity.AsNullable(), layerName, out var key, false)) + continue; + + var layer = sprite[key]; + layer.Visible = false; + } } } @@ -61,33 +73,45 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem return; } - if (!_appearance.TryGetData(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component)) + var numberOfPipeLayers = GetNumberOfPipeLayers(uid, out var atmosPipeLayers); + + if (!_appearance.TryGetData(uid, PipeVisuals.VisualState, out var worldConnectedDirections, args.Component)) { - HideAllPipeConnection((uid, args.Sprite)); + HideAllPipeConnection((uid, args.Sprite), atmosPipeLayers, numberOfPipeLayers); return; } if (!_appearance.TryGetData(uid, PipeColorVisuals.Color, out var color, args.Component)) color = Color.White; - // transform connected directions to local-coordinates - var connectedDirections = worldConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation); - - foreach (var layerKey in Enum.GetValues()) + for (byte i = 0; i < numberOfPipeLayers; i++) { - if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerKey, out var key, false)) - continue; + // Extract the cardinal pipe orientations for the current pipe layer + // '15' is the four bit mask that is used to extract the pipe orientations of interest from 'worldConnectedDirections' + // Fun fact: a collection of four bits is called a 'nibble'! They aren't natively supported :( + var pipeLayerConnectedDirections = (PipeDirection)(15 & (worldConnectedDirections >> (PipeDirectionHelpers.PipeDirections * i))); - var layer = args.Sprite[key]; - var dir = (PipeDirection)layerKey; - var visible = connectedDirections.HasDirection(dir); + // Transform the connected directions to local-coordinates + var connectedDirections = pipeLayerConnectedDirections.RotatePipeDirection(-Transform(uid).LocalRotation); - layer.Visible &= visible; + foreach (var layerKey in Enum.GetValues()) + { + var layerName = layerKey.ToString() + i.ToString(); - if (!visible) - continue; + if (!_sprite.LayerMapTryGet((uid, args.Sprite), layerName, out var key, false)) + continue; - layer.Color = color; + var layer = args.Sprite[key]; + var dir = (PipeDirection)layerKey; + var visible = connectedDirections.HasDirection(dir); + + layer.Visible &= visible; + + if (!visible) + continue; + + layer.Color = color; + } } } diff --git a/Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs new file mode 100644 index 0000000000..f560e0b833 --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/AtmosPipeLayersSystem.cs @@ -0,0 +1,56 @@ +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.EntitySystems; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; +using Robust.Shared.Reflection; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Client.Atmos.EntitySystems; + +/// +/// The system responsible for updating the appearance of layered gas pipe +/// +public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly IReflectionManager _reflection = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAppearanceChange); + } + + private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent ev) + { + if (!TryComp(ent, out var sprite)) + return; + + if (_appearance.TryGetData(ent, AtmosPipeLayerVisuals.Sprite, out var spriteRsi) && + _resourceCache.TryGetResource(SpriteSpecifierSerializer.TextureRoot / spriteRsi, out RSIResource? resource)) + { + _sprite.SetBaseRsi((ent, sprite), resource.RSI); + } + + if (_appearance.TryGetData>(ent, AtmosPipeLayerVisuals.SpriteLayers, out var pipeState)) + { + foreach (var (layerKey, rsiPath) in pipeState) + { + if (TryParseKey(layerKey, out var @enum)) + _sprite.LayerSetRsi((ent, sprite), @enum, new ResPath(rsiPath)); + else + _sprite.LayerSetRsi((ent, sprite), layerKey, new ResPath(rsiPath)); + } + } + } + + private bool TryParseKey(string keyString, [NotNullWhen(true)] out Enum? @enum) + { + return _reflection.TryParseEnumReference(keyString, out @enum); + } +} diff --git a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs index bd221eb4bf..431a598678 100644 --- a/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs +++ b/Content.Client/Atmos/EntitySystems/FireVisualizerSystem.cs @@ -11,7 +11,6 @@ namespace Content.Client.Atmos.EntitySystems; /// public sealed class FireVisualizerSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; [Dependency] private readonly PointLightSystem _lights = default!; public override void Initialize() @@ -33,9 +32,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem(uid, out var sprite) && - _sprite.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false)) + SpriteSystem.LayerMapTryGet((uid, sprite), FireVisualLayers.Fire, out var layer, false)) { - _sprite.RemoveLayer((uid, sprite), layer); + SpriteSystem.RemoveLayer((uid, sprite), layer); } } @@ -44,11 +43,11 @@ public sealed class FireVisualizerSystem : VisualizerSystem(uid, out var sprite) || !TryComp(uid, out AppearanceComponent? appearance)) return; - _sprite.LayerMapReserve((uid, sprite), FireVisualLayers.Fire); - _sprite.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false); + SpriteSystem.LayerMapReserve((uid, sprite), FireVisualLayers.Fire); + SpriteSystem.LayerSetVisible((uid, sprite), FireVisualLayers.Fire, false); sprite.LayerSetShader(FireVisualLayers.Fire, "unshaded"); if (component.Sprite != null) - _sprite.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite)); + SpriteSystem.LayerSetRsi((uid, sprite), FireVisualLayers.Fire, new ResPath(component.Sprite)); UpdateAppearance(uid, component, sprite, appearance); } @@ -61,12 +60,12 @@ public sealed class FireVisualizerSystem : VisualizerSystem(uid, FireVisuals.OnFire, out var onFire, appearance); AppearanceSystem.TryGetData(uid, FireVisuals.FireStacks, out var fireStacks, appearance); - _sprite.LayerSetVisible((uid, sprite), index, onFire); + SpriteSystem.LayerSetVisible((uid, sprite), index, onFire); if (!onFire) { @@ -80,9 +79,9 @@ public sealed class FireVisualizerSystem : VisualizerSystem component.FireStackAlternateState && !string.IsNullOrEmpty(component.AlternateState)) - _sprite.LayerSetRsiState((uid, sprite), index, component.AlternateState); + SpriteSystem.LayerSetRsiState((uid, sprite), index, component.AlternateState); else - _sprite.LayerSetRsiState((uid, sprite), index, component.NormalState); + SpriteSystem.LayerSetRsiState((uid, sprite), index, component.NormalState); component.LightEntity ??= Spawn(null, new EntityCoordinates(uid, default)); var light = EnsureComp(component.LightEntity.Value); diff --git a/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs b/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs index 6d3a373759..472e1e3d34 100644 --- a/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs +++ b/Content.Client/Atmos/Monitor/AtmosAlarmableVisualsSystem.cs @@ -7,11 +7,9 @@ namespace Content.Client.Atmos.Monitor; public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, AtmosAlarmableVisualsComponent component, ref AppearanceChangeEvent args) { - if (args.Sprite == null || !_sprite.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false)) + if (args.Sprite == null || !SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.LayerMap, out var layer, false)) return; if (!args.AppearanceData.TryGetValue(PowerDeviceVisuals.Powered, out var poweredObject) || @@ -24,8 +22,8 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem public sealed class PortableScrubberSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, PortableScrubberVisualsComponent component, ref AppearanceChangeEvent args) { if (args.Sprite == null) @@ -20,15 +18,15 @@ public sealed class PortableScrubberSystem : VisualizerSystem(uid, PortableScrubberVisuals.IsRunning, out var isRunning, args.Component)) { var runningState = isRunning ? component.RunningState : component.IdleState; - _sprite.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), PortableScrubberVisualLayers.IsRunning, runningState); var fullState = isFull ? component.FullState : component.ReadyState; - _sprite.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), PowerDeviceVisualLayers.Powered, fullState); } if (AppearanceSystem.TryGetData(uid, PortableScrubberVisuals.IsDraining, out var isDraining, args.Component)) { - _sprite.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining); + SpriteSystem.LayerSetVisible((uid, args.Sprite), PortableScrubberVisualLayers.IsDraining, isDraining); } } } diff --git a/Content.Client/Botany/PlantHolderVisualizerSystem.cs b/Content.Client/Botany/PlantHolderVisualizerSystem.cs index 7050abb39f..bd5b5e56dc 100644 --- a/Content.Client/Botany/PlantHolderVisualizerSystem.cs +++ b/Content.Client/Botany/PlantHolderVisualizerSystem.cs @@ -7,8 +7,6 @@ namespace Content.Client.Botany; public sealed class PlantHolderVisualizerSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - public override void Initialize() { base.Initialize(); @@ -20,8 +18,8 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem(uid, out var sprite)) return; - _sprite.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant); - _sprite.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false); + SpriteSystem.LayerMapReserve((uid, sprite), PlantHolderLayers.Plant); + SpriteSystem.LayerSetVisible((uid, sprite), PlantHolderLayers.Plant, false); } protected override void OnAppearanceChange(EntityUid uid, PlantHolderVisualsComponent component, ref AppearanceChangeEvent args) @@ -34,12 +32,12 @@ public sealed class PlantHolderVisualizerSystem : VisualizerSystem(uid, TypingIndicatorVisuals.State, out var state); - _sprite.LayerSetVisible((uid, args.Sprite), layer, state != TypingIndicatorState.None); + SpriteSystem.LayerSetVisible((uid, args.Sprite), layer, state != TypingIndicatorState.None); switch (state) { case TypingIndicatorState.Idle: - _sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.IdleState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.IdleState); break; case TypingIndicatorState.Typing: - _sprite.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), layer, proto.TypingState); break; } } diff --git a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs index 00a29563c1..06bc790696 100644 --- a/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs +++ b/Content.Client/Chemistry/Visualizers/FoamVisualizerSystem.cs @@ -11,7 +11,6 @@ namespace Content.Client.Chemistry.Visualizers; public sealed class FoamVisualizerSystem : VisualizerSystem { [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() { @@ -72,7 +71,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem(uid, out var sprite)) - _sprite.SetVisible((uid, sprite), false); + SpriteSystem.SetVisible((uid, sprite), false); } } diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs index 2baef6a535..6fc07785b2 100644 --- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs +++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs @@ -16,7 +16,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem(uid, SolutionContainerVisuals.BaseOverride, out var baseOverride, @@ -80,35 +79,35 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem 0) { - _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true); + SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true); maxFillLevels = reagentProto.MetamorphicMaxFillLevels; fillBaseName = reagentProto.MetamorphicFillBaseName; changeColor = reagentProto.MetamorphicChangeColor; fillSprite = sprite; } else - _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, false); + SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false); if (hasOverlay) - _sprite.LayerSetVisible((uid, args.Sprite), overlayLayer, false); + SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, false); } else { - _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true); + SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true); if (hasOverlay) - _sprite.LayerSetVisible((uid, args.Sprite), overlayLayer, true); + SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, true); if (component.MetamorphicDefaultSprite != null) - _sprite.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite); + SpriteSystem.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite); } } } } else { - _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, true); + SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true); } var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, maxFillLevels + 1); @@ -120,25 +119,25 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem(uid, SolutionContainerVisuals.Color, out var color, args.Component)) - _sprite.LayerSetColor((uid, args.Sprite), fillLayer, color); + SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, color); else - _sprite.LayerSetColor((uid, args.Sprite), fillLayer, Color.White); + SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White); } else { if (component.EmptySpriteName == null) - _sprite.LayerSetVisible((uid, args.Sprite), fillLayer, false); + SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false); else { - _sprite.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName); if (changeColor) - _sprite.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor); + SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor); else - _sprite.LayerSetColor((uid, args.Sprite), fillLayer, Color.White); + SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White); } } diff --git a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs index bde6a4b99a..74fcf48849 100644 --- a/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Client/Clothing/Systems/ChameleonClothingSystem.cs @@ -13,16 +13,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem { [Dependency] private readonly IPrototypeManager _proto = default!; - private static readonly SlotFlags[] IgnoredSlots = - { - SlotFlags.All, - SlotFlags.PREVENTEQUIP, - SlotFlags.NONE - }; - private static readonly SlotFlags[] Slots = Enum.GetValues().Except(IgnoredSlots).ToArray(); - - private readonly Dictionary> _data = new(); - public override void Initialize() { base.Initialize(); @@ -61,49 +51,4 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem borderColor.AccentVColor = otherBorderColor.AccentVColor; } } - - /// - /// Get a list of valid chameleon targets for these slots. - /// - public IEnumerable GetValidTargets(SlotFlags slot) - { - var set = new HashSet(); - foreach (var availableSlot in _data.Keys) - { - if (slot.HasFlag(availableSlot)) - { - set.UnionWith(_data[availableSlot]); - } - } - return set; - } - - private void PrepareAllVariants() - { - _data.Clear(); - var prototypes = _proto.EnumeratePrototypes(); - - foreach (var proto in prototypes) - { - // check if this is valid clothing - if (!IsValidTarget(proto)) - continue; - if (!proto.TryGetComponent(out ClothingComponent? item, Factory)) - continue; - - // sort item by their slot flags - // one item can be placed in several buckets - foreach (var slot in Slots) - { - if (!item.Slots.HasFlag(slot)) - continue; - - if (!_data.ContainsKey(slot)) - { - _data.Add(slot, new List()); - } - _data[slot].Add(proto.ID); - } - } - } } diff --git a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs index bd86ffbec0..876f300e50 100644 --- a/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs +++ b/Content.Client/Clothing/UI/ChameleonBoundUserInterface.cs @@ -42,7 +42,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface var targets = _chameleon.GetValidTargets(st.Slot); if (st.RequiredTag != null) { - var newTargets = new List(); + var newTargets = new List(); foreach (var target in targets) { if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto)) diff --git a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs index bd45be4510..c6dce10776 100644 --- a/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs +++ b/Content.Client/Clothing/UI/ChameleonMenu.xaml.cs @@ -19,8 +19,8 @@ public sealed partial class ChameleonMenu : DefaultWindow private readonly SpriteSystem _sprite; public event Action? OnIdSelected; - private IEnumerable _possibleIds = Enumerable.Empty(); - private string? _selectedId; + private IEnumerable _possibleIds = []; + private EntProtoId? _selectedId; private string _searchFilter = ""; public ChameleonMenu() @@ -32,7 +32,7 @@ public sealed partial class ChameleonMenu : DefaultWindow Search.OnTextChanged += OnSearchEntered; } - public void UpdateState(IEnumerable possibleIds, string? selectedId) + public void UpdateState(IEnumerable possibleIds, string? selectedId) { _possibleIds = possibleIds; _selectedId = selectedId; @@ -57,7 +57,7 @@ public sealed partial class ChameleonMenu : DefaultWindow if (!_prototypeManager.TryIndex(id, out EntityPrototype? proto)) continue; - var lowId = id.ToLowerInvariant(); + var lowId = id.Id.ToLowerInvariant(); var lowName = proto.Name.ToLowerInvariant(); if (!lowId.Contains(searchFilterLow) && !lowName.Contains(_searchFilter)) continue; diff --git a/Content.Client/Construction/ConstructionPlacementHijack.cs b/Content.Client/Construction/ConstructionPlacementHijack.cs index e6a8e0f1f0..79112a8f8e 100644 --- a/Content.Client/Construction/ConstructionPlacementHijack.cs +++ b/Content.Client/Construction/ConstructionPlacementHijack.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Shared.Construction.Prototypes; using Robust.Client.GameObjects; using Robust.Client.Placement; @@ -13,6 +13,9 @@ namespace Content.Client.Construction private readonly ConstructionSystem _constructionSystem; private readonly ConstructionPrototype? _prototype; + public ConstructionSystem? CurrentConstructionSystem { get { return _constructionSystem; } } + public ConstructionPrototype? CurrentPrototype { get { return _prototype; } } + public override bool CanRotate { get; } public ConstructionPlacementHijack(ConstructionSystem constructionSystem, ConstructionPrototype? prototype) diff --git a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs index 2d94034bb9..1b83f5ed03 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs @@ -131,7 +131,7 @@ namespace Content.Client.ContextMenu.UI { if (!Menus.TryPeek(out var topMenu)) { - Logger.Error("Context Menu: Mouse entered menu without any open menus?"); + Log.Error("Context Menu: Mouse entered menu without any open menus?"); return; } @@ -181,7 +181,7 @@ namespace Content.Client.ContextMenu.UI { if (!Menus.TryPeek(out var topMenu)) { - Logger.Error("Context Menu: Attempting to open sub menu without any open menus?"); + Log.Error("Context Menu: Attempting to open sub menu without any open menus?"); return; } diff --git a/Content.Client/ContextMenu/UI/EntityMenuUIController.cs b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs index bda831394d..e0a88300db 100644 --- a/Content.Client/ContextMenu/UI/EntityMenuUIController.cs +++ b/Content.Client/ContextMenu/UI/EntityMenuUIController.cs @@ -306,7 +306,7 @@ namespace Content.Client.ContextMenu.UI // find the element associated with this entity if (!Elements.TryGetValue(entity, out var element)) { - Logger.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent(entity).EntityName} ({entity})"); + Log.Error($"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent(entity).EntityName} ({entity})"); return; } diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs index b38e70f6cc..de866ca9a4 100644 --- a/Content.Client/Damage/DamageVisualsSystem.cs +++ b/Content.Client/Damage/DamageVisualsSystem.cs @@ -27,7 +27,6 @@ namespace Content.Client.Damage; public sealed class DamageVisualsSystem : VisualizerSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() { @@ -229,7 +228,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem private void AddDamageLayerToSprite(Entity spriteEnt, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null) { - var newLayer = _sprite.AddLayer( + var newLayer = SpriteSystem.AddLayer( spriteEnt, new SpriteSpecifier.Rsi( new(sprite.Sprite), state ), index ); - _sprite.LayerMapSet(spriteEnt, mapKey, newLayer); + SpriteSystem.LayerMapSet(spriteEnt, mapKey, newLayer); if (sprite.Color != null) - _sprite.LayerSetColor(spriteEnt, newLayer, Color.FromHex(sprite.Color)); - _sprite.LayerSetVisible(spriteEnt, newLayer, false); + SpriteSystem.LayerSetColor(spriteEnt, newLayer, Color.FromHex(sprite.Color)); + SpriteSystem.LayerSetVisible(spriteEnt, newLayer, false); } protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent damageVisComp, ref AppearanceChangeEvent args) @@ -410,7 +409,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem spriteEnt, DamageVisualsComponent damageVisComp, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold) { - _sprite.LayerMapTryGet(spriteEnt.AsNullable(), key, out var spriteLayer, false); + SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), key, out var spriteLayer, false); var visibility = spriteEnt.Comp[spriteLayer].Visible; - _sprite.RemoveLayer(spriteEnt.AsNullable(), spriteLayer); + SpriteSystem.RemoveLayer(spriteEnt.AsNullable(), spriteLayer); if (threshold == FixedPoint2.Zero) // these should automatically be invisible threshold = damageVisComp.Thresholds[1]; - spriteLayer = _sprite.AddLayer( + spriteLayer = SpriteSystem.AddLayer( spriteEnt.AsNullable(), new SpriteSpecifier.Rsi( new(sprite.Sprite), $"{statePrefix}_{threshold}" ), spriteLayer); - _sprite.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer); - _sprite.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility); + SpriteSystem.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer); + SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility); // this is somewhat iffy since it constantly reallocates damageVisComp.TopMostLayerKey = key; } @@ -610,7 +609,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem private void UpdateOverlay(Entity spriteEnt, FixedPoint2 threshold) { - _sprite.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false); + SpriteSystem.LayerMapTryGet(spriteEnt.AsNullable(), $"DamageOverlay", out var spriteLayer, false); UpdateDamageLayerState(spriteEnt, spriteLayer, @@ -690,7 +689,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, DamageStateVisualsComponent component, ref AppearanceChangeEvent args) { var sprite = args.Sprite; @@ -25,18 +23,18 @@ public sealed class DamageStateVisualizerSystem : VisualizerSystem (int)DrawDepth.DeadMobs) { component.OriginalDrawDepth = sprite.DrawDepth; - _sprite.SetDrawDepth((uid, sprite), (int)DrawDepth.DeadMobs); + SpriteSystem.SetDrawDepth((uid, sprite), (int)DrawDepth.DeadMobs); } } else if (component.OriginalDrawDepth != null) { - _sprite.SetDrawDepth((uid, sprite), component.OriginalDrawDepth.Value); + SpriteSystem.SetDrawDepth((uid, sprite), component.OriginalDrawDepth.Value); component.OriginalDrawDepth = null; } } diff --git a/Content.Client/Decals/DecalPlacementSystem.cs b/Content.Client/Decals/DecalPlacementSystem.cs index a4495042c6..db00534a38 100644 --- a/Content.Client/Decals/DecalPlacementSystem.cs +++ b/Content.Client/Decals/DecalPlacementSystem.cs @@ -2,6 +2,7 @@ using System.Numerics; using Content.Client.Actions; using Content.Client.Decals.Overlays; using Content.Shared.Actions; +using Content.Shared.Actions.Components; using Content.Shared.Decals; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -21,9 +22,12 @@ public sealed class DecalPlacementSystem : EntitySystem [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly InputSystem _inputSystem = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + public static readonly EntProtoId DecalAction = "BaseMappingDecalAction"; + private string? _decalId; private Color _decalColor = Color.White; private Angle _decalAngle = Angle.Zero; @@ -152,19 +156,12 @@ public sealed class DecalPlacementSystem : EntitySystem Cleanable = _cleanable, }; - var actionId = Spawn(null); - AddComp(actionId, new WorldTargetActionComponent - { - // non-unique actions may be considered duplicates when saving/loading. - Icon = decalProto.Sprite, - Repeat = true, - ClientExclusive = true, - CheckCanAccess = false, - CheckCanInteract = false, - Range = -1, - Event = actionEvent, - IconColor = _decalColor, - }); + var actionId = Spawn(DecalAction); + var action = Comp(actionId); + var ent = (actionId, action); + _actions.SetEvent(actionId, actionEvent); + _actions.SetIcon(ent, decalProto.Sprite); + _actions.SetIconColor(ent, _decalColor); _metaData.SetEntityName(actionId, $"{_decalId} ({_decalColor.ToHex()}, {(int) _decalAngle.Degrees})"); diff --git a/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs b/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs index 7fc81219f7..65b70727c0 100644 --- a/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs +++ b/Content.Client/Electrocution/ElectrocutionHUDVisualizerSystem.cs @@ -11,7 +11,6 @@ namespace Content.Client.Electrocution; public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem { [Dependency] private readonly IPlayerManager _playerMan = default!; - [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() { @@ -59,7 +58,7 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem(uid, ElectrifiedVisuals.IsElectrified, out var electrified, appearanceComp)) continue; - _sprite.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, electrified); + SpriteSystem.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, electrified); } } @@ -70,7 +69,7 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem(); while (electrifiedQuery.MoveNext(out var uid, out _, out _, out var spriteComp)) { - _sprite.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, false); + SpriteSystem.LayerSetVisible((uid, spriteComp), ElectrifiedLayers.HUD, false); } } @@ -84,6 +83,6 @@ public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem(player)); + SpriteSystem.LayerSetVisible((uid, args.Sprite), ElectrifiedLayers.HUD, electrified && HasComp(player)); } } diff --git a/Content.Client/Ensnaring/Components/EnsnaringComponent.cs b/Content.Client/Ensnaring/Components/EnsnaringComponent.cs deleted file mode 100644 index 63c8d0dfbe..0000000000 --- a/Content.Client/Ensnaring/Components/EnsnaringComponent.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Content.Shared.Ensnaring.Components; - - diff --git a/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs index d0b39abb37..fee397b27d 100644 --- a/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideEntityEmbed.xaml.cs @@ -28,12 +28,15 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag { [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntitySystemManager _systemManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IUserInterfaceManager _ui = default!; private readonly TagSystem _tagSystem; private readonly ExamineSystem _examineSystem; private readonly GuidebookSystem _guidebookSystem; + private readonly ISawmill _sawmill; + public bool Interactive; public Entity? Sprite => View.Entity == null || View.Sprite == null @@ -53,6 +56,7 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag _tagSystem = _systemManager.GetEntitySystem(); _examineSystem = _systemManager.GetEntitySystem(); _guidebookSystem = _systemManager.GetEntitySystem(); + _sawmill = _logManager.GetSawmill("guidebook.entity"); MouseFilter = MouseFilterMode.Stop; } @@ -135,7 +139,7 @@ public sealed partial class GuideEntityEmbed : BoxContainer, IDocumentTag { if (!args.TryGetValue("Entity", out var proto)) { - Logger.Error("Entity embed tag is missing entity prototype argument"); + _sawmill.Error("Entity embed tag is missing entity prototype argument"); control = null; return false; } diff --git a/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs index da93fb46fd..d6f20d5a25 100644 --- a/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideMicrowaveEmbed.xaml.cs @@ -24,7 +24,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly ILogManager _logManager = default!; - private ISawmill _sawmill = default!; + private readonly ISawmill _sawmill = default!; public IPrototype? RepresentedPrototype { get; private set; } @@ -34,7 +34,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag, IoCManager.InjectDependencies(this); MouseFilter = MouseFilterMode.Stop; - _sawmill = _logManager.GetSawmill("guidemicrowaveembed"); + _sawmill = _logManager.GetSawmill("guidebook.microwave"); } public GuideMicrowaveEmbed(string recipe) : this() diff --git a/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs b/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs index 098e99459c..7c2a0ecfe1 100644 --- a/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs +++ b/Content.Client/Guidebook/Controls/GuideMicrowaveGroupEmbed.cs @@ -15,12 +15,16 @@ namespace Content.Client.Guidebook.Controls; [UsedImplicitly] public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTag { + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + private readonly ISawmill _sawmill; + public GuideMicrowaveGroupEmbed() { Orientation = LayoutOrientation.Vertical; IoCManager.InjectDependencies(this); + _sawmill = _logManager.GetSawmill("guidebook.microwave_group"); MouseFilter = MouseFilterMode.Stop; } @@ -34,7 +38,7 @@ public sealed partial class GuideMicrowaveGroupEmbed : BoxContainer, IDocumentTa control = null; if (!args.TryGetValue("Group", out var group)) { - Logger.Error("Microwave group embed tag is missing group argument"); + _sawmill.Error("Microwave group embed tag is missing group argument"); return false; } diff --git a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs index 78cd765bdb..29569e40e6 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs @@ -25,9 +25,11 @@ namespace Content.Client.Guidebook.Controls; public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISearchableControl, IPrototypeRepresentationControl { [Dependency] private readonly IEntitySystemManager _systemManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; private readonly ChemistryGuideDataSystem _chemistryGuideData; + private readonly ISawmill _sawmill; public IPrototype? RepresentedPrototype { get; private set; } @@ -35,6 +37,7 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _sawmill = _logManager.GetSawmill("guidebook.reagent"); _chemistryGuideData = _systemManager.GetEntitySystem(); MouseFilter = MouseFilterMode.Stop; } @@ -64,13 +67,13 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea control = null; if (!args.TryGetValue("Reagent", out var id)) { - Logger.Error("Reagent embed tag is missing reagent prototype argument"); + _sawmill.Error("Reagent embed tag is missing reagent prototype argument"); return false; } if (!_prototype.TryIndex(id, out var reagent)) { - Logger.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype"); + _sawmill.Error($"Specified reagent prototype \"{id}\" is not a valid reagent prototype"); return false; } diff --git a/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs index 0c9356eccb..5373034b42 100644 --- a/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideReagentGroupEmbed.xaml.cs @@ -17,12 +17,16 @@ namespace Content.Client.Guidebook.Controls; [UsedImplicitly, GenerateTypedNameReferences] public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag { + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + private readonly ISawmill _sawmill; + public GuideReagentGroupEmbed() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _sawmill = _logManager.GetSawmill("guidebook.reagent_group"); MouseFilter = MouseFilterMode.Stop; } @@ -42,7 +46,7 @@ public sealed partial class GuideReagentGroupEmbed : BoxContainer, IDocumentTag control = null; if (!args.TryGetValue("Group", out var group)) { - Logger.Error("Reagent group embed tag is missing group argument"); + _sawmill.Error("Reagent group embed tag is missing group argument"); return false; } diff --git a/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs index 88d264cb05..a01f2a8f17 100644 --- a/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideTechDisciplineEmbed.xaml.cs @@ -17,12 +17,16 @@ namespace Content.Client.Guidebook.Controls; [UsedImplicitly, GenerateTypedNameReferences] public sealed partial class GuideTechDisciplineEmbed : BoxContainer, IDocumentTag { + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; + private readonly ISawmill _sawmill; + public GuideTechDisciplineEmbed() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); + _sawmill = _logManager.GetSawmill("guidebook.tech_discipline"); MouseFilter = MouseFilterMode.Stop; } @@ -42,7 +46,7 @@ public sealed partial class GuideTechDisciplineEmbed : BoxContainer, IDocumentTa control = null; if (!args.TryGetValue("Discipline", out var group)) { - Logger.Error("Technology discipline embed tag is missing discipline argument"); + _sawmill.Error("Technology discipline embed tag is missing discipline argument"); return false; } diff --git a/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs b/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs index d61cc2d961..7d205f7cea 100644 --- a/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuideTechnologyEmbed.xaml.cs @@ -22,10 +22,12 @@ namespace Content.Client.Guidebook.Controls; public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, ISearchableControl { [Dependency] private readonly IEntitySystemManager _systemManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; private readonly ResearchSystem _research; private readonly SpriteSystem _sprite; + private readonly ISawmill _sawmill; public GuideTechnologyEmbed() { @@ -33,6 +35,7 @@ public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, I IoCManager.InjectDependencies(this); _research = _systemManager.GetEntitySystem(); _sprite = _systemManager.GetEntitySystem(); + _sawmill = _logManager.GetSawmill("guidebook.technology"); MouseFilter = MouseFilterMode.Stop; } @@ -61,13 +64,13 @@ public sealed partial class GuideTechnologyEmbed : BoxContainer, IDocumentTag, I control = null; if (!args.TryGetValue("Technology", out var id)) { - Logger.Error("Technology embed tag is missing technology prototype argument"); + _sawmill.Error("Technology embed tag is missing technology prototype argument"); return false; } if (!_prototype.TryIndex(id, out var technology)) { - Logger.Error($"Specified technology prototype \"{id}\" is not a valid technology prototype"); + _sawmill.Error($"Specified technology prototype \"{id}\" is not a valid technology prototype"); return false; } diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs index 8cc4c491f4..716e3e2034 100644 --- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs +++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs @@ -35,7 +35,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - _sawmill = Logger.GetSawmill("Guidebook"); + _sawmill = Logger.GetSawmill("guidebook"); Tree.OnSelectedItemChanged += OnSelectionChanged; @@ -232,7 +232,7 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler, IA { // TODO GUIDEBOOK Maybe allow duplicate entries? // E.g., for adding medicine under both chemicals & the chemist job - Logger.Error($"Adding duplicate guide entry: {id}"); + _sawmill.Error($"Adding duplicate guide entry: {id}"); return null; } diff --git a/Content.Client/Implants/ChameleonControllerSystem.cs b/Content.Client/Implants/ChameleonControllerSystem.cs new file mode 100644 index 0000000000..7db4b37ef2 --- /dev/null +++ b/Content.Client/Implants/ChameleonControllerSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Implants; + +namespace Content.Client.Implants; + +public sealed partial class ChameleonControllerSystem : SharedChameleonControllerSystem; diff --git a/Content.Client/Implants/UI/ChameleonControllerBoundUserInterface.cs b/Content.Client/Implants/UI/ChameleonControllerBoundUserInterface.cs new file mode 100644 index 0000000000..42b891ff50 --- /dev/null +++ b/Content.Client/Implants/UI/ChameleonControllerBoundUserInterface.cs @@ -0,0 +1,49 @@ +using Content.Shared.Clothing; +using Content.Shared.Implants; +using Content.Shared.Preferences.Loadouts; +using Content.Shared.Roles; +using Content.Shared.Timing; +using JetBrains.Annotations; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; + +namespace Content.Client.Implants.UI; + +[UsedImplicitly] +public sealed class ChameleonControllerBoundUserInterface : BoundUserInterface +{ + private readonly UseDelaySystem _delay; + + [ViewVariables] + private ChameleonControllerMenu? _menu; + + public ChameleonControllerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + _delay = EntMan.System(); + } + + protected override void Open() + { + base.Open(); + + _menu = this.CreateWindow(); + _menu.OnJobSelected += OnJobSelected; + } + + private void OnJobSelected(ProtoId outfit) + { + if (!EntMan.TryGetComponent(Owner, out var useDelayComp)) + return; + + if (!_delay.TryResetDelay((Owner, useDelayComp), true)) + return; + + SendMessage(new ChameleonControllerSelectedOutfitMessage(outfit)); + + if (!_delay.TryGetDelayInfo((Owner, useDelayComp), out var delay) || _menu == null) + return; + + _menu._lockedUntil = DateTime.Now.Add(delay.Length); + _menu.UpdateGrid(true); + } +} diff --git a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml new file mode 100644 index 0000000000..39322a2991 --- /dev/null +++ b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs new file mode 100644 index 0000000000..a41e2e9293 --- /dev/null +++ b/Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs @@ -0,0 +1,157 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Roles; +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Implants; +using Content.Shared.StatusIcon; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Implants.UI; + +[GenerateTypedNameReferences] +public sealed partial class ChameleonControllerMenu : FancyWindow +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + private readonly SpriteSystem _sprite; + private readonly JobSystem _job; + + // List of all the job protos that you can select! + private IEnumerable _outfits; + + // Lock the UI until this time + public DateTime? _lockedUntil; + + private static readonly ProtoId UnknownIcon = "JobIconUnknown"; + private static readonly LocId UnknownDepartment = "department-Unknown"; + + public event Action>? OnJobSelected; + + public ChameleonControllerMenu() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _sprite = _entityManager.System(); + _job = _entityManager.System(); + + _outfits = _prototypeManager.EnumeratePrototypes(); + + UpdateGrid(); + } + + /// + /// Fill the grid with the correct job icons and buttons. + /// + /// Set to true to disable all the buttons. + public void UpdateGrid(bool disabled = false) + { + Grid.RemoveAllChildren(); + + // Dictionary to easily put outfits in departments. + // Department name -> UI element holding that department. + var departments = new Dictionary(); + + departments.Add(UnknownDepartment, CreateDepartment(UnknownDepartment)); + + // Go through every outfit and add them to the correct department. + foreach (var outfit in _outfits) + { + _prototypeManager.TryIndex(outfit.Job, out var jobProto); + + var name = outfit.LoadoutName ?? outfit.Name ?? jobProto?.Name ?? "Prototype has no name or job."; + + var jobIconId = outfit.Icon ?? jobProto?.Icon ?? UnknownIcon; + var jobIconProto = _prototypeManager.Index(jobIconId); + + var outfitButton = CreateOutfitButton(disabled, name, jobIconProto, outfit.ID); + + if (outfit.Job != null && _job.TryGetLowestWeightDepartment(outfit.Job, out var departmentPrototype)) + { + if (!departments.ContainsKey(departmentPrototype.Name)) + departments.Add(departmentPrototype.Name, CreateDepartment(departmentPrototype.Name)); + + departments[departmentPrototype.Name].AddChild(outfitButton); + } + else + { + departments[UnknownDepartment].AddChild(outfitButton); + } + } + + // Sort the departments by their weight. + var departmentList = departments.ToList(); + departmentList.Sort((a, b) => a.Value.ChildCount.CompareTo(b.Value.ChildCount)); + + // Actually add the departments to the window. + foreach (var department in departmentList) + { + Grid.AddChild(department.Value); + } + } + + private BoxContainer CreateDepartment(string name) + { + var departmentContainer = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical, + }; + departmentContainer.AddChild(new Label + { + Text = Loc.GetString(name), + }); + + return departmentContainer; + } + + private BoxContainer CreateOutfitButton(bool disabled, string name, JobIconPrototype jobIconProto, ProtoId outfitProto) + { + var outfitButton = new BoxContainer(); + + var button = new Button + { + HorizontalExpand = true, + StyleClasses = {StyleBase.ButtonSquare}, + ToolTip = Loc.GetString(name), + Text = Loc.GetString(name), + Margin = new Thickness(0, 0, 15, 0), + Disabled = disabled, + }; + + var jobIconTexture = new TextureRect + { + Texture = _sprite.Frame0(jobIconProto.Icon), + TextureScale = new Vector2(2.5f, 2.5f), + Stretch = TextureRect.StretchMode.KeepCentered, + Margin = new Thickness(0, 0, 5, 0), + }; + + outfitButton.AddChild(jobIconTexture); + outfitButton.AddChild(button); + + button.OnPressed += _ => JobButtonPressed(outfitProto); + + return outfitButton; + } + + private void JobButtonPressed(ProtoId outfit) + { + OnJobSelected?.Invoke(outfit); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (_lockedUntil == null || DateTime.Now < _lockedUntil) + return; + + _lockedUntil = null; + UpdateGrid(); + } +} diff --git a/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs b/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs index 11ccc86550..fcbba97a9c 100644 --- a/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs +++ b/Content.Client/Light/EntitySystems/EmergencyLightSystem.cs @@ -6,8 +6,6 @@ namespace Content.Client.Light.EntitySystems; public sealed class EmergencyLightSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, EmergencyLightComponent comp, ref AppearanceChangeEvent args) { if (args.Sprite == null) @@ -16,13 +14,13 @@ public sealed class EmergencyLightSystem : VisualizerSystem(uid, EmergencyLightVisuals.On, out var on, args.Component)) on = false; - _sprite.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, !on); - _sprite.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, on); + SpriteSystem.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, !on); + SpriteSystem.LayerSetVisible((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, on); if (AppearanceSystem.TryGetData(uid, EmergencyLightVisuals.Color, out var color, args.Component)) { - _sprite.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, color); - _sprite.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, color); + SpriteSystem.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOn, color); + SpriteSystem.LayerSetColor((uid, args.Sprite), EmergencyLightVisualLayers.LightOff, color); } } } diff --git a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs index bc4c85cca7..e47430677d 100644 --- a/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs +++ b/Content.Client/Light/EntitySystems/ExpendableLightSystem.cs @@ -10,7 +10,6 @@ public sealed class ExpendableLightSystem : VisualizerSystem { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, LightBulbComponent comp, ref AppearanceChangeEvent args) { if (args.Sprite == null) @@ -18,13 +16,13 @@ public sealed class LightBulbSystem : VisualizerSystem switch (state) { case LightBulbState.Normal: - _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.NormalSpriteState); break; case LightBulbState.Broken: - _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BrokenSpriteState); break; case LightBulbState.Burned: - _sprite.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), LightBulbVisualLayers.Base, comp.BurnedSpriteState); break; } } @@ -32,7 +30,7 @@ public sealed class LightBulbSystem : VisualizerSystem // also update sprites color if (AppearanceSystem.TryGetData(uid, LightBulbVisuals.Color, out var color, args.Component)) { - _sprite.SetColor((uid, args.Sprite), color); + SpriteSystem.SetColor((uid, args.Sprite), color); } } } diff --git a/Content.Client/Light/HandheldLightSystem.cs b/Content.Client/Light/HandheldLightSystem.cs index d25b28756f..2a5aa949ff 100644 --- a/Content.Client/Light/HandheldLightSystem.cs +++ b/Content.Client/Light/HandheldLightSystem.cs @@ -44,7 +44,7 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem return; } - if (!_appearance.TryGetData(uid, ToggleableLightVisuals.Enabled, out var enabled, args.Component)) + if (!_appearance.TryGetData(uid, ToggleableVisuals.Enabled, out var enabled, args.Component)) { return; } diff --git a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs index ba83e69af7..7ed1fb2dde 100644 --- a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs +++ b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs @@ -11,7 +11,6 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem(uid, out var light)) { - _sprite.LayerSetColor((uid, args.Sprite), PoweredLightLayers.Glow, light.Color); + SpriteSystem.LayerSetColor((uid, args.Sprite), PoweredLightLayers.Glow, light.Color); } - _sprite.LayerSetVisible((uid, args.Sprite), PoweredLightLayers.Glow, state == PoweredLightState.On); + SpriteSystem.LayerSetVisible((uid, args.Sprite), PoweredLightLayers.Glow, state == PoweredLightState.On); } SetBlinkingAnimation( diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs index cea8248e9f..121e8dbe71 100644 --- a/Content.Client/Lobby/LobbyUIController.cs +++ b/Content.Client/Lobby/LobbyUIController.cs @@ -32,7 +32,6 @@ public sealed class LobbyUIController : UIController, IOnStateEntered { - [Dependency] private readonly SpriteSystem _sprite = default!; - protected override void OnAppearanceChange(EntityUid uid, LockVisualsComponent comp, ref AppearanceChangeEvent args) { if (args.Sprite == null @@ -22,14 +20,14 @@ public sealed class LockVisualizerSystem : VisualizerSystem(uid, StorageVisuals.Open, out var open, args.Component)) { - _sprite.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, !open); + SpriteSystem.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, !open); } else if (!(bool)unlockedStateExist!) - _sprite.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, locked); + SpriteSystem.LayerSetVisible((uid, args.Sprite), LockVisualLayers.Lock, locked); if (!open && (bool)unlockedStateExist!) { - _sprite.LayerSetRsiState((uid, args.Sprite), LockVisualLayers.Lock, locked ? comp.StateLocked : comp.StateUnlocked); + SpriteSystem.LayerSetRsiState((uid, args.Sprite), LockVisualLayers.Lock, locked ? comp.StateLocked : comp.StateUnlocked); } } } diff --git a/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs new file mode 100644 index 0000000000..8fe4f3d386 --- /dev/null +++ b/Content.Client/Machines/Components/MultipartMachineGhostComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Client.Machines.Components; + +/// +/// Component attached to all multipart machine ghosts +/// Intended for client side usage only, but used on prototypes. +/// +[RegisterComponent] +public sealed partial class MultipartMachineGhostComponent : Component +{ + /// + /// Machine this particular ghost is linked to. + /// + public EntityUid? LinkedMachine = null; +} diff --git a/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs new file mode 100644 index 0000000000..4919a5e8f2 --- /dev/null +++ b/Content.Client/Machines/EntitySystems/MultipartMachineSystem.cs @@ -0,0 +1,109 @@ +using Content.Client.Examine; +using Content.Client.Machines.Components; +using Content.Shared.Machines.Components; +using Content.Shared.Machines.EntitySystems; +using Robust.Client.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Spawners; + +namespace Content.Client.Machines.EntitySystems; + +/// +/// Client side handling of multipart machines. +/// Handles client side examination events to show the expected layout of the machine +/// based on the origin of the main entity. +/// +public sealed class MultipartMachineSystem : SharedMultipartMachineSystem +{ + private readonly EntProtoId _ghostPrototype = "MultipartMachineGhost"; + private readonly Color _partiallyTransparent = new Color(255, 255, 255, 180); + + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly ISerializationManager _serialization= default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMachineExamined); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnGhostDespawned); + } + + /// + /// Handles spawning several ghost sprites to show where the different parts of the machine + /// should go and the rotations they're expected to have. + /// Can only show one set of ghost parts at a time and their location depends on the current map/grid + /// location of the origin machine. + /// + /// Entity/Component that has been inspected. + /// Args for the event. + private void OnMachineExamined(Entity ent, ref ClientExaminedEvent args) + { + if (ent.Comp.Ghosts.Count != 0) + { + // Already showing some part ghosts + return; + } + + foreach (var part in ent.Comp.Parts.Values) + { + if (part.Entity.HasValue) + continue; + + var entityCoords = new EntityCoordinates(ent.Owner, part.Offset); + var ghostEnt = Spawn(_ghostPrototype, entityCoords); + + if (!XformQuery.TryGetComponent(ghostEnt, out var xform)) + break; + + xform.LocalRotation = part.Rotation; + + Comp(ghostEnt).LinkedMachine = ent; + + ent.Comp.Ghosts.Add(ghostEnt); + + if (part.GhostProto == null) + continue; + + var entProto = _prototype.Index(part.GhostProto.Value); + if (!entProto.Components.TryGetComponent("Sprite", out var s) || s is not SpriteComponent protoSprite) + return; + + var ghostSprite = EnsureComp(ghostEnt); + _serialization.CopyTo(protoSprite, ref ghostSprite, notNullableOverride: true); + + _sprite.SetColor((ghostEnt, ghostSprite), _partiallyTransparent); + + _metaData.SetEntityName(ghostEnt, entProto.Name); + _metaData.SetEntityDescription(ghostEnt, entProto.Description); + } + } + + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + foreach (var part in ent.Comp.Parts.Values) + { + part.Entity = part.NetEntity.HasValue ? EnsureEntity(part.NetEntity.Value, ent) : null; + } + } + + /// + /// Handles when a ghost part despawns after its short lifetime. + /// Will attempt to remove itself from the list of known ghost entities in the main multipart + /// machine component. + /// + /// Ghost entity that has been despawned. + /// Args for the event. + private void OnGhostDespawned(Entity ent, ref TimedDespawnEvent args) + { + if (!TryComp(ent.Comp.LinkedMachine, out var machine)) + return; + + machine.Ghosts.Remove(ent); + } +} diff --git a/Content.Client/Mapping/MappingSystem.cs b/Content.Client/Mapping/MappingSystem.cs index 80189fbdfc..627977a526 100644 --- a/Content.Client/Mapping/MappingSystem.cs +++ b/Content.Client/Mapping/MappingSystem.cs @@ -4,8 +4,8 @@ using Content.Shared.Mapping; using Content.Shared.Maps; using Robust.Client.Placement; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; -using static Robust.Shared.Utility.SpriteSpecifier; namespace Content.Client.Mapping; @@ -14,16 +14,10 @@ public sealed partial class MappingSystem : EntitySystem [Dependency] private readonly IPlacementManager _placementMan = default!; [Dependency] private readonly ITileDefinitionManager _tileMan = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; - /// - /// The icon to use for space tiles. - /// - private readonly SpriteSpecifier _spaceIcon = new Texture(new ("Tiles/cropped_parallax.png")); - - /// - /// The icon to use for entity-eraser. - /// - private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png")); + public static readonly EntProtoId SpawnAction = "BaseMappingSpawnAction"; + public static readonly EntProtoId EraserAction = "ActionMappingEraser"; public override void Initialize() { @@ -38,90 +32,46 @@ public sealed partial class MappingSystem : EntitySystem /// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd /// prefer if it were to function more like DecalPlacementSystem. /// - private void OnFillActionSlot(FillActionSlotEvent ev) + private void OnFillActionSlot(FillActionSlotEvent args) { if (!_placementMan.IsActive) return; - if (ev.Action != null) + if (args.Action != null) return; - var actionEvent = new StartPlacementActionEvent(); - ITileDefinition? tileDef = null; - - if (_placementMan.CurrentPermission != null) + if (_placementMan.CurrentPermission is {} permission) { - actionEvent.EntityType = _placementMan.CurrentPermission.EntityType; - actionEvent.PlacementOption = _placementMan.CurrentPermission.PlacementOption; + var ev = new StartPlacementActionEvent() + { + EntityType = permission.EntityType, + PlacementOption = permission.PlacementOption, + }; + var action = Spawn(SpawnAction); if (_placementMan.CurrentPermission.IsTile) { - tileDef = _tileMan[_placementMan.CurrentPermission.TileType]; - actionEvent.TileId = tileDef.ID; + if (_tileMan[_placementMan.CurrentPermission.TileType] is not ContentTileDefinition tileDef) + return; + + if (!tileDef.MapAtmosphere && tileDef.Sprite is {} sprite) + _actions.SetIcon(action, new SpriteSpecifier.Texture(sprite)); + ev.TileId = tileDef.ID; + _metaData.SetEntityName(action, Loc.GetString(tileDef.Name)); } + else if (permission.EntityType is {} id) + { + _actions.SetIcon(action, new SpriteSpecifier.EntityPrototype(id)); + _metaData.SetEntityName(action, id); + } + + _actions.SetEvent(action, ev); + args.Action = action; } else if (_placementMan.Eraser) { - actionEvent.Eraser = true; + args.Action = Spawn(EraserAction); } - else - return; - - InstantActionComponent action; - string name; - - if (tileDef != null) - { - if (tileDef is not ContentTileDefinition contentTileDef) - return; - - var tileIcon = contentTileDef.MapAtmosphere - ? _spaceIcon - : new Texture(contentTileDef.Sprite!.Value); - - action = new InstantActionComponent - { - ClientExclusive = true, - CheckCanInteract = false, - Event = actionEvent, - Icon = tileIcon - }; - - name = Loc.GetString(tileDef.Name); - } - else if (actionEvent.Eraser) - { - action = new InstantActionComponent - { - ClientExclusive = true, - CheckCanInteract = false, - Event = actionEvent, - Icon = _deleteIcon, - }; - - name = Loc.GetString("action-name-mapping-erase"); - } - else - { - if (string.IsNullOrWhiteSpace(actionEvent.EntityType)) - return; - - action = new InstantActionComponent - { - ClientExclusive = true, - CheckCanInteract = false, - Event = actionEvent, - Icon = new EntityPrototype(actionEvent.EntityType), - }; - - name = actionEvent.EntityType; - } - - var actionId = Spawn(null); - AddComp(actionId, action); - _metaData.SetEntityName(actionId, name); - - ev.Action = actionId; } private void OnStartPlacementAction(StartPlacementActionEvent args) diff --git a/Content.Client/Movement/Systems/JetpackSystem.cs b/Content.Client/Movement/Systems/JetpackSystem.cs index 6810bb24cc..bf80ed4252 100644 --- a/Content.Client/Movement/Systems/JetpackSystem.cs +++ b/Content.Client/Movement/Systems/JetpackSystem.cs @@ -34,10 +34,6 @@ public sealed class JetpackSystem : SharedJetpackSystem { Appearance.TryGetData(uid, JetpackVisuals.Enabled, out var enabled, args.Component); - var state = "icon" + (enabled ? "-on" : ""); - if (args.Sprite != null) - _sprite.LayerSetRsiState((uid, args.Sprite), 0, state); - if (TryComp(uid, out var clothing)) _clothing.SetEquippedPrefix(uid, enabled ? "on" : null, clothing); } diff --git a/Content.Client/Options/UI/OptionColorSlider.xaml b/Content.Client/Options/UI/OptionColorSlider.xaml new file mode 100644 index 0000000000..4f5f082350 --- /dev/null +++ b/Content.Client/Options/UI/OptionColorSlider.xaml @@ -0,0 +1,7 @@ + + + + diff --git a/Content.Client/Options/UI/OptionColorSlider.xaml.cs b/Content.Client/Options/UI/OptionColorSlider.xaml.cs new file mode 100644 index 0000000000..6f8f46a3b4 --- /dev/null +++ b/Content.Client/Options/UI/OptionColorSlider.xaml.cs @@ -0,0 +1,31 @@ +using Content.Client.Options.UI; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; + +namespace Content.Client.Options.UI; + +/// +/// Standard UI control used for color sliders in the options menu. Intended for use with . +/// +/// +[GenerateTypedNameReferences] +public sealed partial class OptionColorSlider : Control +{ + /// + /// The text describing what this slider affects. + /// + public string? Title + { + get => TitleLabel.Text; + set => TitleLabel.Text = value; + } + + /// + /// The example text showing the current color of the slider. + /// + public string? Example + { + get => ExampleLabel.Text; + set => ExampleLabel.Text = value; + } +} diff --git a/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs b/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs index 31dd9897f4..ad262f94a2 100644 --- a/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs +++ b/Content.Client/Options/UI/OptionsTabControlRow.xaml.cs @@ -121,6 +121,19 @@ public sealed partial class OptionsTabControlRow : Control return AddOption(new OptionSliderFloatCVar(this, _cfg, cVar, slider, min, max, scale, FormatPercent)); } + /// + /// Add a color slider option, backed by a simple string CVar. + /// + /// The CVar represented by the slider. + /// The UI control for the option. + /// The option instance backing the added option. + public OptionColorSliderCVar AddOptionColorSlider( + CVarDef cVar, + OptionColorSlider slider) + { + return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider)); + } + /// /// Add a slider option, backed by a simple integer CVar. /// @@ -518,6 +531,58 @@ public sealed class OptionSliderFloatCVar : BaseOptionCVar } } +/// +/// Implementation of a CVar option that simply corresponds with a string . +/// +/// +public sealed class OptionColorSliderCVar : BaseOptionCVar +{ + private readonly OptionColorSlider _slider; + + protected override string Value + { + get => _slider.Slider.Color.ToHex(); + set + { + _slider.Slider.Color = Color.FromHex(value); + UpdateLabelColor(); + } + } + + /// + /// Creates a new instance of this type. + /// + /// + /// + /// It is generally more convenient to call overloads on + /// such as instead of instantiating this type directly. + /// + /// + /// The control row that owns this option. + /// The configuration manager to get and set values from. + /// The CVar that is being controlled by this option. + /// The UI control for the option. + public OptionColorSliderCVar( + OptionsTabControlRow controller, + IConfigurationManager cfg, + CVarDef cVar, + OptionColorSlider slider) : base(controller, cfg, cVar) + { + _slider = slider; + + slider.Slider.OnColorChanged += _ => + { + ValueChanged(); + UpdateLabelColor(); + }; + } + + private void UpdateLabelColor() + { + _slider.ExampleLabel.FontColorOverride = Color.FromHex(Value); + } +} + /// /// Implementation of a CVar option that simply corresponds with an integer . /// diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml index 5041b498a0..41fac83c59 100644 --- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml +++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml @@ -14,6 +14,10 @@ + +