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 @@ -150,6 +150,7 @@ manifests {
"controlify:default_config",
"controlify:controller_type",
"controlify:default_binds",
"controlify:trigger_effect",
)
))

Expand Down
3 changes: 2 additions & 1 deletion docs/developers/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"controlify-entrypoint.mdx": "Controlify Entrypoint",
"bindings-api.mdx": "Bindings API",
"screen-operation-api.mdx": "Screen Operation API",
"guide-api.mdx": "Guides API"
"guide-api.mdx": "Guides API",
"adaptive-trigger-api.mdx": "Adaptive Trigger API"
}
46 changes: 46 additions & 0 deletions docs/developers/adaptive-trigger-api.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: Adaptive Trigger API
---

Resource packs should be used for fixed adaptive trigger effects. The Java API is available for effects
that depend on the value stored in an item's data component.

Register effects during Controlify pre-initialization:

```java
public final class MyControlifyEntrypoint implements ControlifyEntrypoint {
@Override
public void onControlifyPreInit(PreInitContext context) {
TriggerEffectApi.registerUseItemEffect(
DataComponents.CHARGED_PROJECTILES,
projectiles -> projectiles.isEmpty()
? new DualsenseTriggerEffect.FeedbackSlope(
(byte) 2, (byte) 9, (byte) 5, (byte) 8
)
: new DualsenseTriggerEffect.Weapon(
(byte) 3, (byte) 5, (byte) 1
)
);
}
}
```

`TriggerEffectApi` has use-item and swing-item overloads for:

- A `DataComponentType<T>` and a `Function<? super T, DualsenseTriggerEffect>`, for effects computed from
the component value.
- A `DataComponentType<?>` and a constant `DualsenseTriggerEffect`.
- An `Item` and a constant `DualsenseTriggerEffect`.

Use `registerUseItemEffect` for the input bound to **Use Item** and `registerSwingItemEffect` for the input
bound to **Attack**.

## Priority

[Resource-pack rules](../resource-packs/adaptive-trigger-effects) are always evaluated before Java
registrations, allowing users and pack authors to override mod-provided effects. Among Java registrations,
component functions are checked in registration order before the exact-item fallback. Registering the same
component or item again replaces its previous effect.

Use a resource-pack `off` rule when an effect should be explicitly disabled. An `off` result returned from
Java also disables the trigger, but is only reached when no resource rule matched.
3 changes: 2 additions & 1 deletion docs/resource-packs/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"default-binds.mdx": "Default Binds",
"input-glyphs.mdx": "Input Glyphs",
"guides.mdx": "Button Guides",
"keyboard-layouts.mdx": "Keyboard Layouts"
"keyboard-layouts.mdx": "Keyboard Layouts",
"adaptive-trigger-effects.mdx": "Adaptive Trigger Effects"
}
96 changes: 96 additions & 0 deletions docs/resource-packs/adaptive-trigger-effects.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: Adaptive Trigger Effects
---

Controlify's DualSense adaptive trigger effects can be changed with a resource pack. Effects are applied
to the controller input bound to either **Use Item** or **Attack**.

## Creating trigger effect rules

Create either or both of these files in your resource pack:

- `assets/controlify/trigger_effect/use_item.json`
- `assets/controlify/trigger_effect/swing_item.json`

Each file contains an array of rules. A rule selects either one item with `forItem` or one data component
with `forComponent`, then defines the effect to apply. Exactly one selector must be present.

```json
[
{
"forItem": "minecraft:bow",
"effect": {
"type": "feedback_slope",
"start_position": 3,
"end_position": 9,
"start_strength": 2,
"end_strength": 8
}
},
{
"forComponent": "minecraft:consumable",
"effect": {
"type": "feedback",
"position": 3,
"strength": 1
}
}
]
```

Item rules match that exact item. Component rules match any item stack containing that data component.
Use-item rules check the player's active item and then their offhand item. Swing-item rules check the
main-hand item.

## Rule priority and stacking

The files are additive across resource packs. Rules in the highest-priority pack are checked first, followed
by lower-priority packs. Within each file, rules are checked from top to bottom. The first matching rule wins,
whether it uses `forItem` or `forComponent`.

To remove an effect supplied by Controlify or a lower-priority pack, add a higher-priority rule with an
`off` effect:

```json
[
{
"forItem": "minecraft:bow",
"effect": {
"type": "off"
}
}
]
```

A matching `off` rule stops evaluation, so rules from lower-priority packs and effects registered by mods
will not be used.

If a file has invalid JSON, an unknown item or component, an invalid effect, or a rule with both or neither
selector, Controlify logs the error and skips that entire file layer. Other resource packs continue to work.

## Effect formats

Trigger positions refer to the DualSense's ten trigger zones, numbered `0` through `9`. Strength and amplitude
values use `0` for off and `8` for maximum.

