Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ manifests {
"controlify:controller_type",
"controlify:default_binds",
"controlify:trigger_effect",
"controlify:radial_icons",
)
))

Expand Down
133 changes: 81 additions & 52 deletions docs/developers/bindings-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,84 @@ _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).
Register bindings 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.`
Controlify bindings let users assign controller inputs to actions in your mod. Register a binding through
the `ControlifyBindApi` supplied by the pre-init context:

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

@Override
public void onControlifyPreInit(PreInitContext ctx) {
action1Binding = ctx.bindings().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
public void onControlifyPreInit(PreInitContext context) {
action1Binding = context.bindings().registerBinding(builder -> builder
.id("mymod", "action1")
.category(Component.translatable("mymod.binding.category"))
.allowedContexts(BindContext.IN_GAME)
);
}
```

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)`.
The ID and category are required. By default, the name and optional description use these language keys:

- `controlify.binding.<namespace>.<path>`
- `controlify.binding.<namespace>.<path>.desc`

You can instead provide `.name(Component)` and `.description(Component)` explicitly.

Registration returns an `InputBindingSupplier`. Once a controller exists, use
`action1Binding.on(controller)` to obtain that controller's `InputBinding`. If you registered the binding
with a controller filter, use `onOrNull(controller)` when the controller may not match it.

## Integrating an existing key mapping

Registering the binding provides you with a `BindingSupplier`, where you can then access the
binding with `action1Binding.on(controller);`
On both Fabric and NeoForge, Controlify automatically converts otherwise unhandled modded `KeyMapping`s into
controller bindings. These generated bindings are also radial menu candidates and use a book as their
fallback radial icon. Mods that do not want a Java integration can still assign icons to these bindings; see
[Icons for automatically generated bindings](../resource-packs/radial-icons#icons-for-automatically-generated-bindings).

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...
When explicitly registering a Controlify binding for an existing key mapping, correlate it so Controlify
does not also create an automatic binding. Use `keyEmulation` if pressing the controller binding should press
the key mapping:

```java
@Override
public void onControlifyPreInit(PreInitContext ctx) {
ctx.bindings().exclude(MyMod.myKeyMapping);
action1Binding = context.bindings().registerBinding(builder -> builder
.id("mymod", "action1")
.category(Component.translatable("mymod.binding.category"))
.allowedContexts(BindContext.IN_GAME)
.keyEmulation(MyMod.ACTION_1_KEY)
);
```

`keyEmulation` automatically adds the key correlation. If your code handles the Controlify binding directly
and should not emulate the key, use `.addKeyCorrelation(MyMod.ACTION_1_KEY)` instead.

## Adding the binding to the radial menu

Radial candidates and icons are data-driven. Add the binding ID to
`assets/controlify/radial_icons.json` in your mod's resources:

```json
{
"mymod:action1": {
"model": "mymod:radial/action1"
}
}
```

The Java bindings API does not register radial icons or mark radial candidates. See
[Radial Menu Icons](../resource-packs/radial-icons) for model and texture icon definitions, file locations,
and resource-pack override behavior.

## Defining a default binding

You may have noticed that in the above code, we did not define a default binding, such as the A button.
This is because default bindings are data-driven and defined by resource packs.
If you want to define a default binding, you can do so by creating a JSON file in your mod's resources.
Default controller inputs are data-driven. To assign the south face button to the example binding for every
controller, create:

<CodeTabs>
```json !!tabs (Default for all controllers) assets/controlify/controllers/default_bind/default.json
```json !!tabs assets/controlify/controllers/default_bind/default.json
{
"defaults": {
"mymod:action1": {
Expand All @@ -67,35 +95,36 @@ If you want to define a default binding, you can do so by creating a JSON file i
```
</CodeTabs>

This will set the default binding for `mymod:action1` to the south face button on all controllers.
You can also define defaults for specific controller namespaces by creating a file in
`assets/<namespace>/controllers/default_bind/<path>.json`.

For more information on how to define default bindings, see the [Default Binds](../resource-packs/default-binds) page.
Defaults for a specific controller namespace belong at
`assets/<namespace>/controllers/default_bind/<path>.json`. For the full format, see
[Default Binds](../resource-packs/default-binds).

## Using the binding

Once you have access to a binding through `bindingSupplier.on(controller)`,
you can access many properties of the binding:
Once you have an `InputBinding`, its commonly used state accessors include:

| Property | Description |
|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `inputGlyph()` | Returns the input glyph for the binding, which can be used in text. |
| `digitalNow()` | Returns true if the binding is pressed this tick. |
| `digitalPrev()` | Returns true if the binding was pressed in the previous tick. |
| `analogueNow()` | Gets a float value between 0 - 1 which describes its analogue state. |
| `analoguePrev()` | Gets a float value between 0 - 1 which describes its analogue state in the previous tick. |
| `justPressed()` | Returns if the binding is pressed this tick, if it returns true, the state is consumed and will return false for the remaining tick. |
| `justReleased()` | Returns if the binding was just released this tick, if it returns true, the state is consumed and will return false for the remaining tick. |
| `justTapped()` | Returns if the binding was just tapped this tick, if it returns true, the state is consumed and will return false for the remaining tick. |
| `guiPressed().get()` | Returns true if the binding is pressed this tick in the GUI context. |
| Property | Description |
|----------------------|--------------------------------------------------------------------------------------------------|
| `inputGlyph()` | Returns the input glyph for this binding and controller. |
| `digitalNow()` | Returns whether the binding is pressed this tick. |
| `digitalPrev()` | Returns whether the binding was pressed during the previous tick. |
| `analogueNow()` | Returns the current analogue value, from 0 to 1. |
| `analoguePrev()` | Returns the analogue value from the previous tick. |
| `justPressed()` | Returns whether the binding became pressed this tick. |
| `justReleased()` | Returns whether the binding became released this tick. |
| `justTapped()` | Returns whether the binding was pressed and then released. |
| `guiPressed().get()` | Handles a GUI press that completes only if focus remains on the widget where the press began. |

There are more properties available inside of `InputBinding` which you can look at in the sources,
but the above are the most notable that you will use the most.
See `InputBinding` in the API sources for advanced state access and custom outputs.

## Rendering binding glyphs

There is nothing special about rendering glyphs for controller bindings, as Controlify utilises custom fonts.
Controller glyphs are text components backed by Controlify's input font. With a
`GuiGraphicsExtractor`, render a binding glyph like any other component:

```java
graphics.text(Minecraft.getInstance().font, binding.inputGlyph(), x, y, -1);
```

This means you can use the glyphs within localised text, or just render it with
`graphics.drawString(myBinding.inputGlyph(), x, y, -1);`
`InputBindingSupplier.inputGlyph()` can be used when you want the glyph for the currently selected
controller without first resolving a particular controller's binding.
1 change: 1 addition & 0 deletions docs/resource-packs/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"input-glyphs.mdx": "Input Glyphs",
"guides.mdx": "Button Guides",
"keyboard-layouts.mdx": "Keyboard Layouts",
"radial-icons.mdx": "Radial Menu Icons",
"adaptive-trigger-effects.mdx": "Adaptive Trigger Effects"
}
156 changes: 156 additions & 0 deletions docs/resource-packs/radial-icons.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
title: Radial Menu Icons
---

