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
16 changes: 7 additions & 9 deletions docs/developers/adaptive-trigger-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
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.
Resource packs should be used for adaptive trigger effects based on item identities, tags, or serialized
data-component values. The Java API is available when an effect needs custom code or cannot be expressed
with resource-pack matching.

Register effects during Controlify pre-initialization:

Expand All @@ -13,13 +14,10 @@ public final class MyControlifyEntrypoint implements ControlifyEntrypoint {
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
)
projectiles -> new DualsenseTriggerEffect.Feedback(
(byte) 3,
(byte) Math.min(8, projectiles.items().size() + 1)
)
);
}
}
Expand Down
220 changes: 196 additions & 24 deletions docs/resource-packs/adaptive-trigger-effects.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,143 @@ 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.
Each file contains an array of rules. Every rule has a `when` item predicate and the `effect` to apply.
The `when` object uses Minecraft's standard
[item predicate format](https://minecraft.wiki/w/Template:Nbt_inherit/conditions/item/template). When
multiple conditions are present, all of them must match.

For example, a bow can increase resistance as it is pulled:

```json
[
{
"forItem": "minecraft:bow",
"effect": {
"type": "feedback_slope",
"start_position": 3,
"end_position": 9,
"start_strength": 2,
"end_strength": 8
{
"when": {
"items": "minecraft:bow"
},
"effect": {
"type": "feedback_slope",
"start_position": 3,
"end_position": 9,
"start_strength": 2,
"end_strength": 8
}
}
```

Component presence can give every weapon the same trigger stop:

```json
{
"when": {
"predicates": {
"minecraft:weapon": {}
}
},
{
"forComponent": "minecraft:consumable",
"effect": {
"type": "feedback",
"position": 3,
"strength": 1
"effect": {
"type": "weapon",
"start_position": 3,
"end_position": 5,
"strength": 1
}
}
```

Item tags and component conditions can be combined:

```json
{
"when": {
"items": "#minecraft:fox_food",
"predicates": {
"minecraft:consumable": {}
}
},
"effect": {
"type": "feedback",
"position": 3,
"strength": 1
}
]
}
```

Stack counts can select a vibration effect:

```json
{
"when": {
"items": "minecraft:snowball",
"count": {
"min": 8
}
},
"effect": {
"type": "vibration",
"position": 3,
"amplitude": 4,
"frequency": 30
}
}
```

Semantic component predicates can select a multi-zone effect:

```json
{
"when": {
"items": "minecraft:shield",
"predicates": {
"minecraft:damage": {
"damage": {
"min": 1
}
}
}
},
"effect": {
"type": "feedback_multiple_position",
"strength": [0, 0, 2, 2, 3, 4, 5, 6, 7, 8]
}
}
```

Exact component values can distinguish an unloaded crossbow:

```json
{
"when": {
"items": "minecraft:crossbow",
"components": {
"minecraft:charged_projectiles": []
}
},
"effect": {
"type": "feedback_slope",
"start_position": 2,
"end_position": 9,
"start_strength": 5,
"end_strength": 8
}
}
```

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`.
by lower-priority packs. Within each file, rules are checked from top to bottom. The first matching rule wins.

An empty `when` object matches every item and can be used as a final catch-all rule.

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",
"when": {
"items": "minecraft:bow"
},
"effect": {
"type": "off"
}
Expand All @@ -65,8 +159,86 @@ To remove an effect supplied by Controlify or a lower-priority pack, add a highe
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.
If a file has invalid JSON, an unknown item, tag, component, or predicate, an invalid component or predicate
value, or an invalid effect, Controlify logs the error and skips that entire file layer. Other resource packs
continue to work.

## Using trigger effects from a server

A server can include trigger effect files in the resource pack it sends to players. The server itself does
not need Controlify: clients with Controlify apply the rules, while other clients ignore the additional
assets. This is useful for giving a server's custom items their own trigger effects.

### Matching an item tag

For example, a server datapack can define
`data/example/tags/item/heavy_weapons.json`:

```json
{
"values": [
"minecraft:mace",
"minecraft:netherite_axe"
]
}
```

The server resource pack can then contain
`assets/controlify/trigger_effect/swing_item.json`:

```json
[
{
"when": {
"items": "#example:heavy_weapons"
},
"effect": {
"type": "weapon",
"start_position": 2,
"end_position": 8,
"strength": 6
}
}
]
```

The item tag is synchronized to clients when they join and whenever the server reloads its datapacks, so the
resource-pack rule follows changes made with `/reload`.

### Matching custom item data

Items which share a vanilla item type can instead be distinguished by their `minecraft:custom_data`
component. For example, a server can create a custom heavy weapon with:

```mcfunction
/give @s minecraft:carrot_on_a_stick[minecraft:custom_data={example:{trigger_effect:"heavy"}}]
```

Its server resource pack can match that data with the `minecraft:custom_data` component predicate:

```json
[
{
"when": {
"items": "minecraft:carrot_on_a_stick",
"predicates": {
"minecraft:custom_data": {
"example": {
"trigger_effect": "heavy"
}
}
}
},
"effect": {
"type": "feedback_multiple_position",
"strength": [0, 0, 2, 3, 4, 5, 6, 7, 8, 8]
}
}
]
```

The custom-data predicate is a partial match. The item may contain other custom data in addition to the
fields in the rule.

## Effect formats

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.CommonLifecycleEvents;
import net.fabricmc.fabric.api.networking.v1.FriendlyByteBufs;
import net.fabricmc.fabric.api.resource.v1.ResourceLoader;
import net.fabricmc.fabric.api.resource.v1.pack.PackActivationType;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
Expand Down Expand Up @@ -64,6 +66,15 @@ public void registerClientDisconnected(DisconnectedEvent event) {
});
}

@Override
public void registerClientTagsUpdated(LifecycleEvent event) {
CommonLifecycleEvents.TAGS_LOADED.register((registries, client) -> {
if (client) {
event.onLifecycle(Minecraft.getInstance());
}
});
}

@Override
public void registerAssetReloadListener(ControlifyReloadListener reloadListener) {
ResourceLoader.get(PackType.CLIENT_RESOURCES).registerReloadListener(reloadListener.getReloadId(), reloadListener);
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/dev/isxander/controlify/Controlify.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import dev.isxander.controlify.config.settings.device.DeviceSettings;
import dev.isxander.controlify.config.settings.profile.ProfileSettings;
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;
Expand Down Expand Up @@ -145,7 +144,6 @@ public void preInitialiseControlify() {
this.controllerTypeManager = new ControllerTypeManager();
this.keyboardLayoutManager = new KeyboardLayoutManager();
this.triggerEffectRegistry = new TriggerEffectRegistry();
BuiltinTriggerEffects.register();
PlatformClientUtil.registerAssetReloadListener(inputFontMapper);
PlatformClientUtil.registerAssetReloadListener(defaultBindManager);
PlatformClientUtil.registerAssetReloadListener(defaultConfigManager);
Expand Down Expand Up @@ -193,6 +191,7 @@ public void preInitialiseControlify() {
DebugLog.log("Disconnected from server, resetting server policies");
ServerPolicies.unsetAll();
});
PlatformClientUtil.registerClientTagsUpdated(client -> triggerEffectRegistry.invalidateResolvedResourceRules());

PlatformClientUtil.addHudLayer(CUtil.rl("button_guide"), (graphics, deltaTracker) ->
inGameButtonGuide().ifPresent(guide -> guide.extractRenderState(graphics, deltaTracker.getGameTimeDeltaPartialTick(false))));
Expand Down

This file was deleted.

Loading
Loading