Skip to content
110 changes: 92 additions & 18 deletions docs/developers/bindings-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,54 @@ _Learn how to register new controller bindings._
## Registering a custom input binding

<Callout variant="info">
You should register bindings inside of the Controlify init entrypoint. Read more in [Controlify Entrypoint](controlify-entrypoint#using-the-entrypoint).
The custom input bindings must be registered inside the Controlify pre-init entrypoint. Read more in [Controlify Entrypoint](controlify-entrypoint#using-the-entrypoint).
</Callout>

Controlify allows users to configure different buttons on their controllers to actions in-game. You may want your own mod's actions to be able to be invoked from the controller too.

To register a controller binding, you must use the `ControllerBindingsApi.`
To register a controller binding, use the `ControlifyBindApi.get()` method:

```java
private BindingSupplier action1Binding;
private InputBindingSupplier actionBinding;

@Override
public void onControlifyInit(InitContext ctx) {
action1Binding = ctx.bindings().registerBinding(
final ControlifyBindApi registrar = ControlifyBindApi.get();
registerInputBindings(registrar);
}

private void registerInputBindings(ControlifyBindApi registrar) {
actionBinding = registrar.registerBinding(
builder -> builder
.id("mymod", "action1") // the id of the binding, this should be unique to your mod
.category(Component.translatable("mymod.binding.category")) // the category of the binding, this is used to group bindings together in the settings
.allowedContexts(BindContext.IN_GAME) // a context is where the binding can be used, you can use multiple contexts
.radialCandiate(RadialIcons.getEffect/getIcon(...)) // if you want to allow your binding to be used in the radial menu
.id(ResourceLocation.fromNamespaceAndPath(MyMod.MODID, path))
// The category of the binding, this is used to group bindings together in the settings.
.category(Component.translatable("mymod.binding.category"))
// A context is where the binding can be used. Multiple contexts can be used.
// For example, using `BindContext.IN_GAME` will cause the binding to always return
// `false` inside a screen, even if it's pressed.
.allowedContexts(BindContext.IN_GAME)
// Allow using the binding in the radial menu.
// You can't use a custom modded item directly in here, since it's not registered yet
// and a runtime error will be thrown. Continue reading for a better alternative.
.radialCandidate(RadialIcons.getItem(Items.BARRIER)))
// Prevents Controlify from auto-registering controller bindings for the
// vanilla key mappings due to explicit native support.
.addKeyCorrelation(MyModKeyMappings.ACTION)
// Emulates the vanilla KeyMapping behavior.
// Note: "keyEmulation" calls "addKeyCorrelation" internally, so no need to call both.
//
// It will not work with key mappings that are handled using NeoForge `InputEvent`s.
// It such cases, you can fix the problem either by:
// 1. Patch the mod to migrate to vanilla `KeyMapping` handling (example: https://github.com/iron431/irons-spells-n-spellbooks/pull/989).
// 2. Provide explicit Controlify support.
.keyEmulation(MyModKeyMappings.ACTION)
Comment on lines +49 to +50
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Option 2 means no key emulation, and therefore, only addKeyCorrelation should be used.

I will improve this part soon.

);
}
```

To add a name and description to the binding, you need to define the language keys `controlify.binding.<namespace>.<path>` and `controlify.binding.<namespace>.<path>.desc` respectively, alternatively, you can set `.name(Component)` and `.description(Component)`

Registering the binding provides you with a `BindingSupplier`, where you can then access the binding with `action1Binding.on(controller);`

Controlify automatically converts your existed modded `KeyMapping`s to controller bindings, but relying on this behaviour if you are going to explicitly support Controlify is not recommended. You can stop this conversion with the following...

```java
@Override
public void onControlifyInit(InitContext ctx) {
ctx.bindings().exclude(MyMod.myKeyMapping);
}
```
Registering the binding provides you with a `InputBindingSupplier`, where you can then access the binding with `actionBinding.on(controller);`

## Defining a default binding

Expand Down Expand Up @@ -83,3 +97,63 @@ There are more properties available inside of `InputBinding` which you can look
There is nothing special about rendering glyphs for controller bindings, as Controlify utilises custom fonts.

This means you can use the glyphs within localised text, or just render it with `graphics.drawString(myBinding.inputGlyph(), x, y, -1);`

## Registering Custom Radial Icons

When registering a radial menu candidate, you must provide an icon to render it in the GUI.
You can use `RadialIcons.getEffect(...)` or `RadialIcons.getItem(...)` (for example, `RadialIcons.getItem(Items.REDSTONE)`).

However, **do not reference modded items directly** (e.g., `RadialIcons.getItem(ModItems.CUSTOM.get())`),
as item registration is deferred — meaning those items are not yet registered at this stage.

What you can do instead, is to reference the texture image file in the bundled resource-pack of your mod,
using the following approach:

```java
private enum MyRadialIcons {
CUSTOM(MyMod.rl("textures/item/custom.png")),;

private final @NotNull ResourceLocation id;

MyRadialIcons(@NotNull ResourceLocation id) {
this.id = id;
}
Comment on lines +114 to +120
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ResourceLocation was renamed to Identifier in 1.21.11+, so should we use that instead?


public @NotNull ResourceLocation getId() {
return id;
}
}

@Override
public void onControlifyPreInit(PreInitContext context) {
final ControlifyBindApi registrar = ControlifyBindApi.get();
registerCustomRadialIcons();

// Must be called after registerCustomRadialIcons
registerInputBindings(registrar);
}

private void registerCustomRadialIcons() {
for (MyRadialIcons icon : MyRadialIcons.values()) {
final ResourceLocation location = icon.getId();

// For consistency with the current Controlify radial icons,
// this code is equivalent to:
// https://github.com/isXander/Controlify/blob/f5c94c57d5e0d4954e413624a0d7ead937b6e8ab/src/main/java/dev/isxander/controlify/bindings/RadialIcons.java#L106-L112
RadialIcons.registerIcon(location, (graphics, x, y, tickDelta) -> {
var pose = CGuiPose.ofPush(graphics);
pose.translate(x, y);
pose.scale(0.5f, 0.5f);
Blit.tex(graphics, location, 0, 0, 0, 0, 32, 32, 32, 32);
pose.pop();
});
}
}
Comment on lines +140 to +151
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be worth considering making a public API that provides this functionality.


private void registerInputBindings(ControlifyBindApi registrar) {
actionBinding = registrar.registerBinding(
builder -> builder
.radialCandidate(MyRadialIcons.CUSTOM.getId())
);
}
```
78 changes: 61 additions & 17 deletions docs/developers/controlify-entrypoint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,70 @@ _Learn how to hook into Controlify._

Controlify provides a Fabric entrypoint to hook into important lifecycle stages of Controlify. You do this just like `ClientModInitializer`.

## Registering the entrypoint
## Adding Controlify dependency

In your `build.gradle` (or `build.gradle.kts`), you need to add the maven
repository and the Controlify mod dependency.

- On Fabric, you register an entrypoint like any other, by adding an entry to your `fabric.mod.json` file under the `entrypoints` section.
- On NeoForge, you register an entrypoint using a Java service provider interface (SPI) in your `META-INF/services` directory.

<CodeTabs>
```json !!tabs (Fabric) fabric.mod.json
{
"entrypoints": {
"controlify": [
"com.example.mymod.ControlifyEntrypoint"
]
}
<details>
<summary>Maven repository</summary>

```kotlin
repositories {
exclusiveContent {
forRepository { maven { url "https://maven.isxander.dev/releases" } }
filter { includeGroup "dev.isxander" }
}
```
}
```

</details>

<details>
<summary>Dependency</summary>

```txt !!tabs (NeoForge) META-INF/services/dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint
com.example.mymod.ControlifyEntrypoint
```
</CodeTabs>
Add the version to your `gradle.properties`:

```properties
# You can find the versions in here: https://maven.isxander.dev/#/releases/dev/isxander/controlify
controlify_version=2.4.3+1.21.1-neoforge
```

```kotlin
dependencies {
// Note: "compileOnly" must be replaced with "modCompileOnly" if Fabric Loom is used while targeting an obfuscated Minecraft version.
// This is not needed for deobfuscated Minecraft versions.
compileOnly("dev.isxander:controlify:${project.controlify_version}") {
// Only need Controlify API, ignore the transitive dependencies (e.g, QuiltMC parsers)
transitive = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't it be better to remove the QuiltMC Parser as a transitive dependency, rather than using this workaround?

This is more convenient, because it means users don't have to add YACL as a runtime only.

Without transitive = false (isTransitive in KTS) workaround, Quilt MC parsers will be added to consumers as compileOnly, and they have to add the QuiltMC Maven Repository to prevent build failure (e.g., dependency not found in the following sources).

}
}
```

</details>

## Registering the entrypoint

The entrypoint can be registered using a
Java service provider interface (SPI) in **`META-INF/services/dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint`**

```txt
com.example.mymod.ControlifyEntrypoint
```

### Fabric Entrypoint

On Fabric, the same entrypoint may **also** be registered via `fabric.mod.json`.

```json
{
"entrypoints": {
"controlify": [
"com.example.mymod.ControlifyEntrypoint"
]
}
}
```

Then simply create the class and implement the interface:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ public interface ControlifyEntrypoint {
* Called once Controlify has initialised some systems but controllers
* have not yet been discovered and constructed. This is the ideal
* time to register events in preparation for controller discovery.
* Input bindings cannot be registered here; use {@link #onControlifyPreInit} instead.
*/
void onControlifyInit(InitContext context);


/**
* Called at the end of Controlify's client-side entrypoint.
* You can register guides here.
* Use this to register guides, input bindings, radial icons, or screen processors,
* and to subscribe to events from {@link dev.isxander.controlify.api.event.ControlifyEvents}.
* Avoid referencing objects that are registered later (e.g. custom mod items),
* as they may not be initialized yet and could cause errors due to deferred registration.
*/
void onControlifyPreInit(PreInitContext context);

Expand Down