using System.Collections.Generic; using System.Linq; using Content.Shared.Actions; using Content.Shared.GameObjects.Components.Mobs; using Robust.Shared.GameObjects; namespace Content.Client.GameObjects.Components.Mobs.Actions { /// /// Tracks and manages the hotbar assignments for actions. /// public 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 ActionAssignment?[,] _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) /// private readonly Dictionary> _assignments; /// /// Actions which have been manually cleared by the user, thus should not /// auto-populate. /// private readonly HashSet _preventAutoPopulate = new(); private readonly Dictionary> _preventAutoPopulateItem = new(); private readonly byte _numHotbars; private readonly byte _numSlots; public ActionAssignments(byte numHotbars, byte numSlots) { _numHotbars = numHotbars; _numSlots = numSlots; _assignments = new Dictionary>(); _slots = new ActionAssignment?[numHotbars,numSlots]; } /// /// Updates the assignments based on the current states of all the actions. /// Newly-granted actions or item actions which don't have an assignment will be assigned a slot /// automatically (unless they've been manually cleared). Item-based actions /// which no longer have an associated state will be decoupled from their item. /// public void Reconcile(byte currentHotbar, IReadOnlyDictionary actionStates, IReadOnlyDictionary> itemActionStates) { // if we've been granted any actions which have no assignment to any hotbar, we must auto-populate them // into the hotbar so the user knows about them. // We fill their current hotbar first, rolling over to the next open slot on the next hotbar. foreach (var actionState in actionStates) { var assignment = ActionAssignment.For(actionState.Key); if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment)) { AutoPopulate(assignment, currentHotbar, false); } } foreach (var (item, itemStates) in itemActionStates) { foreach (var itemActionState in itemStates) { // unlike regular actions, we DO actually show user their new item action even when it's disabled. // this allows them to instantly see when an action may be possible that is provided by an item but // something is preventing it // Note that we are checking if there is an explicit assignment for this item action + item, // we will determine during auto-population if we should tie the item to an existing "item action only" // assignment var assignment = ActionAssignment.For(itemActionState.Key, item); if (!_assignments.ContainsKey(assignment)) { AutoPopulate(assignment, currentHotbar, false); } } } // We need to figure out which current item action assignments we had // which once had an associated item but have been revoked (based on our newly provided action states) // so we can dissociate them from the item. If the provided action states do not // have a state for this action type + item, we can assume that the action has been revoked for that item. var assignmentsWithoutItem = new List>>(); foreach (var assignmentEntry in _assignments) { if (!assignmentEntry.Key.TryGetItemActionWithItem(out var actionType, out var item)) continue; // we have this assignment currently tied to an item, // check if it no longer has an associated item in our dict of states if (itemActionStates.TryGetValue(item, out var states)) { if (states.ContainsKey(actionType)) { // we have a state for this item + action type so we won't // remove the item from the assignment continue; } } assignmentsWithoutItem.Add(assignmentEntry); } // reassign without the item for each assignment we found that no longer has an associated item foreach (var (assignment, slots) in assignmentsWithoutItem) { foreach (var (hotbar, slot) in slots) { if (!assignment.TryGetItemActionWithItem(out var actionType, out _)) continue; AssignSlot(hotbar, slot, ActionAssignment.For(actionType)); } } // Additionally, we must find items which have no action states at all in our newly provided states so // we can assume their item was unequipped and reset them to allow auto-population. var itemsWithoutState = _preventAutoPopulateItem.Keys.Where(item => !itemActionStates.ContainsKey(item)); foreach (var toRemove in itemsWithoutState) { _preventAutoPopulateItem.Remove(toRemove); } } /// /// 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, ActionAssignment 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.HasValue) return; if (preventAutoPopulate) { var assignment = currentAction.Value; if (assignment.TryGetAction(out var actionType)) { _preventAutoPopulate.Add(actionType); } else if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item)) { if (!_preventAutoPopulateItem.TryGetValue(item, out var actionTypes)) { actionTypes = new HashSet(); _preventAutoPopulateItem[item] = actionTypes; } actionTypes.Add(itemActionType); } } var assignmentList = _assignments[currentAction.Value]; assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList(); if (assignmentList.Count == 0) { _assignments.Remove(currentAction.Value); } else { _assignments[currentAction.Value] = 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(ActionAssignment toAssign, byte currentHotbar, bool force = true) { if (ShouldPreventAutoPopulate(toAssign, force)) return; // if the assignment to make is an item action with an associated item, // then first look for currently assigned item actions without an item, to replace with this // assignment if (toAssign.TryGetItemActionWithItem(out var actionType, out var _)) { if (_assignments.TryGetValue(ActionAssignment.For(actionType), out var possibilities)) { // use the closest assignment to current hotbar byte hotbar = 0; byte slot = 0; var minCost = int.MaxValue; foreach (var possibility in possibilities) { var cost = possibility.Slot + _numSlots * (currentHotbar >= possibility.Hotbar ? currentHotbar - possibility.Hotbar : (_numHotbars - currentHotbar) + possibility.Hotbar); if (cost < minCost) { hotbar = possibility.Hotbar; slot = possibility.Slot; minCost = cost; } } if (minCost != int.MaxValue) { AssignSlot(hotbar, slot, 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.HasValue) { // if the assignment in this slot is an item action without an associated item, // then tie it to the current item if we are trying to auto populate an item action. if (toAssign.Assignment == Assignment.ItemActionWithItem && slotAssignment.Value.Assignment == Assignment.ItemActionWithoutItem) { AssignSlot(hotbar, slot, toAssign); return; } continue; } // slot's empty, assign AssignSlot(hotbar, slot, toAssign); return; } } // there was no empty slot } private bool ShouldPreventAutoPopulate(ActionAssignment assignment, bool force) { if (force) return false; if (assignment.TryGetAction(out var actionType)) { return _preventAutoPopulate.Contains(actionType); } if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item)) { return _preventAutoPopulateItem.TryGetValue(item, out var itemActionTypes) && itemActionTypes.Contains(itemActionType); } return false; } /// /// Gets the assignment to the indicated slot if there is one. /// public ActionAssignment? this[in byte hotbar, in byte slot] => _slots[hotbar, slot]; /// true if we have the assignment assigned to some slot public bool HasAssignment(ActionAssignment assignment) { return _assignments.ContainsKey(assignment); } } }