Radial menu candidates and their icons are defined by resource packs. Mods can provide an icon for one of
their bindings by including resources in their own JAR; no Java call into Controlify is required.

## Creating the mapping

Create `assets/controlify/radial_icons.json`. The keys are binding IDs and each value defines either an item
model or a GUI sprite:

<CodeTabs>
```json !!tabs assets/controlify/radial_icons.json
{
"example:open_backpack": {
"model": "example:backpack"
},
"example:open_overlay": {
"model": "example:radial/open_overlay"
},
"example:mute_microphone": {
"texture": "example:radial/mute_microphone"
}
}
```
</CodeTabs>

An entry must contain exactly one of `model` or `texture`. Adding a binding to this file also makes it
available in the radial menu editor. The binding itself must still be registered by Controlify or another
mod.

## Model icons

The value of `model` is an **item model definition ID**. You do not normally need to create a model
specifically for the radial menu. If an action already has an item that represents it, prefer reusing that
item's existing model.

For example, if the example mod registers a backpack item whose item model definition is
`assets/example/items/backpack.json`, its Open Backpack binding can use that model directly:

```json
{
"example:open_backpack": {
"model": "example:backpack"
}
}
```

Vanilla item models can be reused in the same way, such as `minecraft:chest` or `minecraft:spyglass`.

