Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Key binding context #3286

Open
wants to merge 15 commits into
base: 1.20.1
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public boolean addCategory(String categoryName) {

@Override
public boolean register(FabricKeyBinding binding) {
return KeyBindingRegistryImpl.registerKeyBinding(binding) != null;
return KeyBindingHelper.registerKeyBinding(binding) != null;
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.client.keybinding.v1;

import org.jetbrains.annotations.ApiStatus;

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;

import net.fabricmc.fabric.impl.client.keybinding.KeyBindingExtensions;

/**
* {@link KeyBindingContext} decides how {@link KeyBinding} with same bounded key behaves in regard to each other.
*
* <p>Bindings with different context will not conflict with each other even if they have the same bounded key.
*/
public interface KeyBindingContext {
/**
* {@link KeyBinding} that used in-game. All vanilla key binds have this context.
*/
KeyBindingContext IN_GAME = () -> MinecraftClient.getInstance().currentScreen == null;

/**
* {@link KeyBinding} that used when a screen is open.
*/
KeyBindingContext IN_SCREEN = () -> MinecraftClient.getInstance().currentScreen != null;

/**
* {@link KeyBinding} that might be used in any context. This context conflicts with any other context.
*/
KeyBindingContext ALL = new KeyBindingContext() {
@Override
public boolean isActive() {
return true;
}

@Override
public boolean conflictsWith(KeyBindingContext other) {
return true;
}
};

static KeyBindingContext of(KeyBinding binding) {
return ((KeyBindingExtensions) binding).fabric_getContext();
}

static boolean conflicts(KeyBindingContext left, KeyBindingContext right) {
return left.conflictsWith(right) || right.conflictsWith(left);
}

boolean isActive();
deirn marked this conversation as resolved.
Show resolved Hide resolved

/**
* Use {@link #conflicts(KeyBindingContext, KeyBindingContext)} for checking if two context conflicts.
*/
@ApiStatus.OverrideOnly
default boolean conflictsWith(KeyBindingContext other) {
return this == other;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesnt feel right for some reason. If I understand this correctly both keybinds must not conflict with each other. So evem if you did return false to have a keybind that could run everywhere it still wouldnt work.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,20 @@ private KeyBindingHelper() {
* @throws IllegalArgumentException when a key binding with the same ID is already registered
*/
public static KeyBinding registerKeyBinding(KeyBinding keyBinding) {
return registerKeyBinding(keyBinding, KeyBindingContext.IN_GAME);
deirn marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Registers the keybinding and add the keybinding category if required.
*
* @param keyBinding the keybinding
* @param context the keybinding context
* @return the keybinding itself
* @throws IllegalArgumentException when a key binding with the same ID is already registered
*/
public static KeyBinding registerKeyBinding(KeyBinding keyBinding, KeyBindingContext context) {
Objects.requireNonNull(keyBinding, "key binding cannot be null");
deirn marked this conversation as resolved.
Show resolved Hide resolved
return KeyBindingRegistryImpl.registerKeyBinding(keyBinding);
return KeyBindingRegistryImpl.registerKeyBinding(keyBinding, context);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.impl.client.keybinding;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;

public interface KeyBindingExtensions {
KeyBindingContext fabric_getContext();

void fabric_setContext(KeyBindingContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package net.fabricmc.fabric.impl.client.keybinding;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -25,10 +27,13 @@

import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;
import net.fabricmc.fabric.mixin.client.keybinding.KeyBindingAccessor;

public final class KeyBindingRegistryImpl {
public static final Map<InputUtil.Key, List<KeyBinding>> KEY_TO_BINDINGS = new HashMap<>();
private static final List<KeyBinding> MODDED_KEY_BINDINGS = new ReferenceArrayList<>(); // ArrayList with identity based comparisons for contains/remove/indexOf etc., required for correctly handling duplicate keybinds

private KeyBindingRegistryImpl() {
Expand All @@ -51,7 +56,7 @@ public static boolean addCategory(String categoryTranslationKey) {
return true;
}

public static KeyBinding registerKeyBinding(KeyBinding binding) {
public static KeyBinding registerKeyBinding(KeyBinding binding, KeyBindingContext context) {
if (MinecraftClient.getInstance().options != null) {
throw new IllegalStateException("GameOptions has already been initialised");
}
Expand All @@ -66,6 +71,7 @@ public static KeyBinding registerKeyBinding(KeyBinding binding) {

// This will do nothing if the category already exists.
addCategory(binding.getCategory());
((KeyBindingExtensions) binding).fabric_setContext(context);
MODDED_KEY_BINDINGS.add(binding);
return binding;
}
Expand All @@ -80,4 +86,8 @@ public static KeyBinding[] process(KeyBinding[] keysAll) {
newKeysAll.addAll(MODDED_KEY_BINDINGS);
return newKeysAll.toArray(new KeyBinding[0]);
}

public static void putToMap(InputUtil.Key key, KeyBinding binding) {
KEY_TO_BINDINGS.computeIfAbsent(key, k -> new ArrayList<>()).add(binding);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.mixin.client.keybinding;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;

import net.minecraft.client.gui.screen.option.ControlsListWidget;

@Mixin(ControlsListWidget.KeyBindingEntry.class)
public class KeyBindingEntryMixin {
@ModifyConstant(method = "update", constant = @Constant(stringValue = ", "))
private String makeConflictTextMultiline(String constant) {
return "\n";
}

@ModifyConstant(method = "update", constant = @Constant(stringValue = "controls.keybinds.duplicateKeybinds"))
private String replaceConflictText(String constant) {
return "fabric.keybinding.conflicts";
deirn marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.mixin.client.keybinding;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;

import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingContext;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.fabricmc.fabric.impl.client.keybinding.KeyBindingExtensions;
import net.fabricmc.fabric.impl.client.keybinding.KeyBindingRegistryImpl;

@Mixin(KeyBinding.class)
public abstract class KeyBindingMixin implements KeyBindingExtensions {
@Shadow
private int timesPressed;

@Shadow
@Final
private static Map<String, KeyBinding> KEYS_BY_ID;

@Shadow
private InputUtil.Key boundKey;

@Unique
private KeyBindingContext fabric_context;

@Unique
private Set<KeyBinding> fabric_conflictingKeyBinds;

@Override
public KeyBindingContext fabric_getContext() {
return fabric_context;
}

@Override
public void fabric_setContext(KeyBindingContext context) {
this.fabric_context = context;
}

@Inject(method = "onKeyPressed", at = @At("HEAD"))
private static void onKeyPressed(InputUtil.Key key, CallbackInfo ci) {
List<KeyBinding> list = KeyBindingRegistryImpl.KEY_TO_BINDINGS.get(key);
if (list == null) return;

Set<KeyBinding> uniqueKeyBinds = Collections.newSetFromMap(new IdentityHashMap<>());

for (KeyBinding binding : list) {
KeyBindingMixin mixed = (KeyBindingMixin) (Object) binding;

if (mixed.fabric_context.isActive() && uniqueKeyBinds.addAll(mixed.fabric_conflictingKeyBinds)) {
((KeyBindingMixin) (Object) binding).timesPressed++;
}
}
}

@Inject(method = "setKeyPressed", at = @At("HEAD"))
private static void setKeyPressed(InputUtil.Key key, boolean pressed, CallbackInfo ci) {
List<KeyBinding> list = KeyBindingRegistryImpl.KEY_TO_BINDINGS.get(key);
if (list == null) return;

Set<KeyBinding> uniqueKeyBinds = Collections.newSetFromMap(new IdentityHashMap<>());

for (KeyBinding binding : list) {
KeyBindingMixin mixed = (KeyBindingMixin) (Object) binding;

if (mixed.fabric_context.isActive() && uniqueKeyBinds.addAll(mixed.fabric_conflictingKeyBinds)) {
binding.setPressed(pressed);
}
}
}

@Inject(method = "updateKeysByCode", at = @At("HEAD"))
private static void updateKeysByCode(CallbackInfo ci) {
KeyBindingRegistryImpl.KEY_TO_BINDINGS.clear();

for (KeyBinding binding : KEYS_BY_ID.values()) {
KeyBindingRegistryImpl.putToMap(KeyBindingHelper.getBoundKeyOf(binding), binding);
}

for (List<KeyBinding> bindings : KeyBindingRegistryImpl.KEY_TO_BINDINGS.values()) {
for (KeyBinding binding : bindings) {
((KeyBindingMixin) (Object) binding).fabric_conflictingKeyBinds.clear();
}

for (KeyBinding binding : bindings) {
KeyBindingMixin mixed = (KeyBindingMixin) (Object) binding;

for (KeyBinding otherBinding : bindings) {
if (binding == otherBinding) continue;
KeyBindingMixin otherMixed = (KeyBindingMixin) (Object) otherBinding;

if (KeyBindingContext.conflicts(mixed.fabric_context, otherMixed.fabric_context)) {
otherMixed.fabric_conflictingKeyBinds.add(binding);
otherMixed.fabric_conflictingKeyBinds.addAll(mixed.fabric_conflictingKeyBinds);
mixed.fabric_conflictingKeyBinds.add(otherBinding);
mixed.fabric_conflictingKeyBinds.addAll(otherMixed.fabric_conflictingKeyBinds);
}
}
}
}
}

@Inject(method = "<init>(Ljava/lang/String;Lnet/minecraft/client/util/InputUtil$Type;ILjava/lang/String;)V", at = @At("TAIL"))
private void init(String translationKey, InputUtil.Type type, int code, String category, CallbackInfo ci) {
fabric_context = KeyBindingContext.IN_GAME;
fabric_conflictingKeyBinds = Collections.newSetFromMap(new IdentityHashMap<>());
KeyBindingRegistryImpl.putToMap(boundKey, (KeyBinding) (Object) this);
}

@Inject(method = "equals", at = @At("RETURN"), cancellable = true)
private void equals(KeyBinding other, CallbackInfoReturnable<Boolean> cir) {
if (!KeyBindingContext.conflicts(fabric_context, KeyBindingContext.of(other))) {
cir.setReturnValue(false);
}
}

// Make KEYS_BY_ID deterministic
@Redirect(method = "<clinit>", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/Maps;newHashMap()Ljava/util/HashMap;", ordinal = 0))
private static HashMap<?, ?> makeMapOrdered() {
return new LinkedHashMap<>();
}

// Return empty set, skipping the loop
@Redirect(method = "updateKeysByCode", at = @At(value = "INVOKE", target = "Ljava/util/Map;values()Ljava/util/Collection;"))
private static Collection<?> skipVanillaLoop(Map<?, ?> instance) {
return Collections.emptySet();
}

// Skip putting this to KEY_TO_BINDINGS, this also skips vanilla onKeyPressed and setKeyPressed loops
@Redirect(method = "<init>(Ljava/lang/String;Lnet/minecraft/client/util/InputUtil$Type;ILjava/lang/String;)V", at = @At(value = "INVOKE", target = "Ljava/util/Map;put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 1))
private Object skipVanillaMapping(Map<?, ?> instance, Object k, Object v) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"fabric.keybinding.conflicts": "This key is conflicting with:\n%s"
}
Loading