using Content.Client.Resources; using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.Access; using Content.Shared.Access.Systems; using Content.Shared.TurretController; using Content.Shared.Turrets; using Robust.Client.AutoGenerated; using Robust.Client.Player; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Player; using Robust.Shared.Prototypes; using System.Linq; using System.Numerics; namespace Content.Client.TurretController; [GenerateTypedNameReferences] public sealed partial class TurretControllerWindow : BaseWindow { [Dependency] private IEntityManager _entManager = default!; [Dependency] private IPrototypeManager _protoManager = default!; [Dependency] private IPlayerManager _playerManager = default!; private readonly IResourceCache _cache; private readonly AccessReaderSystem _accessReaderSystem; private EntityUid? _owner; private int _tabIndex = 0; // Button groups private readonly ButtonGroup _armamentButtons = new(); private readonly ButtonGroup _accessGroupsButtons = new(); // Temp values private List _checkBoxes = new(); private HashSet _accessLevelsForTab = new(); private List _accessLevelEntries = new(); // Events private event Action? OnAccessGroupChangedEvent; public event Action>, bool>? OnAccessLevelsChangedEvent; public event Action? OnArmamentSettingChangedEvent; // Colors private Color[] _themeColors = [Color.FromHex("#33e633"), Color.FromHex("#dfb827"), Color.FromHex("#da2a2a")]; public TurretControllerWindow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _cache = IoCManager.Resolve(); _accessReaderSystem = _entManager.System(); CloseButton.OnPressed += _ => Close(); XamlChildren = ContentsContainer.Children; OnAccessGroupChangedEvent += OnAccessGroupChanged; var smallFont = _cache.NotoStack(size: 8); Footer.FontOverride = smallFont; } private void Initialize() { if (_owner == null) return; // Set up armament buttons SafeButton.OnToggled += args => OnArmamentButtonPressed(SafeButton, -1); StunButton.OnToggled += args => OnArmamentButtonPressed(StunButton, 0); LethalButton.OnToggled += args => OnArmamentButtonPressed(LethalButton, 1); SafeButton.Group = _armamentButtons; StunButton.Group = _armamentButtons; LethalButton.Group = _armamentButtons; SafeButton.Label.AddStyleClass("ConsoleText"); StunButton.Label.AddStyleClass("ConsoleText"); LethalButton.Label.AddStyleClass("ConsoleText"); // Refresh UI RefreshLinkedTurrets(new()); if (_entManager.TryGetComponent(_owner, out var turretController)) UpdateTheme(turretController.ArmamentState); if (_entManager.TryGetComponent(_owner, out var turretTargetSettings)) RefreshAccessControls(turretTargetSettings.ExemptAccessLevels); } private void OnArmamentButtonPressed(Button pressedButton, int index) { UpdateTheme(index); OnArmamentSettingChangedEvent?.Invoke(index); } private void UpdateTheme(int index) { switch (index) { case -1: SafeButton.Pressed = true; break; case 0: StunButton.Pressed = true; break; case 1: LethalButton.Pressed = true; break; } var canInteract = IsLocalPlayerAllowedToInteract(); SafeButton.Disabled = !SafeButton.Pressed && !canInteract; StunButton.Disabled = !StunButton.Pressed && !canInteract; LethalButton.Disabled = !LethalButton.Pressed && !canInteract; var shiftedIndex = index + 1; if (shiftedIndex >= 0 && shiftedIndex < _themeColors.Length) ContentsContainer.Modulate = _themeColors[shiftedIndex]; } public void SetOwner(EntityUid owner) { _owner = owner; Initialize(); } public void UpdateState(DeployableTurretControllerBoundInterfaceState state) { if (_entManager.TryGetComponent(_owner, out var turretController)) UpdateTheme(turretController.ArmamentState); if (_entManager.TryGetComponent(_owner, out var turretTargetSettings)) RefreshAccessControls(turretTargetSettings.ExemptAccessLevels); RefreshLinkedTurrets(state.TurretStates); } public void UpdateMessage(DeployableTurretControllerBoundInterfaceMessage message) { RefreshLinkedTurrets(message.TurretStates); } public void RefreshLinkedTurrets(List<(string, string)> turretStates) { var turretCount = turretStates.Count; var hasTurrets = turretCount > 0; NoLinkedTurretsText.Visible = !hasTurrets; LinkedTurretsContainer.Visible = hasTurrets; LinkedTurretsContainer.RemoveAllChildren(); foreach (var turretState in turretStates) { var box = new BoxContainer() { HorizontalExpand = true, }; var label = new Label() { Text = Loc.GetString("turret-controls-window-turret-status", ("device", turretState.Item1), ("status", Loc.GetString(turretState.Item2))), HorizontalAlignment = HAlignment.Left, Margin = new Thickness(10f, 0f, 10f, 0f), HorizontalExpand = true, SetHeight = 20f, }; label.AddStyleClass("ConsoleText"); box.AddChild(label); LinkedTurretsContainer.AddChild(box); } TurretStatusHeader.Text = Loc.GetString("turret-controls-window-turret-status-label", ("count", turretCount)); } public void RefreshAccessControls(HashSet> exemptAccessLevels) { if (_owner == null) return; if (!_entManager.TryGetComponent(_owner, out var turretControls)) return; var canInteract = IsLocalPlayerAllowedToInteract(); // Create a list of known access groups with which to populate the UI var groupedAccessLevels = new Dictionary>(); foreach (var accessGroup in turretControls.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 turretControls.AccessLevels) { if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto)) continue; IEnumerable associatedGroups = groupedAccessLevels.Keys.Where(x => x.Tags.Contains(accessLevelProto.ID) == true); if (!associatedGroups.Any() && generalAccessProto != null) groupedAccessLevels[generalAccessProto].Add(accessLevelProto); else { foreach (var group in associatedGroups) groupedAccessLevels[group].Add(accessLevelProto); } } // Remove access groups that have no assigned access levels foreach (var (group, accessLevels) in groupedAccessLevels) { if (accessLevels.Count == 0) groupedAccessLevels.Remove(group); } // Did something go wrong...? if (groupedAccessLevels.Count == 0) { AccessGroupList.DisposeAllChildren(); AccessLevelGrid.DisposeAllChildren(); return; } // Adjust the current tab index so it remains in range if (_tabIndex >= groupedAccessLevels.Count) _tabIndex = groupedAccessLevels.Count - 1; // Reorder the access groups alphabetically var orderedAccessGroups = groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList(); // Remove excess group access buttons from the UI while (AccessGroupList.ChildCount > orderedAccessGroups.Count) AccessGroupList.RemoveChild(orderedAccessGroups.Count - 1); // Add missing group access buttons to the UI while (AccessGroupList.ChildCount < orderedAccessGroups.Count) { var monotoneButton = new MonotoneButton { ToggleMode = true, }; AccessGroupList.AddChild(monotoneButton); // Add button styling monotoneButton.Label.AddStyleClass("ConsoleText"); monotoneButton.Label.HorizontalAlignment = HAlignment.Left; monotoneButton.Group = _accessGroupsButtons; var childIndex = AccessGroupList.ChildCount - 1; if (orderedAccessGroups.Count > 1) { if (childIndex == 0) monotoneButton.Shape = MonotoneButtonShape.OpenLeft; else if (orderedAccessGroups.Count > 1 && childIndex == (orderedAccessGroups.Count - 1)) monotoneButton.Shape = MonotoneButtonShape.OpenRight; else monotoneButton.Shape = MonotoneButtonShape.OpenBoth; } // Add button events monotoneButton.OnPressed += _ => { OnAccessGroupChangedEvent?.Invoke(monotoneButton.GetPositionInParent()); }; } // Update the group access buttons for (int i = 0; i < orderedAccessGroups.Count; i++) { if (AccessGroupList.GetChild(i) is not Button { } accessGroupButton) continue; var accessGroup = orderedAccessGroups[i]; var prefix = groupedAccessLevels[accessGroup].Any(x => exemptAccessLevels.Contains(x)) ? "ยป" : " "; accessGroupButton.Text = Loc.GetString("turret-controls-window-access-group-label", ("prefix", prefix), ("label", accessGroup.GetAccessGroupName())); accessGroupButton.Pressed = _tabIndex == orderedAccessGroups.IndexOf(accessGroup); } // Get the access levels associated with the current tab _accessLevelsForTab = groupedAccessLevels[orderedAccessGroups[_tabIndex]]; _accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToHashSet(); // Remove excess access level buttons from the UI // Note: if _accessLevelsForTab is length 'n', AccessLevelGrid should have 'n + 1' children at the end while (AccessLevelGrid.ChildCount > (_accessLevelsForTab.Count + 1)) { var index = AccessLevelGrid.ChildCount - 1; if (AccessLevelGrid.GetChild(AccessLevelGrid.ChildCount - 1) is AccessLevelEntry { } accessLevelEntry) _accessLevelEntries.Remove(accessLevelEntry); AccessLevelGrid.RemoveChild(index); } // Add an 'all' checkbox as the first child of the list if it hasn't been initalized yet // Toggling this checkbox on will mark all other boxes below it on/off if (AccessLevelGrid.ChildCount == 0) { var checkBox = new MonotoneCheckBox { Text = Loc.GetString("turret-controls-window-all-checkbox"), Margin = new Thickness(0, 0, 0, 3), ToggleMode = true, ReservesSpace = false, }; AccessLevelGrid.AddChild(checkBox); // Add checkbox styling checkBox.Label.AddStyleClass("ConsoleText"); // Add checkbox events checkBox.OnPressed += args => { SetCheckBoxPressedState(_checkBoxes, checkBox.Pressed); var accessLevels = new HashSet>(); foreach (var accessLevel in _accessLevelsForTab) accessLevels.Add(accessLevel); OnAccessLevelsChangedEvent?.Invoke(accessLevels, checkBox.Pressed); }; } // Hide the 'all' checkbox if the tab has only one access level var allCheckBoxVisible = _accessLevelsForTab.Count > 1; // Did something go wrong...? if (AccessLevelGrid.GetChild(0) is not CheckBox { } allCheckBox) return; allCheckBox.Visible = allCheckBoxVisible; allCheckBox.Disabled = !canInteract; // Add any remaining missing access level buttons to the UI while (AccessLevelGrid.ChildCount < (_accessLevelsForTab.Count + 1)) { var accessLevelEntry = new AccessLevelEntry(); AccessLevelGrid.AddChild(accessLevelEntry); _accessLevelEntries.Add(accessLevelEntry); // 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 => (CheckBox)x.CheckBox)); OnAccessLevelsChangedEvent?.Invoke (new HashSet>() { accessLevelEntry.AccessLevel }, accessLevelEntry.CheckBox.Pressed); }; } // Update the access levels buttons' appearance for (int i = 0; i < _accessLevelEntries.Count; i++) { var accessLevel = _accessLevelsForTab.ElementAt(i); var accessLevelEntry = _accessLevelEntries[i]; accessLevelEntry.AccessLevel = accessLevel; accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName(); accessLevelEntry.CheckBox.Pressed = exemptAccessLevels.Contains(accessLevel); var isEndOfList = i == (_accessLevelEntries.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; accessLevelEntry.CheckBox.Disabled = !canInteract; } // 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(IEnumerable checkBoxes, bool pressed) { foreach (var checkBox in checkBoxes) checkBox.Pressed = pressed; } protected override DragMode GetDragModeFor(Vector2 relativeMousePos) { return DragMode.Move; } private void OnAccessGroupChanged(int newTabIndex) { if (newTabIndex == _tabIndex) return; _tabIndex = newTabIndex; if (_entManager.TryGetComponent(_owner, out var turretTargetSettings)) RefreshAccessControls(turretTargetSettings.ExemptAccessLevels); } private bool IsLocalPlayerAllowedToInteract() { if (_owner == null || _playerManager.LocalSession?.AttachedEntity == null) return false; return _accessReaderSystem.IsAllowed(_playerManager.LocalSession.AttachedEntity.Value, _owner.Value); } private sealed class AccessLevelEntry : BoxContainer { public ProtoId AccessLevel = default!; public MonotoneCheckBox CheckBox; public LineRenderer CheckBoxLink; public AccessLevelEntry() { HorizontalExpand = true; var lines = new List<(Vector2, Vector2)>() { (new Vector2(0,0), new Vector2(0,0)), (new Vector2(0,0), new Vector2(0,0)) }; CheckBoxLink = new LineRenderer(lines) { SetWidth = 22, VerticalExpand = true, Margin = new Thickness(0, -1), ReservesSpace = false, }; AddChild(CheckBoxLink); CheckBox = new MonotoneCheckBox { ToggleMode = true, Margin = new Thickness(0f, 0f, 0f, 3f), }; AddChild(CheckBox); CheckBox.Label.AddStyleClass("ConsoleText"); } public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines) { CheckBoxLink.Lines = lines; } } }