### Creating an icon-only model

When no existing item represents the action, create an item model definition specifically for the radial
icon. For `"model": "example:radial/open_overlay"`, Minecraft loads:

`assets/example/items/radial/open_overlay.json`

For example, the item model definition can point at a regular generated model:

<CodeTabs>
```json !!tabs assets/example/items/radial/open_overlay.json
{
"model": {
"type": "minecraft:model",
"model": "example:item/radial/open_overlay"
}
}
```

```json !!tabs assets/example/models/item/radial/open_overlay.json
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "example:item/radial/open_overlay"
}
}
```
</CodeTabs>

The texture used by that example belongs at
`assets/example/textures/item/radial/open_overlay.png`.

The file under `items/` does not need to belong to a registered item. Minecraft discovers and bakes every
item model definition supplied by loaded resource packs, so a mod can create definitions used only by
Controlify's radial menu.

Radial model icons are rendered in the GUI display context without the state of a real item stack. Prefer a
plain model for these icons; item model conditions that depend on components or other stack data will not
have that data available.

## Texture icons

The value of `texture` is a sprite ID in Minecraft's GUI atlas, rather than a direct path to an arbitrary PNG.
The default GUI atlas discovers files below `textures/gui/sprites`, so
`"texture": "example:radial/mute_microphone"` resolves to:

`assets/example/textures/gui/sprites/radial/mute_microphone.png`

Sprites added to the GUI atlas by other atlas sources can also be referenced. For example, Minecraft's
Jump Boost effect sprite is `minecraft:mob_effect/jump_boost`.

Texture icons are drawn into an 18 by 18 pixel area, matching the potion-effect icons used by Controlify.

## Icons for automatically generated bindings

Mods do not need a full Controlify integration to assign radial icons. On both Fabric and NeoForge,
Controlify automatically creates controller bindings for modded key mappings that have not been correlated
with an explicitly registered Controlify binding. These generated bindings are always radial candidates and
use a book model when no mapping is provided.

The generated binding ID uses the key mapping name returned by `KeyMapping#getName()`:

`controlify_modded:<sanitized key mapping name>`

Equivalent Java code for the generated ID is:

```java
String path = keyMapping.getName()
.toLowerCase()
.replaceAll("[^a-z0-9/._-]", "_")
.trim();
Identifier id = Identifier.fromNamespaceAndPath("controlify_modded", path);
```

Controlify calculates the path by lowercasing the name, replacing every character outside
`a-z`, `0-9`, `/`, `.`, `_`, and `-` with `_`, and then trimming it. This uses the key mapping's stable name
or translation key, not its translated text. For example:

`key.example.open_backpack` becomes `controlify_modded:key.example.open_backpack`.

The mod can assign the generated binding an icon by including only this resource:

<CodeTabs>
```json !!tabs assets/controlify/radial_icons.json
{
"controlify_modded:key.example.open_backpack": {
"model": "example:backpack"
}
}
```
</CodeTabs>

