Skip to content

fix(gta-core-five): release held key state when a binding is destroyed - #4089

Open
Github-Samuel wants to merge 1 commit into
citizenfx:masterfrom
Github-Samuel:fix/keybind-remap-stuck-down
Open

fix(gta-core-five): release held key state when a binding is destroyed#4089
Github-Samuel wants to merge 1 commit into
citizenfx:masterfrom
Github-Samuel:fix/keybind-remap-stuck-down

Conversation

@Github-Samuel

Copy link
Copy Markdown

Goal of this PR

Stop a keybind from dying permanently when it gets remapped while the key is being held down. Right now the +command fires but -command never does, so anything the bind toggles stays stuck on and the bind does nothing until the game is restarted.

How is this PR achieving the goal

+command bindings are reference counted through g_downSet:

static std::multiset<std::string, std::less<>> g_downSet;

Binding::Update inserts a reference when the key goes down and removes one when it comes up, only emitting +command on the first reference and -command on the last. That reference is owned by the binding's m_wasDown state, but ~Binding only called Unmap() and never gave it back.

Remapping does exactly that inside one frame. BindingManager::Update runs the bindings first:

for (auto& bindingPair : bindings)
{
	auto [source, binding] = bindingPair;

	binding->Update();
}

std::function<void()> func;

while (m_queue.try_pop(func))
{
	func();
}

so +command fires and takes a reference at the top, and the queued Bind() from MapControlInternal runs at the bottom, where the old binding is erased while m_wasDown is still true:

if (it->second->GetCommand() == commandString && IsTagActive(it->second->GetTag()))
{
	it = m_bindings.erase(it);
	continue;
}

The leftover reference is what kills the bind. Every later press finds the command already in the set and skips +command, and every release removes one of the two references and still finds one, so -command is skipped too.

This PR splits the dispatch out of Update into Binding::RunCommands and calls it from the destructor when the binding is still held:

Binding::~Binding()
{
	if (m_wasDown)
	{
		m_wasDown = false;

		RunCommands(false, true);
	}

	Unmap();
}

Nothing inside the dispatch changed, it is only moved. Using the existing path rather than erasing from g_downSet directly keeps the ; splitting and the tag handling identical for both callers.

The same shape already exists on the raw keymap side, where m_wasTriggered is described as "used so we don't ignore key ups if the key was disabled while being pressed" in code/components/extra-natives-five/src/InputNatives.cpp.

This also covers the two other places a binding can be destroyed while held, unbind for a single key and unbindall.

This PR applies to the following area(s)

FiveM

Successfully tested on

Game builds: n/a, this is client input plumbing and does not depend on a game build

Platforms: Windows

Checklist

  • Code compiles and has been tested successfully.
  • Code explains itself well and/or is documented.
  • My commit message explains what the changes do and what they are for.
  • No extra compilation warnings are added by these changes.

I could not do a full client build locally so I left the first box unchecked. What I did check is the reference counting itself, by taking the g_downSet protocol and the binding lifetime out into a small harness and running the reported sequence through it. Before the change the reference is left behind and every following press and release is swallowed, after it the reference is given back and the bind keeps working. Happy to adjust if you would rather see this solved somewhere other than the destructor.

One thing I deliberately left alone: the g_downSet bookkeeping sits inside the if (!ignore) branch, so a binding whose command is already gone (a resource stopping while its key is held) still leaks a reference. That is a separate case from this issue and it looked like it deserves its own change rather than being folded in here.

Fixes issues

fixes #3793

A `+command` binding takes a reference in `g_downSet` when its key goes down
and gives it back when the key comes up. That reference belongs to the
binding's `m_wasDown` state, but `~Binding` never gave it back, so a binding
destroyed while its key was still held left the reference behind forever.

Rebinding from the pause menu does exactly that within a single frame:
`BindingManager::Update` dispatches the bindings first, which fires `+command`
and takes the reference, and only then drains the queue where `Bind` erases the
old binding.

With one reference stuck in the set every later press finds the command already
down and skips `+command`, and every release only removes one of the two
references so `-command` never runs either. The bind stays dead until the game
is restarted.

Move the command dispatch into `Binding::RunCommands` and run the up path from
the destructor when the binding is still held, so the reference is given back.
This also covers the other two places a binding can be destroyed while down,
unbinding a single key and clearing all bindings.

Signed-off-by: Samuel Nicol <99494967+Github-Samuel@users.noreply.github.com>
@github-actions github-actions Bot added the invalid Requires changes before it's considered valid and can be (re)triaged label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

invalid Requires changes before it's considered valid and can be (re)triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keybind stops working after remapping to same key during active press

1 participant