diff --git a/docs/developers/adaptive-trigger-api.mdx b/docs/developers/adaptive-trigger-api.mdx index d66d3687e..eb0f48111 100644 --- a/docs/developers/adaptive-trigger-api.mdx +++ b/docs/developers/adaptive-trigger-api.mdx @@ -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: @@ -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) + ) ); } } diff --git a/docs/resource-packs/adaptive-trigger-effects.mdx b/docs/resource-packs/adaptive-trigger-effects.mdx index 4619a061a..dbaff364e 100644 --- a/docs/resource-packs/adaptive-trigger-effects.mdx +++ b/docs/resource-packs/adaptive-trigger-effects.mdx @@ -12,41 +12,133 @@ 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: @@ -54,7 +146,9 @@ To remove an effect supplied by Controlify or a lower-priority pack, add a highe ```json [ { - "forItem": "minecraft:bow", + "when": { + "items": "minecraft:bow" + }, "effect": { "type": "off" } @@ -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 diff --git a/src/fabric/java/dev/isxander/controlify/fabric/platform/client/FabricPlatformClientImpl.java b/src/fabric/java/dev/isxander/controlify/fabric/platform/client/FabricPlatformClientImpl.java index 8e5ff2bff..ea4fc5ead 100644 --- a/src/fabric/java/dev/isxander/controlify/fabric/platform/client/FabricPlatformClientImpl.java +++ b/src/fabric/java/dev/isxander/controlify/fabric/platform/client/FabricPlatformClientImpl.java @@ -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; @@ -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); diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 0e4b64e70..906131cec 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -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; @@ -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); @@ -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)))); diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java deleted file mode 100644 index 10b7593a1..000000000 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2026 isXander - * This file is part of Controlify. - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ -package dev.isxander.controlify.controller.dualsense; - -import dev.isxander.controlify.api.triggereffect.TriggerEffectApi; -import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; -import net.minecraft.core.component.DataComponents; - -public final class BuiltinTriggerEffects { - private BuiltinTriggerEffects() { - } - - public static void register() { - var quickClick = new DualsenseTriggerEffect.Weapon((byte) 3, (byte) 5, (byte) 1); - - TriggerEffectApi.registerUseItemEffect( - DataComponents.CHARGED_PROJECTILES, - chargedProjectiles -> chargedProjectiles.isEmpty() - ? new DualsenseTriggerEffect.FeedbackSlope((byte) 2, (byte) 9, (byte) 5, (byte) 8) - : quickClick - ); - } -} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java index d22d2fc37..14a75ae7f 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectRegistry.java @@ -10,17 +10,28 @@ import com.google.gson.JsonParser; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.platform.client.resource.SimpleControlifyReloadListener; import dev.isxander.controlify.utils.CUtil; +//? if >=26.2 { +import net.minecraft.advancements.predicates.ItemPredicate; +//?} else { +/*import net.minecraft.advancements.criterion.ItemPredicate; +*///?} +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.CacheSlot; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.RegistryAccess; import net.minecraft.core.component.DataComponentType; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.PlaceholderLookupProvider; +import net.minecraft.util.RegistryContextSwapper; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.Nullable; @@ -32,6 +43,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Function; @@ -43,19 +55,19 @@ public class TriggerEffectRegistry implements SimpleControlifyReloadListener SERIALIZED_RULE_CODEC = RecordCodecBuilder.create(instance -> instance.group( - Identifier.CODEC.optionalFieldOf("forItem").forGetter(SerializedRule::forItem), - Identifier.CODEC.optionalFieldOf("forComponent").forGetter(SerializedRule::forComponent), - TriggerEffectCodecs.CODEC.fieldOf("effect").forGetter(SerializedRule::effect) - ).apply(instance, SerializedRule::new)).comapFlatMap( - rule -> rule.forItem().isPresent() == rule.forComponent().isPresent() - ? DataResult.error(() -> "Exactly one of 'forItem' and 'forComponent' must be specified") - : DataResult.success(rule), - Function.identity() - ); + private static final Codec RULE_CODEC = + RecordCodecBuilder.create(instance -> instance.group( + ItemPredicate.CODEC.fieldOf("when").forGetter(Rule::when), + TriggerEffectCodecs.CODEC.fieldOf("effect").forGetter(Rule::effect) + ).apply(instance, Rule::new)); + private static final Codec> RULES_CODEC = RULE_CODEC.listOf(); - private List useItemResourceRules = List.of(); - private List swingItemResourceRules = List.of(); + private List useItemResourceLayers = List.of(); + private List swingItemResourceLayers = List.of(); + private final CacheSlot resolvedRules = new CacheSlot<>(this::resolveResourceRules); + + private final Map useItemResourceCache = new WeakHashMap<>(); + private final Map swingItemResourceCache = new WeakHashMap<>(); private final Map, Function> useItemComponentEffects = new LinkedHashMap<>(); private final Map, Function> swingItemComponentEffects = new LinkedHashMap<>(); @@ -70,21 +82,22 @@ public CompletableFuture load(ResourceManager manager, Executor ex ), executor); } - private List loadResourceStack(ResourceManager manager, Identifier id) { - List rules = new ArrayList<>(); + private List loadResourceStack(ResourceManager manager, Identifier id) { + List layers = new ArrayList<>(); for (Resource resource : manager.getResourceStack(id).reversed()) { try (BufferedReader reader = resource.openAsReader()) { + PlaceholderLookupProvider lookup = new PlaceholderLookupProvider(RegistryAccess.EMPTY); + DynamicOps ops = lookup.createSerializationContext(JsonOps.INSTANCE); JsonElement json = JsonParser.parseReader(reader); - List serializedRules = SERIALIZED_RULE_CODEC.listOf() - .parse(JsonOps.INSTANCE, json) + List layerRules = RULES_CODEC + .parse(ops, json) .getOrThrow(); - - List layerRules = new ArrayList<>(serializedRules.size()); - for (SerializedRule serializedRule : serializedRules) { - layerRules.add(this.resolveRule(serializedRule)); - } - rules.addAll(layerRules); + layers.add(new PreparedLayer( + List.copyOf(layerRules), + resource.sourcePackId(), + lookup.hasRegisteredPlaceholders() ? lookup.createSwapper() : null + )); } catch (Exception e) { LOGGER.error( "Failed to load adaptive trigger effects from {} in pack {}; skipping this layer", @@ -95,36 +108,19 @@ private List loadResourceStack(ResourceManager manager, Identifier id) { } } - return List.copyOf(rules); - } - - private Rule resolveRule(SerializedRule rule) { - if (rule.forItem().isPresent()) { - Identifier itemId = rule.forItem().orElseThrow(); - if (!BuiltInRegistries.ITEM.containsKey(itemId)) { - throw new IllegalArgumentException("Unknown item '" + itemId + "'"); - } - Item item = BuiltInRegistries.ITEM.getValue(itemId); - return new ItemRule(item, rule.effect()); - } - - Identifier componentId = rule.forComponent().orElseThrow(); - if (!BuiltInRegistries.DATA_COMPONENT_TYPE.containsKey(componentId)) { - throw new IllegalArgumentException("Unknown data component type '" + componentId + "'"); - } - DataComponentType componentType = BuiltInRegistries.DATA_COMPONENT_TYPE.getValue(componentId); - return new ComponentRule(componentType, rule.effect()); + return List.copyOf(layers); } @Override public CompletableFuture apply(Preparations data, ResourceManager manager, Executor executor) { return CompletableFuture.runAsync(() -> { - this.useItemResourceRules = data.useItemRules; - this.swingItemResourceRules = data.swingItemRules; + this.useItemResourceLayers = data.useItemLayers; + this.swingItemResourceLayers = data.swingItemLayers; + this.invalidateResolvedResourceRules(); LOGGER.info( - "Loaded {} use-item and {} swing-item adaptive trigger effect rules", - this.useItemResourceRules.size(), - this.swingItemResourceRules.size() + "Loaded {} use-item and {} swing-item adaptive trigger effect rule layers", + this.useItemResourceLayers.size(), + this.swingItemResourceLayers.size() ); }, executor); } @@ -152,33 +148,37 @@ public void registerSwingItemEffect(Item item, DualsenseTriggerEffect effect) { } public Optional getUseItemEffect(ItemStack stack) { + ResolvedRules rules = this.currentResourceRules(); return findEffect( stack, - this.useItemResourceRules, + rules.useItemRules(), + this.useItemResourceCache, this.useItemComponentEffects, this.useItemEffects ); } public Optional getSwingItemEffect(ItemStack stack) { + ResolvedRules rules = this.currentResourceRules(); return findEffect( stack, - this.swingItemResourceRules, + rules.swingItemRules(), + this.swingItemResourceCache, this.swingItemComponentEffects, this.swingItemEffects ); } - private static Optional findEffect( + private Optional findEffect( ItemStack stack, List resourceRules, + Map resourceCache, Map, Function> componentEffects, Map itemEffects ) { - for (Rule rule : resourceRules) { - if (rule.matches(stack)) { - return Optional.of(rule.effect()); - } + Optional resourceEffect = this.findResourceEffect(stack, resourceRules, resourceCache); + if (resourceEffect.isPresent()) { + return resourceEffect; } for (var entry : componentEffects.entrySet()) { @@ -193,6 +193,78 @@ private static Optional findEffect( return Optional.ofNullable(itemEffects.get(stack.getItem())); } + private Optional findResourceEffect( + ItemStack stack, + List rules, + Map cache + ) { + CachedResourceMatch cached = cache.get(stack); + if (cached == null || !ItemStack.matches(stack, cached.snapshot())) { + Optional effect = rules.stream() + .filter(rule -> rule.matches(stack)) + .map(Rule::effect) + .findFirst(); + cached = new CachedResourceMatch(stack.copy(), effect); + cache.put(stack, cached); + } + + return cached.effect(); + } + + private ResolvedRules currentResourceRules() { + ClientLevel level = Minecraft.getInstance().level; + return level == null ? ResolvedRules.EMPTY : this.resolvedRules.compute(level); + } + + private ResolvedRules resolveResourceRules(ClientLevel level) { + HolderLookup.Provider registries = level.registryAccess(); + List useItemRules = this.resolveResourceLayers(this.useItemResourceLayers, USE_ITEM_RESOURCE, registries); + List swingItemRules = this.resolveResourceLayers(this.swingItemResourceLayers, SWING_ITEM_RESOURCE, registries); + this.invalidateResourceRuleCaches(); + LOGGER.info( + "Resolved {} use-item and {} swing-item adaptive trigger effect rules for the current world", + useItemRules.size(), + swingItemRules.size() + ); + return new ResolvedRules(useItemRules, swingItemRules); + } + + private List resolveResourceLayers( + List layers, + Identifier id, + HolderLookup.Provider registries + ) { + List rules = new ArrayList<>(); + + for (PreparedLayer layer : layers) { + try { + List resolvedRules = layer.registrySwapper() == null + ? layer.rules() + : layer.registrySwapper().swapTo(RULES_CODEC, layer.rules(), registries).getOrThrow(); + rules.addAll(resolvedRules); + } catch (Exception e) { + LOGGER.error( + "Failed to resolve adaptive trigger effects from {} in pack {} for the current world; skipping this layer", + id, + layer.sourcePackId(), + e + ); + } + } + + return List.copyOf(rules); + } + + public void invalidateResolvedResourceRules() { + this.resolvedRules.clear(); + this.invalidateResourceRuleCaches(); + } + + public void invalidateResourceRuleCaches() { + this.useItemResourceCache.clear(); + this.swingItemResourceCache.clear(); + } + @SuppressWarnings("unchecked") private static Function castEffectFunction( Function effectFunction @@ -205,43 +277,42 @@ public Identifier getReloadId() { return RELOAD_ID; } - private record SerializedRule( - Optional forItem, - Optional forComponent, + private record Rule( + ItemPredicate when, DualsenseTriggerEffect effect ) { + public boolean matches(ItemStack stack) { + return this.when.test(stack); + } } - private sealed interface Rule permits ItemRule, ComponentRule { - boolean matches(ItemStack stack); - - DualsenseTriggerEffect effect(); + private record PreparedLayer( + List rules, + String sourcePackId, + @Nullable RegistryContextSwapper registrySwapper + ) { } - private record ItemRule(Item item, DualsenseTriggerEffect effect) implements Rule { - @Override - public boolean matches(ItemStack stack) { - return stack.getItem() == this.item; - } + private record ResolvedRules( + List useItemRules, + List swingItemRules + ) { + private static final ResolvedRules EMPTY = new ResolvedRules(List.of(), List.of()); } - private record ComponentRule( - DataComponentType componentType, - DualsenseTriggerEffect effect - ) implements Rule { - @Override - public boolean matches(ItemStack stack) { - return stack.has(this.componentType); - } + private record CachedResourceMatch( + ItemStack snapshot, + Optional effect + ) { } public static final class Preparations { - private final List useItemRules; - private final List swingItemRules; + private final List useItemLayers; + private final List swingItemLayers; - private Preparations(List useItemRules, List swingItemRules) { - this.useItemRules = useItemRules; - this.swingItemRules = swingItemRules; + private Preparations(List useItemLayers, List swingItemLayers) { + this.useItemLayers = useItemLayers; + this.swingItemLayers = swingItemLayers; } } } diff --git a/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtil.java b/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtil.java index 521afebee..11fb9b456 100644 --- a/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtil.java +++ b/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtil.java @@ -44,6 +44,10 @@ public static void registerClientDisconnected(DisconnectedEvent event) { IMPL.registerClientDisconnected(event); } + public static void registerClientTagsUpdated(LifecycleEvent event) { + IMPL.registerClientTagsUpdated(event); + } + public static void registerAssetReloadListener(ControlifyReloadListener reloadListener) { IMPL.registerAssetReloadListener(reloadListener); } diff --git a/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtilImpl.java b/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtilImpl.java index 73e90a90b..8441641cc 100644 --- a/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtilImpl.java +++ b/src/main/java/dev/isxander/controlify/platform/client/PlatformClientUtilImpl.java @@ -31,6 +31,8 @@ public interface PlatformClientUtilImpl { void registerClientDisconnected(DisconnectedEvent event); + void registerClientTagsUpdated(LifecycleEvent event); + void registerAssetReloadListener(ControlifyReloadListener reloadListener); void registerBuiltinResourcePack(Identifier id, Component displayName); diff --git a/src/main/resources/assets/controlify/trigger_effect/swing_item.json b/src/main/resources/assets/controlify/trigger_effect/swing_item.json index 55d109da8..76d92c043 100644 --- a/src/main/resources/assets/controlify/trigger_effect/swing_item.json +++ b/src/main/resources/assets/controlify/trigger_effect/swing_item.json @@ -1,6 +1,10 @@ [ { - "forComponent": "minecraft:weapon", + "when": { + "predicates": { + "minecraft:weapon": {} + } + }, "effect": { "type": "weapon", "start_position": 3, diff --git a/src/main/resources/assets/controlify/trigger_effect/use_item.json b/src/main/resources/assets/controlify/trigger_effect/use_item.json index 8172e5408..b284fa995 100644 --- a/src/main/resources/assets/controlify/trigger_effect/use_item.json +++ b/src/main/resources/assets/controlify/trigger_effect/use_item.json @@ -1,6 +1,8 @@ [ { - "forItem": "minecraft:bow", + "when": { + "items": "minecraft:bow" + }, "effect": { "type": "feedback_slope", "start_position": 3, @@ -10,7 +12,38 @@ } }, { - "forComponent": "minecraft:consumable", + "when": { + "components": { + "minecraft:charged_projectiles": [] + } + }, + "effect": { + "type": "feedback_slope", + "start_position": 2, + "end_position": 9, + "start_strength": 5, + "end_strength": 8 + } + }, + { + "when": { + "predicates": { + "minecraft:charged_projectiles": {} + } + }, + "effect": { + "type": "weapon", + "start_position": 3, + "end_position": 5, + "strength": 1 + } + }, + { + "when": { + "predicates": { + "minecraft:consumable": {} + } + }, "effect": { "type": "feedback", "position": 3, @@ -18,7 +51,11 @@ } }, { - "forComponent": "minecraft:blocks_attacks", + "when": { + "predicates": { + "minecraft:blocks_attacks": {} + } + }, "effect": { "type": "feedback", "position": 3, @@ -26,7 +63,11 @@ } }, { - "forComponent": "minecraft:equippable", + "when": { + "predicates": { + "minecraft:equippable": {} + } + }, "effect": { "type": "weapon", "start_position": 3, @@ -35,7 +76,11 @@ } }, { - "forComponent": "minecraft:kinetic_weapon", + "when": { + "predicates": { + "minecraft:kinetic_weapon": {} + } + }, "effect": { "type": "feedback", "position": 3, @@ -43,7 +88,11 @@ } }, { - "forComponent": "minecraft:instrument", + "when": { + "predicates": { + "minecraft:instrument": {} + } + }, "effect": { "type": "feedback", "position": 3, diff --git a/src/neoforge/java/dev/isxander/controlify/neoforge/platform/client/NeoforgePlatformClientImpl.java b/src/neoforge/java/dev/isxander/controlify/neoforge/platform/client/NeoforgePlatformClientImpl.java index 4b1eeed78..131950fa4 100644 --- a/src/neoforge/java/dev/isxander/controlify/neoforge/platform/client/NeoforgePlatformClientImpl.java +++ b/src/neoforge/java/dev/isxander/controlify/neoforge/platform/client/NeoforgePlatformClientImpl.java @@ -35,6 +35,7 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.AddPackFindersEvent; import net.neoforged.neoforge.event.GameShuttingDownEvent; +import net.neoforged.neoforge.event.TagsUpdatedEvent; import org.jetbrains.annotations.Nullable; import java.util.Arrays; @@ -73,6 +74,13 @@ public void registerClientDisconnected(DisconnectedEvent event) { }); } + @Override + public void registerClientTagsUpdated(LifecycleEvent event) { + NeoForge.EVENT_BUS.addListener(e -> { + event.onLifecycle(Minecraft.getInstance()); + }); + } + @Override public void registerAssetReloadListener(ControlifyReloadListener reloadListener) { getModEventBus().addListener(e -> {