| Type | Fields |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------|
| `off` | No additional fields. |
| `feedback` | `position`: 0–9; `strength`: 0–8. Applies constant resistance from that position. |
| `weapon` | `start_position`: 2–7; `end_position`: greater than the start and at most 8; `strength`: 0–8. |
| `vibration` | `position`: 0–9; `amplitude`: 0–8; `frequency`: positive signed-byte frequency in hertz. |
| `feedback_multiple_position` | `strength`: exactly 10 values, each 0–8, corresponding to zones 0–9. |
| `feedback_slope` | `start_position`: 0–8; `end_position`: greater than the start and at most 9; `start_strength` and `end_strength`: 1–8. |
| `vibration_multiple_position` | `frequency`: positive signed-byte frequency in hertz; `amplitude`: exactly 10 values, each 0–8. |

For example, independent resistance in each trigger zone is written as:

```json
{
"type": "feedback_multiple_position",
"strength": [0, 0, 2, 2, 3, 4, 5, 6, 7, 8]
}
```

An amplitude or strength of zero disables that zone. Effects whose entire amplitude/strength is zero, or
whose vibration frequency is not positive, behave as `off`.
4 changes: 2 additions & 2 deletions docs/users/mod-comparison.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: Mod Comparison
quality of the mentioned features, and you should try them all out before making a decision. This table is also not
exhaustive, and there are many more features that are not listed here.**