If the mod later registers and correlates an explicit Controlify binding for that key mapping, the automatic
binding is not created. Its radial icon entry should then use the explicit binding's ID instead.

## Resource-pack priority

Controlify merges every loaded `assets/controlify/radial_icons.json` by binding ID. If the same binding is
defined twice, the definition from the higher-priority resource pack—the pack shown earlier in the active
resource-pack list—wins. Definitions for other bindings remain in place, so a pack can override a single icon
without copying the entire built-in file.

If one file contains invalid JSON, an invalid identifier, or an entry with both/neither icon fields,
Controlify logs the error and skips that entire file layer. Definitions from other resource packs continue
to load.
2 changes: 2 additions & 0 deletions src/main/java/dev/isxander/controlify/Controlify.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import dev.isxander.controlify.bindings.BindContext;
import dev.isxander.controlify.bindings.ControlifyBindApiImpl;
import dev.isxander.controlify.bindings.ControlifyBindings;
import dev.isxander.controlify.bindings.RadialIconManager;
import dev.isxander.controlify.bindings.defaults.DefaultBindManager;
import dev.isxander.controlify.compatibility.ControlifyCompat;
import dev.isxander.controlify.config.ConfigManager;
Expand Down Expand Up @@ -151,6 +152,7 @@ public void preInitialiseControlify() {
PlatformClientUtil.registerAssetReloadListener(controllerTypeManager);
PlatformClientUtil.registerAssetReloadListener(keyboardLayoutManager);
PlatformClientUtil.registerAssetReloadListener(triggerEffectRegistry);
PlatformClientUtil.registerAssetReloadListener(RadialIconManager.INSTANCE);
PlatformClientUtil.registerAssetReloadListener(GuideDomains.IN_GAME);
PlatformClientUtil.registerAssetReloadListener(GuideDomains.CONTAINER);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,6 @@ static ControlifyBindApi get() {
*/
List<InputBindingSupplier> getKeyCorrelation(KeyMapping key);

/**
* Registers a new radial icon that can be used in the radial menu.
*
* @param id the id of the icon
* @param icon the icon object
*/
void registerRadialIcon(Identifier id, RadialIcon icon);

/**
* Registers a new bind context that can be used to determine when a binding is active.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import net.minecraft.resources.Identifier;
import org.jetbrains.annotations.ApiStatus;

import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

Expand Down Expand Up @@ -145,14 +144,6 @@
*/
Set<BindContext> contexts();

/**
* Returns the radial icon's ID.
* If the binding does not have a radial icon, this will return an empty optional.
* If empty, consider this binding as not being a radial candidate. It can never be added to the radial menu.
* @return the radial icon's ID or an empty optional
*/
Optional<Identifier> radialIcon();

/**
* Equivalent to calling
* <pre><code>
Expand Down Expand Up @@ -201,7 +192,7 @@
/**
* Equivalent to calling
* <pre><code>
* binding.digitalNow() && !binding.digitalPrev()

Check failure on line 195 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.1, false)

bad HTML entity

Check failure on line 195 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.1, false)

bad HTML entity

Check failure on line 195 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.2, false)

bad HTML entity

Check failure on line 195 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.2, false)

bad HTML entity
* </code></pre>
*
* @return true if the binding is pressed this tick and not pressed the previous tick
Expand All @@ -211,7 +202,7 @@
/**
* Equivalent to calling
* <pre><code>
* !binding.digitalNow() && binding.digitalPrev()

Check failure on line 205 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.1, false)

bad HTML entity

Check failure on line 205 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.1, false)

bad HTML entity

Check failure on line 205 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.2, false)

bad HTML entity

Check failure on line 205 in src/main/java/dev/isxander/controlify/api/bind/InputBinding.java

View workflow job for this annotation

GitHub Actions / Build with gradle (26.2, false)

bad HTML entity
* </code></pre>
*
* @return true if the binding is not pressed this tick and pressed the previous tick
Expand Down
Loading
Loading