Issue with ContextActionService Priority Handling in Roblox

(I now know its not actually a bug but if anyone else has this problem here you can find a solution)

Recently, while figuring out controls for a game, we encountered an issue when trying to disable certain actions by setting another action with higher priority using ContextActionService. Despite setting a higher priority, it seemed that the handling functions of all actions with the same key bind would still be called the first time, causing unexpected behavior.

Problem

When binding multiple actions to the same key with different priorities, all actions are invoked in the order of their priority. This means that even if you set a higher priority action to override a lower priority one, the lower priority action’s function will still be called the first time, then continue normally until the priority order was changed again.

Example:

Local Script (StarterCharacterScripts)

local ContextActionService = game:GetService("ContextActionService")

local function HandleAction(actionName, inputState)
     if inputState == Enum.UserInputState.End then return end

     print('Handled in Script')
end

ContextActionService:BindActionAtPriority('HandleAction', HandleAction, true, 1, Enum.KeyCode.F)

Local Script (Under Tool)

local ContextActionService = game:GetService("ContextActionService")

local tool = script.Parent

local function TakeActionPriority()
     print('Priority took')
end

tool.Equipped:Connect(function()
	ContextActionService:BindActionAtPriority("Priority",  TakeActionPriority , true, 2, Enum.KeyCode.F)
end)

tool.Unequipped:Connect(function()
	ContextActionService:UnbindAction("Priority")
end)

Output as events unfold:
Player Presses F
19:12:32.429 :arrow_forward: Handled in Script - Client - DevExample:6
Tool equipped
19:12:32.429 :arrow_forward: Handled in Script - Client - DevExample:6
Tool unequipped
19:12:41.063 Priority took - Client - Main:6

As you may be able to see from the code and output provided below, the handle functions are being called when priority is changed through binding or unbinding.

Solution

To ensure that the higher priority action fully overrides the lower priority action, we modified the input handling functions to check the input object’s input type. This allowed us to bypass the lower priority action when the higher priority action was invoked.

(Some text was clarified using ChatGPT)

Although its possible to solve this issue, its unexpected behavior can be confusing. Not to mention I haven’t seen it mentioned anywhere in the docs that this would happen. However please do contribute to this discussion and correct me on anything, or ask questions if your uncertain what I mean.

1 Like

This should be in #help-and-feedback:scripting-support more than anything.

Anyways, this is not a bug. The function actually invokes the binded action with the Enum.UserInputState.Cancel enum in 2 situations (source here):

  1. When the action gets unbinded.
  2. When the action keybind(s) gets reused in a newly binded action.

You can observe this behavior with this script:

--!strict

local ContextActionService = game:GetService("ContextActionService")

local function input(actionName: string, userInputState: Enum.UserInputState): Enum.ContextActionResult?
	print(actionName, userInputState)
	
	return nil
end

ContextActionService:BindActionAtPriority("LowerPriority", input, false, 1, Enum.KeyCode.Q)
ContextActionService:BindActionAtPriority("HigherPriority", input, false, 2, Enum.KeyCode.Q)
ContextActionService:UnbindAction("HigherPriority")

-- Output:
-- LowerPriority Enum.UserInputState.Cancel
-- HigherPriority Enum.UserInputState.Cancel
  1. LowerPriority action gets binded.
  2. HigherPriority action gets binded with the same keybind as LowerPriority, invoking the LowerAction action with a state of Enum.UserInputState.Cancel.
  3. HigherPriority action gets unbinded, invoking the HigherPriority action with a state of Enum.UserInputState.Cancel.
1 Like

Thank you, this clarifies a lot. However this should be stated in documentation more clearer nonetheless.

It is a special behavior that is actually on the documentation for ContextActionService here (unfortunately, not a lot of people bother reading documentations in it’s entirety):

*Cancel is sent if some input was in-progress and another action bound over the in-progress input, or if the in-progress bound action was unbound.

To add a bit of information, if the Enum.UserInputState.Cancel is relevant to you or have plans on incorporating it in the future, I would recommend checking if the action is still binded or not:

--!strict

local ContextActionService = game:GetService("ContextActionService")

local function isActionBinded(actionName: string): boolean
	return ContextActionService:GetAllBoundActionInfo()[actionName] ~= nil
end

local function input(actionName: string, userInputState: Enum.UserInputState): Enum.ContextActionResult?
	if userInputState == Enum.UserInputState.Cancel then
		print(actionName, userInputState, if isActionBinded(actionName) == true then "overridden" else "unbinded")
	end
	
	return nil
end

ContextActionService:BindActionAtPriority("LowerPriority", input, false, 1, Enum.KeyCode.Q)
ContextActionService:BindActionAtPriority("HigherPriority", input, false, 2, Enum.KeyCode.Q)
ContextActionService:UnbindAction("HigherPriority")

-- LowerPriority Enum.UserInputState.Cancel overridden
-- HigherPriority Enum.UserInputState.Cancel unbinded
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.