| | Controlify (2.4.0) | [Midnight Controls (1.10.x)](https://github.com/TeamMidnightDust/MidnightControls) | [Controllable (0.23.x)](https://mrcrayfish.com/mods/controllable) | [Controller Support Mod (9.0.0)](https://github.com/Stereowalker/Controller-Support-Mod) |
| | Controlify (3.3.0) | [Midnight Controls (1.10.x)](https://github.com/TeamMidnightDust/MidnightControls) | [Controllable (0.23.x)](https://mrcrayfish.com/mods/controllable) | [Controller Support Mod (9.0.0)](https://github.com/Stereowalker/Controller-Support-Mod) |
|--------------------------------------------------|----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|-------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| **Mod Updates** | Frequent mod updates with new features, updates to the latest snapshots | Generally inactive. Updates to new versions quickly, last update Dec '24 | Maintained, last update June '23 | Very slow updates, last update Oct '24 |
| **Version support** | Maintains latest snapshot, 1.21.8, 1.21.5, 1.21.4, 1.21.3, 1.21.1 for both Fabric and NeoForge | Maintains 1.21.x | Maintains 1.21.4, 1.21.1 | Maintains 1.21.1 |
Expand All @@ -32,5 +32,5 @@ exhaustive, and there are many more features that are not listed here.**
| **On-screen keyboard** | ✅ Works everywhere, data-driven localised layouts | ⛔ | ⛔ | ✅ |
| **Radial action menu** | ✅ Many radial menus for things ranging from quick actions to gamemode switcher to hotbar layout save/load | ✅ | ✅ | ⛔ |
| **HD DualSense haptics** | ✅ Extensible by addon mods, only GUI navigation implemented in Controlify | ⛔ | ⛔ | ⛔ |
| **Adaptive DualSense triggers** | ✅ Extensible by addon mods, no functionality built-in | ⛔ | ⛔ | ⛔ |
| **Adaptive DualSense triggers** | ✅ | ⛔ | ⛔ | ⛔ |
| **Steam Deck support** (back buttons, gyro, etc) | ✅ Uses CEF debugger, requires Decky Loader | ⛔ | ⛔ | ⛔ |
12 changes: 10 additions & 2 deletions src/main/java/dev/isxander/controlify/Controlify.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import dev.isxander.controlify.controller.*;
import dev.isxander.controlify.controller.dualsense.BuiltinTriggerEffects;
import dev.isxander.controlify.controller.dualsense.TriggerEffectManager;
import dev.isxander.controlify.controller.dualsense.TriggerEffectRegistry;
import dev.isxander.controlify.controller.id.ControllerTypeManager;
import dev.isxander.controlify.controller.input.ControllerState;
import dev.isxander.controlify.controller.input.ControllerStateView;
Expand Down Expand Up @@ -97,6 +98,7 @@ public class Controlify implements ControlifyApi {
private DefaultConfigManager defaultConfigManager;
private ControllerTypeManager controllerTypeManager;
private KeyboardLayoutManager keyboardLayoutManager;
private TriggerEffectRegistry triggerEffectRegistry;

private TriggerEffectManager triggerEffectManager;

Expand Down Expand Up @@ -142,11 +144,14 @@ public void preInitialiseControlify() {
this.defaultConfigManager = new DefaultConfigManager();
this.controllerTypeManager = new ControllerTypeManager();
this.keyboardLayoutManager = new KeyboardLayoutManager();
this.triggerEffectRegistry = new TriggerEffectRegistry();
BuiltinTriggerEffects.register();
PlatformClientUtil.registerAssetReloadListener(inputFontMapper);
PlatformClientUtil.registerAssetReloadListener(defaultBindManager);
PlatformClientUtil.registerAssetReloadListener(defaultConfigManager);
PlatformClientUtil.registerAssetReloadListener(controllerTypeManager);
PlatformClientUtil.registerAssetReloadListener(keyboardLayoutManager);
PlatformClientUtil.registerAssetReloadListener(triggerEffectRegistry);
PlatformClientUtil.registerAssetReloadListener(GuideDomains.IN_GAME);
PlatformClientUtil.registerAssetReloadListener(GuideDomains.CONTAINER);

Expand Down Expand Up @@ -252,8 +257,7 @@ public void initializeControlify() {

this.inGameInputHandler = null; // set when the current controller changes
this.virtualMouseHandler = new VirtualMouseHandler();
this.triggerEffectManager = new TriggerEffectManager(this.minecraft);
BuiltinTriggerEffects.register();
this.triggerEffectManager = new TriggerEffectManager(this.minecraft, this.triggerEffectRegistry);

ControlifyEvents.CONTROLLER_CONNECTED.register(event -> this.onControllerAdded(
event.controller(), event.hotplugged()));
Expand Down Expand Up @@ -769,6 +773,10 @@ public TriggerEffectManager triggerEffectManager() {
return triggerEffectManager;
}

public TriggerEffectRegistry triggerEffectRegistry() {
return triggerEffectRegistry;
}

public Set<BindContext> thisTickBindContexts() {
return this.thisTickContexts;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
package dev.isxander.controlify.api.triggereffect;

import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.dualsense.TriggerEffectHolder;
import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.world.item.Item;
Expand All @@ -21,26 +20,26 @@ private TriggerEffectApi() {
}

public static <T> void registerUseItemEffect(DataComponentType<T> componentType, Function<? super T, @NotNull DualsenseTriggerEffect> effectFunction) {
Controlify.instance().triggerEffectManager().registerUseItemComponentEffect(componentType, effectFunction);
Controlify.instance().triggerEffectRegistry().registerUseItemComponentEffect(componentType, effectFunction);
}

public static void registerUseItemEffect(DataComponentType<?> componentType, @NotNull DualsenseTriggerEffect effect) {
registerUseItemEffect(componentType, _ -> effect);
}

public static <T> void registerSwingItemEffect(DataComponentType<T> componentType, Function<? super T, @NotNull DualsenseTriggerEffect> effectFunction) {
Controlify.instance().triggerEffectManager().registerSwingItemComponentEffect(componentType, effectFunction);
Controlify.instance().triggerEffectRegistry().registerSwingItemComponentEffect(componentType, effectFunction);
}

public static void registerSwingItemEffect(DataComponentType<?> componentType, @NotNull DualsenseTriggerEffect effect) {
registerSwingItemEffect(componentType, _ -> effect);
}

public static void registerUseItemEffect(Item item, @NotNull DualsenseTriggerEffect effect) {
((TriggerEffectHolder) item).controlify$assignUseTriggerEffect(effect);
Controlify.instance().triggerEffectRegistry().registerUseItemEffect(item, effect);
}

public static void registerSwingItemEffect(Item item, @NotNull DualsenseTriggerEffect effect) {
((TriggerEffectHolder) item).controlify$assignSwingTriggerEffect(effect);
Controlify.instance().triggerEffectRegistry().registerSwingItemEffect(item, effect);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import dev.isxander.controlify.api.triggereffect.TriggerEffectApi;
import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.item.Items;

public final class BuiltinTriggerEffects {
private BuiltinTriggerEffects() {
Expand All @@ -18,46 +17,11 @@ private BuiltinTriggerEffects() {
public static void register() {
var quickClick = new DualsenseTriggerEffect.Weapon((byte) 3, (byte) 5, (byte) 1);

TriggerEffectApi.registerUseItemEffect(
Items.BOW,
new DualsenseTriggerEffect.FeedbackSlope((byte) 3, (byte) 9, (byte) 2, (byte) 8)
);

TriggerEffectApi.registerSwingItemEffect(
DataComponents.WEAPON,
quickClick
);

TriggerEffectApi.registerUseItemEffect(
DataComponents.CHARGED_PROJECTILES,
chargedProjectiles -> chargedProjectiles.isEmpty()
? new DualsenseTriggerEffect.FeedbackSlope((byte) 2, (byte) 9, (byte) 5, (byte) 8)
: quickClick
);

TriggerEffectApi.registerUseItemEffect(
DataComponents.CONSUMABLE,
new DualsenseTriggerEffect.Feedback((byte) 3, (byte) 1)
);

TriggerEffectApi.registerUseItemEffect(
DataComponents.BLOCKS_ATTACKS,
new DualsenseTriggerEffect.Feedback((byte) 3, (byte) 3)
);

TriggerEffectApi.registerUseItemEffect(
DataComponents.EQUIPPABLE,
quickClick
);

TriggerEffectApi.registerUseItemEffect(
DataComponents.KINETIC_WEAPON,
new DualsenseTriggerEffect.Feedback((byte) 3, (byte) 3)
);

TriggerEffectApi.registerUseItemEffect(
DataComponents.INSTRUMENT,
new DualsenseTriggerEffect.Feedback((byte) 3, (byte) 3)
);
}
}

This file was deleted.

Loading
Loading