diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 1b854e6905..5bae4a1336 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -1,33 +1,17 @@ -using Content.Client.Actions.Assignments; -using Content.Client.Actions.UI; -using Content.Client.Construction; -using Content.Client.DragDrop; -using Content.Client.Hands; -using Content.Client.Items.Managers; -using Content.Client.Outline; -using Content.Client.Popups; +using System.IO; using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; -using Content.Shared.Input; using JetBrains.Annotations; using Robust.Client.GameObjects; -using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Client.UserInterface; -using Robust.Client.Utility; -using Robust.Shared.Audio; using Robust.Shared.ContentPack; using Robust.Shared.GameStates; -using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.Player; 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.Utility; -using System.IO; -using System.Linq; using YamlDotNet.RepresentationModel; namespace Content.Client.Actions @@ -35,268 +19,84 @@ namespace Content.Client.Actions [UsedImplicitly] public sealed class ActionsSystem : SharedActionsSystem { - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IUserInterfaceManager _uiManager = default!; - [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; - [Dependency] private readonly ISerializationManager _serializationManager = default!; - [Dependency] private readonly IResourceManager _resourceManager = default!; - [Dependency] private readonly IOverlayManager _overlayMan = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly InteractionOutlineSystem _interactionOutline = default!; - [Dependency] private readonly TargetOutlineSystem _targetOutline = default!; + [Dependency] private readonly IResourceManager _resources = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; - // TODO Redo assignments, including allowing permanent user configurable slot assignments. - /// - /// Current assignments for all hotbars / slots for this entity. - /// - public ActionAssignments Assignments = new(Hotbars, Slots); + public event Action? OnActionAdded; + public event Action? OnActionRemoved; + public event Action? OnLinkActions; + public event Action? OnUnlinkActions; + public event Action? ClearAssignments; + public event Action>? AssignSlot; - public const byte Hotbars = 9; - public const byte Slots = 10; - - public bool UIDirty; - - public ActionsUI? Ui; - private EntityUid? _highlightedEntity; + public ActionsComponent? PlayerActions { get; private set; } public override void Initialize() { base.Initialize(); - - // set up hotkeys for hotbar - CommandBinds.Builder - .Bind(ContentKeyFunctions.OpenActionsMenu, - InputCmdHandler.FromDelegate(_ => ToggleActionsMenu())) - .Bind(ContentKeyFunctions.Hotbar1, - HandleHotbarKeybind(0)) - .Bind(ContentKeyFunctions.Hotbar2, - HandleHotbarKeybind(1)) - .Bind(ContentKeyFunctions.Hotbar3, - HandleHotbarKeybind(2)) - .Bind(ContentKeyFunctions.Hotbar4, - HandleHotbarKeybind(3)) - .Bind(ContentKeyFunctions.Hotbar5, - HandleHotbarKeybind(4)) - .Bind(ContentKeyFunctions.Hotbar6, - HandleHotbarKeybind(5)) - .Bind(ContentKeyFunctions.Hotbar7, - HandleHotbarKeybind(6)) - .Bind(ContentKeyFunctions.Hotbar8, - HandleHotbarKeybind(7)) - .Bind(ContentKeyFunctions.Hotbar9, - HandleHotbarKeybind(8)) - .Bind(ContentKeyFunctions.Hotbar0, - HandleHotbarKeybind(9)) - .Bind(ContentKeyFunctions.Loadout1, - HandleChangeHotbarKeybind(0)) - .Bind(ContentKeyFunctions.Loadout2, - HandleChangeHotbarKeybind(1)) - .Bind(ContentKeyFunctions.Loadout3, - HandleChangeHotbarKeybind(2)) - .Bind(ContentKeyFunctions.Loadout4, - HandleChangeHotbarKeybind(3)) - .Bind(ContentKeyFunctions.Loadout5, - HandleChangeHotbarKeybind(4)) - .Bind(ContentKeyFunctions.Loadout6, - HandleChangeHotbarKeybind(5)) - .Bind(ContentKeyFunctions.Loadout7, - HandleChangeHotbarKeybind(6)) - .Bind(ContentKeyFunctions.Loadout8, - HandleChangeHotbarKeybind(7)) - .Bind(ContentKeyFunctions.Loadout9, - HandleChangeHotbarKeybind(8)) - // when selecting a target, we intercept clicks in the game world, treating them as our target selection. We want to - // take priority before any other systems handle the click. - .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true), - typeof(ConstructionSystem), typeof(DragDropSystem)) - .BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true)) - .Register(); - SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); - SubscribeLocalEvent(HandleState); + SubscribeLocalEvent(HandleComponentState); } - public override void Dirty(ActionType action) + private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args) { - // Should only ever receive component states for attached player's component. - // --> lets not bother unnecessarily dirtying and prediction-resetting actions for other players. - if (action.AttachedEntity != _playerManager.LocalPlayer?.ControlledEntity) + if (args.Current is not ActionsComponentState currentState) return; - base.Dirty(action); - UIDirty = true; - } + List added = new(); + List removed = new(); - private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args) - { - if (args.Current is not ActionsComponentState state) - return; - - var serverActions = new SortedSet(state.Actions); - - foreach (var act in component.Actions.ToList()) + foreach (var actionType in component.Actions) { - if (act.ClientExclusive) - continue; - - if (!serverActions.TryGetValue(act, out var serverAct)) + if (!currentState.Actions.Contains(actionType)) { - component.Actions.Remove(act); - if (act.AutoRemove && !(Ui?.Locked ?? false)) - Assignments.Remove(act); - continue; + removed.Add(actionType); } - - act.CopyFrom(serverAct); - serverActions.Remove(serverAct); } - // Anything that remains is a new action - foreach (var newAct in serverActions) + foreach (var serverAction in currentState.Actions) { - // We create a new action, not just sorting a reference to the state's action. - component.Actions.Add((ActionType) newAct.Clone()); - } - - UIDirty = true; - } - - /// - /// Highlights the item slot (inventory or hand) that contains this item - /// - /// - public void HighlightItemSlot(EntityUid item) - { - StopHighlightingItemSlot(); - - _highlightedEntity = item; - _itemSlotManager.HighlightEntity(item); - } - - /// - /// Stops highlighting any item slots we are currently highlighting. - /// H - public void StopHighlightingItemSlot() - { - if (_highlightedEntity == null) - return; - - _itemSlotManager.UnHighlightEntity(_highlightedEntity.Value); - _highlightedEntity = null; - } - - protected override void AddActionInternal(ActionsComponent comp, ActionType action) - { - // Sometimes the client receives actions from the server, before predicting that newly added components will add - // their own shared actions. Just in case those systems ever decided to directly access action properties (e.g., - // action.Toggled), we will remove duplicates: - if (comp.Actions.TryGetValue(action, out var existing)) - { - comp.Actions.Remove(existing); - Assignments.Replace(existing, action); - } - - comp.Actions.Add(action); - } - - public override void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true) - { - if (!Resolve(uid, ref comp, false)) - return; - - base.AddAction(uid, action, provider, comp, dirty); - - - if (uid == _playerManager.LocalPlayer?.ControlledEntity) - UIDirty = true; - } - - public override void RemoveActions(EntityUid uid, IEnumerable actions, ActionsComponent? comp = null, bool dirty = true) - { - if (uid != _playerManager.LocalPlayer?.ControlledEntity) - return; - - if (!Resolve(uid, ref comp, false)) - return; - - base.RemoveActions(uid, actions, comp, dirty); - - foreach (var act in actions) - { - if (act.AutoRemove && !(Ui?.Locked ?? false)) - Assignments.Remove(act); - } - - UIDirty = true; - } - - public override void FrameUpdate(float frameTime) - { - // avoid updating GUI when doing predictions & resetting state. - if (UIDirty) - { - UIDirty = false; - UpdateUI(); - } - } - - /// - /// Updates the displayed hotbar (and menu) based on current state of actions. - /// - public void UpdateUI() - { - if (Ui == null) - return; - - foreach (var action in Ui.Component.Actions) - { - if (action.AutoPopulate && !Assignments.Assignments.ContainsKey(action)) - Assignments.AutoPopulate(action, Ui.SelectedHotbar, false); - } - - // get rid of actions that are no longer available to the user - foreach (var (action, index) in Assignments.Assignments.ToList()) - { - if (index.Count == 0) + if (!component.Actions.TryGetValue(serverAction, out var clientAction)) { - Assignments.Assignments.Remove(action); - continue; + added.Add((ActionType) serverAction.Clone()); + } + else + { + clientAction.CopyFrom(serverAction); } - - if (action.AutoRemove && !Ui.Locked && !Ui.Component.Actions.Contains(action)) - Assignments.ClearSlot(index[0].Hotbar, index[0].Slot, false); } - Assignments.PreventAutoPopulate.RemoveWhere(action => !Ui.Component.Actions.Contains(action)); + foreach (var actionType in added) + { + component.Actions.Add(actionType); + OnActionAdded?.Invoke(actionType); + } - Ui.UpdateUI(); - } - - public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args) - { - Ui?.HandleHotbarKeybind(slot, args); - } - - public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args) - { - Ui?.HandleChangeHotbarKeybind(hotbar, args); - } - - private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent args) - { - if (Ui == null) return; - _uiManager.StateRoot.RemoveChild(Ui); - Ui = null; + foreach (var actionType in removed) + { + component.Actions.Remove(actionType); + OnActionRemoved?.Invoke(actionType); + } } private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args) { - Assignments = new(Hotbars, Slots); - Ui = new ActionsUI(this, component); - _uiManager.StateRoot.AddChild(Ui); - UIDirty = true; + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + OnLinkActions?.Invoke(component); + PlayerActions = component; + } + + private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent? args = null) + { + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + OnUnlinkActions?.Invoke(); + PlayerActions = null; } public override void Shutdown() @@ -305,70 +105,25 @@ namespace Content.Client.Actions CommandBinds.Unregister(); } - private PointerInputCmdHandler HandleHotbarKeybind(byte slot) + public void TriggerAction(ActionType? action) { - // delegate to the ActionsUI, simulating a click on it - return new((in PointerInputCmdHandler.PointerInputCmdArgs args) => - { - var playerEntity = _playerManager.LocalPlayer?.ControlledEntity; - if (playerEntity == null || - !EntityManager.TryGetComponent(playerEntity.Value, out var actionsComponent)) return false; - - HandleHotbarKeybind(slot, args); - return true; - }, false); - } - - private PointerInputCmdHandler HandleChangeHotbarKeybind(byte hotbar) - { - // delegate to the ActionsUI, simulating a click on it - return new((in PointerInputCmdHandler.PointerInputCmdArgs args) => - { - var playerEntity = _playerManager.LocalPlayer?.ControlledEntity; - if (!EntityManager.TryGetComponent(playerEntity, out var actionsComponent)) return false; - - HandleChangeHotbarKeybind(hotbar, args); - return true; - }, - false); - } - - private void ToggleActionsMenu() - { - Ui?.ToggleActionsMenu(); - } - - /// - /// A action slot was pressed. This either performs the action or toggles the targeting mode. - /// - internal void OnSlotPressed(ActionSlot slot) - { - if (Ui == null) + if (PlayerActions == null || action == null || _playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } user) return; - if (slot.Action == null || _playerManager.LocalPlayer?.ControlledEntity is not EntityUid user) + if (action.Provider != null && Deleted(action.Provider)) return; - if (slot.Action.Provider != null && Deleted(slot.Action.Provider)) - return; - - if (slot.Action is not InstantAction instantAction) + if (action is not InstantAction instantAction) { - // for target actions, we go into "select target" mode, we don't - // message the server until we actually pick our target. - - // if we're clicking the same thing we're already targeting for, then we simply cancel - // targeting - Ui.ToggleTargeting(slot); return; } - if (slot.Action.ClientExclusive) + if (action.ClientExclusive) { if (instantAction.Event != null) instantAction.Event.Performer = user; - PerformAction(Ui.Component, instantAction, instantAction.Event, GameTiming.CurTime); + PerformAction(PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime); } else { @@ -377,226 +132,6 @@ namespace Content.Client.Actions } } - private bool TargetingCancel(in PointerInputCmdHandler.PointerInputCmdArgs args) - { - if (!GameTiming.IsFirstTimePredicted) - return false; - - // only do something for actual target-based actions - if (Ui?.SelectingTargetFor?.Action == null) - return false; - - Ui.StopTargeting(); - return true; - } - - /// - /// If the user clicked somewhere, and they are currently targeting an action, try and perform it. - /// - private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args) - { - if (!GameTiming.IsFirstTimePredicted) - return false; - - // only do something for actual target-based actions - if (Ui?.SelectingTargetFor?.Action is not TargetedAction action) - return false; - - if (_playerManager.LocalPlayer?.ControlledEntity is not EntityUid user) - return false; - - if (!TryComp(user, out ActionsComponent? comp)) - return false; - - // Is the action currently valid? - if (!action.Enabled - || action.Charges != null && action.Charges == 0 - || action.Cooldown.HasValue && action.Cooldown.Value.End > GameTiming.CurTime) - { - // The user is targeting with this action, but it is not valid. Maybe mark this click as - // handled and prevent further interactions. - return !action.InteractOnMiss; - } - - switch (action) - { - case WorldTargetAction mapTarget: - return TryTargetWorld(args, mapTarget, user, comp) || !action.InteractOnMiss; - - case EntityTargetAction entTarget: - return TargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss; - - default: - Logger.Error($"Unknown targeting action: {action.GetType()}"); - return false; - } - } - - private bool TryTargetWorld(in PointerInputCmdHandler.PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp) - { - var coords = args.Coordinates.ToMap(EntityManager); - - if (!ValidateWorldTarget(user, coords, action)) - { - // Invalid target. - if (action.DeselectOnMiss) - Ui?.StopTargeting(); - - return false; - } - - if (action.ClientExclusive) - { - if (action.Event != null) - { - action.Event.Target = coords; - action.Event.Performer = user; - } - - PerformAction(actionComp, action, action.Event, GameTiming.CurTime); - } - else - EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords)); - - if (!action.Repeat) - Ui?.StopTargeting(); - - return true; - } - - private bool TargetEntity(in PointerInputCmdHandler.PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp) - { - if (!ValidateEntityTarget(user, args.EntityUid, action)) - { - if (action.DeselectOnMiss) - Ui?.StopTargeting(); - - return false; - } - - if (action.ClientExclusive) - { - if (action.Event != null) - { - action.Event.Target = args.EntityUid; - action.Event.Performer = user; - } - - PerformAction(actionComp, action, action.Event, GameTiming.CurTime); - } - else - EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid)); - - if (!action.Repeat) - Ui?.StopTargeting(); - - return true; - } - - /// - /// Execute convenience functionality for actions (pop-ups, sound, speech) - /// - protected override bool PerformBasicActions(EntityUid user, ActionType action) - { - var performedAction = action.Sound != null - || !string.IsNullOrWhiteSpace(action.UserPopup) - || !string.IsNullOrWhiteSpace(action.Popup); - - if (!GameTiming.IsFirstTimePredicted) - return performedAction; - - if (!string.IsNullOrWhiteSpace(action.UserPopup)) - { - var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix)) - ? Loc.GetString(action.UserPopup) - : Loc.GetString(action.UserPopup + action.PopupToggleSuffix); - - _popupSystem.PopupEntity(msg, user); - } - else if (!string.IsNullOrWhiteSpace(action.Popup)) - { - var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix)) - ? Loc.GetString(action.Popup) - : Loc.GetString(action.Popup + action.PopupToggleSuffix); - - _popupSystem.PopupEntity(msg, user); - } - - if (action.Sound != null) - SoundSystem.Play(action.Sound.GetSound(), Filter.Local(), user, action.AudioParams); - - return performedAction; - } - - internal void StopTargeting() - { - _targetOutline.Disable(); - _interactionOutline.SetEnabled(true); - - if (!_overlayMan.TryGetOverlay(out var handOverlay) || handOverlay == null) - return; - - handOverlay.IconOverride = null; - handOverlay.EntityOverride = null; - } - - internal void StartTargeting(TargetedAction action) - { - // override "held-item" overlay - if (action.TargetingIndicator - && _overlayMan.TryGetOverlay(out var handOverlay) - && handOverlay != null) - { - if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null) - { - handOverlay.EntityOverride = action.Provider; - } - else if (action.Toggled && action.IconOn != null) - handOverlay.IconOverride = action.IconOn.Frame0(); - else if (action.Icon != null) - handOverlay.IconOverride = action.Icon.Frame0(); - } - - // TODO: allow world-targets to check valid positions. E.g., maybe: - // - Draw a red/green ghost entity - // - Add a yes/no checkmark where the HandItemOverlay usually is - - // Highlight valid entity targets - if (action is not EntityTargetAction entityAction) - return; - - Func? predicate = null; - - if (!entityAction.CanTargetSelf) - predicate = e => e != entityAction.AttachedEntity; - - var range = entityAction.CheckCanAccess ? action.Range : -1; - - _interactionOutline.SetEnabled(false); - _targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null); - } - - internal void TryFillSlot(byte hotbar, byte index) - { - if (Ui == null) - return; - - var fillEvent = new FillActionSlotEvent(); - RaiseLocalEvent(Ui.Component.Owner, fillEvent, broadcast: true); - - if (fillEvent.Action == null) - return; - - fillEvent.Action.ClientExclusive = true; - fillEvent.Action.Temporary = true; - fillEvent.Action.AutoPopulate = false; - - Ui.Component.Actions.Add(fillEvent.Action); - Assignments.AssignSlot(hotbar, index, fillEvent.Action); - - Ui.UpdateUI(); - } - /*public void SaveActionAssignments(string path) { @@ -624,13 +159,13 @@ namespace Content.Client.Actions /// public void LoadActionAssignments(string path, bool userData) { - if (Ui == null) + if (PlayerActions == null) return; var file = new ResourcePath(path).ToRootedPath(); TextReader reader = userData - ? _resourceManager.UserData.OpenText(file) - : _resourceManager.ContentFileReadText(file); + ? _resources.UserData.OpenText(file) + : _resources.ContentFileReadText(file); var yamlStream = new YamlStream(); yamlStream.Load(reader); @@ -638,13 +173,9 @@ namespace Content.Client.Actions if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence) return; - foreach (var (action, assigns) in Assignments.Assignments) - { - foreach (var index in assigns) - { - Assignments.ClearSlot(index.Hotbar, index.Slot, true); - } - } + ClearAssignments?.Invoke(); + + var assignments = new List(); foreach (var entry in sequence.Sequence) { @@ -654,28 +185,33 @@ namespace Content.Client.Actions if (!map.TryGet("action", out var actionNode)) continue; - var action = _serializationManager.Read(actionNode); + var action = _serialization.Read(actionNode); - if (Ui.Component.Actions.TryGetValue(action, out var existingAction)) + if (PlayerActions.Actions.TryGetValue(action, out var existingAction)) { existingAction.CopyFrom(action); action = existingAction; } else - Ui.Component.Actions.Add(action); + { + PlayerActions.Actions.Add(action); + } if (!map.TryGet("assignments", out var assignmentNode)) continue; - var assignments = _serializationManager.Read>(assignmentNode); + var nodeAssignments = _serialization.Read>(assignmentNode); - foreach (var index in assignments) + foreach (var index in nodeAssignments) { - Assignments.AssignSlot(index.Hotbar, index.Slot, action); + var assignment = new SlotAssignment(index.Hotbar, index.Slot, action); + assignments.Add(assignment); } } - UIDirty = true; + AssignSlot?.Invoke(assignments); } + + public record struct SlotAssignment(byte Hotbar, byte Slot, ActionType Action); } } diff --git a/Content.Client/Actions/Assignments/ActionAssignments.cs b/Content.Client/Actions/Assignments/ActionAssignments.cs deleted file mode 100644 index 5c9c178eb1..0000000000 --- a/Content.Client/Actions/Assignments/ActionAssignments.cs +++ /dev/null @@ -1,167 +0,0 @@ -using Content.Shared.Actions; -using Content.Shared.Actions.ActionTypes; -using System.Linq; - -namespace Content.Client.Actions.Assignments -{ - /// - /// Tracks and manages the hotbar assignments for actions. - /// - [DataDefinition] - public sealed class ActionAssignments - { - // the slots and assignments fields hold client's assignments (what action goes in what slot), - // which are completely client side and independent of what actions they've actually been granted and - // what item the action is actually for. - - /// - /// x = hotbar number, y = slot of that hotbar (index 0 corresponds to the one labeled "1", - /// index 9 corresponds to the one labeled "0"). Essentially the inverse of _assignments. - /// - private readonly ActionType?[,] _slots; - - /// - /// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1", - /// slot index 9 corresponds to the one labeled "0"). The key corresponds to an index in the _slots array. - /// The value is a list because actions can be assigned to multiple slots. Even if an action type has not been granted, - /// it can still be assigned to a slot. Essentially the inverse of _slots. - /// There will be no entry if there is no assignment (no empty lists in this dict) - /// - [DataField("assignments")] - public readonly Dictionary> Assignments = new(); - - /// - /// Actions which have been manually cleared by the user, thus should not - /// auto-populate. - /// - public readonly SortedSet PreventAutoPopulate = new(); - - private readonly byte _numHotbars; - private readonly byte _numSlots; - - public ActionAssignments(byte numHotbars, byte numSlots) - { - _numHotbars = numHotbars; - _numSlots = numSlots; - _slots = new ActionType?[numHotbars, numSlots]; - } - - public bool Remove(ActionType action) => Replace(action, null); - - internal bool Replace(ActionType action, ActionType? newAction) - { - if (!Assignments.Remove(action, out var assigns)) - return false; - - if (newAction != null) - Assignments[newAction] = assigns; - - foreach (var (bar, slot) in assigns) - { - _slots[bar, slot] = newAction; - } - - return true; - } - - /// - /// Assigns the indicated hotbar slot to the specified action type. - /// - /// hotbar whose slot is being assigned - /// slot of the hotbar to assign to (0 = the slot labeled 1, 9 = the slot labeled 0) - /// action to assign to the slot - public void AssignSlot(byte hotbar, byte slot, ActionType actionType) - { - ClearSlot(hotbar, slot, false); - _slots[hotbar, slot] = actionType; - if (Assignments.TryGetValue(actionType, out var slotList)) - { - slotList.Add((hotbar, slot)); - } - else - { - var newList = new List<(byte Hotbar, byte Slot)> { (hotbar, slot) }; - Assignments[actionType] = newList; - } - } - - /// - /// Clear the assignment from the indicated slot. - /// - /// hotbar whose slot is being cleared - /// slot of the hotbar to clear (0 = the slot labeled 1, 9 = the slot labeled 0) - /// if true, the action assigned to this slot - /// will be prevented from being auto-populated in the future when it is newly granted. - /// Item actions will automatically be allowed to auto populate again - /// when their associated item are unequipped. This ensures that items that are newly - /// picked up will always present their actions to the user even if they had earlier been cleared. - /// - public void ClearSlot(byte hotbar, byte slot, bool preventAutoPopulate) - { - // remove this particular assignment from our data structures - // (keeping in mind something can be assigned multiple slots) - var currentAction = _slots[hotbar, slot]; - - if (currentAction == null) - return; - - if (preventAutoPopulate) - PreventAutoPopulate.Add(currentAction); - - var assignmentList = Assignments[currentAction]; - assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList(); - if (!assignmentList.Any()) - { - Assignments.Remove(currentAction); - } - else - { - Assignments[currentAction] = assignmentList; - } - - _slots[hotbar, slot] = null; - } - - /// - /// Finds the next open slot the action can go in and assigns it there, - /// starting from the currently selected hotbar. - /// Does not update any UI elements, only updates the assignment data structures. - /// - /// if true, will force the assignment to occur - /// regardless of whether this assignment has been prevented from auto population - /// via ClearSlot's preventAutoPopulate parameter. If false, will have no effect - /// if this assignment has been prevented from auto population. - public void AutoPopulate(ActionType toAssign, byte currentHotbar, bool force = true) - { - if (!force && PreventAutoPopulate.Contains(toAssign)) - return; - - for (byte hotbarOffset = 0; hotbarOffset < _numHotbars; hotbarOffset++) - { - for (byte slot = 0; slot < _numSlots; slot++) - { - var hotbar = (byte) ((currentHotbar + hotbarOffset) % _numHotbars); - var slotAssignment = _slots[hotbar, slot]; - - if (slotAssignment != null) - continue; - - AssignSlot(hotbar, slot, toAssign); - return; - } - } - // there was no empty slot - } - - /// - /// Gets the assignment to the indicated slot if there is one. - /// - public ActionType? this[in byte hotbar, in byte slot] => _slots[hotbar, slot]; - - /// true if we have the assignment assigned to some slot - public bool HasAssignment(ActionType assignment) - { - return Assignments.ContainsKey(assignment); - } - } -} diff --git a/Content.Client/Actions/UI/ActionMenu.cs b/Content.Client/Actions/UI/ActionMenu.cs deleted file mode 100644 index 88d08be32c..0000000000 --- a/Content.Client/Actions/UI/ActionMenu.cs +++ /dev/null @@ -1,426 +0,0 @@ -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Content.Client.DragDrop; -using Content.Client.HUD; -using Content.Client.Stylesheets; -using Content.Shared.Actions; -using Content.Shared.Actions.ActionTypes; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Client.Utility; -using Robust.Shared.Input; -using Robust.Shared.Timing; -using static Robust.Client.UserInterface.Controls.BaseButton; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.Actions.UI -{ - /// - /// Action selection menu, allows filtering and searching over all possible - /// actions and populating those actions into the hotbar. - /// - public sealed class ActionMenu : DefaultWindow - { - // Pre-defined global filters that can be used to select actions based on their properties (as opposed to their - // own yaml-defined filters). - // TODO LOC STRINGs - private const string AllFilter = "all"; - private const string ItemFilter = "item"; - private const string InnateFilter = "innate"; - private const string EnabledFilter = "enabled"; - private const string InstantFilter = "instant"; - private const string TargetedFilter = "targeted"; - - private readonly string[] _filters = - { - AllFilter, - ItemFilter, - InnateFilter, - EnabledFilter, - InstantFilter, - TargetedFilter - }; - - private const int MinSearchLength = 3; - private static readonly Regex NonAlphanumeric = new Regex(@"\W", RegexOptions.Compiled); - private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled); - - /// - /// Is an action currently being dragged from this window? - /// - public bool IsDragging => _dragDropHelper.IsDragging; - - private readonly ActionsUI _actionsUI; - private readonly LineEdit _searchBar; - private readonly MultiselectOptionButton _filterButton; - private readonly Label _filterLabel; - private readonly Button _clearButton; - private readonly GridContainer _resultsGrid; - private readonly TextureRect _dragShadow; - private readonly IGameHud _gameHud; - private readonly DragDropHelper _dragDropHelper; - private readonly IEntityManager _entMan; - - public ActionMenu(ActionsUI actionsUI) - { - _actionsUI = actionsUI; - _gameHud = IoCManager.Resolve(); - _entMan = IoCManager.Resolve(); - - Title = Loc.GetString("ui-actionmenu-title"); - MinSize = (320, 300); - - Contents.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Children = - { - new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (_searchBar = new LineEdit - { - StyleClasses = { StyleNano.StyleClassActionSearchBox }, - HorizontalExpand = true, - PlaceHolder = Loc.GetString("ui-actionmenu-search-bar-placeholder-text") - }), - (_filterButton = new MultiselectOptionButton() - { - Label = Loc.GetString("ui-actionmenu-filter-button") - }) - } - }, - (_clearButton = new Button - { - Text = Loc.GetString("ui-actionmenu-clear-button"), - }), - (_filterLabel = new Label()), - new ScrollContainer - { - //TODO: needed? MinSize = new Vector2(200.0f, 0.0f), - VerticalExpand = true, - HorizontalExpand = true, - Children = - { - (_resultsGrid = new GridContainer - { - MaxGridWidth = 300 - }) - } - } - } - }); - - foreach (var tag in _filters) - { - _filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag); - } - - // default to showing all actions. - _filterButton.SelectKey(AllFilter); - - UpdateFilterLabel(); - - _dragShadow = new TextureRect - { - MinSize = (64, 64), - Stretch = TextureRect.StretchMode.Scale, - Visible = false, - SetSize = (64, 64) - }; - UserInterfaceManager.PopupRoot.AddChild(_dragShadow); - - _dragDropHelper = new DragDropHelper(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag); - } - - protected override void EnteredTree() - { - base.EnteredTree(); - _clearButton.OnPressed += OnClearButtonPressed; - _searchBar.OnTextChanged += OnSearchTextChanged; - _filterButton.OnItemSelected += OnFilterItemSelected; - _gameHud.ActionsButtonDown = true; - } - - protected override void ExitedTree() - { - base.ExitedTree(); - _dragDropHelper.EndDrag(); - _clearButton.OnPressed -= OnClearButtonPressed; - _searchBar.OnTextChanged -= OnSearchTextChanged; - _filterButton.OnItemSelected -= OnFilterItemSelected; - _gameHud.ActionsButtonDown = false; - foreach (var actionMenuControl in _resultsGrid.Children) - { - var actionMenuItem = (ActionMenuItem) actionMenuControl; - actionMenuItem.OnButtonDown -= OnItemButtonDown; - actionMenuItem.OnButtonUp -= OnItemButtonUp; - actionMenuItem.OnPressed -= OnItemPressed; - } - } - - private void OnFilterItemSelected(MultiselectOptionButton.ItemPressedEventArgs args) - { - UpdateFilterLabel(); - SearchAndDisplay(); - } - - protected override void Resized() - { - base.Resized(); - // TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done, - // currently no good way to let the grid know what size it has to "work with", so must manually resize - _resultsGrid.MaxGridWidth = Width; - } - - private bool OnBeginActionDrag() - { - _dragShadow.Texture = _dragDropHelper.Dragged?.Action?.Icon?.Frame0(); - // don't make visible until frameupdate, otherwise it'll flicker - LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32)); - return true; - } - - private bool OnContinueActionDrag(float frameTime) - { - // keep dragged entity centered under mouse - LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32)); - // we don't set this visible until frameupdate, otherwise it flickers - _dragShadow.Visible = true; - return true; - } - - private void OnEndActionDrag() - { - _dragShadow.Visible = false; - } - - private void OnItemButtonDown(ButtonEventArgs args) - { - if (args.Event.Function != EngineKeyFunctions.UIClick || - args.Button is not ActionMenuItem action) - { - return; - } - - _dragDropHelper.MouseDown(action); - } - - private void OnItemButtonUp(ButtonEventArgs args) - { - // note the buttonup only fires on the control that was originally - // pressed to initiate the drag, NOT the one we are currently hovering - if (args.Event.Function != EngineKeyFunctions.UIClick) return; - - if (UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot) - { - if (!_dragDropHelper.IsDragging || _dragDropHelper.Dragged?.Action == null) - { - _dragDropHelper.EndDrag(); - return; - } - - _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action); - _actionsUI.UpdateUI(); - } - - _dragDropHelper.EndDrag(); - } - - private void OnItemFocusExited(ActionMenuItem item) - { - // lost focus, cancel the drag if one is in progress - _dragDropHelper.EndDrag(); - } - - private void OnItemPressed(ButtonEventArgs args) - { - if (args.Button is not ActionMenuItem actionMenuItem) return; - - _actionsUI.System.Assignments.AutoPopulate(actionMenuItem.Action, _actionsUI.SelectedHotbar); - _actionsUI.UpdateUI(); - } - - private void OnClearButtonPressed(ButtonEventArgs args) - { - _searchBar.Clear(); - _filterButton.DeselectAll(); - UpdateFilterLabel(); - SearchAndDisplay(); - } - - private void OnSearchTextChanged(LineEdit.LineEditEventArgs obj) - { - SearchAndDisplay(); - } - - private void SearchAndDisplay() - { - var search = Standardize(_searchBar.Text); - // only display nothing if there are no filters selected and text is not long enough. - // otherwise we will search if even one filter is applied, regardless of length of search string - if (_filterButton.SelectedKeys.Count == 0 && - (string.IsNullOrWhiteSpace(search) || search.Length < MinSearchLength)) - { - ClearList(); - return; - } - - var matchingActions = _actionsUI.Component.Actions - .Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys)); - - PopulateActions(matchingActions); - } - - private void UpdateFilterLabel() - { - if (_filterButton.SelectedKeys.Count == 0) - { - _filterLabel.Visible = false; - } - else - { - _filterLabel.Visible = true; - _filterLabel.Text = Loc.GetString("ui-actionmenu-filter-label", - ("selectedLabels", string.Join(", ", _filterButton.SelectedLabels))); - } - } - - private bool MatchesSearchCriteria(ActionType action, string standardizedSearch, - IReadOnlyList selectedFilterTags) - { - // check filter tag match first - each action must contain all filter tags currently selected. - // if no tags selected, don't check tags - if (selectedFilterTags.Count > 0 && selectedFilterTags.Any(filterTag => !ActionMatchesFilterTag(action, filterTag))) - { - return false; - } - - // check search tag match against the search query - if (action.Keywords.Any(standardizedSearch.Contains)) - { - return true; - } - - if (Standardize(action.DisplayName.ToString()).Contains(standardizedSearch)) - { - return true; - } - - // search by provider name - if (action.Provider == null || action.Provider == _actionsUI.Component.Owner) - return false; - - var name = _entMan.GetComponent(action.Provider.Value).EntityName; - return Standardize(name).Contains(standardizedSearch); - } - - private bool ActionMatchesFilterTag(ActionType action, string tag) - { - return tag switch - { - EnabledFilter => action.Enabled, - ItemFilter => action.Provider != null && action.Provider != _actionsUI.Component.Owner, - InnateFilter => action.Provider == null || action.Provider == _actionsUI.Component.Owner, - InstantFilter => action is InstantAction, - TargetedFilter => action is TargetedAction, - _ => true - }; - } - - /// - /// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace), - /// trimmed, 1 space max per whitespace gap, - /// and optional spaces between case change - /// - private static string Standardize(string rawText, bool splitOnCaseChange = false) - { - rawText ??= string.Empty; - - // treat non-alphanumeric characters as whitespace - rawText = NonAlphanumeric.Replace(rawText, " "); - - // trim spaces and reduce internal whitespaces to 1 max - rawText = Whitespace.Replace(rawText, " ").Trim(); - if (splitOnCaseChange) - { - // insert a space when case switches from lower to upper - rawText = AddSpaces(rawText, true); - } - - return rawText.ToLowerInvariant().Trim(); - } - - // taken from https://stackoverflow.com/a/272929 (CC BY-SA 3.0) - private static string AddSpaces(string text, bool preserveAcronyms) - { - if (string.IsNullOrWhiteSpace(text)) - return string.Empty; - var newText = new StringBuilder(text.Length * 2); - newText.Append(text[0]); - for (var i = 1; i < text.Length; i++) - { - if (char.IsUpper(text[i])) - { - if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) || - (preserveAcronyms && char.IsUpper(text[i - 1]) && - i < text.Length - 1 && !char.IsUpper(text[i + 1]))) - newText.Append(' '); - } - - newText.Append(text[i]); - } - return newText.ToString(); - } - - private void PopulateActions(IEnumerable actions) - { - ClearList(); - - foreach (var action in actions) - { - var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited); - _resultsGrid.Children.Add(actionItem); - actionItem.SetActionState(action.Enabled); - actionItem.OnButtonDown += OnItemButtonDown; - actionItem.OnButtonUp += OnItemButtonUp; - actionItem.OnPressed += OnItemPressed; - } - } - - private void ClearList() - { - // TODO: Not sure if this unsub is needed if children are all being cleared - foreach (var actionItem in _resultsGrid.Children) - { - ((ActionMenuItem) actionItem).OnPressed -= OnItemPressed; - } - _resultsGrid.Children.Clear(); - } - - /// - /// Should be invoked when action states change, ensures - /// currently displayed actions are properly showing their revoked / granted status - /// - public void UpdateUI() - { - foreach (var actionItem in _resultsGrid.Children) - { - var actionMenuItem = ((ActionMenuItem) actionItem); - actionMenuItem.SetActionState(actionMenuItem.Action.Enabled); - } - - SearchAndDisplay(); - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - _dragDropHelper.Update(args.DeltaSeconds); - } - } -} diff --git a/Content.Client/Actions/UI/ActionMenuItem.cs b/Content.Client/Actions/UI/ActionMenuItem.cs deleted file mode 100644 index a2ce3023f9..0000000000 --- a/Content.Client/Actions/UI/ActionMenuItem.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using Content.Client.Stylesheets; -using Content.Shared.Actions; -using Content.Shared.Actions.ActionTypes; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.Utility; -using Robust.Shared.Utility; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.Actions.UI -{ - // TODO merge this with action-slot when it gets XAMLd - // this has way too much overlap, especially now that they both have the item-sprite icons. - - /// - /// An individual action visible in the action menu. - /// - public sealed class ActionMenuItem : ContainerButton - { - // shorter than default tooltip delay so user can - // quickly explore what each action is - private const float CustomTooltipDelay = 0.2f; - - private readonly TextureRect _bigActionIcon; - private readonly TextureRect _smallActionIcon; - private readonly SpriteView _smallItemSpriteView; - private readonly SpriteView _bigItemSpriteView; - - public ActionType Action; - - private Action _onControlFocusExited; - - private readonly ActionsUI _actionsUI; - - public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action onControlFocusExited) - { - _actionsUI = actionsUI; - Action = action; - _onControlFocusExited = onControlFocusExited; - - SetSize = (64, 64); - VerticalAlignment = VAlignment.Top; - - _bigActionIcon = new TextureRect - { - HorizontalExpand = true, - VerticalExpand = true, - Stretch = TextureRect.StretchMode.Scale, - Visible = false - }; - _bigItemSpriteView = new SpriteView - { - HorizontalExpand = true, - VerticalExpand = true, - Scale = (2, 2), - Visible = false, - OverrideDirection = Direction.South, - }; - _smallActionIcon = new TextureRect - { - HorizontalAlignment = HAlignment.Right, - VerticalAlignment = VAlignment.Bottom, - Stretch = TextureRect.StretchMode.Scale, - Visible = false - }; - _smallItemSpriteView = new SpriteView - { - HorizontalAlignment = HAlignment.Right, - VerticalAlignment = VAlignment.Bottom, - Visible = false, - OverrideDirection = Direction.South, - }; - - // padding to the left of the small icon - var paddingBoxItemIcon = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true, - VerticalExpand = true, - MinSize = (64, 64) - }; - paddingBoxItemIcon.AddChild(new Control() - { - MinSize = (32, 32), - }); - paddingBoxItemIcon.AddChild(new Control - { - Children = - { - _smallActionIcon, - _smallItemSpriteView - } - }); - AddChild(_bigActionIcon); - AddChild(_bigItemSpriteView); - AddChild(paddingBoxItemIcon); - - TooltipDelay = CustomTooltipDelay; - TooltipSupplier = SupplyTooltip; - UpdateIcons(); - } - - - public void UpdateIcons() - { - UpdateItemIcon(); - - if (Action == null) - { - SetActionIcon(null); - return; - } - - if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null) - SetActionIcon(Action.IconOn.Frame0()); - else - SetActionIcon(Action.Icon?.Frame0()); - } - - private void SetActionIcon(Texture? texture) - { - if (texture == null || Action == null) - { - _bigActionIcon.Texture = null; - _bigActionIcon.Visible = false; - _smallActionIcon.Texture = null; - _smallActionIcon.Visible = false; - } - else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem) - { - _smallActionIcon.Texture = texture; - _smallActionIcon.Modulate = Action.IconColor; - _smallActionIcon.Visible = true; - _bigActionIcon.Texture = null; - _bigActionIcon.Visible = false; - } - else - { - _bigActionIcon.Texture = texture; - _bigActionIcon.Modulate = Action.IconColor; - _bigActionIcon.Visible = true; - _smallActionIcon.Texture = null; - _smallActionIcon.Visible = false; - } - } - - private void UpdateItemIcon() - { - if (Action?.EntityIcon == null || !IoCManager.Resolve().TryGetComponent(Action.EntityIcon.Value, out SpriteComponent? sprite)) - { - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - } - else - { - switch (Action.ItemIconStyle) - { - case ItemActionIconStyle.BigItem: - _bigItemSpriteView.Visible = true; - _bigItemSpriteView.Sprite = sprite; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - break; - case ItemActionIconStyle.BigAction: - - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = true; - _smallItemSpriteView.Sprite = sprite; - break; - - case ItemActionIconStyle.NoItem: - - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - break; - } - } - } - - protected override void ControlFocusExited() - { - base.ControlFocusExited(); - _onControlFocusExited.Invoke(this); - } - - private Control SupplyTooltip(Control? sender) - { - var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName)); - var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description)); - - var tooltip = new ActionAlertTooltip(name, decr); - - if (Action.Enabled && (Action.Charges == null || Action.Charges != 0)) - tooltip.Cooldown = Action.Cooldown; - - return tooltip; - } - - /// - /// Change how this is displayed depending on if it is granted or revoked - /// - public void SetActionState(bool granted) - { - if (granted) - { - if (HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked)) - { - RemoveStyleClass(StyleNano.StyleClassActionMenuItemRevoked); - } - } - else - { - if (!HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked)) - { - AddStyleClass(StyleNano.StyleClassActionMenuItemRevoked); - } - } - } - } -} diff --git a/Content.Client/Actions/UI/ActionSlot.cs b/Content.Client/Actions/UI/ActionSlot.cs deleted file mode 100644 index dd4ff87b62..0000000000 --- a/Content.Client/Actions/UI/ActionSlot.cs +++ /dev/null @@ -1,551 +0,0 @@ -using System; -using Content.Client.Cooldown; -using Content.Client.Stylesheets; -using Content.Shared.Actions; -using Content.Shared.Actions.ActionTypes; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.Utility; -using Robust.Shared.Input; -using Robust.Shared.Timing; -using Robust.Shared.Utility; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.Actions.UI -{ - /// - /// A slot in the action hotbar. Not extending BaseButton because - /// its needs diverged too much. - /// - public sealed class ActionSlot : PanelContainer - { - // shorter than default tooltip delay so user can more easily - // see what actions they've been given - private const float CustomTooltipDelay = 0.5f; - - private static readonly string EnabledColor = "#7b7e9e"; - private static readonly string DisabledColor = "#950000"; - - private bool _spriteViewDirty = false; - - /// - /// Current action in this slot. - /// - public ActionType? Action { get; private set; } - - /// - /// 1-10 corresponding to the number label on the slot (10 is labeled as 0) - /// - private byte SlotNumber => (byte) (SlotIndex + 1); - public byte SlotIndex { get; } - - private readonly IGameTiming _gameTiming; - private readonly IEntityManager _entMan; - private readonly RichTextLabel _number; - private readonly TextureRect _bigActionIcon; - private readonly TextureRect _smallActionIcon; - private readonly SpriteView _smallItemSpriteView; - private readonly SpriteView _bigItemSpriteView; - private readonly CooldownGraphic _cooldownGraphic; - private readonly ActionsUI _actionsUI; - private readonly ActionMenu _actionMenu; - // whether button is currently pressed down by mouse or keybind down. - private bool _depressed; - private bool _beingHovered; - - /// - /// Creates an action slot for the specified number - /// - /// slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0". - public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, byte slotIndex, IGameTiming timing, IEntityManager entMan) - { - _actionsUI = actionsUI; - _actionMenu = actionMenu; - _gameTiming = timing; - _entMan = entMan; - SlotIndex = slotIndex; - MouseFilter = MouseFilterMode.Stop; - - SetSize = (64, 64); - VerticalAlignment = VAlignment.Top; - TooltipDelay = CustomTooltipDelay; - TooltipSupplier = SupplyTooltip; - - _number = new RichTextLabel - { - StyleClasses = {StyleNano.StyleClassHotbarSlotNumber} - }; - _number.SetMessage(SlotNumberLabel()); - - _bigActionIcon = new TextureRect - { - HorizontalExpand = true, - VerticalExpand = true, - Stretch = TextureRect.StretchMode.Scale, - Visible = false - }; - _bigItemSpriteView = new SpriteView - { - HorizontalExpand = true, - VerticalExpand = true, - Scale = (2,2), - Visible = false, - OverrideDirection = Direction.South, - }; - _smallActionIcon = new TextureRect - { - HorizontalAlignment = HAlignment.Right, - VerticalAlignment = VAlignment.Bottom, - Stretch = TextureRect.StretchMode.Scale, - Visible = false - }; - _smallItemSpriteView = new SpriteView - { - HorizontalAlignment = HAlignment.Right, - VerticalAlignment = VAlignment.Bottom, - Visible = false, - OverrideDirection = Direction.South, - }; - - _cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false}; - - // padding to the left of the number to shift it right - var paddingBox = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true, - VerticalExpand = true, - MinSize = (64, 64) - }; - paddingBox.AddChild(new Control() - { - MinSize = (4, 4), - }); - paddingBox.AddChild(_number); - - // padding to the left of the small icon - var paddingBoxItemIcon = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true, - VerticalExpand = true, - MinSize = (64, 64) - }; - paddingBoxItemIcon.AddChild(new Control() - { - MinSize = (32, 32), - }); - paddingBoxItemIcon.AddChild(new Control - { - Children = - { - _smallActionIcon, - _smallItemSpriteView - } - }); - AddChild(_bigActionIcon); - AddChild(_bigItemSpriteView); - AddChild(_cooldownGraphic); - AddChild(paddingBox); - AddChild(paddingBoxItemIcon); - DrawModeChanged(); - } - - private Control? SupplyTooltip(Control sender) - { - if (Action == null) - return null; - - string? extra = null; - if (Action.Charges != null) - { - extra = Loc.GetString("ui-actionslot-charges", ("charges", Action.Charges)); - } - - var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName)); - var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description)); - - var tooltip = new ActionAlertTooltip(name, decr, extra); - - if (Action.Enabled && (Action.Charges == null || Action.Charges != 0)) - tooltip.Cooldown = Action.Cooldown; - - return tooltip; - } - - protected override void MouseEntered() - { - base.MouseEntered(); - - _beingHovered = true; - DrawModeChanged(); - - if (Action?.Provider != null) - _actionsUI.System.HighlightItemSlot(Action.Provider.Value); - } - - protected override void MouseExited() - { - base.MouseExited(); - _beingHovered = false; - CancelPress(); - DrawModeChanged(); - _actionsUI.System.StopHighlightingItemSlot(); - } - - protected override void KeyBindDown(GUIBoundKeyEventArgs args) - { - base.KeyBindDown(args); - - if (Action == null) - { - // No action for this slot. Maybe the user is trying to add a mapping action? - _actionsUI.System.TryFillSlot(_actionsUI.SelectedHotbar, SlotIndex); - return; - } - - // only handle clicks, and can't do anything to this if no assignment - if (args.Function == EngineKeyFunctions.UIClick) - { - // might turn into a drag or a full press if released - Depress(true); - _actionsUI.DragDropHelper.MouseDown(this); - DrawModeChanged(); - return; - } - - if (args.Function != EngineKeyFunctions.UIRightClick || _actionsUI.Locked) - return; - - if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging) - return; - - // user right clicked on an action slot, so we clear it. - _actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true); - - // If this was a temporary action, and it is no longer assigned to any slots, then we remove the action - // altogether. - if (Action.Temporary) - { - // Theres probably a better way to do this..... - DebugTools.Assert(Action.ClientExclusive, "Temporary-actions must be client exclusive"); - - if (!_actionsUI.System.Assignments.Assignments.TryGetValue(Action, out var index) - || index.Count == 0) - { - _actionsUI.Component.Actions.Remove(Action); - } - } - - _actionsUI.StopTargeting(); - _actionsUI.UpdateUI(); - } - - protected override void KeyBindUp(GUIBoundKeyEventArgs args) - { - base.KeyBindUp(args); - - if (args.Function != EngineKeyFunctions.UIClick) - return; - - // might be finishing a drag or using the action - if (_actionsUI.DragDropHelper.IsDragging && - _actionsUI.DragDropHelper.Dragged == this && - UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot && - targetSlot != this) - { - // finish the drag, swap the 2 slots - var fromIdx = SlotIndex; - var fromAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx]; - var toIdx = targetSlot.SlotIndex; - var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx]; - - if (fromIdx == toIdx) return; - if (fromAssignment == null) return; - - _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment); - if (toAssignment != null) - { - _actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment); - } - else - { - _actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false); - } - _actionsUI.UpdateUI(); - } - else - { - // perform the action - if (UserInterfaceManager.CurrentlyHovered == this) - { - Depress(false); - } - } - _actionsUI.DragDropHelper.EndDrag(); - DrawModeChanged(); - } - - protected override void ControlFocusExited() - { - // lost focus for some reason, cancel the drag if there is one. - base.ControlFocusExited(); - _actionsUI.DragDropHelper.EndDrag(); - DrawModeChanged(); - } - - /// - /// Cancel current press without triggering the action - /// - public void CancelPress() - { - _depressed = false; - DrawModeChanged(); - } - - /// - /// Press this button down. If it was depressed and now set to not depressed, will - /// trigger the action. - /// - public void Depress(bool depress) - { - // action can still be toggled if it's allowed to stay selected - if (Action == null || !Action.Enabled) return; - - if (_depressed && !depress) - { - // fire the action - _actionsUI.System.OnSlotPressed(this); - } - _depressed = depress; - } - - /// - /// Updates the item action assigned to this slot, tied to a specific item. - /// - /// action to assign - /// item the action is provided by - public void Assign(ActionType action) - { - // already assigned - if (Action != null && Action == action) return; - - Action = action; - HideTooltip(); - UpdateIcons(); - DrawModeChanged(); - _number.SetMessage(SlotNumberLabel()); - } - - /// - /// Clears the action assigned to this slot - /// - public void Clear() - { - if (Action == null) return; - Action = null; - _depressed = false; - HideTooltip(); - UpdateIcons(); - DrawModeChanged(); - _number.SetMessage(SlotNumberLabel()); - } - - /// - /// Display the action in this slot (if there is one) as enabled - /// - public void Enable() - { - DrawModeChanged(); - _number.SetMessage(SlotNumberLabel()); - } - - /// - /// Display the action in this slot (if there is one) as disabled. - /// The slot is still clickable. - /// - public void Disable() - { - _depressed = false; - DrawModeChanged(); - _number.SetMessage(SlotNumberLabel()); - } - - private FormattedMessage SlotNumberLabel() - { - if (SlotNumber > 10) return FormattedMessage.FromMarkup(""); - var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString()); - var color = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor; - return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]"); - } - - public void UpdateIcons() - { - UpdateItemIcon(); - - if (Action == null) - { - SetActionIcon(null); - return; - } - - if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null) - SetActionIcon(Action.IconOn.Frame0()); - else - SetActionIcon(Action.Icon?.Frame0()); - } - - private void SetActionIcon(Texture? texture) - { - if (texture == null || Action == null) - { - _bigActionIcon.Texture = null; - _bigActionIcon.Visible = false; - _smallActionIcon.Texture = null; - _smallActionIcon.Visible = false; - } - else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem) - { - _smallActionIcon.Texture = texture; - _smallActionIcon.Modulate = Action.IconColor; - _smallActionIcon.Visible = true; - _bigActionIcon.Texture = null; - _bigActionIcon.Visible = false; - } - else - { - _bigActionIcon.Texture = texture; - _bigActionIcon.Modulate = Action.IconColor; - _bigActionIcon.Visible = true; - _smallActionIcon.Texture = null; - _smallActionIcon.Visible = false; - } - } - - private void UpdateItemIcon() - { - if (Action?.EntityIcon != null && !_entMan.EntityExists(Action.EntityIcon)) - { - // This is almost certainly because a player received/processed their own actions component state before - // being send the entity in their inventory that enabled this action. - - // Defer updating icons to the next FrameUpdate(). - _spriteViewDirty = true; - return; - } - - if (Action?.EntityIcon == null || !_entMan.TryGetComponent(Action.EntityIcon.Value, out SpriteComponent? sprite)) - { - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - } - else - { - switch (Action.ItemIconStyle) - { - case ItemActionIconStyle.BigItem: - _bigItemSpriteView.Visible = true; - _bigItemSpriteView.Sprite = sprite; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - break; - case ItemActionIconStyle.BigAction: - - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = true; - _smallItemSpriteView.Sprite = sprite; - break; - - case ItemActionIconStyle.NoItem: - - _bigItemSpriteView.Visible = false; - _bigItemSpriteView.Sprite = null; - _smallItemSpriteView.Visible = false; - _smallItemSpriteView.Sprite = null; - break; - } - } - } - - public void DrawModeChanged() - { - // always show the normal empty button style if no action in this slot - if (Action == null) - { - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal); - return; - } - - // show a hover only if the action is usable or another action is being dragged on top of this - if (_beingHovered && (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging || Action.Enabled)) - { - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover); - } - - // it's only depress-able if it's usable, so if we're depressed - // show the depressed style - if (_depressed) - { - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed); - return; - } - - // if it's toggled on, always show the toggled on style (currently same as depressed style) - if (Action.Toggled || _actionsUI.SelectingTargetFor == this) - { - // when there's a toggle sprite, we're showing that sprite instead of highlighting this slot - SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal : - ContainerButton.StylePseudoClassPressed); - return; - } - - if (!Action.Enabled) - { - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled); - return; - } - - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal); - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - - if (_spriteViewDirty) - { - _spriteViewDirty = false; - UpdateIcons(); - } - - if (Action == null || Action.Cooldown == null || !Action.Enabled) - { - _cooldownGraphic.Visible = false; - _cooldownGraphic.Progress = 0; - return; - } - - var cooldown = Action.Cooldown.Value; - var duration = cooldown.End - cooldown.Start; - var curTime = _gameTiming.CurTime; - var length = duration.TotalSeconds; - var progress = (curTime - cooldown.Start).TotalSeconds / length; - var ratio = (progress <= 1 ? (1 - progress) : (curTime - cooldown.End).TotalSeconds * -5); - - _cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1); - if (ratio > -1f) - _cooldownGraphic.Visible = true; - else - { - _cooldownGraphic.Visible = false; - Action.Cooldown = null; - DrawModeChanged(); - } - } - } -} diff --git a/Content.Client/Actions/UI/ActionsUI.cs b/Content.Client/Actions/UI/ActionsUI.cs deleted file mode 100644 index 9cd0369221..0000000000 --- a/Content.Client/Actions/UI/ActionsUI.cs +++ /dev/null @@ -1,505 +0,0 @@ -using Content.Client.DragDrop; -using Content.Client.HUD; -using Content.Client.Resources; -using Content.Client.Stylesheets; -using Content.Shared.Actions; -using Content.Shared.Actions.ActionTypes; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.Utility; -using Robust.Shared.Input; -using Robust.Shared.Input.Binding; -using Robust.Shared.Timing; -using Robust.Shared.Utility; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.Actions.UI -{ - /// - /// The action hotbar on the left side of the screen. - /// - public sealed class ActionsUI : Container - { - private const float DragDeadZone = 10f; - private const float CustomTooltipDelay = 0.4f; - internal readonly ActionsSystem System; - private readonly IGameHud _gameHud; - private readonly IEntityManager _entMan; - private readonly IGameTiming _timing; - - /// - /// The action component of the currently attached entity. - /// - public readonly ActionsComponent Component; - - private readonly ActionSlot[] _slots; - - private readonly GridContainer _slotContainer; - - private readonly TextureButton _lockButton; - private readonly TextureButton _settingsButton; - private readonly Label _loadoutNumber; - private readonly Texture _lockTexture; - private readonly Texture _unlockTexture; - private readonly BoxContainer _loadoutContainer; - - private readonly TextureRect _dragShadow; - - private readonly ActionMenu _menu; - - /// - /// Index of currently selected hotbar - /// - public byte SelectedHotbar { get; private set; } - - /// - /// Action slot we are currently selecting a target for. - /// - public ActionSlot? SelectingTargetFor { get; private set; } - - /// - /// Drag drop helper for coordinating drag drops between action slots - /// - public DragDropHelper DragDropHelper { get; } - - /// - /// Whether the bar is currently locked by the user. This is intended to prevent drag / drop - /// and right click clearing slots. Anything else is still doable. - /// - public bool Locked { get; private set; } - - /// - /// All the action slots in order. - /// - public IEnumerable Slots => _slots; - - public ActionsUI(ActionsSystem system, ActionsComponent component) - { - SetValue(LayoutContainer.DebugProperty, true); - System = system; - Component = component; - _gameHud = IoCManager.Resolve(); - _timing = IoCManager.Resolve(); - _entMan = IoCManager.Resolve(); - _menu = new ActionMenu(this); - - LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End); - LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain); - LayoutContainer.SetAnchorTop(this, 0f); - LayoutContainer.SetAnchorBottom(this, 0.8f); - LayoutContainer.SetMarginLeft(this, 13); - LayoutContainer.SetMarginTop(this, 110); - - HorizontalAlignment = HAlignment.Left; - VerticalExpand = true; - - var resourceCache = IoCManager.Resolve(); - - // everything needs to go within an inner panel container so the panel resizes to fit the elements. - // Because ActionsUI is being anchored by layoutcontainer, the hotbar backing would appear too tall - // if ActionsUI was the panel container - - var panelContainer = new PanelContainer() - { - StyleClasses = {StyleNano.StyleClassHotbarPanel}, - HorizontalAlignment = HAlignment.Left, - VerticalAlignment = VAlignment.Top - }; - AddChild(panelContainer); - - var hotbarContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - SeparationOverride = 3, - HorizontalAlignment = HAlignment.Left - }; - panelContainer.AddChild(hotbarContainer); - - var settingsContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true - }; - hotbarContainer.AddChild(settingsContainer); - - settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 }); - _lockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock.svg.192dpi.png"); - _unlockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock_open.svg.192dpi.png"); - _lockButton = new TextureButton - { - TextureNormal = _unlockTexture, - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center, - SizeFlagsStretchRatio = 1, - Scale = (0.5f, 0.5f), - ToolTip = Loc.GetString("ui-actionsui-function-lock-action-slots"), - TooltipDelay = CustomTooltipDelay - }; - settingsContainer.AddChild(_lockButton); - settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 }); - _settingsButton = new TextureButton - { - TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/gear.svg.192dpi.png"), - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center, - SizeFlagsStretchRatio = 1, - Scale = (0.5f, 0.5f), - ToolTip = Loc.GetString("ui-actionsui-function-open-abilities-menu"), - TooltipDelay = CustomTooltipDelay - }; - settingsContainer.AddChild(_settingsButton); - settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 }); - - // this allows a 2 column layout if window gets too small - _slotContainer = new GridContainer - { - MaxGridHeight = CalcMaxHeight() - }; - hotbarContainer.AddChild(_slotContainer); - - _loadoutContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalExpand = true, - MouseFilter = MouseFilterMode.Stop - }; - hotbarContainer.AddChild(_loadoutContainer); - - _loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 }); - var previousHotbarIcon = new TextureRect() - { - Texture = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.192dpi.png"), - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center, - SizeFlagsStretchRatio = 1, - TextureScale = (0.5f, 0.5f) - }; - _loadoutContainer.AddChild(previousHotbarIcon); - _loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 }); - _loadoutNumber = new Label - { - Text = "1", - SizeFlagsStretchRatio = 1 - }; - _loadoutContainer.AddChild(_loadoutNumber); - _loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 }); - var nextHotbarIcon = new TextureRect - { - Texture = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.192dpi.png"), - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center, - SizeFlagsStretchRatio = 1, - TextureScale = (0.5f, 0.5f) - }; - _loadoutContainer.AddChild(nextHotbarIcon); - _loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 }); - - _slots = new ActionSlot[ActionsSystem.Slots]; - - _dragShadow = new TextureRect - { - MinSize = (64, 64), - Stretch = TextureRect.StretchMode.Scale, - Visible = false, - SetSize = (64, 64) - }; - UserInterfaceManager.PopupRoot.AddChild(_dragShadow); - - for (byte i = 0; i < ActionsSystem.Slots; i++) - { - var slot = new ActionSlot(this, _menu, i, _timing, _entMan); - _slotContainer.AddChild(slot); - _slots[i] = slot; - } - - DragDropHelper = new DragDropHelper(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag); - DragDropHelper.Deadzone = DragDeadZone; - - MinSize = (10, 400); - } - - protected override void EnteredTree() - { - base.EnteredTree(); - _lockButton.OnPressed += OnLockPressed; - _settingsButton.OnPressed += OnToggleActionsMenu; - _loadoutContainer.OnKeyBindDown += OnHotbarPaginate; - _gameHud.ActionsButtonToggled += OnToggleActionsMenuTopButton; - _gameHud.ActionsButtonDown = false; - _gameHud.ActionsButtonVisible = true; - } - - protected override void ExitedTree() - { - base.ExitedTree(); - StopTargeting(); - _menu.Close(); - _lockButton.OnPressed -= OnLockPressed; - _settingsButton.OnPressed -= OnToggleActionsMenu; - _loadoutContainer.OnKeyBindDown -= OnHotbarPaginate; - _gameHud.ActionsButtonToggled -= OnToggleActionsMenuTopButton; - _gameHud.ActionsButtonDown = false; - _gameHud.ActionsButtonVisible = false; - } - - protected override void Resized() - { - base.Resized(); - _slotContainer.MaxGridHeight = CalcMaxHeight(); - } - - private float CalcMaxHeight() - { - // TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done, - // this is here because there isn't currently a good way to allow the grid to adjust its height based - // on constraints, otherwise we would use anchors to lay it out - - // it looks bad to have an uneven number of slots in the columns, - // so we either do a single column or 2 equal sized columns - if (Height < 650) - { - // 2 column - return 400; - } - else - { - // 1 column - return 900; - } - } - - protected override void UIScaleChanged() - { - _slotContainer.MaxGridHeight = CalcMaxHeight(); - base.UIScaleChanged(); - } - - /// - /// Refresh the display of all the slots in the currently displayed hotbar, - /// to reflect the current component state and assignments of actions component. - /// - public void UpdateUI() - { - _menu.UpdateUI(); - - foreach (var actionSlot in Slots) - { - var action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex]; - - if (action == null) - { - if (SelectingTargetFor == actionSlot) - StopTargeting(true); - actionSlot.Clear(); - continue; - } - - if (Component.Actions.TryGetValue(action, out var actualAction)) - { - UpdateActionSlot(actualAction, actionSlot); - continue; - } - - // Action not in the actions component, but in the assignment list. - // This is either an action that doesn't auto-clear from the menu, or the action menu was locked. - // Show the old action, but make sure it is disabled; - action.Enabled = false; - action.Toggled = false; - - // If we enable the item-sprite, and if the item-sprite has a visual toggle, then the player will be - // able to know whether the item is toggled, even if it is not in their LOS (but in PVS). And for things - // like PDA sprites, the player can even see whether the action's item is currently inside of their PVS. - // SO unless theres some way of "freezing" a sprite-view, we just have to disable it. - action.ItemIconStyle = ItemActionIconStyle.NoItem; - - UpdateActionSlot(action, actionSlot); - } - } - - private void UpdateActionSlot(ActionType action, ActionSlot actionSlot) - { - actionSlot.Assign(action); - - if (!action.Enabled) - { - // just revoked an action we were trying to target with, stop targeting - if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action) - { - StopTargeting(); - } - - actionSlot.Disable(); - } - else - { - actionSlot.Enable(); - } - - actionSlot.UpdateIcons(); - actionSlot.DrawModeChanged(); - } - - private void OnHotbarPaginate(GUIBoundKeyEventArgs args) - { - // rather than clicking the arrows themselves, the user can click the hbox so it's more - // "forgiving" for misclicks, and we simply check which side they are closer to - if (args.Function != EngineKeyFunctions.UIClick) return; - - var rightness = args.RelativePosition.X / _loadoutContainer.Width; - if (rightness > 0.5) - { - ChangeHotbar((byte) ((SelectedHotbar + 1) % ActionsSystem.Hotbars)); - } - else - { - var newBar = SelectedHotbar == 0 ? ActionsSystem.Hotbars - 1 : SelectedHotbar - 1; - ChangeHotbar((byte) newBar); - } - } - - private void ChangeHotbar(byte hotbar) - { - StopTargeting(); - SelectedHotbar = hotbar; - _loadoutNumber.Text = (hotbar + 1).ToString(); - UpdateUI(); - } - - /// - /// If currently targeting with this slot, stops targeting. - /// If currently targeting with no slot or a different slot, switches to - /// targeting with the specified slot. - /// - /// - public void ToggleTargeting(ActionSlot slot) - { - if (SelectingTargetFor == slot) - { - StopTargeting(); - return; - } - StartTargeting(slot); - } - - /// - /// Puts us in targeting mode, where we need to pick either a target point or entity - /// - private void StartTargeting(ActionSlot actionSlot) - { - if (actionSlot.Action == null) - return; - - // If we were targeting something else we should stop - StopTargeting(); - - SelectingTargetFor = actionSlot; - - if (actionSlot.Action is TargetedAction targetAction) - System.StartTargeting(targetAction); - - UpdateUI(); - } - - /// - /// Switch out of targeting mode if currently selecting target for an action - /// - public void StopTargeting(bool updating = false) - { - if (SelectingTargetFor == null) - return; - - SelectingTargetFor = null; - System.StopTargeting(); - - // Sometimes targeting gets stopped mid-UI update. - // in that case, don't need to do a nested UI refresh. - if (!updating) - UpdateUI(); - } - - private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args) - { - ToggleActionsMenu(); - } - - private void OnToggleActionsMenuTopButton(bool open) - { - if (open == _menu.IsOpen) return; - ToggleActionsMenu(); - } - - public void ToggleActionsMenu() - { - if (_menu.IsOpen) - { - _menu.Close(); - } - else - { - _menu.OpenCentered(); - } - } - - private void OnLockPressed(BaseButton.ButtonEventArgs obj) - { - Locked = !Locked; - _lockButton.TextureNormal = Locked ? _lockTexture : _unlockTexture; - } - - private bool OnBeginActionDrag() - { - // only initiate the drag if the slot has an action in it - if (Locked || DragDropHelper.Dragged?.Action == null) return false; - - _dragShadow.Texture = DragDropHelper.Dragged.Action.Icon?.Frame0(); - LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32)); - DragDropHelper.Dragged.CancelPress(); - return true; - } - - private bool OnContinueActionDrag(float frameTime) - { - // stop if there's no action in the slot - if (Locked || DragDropHelper.Dragged?.Action == null) return false; - - // keep dragged entity centered under mouse - LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32)); - // we don't set this visible until frameupdate, otherwise it flickers - _dragShadow.Visible = true; - return true; - } - - private void OnEndActionDrag() - { - _dragShadow.Visible = false; - } - - /// - /// Handle keydown / keyup for one of the slots via a keybinding, simulates mousedown/mouseup on it. - /// - /// slot index to to receive the press (0 corresponds to the one labeled 1, 9 corresponds to the one labeled 0) - public void HandleHotbarKeybind(byte slot, PointerInputCmdHandler.PointerInputCmdArgs args) - { - var actionSlot = _slots[slot]; - actionSlot.Depress(args.State == BoundKeyState.Down); - actionSlot.DrawModeChanged(); - } - - /// - /// Handle hotbar change. - /// - /// hotbar index to switch to - public void HandleChangeHotbarKeybind(byte hotbar, PointerInputCmdHandler.PointerInputCmdArgs args) - { - ChangeHotbar(hotbar); - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - DragDropHelper.Update(args.DeltaSeconds); - } - } -} diff --git a/Content.Client/Administration/Systems/AdminSystem.Menu.cs b/Content.Client/Administration/Systems/AdminSystem.Menu.cs deleted file mode 100644 index c93d0cad94..0000000000 --- a/Content.Client/Administration/Systems/AdminSystem.Menu.cs +++ /dev/null @@ -1,184 +0,0 @@ -using Content.Client.Administration.Managers; -using Content.Client.Administration.UI; -using Content.Client.Administration.UI.Tabs.ObjectsTab; -using Content.Client.Administration.UI.Tabs.PlayerTab; -using Content.Client.HUD; -using Content.Client.Verbs; -using Content.Shared.Input; -using Robust.Client.Console; -using Robust.Client.Graphics; -using Robust.Client.Input; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.Input; -using Robust.Shared.Input.Binding; -using Robust.Shared.Network; - -namespace Content.Client.Administration.Systems -{ - public sealed partial class AdminSystem - { - [Dependency] private readonly INetManager _netManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly IGameHud _gameHud = default!; - [Dependency] private readonly IClientAdminManager _clientAdminManager = default!; - [Dependency] private readonly IClientConGroupController _clientConGroupController = default!; - [Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!; - - [Dependency] private readonly VerbSystem _verbSystem = default!; - - private AdminMenuWindow? _window; - private readonly List _commandWindows = new(); - - private void InitializeMenu() - { - // Reset the AdminMenu Window on disconnect - _netManager.Disconnect += (_, _) => ResetWindow(); - - _inputManager.SetInputCommand(ContentKeyFunctions.OpenAdminMenu, - InputCmdHandler.FromDelegate(_ => Toggle())); - - _clientAdminManager.AdminStatusUpdated += () => - { - // when status changes, show the top button if we can open admin menu. - // if we can't or we lost admin status, close it and hide the button. - _gameHud.AdminButtonVisible = CanOpen(); - if (!_gameHud.AdminButtonVisible) - { - Close(); - } - }; - _gameHud.AdminButtonToggled += (open) => - { - if (open) - { - TryOpen(); - } - else - { - Close(); - } - }; - _gameHud.AdminButtonVisible = CanOpen(); - _gameHud.AdminButtonDown = false; - } - - - public void ResetWindow() - { - _window?.Close(); - _window?.Dispose(); - _window = null; - - foreach (var window in _commandWindows) - { - window.Close(); - window.Dispose(); - } - - _commandWindows.Clear(); - } - - public void OpenCommand(BaseWindow window) - { - _commandWindows.Add(window); - window.OpenCentered(); - } - - public void Open() - { - if (_window == null) - { - _window = new AdminMenuWindow(); - _window.OnClose += Close; - } - - _window.PlayerTabControl.OnEntryPressed += PlayerTabEntryPressed; - _window.ObjectsTabControl.OnEntryPressed += ObjectsTabEntryPressed; - _window.OpenCentered(); - } - - public void Close() - { - if (_window != null) - { - _window.PlayerTabControl.OnEntryPressed -= PlayerTabEntryPressed; - _window.ObjectsTabControl.OnEntryPressed -= ObjectsTabEntryPressed; - } - - _window?.Close(); - - foreach (var window in _commandWindows) - window?.Dispose(); - _commandWindows.Clear(); - } - - /// - /// Checks if the player can open the window - /// - /// True if the player is allowed - public bool CanOpen() - { - return _clientConGroupController.CanAdminMenu(); - } - - /// - /// Checks if the player can open the window and tries to open it - /// - public void TryOpen() - { - if (CanOpen()) - Open(); - } - - public void Toggle() - { - if (_window != null && _window.IsOpen) - { - Close(); - } - else - { - TryOpen(); - } - } - - private void PlayerTabEntryPressed(BaseButton.ButtonEventArgs args) - { - if (args.Button is not PlayerTabEntry button - || button.PlayerUid == null) - return; - - var uid = button.PlayerUid.Value; - var function = args.Event.Function; - - if (function == EngineKeyFunctions.UIClick) - _clientConsoleHost.ExecuteCommand($"vv {uid}"); - else if (function == EngineKeyFunctions.UseSecondary) - _verbSystem.VerbMenu.OpenVerbMenu(uid, true); - else - return; - - args.Event.Handle(); - } - - private void ObjectsTabEntryPressed(BaseButton.ButtonEventArgs args) - { - if (args.Button is not ObjectsTabEntry button) - return; - - var uid = button.AssocEntity; - var function = args.Event.Function; - - if (function == EngineKeyFunctions.UIClick) - _clientConsoleHost.ExecuteCommand($"vv {uid}"); - else if (function == EngineKeyFunctions.UseSecondary) - _verbSystem.VerbMenu.OpenVerbMenu(uid, true); - else - return; - - args.Event.Handle(); - } - } -} diff --git a/Content.Client/Administration/Systems/AdminSystem.cs b/Content.Client/Administration/Systems/AdminSystem.cs index de62caa4ef..b353df680c 100644 --- a/Content.Client/Administration/Systems/AdminSystem.cs +++ b/Content.Client/Administration/Systems/AdminSystem.cs @@ -26,7 +26,6 @@ namespace Content.Client.Administration.Systems base.Initialize(); InitializeOverlay(); - InitializeMenu(); SubscribeNetworkEvent(OnPlayerListChanged); SubscribeNetworkEvent(OnPlayerInfoChanged); SubscribeNetworkEvent(OnRoundRestartCleanup); diff --git a/Content.Client/Administration/Systems/BwoinkSystem.cs b/Content.Client/Administration/Systems/BwoinkSystem.cs index be3e2c0f31..a1d7a17dc9 100644 --- a/Content.Client/Administration/Systems/BwoinkSystem.cs +++ b/Content.Client/Administration/Systems/BwoinkSystem.cs @@ -1,150 +1,18 @@ #nullable enable -using System.Diagnostics.CodeAnalysis; -using Content.Client.Administration.Managers; -using Content.Client.Administration.UI; -using Content.Client.Administration.UI.CustomControls; -using Content.Client.HUD; using Content.Shared.Administration; using JetBrains.Annotations; -using Robust.Client.Graphics; -using Robust.Client.Player; -using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.Audio; using Robust.Shared.Network; -using Robust.Shared.Player; namespace Content.Client.Administration.Systems { [UsedImplicitly] public sealed class BwoinkSystem : SharedBwoinkSystem { - [Dependency] private readonly IClientAdminManager _adminManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IClyde _clyde = default!; - [Dependency] private readonly IGameHud _hud = default!; - - private BwoinkWindow? _adminWindow; - private DefaultWindow? _plainWindow; - private readonly Dictionary _activePanelMap = new(); - - public bool IsOpen => (_adminWindow?.IsOpen ?? false) || (_plainWindow?.IsOpen ?? false); - - public event Action? AdminReceivedAHelp; - public event Action? AdminOpenedAHelp; + public event EventHandler? OnBwoinkTextMessageRecieved; protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs) { - base.OnBwoinkTextMessage(message, eventArgs); - LogBwoink(message); - // Actual line - var window = EnsurePanel(message.UserId); - window.ReceiveLine(message); - // Play a sound if we didn't send it - var localPlayer = _playerManager.LocalPlayer; - if (localPlayer?.UserId != message.TrueSender) - { - SoundSystem.Play("/Audio/Effects/adminhelp.ogg", Filter.Local()); - _clyde.RequestWindowAttention(); - } - - // If they're not an admin force it open so they read - // If it's admin-admin messaging then eh. - if (!_adminManager.HasFlag(AdminFlags.Adminhelp)) - _plainWindow?.Open(); - else - { - _adminWindow?.OnBwoink(message.UserId); - - if (_adminWindow?.IsOpen == true) - return; - AdminReceivedAHelp?.Invoke(); - _hud.SetInfoRed(true); - } - } - - public bool TryGetChannel(NetUserId ch, [NotNullWhen(true)] out BwoinkPanel? bp) => _activePanelMap.TryGetValue(ch, out bp); - - private BwoinkPanel EnsureAdmin(NetUserId channelId) - { - _adminWindow ??= new BwoinkWindow(this); - - if (!_activePanelMap.TryGetValue(channelId, out var existingPanel)) - { - _activePanelMap[channelId] = existingPanel = new BwoinkPanel(this, channelId); - existingPanel.Visible = false; - if (!_adminWindow.BwoinkArea.Children.Contains(existingPanel)) - _adminWindow.BwoinkArea.AddChild(existingPanel); - } - - return existingPanel; - } - - private BwoinkPanel EnsurePlain(NetUserId channelId) - { - BwoinkPanel bp; - if (_plainWindow is null) - { - bp = new BwoinkPanel(this, channelId); - _plainWindow = new DefaultWindow() - { - TitleClass="windowTitleAlert", - HeaderClass="windowHeaderAlert", - Title=Loc.GetString("bwoink-user-title"), - SetSize=(400, 200), - }; - - _plainWindow.Contents.AddChild(bp); - } - else - { - bp = (BwoinkPanel) _plainWindow.Contents.GetChild(0); - } - - return bp; - } - - public BwoinkPanel EnsurePanel(NetUserId channelId) - { - if (_adminManager.HasFlag(AdminFlags.Adminhelp)) - return EnsureAdmin(channelId); - - return EnsurePlain(channelId); - } - - public void Open(NetUserId? channelId = null) - { - if (channelId == null) - { - var localPlayer = _playerManager.LocalPlayer; - if (localPlayer != null) - Open(localPlayer.UserId); - return; - } - - _hud.SetInfoRed(false); - AdminOpenedAHelp?.Invoke(); - - if (_adminManager.HasFlag(AdminFlags.Adminhelp)) - { - SelectChannel(channelId.Value); - _adminWindow?.Open(); - return; - } - - EnsurePlain(channelId.Value); - _plainWindow?.Open(); - } - - public void Close() - { - _adminWindow?.Close(); - _plainWindow?.Close(); - } - - private void SelectChannel(NetUserId uid) - { - _adminWindow ??= new BwoinkWindow(this); - _adminWindow.SelectChannel(uid); + OnBwoinkTextMessageRecieved?.Invoke(this, message); } public void Send(NetUserId channelId, string text) diff --git a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs index 716d1d6e6a..385dbb4cee 100644 --- a/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs +++ b/Content.Client/Administration/UI/AdminMenuWindow.xaml.cs @@ -1,18 +1,12 @@ -using Content.Client.Administration.UI.Tabs; -using Content.Client.HUD; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.IoC; -using Robust.Shared.Localization; namespace Content.Client.Administration.UI { [GenerateTypedNameReferences] public sealed partial class AdminMenuWindow : DefaultWindow { - [Dependency] private readonly IGameHud? _gameHud = default!; - public AdminMenuWindow() { MinSize = (500, 250); @@ -27,19 +21,5 @@ namespace Content.Client.Administration.UI MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-players-tab")); MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-objects-tab")); } - - protected override void EnteredTree() - { - base.EnteredTree(); - if (_gameHud != null) - _gameHud.AdminButtonDown = true; - } - - protected override void ExitedTree() - { - base.ExitedTree(); - if (_gameHud != null) - _gameHud.AdminButtonDown = false; - } } } diff --git a/Content.Client/Administration/UI/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/BwoinkWindow.xaml.cs index 101b3b38d3..eb1fbd4030 100644 --- a/Content.Client/Administration/UI/BwoinkWindow.xaml.cs +++ b/Content.Client/Administration/UI/BwoinkWindow.xaml.cs @@ -6,6 +6,7 @@ using Content.Client.Administration.Systems; using Content.Client.Administration.UI.CustomControls; using Content.Client.Administration.UI.Tabs.AdminTab; using Content.Client.Stylesheets; +using Content.Client.UserInterface.Systems.Bwoink; using Content.Shared.Administration; using Robust.Client.AutoGenerated; using Robust.Client.Console; @@ -27,15 +28,16 @@ namespace Content.Client.Administration.UI { [Dependency] private readonly IClientAdminManager _adminManager = default!; [Dependency] private readonly IClientConsoleHost _console = default!; + private readonly AdminAHelpUIHandler _adminAHelpHelper; - private readonly BwoinkSystem _bwoinkSystem; + //private readonly BwoinkSystem _bwoinkSystem; private PlayerInfo? _currentPlayer = default; - public BwoinkWindow(BwoinkSystem bs) + public BwoinkWindow(AdminAHelpUIHandler adminAHelpHelper) { + _adminAHelpHelper = adminAHelpHelper; RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - _bwoinkSystem = bs; _adminManager.AdminStatusUpdated += FixButtons; FixButtons(); @@ -57,7 +59,7 @@ namespace Content.Client.Administration.UI var sb = new StringBuilder(); sb.Append(info.Connected ? '●' : '○'); sb.Append(' '); - if (_bwoinkSystem.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0) + if (_adminAHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0) { if (panel.Unread < 11) sb.Append(new Rune('➀' + (panel.Unread-1))); @@ -76,8 +78,8 @@ namespace Content.Client.Administration.UI ChannelSelector.Comparison = (a, b) => { - var aChannelExists = _bwoinkSystem.TryGetChannel(a.SessionId, out var ach); - var bChannelExists = _bwoinkSystem.TryGetChannel(b.SessionId, out var bch); + var aChannelExists = _adminAHelpHelper.TryGetChannel(a.SessionId, out var ach); + var bChannelExists = _adminAHelpHelper.TryGetChannel(b.SessionId, out var bch); if (!aChannelExists && !bChannelExists) return 0; @@ -174,7 +176,7 @@ namespace Content.Client.Administration.UI var sb = new StringBuilder(); sb.Append(pl.Connected ? '●' : '○'); sb.Append(' '); - if (_bwoinkSystem.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0) + if (_adminAHelpHelper.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0) { if (panel.Unread < 11) sb.Append(new Rune('➀' + (panel.Unread-1))); @@ -200,7 +202,7 @@ namespace Content.Client.Administration.UI { foreach (var bw in BwoinkArea.Children) bw.Visible = false; - var panel = _bwoinkSystem.EnsurePanel(ch); + var panel = _adminAHelpHelper.EnsurePanel(ch); panel.Visible = true; } diff --git a/Content.Client/Administration/UI/CustomControls/BwoinkPanel.xaml.cs b/Content.Client/Administration/UI/CustomControls/BwoinkPanel.xaml.cs index 70714b9247..16edcc1270 100644 --- a/Content.Client/Administration/UI/CustomControls/BwoinkPanel.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/BwoinkPanel.xaml.cs @@ -1,6 +1,7 @@ #nullable enable using System; using Content.Client.Administration.Systems; +using Content.Client.UserInterface.Systems.Bwoink; using Content.Shared.Administration; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; @@ -13,17 +14,15 @@ namespace Content.Client.Administration.UI.CustomControls [GenerateTypedNameReferences] public sealed partial class BwoinkPanel : BoxContainer { - private readonly BwoinkSystem _bwoinkSystem; - public readonly NetUserId ChannelId; + private readonly Action _messageSender; public int Unread { get; private set; } = 0; public DateTime LastMessage { get; private set; } = DateTime.MinValue; - public BwoinkPanel(BwoinkSystem bwoinkSys, NetUserId userId) + public BwoinkPanel(Action messageSender) { RobustXamlLoader.Load(this); - _bwoinkSystem = bwoinkSys; - ChannelId = userId; + _messageSender = messageSender; OnVisibilityChanged += c => { @@ -38,7 +37,7 @@ namespace Content.Client.Administration.UI.CustomControls if (string.IsNullOrWhiteSpace(args.Text)) return; - _bwoinkSystem.Send(ChannelId, args.Text); + _messageSender.Invoke(args.Text); SenderLineEdit.Clear(); } diff --git a/Content.Client/Alerts/UI/AlertsUI.xaml b/Content.Client/Alerts/UI/AlertsUI.xaml deleted file mode 100644 index eac250bdac..0000000000 --- a/Content.Client/Alerts/UI/AlertsUI.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/Content.Client/Alerts/UI/AlertsUI.xaml.cs b/Content.Client/Alerts/UI/AlertsUI.xaml.cs deleted file mode 100644 index e23f2fd140..0000000000 --- a/Content.Client/Alerts/UI/AlertsUI.xaml.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Client.Chat.Managers; -using Content.Client.Chat.UI; -using Content.Shared.Alert; -using Robust.Client.AutoGenerated; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.IoC; -using Robust.Shared.Log; - -namespace Content.Client.Alerts.UI; - -public sealed class AlertsFramePresenter : IDisposable -{ - [Dependency] private readonly IEntitySystemManager _systemManager = default!; - [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - - private IAlertsFrameView _alertsFrame; - private ClientAlertsSystem? _alertsSystem; - - public AlertsFramePresenter() - { - // This is a lot easier than a factory - IoCManager.InjectDependencies(this); - - _alertsFrame = new AlertsUI(_chatManager); - _userInterfaceManager.StateRoot.AddChild((AlertsUI) _alertsFrame); - - // This is required so that if we load after the system is initialized, we can bind to it immediately - if (_systemManager.TryGetEntitySystem(out var alertsSystem)) - SystemBindingChanged(alertsSystem); - - _systemManager.SystemLoaded += OnSystemLoaded; - _systemManager.SystemUnloaded += OnSystemUnloaded; - - _alertsFrame.AlertPressed += OnAlertPressed; - - // initially populate the frame if system is available - var alerts = alertsSystem?.ActiveAlerts; - if (alerts != null) - { - SystemOnSyncAlerts(alertsSystem, alerts); - } - } - - /// - public void Dispose() - { - _userInterfaceManager.StateRoot.RemoveChild((AlertsUI) _alertsFrame); - _alertsFrame.Dispose(); - _alertsFrame = null!; - - SystemBindingChanged(null); - _systemManager.SystemLoaded -= OnSystemLoaded; - _systemManager.SystemUnloaded -= OnSystemUnloaded; - } - - private void OnAlertPressed(object? sender, AlertType e) - { - _alertsSystem?.AlertClicked(e); - } - - private void SystemOnClearAlerts(object? sender, EventArgs e) - { - _alertsFrame.ClearAllControls(); - } - - private void SystemOnSyncAlerts(object? sender, IReadOnlyDictionary e) - { - if (sender is ClientAlertsSystem system) - _alertsFrame.SyncControls(system, system.AlertOrder, e); - } - - //TODO: This system binding boilerplate seems to be duplicated between every presenter - // prob want to pull it out into a generic object with callbacks for Onbind/OnUnbind - #region System Binding - - private void OnSystemLoaded(object? sender, SystemChangedArgs args) - { - if (args.System is ClientAlertsSystem system) SystemBindingChanged(system); - } - - private void OnSystemUnloaded(object? sender, SystemChangedArgs args) - { - if (args.System is ClientAlertsSystem) SystemBindingChanged(null); - } - - private void SystemBindingChanged(ClientAlertsSystem? newSystem) - { - if (newSystem is null) - { - if (_alertsSystem is null) - return; - - UnbindFromSystem(); - } - else - { - if (_alertsSystem is null) - { - BindToSystem(newSystem); - return; - } - - UnbindFromSystem(); - BindToSystem(newSystem); - } - } - - private void BindToSystem(ClientAlertsSystem system) - { - _alertsSystem = system; - system.SyncAlerts += SystemOnSyncAlerts; - system.ClearAlerts += SystemOnClearAlerts; - } - - private void UnbindFromSystem() - { - var system = _alertsSystem; - - if (system is null) - throw new InvalidOperationException(); - - system.SyncAlerts -= SystemOnSyncAlerts; - system.ClearAlerts -= SystemOnClearAlerts; - } - - #endregion -} - -/// -/// This is the frame of vertical set of alerts that show up on the HUD. -/// -public interface IAlertsFrameView : IDisposable -{ - event EventHandler? AlertPressed; - - void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, - IReadOnlyDictionary alertStates); - void ClearAllControls(); -} - -/// -/// The status effects display on the right side of the screen. -/// -[GenerateTypedNameReferences] -public sealed partial class AlertsUI : Control, IAlertsFrameView -{ - // also known as Control.Children? - private readonly Dictionary _alertControls = new(); - - public AlertsUI(IChatManager chatManager) - { - _chatManager = chatManager; - RobustXamlLoader.Load(this); - - LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin); - LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End); - LayoutContainer.SetAnchorTop(this, 0f); - LayoutContainer.SetAnchorRight(this, 1f); - LayoutContainer.SetAnchorBottom(this, 1f); - LayoutContainer.SetMarginBottom(this, -180); - LayoutContainer.SetMarginTop(this, 250); - LayoutContainer.SetMarginRight(this, -10); - } - - public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, - IReadOnlyDictionary alertStates) - { - // remove any controls with keys no longer present - if (SyncRemoveControls(alertStates)) return; - - // now we know that alertControls contains alerts that should still exist but - // may need to updated, - // also there may be some new alerts we need to show. - // further, we need to ensure they are ordered w.r.t their configured order - SyncUpdateControls(alertsSystem, alertOrderPrototype, alertStates); - } - - public void ClearAllControls() - { - foreach (var alertControl in _alertControls.Values) - { - alertControl.OnPressed -= AlertControlPressed; - alertControl.Dispose(); - } - - _alertControls.Clear(); - } - - public event EventHandler? AlertPressed; - - //TODO: This control caring about it's layout relative to other controls in the tree is terrible - // the presenters or gamescreen should be dealing with this - // probably want to tackle this after chatbox gets MVP'd - #region Spaghetti - - public const float ChatSeparation = 38f; - private readonly IChatManager _chatManager; - - protected override void EnteredTree() - { - base.EnteredTree(); - _chatManager.OnChatBoxResized += OnChatResized; - OnChatResized(new ChatResizedEventArgs(HudChatBox.InitialChatBottom)); - } - - protected override void ExitedTree() - { - base.ExitedTree(); - _chatManager.OnChatBoxResized -= OnChatResized; - } - - private void OnChatResized(ChatResizedEventArgs chatResizedEventArgs) - { - // resize us to fit just below the chat box - if (_chatManager.CurrentChatBox != null) - LayoutContainer.SetMarginTop(this, chatResizedEventArgs.NewBottom + ChatSeparation); - else - LayoutContainer.SetMarginTop(this, 250); - } - - #endregion - - // This makes no sense but I'm leaving it in place in case I break anything by removing it. - protected override void Resized() - { - // TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done, - // this is here because there isn't currently a good way to allow the grid to adjust its height based - // on constraints, otherwise we would use anchors to lay it out - base.Resized(); - AlertContainer.MaxGridHeight = Height; - } - - protected override void UIScaleChanged() - { - AlertContainer.MaxGridHeight = Height; - base.UIScaleChanged(); - } - - private bool SyncRemoveControls(IReadOnlyDictionary alertStates) - { - var toRemove = new List(); - foreach (var existingKey in _alertControls.Keys) - { - if (!alertStates.ContainsKey(existingKey)) toRemove.Add(existingKey); - } - - foreach (var alertKeyToRemove in toRemove) - { - _alertControls.Remove(alertKeyToRemove, out var control); - if (control == null) return true; - AlertContainer.Children.Remove(control); - } - - return false; - } - - private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype, - IReadOnlyDictionary alertStates) - { - foreach (var (alertKey, alertState) in alertStates) - { - if (!alertKey.AlertType.HasValue) - { - Logger.WarningS("alert", "found alertkey without alerttype," + - " alert keys should never be stored without an alerttype set: {0}", alertKey); - continue; - } - - var alertType = alertKey.AlertType.Value; - if (!alertsSystem.TryGet(alertType, out var newAlert)) - { - Logger.ErrorS("alert", "Unrecognized alertType {0}", alertType); - continue; - } - - if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) && - existingAlertControl.Alert.AlertType == newAlert.AlertType) - { - // key is the same, simply update the existing control severity / cooldown - existingAlertControl.SetSeverity(alertState.Severity); - existingAlertControl.Cooldown = alertState.Cooldown; - } - else - { - if (existingAlertControl != null) AlertContainer.Children.Remove(existingAlertControl); - - // this is a new alert + alert key or just a different alert with the same - // key, create the control and add it in the appropriate order - var newAlertControl = CreateAlertControl(newAlert, alertState); - - //TODO: Can the presenter sort the states before giving it to us? - if (alertOrderPrototype != null) - { - var added = false; - foreach (var alertControl in AlertContainer.Children) - { - if (alertOrderPrototype.Compare(newAlert, ((AlertControl) alertControl).Alert) >= 0) - continue; - - var idx = alertControl.GetPositionInParent(); - AlertContainer.Children.Add(newAlertControl); - newAlertControl.SetPositionInParent(idx); - added = true; - break; - } - - if (!added) AlertContainer.Children.Add(newAlertControl); - } - else - AlertContainer.Children.Add(newAlertControl); - - _alertControls[newAlert.AlertKey] = newAlertControl; - } - } - } - - private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState) - { - var alertControl = new AlertControl(alert, alertState.Severity) - { - Cooldown = alertState.Cooldown - }; - alertControl.OnPressed += AlertControlPressed; - return alertControl; - } - - private void AlertControlPressed(BaseButton.ButtonEventArgs args) - { - if (args.Button is not AlertControl control) - return; - - if (args.Event.Function != EngineKeyFunctions.UIClick) - return; - - AlertPressed?.Invoke(this, control.Alert.AlertType); - } -} diff --git a/Content.Client/Changelog/ChangelogButton.cs b/Content.Client/Changelog/ChangelogButton.cs index b029770857..6e8862492c 100644 --- a/Content.Client/Changelog/ChangelogButton.cs +++ b/Content.Client/Changelog/ChangelogButton.cs @@ -1,6 +1,4 @@ using Content.Client.Stylesheets; -using Content.Client.UserInterface.Systems.EscapeMenu; -using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; namespace Content.Client.Changelog @@ -13,8 +11,6 @@ namespace Content.Client.Changelog { IoCManager.InjectDependencies(this); - OnPressed += OnChangelogPressed; - // So that measuring before opening returns a correct height, // and the window has the correct size when opened. Text = " "; @@ -35,11 +31,6 @@ namespace Content.Client.Changelog _changelogManager.NewChangelogEntriesChanged -= UpdateStuff; } - private void OnChangelogPressed(ButtonEventArgs obj) - { - IoCManager.Resolve().GetUIController().ToggleWindow(); - } - private void UpdateStuff() { if (_changelogManager.NewChangelogEntries) diff --git a/Content.Client/Changelog/ChangelogWindow.xaml.cs b/Content.Client/Changelog/ChangelogWindow.xaml.cs index f666693772..1cb8377c99 100644 --- a/Content.Client/Changelog/ChangelogWindow.xaml.cs +++ b/Content.Client/Changelog/ChangelogWindow.xaml.cs @@ -205,7 +205,7 @@ namespace Content.Client.Changelog public void Execute(IConsoleShell shell, string argStr, string[] args) { - IoCManager.Resolve().GetUIController().ToggleWindow(); + IoCManager.Resolve().GetUIController().OpenWindow(); } } } diff --git a/Content.Client/CharacterInfo/CharacterInfoSystem.cs b/Content.Client/CharacterInfo/CharacterInfoSystem.cs new file mode 100644 index 0000000000..7257c230ba --- /dev/null +++ b/Content.Client/CharacterInfo/CharacterInfoSystem.cs @@ -0,0 +1,58 @@ +using Content.Shared.CharacterInfo; +using Content.Shared.Objectives; +using Robust.Client.GameObjects; +using Robust.Client.Player; + +namespace Content.Client.CharacterInfo; + +public sealed class CharacterInfoSystem : EntitySystem +{ + [Dependency] private readonly IPlayerManager _players = default!; + + public event Action? OnCharacterUpdate; + public event Action? OnCharacterDetached; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPlayerAttached); + + SubscribeNetworkEvent(OnCharacterInfoEvent); + } + + public void RequestCharacterInfo() + { + var entity = _players.LocalPlayer?.ControlledEntity; + if (entity == null) + { + return; + } + + RaiseNetworkEvent(new RequestCharacterInfoEvent(entity.Value)); + } + + private void OnPlayerAttached(PlayerAttachSysMessage msg) + { + if (msg.AttachedEntity == default) + { + OnCharacterDetached?.Invoke(); + } + } + + private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args) + { + var sprite = CompOrNull(msg.EntityUid); + var data = new CharacterData(msg.JobTitle, msg.Objectives, msg.Briefing, sprite, Name(msg.EntityUid)); + + OnCharacterUpdate?.Invoke(data); + } + + public readonly record struct CharacterData( + string Job, + Dictionary> Objectives, + string Briefing, + ISpriteComponent? Sprite, + string EntityName + ); +} diff --git a/Content.Client/CharacterInfo/Components/CharacterInfoComponent.cs b/Content.Client/CharacterInfo/Components/CharacterInfoComponent.cs deleted file mode 100644 index a5b52b7c82..0000000000 --- a/Content.Client/CharacterInfo/Components/CharacterInfoComponent.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Content.Client.CharacterInterface; -using Content.Client.Stylesheets; -using Content.Client.UserInterface.Controls; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.CharacterInfo.Components -{ - [RegisterComponent] - public sealed class CharacterInfoComponent : Component, ICharacterUI - { - public CharacterInfoControl Control = default!; - - public Control Scene { get; set; } = default!; - public UIPriority Priority => UIPriority.Info; - - public void Opened() - { - IoCManager.Resolve().GetEntitySystem().RequestCharacterInfo(Owner); - } - - public sealed class CharacterInfoControl : BoxContainer - { - public SpriteView SpriteView { get; } - public Label NameLabel { get; } - public Label SubText { get; } - - public BoxContainer ObjectivesContainer { get; } - - public CharacterInfoControl() - { - IoCManager.InjectDependencies(this); - - Orientation = LayoutOrientation.Vertical; - - AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Children = - { - (SpriteView = new SpriteView { OverrideDirection = Direction.South, Scale = (2,2)}), - new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - VerticalAlignment = VAlignment.Top, - Children = - { - (NameLabel = new Label()), - (SubText = new Label - { - VerticalAlignment = VAlignment.Top, - StyleClasses = {StyleBase.StyleClassLabelSubText}, - - }) - } - } - } - }); - - AddChild(new Label - { - Text = Loc.GetString("character-info-objectives-label"), - HorizontalAlignment = HAlignment.Center - }); - ObjectivesContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - AddChild(ObjectivesContainer); - - AddChild(new Placeholder - { - PlaceholderText = Loc.GetString("character-info-roles-antagonist-text") - }); - } - } - } -} diff --git a/Content.Client/CharacterInfo/Components/CharacterInfoSystem.cs b/Content.Client/CharacterInfo/Components/CharacterInfoSystem.cs deleted file mode 100644 index 1423ef0cdb..0000000000 --- a/Content.Client/CharacterInfo/Components/CharacterInfoSystem.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Content.Client.UserInterface.Controls; -using Content.Shared.CharacterInfo; -using Content.Shared.Objectives; -using Robust.Client.GameObjects; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.Utility; - -namespace Content.Client.CharacterInfo.Components; - -public sealed class CharacterInfoSystem : EntitySystem -{ - [Dependency] private readonly SpriteSystem _sprite = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeNetworkEvent(OnCharacterInfoEvent); - SubscribeLocalEvent(OnComponentAdd); - } - - private void OnComponentAdd(EntityUid uid, CharacterInfoComponent component, ComponentAdd args) - { - component.Scene = component.Control = new CharacterInfoComponent.CharacterInfoControl(); - } - - public void RequestCharacterInfo(EntityUid entityUid) - { - RaiseNetworkEvent(new RequestCharacterInfoEvent(entityUid)); - } - - private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args) - { - if (!EntityManager.TryGetComponent(msg.EntityUid, out CharacterInfoComponent? characterInfoComponent)) - return; - - UpdateUI(characterInfoComponent, msg.JobTitle, msg.Objectives, msg.Briefing); - if (EntityManager.TryGetComponent(msg.EntityUid, out ISpriteComponent? spriteComponent)) - { - characterInfoComponent.Control.SpriteView.Sprite = spriteComponent; - } - - if (!EntityManager.TryGetComponent(msg.EntityUid, out MetaDataComponent? metadata)) - return; - characterInfoComponent.Control.NameLabel.Text = metadata.EntityName; - } - - private void UpdateUI(CharacterInfoComponent comp, string jobTitle, Dictionary> objectives, string briefing) - { - comp.Control.SubText.Text = jobTitle; - - comp.Control.ObjectivesContainer.RemoveAllChildren(); - foreach (var (groupId, objectiveConditions) in objectives) - { - var vbox = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Vertical, - Modulate = Color.Gray - }; - - vbox.AddChild(new Label - { - Text = groupId, - Modulate = Color.LightSkyBlue - }); - - foreach (var objectiveCondition in objectiveConditions) - { - var hbox = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal - }; - hbox.AddChild(new ProgressTextureRect - { - Texture = _sprite.Frame0(objectiveCondition.SpriteSpecifier), - Progress = objectiveCondition.Progress, - VerticalAlignment = Control.VAlignment.Center - }); - hbox.AddChild(new Control - { - MinSize = (10,0) - }); - hbox.AddChild(new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Vertical, - Children = - { - new Label{Text = objectiveCondition.Title}, - new Label{Text = objectiveCondition.Description} - } - } - ); - vbox.AddChild(hbox); - } - var briefinghBox = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal - }; - - briefinghBox.AddChild(new Label - { - Text = briefing, - Modulate = Color.Yellow - }); - - vbox.AddChild(briefinghBox); - comp.Control.ObjectivesContainer.AddChild(vbox); - } - } -} diff --git a/Content.Client/CharacterInfo/Components/ICharacterUI.cs b/Content.Client/CharacterInfo/Components/ICharacterUI.cs deleted file mode 100644 index f71f1b0ca4..0000000000 --- a/Content.Client/CharacterInfo/Components/ICharacterUI.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Client.CharacterInterface; -using Robust.Client.UserInterface; - -namespace Content.Client.CharacterInfo.Components -{ - /// - /// An interface which is gathered to assemble the character window from multiple components - /// - public interface ICharacterUI - { - /// - /// The control which holds the character user interface to be included in the window - /// - Control Scene { get; } - - /// - /// The order it will appear in the character UI, higher is lower - /// - UIPriority Priority { get; } - - /// - /// Called when the CharacterUi was opened - /// - void Opened(){} - } -} diff --git a/Content.Client/CharacterInterface/CharacterInterfaceComponent.cs b/Content.Client/CharacterInterface/CharacterInterfaceComponent.cs deleted file mode 100644 index 1d30edb9e0..0000000000 --- a/Content.Client/CharacterInterface/CharacterInterfaceComponent.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using Content.Client.CharacterInfo.Components; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.GameObjects; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.CharacterInterface -{ - /// - /// A semi-abstract component which gets added to entities upon attachment and collects all character - /// user interfaces into a single window and keybind for the user - /// - [RegisterComponent] - public sealed class CharacterInterfaceComponent : Component - { - /// - /// Window to hold each of the character interfaces - /// - /// - /// Null if it would otherwise be empty. - /// - public CharacterWindow? Window { get; set; } - - public List? UIComponents; - - /// - /// A window that collects and shows all the individual character user interfaces - /// - public sealed class CharacterWindow : DefaultWindow - { - private readonly List _windowComponents; - - public CharacterWindow(List windowComponents) - { - Title = Loc.GetString("character-info-title"); - - var contentsVBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - - var mainScrollContainer = new ScrollContainer { }; - mainScrollContainer.AddChild(contentsVBox); - - Contents.AddChild(mainScrollContainer); - - windowComponents.Sort((a, b) => ((int) a.Priority).CompareTo((int) b.Priority)); - foreach (var element in windowComponents) - { - contentsVBox.AddChild(element.Scene); - } - - _windowComponents = windowComponents; - } - - protected override void Opened() - { - base.Opened(); - foreach (var windowComponent in _windowComponents) - { - windowComponent.Opened(); - } - } - } - } - - /// - /// Determines ordering of the character user interface, small values come sooner - /// - public enum UIPriority - { - First = 0, - Info = 5, - Species = 100, - Last = 99999 - } -} diff --git a/Content.Client/CharacterInterface/CharacterInterfaceSystem.cs b/Content.Client/CharacterInterface/CharacterInterfaceSystem.cs deleted file mode 100644 index 09a17e7459..0000000000 --- a/Content.Client/CharacterInterface/CharacterInterfaceSystem.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Linq; -using Content.Client.CharacterInfo.Components; -using Content.Client.HUD; -using Content.Shared.Input; -using JetBrains.Annotations; -using Robust.Client.GameObjects; -using Robust.Client.Input; -using Robust.Client.Player; -using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.GameObjects; -using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; - -namespace Content.Client.CharacterInterface -{ - [UsedImplicitly] - public sealed class CharacterInterfaceSystem : EntitySystem - { - [Dependency] private readonly IGameHud _gameHud = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; - - public override void Initialize() - { - base.Initialize(); - - CommandBinds.Builder - .Bind(ContentKeyFunctions.OpenCharacterMenu, - InputCmdHandler.FromDelegate(_ => HandleOpenCharacterMenu())) - .Register(); - - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnComponentRemove); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); - } - - public override void Shutdown() - { - CommandBinds.Unregister(); - base.Shutdown(); - } - - private void OnComponentInit(EntityUid uid, CharacterInterfaceComponent comp, ComponentInit args) - { - //Use all the character ui interfaced components to create the character window - comp.UIComponents = EntityManager.GetComponents(uid).ToList(); - if (comp.UIComponents.Count == 0) - return; - - comp.Window = new CharacterInterfaceComponent.CharacterWindow(comp.UIComponents) - { - SetSize = (545, 400) - }; - - comp.Window.OnClose += () => _gameHud.CharacterButtonDown = false; - } - - private void OnComponentRemove(EntityUid uid, CharacterInterfaceComponent comp, ComponentRemove args) - { - if (comp.UIComponents != null) - { - foreach (var component in comp.UIComponents) - { - // Make sure these don't get deleted when the window is disposed. - component.Scene.Orphan(); - } - } - - comp.UIComponents = null; - - comp.Window?.Close(); - comp.Window = null; - - _inputManager.SetInputCommand(ContentKeyFunctions.OpenCharacterMenu, null); - } - - private void OnPlayerAttached(EntityUid uid, CharacterInterfaceComponent comp, PlayerAttachedEvent args) - { - if (comp.Window == null) - return; - - _gameHud.CharacterButtonVisible = true; - _gameHud.CharacterButtonToggled += ToggleWindow; - } - - private void ToggleWindow(bool toggle) - { - if (!TryComp(_playerManager.LocalPlayer?.Session?.AttachedEntity, out CharacterInterfaceComponent? comp)) - return; - - if (toggle) - comp.Window?.OpenCentered(); - else - comp.Window?.Close(); - } - - private void OnPlayerDetached(EntityUid uid, CharacterInterfaceComponent comp, PlayerDetachedEvent args) - { - if (comp.Window == null) - return; - - _gameHud.CharacterButtonVisible = false; - _gameHud.CharacterButtonToggled -= ToggleWindow; - comp.Window.Close(); - } - - private void HandleOpenCharacterMenu() - { - if (_playerManager.LocalPlayer?.ControlledEntity == null - || !EntityManager.TryGetComponent(_playerManager.LocalPlayer.ControlledEntity, out CharacterInterfaceComponent? characterInterface)) - return; - - var menu = characterInterface.Window; - if (menu == null) - return; - - if (menu.IsOpen) - { - if (menu.IsAtFront()) - _setOpenValue(menu, false); - else - menu.MoveToFront(); - } - else - { - _setOpenValue(menu, true); - } - } - - private void _setOpenValue(DefaultWindow menu, bool value) - { - _gameHud.CharacterButtonDown = value; - if (value) - menu.OpenCentered(); - else - menu.Close(); - } - } -} diff --git a/Content.Client/Chat/ChatHelper.cs b/Content.Client/Chat/ChatHelper.cs deleted file mode 100644 index a0db4f97d5..0000000000 --- a/Content.Client/Chat/ChatHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Content.Shared.Chat; -using Robust.Shared.Maths; - -namespace Content.Client.Chat -{ - public sealed class ChatHelper - { - public static Color ChatColor(ChatChannel channel) => - channel switch - { - ChatChannel.Server => Color.Orange, - ChatChannel.Radio => Color.LimeGreen, - ChatChannel.LOOC => Color.MediumTurquoise, - ChatChannel.OOC => Color.LightSkyBlue, - ChatChannel.Dead => Color.MediumPurple, - ChatChannel.Admin => Color.Red, - ChatChannel.Whisper => Color.DarkGray, - _ => Color.LightGray - }; - } -} diff --git a/Content.Client/Chat/ChatInput.cs b/Content.Client/Chat/ChatInput.cs deleted file mode 100644 index 3633e66e69..0000000000 --- a/Content.Client/Chat/ChatInput.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Content.Client.Chat.UI; -using Content.Client.Gameplay; -using Content.Client.Viewport; -using Content.Shared.Chat; -using Content.Shared.Input; -using Robust.Client.Input; -using Robust.Shared.Input.Binding; - -namespace Content.Client.Chat -{ - public static class ChatInput - { - public static void SetupChatInputHandlers(IInputManager inputManager, ChatBox chatBox) - { - inputManager.SetInputCommand(ContentKeyFunctions.FocusChat, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChat(chatBox))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusLocalChat, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Local))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusWhisperChat, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Whisper))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusOOC, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.OOC))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusAdminChat, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Admin))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusRadio, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Radio))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusDeadChat, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Dead))); - - inputManager.SetInputCommand(ContentKeyFunctions.FocusConsoleChat, - InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Console))); - - inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward, - InputCmdHandler.FromDelegate(_ => chatBox.CycleChatChannel(true))); - - inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward, - InputCmdHandler.FromDelegate(_ => chatBox.CycleChatChannel(false))); - } - } -} diff --git a/Content.Client/Chat/ChatSystem.cs b/Content.Client/Chat/ChatSystem.cs index 48e7b41bae..d594347845 100644 --- a/Content.Client/Chat/ChatSystem.cs +++ b/Content.Client/Chat/ChatSystem.cs @@ -1,65 +1,5 @@ -using Content.Client.Chat.Managers; -using Content.Client.Chat.UI; -using Content.Client.Examine; using Content.Shared.Chat; -using Content.Shared.Examine; -using Robust.Client.Player; -using Robust.Shared.Map; namespace Content.Client.Chat; -public sealed class ChatSystem : SharedChatSystem -{ - [Dependency] private readonly IChatManager _manager = default!; - [Dependency] private readonly IPlayerManager _player = default!; - [Dependency] private readonly ExamineSystem _examineSystem = default!; - - public override void FrameUpdate(float frameTime) - { - base.FrameUpdate(frameTime); - - var player = _player.LocalPlayer?.ControlledEntity; - var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data) - => uid == data.compOwner || uid == data.attachedEntity; - var bubbles = _manager.GetSpeechBubbles(); - var playerPos = player != null ? Transform(player.Value).MapPosition : MapCoordinates.Nullspace; - - var occluded = player != null && _examineSystem.IsOccluded(player.Value); - - foreach (var (ent, bubs) in bubbles) - { - if (Deleted(ent)) - { - SetBubbles(bubs, false); - continue; - } - - if (ent == player) - { - SetBubbles(bubs, true); - continue; - } - - var otherPos = Transform(ent).MapPosition; - - if (occluded && !ExamineSystemShared.InRangeUnOccluded( - playerPos, - otherPos, 0f, - (ent, player), predicate)) - { - SetBubbles(bubs, false); - continue; - } - - SetBubbles(bubs, true); - } - } - - private void SetBubbles(List bubbles, bool value) - { - foreach (var bubble in bubbles) - { - bubble.Visible = value; - } - } -} +public sealed class ChatSystem : SharedChatSystem {} diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index 4f2c9988d5..9b5209d9a3 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -1,327 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Content.Client.Administration.Managers; -using Content.Client.Chat.UI; -using Content.Client.Gameplay; using Content.Client.Ghost; -using Content.Client.Viewport; using Content.Shared.Administration; -using Content.Shared.CCVar; using Content.Shared.Chat; using Robust.Client.Console; -using Robust.Client.Graphics; -using Robust.Client.Player; -using Robust.Client.State; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Network; -using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Client.Chat.Managers { - internal sealed class ChatManager : IChatManager, IPostInjectInit + internal sealed class ChatManager : IChatManager { - private struct SpeechBubbleData - { - public string Message; - public SpeechBubble.SpeechType Type; - } + [Dependency] private readonly IClientConsoleHost _consoleHost = default!; + [Dependency] private readonly IClientAdminManager _adminMgr = default!; + [Dependency] private readonly IEntitySystemManager _systems = default!; private ISawmill _sawmill = default!; - /// - /// The max amount of chars allowed to fit in a single speech bubble. - /// - private const int SingleBubbleCharLimit = 100; - - /// - /// Base queue delay each speech bubble has. - /// - private const float BubbleDelayBase = 0.2f; - - /// - /// Factor multiplied by speech bubble char length to add to delay. - /// - private const float BubbleDelayFactor = 0.8f / SingleBubbleCharLimit; - - /// - /// The max amount of speech bubbles over a single entity at once. - /// - private const int SpeechBubbleCap = 4; - - /// - /// The max amount of characters an entity can send in one message - /// - public int MaxMessageLength => _cfg.GetCVar(CCVars.ChatMaxMessageLength); - - private readonly List _history = new(); - public IReadOnlyList History => _history; - - // currently enabled channel filters set by the user. - // All values default to on, even if they aren't a filterable chat channel currently. - // Note that these are persisted here, at the manager, - // rather than the chatbox so that these settings persist between instances of different - // chatboxes. - public ChatChannel ChannelFilters { get; private set; } = (ChatChannel) ushort.MaxValue; - - // Maintains which channels a client should be able to filter (for showing in the chatbox) - // and select (for attempting to send on). - // This may not always actually match with what the server will actually allow them to - // send / receive on, it is only what the user can select in the UI. For example, - // if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up - // to the server to handle invalid attempts to use particular channels and not send messages for - // channels the user shouldn't be able to hear. - // - // Note that Command is an available selection in the chatbox channel selector, - // which is not actually a chat channel but is always available. - public ChatSelectChannel SelectableChannels { get; private set; } - public ChatChannel FilterableChannels { get; private set; } - - /// - /// For currently disabled chat filters, - /// unread messages (messages received since the channel has been filtered out). - /// - private readonly Dictionary _unreadMessages = new(); - - public IReadOnlyDictionary UnreadMessages => _unreadMessages; - - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IClientNetManager _netManager = default!; - [Dependency] private readonly IClientConsoleHost _consoleHost = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IEyeManager _eyeManager = default!; - [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - [Dependency] private readonly IClientAdminManager _adminMgr = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IStateManager _stateManager = default!; - - /// - /// Current chat box control. This can be modified, so do not depend on saving a reference to this. - /// - public ChatBox? CurrentChatBox { get; private set; } - - /// - /// Invoked when CurrentChatBox is resized (including after setting initial default size) - /// - public event Action? OnChatBoxResized; - - public event Action? ChatPermissionsUpdated; - public event Action? UnreadMessageCountsUpdated; - public event Action? MessageAdded; - public event Action? FiltersUpdated; - - private Control _speechBubbleRoot = null!; - - /// - /// Speech bubbles that are currently visible on screen. - /// We track them to push them up when new ones get added. - /// - private readonly Dictionary> _activeSpeechBubbles = - new(); - - /// - /// Speech bubbles that are to-be-sent because of the "rate limit" they have. - /// - private readonly Dictionary _queuedSpeechBubbles - = new(); - public void Initialize() { _sawmill = Logger.GetSawmill("chat"); _sawmill.Level = LogLevel.Info; - _netManager.RegisterNetMessage(OnChatMessage); - - _speechBubbleRoot = new LayoutContainer(); - LayoutContainer.SetAnchorPreset(_speechBubbleRoot, LayoutContainer.LayoutPreset.Wide); - _userInterfaceManager.StateRoot.AddChild(_speechBubbleRoot); - _speechBubbleRoot.SetPositionFirst(); - _stateManager.OnStateChanged += _ => UpdateChannelPermissions(); } - public IReadOnlyDictionary> GetSpeechBubbles() => _activeSpeechBubbles; - - public void PostInject() + public void SendMessage(ReadOnlyMemory text, ChatSelectChannel channel) { - _adminMgr.AdminStatusUpdated += UpdateChannelPermissions; - _playerManager.LocalPlayerChanged += OnLocalPlayerChanged; - OnLocalPlayerChanged(new LocalPlayerChangedEventArgs(null, _playerManager.LocalPlayer)); - } - - private void OnLocalPlayerChanged(LocalPlayerChangedEventArgs obj) - { - if (obj.OldPlayer != null) - { - obj.OldPlayer.EntityAttached -= OnLocalPlayerEntityAttached; - obj.OldPlayer.EntityDetached -= OnLocalPlayerEntityDetached; - } - - if (obj.NewPlayer != null) - { - obj.NewPlayer.EntityAttached += OnLocalPlayerEntityAttached; - obj.NewPlayer.EntityDetached += OnLocalPlayerEntityDetached; - } - - UpdateChannelPermissions(); - } - - private void OnLocalPlayerEntityAttached(EntityAttachedEventArgs obj) - { - UpdateChannelPermissions(); - } - - private void OnLocalPlayerEntityDetached(EntityDetachedEventArgs obj) - { - UpdateChannelPermissions(); - } - - // go through all of the various channels and update filter / select permissions - // appropriately, also enabling them if our enabledChannels dict doesn't have an entry - // for any newly-granted channels - private void UpdateChannelPermissions() - { - var oldSelectable = SelectableChannels; - SelectableChannels = default; - FilterableChannels = default; - - // Can always send console stuff. - SelectableChannels |= ChatSelectChannel.Console; - - // can always send/recieve OOC - SelectableChannels |= ChatSelectChannel.OOC; - FilterableChannels |= ChatChannel.OOC; - SelectableChannels |= ChatSelectChannel.LOOC; - FilterableChannels |= ChatChannel.LOOC; - - // can always hear server (nobody can actually send server messages). - FilterableChannels |= ChatChannel.Server; - - if (_stateManager.CurrentState is GameplayStateBase) - { - // can always hear local / radio / emote when in the game - FilterableChannels |= ChatChannel.Local; - FilterableChannels |= ChatChannel.Whisper; - FilterableChannels |= ChatChannel.Radio; - FilterableChannels |= ChatChannel.Emotes; - - // Can only send local / radio / emote when attached to a non-ghost entity. - // TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this? - if (!IsGhost) - { - SelectableChannels |= ChatSelectChannel.Local; - SelectableChannels |= ChatSelectChannel.Whisper; - SelectableChannels |= ChatSelectChannel.Radio; - SelectableChannels |= ChatSelectChannel.Emotes; - } - } - - // Only ghosts and admins can send / see deadchat. - if (_adminMgr.HasFlag(AdminFlags.Admin) || IsGhost) - { - FilterableChannels |= ChatChannel.Dead; - SelectableChannels |= ChatSelectChannel.Dead; - } - - // only admins can see / filter asay - if (_adminMgr.HasFlag(AdminFlags.Admin)) - { - FilterableChannels |= ChatChannel.Admin; - SelectableChannels |= ChatSelectChannel.Admin; - } - - // Necessary so that we always have a channel to fall back to. - DebugTools.Assert((SelectableChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available"); - DebugTools.Assert((FilterableChannels & ChatChannel.OOC) != 0, "OOC must always be available"); - - // let our chatbox know all the new settings - ChatPermissionsUpdated?.Invoke(new ChatPermissionsUpdatedEventArgs {OldSelectableChannels = oldSelectable}); - } - - public bool IsGhost => _playerManager.LocalPlayer?.ControlledEntity is {} uid && - uid.IsValid() && - _entityManager.HasComponent(uid); - - public void FrameUpdate(FrameEventArgs delta) - { - // Update queued speech bubbles. - if (_queuedSpeechBubbles.Count == 0) - { - return; - } - - foreach (var (entity, queueData) in _queuedSpeechBubbles.ShallowClone()) - { - if (!_entityManager.EntityExists(entity)) - { - _queuedSpeechBubbles.Remove(entity); - continue; - } - - queueData.TimeLeft -= delta.DeltaSeconds; - if (queueData.TimeLeft > 0) - { - continue; - } - - if (queueData.MessageQueue.Count == 0) - { - _queuedSpeechBubbles.Remove(entity); - continue; - } - - var msg = queueData.MessageQueue.Dequeue(); - - queueData.TimeLeft += BubbleDelayBase + msg.Message.Length * BubbleDelayFactor; - - // We keep the queue around while it has 0 items. This allows us to keep the timer. - // When the timer hits 0 and there's no messages left, THEN we can clear it up. - CreateSpeechBubble(entity, msg); - } - } - - public void SetChatBox(ChatBox chatBox) - { - CurrentChatBox = chatBox; - } - - public void ClearUnfilteredUnreads() - { - foreach (var channel in _unreadMessages.Keys.ToArray()) - { - if ((ChannelFilters & channel) != 0) - _unreadMessages.Remove(channel); - } - } - - public void ChatBoxOnResized(ChatResizedEventArgs chatResizedEventArgs) - { - OnChatBoxResized?.Invoke(chatResizedEventArgs); - } - - public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble) - { - bubble.Dispose(); - - var list = _activeSpeechBubbles[entityUid]; - list.Remove(bubble); - - if (list.Count == 0) - { - _activeSpeechBubbles.Remove(entityUid); - } - } - - public void OnChatBoxTextSubmitted(ChatBox chatBox, ReadOnlyMemory text, ChatSelectChannel channel) - { - DebugTools.Assert(chatBox == CurrentChatBox); - var str = text.ToString(); - switch (channel) { case ChatSelectChannel.Console: @@ -346,9 +48,10 @@ namespace Content.Client.Chat.Managers break; case ChatSelectChannel.Dead: - if (IsGhost) + if (_systems.GetEntitySystemOrNull() is {IsGhost: true}) goto case ChatSelectChannel.Local; - else if (_adminMgr.HasFlag(AdminFlags.Admin)) + + if (_adminMgr.HasFlag(AdminFlags.Admin)) _consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\""); else _sawmill.Warning("Tried to speak on deadchat without being ghost or admin."); @@ -370,190 +73,5 @@ namespace Content.Client.Chat.Managers throw new ArgumentOutOfRangeException(nameof(channel), channel, null); } } - - public void OnFilterButtonToggled(ChatChannel channel, bool enabled) - { - if (enabled) - { - ChannelFilters |= channel; - _unreadMessages.Remove(channel); - UnreadMessageCountsUpdated?.Invoke(); - } - else - { - ChannelFilters &= ~channel; - } - - FiltersUpdated?.Invoke(); - } - - private void OnChatMessage(MsgChatMessage msg) - { - // Log all incoming chat to repopulate when filter is un-toggled - if (!msg.HideChat) - { - var storedMessage = new StoredChatMessage(msg); - _history.Add(storedMessage); - MessageAdded?.Invoke(storedMessage); - - if (!storedMessage.Read) - { - _sawmill.Debug($"Message filtered: {storedMessage.Channel}: {storedMessage.Message}"); - if (!_unreadMessages.TryGetValue(msg.Channel, out var count)) - count = 0; - - count += 1; - _unreadMessages[msg.Channel] = count; - UnreadMessageCountsUpdated?.Invoke(); - } - } - - // Local messages that have an entity attached get a speech bubble. - if (msg.SenderEntity == default) - return; - - switch (msg.Channel) - { - case ChatChannel.Local: - AddSpeechBubble(msg, SpeechBubble.SpeechType.Say); - break; - - case ChatChannel.Whisper: - AddSpeechBubble(msg, SpeechBubble.SpeechType.Whisper); - break; - - case ChatChannel.Dead: - if (!IsGhost) - break; - - AddSpeechBubble(msg, SpeechBubble.SpeechType.Say); - break; - - case ChatChannel.Emotes: - AddSpeechBubble(msg, SpeechBubble.SpeechType.Emote); - break; - } - } - - private void AddSpeechBubble(MsgChatMessage msg, SpeechBubble.SpeechType speechType) - { - if (!_entityManager.EntityExists(msg.SenderEntity)) - { - _sawmill.Debug("Got local chat message with invalid sender entity: {0}", msg.SenderEntity); - return; - } - - var messages = SplitMessage(FormattedMessage.RemoveMarkup(msg.Message)); - - foreach (var message in messages) - { - EnqueueSpeechBubble(msg.SenderEntity, message, speechType); - } - } - - private List SplitMessage(string msg) - { - // Split message into words separated by spaces. - var words = msg.Split(' '); - var messages = new List(); - var currentBuffer = new List(); - - // Really shoddy way to approximate word length. - // Yes, I am aware of all the crimes here. - // TODO: Improve this to use actual glyph width etc.. - var currentWordLength = 0; - foreach (var word in words) - { - // +1 for the space. - currentWordLength += word.Length + 1; - - if (currentWordLength > SingleBubbleCharLimit) - { - // Too long for the current speech bubble, flush it. - messages.Add(string.Join(" ", currentBuffer)); - currentBuffer.Clear(); - - currentWordLength = word.Length; - - if (currentWordLength > SingleBubbleCharLimit) - { - // Word is STILL too long. - // Truncate it with an ellipse. - messages.Add($"{word.Substring(0, SingleBubbleCharLimit - 3)}..."); - currentWordLength = 0; - continue; - } - } - - currentBuffer.Add(word); - } - - if (currentBuffer.Count != 0) - { - // Don't forget the last bubble. - messages.Add(string.Join(" ", currentBuffer)); - } - - return messages; - } - - private void EnqueueSpeechBubble(EntityUid entity, string contents, SpeechBubble.SpeechType speechType) - { - // Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps? - if (_entityManager.GetComponent(entity).MapID != _eyeManager.CurrentMap) - return; - - if (!_queuedSpeechBubbles.TryGetValue(entity, out var queueData)) - { - queueData = new SpeechBubbleQueueData(); - _queuedSpeechBubbles.Add(entity, queueData); - } - - queueData.MessageQueue.Enqueue(new SpeechBubbleData - { - Message = contents, - Type = speechType, - }); - } - - private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData) - { - var bubble = - SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this, _entityManager); - - if (_activeSpeechBubbles.TryGetValue(entity, out var existing)) - { - // Push up existing bubbles above the mob's head. - foreach (var existingBubble in existing) - { - existingBubble.VerticalOffset += bubble.ContentSize.Y; - } - } - else - { - existing = new List(); - _activeSpeechBubbles.Add(entity, existing); - } - - existing.Add(bubble); - _speechBubbleRoot.AddChild(bubble); - - if (existing.Count > SpeechBubbleCap) - { - // Get the oldest to start fading fast. - var last = existing[0]; - last.FadeNow(); - } - } - - private sealed class SpeechBubbleQueueData - { - /// - /// Time left until the next speech bubble can appear. - /// - public float TimeLeft { get; set; } - - public Queue MessageQueue { get; } = new(); - } } } diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs index e59de20183..fc11745b37 100644 --- a/Content.Client/Chat/Managers/IChatManager.cs +++ b/Content.Client/Chat/Managers/IChatManager.cs @@ -1,55 +1,11 @@ -using System; -using System.Collections.Generic; -using Content.Client.Chat.UI; using Content.Shared.Chat; -using Robust.Shared.GameObjects; -using Robust.Shared.Timing; namespace Content.Client.Chat.Managers { public interface IChatManager { - ChatChannel ChannelFilters { get; } - ChatSelectChannel SelectableChannels { get; } - ChatChannel FilterableChannels { get; } - void Initialize(); - void FrameUpdate(FrameEventArgs delta); - - void SetChatBox(ChatBox chatBox); - - void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble); - - /// - /// Current chat box control. This can be modified, so do not depend on saving a reference to this. - /// - ChatBox? CurrentChatBox { get; } - - IReadOnlyDictionary> GetSpeechBubbles(); - IReadOnlyDictionary UnreadMessages { get; } - IReadOnlyList History { get; } - int MaxMessageLength { get; } - bool IsGhost { get; } - - /// - /// Invoked when CurrentChatBox is resized (including after setting initial default size) - /// - event Action? OnChatBoxResized; - - event Action? ChatPermissionsUpdated; - event Action? UnreadMessageCountsUpdated; - event Action? MessageAdded; - event Action? FiltersUpdated; - - void ClearUnfilteredUnreads(); - void ChatBoxOnResized(ChatResizedEventArgs chatResizedEventArgs); - void OnChatBoxTextSubmitted(ChatBox chatBox, ReadOnlyMemory text, ChatSelectChannel channel); - void OnFilterButtonToggled(ChatChannel channel, bool enabled); - } - - public struct ChatPermissionsUpdatedEventArgs - { - public ChatSelectChannel OldSelectableChannels; + public void SendMessage(ReadOnlyMemory text, ChatSelectChannel channel); } } diff --git a/Content.Client/Chat/UI/ChatBox.xaml b/Content.Client/Chat/UI/ChatBox.xaml deleted file mode 100644 index 3f66665d95..0000000000 --- a/Content.Client/Chat/UI/ChatBox.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Content.Client/Chat/UI/ChatBox.xaml.cs b/Content.Client/Chat/UI/ChatBox.xaml.cs deleted file mode 100644 index f84d3c9b0f..0000000000 --- a/Content.Client/Chat/UI/ChatBox.xaml.cs +++ /dev/null @@ -1,754 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Content.Client.Alerts.UI; -using Content.Client.Chat.Managers; -using Content.Client.Chat.TypingIndicator; -using Content.Client.Resources; -using Content.Client.Stylesheets; -using Content.Shared.Chat; -using Content.Shared.Input; -using Robust.Client.AutoGenerated; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Input; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Log; -using Robust.Shared.Maths; -using Robust.Shared.Utility; - -namespace Content.Client.Chat.UI -{ - [GenerateTypedNameReferences] - [Virtual] - public partial class ChatBox : Control - { - [Dependency] protected readonly IChatManager ChatMgr = default!; - - // order in which the available channel filters show up when available - private static readonly ChatChannel[] ChannelFilterOrder = - { - ChatChannel.Local, - ChatChannel.Whisper, - ChatChannel.Emotes, - ChatChannel.Radio, - ChatChannel.OOC, - ChatChannel.Dead, - ChatChannel.Admin, - ChatChannel.Server - }; - - // order in which the channels show up in the channel selector - private static readonly ChatSelectChannel[] ChannelSelectorOrder = - { - ChatSelectChannel.Local, - ChatSelectChannel.Whisper, - ChatSelectChannel.Emotes, - ChatSelectChannel.Radio, - ChatSelectChannel.LOOC, - ChatSelectChannel.OOC, - ChatSelectChannel.Dead, - ChatSelectChannel.Admin - // NOTE: Console is not in there and it can never be permanently selected. - // You can, however, still submit commands as console by prefixing with /. - }; - - public const char AliasLocal = '.'; - public const char AliasConsole = '/'; - public const char AliasDead = '\\'; - public const char AliasOOC = '['; - public const char AliasEmotes = '@'; - public const char AliasAdmin = ']'; - public const char AliasRadio = ';'; - public const char AliasWhisper = ','; - - private static readonly Dictionary PrefixToChannel = new() - { - {AliasLocal, ChatSelectChannel.Local}, - {AliasWhisper, ChatSelectChannel.Whisper}, - {AliasConsole, ChatSelectChannel.Console}, - {AliasOOC, ChatSelectChannel.OOC}, - {AliasEmotes, ChatSelectChannel.Emotes}, - {AliasAdmin, ChatSelectChannel.Admin}, - {AliasRadio, ChatSelectChannel.Radio}, - {AliasDead, ChatSelectChannel.Dead} - }; - - private static readonly Dictionary ChannelPrefixes = - PrefixToChannel.ToDictionary(kv => kv.Value, kv => kv.Key); - - private const float FilterPopupWidth = 110; - - /// - /// The currently default channel that will be used if no prefix is specified. - /// - public ChatSelectChannel SelectedChannel { get; private set; } = ChatSelectChannel.OOC; - - /// - /// The "preferred" channel. Will be switched to if permissions change and the channel becomes available, - /// such as by re-entering body. Gets changed if the user manually selects a channel with the buttons. - /// - public ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC; - - public bool ReleaseFocusOnEnter { get; set; } = true; - - private readonly Popup _channelSelectorPopup; - private readonly BoxContainer _channelSelectorHBox; - private readonly Popup _filterPopup; - private readonly PanelContainer _filterPopupPanel; - private readonly BoxContainer _filterVBox; - - /// - /// When lobbyMode is false, will position / add to correct location in StateRoot and - /// be resizable. - /// wWen true, will leave layout up to parent and not be resizable. - /// - public ChatBox() - { - IoCManager.InjectDependencies(this); - RobustXamlLoader.Load(this); - - LayoutContainer.SetMarginLeft(this, 4); - LayoutContainer.SetMarginRight(this, 4); - - _filterPopup = new Popup - { - Children = - { - (_filterPopupPanel = new PanelContainer - { - StyleClasses = {StyleNano.StyleClassBorderedWindowPanel}, - Children = - { - new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - Children = - { - new Control {MinSize = (4, 0)}, - (_filterVBox = new BoxContainer - { - Margin = new Thickness(0, 10), - Orientation = BoxContainer.LayoutOrientation.Vertical, - SeparationOverride = 4 - }) - } - } - } - }) - } - }; - - _channelSelectorPopup = new Popup - { - Children = - { - (_channelSelectorHBox = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - SeparationOverride = 1 - }) - } - }; - - ChannelSelector.OnToggled += OnChannelSelectorToggled; - FilterButton.OnToggled += OnFilterButtonToggled; - Input.OnKeyBindDown += InputKeyBindDown; - Input.OnTextEntered += Input_OnTextEntered; - Input.OnTextChanged += InputOnTextChanged; - _channelSelectorPopup.OnPopupHide += OnChannelSelectorPopupHide; - _filterPopup.OnPopupHide += OnFilterPopupHide; - } - - protected override void EnteredTree() - { - base.EnteredTree(); - - ChatMgr.MessageAdded += WriteChatMessage; - ChatMgr.ChatPermissionsUpdated += OnChatPermissionsUpdated; - ChatMgr.UnreadMessageCountsUpdated += UpdateUnreadMessageCounts; - ChatMgr.FiltersUpdated += Repopulate; - - // The chat manager may have messages logged from before there was a chat box. - // In this case, these messages will be marked as unread despite the filters allowing them through. - // Tell chat manager to clear these. - ChatMgr.ClearUnfilteredUnreads(); - - ChatPermissionsUpdated(0); - UpdateChannelSelectButton(); - Repopulate(); - } - - protected override void ExitedTree() - { - base.ExitedTree(); - - ChatMgr.MessageAdded -= WriteChatMessage; - ChatMgr.ChatPermissionsUpdated -= OnChatPermissionsUpdated; - ChatMgr.UnreadMessageCountsUpdated -= UpdateUnreadMessageCounts; - ChatMgr.FiltersUpdated -= Repopulate; - } - - private void OnChatPermissionsUpdated(ChatPermissionsUpdatedEventArgs eventArgs) - { - ChatPermissionsUpdated(eventArgs.OldSelectableChannels); - } - - private void ChatPermissionsUpdated(ChatSelectChannel oldSelectable) - { - // update the channel selector - _channelSelectorHBox.Children.Clear(); - foreach (var selectableChannel in ChannelSelectorOrder) - { - if ((ChatMgr.SelectableChannels & selectableChannel) == 0) - continue; - - var newButton = new ChannelItemButton(selectableChannel); - newButton.OnPressed += OnChannelSelectorItemPressed; - _channelSelectorHBox.AddChild(newButton); - } - - // Selected channel no longer available, switch to OOC? - if ((ChatMgr.SelectableChannels & SelectedChannel) == 0) - { - // Handle local -> dead mapping when you e.g. ghost. - // Only necessary for admins because they always have deadchat - // so the normal preferred check won't see it as newly available and do nothing. - var mappedSelect = MapLocalIfGhost(SelectedChannel); - if ((ChatMgr.SelectableChannels & mappedSelect) != 0) - SafelySelectChannel(mappedSelect); - else - SafelySelectChannel(ChatSelectChannel.OOC); - } - - // If the preferred channel just became available, switch to it. - var pref = MapLocalIfGhost(PreferredChannel); - if ((oldSelectable & pref) == 0 && (ChatMgr.SelectableChannels & pref) != 0) - SafelySelectChannel(pref); - - // update the channel filters - _filterVBox.Children.Clear(); - foreach (var channelFilter in ChannelFilterOrder) - { - if ((ChatMgr.FilterableChannels & channelFilter) == 0) - continue; - - int? unreadCount = null; - if (ChatMgr.UnreadMessages.TryGetValue(channelFilter, out var unread)) - unreadCount = unread; - - var newCheckBox = new ChannelFilterCheckbox(channelFilter, unreadCount) - { - Pressed = (ChatMgr.ChannelFilters & channelFilter) != 0 - }; - - newCheckBox.OnToggled += OnFilterCheckboxToggled; - _filterVBox.AddChild(newCheckBox); - } - - UpdateChannelSelectButton(); - } - - private void UpdateUnreadMessageCounts() - { - foreach (var channelFilter in _filterVBox.Children) - { - if (channelFilter is not ChannelFilterCheckbox filterCheckbox) continue; - if (ChatMgr.UnreadMessages.TryGetValue(filterCheckbox.Channel, out var unread)) - { - filterCheckbox.UpdateUnreadCount(unread); - } - else - { - filterCheckbox.UpdateUnreadCount(null); - } - } - } - - private void OnFilterCheckboxToggled(BaseButton.ButtonToggledEventArgs args) - { - if (args.Button is not ChannelFilterCheckbox checkbox) - return; - - ChatMgr.OnFilterButtonToggled(checkbox.Channel, checkbox.Pressed); - } - - private void OnFilterButtonToggled(BaseButton.ButtonToggledEventArgs args) - { - if (args.Pressed) - { - var globalPos = FilterButton.GlobalPosition; - var (minX, minY) = _filterPopupPanel.MinSize; - var box = UIBox2.FromDimensions(globalPos - (FilterPopupWidth, 0), - (Math.Max(minX, FilterPopupWidth), minY)); - UserInterfaceManager.ModalRoot.AddChild(_filterPopup); - _filterPopup.Open(box); - } - else - { - _filterPopup.Close(); - } - } - - private void OnChannelSelectorToggled(BaseButton.ButtonToggledEventArgs args) - { - if (args.Pressed) - { - var globalLeft = GlobalPosition.X; - var globalBot = GlobalPosition.Y + Height; - var box = UIBox2.FromDimensions((globalLeft, globalBot), (SizeBox.Width, AlertsUI.ChatSeparation)); - UserInterfaceManager.ModalRoot.AddChild(_channelSelectorPopup); - _channelSelectorPopup.Open(box); - } - else - { - _channelSelectorPopup.Close(); - } - } - - private void OnFilterPopupHide() - { - OnPopupHide(_filterPopup, FilterButton); - } - - private void OnChannelSelectorPopupHide() - { - OnPopupHide(_channelSelectorPopup, ChannelSelector); - } - - private void OnPopupHide(Control popup, BaseButton button) - { - UserInterfaceManager.ModalRoot.RemoveChild(popup); - - // this weird check here is because the hiding of the popup happens prior to the button - // receiving the keydown, which would cause it to then become unpressed - // and reopen immediately. To avoid this, if the popup was hidden due to clicking on the button, - // we will not auto-unpress the button, instead leaving it up to the button toggle logic - // (and this requires the button to be set to EnableAllKeybinds = true) - if (UserInterfaceManager.CurrentlyHovered != button) - { - button.Pressed = false; - } - } - - private void OnChannelSelectorItemPressed(BaseButton.ButtonEventArgs obj) - { - if (obj.Button is not ChannelItemButton button) - return; - - PreferredChannel = button.Channel; - SafelySelectChannel(button.Channel); - _channelSelectorPopup.Close(); - } - - public bool SafelySelectChannel(ChatSelectChannel toSelect) - { - toSelect = MapLocalIfGhost(toSelect); - if ((ChatMgr.SelectableChannels & toSelect) == 0) - return false; - - SelectedChannel = toSelect; - UpdateChannelSelectButton(); - return true; - } - - private void UpdateChannelSelectButton() - { - var (prefixChannel, _) = SplitInputContents(); - - var channel = prefixChannel == 0 ? SelectedChannel : prefixChannel; - - ChannelSelector.Text = ChannelSelectorName(channel); - ChannelSelector.Modulate = ChannelSelectColor(channel); - } - - protected override void KeyBindDown(GUIBoundKeyEventArgs args) - { - base.KeyBindDown(args); - - if (args.CanFocus) - { - Input.GrabKeyboardFocus(); - } - } - - public void CycleChatChannel(bool forward) - { - Input.IgnoreNext = true; - - var idx = Array.IndexOf(ChannelSelectorOrder, SelectedChannel); - do - { - // go over every channel until we find one we can actually select. - idx += forward ? 1 : -1; - idx = MathHelper.Mod(idx, ChannelSelectorOrder.Length); - } while ((ChatMgr.SelectableChannels & ChannelSelectorOrder[idx]) == 0); - - SafelySelectChannel(ChannelSelectorOrder[idx]); - } - - private void Repopulate() - { - Contents.Clear(); - - foreach (var msg in ChatMgr.History) - { - WriteChatMessage(msg); - } - } - - private void WriteChatMessage(StoredChatMessage message) - { - var messageText = FormattedMessage.EscapeText(message.Message); - if (!string.IsNullOrEmpty(message.MessageWrap)) - { - messageText = string.Format(message.MessageWrap, messageText); - } - - Logger.DebugS("chat", $"{message.Channel}: {messageText}"); - - if (IsFilteredOut(message.Channel)) - return; - - // TODO: Can make this "smarter" later by only setting it false when the message has been scrolled to - message.Read = true; - - var color = message.MessageColorOverride != Color.Transparent - ? message.MessageColorOverride - : ChatHelper.ChatColor(message.Channel); - - AddLine(messageText, message.Channel, color); - } - - private bool IsFilteredOut(ChatChannel channel) - { - return (ChatMgr.ChannelFilters & channel) == 0; - } - - private void InputKeyBindDown(GUIBoundKeyEventArgs args) - { - if (args.Function == EngineKeyFunctions.TextReleaseFocus) - { - Input.ReleaseKeyboardFocus(); - args.Handle(); - return; - } - - if (args.Function == ContentKeyFunctions.CycleChatChannelForward) - { - CycleChatChannel(true); - args.Handle(); - return; - } - - if (args.Function == ContentKeyFunctions.CycleChatChannelBackward) - { - CycleChatChannel(false); - args.Handle(); - } - } - - private (ChatSelectChannel selChannel, ReadOnlyMemory text) SplitInputContents() - { - var text = Input.Text.AsMemory().Trim(); - if (text.Length == 0) - return default; - - var prefixChar = text.Span[0]; - var channel = GetChannelFromPrefix(prefixChar); - - if ((ChatMgr.SelectableChannels & channel) != 0) - // Cut off prefix if it's valid and we can use the channel in question. - text = text[1..]; - else - channel = 0; - - channel = MapLocalIfGhost(channel); - - // Trim from start again to cut out any whitespace between the prefix and message, if any. - return (channel, text.TrimStart()); - } - - private void InputOnTextChanged(LineEdit.LineEditEventArgs obj) - { - // Update channel select button to correct channel if we have a prefix. - UpdateChannelSelectButton(); - - // Warn typing indicator about change - IoCManager.Resolve().GetEntitySystem().ClientChangedChatText(); - } - - private static ChatSelectChannel GetChannelFromPrefix(char prefix) - { - return PrefixToChannel.GetValueOrDefault(prefix); - } - - public static char GetPrefixFromChannel(ChatSelectChannel channel) - { - return ChannelPrefixes.GetValueOrDefault(channel); - } - - public static string ChannelSelectorName(ChatSelectChannel channel) - { - return Loc.GetString($"hud-chatbox-select-channel-{channel}"); - } - - public static Color ChannelSelectColor(ChatSelectChannel channel) - { - return channel switch - { - ChatSelectChannel.Radio => Color.LimeGreen, - ChatSelectChannel.LOOC => Color.MediumTurquoise, - ChatSelectChannel.OOC => Color.LightSkyBlue, - ChatSelectChannel.Dead => Color.MediumPurple, - ChatSelectChannel.Admin => Color.Red, - _ => Color.DarkGray - }; - } - - public void AddLine(string message, ChatChannel channel, Color color) - { - DebugTools.Assert(!Disposed); - - var formatted = new FormattedMessage(3); - formatted.PushColor(color); - formatted.AddMarkup(message); - formatted.Pop(); - Contents.AddMessage(formatted); - } - - private void Input_OnTextEntered(LineEdit.LineEditEventArgs args) - { - // Warn typing indicator about entered text - IoCManager.Resolve().GetEntitySystem().ClientSubmittedChatText(); - - if (!string.IsNullOrWhiteSpace(args.Text)) - { - var (prefixChannel, text) = SplitInputContents(); - - // Check if message is longer than the character limit - if (text.Length > ChatMgr.MaxMessageLength) - { - string locWarning = Loc.GetString( - "chat-manager-max-message-length", - ("maxMessageLength", ChatMgr.MaxMessageLength)); - - AddLine(locWarning, ChatChannel.Server, Color.Orange); - return; - } - - ChatMgr.OnChatBoxTextSubmitted(this, text, prefixChannel == 0 ? SelectedChannel : prefixChannel); - } - - Input.Clear(); - UpdateChannelSelectButton(); - - if (ReleaseFocusOnEnter) - Input.ReleaseKeyboardFocus(); - } - - public void Focus(ChatSelectChannel? channel = null) - { - var selectStart = Index.End; - if (channel != null) - { - channel = MapLocalIfGhost(channel.Value); - - // Channel not selectable, just do NOTHING (not even focus). - if (!((ChatMgr.SelectableChannels & channel.Value) != 0)) - return; - - var (_, text) = SplitInputContents(); - - var newPrefix = GetPrefixFromChannel(channel.Value); - DebugTools.Assert(newPrefix != default, "Focus channel must have prefix!"); - - if (channel == SelectedChannel) - { - // New selected channel is just the selected channel, - // just remove prefix (if any) and leave text unchanged. - - Input.Text = text.ToString(); - selectStart = Index.Start; - } - else - { - // Change prefix to new focused channel prefix and leave text unchanged. - Input.Text = string.Concat(newPrefix.ToString(), " ", text.Span); - selectStart = Index.FromStart(2); - } - - UpdateChannelSelectButton(); - } - - Input.IgnoreNext = true; - Input.GrabKeyboardFocus(); - - Input.CursorPosition = Input.Text.Length; - Input.SelectionStart = selectStart.GetOffset(Input.Text.Length); - } - - private ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel) - { - if (channel == ChatSelectChannel.Local && ChatMgr.IsGhost) - return ChatSelectChannel.Dead; - - return channel; - } - } - - /// - /// Only needed to avoid the issue where right click on the button closes the popup - /// but leaves the button highlighted. - /// - public sealed class ChannelSelectorButton : Button - { - public ChannelSelectorButton() - { - // needed so the popup is untoggled regardless of which key is pressed when hovering this button. - // If we don't have this, then right clicking the button while it's toggled on will hide - // the popup but keep the button toggled on - Mode = ActionMode.Press; - EnableAllKeybinds = true; - } - - protected override void KeyBindDown(GUIBoundKeyEventArgs args) - { - // needed since we need EnableAllKeybinds - don't double-send both UI click and Use - if (args.Function == EngineKeyFunctions.Use) - return; - - base.KeyBindDown(args); - } - } - - public sealed class FilterButton : ContainerButton - { - private static readonly Color ColorNormal = Color.FromHex("#7b7e9e"); - private static readonly Color ColorHovered = Color.FromHex("#9699bb"); - private static readonly Color ColorPressed = Color.FromHex("#789B8C"); - - private readonly TextureRect _textureRect; - - public FilterButton() - { - var filterTexture = IoCManager.Resolve() - .GetTexture("/Textures/Interface/Nano/filter.svg.96dpi.png"); - - // needed for same reason as ChannelSelectorButton - Mode = ActionMode.Press; - EnableAllKeybinds = true; - - AddChild( - (_textureRect = new TextureRect - { - Texture = filterTexture, - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center - }) - ); - - ToggleMode = true; - } - - protected override void KeyBindDown(GUIBoundKeyEventArgs args) - { - // needed since we need EnableAllKeybinds - don't double-send both UI click and Use - if (args.Function == EngineKeyFunctions.Use) return; - base.KeyBindDown(args); - } - - private void UpdateChildColors() - { - if (_textureRect == null) return; - switch (DrawMode) - { - case DrawModeEnum.Normal: - _textureRect.ModulateSelfOverride = ColorNormal; - break; - - case DrawModeEnum.Pressed: - _textureRect.ModulateSelfOverride = ColorPressed; - break; - - case DrawModeEnum.Hover: - _textureRect.ModulateSelfOverride = ColorHovered; - break; - - case DrawModeEnum.Disabled: - break; - } - } - - protected override void DrawModeChanged() - { - base.DrawModeChanged(); - UpdateChildColors(); - } - - protected override void StylePropertiesChanged() - { - base.StylePropertiesChanged(); - UpdateChildColors(); - } - } - - public sealed class ChannelItemButton : Button - { - public readonly ChatSelectChannel Channel; - - public ChannelItemButton(ChatSelectChannel channel) - { - Channel = channel; - AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton); - Text = ChatBox.ChannelSelectorName(channel); - - var prefix = ChatBox.GetPrefixFromChannel(channel); - if (prefix != default) - Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix)); - } - } - - public sealed class ChannelFilterCheckbox : CheckBox - { - public readonly ChatChannel Channel; - - public ChannelFilterCheckbox(ChatChannel channel, int? unreadCount) - { - Channel = channel; - - UpdateText(unreadCount); - } - - private void UpdateText(int? unread) - { - var name = Loc.GetString($"hud-chatbox-channel-{Channel}"); - - if (unread > 0) - // todo: proper fluent stuff here. - name += " (" + (unread > 9 ? "9+" : unread) + ")"; - - Text = name; - } - - public void UpdateUnreadCount(int? unread) - { - UpdateText(unread); - } - } - - public readonly struct ChatResizedEventArgs - { - /// new bottom that the chat rect is going to have in virtual pixels - /// after the imminent relayout - public readonly float NewBottom; - - public ChatResizedEventArgs(float newBottom) - { - NewBottom = newBottom; - } - } -} diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 8f09b9f333..a74395c2a1 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,12 +1,7 @@ -using System; using Content.Client.Chat.Managers; -using Content.Client.Viewport; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Maths; using Robust.Shared.Timing; namespace Content.Client.Chat.UI @@ -48,6 +43,9 @@ namespace Content.Client.Chat.UI public Vector2 ContentSize { get; private set; } + // man down + public event Action? OnDied; + public static SpeechBubble CreateSpeechBubble(SpeechType type, string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager) { switch (type) @@ -148,7 +146,7 @@ namespace Content.Client.Chat.UI return; } - _chatManager.RemoveSpeechBubble(_senderEntity, this); + OnDied?.Invoke(_senderEntity, this); } /// @@ -164,7 +162,6 @@ namespace Content.Client.Chat.UI } public sealed class TextSpeechBubble : SpeechBubble - { public TextSpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass) : base(text, senderEntity, eyeManager, chatManager, entityManager, speechStyleClass) diff --git a/Content.Client/CombatMode/CombatModeComponent.cs b/Content.Client/CombatMode/CombatModeComponent.cs index c1874dae04..482a704adf 100644 --- a/Content.Client/CombatMode/CombatModeComponent.cs +++ b/Content.Client/CombatMode/CombatModeComponent.cs @@ -1,11 +1,7 @@ -using Content.Client.HUD; using Content.Client.Verbs; using Content.Shared.CombatMode; using Content.Shared.Targeting; -using Robust.Client.GameObjects; using Robust.Client.Player; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; namespace Content.Client.CombatMode { @@ -14,7 +10,6 @@ namespace Content.Client.CombatMode public sealed class CombatModeComponent : SharedCombatModeComponent { [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IGameHud _gameHud = default!; public override bool IsInCombatMode { @@ -36,14 +31,6 @@ namespace Content.Client.CombatMode } } - public void PlayerDetached() { _gameHud.CombatPanelVisible = false; } - - public void PlayerAttached() - { - _gameHud.CombatPanelVisible = false; // TODO BOBBY SYSTEM Make the targeting doll actually do something. - UpdateHud(); - } - private void UpdateHud() { if (Owner != _playerManager.LocalPlayer?.ControlledEntity) @@ -53,7 +40,6 @@ namespace Content.Client.CombatMode var verbs = IoCManager.Resolve().GetEntitySystem(); verbs.CloseAllMenus(); - _gameHud.TargetingZone = ActiveZone; } } } diff --git a/Content.Client/CombatMode/CombatModeSystem.cs b/Content.Client/CombatMode/CombatModeSystem.cs index 174f3406df..6c9de790dc 100644 --- a/Content.Client/CombatMode/CombatModeSystem.cs +++ b/Content.Client/CombatMode/CombatModeSystem.cs @@ -1,8 +1,6 @@ -using Content.Client.HUD; using Content.Shared.CombatMode; using Content.Shared.Targeting; using JetBrains.Annotations; -using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Shared.GameStates; using Robust.Shared.Input.Binding; @@ -12,17 +10,12 @@ namespace Content.Client.CombatMode [UsedImplicitly] public sealed class CombatModeSystem : SharedCombatModeSystem { - [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; public override void Initialize() { base.Initialize(); - _gameHud.OnTargetingZoneChanged = OnTargetingZoneChanged; - - SubscribeLocalEvent((_, component, _) => component.PlayerAttached()); - SubscribeLocalEvent((_, component, _) => component.PlayerDetached()); SubscribeLocalEvent(OnHandleState); } @@ -34,7 +27,6 @@ namespace Content.Client.CombatMode component.IsInCombatMode = state.IsInCombatMode; component.ActiveZone = state.TargetingZone; } - public override void Shutdown() { CommandBinds.Unregister(); diff --git a/Content.Client/Commands/OpenAHelpCommand.cs b/Content.Client/Commands/OpenAHelpCommand.cs index 5d6ecfaed1..3d098f28a7 100644 --- a/Content.Client/Commands/OpenAHelpCommand.cs +++ b/Content.Client/Commands/OpenAHelpCommand.cs @@ -1,7 +1,10 @@ using System; using Content.Client.Administration; using Content.Client.Administration.Systems; +using Content.Client.UserInterface.Systems.Bwoink; +using Content.Client.UserInterface.Systems.EscapeMenu; using Content.Shared.Administration; +using Robust.Client.UserInterface; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.Network; @@ -24,13 +27,14 @@ namespace Content.Client.Commands } if (args.Length == 0) { - EntitySystem.Get().Open(); + IoCManager.Resolve().GetUIController().Open(); } else { if (Guid.TryParse(args[0], out var guid)) { - EntitySystem.Get().Open(new NetUserId(guid)); + var targetUser = new NetUserId(guid); + IoCManager.Resolve().GetUIController().Open(targetUser); } else { diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index ba786f27ea..f403eb74cb 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -1,22 +1,13 @@ -using System; -using System.Collections.Generic; using System.Linq; -using Content.Client.HUD; -using Content.Client.Resources; +using Content.Client.UserInterface.Systems.MenuBar.Widgets; using Content.Shared.Construction.Prototypes; -using Content.Shared.Construction.Steps; -using Content.Shared.Tools; -using Content.Shared.Tools.Components; -using Robust.Client.Graphics; using Robust.Client.Placement; -using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.Utility; using Robust.Shared.Enums; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Prototypes; +using static Robust.Client.UserInterface.Controls.BaseButton; namespace Content.Client.Construction.UI { @@ -30,8 +21,8 @@ namespace Content.Client.Construction.UI [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!; + [Dependency] private readonly IUserInterfaceManager _uiManager = default!; - private readonly IGameHud _gameHud; private readonly IConstructionMenuView _constructionView; private ConstructionSystem? _constructionSystem; @@ -39,10 +30,10 @@ namespace Content.Client.Construction.UI private bool CraftingAvailable { - get => _gameHud.CraftingButtonVisible; + get => _uiManager.GetActiveUIWidget().CraftingButton.Visible; set { - _gameHud.CraftingButtonVisible = value; + _uiManager.GetActiveUIWidget().CraftingButton.Visible = value; if (!value) _constructionView.Close(); } @@ -77,12 +68,10 @@ namespace Content.Client.Construction.UI /// Constructs a new instance of . /// /// GUI that is being presented to. - public ConstructionMenuPresenter(IGameHud gameHud) + public ConstructionMenuPresenter() { // This is a lot easier than a factory IoCManager.InjectDependencies(this); - - _gameHud = gameHud; _constructionView = new ConstructionMenu(); // This is required so that if we load after the system is initialized, we can bind to it immediately @@ -94,7 +83,7 @@ namespace Content.Client.Construction.UI _placementManager.PlacementChanged += OnPlacementChanged; - _constructionView.OnClose += () => _gameHud.CraftingButtonDown = false; + _constructionView.OnClose += () => _uiManager.GetActiveUIWidget().CraftingButton.Pressed = false; _constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts(); _constructionView.PopulateRecipes += OnViewPopulateRecipes; _constructionView.RecipeSelected += OnViewRecipeSelected; @@ -110,12 +99,11 @@ namespace Content.Client.Construction.UI PopulateCategories(); OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty)); - _gameHud.CraftingButtonToggled += OnHudCraftingButtonToggled; } - private void OnHudCraftingButtonToggled(bool b) + public void OnHudCraftingButtonToggled(ButtonToggledEventArgs args) { - WindowOpen = b; + WindowOpen = args.Pressed; } /// @@ -128,8 +116,6 @@ namespace Content.Client.Construction.UI _systemManager.SystemUnloaded -= OnSystemUnloaded; _placementManager.PlacementChanged -= OnPlacementChanged; - - _gameHud.CraftingButtonToggled -= OnHudCraftingButtonToggled; } private void OnPlacementChanged(object? sender, EventArgs e) @@ -344,7 +330,10 @@ namespace Content.Client.Construction.UI system.ToggleCraftingWindow += SystemOnToggleMenu; system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged; system.ConstructionGuideAvailable += SystemGuideAvailable; - CraftingAvailable = system.CraftingEnabled; + if (_uiManager.GetActiveUIWidgetOrNull() != null) + { + CraftingAvailable = system.CraftingEnabled; + } } private void UnbindFromSystem() @@ -362,6 +351,8 @@ namespace Content.Client.Construction.UI private void SystemCraftingAvailabilityChanged(object? sender, CraftingAvailabilityChangedArgs e) { + if (_uiManager.ActiveScreen == null) + return; CraftingAvailable = e.Available; } @@ -375,7 +366,7 @@ namespace Content.Client.Construction.UI if (IsAtFront) { WindowOpen = false; - _gameHud.CraftingButtonDown = false; // This does not call CraftingButtonToggled + _uiManager.GetActiveUIWidget().CraftingButton.Pressed = false; // This does not call CraftingButtonToggled } else _constructionView.MoveToFront(); @@ -383,7 +374,7 @@ namespace Content.Client.Construction.UI else { WindowOpen = true; - _gameHud.CraftingButtonDown = true; // This does not call CraftingButtonToggled + _uiManager.GetActiveUIWidget().CraftingButton.Pressed = true; // This does not call CraftingButtonToggled } } diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj index 07bcb7563c..cad7fd5ec4 100644 --- a/Content.Client/Content.Client.csproj +++ b/Content.Client/Content.Client.csproj @@ -23,6 +23,17 @@ + + + GameTopMenuBar.xaml + + + HotbarGui.xaml + + + StrippingWindow.xaml + + diff --git a/Content.Client/Cooldown/CooldownGraphic.cs b/Content.Client/Cooldown/CooldownGraphic.cs index 3ef966a140..25063f8133 100644 --- a/Content.Client/Cooldown/CooldownGraphic.cs +++ b/Content.Client/Cooldown/CooldownGraphic.cs @@ -1,16 +1,13 @@ -using System; -using Robust.Client.Graphics; +using Robust.Client.Graphics; using Robust.Client.UserInterface; -using Robust.Shared.IoC; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Timing; namespace Content.Client.Cooldown { - public sealed class CooldownGraphic : Control { - + [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!; private readonly ShaderInstance _shader; @@ -51,6 +48,16 @@ namespace Content.Client.Cooldown handle.UseShader(null); } - } + public void FromTime(TimeSpan start, TimeSpan end) + { + var duration = end - start; + var curTime = _gameTiming.CurTime; + var length = duration.TotalSeconds; + var progress = (curTime - start).TotalSeconds / length; + var ratio = (progress <= 1 ? (1 - progress) : (curTime - end).TotalSeconds * -5); + Progress = MathHelper.Clamp((float) ratio, -1, 1); + Visible = ratio > -1f; + } + } } diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 78563a883c..32f02c7d44 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -1,12 +1,10 @@ using Content.Client.Administration.Managers; using Content.Client.Changelog; -using Content.Client.CharacterInterface; using Content.Client.Chat.Managers; using Content.Client.Options; using Content.Client.Eui; using Content.Client.Flash; using Content.Client.GhostKick; -using Content.Client.HUD; using Content.Client.Info; using Content.Client.Input; using Content.Client.IoC; @@ -31,7 +29,6 @@ using Content.Shared.Markers; using Robust.Client; using Robust.Client.Graphics; using Robust.Client.Input; -using Robust.Client.Player; using Robust.Client.State; using Robust.Client.UserInterface; using Robust.Shared.Configuration; @@ -42,14 +39,11 @@ using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Timing; namespace Content.Client.Entry { public sealed class EntryPoint : GameClient { - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IBaseClient _baseClient = default!; [Dependency] private readonly IGameController _gameController = default!; [Dependency] private readonly IStateManager _stateManager = default!; @@ -76,7 +70,6 @@ namespace Content.Client.Entry [Dependency] private readonly GhostKickManager _ghostKick = default!; [Dependency] private readonly ExtendedDisconnectInformationManager _extendedDisconnectInformation = default!; [Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!; - [Dependency] private readonly IGameHud _gameHud = default!; public const int NetBufferSizeOverride = 2; @@ -168,9 +161,6 @@ namespace Content.Client.Entry _overlayManager.AddOverlay(new FlashOverlay()); _overlayManager.AddOverlay(new RadiationPulseOverlay()); - _baseClient.PlayerJoinedServer += SubscribePlayerAttachmentEvents; - _baseClient.PlayerLeaveServer += UnsubscribePlayerAttachmentEvents; - _gameHud.Initialize(); _chatManager.Initialize(); _clientPreferencesManager.Initialize(); _euiManager.Initialize(); @@ -194,56 +184,6 @@ namespace Content.Client.Entry SwitchToDefaultState(); } - public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs) - { - base.Update(level, frameEventArgs); - - switch (level) - { - case ModUpdateLevel.FramePreEngine: - // TODO: Turn IChatManager into an EntitySystem and remove the line below. - IoCManager.Resolve().FrameUpdate(frameEventArgs); - break; - } - } - - /// - /// Subscribe events to the player manager after the player manager is set up - /// - /// - /// - public void SubscribePlayerAttachmentEvents(object? sender, EventArgs args) - { - if (_playerManager.LocalPlayer != null) - { - _playerManager.LocalPlayer.EntityAttached += AttachPlayerToEntity; - _playerManager.LocalPlayer.EntityDetached += DetachPlayerFromEntity; - } - } - public void UnsubscribePlayerAttachmentEvents(object? sender, EventArgs args) - { - if (_playerManager.LocalPlayer != null) - { - _playerManager.LocalPlayer.EntityAttached -= AttachPlayerToEntity; - _playerManager.LocalPlayer.EntityDetached -= DetachPlayerFromEntity; - } - } - - public void AttachPlayerToEntity(EntityAttachedEventArgs eventArgs) - { - // TODO This is shitcode. Move this to an entity system, FOR FUCK'S SAKE - _entityManager.AddComponent(eventArgs.NewEntity); - } - - public void DetachPlayerFromEntity(EntityDetachedEventArgs eventArgs) - { - // TODO This is shitcode. Move this to an entity system, FOR FUCK'S SAKE - if (!_entityManager.Deleted(eventArgs.OldEntity)) - { - _entityManager.RemoveComponent(eventArgs.OldEntity); - } - } - private void SwitchToDefaultState(bool disconnected = false) { // Fire off into state dependent on launcher or not. diff --git a/Content.Client/Gameplay/GameplayState.cs b/Content.Client/Gameplay/GameplayState.cs index 68e99bed0d..dece86421d 100644 --- a/Content.Client/Gameplay/GameplayState.cs +++ b/Content.Client/Gameplay/GameplayState.cs @@ -1,14 +1,8 @@ -using Content.Client.Alerts.UI; -using Content.Client.Chat; -using Content.Client.Chat.Managers; -using Content.Client.Chat.UI; using Content.Client.Construction.UI; using Content.Client.Hands; -using Content.Client.HUD; using Content.Client.UserInterface.Controls; +using Content.Client.UserInterface.Screens; using Content.Client.Viewport; -using Content.Client.Voting; -using Content.Shared.Chat; using Content.Shared.CCVar; using Robust.Client.Graphics; using Robust.Client.Input; @@ -22,40 +16,26 @@ namespace Content.Client.Gameplay { public sealed class GameplayState : GameplayStateBase, IMainViewportState { - public static readonly Vector2i ViewportSize = (EyeManager.PixelsPerMeter * 21, EyeManager.PixelsPerMeter * 15); - - [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; - [Dependency] private readonly IGameHud _gameHud = default!; - [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IVoteManager _voteManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IUserInterfaceManager _uiManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; - [ViewVariables] private ChatBox? _gameChat; - private ConstructionMenuPresenter? _constructionMenu; - private AlertsFramePresenter? _alertsFramePresenter; - + protected override Type? LinkedScreenType => typeof(DefaultGameScreen); + public static readonly Vector2i ViewportSize = (EyeManager.PixelsPerMeter * 21, EyeManager.PixelsPerMeter * 15); private FpsCounter _fpsCounter = default!; public MainViewport Viewport { get; private set; } = default!; + public GameplayState() + { + IoCManager.InjectDependencies(this); + } + protected override void Startup() { base.Startup(); - - _gameChat = new HudChatBox {PreferredChannel = ChatSelectChannel.Local}; - - UserInterfaceManager.StateRoot.AddChild(_gameChat); - LayoutContainer.SetAnchorAndMarginPreset(_gameChat, LayoutContainer.LayoutPreset.TopRight, margin: 10); - LayoutContainer.SetAnchorAndMarginPreset(_gameChat, LayoutContainer.LayoutPreset.TopRight, margin: 10); - LayoutContainer.SetMarginLeft(_gameChat, -475); - LayoutContainer.SetMarginBottom(_gameChat, HudChatBox.InitialChatBottom); - - _chatManager.ChatBoxOnResized(new ChatResizedEventArgs(HudChatBox.InitialChatBottom)); - Viewport = new MainViewport { Viewport = @@ -63,25 +43,13 @@ namespace Content.Client.Gameplay ViewportSize = ViewportSize } }; - - _userInterfaceManager.StateRoot.AddChild(Viewport); + UserInterfaceManager.StateRoot.AddChild(Viewport); LayoutContainer.SetAnchorPreset(Viewport, LayoutContainer.LayoutPreset.Wide); Viewport.SetPositionFirst(); - - _userInterfaceManager.StateRoot.AddChild(_gameHud.RootControl); - _chatManager.SetChatBox(_gameChat); - _voteManager.SetPopupContainer(_gameHud.VoteContainer); - - ChatInput.SetupChatInputHandlers(_inputManager, _gameChat); - - SetupPresenters(); - _eyeManager.MainViewport = Viewport.Viewport; - _overlayManager.AddOverlay(new ShowHandItemOverlay()); - _fpsCounter = new FpsCounter(_gameTiming); - _userInterfaceManager.StateRoot.AddChild(_fpsCounter); + UserInterfaceManager.PopupRoot.AddChild(_fpsCounter); _fpsCounter.Visible = _configurationManager.GetCVar(CCVars.HudFpsCounterVisible); _configurationManager.OnValueChanged(CCVars.HudFpsCounterVisible, (show) => { _fpsCounter.Visible = show; }); } @@ -89,56 +57,13 @@ namespace Content.Client.Gameplay protected override void Shutdown() { _overlayManager.RemoveOverlay(); - DisposePresenters(); base.Shutdown(); - - _gameChat?.Dispose(); Viewport.Dispose(); - _gameHud.RootControl.Orphan(); // Clear viewport to some fallback, whatever. - _eyeManager.MainViewport = _userInterfaceManager.MainViewport; + _eyeManager.MainViewport = UserInterfaceManager.MainViewport; _fpsCounter.Dispose(); - } - - /// - /// All UI Presenters should be constructed in here. - /// - private void SetupPresenters() - { - // HUD - _alertsFramePresenter = new AlertsFramePresenter(); - - // Windows - _constructionMenu = new ConstructionMenuPresenter(_gameHud); - } - - /// - /// All UI Presenters should be disposed in here. - /// - private void DisposePresenters() - { - // Windows - _constructionMenu?.Dispose(); - - // HUD - _alertsFramePresenter?.Dispose(); - } - - internal static void FocusChat(ChatBox chat) - { - if (chat.UserInterfaceManager.KeyboardFocused != null) - return; - - chat.Focus(); - } - - internal static void FocusChannel(ChatBox chat, ChatSelectChannel channel) - { - if (chat.UserInterfaceManager.KeyboardFocused != null) - return; - - chat.Focus(channel); + _uiManager.ClearWindows(); } public override void FrameUpdate(FrameEventArgs e) diff --git a/Content.Client/Ghost/GhostComponent.cs b/Content.Client/Ghost/GhostComponent.cs index 57f7b7753a..3b228a8f8b 100644 --- a/Content.Client/Ghost/GhostComponent.cs +++ b/Content.Client/Ghost/GhostComponent.cs @@ -1,6 +1,4 @@ -using Content.Client.UserInterface.Systems.Ghost; using Content.Shared.Ghost; -using Robust.Client.Player; namespace Content.Client.Ghost { @@ -8,24 +6,6 @@ namespace Content.Client.Ghost [ComponentReference(typeof(SharedGhostComponent))] public sealed class GhostComponent : SharedGhostComponent { - [Dependency] private readonly IPlayerManager _playerManager = default!; - - public GhostGui? Gui { get; set; } public bool IsAttached { get; set; } - - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - - if (curState is not GhostComponentState) - { - return; - } - - if (Owner == _playerManager.LocalPlayer?.ControlledEntity) - { - Gui?.Update(); - } - } } } diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index f44ba7b31a..09e0331cdf 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -1,21 +1,19 @@ -using Content.Client.HUD; -using Content.Client.UserInterface.Systems.Ghost; using Content.Shared.Ghost; using JetBrains.Annotations; +using Robust.Client.Console; using Robust.Client.GameObjects; using Robust.Client.Player; +using Robust.Shared.GameStates; namespace Content.Client.Ghost { [UsedImplicitly] public sealed class GhostSystem : SharedGhostSystem { + [Dependency] private readonly IClientConsoleHost _console = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IGameHud _gameHud = default!; - // Changes to this value are manually propagated. - // No good way to get an event into the UI. - public int AvailableGhostRoleCount { get; private set; } = 0; + public int AvailableGhostRoleCount { get; private set; } private bool _ghostVisibility = true; @@ -38,23 +36,36 @@ namespace Content.Client.Ghost } } + public GhostComponent? Player => CompOrNull(_playerManager.LocalPlayer?.ControlledEntity); + public bool IsGhost => Player != null; + + public event Action? PlayerRemoved; + public event Action? PlayerUpdated; + public event Action? PlayerAttached; + public event Action? PlayerDetached; + public event Action? GhostWarpsResponse; + public event Action? GhostRoleCountUpdated; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGhostInit); SubscribeLocalEvent(OnGhostRemove); + SubscribeLocalEvent(OnGhostState); SubscribeLocalEvent(OnGhostPlayerAttach); SubscribeLocalEvent(OnGhostPlayerDetach); + SubscribeLocalEvent(OnPlayerAttach); + SubscribeNetworkEvent(OnGhostWarpsResponse); SubscribeNetworkEvent(OnUpdateGhostRoleCount); } private void OnGhostInit(EntityUid uid, GhostComponent component, ComponentInit args) { - if (EntityManager.TryGetComponent(component.Owner, out SpriteComponent? sprite)) + if (TryComp(component.Owner, out SpriteComponent? sprite)) { sprite.Visible = GhostVisibility; } @@ -62,55 +73,87 @@ namespace Content.Client.Ghost private void OnGhostRemove(EntityUid uid, GhostComponent component, ComponentRemove args) { - component.Gui?.Dispose(); - component.Gui = null; + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + if (component.IsAttached) + { + GhostVisibility = false; + } + + PlayerRemoved?.Invoke(component); } private void OnGhostPlayerAttach(EntityUid uid, GhostComponent component, PlayerAttachedEvent playerAttachedEvent) { - // I hate UI I hate UI I Hate UI - if (component.Gui == null) - { - component.Gui = new GhostGui(component, this, EntityManager.EntityNetManager!); - component.Gui.Update(); - } + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; - _gameHud.HandsContainer.AddChild(component.Gui); GhostVisibility = true; component.IsAttached = true; + PlayerAttached?.Invoke(component); + } + + private void OnGhostState(EntityUid uid, GhostComponent component, ref ComponentHandleState args) + { + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + PlayerUpdated?.Invoke(component); + } + + private bool PlayerDetach(EntityUid uid) + { + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return false; + + GhostVisibility = false; + PlayerDetached?.Invoke(); + return true; } private void OnGhostPlayerDetach(EntityUid uid, GhostComponent component, PlayerDetachedEvent args) { - component.Gui?.Parent?.RemoveChild(component.Gui); - GhostVisibility = false; - component.IsAttached = false; + if (PlayerDetach(uid)) + component.IsAttached = false; + } + + private void OnPlayerAttach(PlayerAttachedEvent ev) + { + if (!HasComp(ev.Entity)) + PlayerDetach(ev.Entity); } private void OnGhostWarpsResponse(GhostWarpsResponseEvent msg) { - var entity = _playerManager.LocalPlayer?.ControlledEntity; - - if (entity == null || - !EntityManager.TryGetComponent(entity.Value, out GhostComponent? ghost)) + if (!IsGhost) { return; } - var window = ghost.Gui?.TargetWindow; - - if (window != null) - { - window.UpdateWarps(msg.Warps); - window.Populate(); - } + GhostWarpsResponse?.Invoke(msg); } private void OnUpdateGhostRoleCount(GhostUpdateGhostRoleCountEvent msg) { AvailableGhostRoleCount = msg.AvailableGhostRoles; - foreach (var ghost in EntityManager.EntityQuery(true)) - ghost.Gui?.Update(); + GhostRoleCountUpdated?.Invoke(msg); + } + + public void RequestWarps() + { + RaiseNetworkEvent(new GhostWarpsRequestEvent()); + } + + public void ReturnToBody() + { + var msg = new GhostReturnToBodyRequest(); + RaiseNetworkEvent(msg); + } + + public void OpenGhostRoles() + { + _console.RemoteExecuteCommand(null, "ghostroles"); } } } diff --git a/Content.Client/HUD/GameHud.ButtonBar.cs b/Content.Client/HUD/GameHud.ButtonBar.cs deleted file mode 100644 index e4925213d2..0000000000 --- a/Content.Client/HUD/GameHud.ButtonBar.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System; -using Content.Client.HUD.UI; -using Content.Client.Resources; -using Content.Client.Stylesheets; -using Content.Shared.Input; -using Robust.Client.Input; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Input; -using Robust.Shared.Localization; -using Robust.Shared.Maths; - -namespace Content.Client.HUD; - -public interface IButtonBarView -{ - // Escape top button. - bool EscapeButtonDown { get; set; } - event Action EscapeButtonToggled; - - // Character top button. - bool CharacterButtonDown { get; set; } - bool CharacterButtonVisible { get; set; } - event Action CharacterButtonToggled; - - // Inventory top button. - bool InventoryButtonDown { get; set; } - bool InventoryButtonVisible { get; set; } - event Action InventoryButtonToggled; - - // Crafting top button. - bool CraftingButtonDown { get; set; } - bool CraftingButtonVisible { get; set; } - event Action CraftingButtonToggled; - - // Actions top button. - bool ActionsButtonDown { get; set; } - bool ActionsButtonVisible { get; set; } - event Action ActionsButtonToggled; - - // Admin top button. - bool AdminButtonDown { get; set; } - bool AdminButtonVisible { get; set; } - event Action AdminButtonToggled; - - // Sandbox top button. - bool SandboxButtonDown { get; set; } - bool SandboxButtonVisible { get; set; } - event Action SandboxButtonToggled; - - // Info top button - event Action InfoButtonPressed; - void SetInfoRed(bool value); -} - -internal sealed partial class GameHud -{ - private TopButton _buttonEscapeMenu = default!; - private TopButton _buttonInfo = default!; - private TopButton _buttonCharacterMenu = default!; - private TopButton _buttonInventoryMenu = default!; - private TopButton _buttonCraftingMenu = default!; - private TopButton _buttonActionsMenu = default!; - private TopButton _buttonAdminMenu = default!; - private TopButton _buttonSandboxMenu = default!; - - private BoxContainer GenerateButtonBar(IResourceCache resourceCache, IInputManager inputManager) - { - var topButtonsContainer = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - SeparationOverride = 8 - }; - - LayoutContainer.SetAnchorAndMarginPreset(topButtonsContainer, LayoutContainer.LayoutPreset.TopLeft, margin: 10); - - // the icon textures here should all have the same image height (32) but different widths, so in order to ensure - // the buttons themselves are consistent widths we set a common custom min size - Vector2 topMinSize = (42, 64); - - // Escape - { - _buttonEscapeMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/hamburger.svg.192dpi.png"), - EngineKeyFunctions.EscapeMenu, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-escape-menu-button-tooltip"), - MinSize = (70, 64), - StyleClasses = { StyleBase.ButtonOpenRight } - }; - - topButtonsContainer.AddChild(_buttonEscapeMenu); - - _buttonEscapeMenu.OnToggled += args => EscapeButtonToggled?.Invoke(args.Pressed); - } - - // Character - { - _buttonCharacterMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/character.svg.192dpi.png"), - ContentKeyFunctions.OpenCharacterMenu, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-character-menu-button-tooltip"), - MinSize = topMinSize, - Visible = false, - StyleClasses = { StyleBase.ButtonSquare } - }; - - topButtonsContainer.AddChild(_buttonCharacterMenu); - - _buttonCharacterMenu.OnToggled += args => CharacterButtonToggled?.Invoke(args.Pressed); - } - - // Inventory - { - _buttonInventoryMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/inventory.svg.192dpi.png"), - ContentKeyFunctions.OpenInventoryMenu, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-inventory-menu-button-tooltip"), - MinSize = topMinSize, - Visible = false, - StyleClasses = { StyleBase.ButtonSquare } - }; - - topButtonsContainer.AddChild(_buttonInventoryMenu); - - _buttonInventoryMenu.OnToggled += args => InventoryButtonToggled?.Invoke(args.Pressed); - } - - // Crafting - { - _buttonCraftingMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/hammer.svg.192dpi.png"), - ContentKeyFunctions.OpenCraftingMenu, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-crafting-menu-button-tooltip"), - MinSize = topMinSize, - Visible = false, - StyleClasses = { StyleBase.ButtonSquare } - }; - - topButtonsContainer.AddChild(_buttonCraftingMenu); - - _buttonCraftingMenu.OnToggled += args => CraftingButtonToggled?.Invoke(args.Pressed); - } - - // Actions - { - _buttonActionsMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/fist.svg.192dpi.png"), - ContentKeyFunctions.OpenActionsMenu, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-actions-menu-button-tooltip"), - MinSize = topMinSize, - Visible = false, - StyleClasses = { StyleBase.ButtonSquare } - }; - - topButtonsContainer.AddChild(_buttonActionsMenu); - - _buttonActionsMenu.OnToggled += args => ActionsButtonToggled?.Invoke(args.Pressed); - } - - // Admin - { - _buttonAdminMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/gavel.svg.192dpi.png"), - ContentKeyFunctions.OpenAdminMenu, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-admin-menu-button-tooltip"), - MinSize = topMinSize, - Visible = false, - StyleClasses = { StyleBase.ButtonSquare } - }; - - topButtonsContainer.AddChild(_buttonAdminMenu); - - _buttonAdminMenu.OnToggled += args => AdminButtonToggled?.Invoke(args.Pressed); - } - - // Sandbox - { - _buttonSandboxMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/sandbox.svg.192dpi.png"), - ContentKeyFunctions.OpenSandboxWindow, inputManager) - { - ToolTip = Loc.GetString("game-hud-open-sandbox-menu-button-tooltip"), - MinSize = topMinSize, - Visible = false, - StyleClasses = { StyleBase.ButtonSquare } - }; - - topButtonsContainer.AddChild(_buttonSandboxMenu); - - _buttonSandboxMenu.OnToggled += args => SandboxButtonToggled?.Invoke(args.Pressed); - } - - // Info Window - { - _buttonInfo = new TopButton(resourceCache.GetTexture("/Textures/Interface/info.svg.192dpi.png"), - ContentKeyFunctions.OpenInfo, inputManager) - { - ToolTip = Loc.GetString("ui-options-function-open-info"), - MinSize = topMinSize, - StyleClasses = { StyleBase.ButtonOpenLeft }, - ToggleMode = false - }; - - topButtonsContainer.AddChild(_buttonInfo); - - _buttonInfo.OnPressed += args => InfoButtonPressed?.Invoke(); - } - - return topButtonsContainer; - } - - /// - public bool EscapeButtonDown - { - get => _buttonEscapeMenu.Pressed; - set => _buttonEscapeMenu.Pressed = value; - } - - /// - public event Action? EscapeButtonToggled; - - /// - public bool CharacterButtonDown - { - get => _buttonCharacterMenu.Pressed; - set => _buttonCharacterMenu.Pressed = value; - } - - /// - public bool CharacterButtonVisible - { - get => _buttonCharacterMenu.Visible; - set => _buttonCharacterMenu.Visible = value; - } - - /// - public event Action? CharacterButtonToggled; - - /// - public bool InventoryButtonDown - { - get => _buttonInventoryMenu.Pressed; - set => _buttonInventoryMenu.Pressed = value; - } - - /// - public bool InventoryButtonVisible - { - get => _buttonInventoryMenu.Visible; - set => _buttonInventoryMenu.Visible = value; - } - - /// - public event Action? InventoryButtonToggled; - - /// - public bool CraftingButtonDown - { - get => _buttonCraftingMenu.Pressed; - set => _buttonCraftingMenu.Pressed = value; - } - - /// - public bool CraftingButtonVisible - { - get => _buttonCraftingMenu.Visible; - set => _buttonCraftingMenu.Visible = value; - } - - /// - public event Action? CraftingButtonToggled; - - /// - public bool ActionsButtonDown - { - get => _buttonActionsMenu.Pressed; - set => _buttonActionsMenu.Pressed = value; - } - - /// - public bool ActionsButtonVisible - { - get => _buttonActionsMenu.Visible; - set => _buttonActionsMenu.Visible = value; - } - - /// - public event Action? ActionsButtonToggled; - - /// - public bool AdminButtonDown - { - get => _buttonAdminMenu.Pressed; - set => _buttonAdminMenu.Pressed = value; - } - - /// - public bool AdminButtonVisible - { - get => _buttonAdminMenu.Visible; - set => _buttonAdminMenu.Visible = value; - } - - /// - public event Action? AdminButtonToggled; - - /// - public bool SandboxButtonDown - { - get => _buttonSandboxMenu.Pressed; - set => _buttonSandboxMenu.Pressed = value; - } - - /// - public bool SandboxButtonVisible - { - get => _buttonSandboxMenu.Visible; - set => _buttonSandboxMenu.Visible = value; - } - - /// - public event Action? SandboxButtonToggled; - - /// - public event Action? InfoButtonPressed; - - public void SetInfoRed(bool value) - { - if (value) - _buttonInfo.StyleClasses.Add(TopButton.StyleClassRedTopButton); - else - _buttonInfo.StyleClasses.Remove(TopButton.StyleClassRedTopButton); - } -} diff --git a/Content.Client/HUD/GameHud.cs b/Content.Client/HUD/GameHud.cs deleted file mode 100644 index d31960894c..0000000000 --- a/Content.Client/HUD/GameHud.cs +++ /dev/null @@ -1,274 +0,0 @@ -using System; -using System.Linq; -using Content.Client.HUD.UI; -using Content.Client.Info; -using Content.Client.Administration; -using Content.Client.Administration.Systems; -using Content.Client.Resources; -using Content.Client.Targeting; -using Content.Shared.CCVar; -using Content.Shared.HUD; -using Content.Shared.Input; -using Content.Shared.Targeting; -using Robust.Client.Graphics; -using Robust.Client.Input; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Configuration; -using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; -using static Robust.Client.UserInterface.Controls.BoxContainer; -using Control = Robust.Client.UserInterface.Control; -using LC = Robust.Client.UserInterface.Controls.LayoutContainer; - -namespace Content.Client.HUD -{ - /// - /// Responsible for laying out the default game HUD. - /// - public interface IGameHud : IButtonBarView - { - Control RootControl { get; } - - Control HandsContainer { get; } - Control SuspicionContainer { get; } - Control BottomLeftInventoryQuickButtonContainer { get; } - Control BottomRightInventoryQuickButtonContainer { get; } - Control TopInventoryQuickButtonContainer { get; } - - bool CombatPanelVisible { get; set; } - TargetingZone TargetingZone { get; set; } - Action? OnTargetingZoneChanged { get; set; } - - Control VoteContainer { get; } - - void AddTopNotification(TopNotification notification); - - Texture GetHudTexture(string path); - - bool ValidateHudTheme(int idx); - - // Init logic. - void Initialize(); - } - - internal sealed partial class GameHud : IGameHud - { - private TargetingDoll _targetingDoll = default!; - private BoxContainer _combatPanelContainer = default!; - private BoxContainer _topNotificationContainer = default!; - - [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IInputManager _inputManager = default!; - [Dependency] private readonly INetConfigurationManager _configManager = default!; - - public Control HandsContainer { get; private set; } = default!; - public Control SuspicionContainer { get; private set; } = default!; - public Control TopInventoryQuickButtonContainer { get; private set; } = default!; - public Control BottomLeftInventoryQuickButtonContainer { get; private set; } = default!; - public Control BottomRightInventoryQuickButtonContainer { get; private set; } = default!; - - public bool CombatPanelVisible - { - get => _combatPanelContainer.Visible; - set => _combatPanelContainer.Visible = value; - } - - public TargetingZone TargetingZone - { - get => _targetingDoll.ActiveZone; - set => _targetingDoll.ActiveZone = value; - } - public Action? OnTargetingZoneChanged { get; set; } - - public void AddTopNotification(TopNotification notification) - { - _topNotificationContainer.AddChild(notification); - } - - public bool ValidateHudTheme(int idx) - { - if (!_prototypeManager.TryIndex(idx.ToString(), out HudThemePrototype? _)) - { - Logger.ErrorS("hud", "invalid HUD theme id {0}, using different theme", - idx); - var proto = _prototypeManager.EnumeratePrototypes().FirstOrDefault(); - if (proto == null) - { - throw new NullReferenceException("No valid HUD prototypes!"); - } - var id = int.Parse(proto.ID); - _configManager.SetCVar(CCVars.HudTheme, id); - return false; - } - return true; - } - - public Texture GetHudTexture(string path) - { - var id = _configManager.GetCVar("hud.theme"); - var dir = string.Empty; - if (!_prototypeManager.TryIndex(id.ToString(), out HudThemePrototype? proto)) - { - throw new ArgumentOutOfRangeException(); - } - dir = proto.Path; - - var resourcePath = (new ResourcePath("/Textures/Interface/") / dir) / "Slots" / path; - return _resourceCache.GetTexture(resourcePath); - } - - public void Initialize() - { - RootControl = new LC { Name = "AAAAAAAAAAAAAAAAAAAAAA"}; - LC.SetAnchorPreset(RootControl, LC.LayoutPreset.Wide); - - RootControl.AddChild(GenerateButtonBar(_resourceCache, _inputManager)); - - InventoryButtonToggled += down => TopInventoryQuickButtonContainer.Visible = down; - InfoButtonPressed += () => ButtonInfoOnPressed(); - - _inputManager.SetInputCommand(ContentKeyFunctions.OpenInfo, - InputCmdHandler.FromDelegate(s => ButtonInfoOnPressed())); - - _combatPanelContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - HorizontalAlignment = Control.HAlignment.Left, - VerticalAlignment = Control.VAlignment.Bottom, - Children = - { - (_targetingDoll = new TargetingDoll(_resourceCache)) - } - }; - - LC.SetGrowHorizontal(_combatPanelContainer, LC.GrowDirection.Begin); - LC.SetGrowVertical(_combatPanelContainer, LC.GrowDirection.Begin); - LC.SetAnchorAndMarginPreset(_combatPanelContainer, LC.LayoutPreset.BottomRight); - LC.SetMarginBottom(_combatPanelContainer, -10f); - - _targetingDoll.OnZoneChanged += args => OnTargetingZoneChanged?.Invoke(args); - - var centerBottomContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - SeparationOverride = 5, - HorizontalAlignment = Control.HAlignment.Center - }; - LC.SetAnchorAndMarginPreset(centerBottomContainer, LC.LayoutPreset.CenterBottom); - LC.SetGrowHorizontal(centerBottomContainer, LC.GrowDirection.Both); - LC.SetGrowVertical(centerBottomContainer, LC.GrowDirection.Begin); - LC.SetMarginBottom(centerBottomContainer, -10f); - RootControl.AddChild(centerBottomContainer); - - HandsContainer = new BoxContainer() - { - VerticalAlignment = Control.VAlignment.Bottom, - HorizontalAlignment = Control.HAlignment.Center, - Orientation = LayoutOrientation.Vertical, - }; - BottomRightInventoryQuickButtonContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - VerticalAlignment = Control.VAlignment.Bottom, - HorizontalAlignment = Control.HAlignment.Right - }; - BottomLeftInventoryQuickButtonContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - VerticalAlignment = Control.VAlignment.Bottom, - HorizontalAlignment = Control.HAlignment.Left - }; - TopInventoryQuickButtonContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - Visible = false, - VerticalAlignment = Control.VAlignment.Bottom, - HorizontalAlignment = Control.HAlignment.Center - }; - var bottomRow = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalAlignment = Control.HAlignment.Center - - }; - bottomRow.AddChild(new Control {MinSize = (69, 0)}); //Padding (nice) - bottomRow.AddChild(BottomLeftInventoryQuickButtonContainer); - bottomRow.AddChild(HandsContainer); - bottomRow.AddChild(BottomRightInventoryQuickButtonContainer); - bottomRow.AddChild(new Control {MinSize = (1, 0)}); //Padding - - - centerBottomContainer.AddChild(TopInventoryQuickButtonContainer); - centerBottomContainer.AddChild(bottomRow); - - SuspicionContainer = new Control - { - HorizontalAlignment = Control.HAlignment.Center - }; - - var rightBottomContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - SeparationOverride = 5 - }; - LC.SetAnchorAndMarginPreset(rightBottomContainer, LC.LayoutPreset.BottomRight); - LC.SetGrowHorizontal(rightBottomContainer, LC.GrowDirection.Begin); - LC.SetGrowVertical(rightBottomContainer, LC.GrowDirection.Begin); - LC.SetMarginBottom(rightBottomContainer, -10f); - LC.SetMarginRight(rightBottomContainer, -10f); - RootControl.AddChild(rightBottomContainer); - - rightBottomContainer.AddChild(_combatPanelContainer); - - RootControl.AddChild(SuspicionContainer); - - LC.SetAnchorAndMarginPreset(SuspicionContainer, LC.LayoutPreset.BottomLeft, - margin: 10); - LC.SetGrowHorizontal(SuspicionContainer, LC.GrowDirection.End); - LC.SetGrowVertical(SuspicionContainer, LC.GrowDirection.Begin); - - _topNotificationContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - MinSize = (600, 0) - }; - RootControl.AddChild(_topNotificationContainer); - LC.SetAnchorPreset(_topNotificationContainer, LC.LayoutPreset.CenterTop); - LC.SetGrowHorizontal(_topNotificationContainer, LC.GrowDirection.Both); - LC.SetGrowVertical(_topNotificationContainer, LC.GrowDirection.End); - - VoteContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - RootControl.AddChild(VoteContainer); - LC.SetAnchorPreset(VoteContainer, LC.LayoutPreset.TopLeft); - LC.SetMarginLeft(VoteContainer, 180); - LC.SetMarginTop(VoteContainer, 100); - LC.SetGrowHorizontal(VoteContainer, LC.GrowDirection.End); - LC.SetGrowVertical(VoteContainer, LC.GrowDirection.End); - } - - private void ButtonInfoOnPressed() - { - var bwoinkSystem = EntitySystem.Get(); - if (bwoinkSystem.IsOpen) - { - bwoinkSystem.Close(); - } - else - { - bwoinkSystem.Open(); - } - } - - public Control RootControl { get; private set; } = default!; - - public Control VoteContainer { get; private set; } = default!; - } -} diff --git a/Content.Client/HUD/UI/TopButton.cs b/Content.Client/HUD/UI/TopButton.cs deleted file mode 100644 index a8d2d39b3f..0000000000 --- a/Content.Client/HUD/UI/TopButton.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Robust.Client.Graphics; -using Robust.Client.Input; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Input; -using Robust.Shared.Localization; -using Robust.Shared.Maths; -using Robust.Shared.Utility; - -namespace Content.Client.HUD.UI; - -internal sealed class TopButton : ContainerButton -{ - public const string StyleClassLabelTopButton = "topButtonLabel"; - public const string StyleClassRedTopButton = "topButtonLabel"; - private const float CustomTooltipDelay = 0.4f; - - private static readonly Color ColorNormal = Color.FromHex("#7b7e9e"); - private static readonly Color ColorRedNormal = Color.FromHex("#FEFEFE"); - private static readonly Color ColorHovered = Color.FromHex("#9699bb"); - private static readonly Color ColorRedHovered = Color.FromHex("#FFFFFF"); - private static readonly Color ColorPressed = Color.FromHex("#789B8C"); - - private const float VertPad = 8f; - private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal; - private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered; - - private readonly TextureRect _textureRect; - private readonly Label _label; - private readonly BoundKeyFunction _function; - private readonly IInputManager _inputManager; - - public TopButton(Texture texture, BoundKeyFunction function, IInputManager inputManager) - { - _function = function; - _inputManager = inputManager; - TooltipDelay = CustomTooltipDelay; - - AddChild( - new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Vertical, - Children = - { - (_textureRect = new TextureRect - { - TextureScale = (0.5f, 0.5f), - Texture = texture, - HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Center, - VerticalExpand = true, - Margin = new Thickness(0, VertPad), - ModulateSelfOverride = NormalColor, - Stretch = TextureRect.StretchMode.KeepCentered - }), - (_label = new Label - { - Text = ShortKeyName(_function), - HorizontalAlignment = HAlignment.Center, - ModulateSelfOverride = NormalColor, - StyleClasses = {StyleClassLabelTopButton} - }) - } - } - ); - - ToggleMode = true; - } - - protected override void EnteredTree() - { - _inputManager.OnKeyBindingAdded += OnKeyBindingChanged; - _inputManager.OnKeyBindingRemoved += OnKeyBindingChanged; - _inputManager.OnInputModeChanged += OnKeyBindingChanged; - } - - protected override void ExitedTree() - { - _inputManager.OnKeyBindingAdded -= OnKeyBindingChanged; - _inputManager.OnKeyBindingRemoved -= OnKeyBindingChanged; - _inputManager.OnInputModeChanged -= OnKeyBindingChanged; - } - - - private void OnKeyBindingChanged(IKeyBinding obj) - { - _label.Text = ShortKeyName(_function); - } - - private void OnKeyBindingChanged() - { - _label.Text = ShortKeyName(_function); - } - - private string ShortKeyName(BoundKeyFunction keyFunction) - { - // need to use shortened key names so they fit in the buttons. - return TryGetShortKeyName(keyFunction, out var name) ? Loc.GetString(name) : " "; - } - - private bool TryGetShortKeyName(BoundKeyFunction keyFunction, [NotNullWhen(true)] out string? name) - { - if (_inputManager.TryGetKeyBinding(keyFunction, out var binding)) - { - // can't possibly fit a modifier key in the top button, so omit it - var key = binding.BaseKey; - if (binding.Mod1 != Keyboard.Key.Unknown || binding.Mod2 != Keyboard.Key.Unknown || - binding.Mod3 != Keyboard.Key.Unknown) - { - name = null; - return false; - } - - name = null; - name = key switch - { - Keyboard.Key.Apostrophe => "'", - Keyboard.Key.Comma => ",", - Keyboard.Key.Delete => "Del", - Keyboard.Key.Down => "Dwn", - Keyboard.Key.Escape => "Esc", - Keyboard.Key.Equal => "=", - Keyboard.Key.Home => "Hom", - Keyboard.Key.Insert => "Ins", - Keyboard.Key.Left => "Lft", - Keyboard.Key.Menu => "Men", - Keyboard.Key.Minus => "-", - Keyboard.Key.Num0 => "0", - Keyboard.Key.Num1 => "1", - Keyboard.Key.Num2 => "2", - Keyboard.Key.Num3 => "3", - Keyboard.Key.Num4 => "4", - Keyboard.Key.Num5 => "5", - Keyboard.Key.Num6 => "6", - Keyboard.Key.Num7 => "7", - Keyboard.Key.Num8 => "8", - Keyboard.Key.Num9 => "9", - Keyboard.Key.Pause => "||", - Keyboard.Key.Period => ".", - Keyboard.Key.Return => "Ret", - Keyboard.Key.Right => "Rgt", - Keyboard.Key.Slash => "/", - Keyboard.Key.Space => "Spc", - Keyboard.Key.Tab => "Tab", - Keyboard.Key.Tilde => "~", - Keyboard.Key.BackSlash => "\\", - Keyboard.Key.BackSpace => "Bks", - Keyboard.Key.LBracket => "[", - Keyboard.Key.MouseButton4 => "M4", - Keyboard.Key.MouseButton5 => "M5", - Keyboard.Key.MouseButton6 => "M6", - Keyboard.Key.MouseButton7 => "M7", - Keyboard.Key.MouseButton8 => "M8", - Keyboard.Key.MouseButton9 => "M9", - Keyboard.Key.MouseLeft => "ML", - Keyboard.Key.MouseMiddle => "MM", - Keyboard.Key.MouseRight => "MR", - Keyboard.Key.NumpadDecimal => "N.", - Keyboard.Key.NumpadDivide => "N/", - Keyboard.Key.NumpadEnter => "Ent", - Keyboard.Key.NumpadMultiply => "*", - Keyboard.Key.NumpadNum0 => "0", - Keyboard.Key.NumpadNum1 => "1", - Keyboard.Key.NumpadNum2 => "2", - Keyboard.Key.NumpadNum3 => "3", - Keyboard.Key.NumpadNum4 => "4", - Keyboard.Key.NumpadNum5 => "5", - Keyboard.Key.NumpadNum6 => "6", - Keyboard.Key.NumpadNum7 => "7", - Keyboard.Key.NumpadNum8 => "8", - Keyboard.Key.NumpadNum9 => "9", - Keyboard.Key.NumpadSubtract => "N-", - Keyboard.Key.PageDown => "PgD", - Keyboard.Key.PageUp => "PgU", - Keyboard.Key.RBracket => "]", - Keyboard.Key.SemiColon => ";", - _ => DefaultShortKeyName(keyFunction) - }; - return name != null; - } - - name = null; - return false; - } - - private string? DefaultShortKeyName(BoundKeyFunction keyFunction) - { - var name = FormattedMessage.EscapeText(_inputManager.GetKeyFunctionButtonString(keyFunction)); - return name.Length > 3 ? null : name; - } - - protected override void StylePropertiesChanged() - { - // colors of children depend on style, so ensure we update when style is changed - base.StylePropertiesChanged(); - UpdateChildColors(); - } - - private void UpdateChildColors() - { - if (_label == null || _textureRect == null) return; - switch (DrawMode) - { - case DrawModeEnum.Normal: - _textureRect.ModulateSelfOverride = NormalColor; - _label.ModulateSelfOverride = NormalColor; - break; - - case DrawModeEnum.Pressed: - _textureRect.ModulateSelfOverride = ColorPressed; - _label.ModulateSelfOverride = ColorPressed; - break; - - case DrawModeEnum.Hover: - _textureRect.ModulateSelfOverride = HoveredColor; - _label.ModulateSelfOverride = HoveredColor; - break; - - case DrawModeEnum.Disabled: - break; - } - } - - - protected override void DrawModeChanged() - { - base.DrawModeChanged(); - UpdateChildColors(); - } -} diff --git a/Content.Client/HUD/UI/TopNotification.cs b/Content.Client/HUD/UI/TopNotification.cs deleted file mode 100644 index a8d3a67ae4..0000000000 --- a/Content.Client/HUD/UI/TopNotification.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Client.UserInterface; - -namespace Content.Client.HUD.UI -{ - public sealed class TopNotification : Control - { - - } -} diff --git a/Content.Client/Hands/HandButton.cs b/Content.Client/Hands/HandButton.cs deleted file mode 100644 index a98deb4134..0000000000 --- a/Content.Client/Hands/HandButton.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Content.Client.HUD; -using Content.Client.Items.UI; -using Content.Shared.Hands.Components; -using Robust.Client.Graphics; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.Hands -{ - public sealed class HandButton : ItemSlotButton - { - private bool _activeHand; - private bool _highlighted; - - public HandButton(int size, string textureName, string storageTextureName, IGameHud gameHud, Texture blockedTexture, HandLocation location) : base(size, textureName, storageTextureName, gameHud) - { - Location = location; - - AddChild(Blocked = new TextureRect - { - Texture = blockedTexture, - TextureScale = (2, 2), - MouseFilter = MouseFilterMode.Stop, - Visible = false - }); - } - - public HandLocation Location { get; } - public TextureRect Blocked { get; } - - public void SetActiveHand(bool active) - { - _activeHand = active; - UpdateHighlight(); - } - - public override void Highlight(bool highlight) - { - _highlighted = highlight; - UpdateHighlight(); - } - - private void UpdateHighlight() - { - // always stay highlighted if active - base.Highlight(_activeHand || _highlighted); - } - } -} diff --git a/Content.Client/Hands/HandsComponent.cs b/Content.Client/Hands/HandsComponent.cs index 7214b098de..aa38598bfc 100644 --- a/Content.Client/Hands/HandsComponent.cs +++ b/Content.Client/Hands/HandsComponent.cs @@ -15,8 +15,6 @@ namespace Content.Client.Hands [DataField("showInHands")] public bool ShowInHands = true; - public HandsGui? Gui { get; set; } - /// /// Data about the current sprite layers that the hand is contributing to the owner entity. Used for sprite in-hands. /// diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index 97557d27b5..dab97ee99b 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -1,8 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Client.Animations; -using Content.Client.Hands.UI; -using Content.Client.HUD; +using Content.Client.Examine; +using Content.Client.Verbs; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -21,24 +20,39 @@ namespace Content.Client.Hands.Systems public sealed class HandsSystem : SharedHandsSystem { [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly ExamineSystem _examine = default!; + [Dependency] private readonly VerbSystem _verbs = default!; + + public event Action? OnPlayerAddHand; + public event Action? OnPlayerRemoveHand; + public event Action? OnPlayerSetActiveHand; + public event Action? OnPlayerHandsAdded; + public event Action? OnPlayerHandsRemoved; + public event Action? OnPlayerItemAdded; + public event Action? OnPlayerItemRemoved; + public event Action? OnPlayerHandBlocked; + public event Action? OnPlayerHandUnblocked; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(HandleContainerModified); - SubscribeLocalEvent(HandleContainerModified); + SubscribeLocalEvent(HandleItemRemoved); + SubscribeLocalEvent(HandleItemAdded); SubscribeLocalEvent(HandlePlayerAttached); SubscribeLocalEvent(HandlePlayerDetached); + SubscribeLocalEvent(HandleCompAdd); SubscribeLocalEvent(HandleCompRemove); SubscribeLocalEvent(HandleComponentState); SubscribeLocalEvent(OnVisualsChanged); SubscribeNetworkEvent(HandlePickupAnimation); + + OnHandSetActive += OnHandActivated; } #region StateHandling @@ -49,30 +63,42 @@ namespace Content.Client.Hands.Systems var handsModified = component.Hands.Count != state.Hands.Count; var manager = EnsureComp(uid); - foreach (var hand in state.Hands) - { - if (component.Hands.TryAdd(hand.Name, hand)) - { - hand.Container = _containerSystem.EnsureContainer(uid, hand.Name, manager); - handsModified = true; - } - } if (handsModified) { + List addedHands = new(); + foreach (var hand in state.Hands) + { + if (component.Hands.TryAdd(hand.Name, hand)) + { + hand.Container = _containerSystem.EnsureContainer(uid, hand.Name, manager); + addedHands.Add(hand); + } + } + foreach (var name in component.Hands.Keys) { if (!state.HandNames.Contains(name)) - component.Hands.Remove(name); + { + RemoveHand(uid, name, component); + } + } + + foreach (var hand in addedHands) + { + AddHand(uid, hand, component); } component.SortedHands = new(state.HandNames); } - TrySetActiveHand(uid, state.ActiveHand, component); + if (component.ActiveHand == null && state.ActiveHand == null) + return; //edge case - if (uid == _playerManager.LocalPlayer?.ControlledEntity) - UpdateGui(); + if (component.ActiveHand != null && state.ActiveHand != component.ActiveHand.Name) + { + SetActiveHand(uid, component.Hands[state.ActiveHand!], component); + } } #endregion @@ -175,13 +201,64 @@ namespace Content.Client.Hands.Systems EntityManager.RaisePredictiveEvent(new RequestActivateInHandEvent(handName)); } - #region visuals - private void HandleContainerModified(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args) + public void UIInventoryExamine(string handName) { - if (handComp.Hands.TryGetValue(args.Container.ID, out var hand)) + if (!TryGetPlayerHands(out var hands) || + !hands.Hands.TryGetValue(handName, out var hand) || + hand.HeldEntity is not { Valid: true } entity) { - UpdateHandVisuals(uid, args.Entity, hand); + return; } + + _examine.DoExamine(entity); + } + + /// + /// Called when a user clicks on the little "activation" icon in the hands GUI. This is currently only used + /// by storage (backpacks, etc). + /// + public void UIHandOpenContextMenu(string handName) + { + if (!TryGetPlayerHands(out var hands) || + !hands.Hands.TryGetValue(handName, out var hand) || + hand.HeldEntity is not { Valid: true } entity) + { + return; + } + + _verbs.VerbMenu.OpenVerbMenu(entity); + } + + #region visuals + + private void HandleItemAdded(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args) + { + if (!handComp.Hands.TryGetValue(args.Container.ID, out var hand)) + return; + UpdateHandVisuals(uid, args.Entity, hand); + + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + OnPlayerItemAdded?.Invoke(hand.Name, args.Entity); + + if (HasComp(args.Entity)) + OnPlayerHandBlocked?.Invoke(hand.Name); + } + + private void HandleItemRemoved(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args) + { + if (!handComp.Hands.TryGetValue(args.Container.ID, out var hand)) + return; + UpdateHandVisuals(uid, args.Entity, hand); + + if (uid != _playerManager.LocalPlayer?.ControlledEntity) + return; + + OnPlayerItemRemoved?.Invoke(hand.Name, args.Entity); + + if (HasComp(args.Entity)) + OnPlayerHandUnblocked?.Invoke(hand.Name); } /// @@ -192,9 +269,6 @@ namespace Content.Client.Hands.Systems if (!Resolve(uid, ref handComp, ref sprite, false)) return; - if (uid == _playerManager.LocalPlayer?.ControlledEntity) - UpdateGui(); - if (!handComp.ShowInHands) return; @@ -206,6 +280,7 @@ namespace Content.Client.Hands.Systems { sprite.RemoveLayer(key); } + revealedLayers.Clear(); } else @@ -267,52 +342,76 @@ namespace Content.Client.Hands.Systems #endregion #region Gui - public void UpdateGui(HandsComponent? hands = null) - { - if (hands == null && !TryGetPlayerHands(out hands) || hands.Gui == null) - return; - - var states = hands.Hands.Values - .Select(hand => new GuiHand(hand.Name, hand.Location, hand.HeldEntity)) - .ToArray(); - - hands.Gui.Update(new HandsGuiState(states, hands.ActiveHand?.Name)); - } - - public override bool TrySetActiveHand(EntityUid uid, string? value, SharedHandsComponent? handComp = null) - { - if (!base.TrySetActiveHand(uid, value, handComp)) - return false; - - if (uid == _playerManager.LocalPlayer?.ControlledEntity) - UpdateGui(); - - return true; - } private void HandlePlayerAttached(EntityUid uid, HandsComponent component, PlayerAttachedEvent args) { - component.Gui = new HandsGui(component, this); - _gameHud.HandsContainer.AddChild(component.Gui); - component.Gui.SetPositionFirst(); - UpdateGui(component); + if (_playerManager.LocalPlayer?.ControlledEntity == uid) + OnPlayerHandsAdded?.Invoke(component); } - private static void HandlePlayerDetached(EntityUid uid, HandsComponent component, PlayerDetachedEvent args) + private void HandlePlayerDetached(EntityUid uid, HandsComponent component, PlayerDetachedEvent args) { - ClearGui(component); + OnPlayerHandsRemoved?.Invoke(); } - private static void HandleCompRemove(EntityUid uid, HandsComponent component, ComponentRemove args) + private void HandleCompAdd(EntityUid uid, HandsComponent component, ComponentAdd args) { - ClearGui(component); + if (_playerManager.LocalPlayer?.ControlledEntity == uid) + OnPlayerHandsAdded?.Invoke(component); } - private static void ClearGui(HandsComponent comp) + private void HandleCompRemove(EntityUid uid, HandsComponent component, ComponentRemove args) { - comp.Gui?.Orphan(); - comp.Gui = null; + if (_playerManager.LocalPlayer?.ControlledEntity == uid) + OnPlayerHandsRemoved?.Invoke(); } #endregion + + private void AddHand(EntityUid uid, Hand newHand, SharedHandsComponent? handsComp = null) + { + AddHand(uid, newHand.Name, newHand.Location, handsComp); + } + + public override void AddHand(EntityUid uid, string handName, HandLocation handLocation, SharedHandsComponent? handsComp = null) + { + base.AddHand(uid, handName, handLocation, handsComp); + + if (uid == _playerManager.LocalPlayer?.ControlledEntity) + OnPlayerAddHand?.Invoke(handName, handLocation); + + if (handsComp == null) + return; + + if (handsComp.ActiveHand == null) + SetActiveHand(uid, handsComp.Hands[handName], handsComp); + } + public override void RemoveHand(EntityUid uid, string handName, SharedHandsComponent? handsComp = null) + { + if (uid == _playerManager.LocalPlayer?.ControlledEntity && handsComp != null && + handsComp.Hands.ContainsKey(handName) && uid == + _playerManager.LocalPlayer?.ControlledEntity) + { + OnPlayerRemoveHand?.Invoke(handName); + } + + base.RemoveHand(uid, handName, handsComp); + } + + private void OnHandActivated(SharedHandsComponent? handsComponent) + { + if (handsComponent == null) + return; + + if (_playerManager.LocalPlayer?.ControlledEntity != handsComponent.Owner) + return; + + if (handsComponent.ActiveHand == null) + { + OnPlayerSetActiveHand?.Invoke(null); + return; + } + + OnPlayerSetActiveHand?.Invoke(handsComponent.ActiveHand.Name); + } } } diff --git a/Content.Client/Hands/UI/HandsGui.xaml b/Content.Client/Hands/UI/HandsGui.xaml deleted file mode 100644 index d39c044821..0000000000 --- a/Content.Client/Hands/UI/HandsGui.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Content.Client/Hands/UI/HandsGui.xaml.cs b/Content.Client/Hands/UI/HandsGui.xaml.cs deleted file mode 100644 index 515c521024..0000000000 --- a/Content.Client/Hands/UI/HandsGui.xaml.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Client.Hands.Systems; -using Content.Client.HUD; -using Content.Client.Inventory; -using Content.Client.Items.Managers; -using Content.Client.Items.UI; -using Content.Client.Resources; -using Content.Shared.CCVar; -using Content.Shared.Hands.Components; -using Robust.Client.AutoGenerated; -using Robust.Client.Graphics; -using Robust.Client.ResourceManagement; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.XAML; -using Robust.Shared.Configuration; -using Robust.Shared.Input; -using Robust.Shared.Timing; - -namespace Content.Client.Hands.UI -{ - [GenerateTypedNameReferences] - public sealed partial class HandsGui : Control - { - [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; - [Dependency] private readonly IGameHud _gameHud = default!; - [Dependency] private readonly INetConfigurationManager _configManager = default!; - - private readonly HandsSystem _handsSystem; - private readonly HandsComponent _handsComponent; - - private string StorageTexture => "back.png"; - private Texture BlockedTexture => _resourceCache.GetTexture("/Textures/Interface/Default/blocked.png"); - - private ItemStatusPanel StatusPanel { get; } - - [ViewVariables] private GuiHand[] _hands = Array.Empty(); - - private string? ActiveHand { get; set; } - - public HandsGui(HandsComponent hands, HandsSystem handsSystem) - { - _handsComponent = hands; - _handsSystem = handsSystem; - - RobustXamlLoader.Load(this); - IoCManager.InjectDependencies(this); - - StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle); - StatusContainer.AddChild(StatusPanel); - StatusPanel.SetPositionFirst(); - } - - protected override void EnteredTree() - { - base.EnteredTree(); - - _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); - } - - protected override void ExitedTree() - { - base.ExitedTree(); - - _configManager.UnsubValueChanged(CCVars.HudTheme, UpdateHudTheme); - } - - public void Update(HandsGuiState state) - { - ActiveHand = state.ActiveHand; - _hands = state.GuiHands; - Array.Sort(_hands, HandOrderComparer.Instance); - UpdateGui(); - } - - private void UpdateGui() - { - HandsContainer.DisposeAllChildren(); - var entManager = IoCManager.Resolve(); - foreach (var hand in _hands) - { - var newButton = MakeHandButton(hand.HandLocation); - HandsContainer.AddChild(newButton); - hand.HandButton = newButton; - - var handName = hand.Name; - newButton.OnPressed += args => OnHandPressed(args, handName); - newButton.OnStoragePressed += _ => OnStoragePressed(handName); - - _itemSlotManager.SetItemSlot(newButton, hand.HeldItem); - - // Show blocked overlay if hand is blocked. - newButton.Blocked.Visible = - hand.HeldItem != null && entManager.HasComponent(hand.HeldItem.Value); - } - - if (TryGetActiveHand(out var activeHand)) - { - activeHand.HandButton.SetActiveHand(true); - StatusPanel.Update(activeHand.HeldItem); - } - } - - private void OnHandPressed(GUIBoundKeyEventArgs args, string handName) - { - if (args.Function == EngineKeyFunctions.UIClick) - { - _handsSystem.UIHandClick(_handsComponent, handName); - } - else if (TryGetHand(handName, out var hand)) - { - _itemSlotManager.OnButtonPressed(args, hand.HeldItem); - } - } - - private void OnStoragePressed(string handName) - { - _handsSystem.UIHandActivate(handName); - } - - private bool TryGetActiveHand([NotNullWhen(true)] out GuiHand? activeHand) - { - TryGetHand(ActiveHand, out activeHand); - return activeHand != null; - } - - private bool TryGetHand(string? handName, [NotNullWhen(true)] out GuiHand? foundHand) - { - foundHand = null; - - if (handName == null) - return false; - - foreach (var hand in _hands) - { - if (hand.Name == handName) - foundHand = hand; - } - - return foundHand != null; - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - - foreach (var hand in _hands) - { - _itemSlotManager.UpdateCooldown(hand.HandButton, hand.HeldItem); - } - } - - private HandButton MakeHandButton(HandLocation buttonLocation) - { - var buttonTextureName = buttonLocation switch - { - HandLocation.Right => "hand_r.png", - _ => "hand_l.png" - }; - - return new HandButton(ClientInventorySystem.ButtonSize, buttonTextureName, StorageTexture, _gameHud, BlockedTexture, buttonLocation); - } - - private void UpdateHudTheme(int idx) - { - UpdateGui(); - } - - private sealed class HandOrderComparer : IComparer - { - public static readonly HandOrderComparer Instance = new(); - - public int Compare(GuiHand? x, GuiHand? y) - { - if (ReferenceEquals(x, y)) return 0; - if (ReferenceEquals(null, y)) return 1; - if (ReferenceEquals(null, x)) return -1; - - var orderX = Map(x.HandLocation); - var orderY = Map(y.HandLocation); - - return orderX.CompareTo(orderY); - - static int Map(HandLocation loc) - { - return loc switch - { - HandLocation.Left => 3, - HandLocation.Middle => 2, - HandLocation.Right => 1, - _ => throw new ArgumentOutOfRangeException(nameof(loc), loc, null) - }; - } - } - } - } - - /// - /// Info on a set of hands to be displayed. - /// - public sealed class HandsGuiState - { - /// - /// The set of hands to be displayed. - /// - [ViewVariables] - public GuiHand[] GuiHands { get; } - - /// - /// The name of the currently active hand. - /// - [ViewVariables] - public string? ActiveHand { get; } - - public HandsGuiState(GuiHand[] guiHands, string? activeHand = null) - { - GuiHands = guiHands; - ActiveHand = activeHand; - } - } - - /// - /// Info on an individual hand to be displayed. - /// - public sealed class GuiHand - { - /// - /// The name of this hand. - /// - [ViewVariables] - public string Name { get; } - - /// - /// Where this hand is located. - /// - [ViewVariables] - public HandLocation HandLocation { get; } - - /// - /// The item being held in this hand. - /// - [ViewVariables] - public EntityUid? HeldItem { get; } - - /// - /// The button in the gui associated with this hand. Assumed to be set by gui shortly after being received from the client HandsComponent. - /// - [ViewVariables] - public HandButton HandButton { get; set; } = default!; - - public GuiHand(string name, HandLocation handLocation, EntityUid? heldItem) - { - Name = name; - HandLocation = handLocation; - HeldItem = heldItem; - } - } -} diff --git a/Content.Client/Info/LinkBanner.cs b/Content.Client/Info/LinkBanner.cs index b79293bd37..6e87dc5c40 100644 --- a/Content.Client/Info/LinkBanner.cs +++ b/Content.Client/Info/LinkBanner.cs @@ -1,6 +1,7 @@ using Content.Client.Changelog; using Content.Client.Credits; using Content.Client.Links; +using Content.Client.UserInterface.Systems.EscapeMenu; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.IoC; @@ -33,6 +34,7 @@ namespace Content.Client.Info var wikiButton = new Button {Text = Loc.GetString("server-info-wiki-button")}; wikiButton.OnPressed += args => uriOpener.OpenUri(UILinks.Wiki); var changelogButton = new ChangelogButton(); + changelogButton.OnPressed += args => UserInterfaceManager.GetUIController().ToggleWindow(); buttons.AddChild(changelogButton); buttons.AddChild(rulesButton); buttons.AddChild(discordButton); diff --git a/Content.Client/Info/RulesAndInfoWindow.cs b/Content.Client/Info/RulesAndInfoWindow.cs index fde0394974..b93db9300e 100644 --- a/Content.Client/Info/RulesAndInfoWindow.cs +++ b/Content.Client/Info/RulesAndInfoWindow.cs @@ -1,4 +1,3 @@ -using Content.Client.Options.UI; using Content.Client.UserInterface.Systems.EscapeMenu; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; @@ -20,7 +19,6 @@ namespace Content.Client.Info { IoCManager.InjectDependencies(this); - Title = Loc.GetString("ui-info-title"); var rootContainer = new TabContainer(); @@ -50,12 +48,7 @@ namespace Content.Client.Info AddSection(tutorialList, Loc.GetString("ui-info-header-gameplay"), "Gameplay.txt", true); AddSection(tutorialList, Loc.GetString("ui-info-header-sandbox"), "Sandbox.txt", true); - infoControlSection.ControlsButton.OnPressed += OnOptionsPressed; - } - - private void OnOptionsPressed(BaseButton.ButtonEventArgs obj) - { - IoCManager.Resolve().GetUIController().ToggleWindow(); + infoControlSection.ControlsButton.OnPressed += _ => UserInterfaceManager.GetUIController().OpenWindow(); } private static void AddSection(Info info, Control control) diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index abee8f9177..420d6ea5e4 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -23,7 +23,7 @@ namespace Content.Client.Input common.AddFunction(ContentKeyFunctions.CycleChatChannelForward); common.AddFunction(ContentKeyFunctions.CycleChatChannelBackward); common.AddFunction(ContentKeyFunctions.ExamineEntity); - common.AddFunction(ContentKeyFunctions.OpenInfo); + common.AddFunction(ContentKeyFunctions.OpenAHelp); common.AddFunction(ContentKeyFunctions.TakeScreenshot); common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI); common.AddFunction(ContentKeyFunctions.Point); @@ -63,25 +63,16 @@ namespace Content.Client.Input // actions should be common (for ghosts, mobs, etc) common.AddFunction(ContentKeyFunctions.OpenActionsMenu); - common.AddFunction(ContentKeyFunctions.Hotbar0); - common.AddFunction(ContentKeyFunctions.Hotbar1); - common.AddFunction(ContentKeyFunctions.Hotbar2); - common.AddFunction(ContentKeyFunctions.Hotbar3); - common.AddFunction(ContentKeyFunctions.Hotbar4); - common.AddFunction(ContentKeyFunctions.Hotbar5); - common.AddFunction(ContentKeyFunctions.Hotbar6); - common.AddFunction(ContentKeyFunctions.Hotbar7); - common.AddFunction(ContentKeyFunctions.Hotbar8); - common.AddFunction(ContentKeyFunctions.Hotbar9); - common.AddFunction(ContentKeyFunctions.Loadout1); - common.AddFunction(ContentKeyFunctions.Loadout2); - common.AddFunction(ContentKeyFunctions.Loadout3); - common.AddFunction(ContentKeyFunctions.Loadout4); - common.AddFunction(ContentKeyFunctions.Loadout5); - common.AddFunction(ContentKeyFunctions.Loadout6); - common.AddFunction(ContentKeyFunctions.Loadout7); - common.AddFunction(ContentKeyFunctions.Loadout8); - common.AddFunction(ContentKeyFunctions.Loadout9); + + foreach (var boundKey in ContentKeyFunctions.GetHotbarBoundKeys()) + { + common.AddFunction(boundKey); + } + + foreach (var boundKey in ContentKeyFunctions.GetLoadoutBoundKeys()) + { + common.AddFunction(boundKey); + } var aghost = contexts.New("aghost", "common"); aghost.AddFunction(EngineKeyFunctions.MoveUp); diff --git a/Content.Client/Inventory/ClientInventoryComponent.cs b/Content.Client/Inventory/ClientInventoryComponent.cs index ef10eaf468..d0e514d056 100644 --- a/Content.Client/Inventory/ClientInventoryComponent.cs +++ b/Content.Client/Inventory/ClientInventoryComponent.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Content.Client.Items.UI; +using Content.Client.UserInterface.Controls; using Content.Shared.Inventory; using Robust.Client.UserInterface; using Robust.Client.UserInterface.CustomControls; @@ -19,24 +19,15 @@ namespace Content.Client.Inventory [Access(typeof(ClientInventorySystem))] public sealed class ClientInventoryComponent : InventoryComponent { - public Control BottomLeftButtons = default!; - public Control BottomRightButtons = default!; - public Control TopQuickButtons = default!; - - public DefaultWindow InventoryWindow = default!; - - public readonly Dictionary> SlotButtons = new(); - [ViewVariables] [DataField("speciesId")] public string? SpeciesId { get; set; } - + [ViewVariables] + public readonly Dictionary SlotData = new (); /// /// Data about the current layers that have been added to the players sprite due to the items in each equipment slot. /// [ViewVariables] [Access(typeof(ClientInventorySystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends public readonly Dictionary> VisualLayerKeys = new(); - - public bool AttachedToGameHud; } } diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index 1207556b54..5e28dba983 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -1,76 +1,78 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Content.Client.Clothing; -using Content.Client.HUD; -using Content.Shared.Input; -using Content.Client.Items.Managers; -using Content.Client.Items.UI; -using Content.Shared.CCVar; +using Content.Client.Examine; +using Content.Client.Storage; +using Content.Client.UserInterface.Controls; +using Content.Client.Verbs; using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; -using Content.Shared.Item; using JetBrains.Annotations; using Robust.Client.GameObjects; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Shared.Configuration; +using Robust.Client.Player; using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; -using Content.Shared.Interaction.Events; namespace Content.Client.Inventory { [UsedImplicitly] public sealed class ClientInventorySystem : InventorySystem { - [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IConfigurationManager _config = default!; - [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly ClothingVisualsSystem _clothingVisualsSystem = default!; + [Dependency] private readonly ExamineSystem _examine = default!; + [Dependency] private readonly VerbSystem _verbs = default!; - public const int ButtonSize = 64; - private const int ButtonSeparation = 4; - private const int RightSeparation = 2; + public Action? EntitySlotUpdate = null; + public Action? OnSlotAdded = null; + public Action? OnSlotRemoved = null; + public Action? OnLinkInventory = null; + public Action? OnUnlinkInventory = null; + public Action? OnSpriteUpdate = null; - /// - /// Stores delegates used to create controls for a given . - /// - private readonly - Dictionary>, (DefaultWindow window, Control bottomLeft, Control bottomRight, Control - topQuick)>> - _uiGenerateDelegates = new(); + private readonly Queue<(ClientInventoryComponent comp, EntityEventArgs args)> _equipEventsQueue = new(); public override void Initialize() { base.Initialize(); - CommandBinds.Builder - .Bind(ContentKeyFunctions.OpenInventoryMenu, - InputCmdHandler.FromDelegate(_ => HandleOpenInventoryMenu())) - .Register(); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnDidEquip); - SubscribeLocalEvent(OnDidUnequip); + SubscribeLocalEvent((_, comp, args) => + _equipEventsQueue.Enqueue((comp, args))); + SubscribeLocalEvent((_, comp, args) => + _equipEventsQueue.Enqueue((comp, args))); SubscribeLocalEvent(OnUseInHand); + } - _config.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); + public override void Update(float frameTime) + { + base.Update(frameTime); + + while (_equipEventsQueue.TryDequeue(out var tuple)) + { + var (component, args) = tuple; + + switch (args) + { + case DidEquipEvent equipped: + OnDidEquip(component, equipped); + break; + case DidUnequipEvent unequipped: + OnDidUnequip(component, unequipped); + break; + default: + throw new InvalidOperationException($"Received queued event of unknown type: {args.GetType()}"); + } + } } private void OnUseInHand(EntityUid uid, ClothingComponent component, UseInHandEvent args) @@ -81,85 +83,60 @@ namespace Content.Client.Inventory QuickEquip(uid, component, args); } - private void OnDidUnequip(EntityUid uid, ClientInventoryComponent component, DidUnequipEvent args) + private void OnDidUnequip(ClientInventoryComponent component, DidUnequipEvent args) { - UpdateComponentUISlot(uid, args.Slot, null, component); - } - - private void OnDidEquip(EntityUid uid, ClientInventoryComponent component, DidEquipEvent args) - { - UpdateComponentUISlot(uid, args.Slot, args.Equipment, component); - } - - private void UpdateComponentUISlot(EntityUid uid, string slot, EntityUid? item, ClientInventoryComponent? component = null) - { - if (!Resolve(uid, ref component)) + UpdateSlot(args.Equipee, component, args.Slot); + if (args.Equipee != _playerManager.LocalPlayer?.ControlledEntity) return; + var update = new SlotSpriteUpdate(args.SlotGroup, args.Slot, null, false); + OnSpriteUpdate?.Invoke(update); + } - if (!component.SlotButtons.TryGetValue(slot, out var buttons)) + private void OnDidEquip(ClientInventoryComponent component, DidEquipEvent args) + { + UpdateSlot(args.Equipee, component, args.Slot); + if (args.Equipee != _playerManager.LocalPlayer?.ControlledEntity) return; - - UpdateUISlot(buttons, item); - } - - private void UpdateUISlot(List buttons, EntityUid? entity) - { - foreach (var button in buttons) - { - _itemSlotManager.SetItemSlot(button, entity); - } - } - - private void OnPlayerDetached(EntityUid uid, ClientInventoryComponent component, PlayerDetachedEvent? args = null) - { - if(!component.AttachedToGameHud) return; - - _gameHud.InventoryButtonVisible = false; - _gameHud.BottomLeftInventoryQuickButtonContainer.RemoveChild(component.BottomLeftButtons); - _gameHud.BottomRightInventoryQuickButtonContainer.RemoveChild(component.BottomRightButtons); - _gameHud.TopInventoryQuickButtonContainer.RemoveChild(component.TopQuickButtons); - component.AttachedToGameHud = false; + var sprite = EntityManager.GetComponentOrNull(args.Equipment); + var update = new SlotSpriteUpdate(args.SlotGroup, args.Slot, sprite, + HasComp(args.Equipment)); + OnSpriteUpdate?.Invoke(update); } private void OnShutdown(EntityUid uid, ClientInventoryComponent component, ComponentShutdown args) { - OnPlayerDetached(uid, component); + if (component.Owner != _playerManager.LocalPlayer?.ControlledEntity) + return; + + OnUnlinkInventory?.Invoke(); } private void OnPlayerAttached(EntityUid uid, ClientInventoryComponent component, PlayerAttachedEvent args) { - if(component.AttachedToGameHud) return; - - _gameHud.InventoryButtonVisible = true; - _gameHud.BottomLeftInventoryQuickButtonContainer.AddChild(component.BottomLeftButtons); - _gameHud.BottomRightInventoryQuickButtonContainer.AddChild(component.BottomRightButtons); - _gameHud.TopInventoryQuickButtonContainer.AddChild(component.TopQuickButtons); - component.AttachedToGameHud = true; - } - - private void UpdateHudTheme(int obj) - { - if (!_gameHud.ValidateHudTheme(obj)) + if (TryGetSlots(uid, out var definitions)) { - return; - } - - foreach (var inventoryComponent in EntityManager.EntityQuery(true)) - { - foreach (var slotButton in inventoryComponent.SlotButtons) + foreach (var definition in definitions) { - foreach (var btn in slotButton.Value) + if (!TryGetSlotContainer(uid, definition.Name, out var container, out _, component)) + continue; + + if (!component.SlotData.TryGetValue(definition.Name, out var data)) { - btn.RefreshTextures(_gameHud); + data = new SlotData(definition); + component.SlotData[definition.Name] = data; } + + data.Container = container; } } + + if (uid == _playerManager.LocalPlayer?.ControlledEntity) + OnLinkInventory?.Invoke(component); } public override void Shutdown() { CommandBinds.Unregister(); - _config.UnsubValueChanged(CCVars.HudTheme, UpdateHudTheme); base.Shutdown(); } @@ -167,28 +144,77 @@ namespace Content.Client.Inventory { _clothingVisualsSystem.InitClothing(uid, component); - if (!TryGetUIElements(uid, out var window, out var bottomLeft, out var bottomRight, out var topQuick, - component)) + if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate)) return; - if (TryComp(uid, out var containerManager)) + foreach (var slot in invTemplate.Slots) { - foreach (var (slot, buttons) in component.SlotButtons) - { - if (!TryGetSlotEntity(uid, slot, out var entity, component, containerManager)) - continue; - - UpdateUISlot(buttons, entity); - } + TryAddSlotDef(uid, component, slot); } - - component.InventoryWindow = window; - component.BottomLeftButtons = bottomLeft; - component.BottomRightButtons = bottomRight; - component.TopQuickButtons = topQuick; } - private void HoverInSlotButton(EntityUid uid, string slot, ItemSlotButton button, InventoryComponent? inventoryComponent = null, SharedHandsComponent? hands = null) + public void SetSlotHighlight(EntityUid owner, ClientInventoryComponent component, string slotName, bool state) + { + var oldData = component.SlotData[slotName]; + var newData = component.SlotData[slotName] = new SlotData(oldData, state); + if (owner == _playerManager.LocalPlayer?.ControlledEntity) + EntitySlotUpdate?.Invoke(newData); + } + + public void UpdateSlot(EntityUid owner, ClientInventoryComponent component, string slotName, + bool? blocked = null, bool? highlight = null) + { + var oldData = component.SlotData[slotName]; + var newHighlight = oldData.Highlighted; + var newBlocked = oldData.Blocked; + + if (blocked != null) + newBlocked = blocked.Value; + + if (highlight != null) + newHighlight = highlight.Value; + + var newData = component.SlotData[slotName] = + new SlotData(component.SlotData[slotName], newHighlight, newBlocked); + if (owner == _playerManager.LocalPlayer?.ControlledEntity) + EntitySlotUpdate?.Invoke(newData); + } + + public bool TryAddSlotDef(EntityUid owner, ClientInventoryComponent component, SlotDefinition newSlotDef) + { + SlotData newSlotData = newSlotDef; //convert to slotData + if (!component.SlotData.TryAdd(newSlotDef.Name, newSlotData)) + return false; + + + if (owner == _playerManager.LocalPlayer?.ControlledEntity) + OnSlotAdded?.Invoke(newSlotData); + return true; + } + + public void RemoveSlotDef(EntityUid owner, ClientInventoryComponent component, SlotData slotData) + { + if (component.SlotData.Remove(slotData.SlotName)) + { + if (owner == _playerManager.LocalPlayer?.ControlledEntity) + OnSlotRemoved?.Invoke(slotData); + } + } + + public void RemoveSlotDef(EntityUid owner, ClientInventoryComponent component, string slotName) + { + if (!component.SlotData.TryGetValue(slotName, out var slotData)) + return; + + component.SlotData.Remove(slotName); + + if (owner == _playerManager.LocalPlayer?.ControlledEntity) + OnSlotRemoved?.Invoke(slotData); + } + + // TODO hud refactor This should also live in a UI Controller + private void HoverInSlotButton(EntityUid uid, string slot, SlotControl control, + InventoryComponent? inventoryComponent = null, SharedHandsComponent? hands = null) { if (!Resolve(uid, ref inventoryComponent)) return; @@ -199,160 +225,101 @@ namespace Content.Client.Inventory if (hands.ActiveHandEntity is not EntityUid heldEntity) return; - if(!TryGetSlotContainer(uid, slot, out var containerSlot, out var slotDef, inventoryComponent)) + if (!TryGetSlotContainer(uid, slot, out var containerSlot, out var slotDef, inventoryComponent)) return; - - _itemSlotManager.HoverInSlot(button, heldEntity, - CanEquip(uid, heldEntity, slot, out _, slotDef, inventoryComponent) && - containerSlot.CanInsert(heldEntity, EntityManager)); } - private void HandleSlotButtonPressed(EntityUid uid, string slot, ItemSlotButton button, - GUIBoundKeyEventArgs args) + public void UIInventoryActivate(string slot) { - if (TryGetSlotEntity(uid, slot, out var itemUid) && _itemSlotManager.OnButtonPressed(args, itemUid.Value)) - return; - - if (args.Function != EngineKeyFunctions.UIClick) - return; - - // only raise event if either itemUid is not null, or the user is holding something - if (itemUid != null || TryComp(uid, out SharedHandsComponent? hands) && hands.ActiveHandEntity != null) - EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot)); + EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot)); } - private bool TryGetUIElements(EntityUid uid, [NotNullWhen(true)] out DefaultWindow? invWindow, - [NotNullWhen(true)] out Control? invBottomLeft, [NotNullWhen(true)] out Control? invBottomRight, - [NotNullWhen(true)] out Control? invTopQuick, ClientInventoryComponent? component = null) + public void UIInventoryStorageActivate(string slot) { - invWindow = null; - invBottomLeft = null; - invBottomRight = null; - invTopQuick = null; + EntityManager.RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot)); + } - if (!Resolve(uid, ref component)) - return false; + public void UIInventoryExamine(string slot, EntityUid uid) + { + if (!TryGetSlotEntity(uid, slot, out var item)) + return; - if(!_prototypeManager.TryIndex(component.TemplateId, out var template)) - return false; + _examine.DoExamine(item.Value); + } - if (!_uiGenerateDelegates.TryGetValue(component.TemplateId, out var genfunc)) + public void UIInventoryOpenContextMenu(string slot, EntityUid uid) + { + if (!TryGetSlotEntity(uid, slot, out var item)) + return; + + _verbs.VerbMenu.OpenVerbMenu(item.Value); + } + + public void UIInventoryActivateItem(string slot, EntityUid uid) + { + if (!TryGetSlotEntity(uid, slot, out var item)) + return; + + EntityManager.EntityNetManager?.SendSystemNetworkMessage( + new InteractInventorySlotEvent(item.Value, altInteract: false)); + } + + public void UIInventoryAltActivateItem(string slot, EntityUid uid) + { + if (!TryGetSlotEntity(uid, slot, out var item)) + return; + + EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(item.Value, altInteract: true)); + } + + public sealed class SlotData + { + public readonly SlotDefinition SlotDef; + public EntityUid? HeldEntity => Container?.ContainedEntity; + public bool Blocked; + public bool Highlighted; + public ContainerSlot? Container; + public bool HasSlotGroup => SlotDef.SlotGroup != "Default"; + public Vector2i ButtonOffset => SlotDef.UIWindowPosition; + public string SlotName => SlotDef.Name; + public bool ShowInWindow => SlotDef.ShowInWindow; + public string SlotGroup => SlotDef.SlotGroup; + public string SlotDisplayName => SlotDef.DisplayName; + public string TextureName => "Slots/" + SlotDef.TextureName; + + public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false, + bool blocked = false) { - _uiGenerateDelegates[component.TemplateId] = genfunc = (entityUid, list) => - { - var window = new DefaultWindow() - { - Title = Loc.GetString("human-inventory-window-title"), - Resizable = false - }; - window.OnClose += () => - { - _gameHud.InventoryButtonDown = false; - _gameHud.TopInventoryQuickButtonContainer.Visible = false; - }; - var windowContents = new LayoutContainer - { - MinSize = (ButtonSize * 4 + ButtonSeparation * 3 + RightSeparation, - ButtonSize * 4 + ButtonSeparation * 3) - }; - window.Contents.AddChild(windowContents); - - ItemSlotButton GetButton(SlotDefinition definition, string textureBack) - { - var btn = new ItemSlotButton(ButtonSize, $"{definition.TextureName}.png", textureBack, - _gameHud) - { - OnStoragePressed = (e) => - { - if (e.Function != EngineKeyFunctions.UIClick && - e.Function != ContentKeyFunctions.ActivateItemInWorld) - return; - RaiseNetworkEvent(new OpenSlotStorageNetworkMessage(definition.Name)); - } - }; - btn.OnHover = (_) => - { - HoverInSlotButton(entityUid, definition.Name, btn); - }; - btn.OnPressed = (e) => - { - HandleSlotButtonPressed(entityUid, definition.Name, btn, e); - }; - return btn; - } - - void AddButton(SlotDefinition definition, Vector2i position) - { - var button = GetButton(definition, "back.png"); - LayoutContainer.SetPosition(button, position); - windowContents.AddChild(button); - if (!list.ContainsKey(definition.Name)) - list[definition.Name] = new(); - list[definition.Name].Add(button); - } - - void AddHUDButton(BoxContainer container, SlotDefinition definition) - { - var button = GetButton(definition, "back.png"); - container.AddChild(button); - if (!list.ContainsKey(definition.Name)) - list[definition.Name] = new(); - list[definition.Name].Add(button); - } - - var topQuick = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - SeparationOverride = 5 - }; - var bottomRight = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - SeparationOverride = 5 - }; - var bottomLeft = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - SeparationOverride = 5 - }; - - const int sizep = (ButtonSize + ButtonSeparation); - - foreach (var slotDefinition in template.Slots) - { - switch (slotDefinition.UIContainer) - { - case SlotUIContainer.BottomLeft: - AddHUDButton(bottomLeft, slotDefinition); - break; - case SlotUIContainer.BottomRight: - AddHUDButton(bottomRight, slotDefinition); - break; - case SlotUIContainer.Top: - AddHUDButton(topQuick, slotDefinition); - break; - } - - AddButton(slotDefinition, slotDefinition.UIWindowPosition * sizep); - } - - return (window, bottomLeft, bottomRight, topQuick); - }; + SlotDef = slotDef; + Highlighted = highlighted; + Blocked = blocked; + Container = container; } - var res = genfunc(uid, component.SlotButtons); - invWindow = res.window; - invBottomLeft = res.bottomLeft; - invBottomRight = res.bottomRight; - invTopQuick = res.topQuick; - return true; + public SlotData(SlotData oldData, bool highlighted = false, bool blocked = false) + { + SlotDef = oldData.SlotDef; + Highlighted = highlighted; + Container = oldData.Container; + Blocked = blocked; + } + + public static implicit operator SlotData(SlotDefinition s) + { + return new SlotData(s); + } + + public static implicit operator SlotDefinition(SlotData s) + { + return s.SlotDef; + } } - - private void HandleOpenInventoryMenu() - { - _gameHud.InventoryButtonDown = !_gameHud.InventoryButtonDown; - _gameHud.TopInventoryQuickButtonContainer.Visible = _gameHud.InventoryButtonDown; - } + public readonly record struct SlotSpriteUpdate( + string Group, + string Name, + ISpriteComponent? Sprite, + bool ShowStorage + ); } } diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index cde59b9268..54e40f2995 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -5,9 +5,7 @@ using Content.Client.Clickable; using Content.Client.Options; using Content.Client.Eui; using Content.Client.GhostKick; -using Content.Client.HUD; using Content.Client.Info; -using Content.Client.Items.Managers; using Content.Client.Launcher; using Content.Client.Module; using Content.Client.Parallax.Managers; @@ -27,12 +25,10 @@ namespace Content.Client.IoC { public static void Register() { - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Client/Items/Managers/IItemSlotManager.cs b/Content.Client/Items/Managers/IItemSlotManager.cs deleted file mode 100644 index 43ea8b4eb8..0000000000 --- a/Content.Client/Items/Managers/IItemSlotManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Content.Client.Items.UI; -using Robust.Client.UserInterface; -using Robust.Shared.GameObjects; - -namespace Content.Client.Items.Managers -{ - public interface IItemSlotManager - { - bool OnButtonPressed(GUIBoundKeyEventArgs args, EntityUid? item); - void UpdateCooldown(ItemSlotButton? cooldownTexture, EntityUid? entity); - bool SetItemSlot(ItemSlotButton button, EntityUid? entity); - void HoverInSlot(ItemSlotButton button, EntityUid? entity, bool fits); - event Action? EntityHighlightedUpdated; - bool IsHighlighted(EntityUid? uid); - - /// - /// Highlight all slot controls that contain the specified entity. - /// - /// The UID of the entity to highlight. - /// - void HighlightEntity(EntityUid uid); - - /// - /// Remove highlighting for the specified entity. - /// - /// The UID of the entity to unhighlight. - /// - void UnHighlightEntity(EntityUid uid); - } -} diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs deleted file mode 100644 index 323fa228a8..0000000000 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Collections.Generic; -using Content.Client.Examine; -using Content.Client.Items.UI; -using Content.Client.Storage; -using Content.Client.Verbs; -using Content.Shared.Cooldown; -using Content.Shared.Hands.Components; -using Content.Shared.Input; -using Content.Shared.Interaction; -using Robust.Client.GameObjects; -using Robust.Client.UserInterface; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Timing; - -namespace Content.Client.Items.Managers -{ - public sealed class ItemSlotManager : IItemSlotManager - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - - private readonly HashSet _highlightEntities = new(); - - public event Action? EntityHighlightedUpdated; - - public bool SetItemSlot(ItemSlotButton button, EntityUid? entity) - { - if (entity == null) - { - button.SpriteView.Sprite = null; - button.StorageButton.Visible = false; - } - else - { - ISpriteComponent? sprite; - if (_entityManager.TryGetComponent(entity, out HandVirtualItemComponent? virtPull) - && _entityManager.TryGetComponent(virtPull.BlockingEntity, out ISpriteComponent? pulledSprite)) - { - sprite = pulledSprite; - } - else if (!_entityManager.TryGetComponent(entity, out sprite)) - { - return false; - } - - button.ClearHover(); - button.SpriteView.Sprite = sprite; - button.StorageButton.Visible = _entityManager.HasComponent(entity); - } - - button.Entity = entity ?? default; - - // im lazy - button.UpdateSlotHighlighted(); - return true; - } - - public bool OnButtonPressed(GUIBoundKeyEventArgs args, EntityUid? item) - { - if (item == null) - return false; - - if (args.Function == ContentKeyFunctions.ExamineEntity) - { - _entitySystemManager.GetEntitySystem() - .DoExamine(item.Value); - } - else if (args.Function == EngineKeyFunctions.UseSecondary) - { - _entitySystemManager.GetEntitySystem().VerbMenu.OpenVerbMenu(item.Value); - } - else if (args.Function == ContentKeyFunctions.ActivateItemInWorld) - { - _entityManager.EntityNetManager?.SendSystemNetworkMessage(new InteractInventorySlotEvent(item.Value, altInteract: false)); - } - else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld) - { - _entityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(item.Value, altInteract: true)); - } - else - { - return false; - } - args.Handle(); - return true; - } - - public void UpdateCooldown(ItemSlotButton? button, EntityUid? entity) - { - var cooldownDisplay = button?.CooldownDisplay; - - if (cooldownDisplay == null) - { - return; - } - - if (entity == null || _entityManager.Deleted(entity) || - !_entityManager.TryGetComponent(entity, out ItemCooldownComponent? cooldown) || - !cooldown.CooldownStart.HasValue || - !cooldown.CooldownEnd.HasValue) - { - cooldownDisplay.Visible = false; - return; - } - - var start = cooldown.CooldownStart.Value; - var end = cooldown.CooldownEnd.Value; - - var length = (end - start).TotalSeconds; - var progress = (_gameTiming.CurTime - start).TotalSeconds / length; - var ratio = (progress <= 1 ? (1 - progress) : (_gameTiming.CurTime - end).TotalSeconds * -5); - - cooldownDisplay.Progress = MathHelper.Clamp((float) ratio, -1, 1); - cooldownDisplay.Visible = ratio > -1f; - } - - public void HoverInSlot(ItemSlotButton button, EntityUid? entity, bool fits) - { - if (entity == null || !button.MouseIsHovering) - { - button.ClearHover(); - return; - } - - if (!_entityManager.HasComponent(entity)) - { - return; - } - - // Set green / red overlay at 50% transparency - var hoverEntity = _entityManager.SpawnEntity("hoverentity", MapCoordinates.Nullspace); - var hoverSprite = _entityManager.GetComponent(hoverEntity); - hoverSprite.CopyFrom(_entityManager.GetComponent(entity.Value)); - hoverSprite.Color = fits ? new Color(0, 255, 0, 127) : new Color(255, 0, 0, 127); - - button.HoverSpriteView.Sprite = hoverSprite; - } - - public bool IsHighlighted(EntityUid? uid) - { - if (uid == null) return false; - return _highlightEntities.Contains(uid.Value); - } - - public void HighlightEntity(EntityUid uid) - { - if (!_highlightEntities.Add(uid)) - return; - - EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, true)); - } - - public void UnHighlightEntity(EntityUid uid) - { - if (!_highlightEntities.Remove(uid)) - return; - - EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, false)); - } - } - - public readonly struct EntitySlotHighlightedEventArgs - { - public EntitySlotHighlightedEventArgs(EntityUid entity, bool newHighlighted) - { - Entity = entity; - NewHighlighted = newHighlighted; - } - - public EntityUid Entity { get; } - public bool NewHighlighted { get; } - } -} diff --git a/Content.Client/Items/UI/ItemSlotButton.cs b/Content.Client/Items/UI/ItemSlotButton.cs deleted file mode 100644 index e7e41a4296..0000000000 --- a/Content.Client/Items/UI/ItemSlotButton.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using Content.Client.Cooldown; -using Content.Client.HUD; -using Content.Client.Items.Managers; -using Content.Client.Stylesheets; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.Utility; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Utility; - -namespace Content.Client.Items.UI -{ - [Virtual] - public class ItemSlotButton : Control, IEntityEventSubscriber - { - private const string HighlightShader = "SelectionOutlineInrange"; - - [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; - - public EntityUid? Entity { get; set; } - public TextureRect Button { get; } - public SpriteView SpriteView { get; } - public SpriteView HoverSpriteView { get; } - public TextureButton StorageButton { get; } - public CooldownGraphic CooldownDisplay { get; } - - public Action? OnPressed { get; set; } - public Action? OnStoragePressed { get; set; } - public Action? OnHover { get; set; } - - public bool EntityHover => HoverSpriteView.Sprite != null; - public bool MouseIsHovering; - - private readonly PanelContainer _highlightRect; - - private string _textureName; - private string _storageTextureName; - - public ItemSlotButton(int size, string textureName, string storageTextureName, IGameHud gameHud) - { - _textureName = textureName; - _storageTextureName = storageTextureName; - IoCManager.InjectDependencies(this); - - MinSize = (size, size); - - AddChild(Button = new TextureRect - { - TextureScale = (2, 2), - MouseFilter = MouseFilterMode.Stop - }); - - AddChild(_highlightRect = new PanelContainer - { - StyleClasses = { StyleNano.StyleClassHandSlotHighlight }, - MinSize = (32, 32), - Visible = false - }); - - Button.OnKeyBindDown += OnButtonPressed; - - AddChild(SpriteView = new SpriteView - { - Scale = (2, 2), - OverrideDirection = Direction.South - }); - - AddChild(HoverSpriteView = new SpriteView - { - Scale = (2, 2), - OverrideDirection = Direction.South - }); - - AddChild(StorageButton = new TextureButton - { - Scale = (0.75f, 0.75f), - HorizontalAlignment = HAlignment.Right, - VerticalAlignment = VAlignment.Bottom, - Visible = false, - }); - - StorageButton.OnKeyBindDown += args => - { - if (args.Function != EngineKeyFunctions.UIClick) - { - OnButtonPressed(args); - } - }; - - StorageButton.OnPressed += OnStorageButtonPressed; - - Button.OnMouseEntered += _ => - { - MouseIsHovering = true; - }; - Button.OnMouseEntered += OnButtonHover; - - Button.OnMouseExited += _ => - { - MouseIsHovering = false; - ClearHover(); - }; - - AddChild(CooldownDisplay = new CooldownGraphic - { - Visible = false, - }); - - RefreshTextures(gameHud); - } - - public void RefreshTextures(IGameHud gameHud) - { - Button.Texture = gameHud.GetHudTexture(_textureName); - StorageButton.TextureNormal = gameHud.GetHudTexture(_storageTextureName); - } - - protected override void EnteredTree() - { - base.EnteredTree(); - - _itemSlotManager.EntityHighlightedUpdated += HandleEntitySlotHighlighted; - UpdateSlotHighlighted(); - } - - protected override void ExitedTree() - { - base.ExitedTree(); - - _itemSlotManager.EntityHighlightedUpdated -= HandleEntitySlotHighlighted; - } - - private void HandleEntitySlotHighlighted(EntitySlotHighlightedEventArgs entitySlotHighlightedEventArgs) - { - UpdateSlotHighlighted(); - } - - public void UpdateSlotHighlighted() - { - Highlight(_itemSlotManager.IsHighlighted(Entity)); - } - - public void ClearHover() - { - if (EntityHover) - { - ISpriteComponent? tempQualifier = HoverSpriteView.Sprite; - if (tempQualifier != null) - { - IoCManager.Resolve().DeleteEntity(tempQualifier.Owner); - } - - HoverSpriteView.Sprite = null; - } - } - - public virtual void Highlight(bool highlight) - { - if (highlight) - { - _highlightRect.Visible = true; - } - else - { - _highlightRect.Visible = false; - } - } - - private void OnButtonPressed(GUIBoundKeyEventArgs args) - { - OnPressed?.Invoke(args); - } - - private void OnStorageButtonPressed(BaseButton.ButtonEventArgs args) - { - if (args.Event.Function == EngineKeyFunctions.UIClick) - { - OnStoragePressed?.Invoke(args.Event); - } - else - { - OnPressed?.Invoke(args.Event); - } - } - - private void OnButtonHover(GUIMouseHoverEventArgs args) - { - OnHover?.Invoke(args); - } - } -} diff --git a/Content.Client/Items/UI/ItemStatusPanel.cs b/Content.Client/Items/UI/ItemStatusPanel.cs deleted file mode 100644 index 31afcbd36d..0000000000 --- a/Content.Client/Items/UI/ItemStatusPanel.cs +++ /dev/null @@ -1,182 +0,0 @@ -using Content.Client.Resources; -using Content.Client.Stylesheets; -using Content.Shared.Hands.Components; -using Content.Shared.IdentityManagement; -using Robust.Client.Graphics; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Timing; -using Robust.Shared.Utility; -using static Content.Client.IoC.StaticIoC; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.Items.UI -{ - public sealed class ItemStatusPanel : Control - { - [Dependency] private readonly IEntityManager _entityManager = default!; - - [ViewVariables] - private readonly Label _itemNameLabel; - [ViewVariables] - private readonly BoxContainer _statusContents; - [ViewVariables] - private readonly PanelContainer _panel; - - [ViewVariables] - private EntityUid? _entity; - - public ItemStatusPanel(Texture texture, StyleBox.Margin cutout, StyleBox.Margin flat, Label.AlignMode textAlign) - { - IoCManager.InjectDependencies(this); - - var panel = new StyleBoxTexture - { - Texture = texture - }; - panel.SetContentMarginOverride(StyleBox.Margin.Vertical, 4); - panel.SetContentMarginOverride(StyleBox.Margin.Horizontal, 6); - panel.SetPatchMargin(flat, 2); - panel.SetPatchMargin(cutout, 13); - - AddChild(_panel = new PanelContainer - { - PanelOverride = panel, - ModulateSelfOverride = Color.White.WithAlpha(0.9f), - Children = - { - new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - SeparationOverride = 0, - Children = - { - (_statusContents = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }), - (_itemNameLabel = new Label - { - ClipText = true, - StyleClasses = {StyleNano.StyleClassItemStatus}, - Align = textAlign - }) - } - } - } - }); - VerticalAlignment = VAlignment.Bottom; - - // TODO: Depending on if its a two-hand panel or not - MinSize = (150, 0); - } - - /// - /// Creates a new instance of - /// based on whether or not it is being created for the right - /// or left hand. - /// - /// - /// The location of the hand that this panel is for - /// - /// the new instance - public static ItemStatusPanel FromSide(HandLocation location) - { - string texture; - StyleBox.Margin cutOut; - StyleBox.Margin flat; - Label.AlignMode textAlign; - - switch (location) - { - case HandLocation.Left: - texture = "/Textures/Interface/Nano/item_status_right.svg.96dpi.png"; - cutOut = StyleBox.Margin.Left | StyleBox.Margin.Top; - flat = StyleBox.Margin.Right | StyleBox.Margin.Bottom; - textAlign = Label.AlignMode.Right; - break; - case HandLocation.Middle: - texture = "/Textures/Interface/Nano/item_status_middle.svg.96dpi.png"; - cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top; - flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom; - textAlign = Label.AlignMode.Left; - break; - case HandLocation.Right: - texture = "/Textures/Interface/Nano/item_status_left.svg.96dpi.png"; - cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top; - flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom; - textAlign = Label.AlignMode.Left; - break; - default: - throw new ArgumentOutOfRangeException(nameof(location), location, null); - } - - return new ItemStatusPanel(ResC.GetTexture(texture), cutOut, flat, textAlign); - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - - UpdateItemName(); - } - - public void Update(EntityUid? entity) - { - if (entity == null) - { - ClearOldStatus(); - _entity = null; - _panel.Visible = false; - return; - } - - if (entity != _entity) - { - _entity = entity.Value; - BuildNewEntityStatus(); - - UpdateItemName(); - } - - _panel.Visible = true; - } - - private void UpdateItemName() - { - if (_entity == null) - return; - - if (_entityManager.TryGetComponent(_entity, out HandVirtualItemComponent? virtualItem) - && _entityManager.EntityExists(virtualItem.BlockingEntity)) - { - // Uses identity because we can be blocked by pulling someone - _itemNameLabel.Text = Identity.Name(virtualItem.BlockingEntity, _entityManager); - } - else - { - _itemNameLabel.Text = Identity.Name(_entity.Value, _entityManager); - } - } - - private void ClearOldStatus() - { - _statusContents.RemoveAllChildren(); - } - - private void BuildNewEntityStatus() - { - DebugTools.AssertNotNull(_entity); - - ClearOldStatus(); - - var collectMsg = new ItemStatusCollectMessage(); - _entityManager.EventBus.RaiseLocalEvent(_entity!.Value, collectMsg, true); - - foreach (var control in collectMsg.Controls) - { - _statusContents.AddChild(control); - } - } - } -} diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index 7f4a98542a..204a055e83 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -1,17 +1,10 @@ -using System; -using System.Linq; -using Content.Client.Chat; using Content.Client.Chat.Managers; -using Content.Client.Options.UI; using Content.Client.GameTicking.Managers; using Content.Client.LateJoin; using Content.Client.Lobby.UI; using Content.Client.Preferences; using Content.Client.Preferences.UI; -using Content.Client.Resources; -using Content.Client.UserInterface.Systems.EscapeMenu; using Content.Client.Voting; -using Content.Shared.GameTicking; using Robust.Client; using Robust.Client.Console; using Robust.Client.Input; @@ -20,13 +13,10 @@ using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Configuration; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -using Robust.Shared.Utility; -using Robust.Shared.ViewVariables; +using Content.Client.UserInterface.Systems.EscapeMenu; + namespace Content.Client.Lobby { @@ -74,14 +64,8 @@ namespace Content.Client.Lobby }; LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide); - - _chatManager.SetChatBox(_lobby.Chat); _voteManager.SetPopupContainer(_lobby.VoteContainer); - - _lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; - - ChatInput.SetupChatInputHandlers(_inputManager, _lobby.Chat); - + _lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you... UpdateLobbyUi(); _lobby.CharacterPreview.CharacterSetupButton.OnPressed += _ => @@ -107,7 +91,7 @@ namespace Content.Client.Lobby }; _lobby.LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect"); - _lobby.OptionsButton.OnPressed += OnOptionsPressed; + _lobby.OptionsButton.OnPressed += _ => _userInterfaceManager.GetUIController().ToggleWindow(); _gameTicker.InfoBlobUpdated += UpdateLobbyUi; @@ -115,11 +99,6 @@ namespace Content.Client.Lobby _gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated; } - private void OnOptionsPressed(BaseButton.ButtonEventArgs obj) - { - IoCManager.Resolve().GetUIController().ToggleWindow(); - } - protected override void Shutdown() { _gameTicker.InfoBlobUpdated -= UpdateLobbyUi; diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs index 8eb3e4a039..f6aaf08a63 100644 --- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs +++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Client.HUD.UI; using Content.Client.Humanoid; using Content.Client.Inventory; using Content.Client.Preferences; diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml b/Content.Client/Lobby/UI/LobbyGui.xaml index 1c885dc281..7b90d84db8 100644 --- a/Content.Client/Lobby/UI/LobbyGui.xaml +++ b/Content.Client/Lobby/UI/LobbyGui.xaml @@ -5,9 +5,9 @@ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:vote="clr-namespace:Content.Client.Voting.UI" xmlns:style="clr-namespace:Content.Client.Stylesheets" - xmlns:chatUi="clr-namespace:Content.Client.Chat.UI" xmlns:lobbyUi="clr-namespace:Content.Client.Lobby.UI" - xmlns:info="clr-namespace:Content.Client.Info"> + xmlns:info="clr-namespace:Content.Client.Info" + xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"> @@ -42,36 +42,36 @@ - - - - - - - - - - - - - public readonly string Slot; + /// + /// The slot group the entity got unequipped from. + /// + public readonly string SlotGroup; + public UnequippedEventBase(EntityUid equipee, EntityUid equipment, SlotDefinition slotDefinition) { Equipee = equipee; Equipment = equipment; Slot = slotDefinition.Name; + SlotGroup = slotDefinition.SlotGroup; } } diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs index ad8f7a5993..1bb50ea08f 100644 --- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs +++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs @@ -6,32 +6,28 @@ namespace Content.Shared.Inventory; [Prototype("inventoryTemplate")] public sealed class InventoryTemplatePrototype : IPrototype { - [IdDataFieldAttribute] - public string ID { get; } = string.Empty; + [IdDataFieldAttribute] public string ID { get; } = string.Empty; - [DataField("slots")] - public SlotDefinition[] Slots { get; } = Array.Empty(); + [DataField("slots")] public SlotDefinition[] Slots { get; } = Array.Empty(); } [DataDefinition] public sealed class SlotDefinition { [DataField("name", required: true)] public string Name { get; } = string.Empty; - [DataField("slotTexture")] public string TextureName { get; } = "pocket"; - [DataField("slotFlags")] public SlotFlags SlotFlags { get; } = SlotFlags.PREVENTEQUIP; - + [DataField("showInWindow")] public bool ShowInWindow { get; } = true; + [DataField("slotGroup")] public string SlotGroup { get; } = "Default"; [DataField("stripTime")] public float StripTime { get; } = 4f; - [DataField("uiContainer")] public SlotUIContainer UIContainer { get; } = SlotUIContainer.None; + [DataField("uiWindowPos", required: true)] + public Vector2i UIWindowPosition { get; } - [DataField("uiWindowPos", required: true)] public Vector2i UIWindowPosition { get; } - - //todo this is supreme shit and ideally slots should be stored in a given equipmentslotscomponent on each equipment [DataField("dependsOn")] public string? DependsOn { get; } - [DataField("displayName", required: true)] public string DisplayName { get; } = string.Empty; + [DataField("displayName", required: true)] + public string DisplayName { get; } = string.Empty; [DataField("stripHidden")] public bool StripHidden { get; } @@ -50,11 +46,3 @@ public sealed class SlotDefinition /// [DataField("blacklist")] public EntityWhitelist? Blacklist = null; } - -public enum SlotUIContainer -{ - None, - BottomLeft, - BottomRight, - Top -} diff --git a/Resources/Locale/en-US/chat/ui/chat-box.ftl b/Resources/Locale/en-US/chat/ui/chat-box.ftl index 6e89b024ba..77e5131ee6 100644 --- a/Resources/Locale/en-US/chat/ui/chat-box.ftl +++ b/Resources/Locale/en-US/chat/ui/chat-box.ftl @@ -9,6 +9,8 @@ hud-chatbox-select-channel-Local = Local hud-chatbox-select-channel-Whisper = Whisper hud-chatbox-select-channel-LOOC = LOOC hud-chatbox-select-channel-OOC = OOC +hud-chatbox-select-channel-Damage = Damage +hud-chatbox-select-channel-Visual = Actions hud-chatbox-select-channel-Radio = Radio hud-chatbox-channel-Admin = Admin @@ -20,5 +22,6 @@ hud-chatbox-channel-LOOC = LOOC hud-chatbox-channel-OOC = OOC hud-chatbox-channel-Radio = Radio hud-chatbox-channel-Server = Server -hud-chatbox-channel-Visual = Visual +hud-chatbox-channel-Visual = Actions +hud-chatbox-channel-Damage = Damage hud-chatbox-channel-Unspecified = Unspecified diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index e46937a98a..fdf85e1b88 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -123,7 +123,7 @@ ui-options-function-open-character-menu = Open character menu ui-options-function-open-context-menu = Open context menu ui-options-function-open-crafting-menu = Open crafting menu ui-options-function-open-inventory-menu = Open inventory -ui-options-function-open-info = Open admin help +ui-options-function-open-ahelp = Open admin help ui-options-function-open-abilities-menu = Open action menu ui-options-function-open-entity-spawn-window = Open entity spawn menu ui-options-function-open-sandbox-window = Open sandbox menu diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml index 2a16608d73..0e4ea542f9 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity parent: ClothingShoesBase id: ClothingShoesBootsMag name: magboots diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index f3f0621aa1..b844cbba18 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -32,7 +32,6 @@ - MobMask layer: - MachineLayer - - type: CharacterInfo - type: Humanoid - type: AnimationPlayer - type: MeleeWeapon diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 94b6b564d3..f2a9afa2c6 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -240,7 +240,6 @@ - type: Ensnareable sprite: Objects/Misc/ensnare.rsi state: icon - - type: CharacterInfo - type: AnimationPlayer - type: Buckle - type: MeleeWeapon diff --git a/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml index aa331d2d54..98f14b7bf0 100644 --- a/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml +++ b/Resources/Prototypes/InventoryTemplates/drone_inventory_template.yml @@ -4,7 +4,7 @@ - name: head slotTexture: head slotFlags: HEAD - uiContainer: BottomLeft + slotGroup: MainHotbar uiWindowPos: 0,0 displayName: Head offset: 0, -0.45 diff --git a/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml index 851ba4c078..6385b08bae 100644 --- a/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml +++ b/Resources/Prototypes/InventoryTemplates/human_inventory_template.yml @@ -5,66 +5,58 @@ slotTexture: shoes slotFlags: FEET stripTime: 3 - uiContainer: Top uiWindowPos: 1,3 displayName: Shoes - name: jumpsuit slotTexture: uniform slotFlags: INNERCLOTHING stripTime: 6 - uiContainer: Top uiWindowPos: 0,2 displayName: Jumpsuit - name: outerClothing slotTexture: suit slotFlags: OUTERCLOTHING + slotGroup: MainHotbar stripTime: 6 - uiContainer: Top uiWindowPos: 1,2 displayName: Suit - name: gloves slotTexture: gloves slotFlags: GLOVES - uiContainer: Top uiWindowPos: 2,2 displayName: Gloves - name: neck slotTexture: neck slotFlags: NECK - uiContainer: Top uiWindowPos: 0,1 displayName: Neck - name: mask slotTexture: mask slotFlags: MASK - uiContainer: Top uiWindowPos: 1,1 displayName: Mask - name: eyes slotTexture: glasses slotFlags: EYES stripTime: 3 - uiContainer: Top uiWindowPos: 0,0 displayName: Eyes - name: ears slotTexture: ears slotFlags: EARS stripTime: 3 - uiContainer: Top uiWindowPos: 2,0 displayName: Ears - name: head slotTexture: head slotFlags: HEAD - uiContainer: Top uiWindowPos: 1,0 displayName: Head - name: pocket1 slotTexture: pocket slotFlags: POCKET + slotGroup: MainHotbar stripTime: 3 - uiContainer: BottomRight uiWindowPos: 0,3 dependsOn: jumpsuit displayName: Pocket 1 @@ -72,8 +64,8 @@ - name: pocket2 slotTexture: pocket slotFlags: POCKET + slotGroup: MainHotbar stripTime: 3 - uiContainer: BottomRight uiWindowPos: 2,3 dependsOn: jumpsuit displayName: Pocket 2 @@ -82,29 +74,28 @@ slotTexture: suit_storage slotFlags: SUITSTORAGE stripTime: 3 - uiContainer: BottomLeft uiWindowPos: 2,0 dependsOn: outerClothing displayName: Suit Storage - name: id slotTexture: id slotFlags: IDCARD + slotGroup: SecondHotbar stripTime: 6 - uiContainer: BottomRight uiWindowPos: 2,1 dependsOn: jumpsuit displayName: ID - name: belt slotTexture: belt slotFlags: BELT + slotGroup: SecondHotbar stripTime: 6 - uiContainer: BottomLeft uiWindowPos: 3,1 displayName: Belt - name: back slotTexture: back slotFlags: BACK + slotGroup: SecondHotbar stripTime: 6 - uiContainer: BottomLeft uiWindowPos: 3,0 displayName: Back diff --git a/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml index 9bd19c9952..572bd67ec8 100644 --- a/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml +++ b/Resources/Prototypes/InventoryTemplates/monkey_inventory_template.yml @@ -4,6 +4,6 @@ - name: head slotTexture: head slotFlags: HEAD - uiContainer: BottomLeft + slotGroup: MainHotbar uiWindowPos: 0,0 - displayName: Head \ No newline at end of file + displayName: Head diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 6eae21942d..56dc8090f4 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -197,7 +197,7 @@ binds: - function: OpenCraftingMenu type: State key: G -- function: OpenInfo +- function: OpenAHelp type: State key: F1 - function: OpenInventoryMenu @@ -429,6 +429,10 @@ binds: - function: Hotbar9 type: State key: Num9 +- function: Loadout0 + type: State + key: Num0 + mod1: Shift - function: Loadout1 type: State key: Num1 diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index 2f9ec54ff3..041587116f 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -481,6 +481,7 @@ public sealed class $CLASS$ : Shared$CLASS$ { True True True + True True True True @@ -491,6 +492,7 @@ public sealed class $CLASS$ : Shared$CLASS$ { True True True + True True <data /> <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Lidgren.Network" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data>