From 9b1342839936a581e32c09fede81b3af876a15f4 Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 17:31:00 +0100 Subject: [PATCH 1/8] adaptive triggers --- .../dev/isxander/controlify/Controlify.java | 19 ++- .../api/buttonguide/ButtonGuideApi.java | 4 + .../api/triggereffect/TriggerEffectApi.java | 40 +++++ .../config/dto/profile/DualSenseConfig.java | 12 ++ .../config/dto/profile/ProfileConfig.java | 6 +- .../settings/profile/DualSenseSettings.java | 19 +++ .../settings/profile/ProfileSettings.java | 11 +- .../dualsense/BuiltinTriggerEffects.java | 42 +++++ .../dualsense/DualSenseComponent.java | 21 ++- .../dualsense/TriggerEffectCodecs.java | 130 +++++++++++++++ .../dualsense/TriggerEffectHolder.java | 16 ++ .../dualsense/TriggerEffectManager.java | 155 ++++++++++++++++++ .../{sdl => }/dualsense/DS5EffectsState.java | 2 +- .../dualsense/DualsenseTriggerEffect.java | 6 +- .../DualsenseTriggerEffectTypes.java | 2 +- .../driver/sdl/SDLCommonDriver.java | 8 +- .../feature/triggereffect/ItemMixin.java | 40 +++++ .../controlify/utils/codec/CExtraCodecs.java | 22 +++ .../controlify/utils/codec/FuzzyMapCodec.java | 2 +- .../controllers/default_config.json | 3 + src/main/resources/controlify.mixins.json | 3 + 21 files changed, 543 insertions(+), 20 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java create mode 100644 src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java create mode 100644 src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java create mode 100644 src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java create mode 100644 src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java create mode 100644 src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java create mode 100644 src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java rename src/main/java/dev/isxander/controlify/driver/{sdl => }/dualsense/DS5EffectsState.java (99%) rename src/main/java/dev/isxander/controlify/driver/{sdl => }/dualsense/DualsenseTriggerEffect.java (98%) rename src/main/java/dev/isxander/controlify/driver/{sdl => }/dualsense/DualsenseTriggerEffectTypes.java (96%) create mode 100644 src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 9a87ca18e..b37bd7451 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -12,6 +12,7 @@ import dev.isxander.controlify.api.entrypoint.InitContext; import dev.isxander.controlify.api.entrypoint.PreInitContext; import dev.isxander.controlify.api.guide.*; +import dev.isxander.controlify.api.triggereffect.TriggerEffectApi; import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.bindings.ControlifyBindApiImpl; import dev.isxander.controlify.bindings.ControlifyBindings; @@ -22,6 +23,8 @@ 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.id.ControllerTypeManager; import dev.isxander.controlify.controller.input.ControllerState; import dev.isxander.controlify.controller.input.ControllerStateView; @@ -29,6 +32,7 @@ import dev.isxander.controlify.controller.rumble.RumbleComponent; import dev.isxander.controlify.controllermanager.ControllerManager; import dev.isxander.controlify.controllermanager.SDLControllerManager; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.driver.sdl.SDLNativesLoader; import dev.isxander.controlify.driver.steamdeck.SteamDeckMode; import dev.isxander.controlify.driver.steamdeck.SteamDeckUtil; @@ -62,6 +66,7 @@ import net.minecraft.client.multiplayer.ServerData; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Items; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; @@ -94,6 +99,8 @@ public class Controlify implements ControlifyApi { private ControllerTypeManager controllerTypeManager; private KeyboardLayoutManager keyboardLayoutManager; + private TriggerEffectManager triggerEffectManager; + private Set thisTickContexts; private ConfigManager config; @@ -242,10 +249,12 @@ public void initializeControlify() { CUtil.LOGGER.log("Initializing Controlify..."); this.minecraft = Minecraft.getInstance(); + config().loadOrDefault(); + this.inGameInputHandler = null; // set when the current controller changes this.virtualMouseHandler = new VirtualMouseHandler(); - - config().loadOrDefault(); + this.triggerEffectManager = new TriggerEffectManager(this.minecraft); + BuiltinTriggerEffects.register(); ControlifyEvents.CONTROLLER_CONNECTED.register(event -> this.onControllerAdded( event.controller(), event.hotplugged())); @@ -520,6 +529,8 @@ private void tickActiveController(ControllerEntity controller, boolean outOfFocu rumbleManager.ifPresent(RumbleManager::tick); } + triggerEffectManager.applyTriggerEffects(controller); + if (state.isGivingInput()) { minecraft.getFramerateLimitTracker().onInputReceived(); @@ -754,6 +765,10 @@ public KeyboardLayoutManager keyboardLayoutManager() { return keyboardLayoutManager; } + public TriggerEffectManager triggerEffectManager() { + return triggerEffectManager; + } + public Set thisTickBindContexts() { return this.thisTickContexts; } diff --git a/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuideApi.java b/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuideApi.java index 11cf378a8..ab2bca95c 100644 --- a/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuideApi.java +++ b/src/main/java/dev/isxander/controlify/api/buttonguide/ButtonGuideApi.java @@ -18,6 +18,10 @@ * This should be called every time a button is initialised, like in {@link Screen#init()} */ public final class ButtonGuideApi { + + private ButtonGuideApi() { + } + /** * Makes the button render the image of the binding specified. * This does not invoke the button press on binding trigger, only renders the guide. diff --git a/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java b/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java new file mode 100644 index 000000000..2f4e04b82 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java @@ -0,0 +1,40 @@ +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; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +public final class TriggerEffectApi { + + private TriggerEffectApi() { + } + + public static void registerUseItemEffect(DataComponentType componentType, Function effectFunction) { + Controlify.instance().triggerEffectManager().registerUseItemComponentEffect(componentType, effectFunction); + } + + public static void registerUseItemEffect(DataComponentType componentType, @NotNull DualsenseTriggerEffect effect) { + registerUseItemEffect(componentType, _ -> effect); + } + + public static void registerSwingItemEffect(DataComponentType componentType, Function effectFunction) { + Controlify.instance().triggerEffectManager().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); + } + + public static void registerSwingItemEffect(Item item, @NotNull DualsenseTriggerEffect effect) { + ((TriggerEffectHolder) item).controlify$assignSwingTriggerEffect(effect); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java b/src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java new file mode 100644 index 000000000..352d6411f --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java @@ -0,0 +1,12 @@ +package dev.isxander.controlify.config.dto.profile; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public record DualSenseConfig( + boolean triggerEffects +) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.BOOL.fieldOf("trigger_effects").forGetter(DualSenseConfig::triggerEffects) + ).apply(instance, DualSenseConfig::new)); +} diff --git a/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java b/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java index e4d76c18e..660240004 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java +++ b/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java @@ -19,7 +19,8 @@ public record ProfileConfig( RumbleConfig rumble, HDHapticConfig hdHaptic, GyroConfig gyro, - BluetoothDeviceConfig bluetoothDevice + BluetoothDeviceConfig bluetoothDevice, + DualSenseConfig dualsense ) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.STRING.optionalFieldOf("name").forGetter(ProfileConfig::name), @@ -29,6 +30,7 @@ public record ProfileConfig( RumbleConfig.CODEC.fieldOf("rumble").forGetter(ProfileConfig::rumble), HDHapticConfig.CODEC.fieldOf("hd_haptic").forGetter(ProfileConfig::hdHaptic), GyroConfig.CODEC.fieldOf("gyro").forGetter(ProfileConfig::gyro), - BluetoothDeviceConfig.CODEC.fieldOf("bluetooth_device").forGetter(ProfileConfig::bluetoothDevice) + BluetoothDeviceConfig.CODEC.fieldOf("bluetooth_device").forGetter(ProfileConfig::bluetoothDevice), + DualSenseConfig.CODEC.fieldOf("dualsense").forGetter(ProfileConfig::dualsense) ).apply(instance, ProfileConfig::new)); } diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java new file mode 100644 index 000000000..bc8cb5795 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java @@ -0,0 +1,19 @@ +package dev.isxander.controlify.config.settings.profile; + +import dev.isxander.controlify.config.dto.profile.DualSenseConfig; + +public class DualSenseSettings { + public boolean triggerEffects; + + public DualSenseSettings(boolean triggerEffects) { + this.triggerEffects = triggerEffects; + } + + public static DualSenseSettings fromDTO(DualSenseConfig dto) { + return new DualSenseSettings(dto.triggerEffects()); + } + + public DualSenseConfig toDTO() { + return new DualSenseConfig(triggerEffects); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java index 4d5966008..a2105043e 100644 --- a/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java +++ b/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java @@ -21,6 +21,7 @@ public class ProfileSettings { public final HDHapticSettings hdHaptic; public final GyroSettings gyro; public final BluetoothDeviceSettings bluetoothDevice; + public final DualSenseSettings dualsense; public ProfileSettings( @Nullable String name, @@ -30,7 +31,8 @@ public ProfileSettings( RumbleSettings rumble, HDHapticSettings hdHaptic, GyroSettings gyro, - BluetoothDeviceSettings bluetoothDevice + BluetoothDeviceSettings bluetoothDevice, + DualSenseSettings dualsense ) { this.name = name; this.controllerUid = controllerUid; @@ -40,6 +42,7 @@ public ProfileSettings( this.hdHaptic = hdHaptic; this.gyro = gyro; this.bluetoothDevice = bluetoothDevice; + this.dualsense = dualsense; } public static ProfileSettings fromDTO(ProfileConfig dto) { @@ -51,7 +54,8 @@ public static ProfileSettings fromDTO(ProfileConfig dto) { RumbleSettings.fromDTO(dto.rumble()), HDHapticSettings.fromDTO(dto.hdHaptic()), GyroSettings.fromDTO(dto.gyro()), - BluetoothDeviceSettings.fromDTO(dto.bluetoothDevice()) + BluetoothDeviceSettings.fromDTO(dto.bluetoothDevice()), + DualSenseSettings.fromDTO(dto.dualsense()) ); } @@ -64,7 +68,8 @@ public ProfileConfig toDTO() { rumble.toDTO(), hdHaptic.toDTO(), gyro.toDTO(), - bluetoothDevice.toDTO() + bluetoothDevice.toDTO(), + dualsense.toDTO() ); } diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java new file mode 100644 index 000000000..c05a62e27 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java @@ -0,0 +1,42 @@ +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; +import net.minecraft.world.item.Items; + +public final class BuiltinTriggerEffects { + 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) + ); + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java b/src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java index c33f6f14c..c7369f0b7 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java @@ -6,11 +6,14 @@ */ package dev.isxander.controlify.controller.dualsense; +import dev.isxander.controlify.config.settings.profile.DualSenseSettings; import dev.isxander.controlify.controller.impl.ECSComponentImpl; -import dev.isxander.controlify.driver.sdl.dualsense.DualsenseTriggerEffect; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.utils.CUtil; import net.minecraft.resources.Identifier; +import java.util.Objects; + public class DualSenseComponent extends ECSComponentImpl { public static final Identifier ID = CUtil.rl("dualsense"); @@ -22,8 +25,10 @@ public class DualSenseComponent extends ECSComponentImpl { private boolean dirty; public void setLeftTriggerEffect(DualsenseTriggerEffect effect) { + if (!Objects.equals(effect, this.leftTriggerEffect)) { + this.setDirty(); + } this.leftTriggerEffect = effect; - this.setDirty(); } public DualsenseTriggerEffect getLeftTriggerEffect() { @@ -31,8 +36,10 @@ public DualsenseTriggerEffect getLeftTriggerEffect() { } public void setRightTriggerEffect(DualsenseTriggerEffect effect) { + if (!Objects.equals(effect, this.rightTriggerEffect)) { + this.setDirty(); + } this.rightTriggerEffect = effect; - this.setDirty(); } public DualsenseTriggerEffect getRightTriggerEffect() { @@ -60,6 +67,14 @@ public boolean consumeDirty() { return old; } + public DualSenseSettings settings() { + return this.controller().settings().dualsense; + } + + public DualSenseSettings defaultSettings() { + return this.controller().defaultSettings().dualsense; + } + @Override public Identifier id() { return ID; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java new file mode 100644 index 000000000..61a51b606 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java @@ -0,0 +1,130 @@ +package dev.isxander.controlify.controller.dualsense; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import dev.isxander.controlify.utils.codec.CExtraCodecs; +import net.minecraft.util.ExtraCodecs; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class TriggerEffectCodecs { + + private TriggerEffectCodecs() { + } + + public static final MapCodec MAP_CODEC_OFF = + MapCodec.unit(DualsenseTriggerEffect.Off.INSTANCE); + + public static final MapCodec MAP_CODEC_FEEDBACK = + RecordCodecBuilder.mapCodec(instance -> instance.group( + CExtraCodecs.byteRange(0, 9).fieldOf("position") + .forGetter(DualsenseTriggerEffect.Feedback::position), + CExtraCodecs.byteRange(0, 8).fieldOf("strength") + .forGetter(DualsenseTriggerEffect.Feedback::strength) + ).apply(instance, DualsenseTriggerEffect.Feedback::new)); + + public static final MapCodec MAP_CODEC_WEAPON = + RecordCodecBuilder.mapCodec(instance -> instance.group( + CExtraCodecs.byteRange(2, 7).fieldOf("start_position") + .forGetter(DualsenseTriggerEffect.Weapon::startPosition), + CExtraCodecs.byteRange(3, 8).fieldOf("end_position") + .forGetter(DualsenseTriggerEffect.Weapon::endPosition), + CExtraCodecs.byteRange(0, 8).fieldOf("strength") + .forGetter(DualsenseTriggerEffect.Weapon::strength) + ).apply(instance, DualsenseTriggerEffect.Weapon::new)); + + public static final MapCodec MAP_CODEC_VIBRATION = + RecordCodecBuilder.mapCodec(instance -> instance.group( + CExtraCodecs.byteRange(0, 9).fieldOf("position") + .forGetter(DualsenseTriggerEffect.Vibration::position), + CExtraCodecs.byteRange(0, 8).fieldOf("amplitude") + .forGetter(DualsenseTriggerEffect.Vibration::amplitude), + Codec.BYTE.fieldOf("frequency") + .forGetter(DualsenseTriggerEffect.Vibration::frequency) + ).apply(instance, DualsenseTriggerEffect.Vibration::new)); + + public static final MapCodec MAP_CODEC_FEEDBACK_MULTIPLE_POSITION = + RecordCodecBuilder.mapCodec(instance -> instance.group( + CExtraCodecs.byteArray(CExtraCodecs.byteRange(0, 9).listOf(10, 10)).fieldOf("strength") + .forGetter(DualsenseTriggerEffect.FeedbackMultiplePosition::strength) + ).apply(instance, DualsenseTriggerEffect.FeedbackMultiplePosition::new)); + + public static final MapCodec MAP_CODEC_FEEDBACK_SLOPE = + RecordCodecBuilder.mapCodec(instance -> instance.group( + CExtraCodecs.byteRange(0, 8).fieldOf("start_position") + .forGetter(DualsenseTriggerEffect.FeedbackSlope::startPosition), + CExtraCodecs.byteRange(1, 9).fieldOf("end_position") + .forGetter(DualsenseTriggerEffect.FeedbackSlope::endPosition), + CExtraCodecs.byteRange(1, 8).fieldOf("start_strength") + .forGetter(DualsenseTriggerEffect.FeedbackSlope::startStrength), + CExtraCodecs.byteRange(1, 8).fieldOf("end_strength") + .forGetter(DualsenseTriggerEffect.FeedbackSlope::endStrength) + ).apply(instance, DualsenseTriggerEffect.FeedbackSlope::new)); + + public static final MapCodec MAP_CODEC_VIBRATION_MULTIPLE_POSITION = + RecordCodecBuilder.mapCodec(instance -> instance.group( + Codec.BYTE.fieldOf("frequency") + .forGetter(DualsenseTriggerEffect.VibrationMultiplePosition::frequency), + CExtraCodecs.byteArray(CExtraCodecs.byteRange(0, 8).listOf(10, 10)).fieldOf("amplitude") + .forGetter(DualsenseTriggerEffect.VibrationMultiplePosition::amplitude) + ).apply(instance, DualsenseTriggerEffect.VibrationMultiplePosition::new)); + + private record TriggerEffectType(String id, MapCodec mapCodec) { + } + + private static final TriggerEffectType TYPE_OFF = + new TriggerEffectType<>("off", MAP_CODEC_OFF); + + private static final TriggerEffectType TYPE_FEEDBACK = + new TriggerEffectType<>("feedback", MAP_CODEC_FEEDBACK); + + private static final TriggerEffectType TYPE_WEAPON = + new TriggerEffectType<>("weapon", MAP_CODEC_WEAPON); + + private static final TriggerEffectType TYPE_VIBRATION = + new TriggerEffectType<>("vibration", MAP_CODEC_VIBRATION); + + private static final TriggerEffectType TYPE_FEEDBACK_MULTIPLE_POSITION = + new TriggerEffectType<>("feedback_multiple_position", MAP_CODEC_FEEDBACK_MULTIPLE_POSITION); + + private static final TriggerEffectType TYPE_FEEDBACK_SLOPE = + new TriggerEffectType<>("feedback_slope", MAP_CODEC_FEEDBACK_SLOPE); + + private static final TriggerEffectType TYPE_VIBRATION_MULTIPLE_POSITION = + new TriggerEffectType<>("vibration_multiple_position", MAP_CODEC_VIBRATION_MULTIPLE_POSITION); + + private static final Map> TYPES_BY_ID = Stream.of( + TYPE_OFF, + TYPE_FEEDBACK, + TYPE_WEAPON, + TYPE_VIBRATION, + TYPE_FEEDBACK_MULTIPLE_POSITION, + TYPE_FEEDBACK_SLOPE, + TYPE_VIBRATION_MULTIPLE_POSITION + ).collect(Collectors.toUnmodifiableMap( + TriggerEffectType::id, + Function.identity() + )); + + private static final Codec> TYPE_CODEC = + ExtraCodecs.idResolverCodec(Codec.STRING, TYPES_BY_ID::get, TriggerEffectType::id); + + public static final Codec CODEC = TYPE_CODEC.dispatch( + "type", + effect -> switch (effect) { + case DualsenseTriggerEffect.Off _ -> TYPE_OFF; + case DualsenseTriggerEffect.Feedback _ -> TYPE_FEEDBACK; + case DualsenseTriggerEffect.Weapon _ -> TYPE_WEAPON; + case DualsenseTriggerEffect.Vibration _ -> TYPE_VIBRATION; + case DualsenseTriggerEffect.FeedbackMultiplePosition _ -> TYPE_FEEDBACK_MULTIPLE_POSITION; + case DualsenseTriggerEffect.FeedbackSlope _ -> TYPE_FEEDBACK_SLOPE; + case DualsenseTriggerEffect.VibrationMultiplePosition _ -> TYPE_VIBRATION_MULTIPLE_POSITION; + }, + TriggerEffectType::mapCodec + ); +} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java new file mode 100644 index 000000000..1ff72952c --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.controller.dualsense; + +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public interface TriggerEffectHolder { + Optional controlify$getUseTriggerEffect(); + + Optional controlify$getSwingTriggerEffect(); + + void controlify$assignUseTriggerEffect(@NotNull DualsenseTriggerEffect effect); + + void controlify$assignSwingTriggerEffect(@NotNull DualsenseTriggerEffect effect); +} diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java new file mode 100644 index 000000000..a059715c2 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java @@ -0,0 +1,155 @@ +package dev.isxander.controlify.controller.dualsense; + +import dev.isxander.controlify.bindings.ControlifyBindings; +import dev.isxander.controlify.controller.ControllerEntity; +import dev.isxander.controlify.controller.input.GamepadInputs; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import dev.isxander.controlify.utils.MinecraftUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; + +import java.util.*; +import java.util.function.Function; + +public class TriggerEffectManager { + private final Minecraft minecraft; + + private final Map, Function> useItemComponentEffects; + private final Map, Function> swingItemComponentEffects; + + public TriggerEffectManager(Minecraft minecraft) { + this.minecraft = minecraft; + this.useItemComponentEffects = new LinkedHashMap<>(); + this.swingItemComponentEffects = new LinkedHashMap<>(); + } + + public void applyTriggerEffects(ControllerEntity controller) { + controller.dualSense().ifPresent(ds -> { + ds.setLeftTriggerEffect(this.getCurrentLeftTriggerEffect(controller)); + ds.setRightTriggerEffect(this.getCurrentRightTriggerEffect(controller)); + }); + } + + public void registerUseItemComponentEffect(DataComponentType componentType, Function effectFunction) { + this.useItemComponentEffects.put(componentType, (Function) effectFunction); + } + + public void registerSwingItemComponentEffect(DataComponentType componentType, Function effectFunction) { + this.swingItemComponentEffects.put(componentType, (Function) effectFunction); + } + + public DualsenseTriggerEffect getCurrentLeftTriggerEffect(ControllerEntity controller) { + if (!this.shouldUseTriggerEffects(controller)) { + return DualsenseTriggerEffect.Off.INSTANCE; + } + + LocalPlayer player = Objects.requireNonNull(this.minecraft.player); + + if (this.getUseItemTrigger(controller) == Trigger.LEFT) { + return this.getUseItemTriggerEffect(player) + .orElse(DualsenseTriggerEffect.Off.INSTANCE); + } + if (this.getSwingItemTrigger(controller) == Trigger.LEFT) { + return this.getSwingItemTriggerEffect(player) + .orElse(DualsenseTriggerEffect.Off.INSTANCE); + } + + return DualsenseTriggerEffect.Off.INSTANCE; + } + + public DualsenseTriggerEffect getCurrentRightTriggerEffect(ControllerEntity controller) { + if (!this.shouldUseTriggerEffects(controller)) { + return DualsenseTriggerEffect.Off.INSTANCE; + } + + LocalPlayer player = Objects.requireNonNull(this.minecraft.player); + + if (this.getUseItemTrigger(controller) == Trigger.RIGHT) { + return this.getUseItemTriggerEffect(player) + .orElse(DualsenseTriggerEffect.Off.INSTANCE); + } + if (this.getSwingItemTrigger(controller) == Trigger.RIGHT) { + return this.getSwingItemTriggerEffect(player) + .orElse(DualsenseTriggerEffect.Off.INSTANCE); + } + + return DualsenseTriggerEffect.Off.INSTANCE; + } + + public boolean shouldUseTriggerEffects(ControllerEntity controller) { + return MinecraftUtil.getScreen() == null + && this.minecraft.player != null + && controller.dualSense().map(ds -> ds.settings().triggerEffects).orElse(false); + } + + private Trigger getUseItemTrigger(ControllerEntity controller) { + List relevantInputs = ControlifyBindings.USE.on(controller).boundInput().getRelevantInputs(); + + if (relevantInputs.contains(GamepadInputs.LEFT_TRIGGER_AXIS)) { + return Trigger.LEFT; + } else if (relevantInputs.contains(GamepadInputs.RIGHT_TRIGGER_AXIS)) { + return Trigger.RIGHT; + } else { + return Trigger.NEITHER; + } + } + + private Trigger getSwingItemTrigger(ControllerEntity controller) { + List relevantInputs = ControlifyBindings.ATTACK.on(controller).boundInput().getRelevantInputs(); + + if (relevantInputs.contains(GamepadInputs.LEFT_TRIGGER_AXIS)) { + return Trigger.LEFT; + } else if (relevantInputs.contains(GamepadInputs.RIGHT_TRIGGER_AXIS)) { + return Trigger.RIGHT; + } else { + return Trigger.NEITHER; + } + } + + public Optional getUseItemTriggerEffect(LocalPlayer player) { + return this.getUseItemTriggerEffect(player.getActiveItem()) + .or(() -> this.getUseItemTriggerEffect(player.getOffhandItem())); + } + + public Optional getUseItemTriggerEffect(ItemStack stack) { + for (var entry : this.useItemComponentEffects.entrySet()) { + if (stack.has(entry.getKey())) { + Function effectFunction = entry.getValue(); + DualsenseTriggerEffect effect = effectFunction.apply(stack.get(entry.getKey())); + if (effect != null) { + return Optional.of(effect); + } + } + } + + var effectHolder = (TriggerEffectHolder) stack.getItem(); + return effectHolder.controlify$getUseTriggerEffect(); + } + + public Optional getSwingItemTriggerEffect(LocalPlayer player) { + return this.getSwingItemTriggerEffect(player.getMainHandItem()); + } + + public Optional getSwingItemTriggerEffect(ItemStack stack) { + for (var entry : this.swingItemComponentEffects.entrySet()) { + if (stack.has(entry.getKey())) { + Function effectFunction = entry.getValue(); + DualsenseTriggerEffect effect = effectFunction.apply(stack.get(entry.getKey())); + if (effect != null) { + return Optional.of(effect); + } + } + } + + var effectHolder = (TriggerEffectHolder) stack.getItem(); + return effectHolder.controlify$getSwingTriggerEffect(); + } + + + private enum Trigger { + LEFT, RIGHT, NEITHER + } +} diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DS5EffectsState.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DS5EffectsState.java similarity index 99% rename from src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DS5EffectsState.java rename to src/main/java/dev/isxander/controlify/driver/dualsense/DS5EffectsState.java index 359b02e3c..601a6b966 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DS5EffectsState.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DS5EffectsState.java @@ -4,7 +4,7 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.driver.sdl.dualsense; +package dev.isxander.controlify.driver.dualsense; import org.intellij.lang.annotations.MagicConstant; diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DualsenseTriggerEffect.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java similarity index 98% rename from src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DualsenseTriggerEffect.java rename to src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java index 364d91cbf..1fbae64e6 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DualsenseTriggerEffect.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java @@ -4,13 +4,13 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.driver.sdl.dualsense; +package dev.isxander.controlify.driver.dualsense; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Range; -public interface DualsenseTriggerEffect { +public sealed interface DualsenseTriggerEffect { DS5EffectsState.TriggerEffect createState(); /** @@ -18,6 +18,8 @@ public interface DualsenseTriggerEffect { * This is an official effect and is expected to be present in future DualSense firmware versions. */ record Off() implements DualsenseTriggerEffect { + public static final Off INSTANCE = new Off(); + @Override public DS5EffectsState.TriggerEffect createState() { return DS5EffectsState.TriggerEffect.OFF; diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DualsenseTriggerEffectTypes.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffectTypes.java similarity index 96% rename from src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DualsenseTriggerEffectTypes.java rename to src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffectTypes.java index 3e4144e60..9fc0680a7 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/dualsense/DualsenseTriggerEffectTypes.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffectTypes.java @@ -4,7 +4,7 @@ * * SPDX-License-Identifier: LGPL-3.0-or-later */ -package dev.isxander.controlify.driver.sdl.dualsense; +package dev.isxander.controlify.driver.dualsense; public final class DualsenseTriggerEffectTypes { diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java index 6d4980d16..bf21f5451 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java @@ -22,7 +22,7 @@ import dev.isxander.controlify.controller.rumble.RumbleComponent; import dev.isxander.controlify.controller.rumble.TriggerRumbleComponent; import dev.isxander.controlify.driver.Driver; -import dev.isxander.controlify.driver.sdl.dualsense.DS5EffectsState; +import dev.isxander.controlify.driver.dualsense.DS5EffectsState; import dev.isxander.controlify.rumble.RumbleState; import dev.isxander.controlify.rumble.TriggerRumbleState; import dev.isxander.controlify.utils.CUtil; @@ -113,8 +113,8 @@ public SDLCommonDriver(Sdl sdl, SdlController ptrController, SdlJoystickId jid, // open audio device for dualsense hd haptics this.dualsenseAudioHandles = new ArrayList<>(); - if (CUtil.rl("dualsense").equals(type.namespace())) { - this.isDualsense = true; + this.isDualsense = CUtil.rl("dualsense").equals(type.namespace()); + if (this.isDualsense) { logger.debugLog("DualSense controller detected."); // macOS HD haptics are broken @@ -141,8 +141,6 @@ public SDLCommonDriver(Sdl sdl, SdlController ptrController, SdlJoystickId jid, logger.debugLog("DualSense HD Haptics audio device not found."); } } - } else { - this.isDualsense = false; } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java new file mode 100644 index 000000000..c471bdf38 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java @@ -0,0 +1,40 @@ +package dev.isxander.controlify.mixins.feature.triggereffect; + +import dev.isxander.controlify.controller.dualsense.TriggerEffectHolder; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.Optional; + +@Mixin(Item.class) +public class ItemMixin implements TriggerEffectHolder { + @Unique + private @Nullable DualsenseTriggerEffect controlify$useEffect = null; + + @Unique + private @Nullable DualsenseTriggerEffect controlify$swingEffect = null; + + @Override + public Optional controlify$getUseTriggerEffect() { + return Optional.ofNullable(controlify$useEffect); + } + + @Override + public Optional controlify$getSwingTriggerEffect() { + return Optional.ofNullable(controlify$swingEffect); + } + + @Override + public void controlify$assignUseTriggerEffect(@NotNull DualsenseTriggerEffect effect) { + this.controlify$useEffect = effect; + } + + @Override + public void controlify$assignSwingTriggerEffect(@NotNull DualsenseTriggerEffect effect) { + this.controlify$swingEffect = effect; + } +} diff --git a/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java b/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java index dd3a9a8f7..dcd225c36 100644 --- a/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/CExtraCodecs.java @@ -8,6 +8,7 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.*; +import it.unimi.dsi.fastutil.bytes.ByteArrayList; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -90,5 +91,26 @@ public static Codec stringResolver(final Function toString, fi ); } + static > Function> checkRange(final N minInclusive, final N maxInclusive) { + return value -> { + if (value.compareTo(minInclusive) >= 0 && value.compareTo(maxInclusive) <= 0) { + return DataResult.success(value); + } + return DataResult.error(() -> "Value " + value + " outside of range [" + minInclusive + ":" + maxInclusive + "]"); + }; + } + + public static Codec byteRange(int minInclusive, int maxInclusive) { + final Function> checker = checkRange((byte) minInclusive, (byte) maxInclusive); + return Codec.BYTE.flatXmap(checker, checker); + } + + public static Codec byteArray(Codec> byteListCodec) { + return byteListCodec.xmap( + byteList -> new ByteArrayList(byteList).toByteArray(), + ByteArrayList::wrap + ); + } + private CExtraCodecs() {} } diff --git a/src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java b/src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java index 75200f235..1c49aceda 100644 --- a/src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java +++ b/src/main/java/dev/isxander/controlify/utils/codec/FuzzyMapCodec.java @@ -31,7 +31,7 @@ public DataResult decode(DynamicOps ops, MapLike input) { } } - return DataResult.error(() -> "No matching codec found."); + return DataResult.error(() -> "No matching mapCodec found."); } @Override diff --git a/src/main/resources/assets/controlify/controllers/default_config.json b/src/main/resources/assets/controlify/controllers/default_config.json index edb9f9e0f..033431232 100644 --- a/src/main/resources/assets/controlify/controllers/default_config.json +++ b/src/main/resources/assets/controlify/controllers/default_config.json @@ -63,5 +63,8 @@ }, "bluetooth_device": { "dont_show_warning": false + }, + "dualsense": { + "trigger_effects": true } } diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index f924794ca..a7f9418c2 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -122,5 +122,8 @@ "feature.virtualmouse.snapping.RecipeBookPageAccessor", "feature.virtualmouse.snapping.ScreenMixin", "feature.virtualmouse.snapping.StonecutterScreenMixin" + ], + "mixins": [ + "feature.triggereffect.ItemMixin" ] } From 8609236a877cd0012c09bc052c43bd8905206680 Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 18:10:27 +0100 Subject: [PATCH 2/8] add more trigger effects --- .../dualsense/BuiltinTriggerEffects.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java index c05a62e27..4e41d2469 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java @@ -38,5 +38,20 @@ public static void register() { 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) + ); } } From e8c683a76da671050c89302cd4e7d08510917b72 Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 18:10:40 +0100 Subject: [PATCH 3/8] add gui option to disable trigger effects --- .../screen/ControllerConfigScreenFactory.java | 27 +++++++++++++++++++ .../assets/controlify/lang/en_us.json | 3 +++ 2 files changed, 30 insertions(+) diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index 57bdf5e6a..6483c597d 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -451,6 +451,7 @@ private ConfigCategory createAdvancedCategory( makeVibrationGroup(settings, defaults, controller).ifPresent(builder::group); makeGyroGroup(settings, defaults, controller).ifPresent(builder::group); + makeTriggerEffectsGroup(settings, defaults, controller).ifPresent(builder::group); makeControllerMappingGroup(settings, defaults, controller).ifPresent(builder::group); return builder.build(); @@ -682,6 +683,32 @@ private Optional makeGyroGroup( return Optional.of(gyroGroup.build()); } + private Optional makeTriggerEffectsGroup( + ProfileSettings settings, + ProfileSettings defaults, + Optional controller + ) { + var group = OptionGroup.createBuilder() + .name(Component.translatable("controlify.gui.group.trigger_effects")) + .description(OptionDescription.createBuilder() + .text(Component.translatable("controlify.gui.trigger_effects.tooltip")) + .build()); + + if (controller.isPresent() && controller.get().dualSense().isEmpty()) { + group.collapsed(true); + group.option(LabelOption.create(Component.translatable("controlify.gui.group.gyro.no_dualsense.tooltip").withStyle(ChatFormatting.RED))); + } + + group.option(Option.createBuilder() + .name(Component.translatable("controlify.gui.trigger_effects")) + .description(OptionDescription.of(Component.translatable("controlify.gui.trigger_effects.tooltip"))) + .binding(defaults.dualsense.triggerEffects, () -> settings.dualsense.triggerEffects, v -> settings.dualsense.triggerEffects = v) + .controller(TickBoxControllerBuilder::create) + .build()); + + return Optional.of(group.build()); + } + private Optional makeBindsCategory( ProfileSettings settings, ProfileSettings defaults, diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 615c20d30..8c32de95a 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -189,6 +189,9 @@ "controlify.gui.screen_repeat_navi_delay.tooltip": "How fast a screen navigation action repeats.", "controlify.gui.test_vibration": "Test Vibration", "controlify.gui.test_vibration.tooltip": "Test the vibration of your controller.", + "controlify.gui.group.trigger_effects": "Trigger Effects", + "controlify.gui.trigger_effects": "Trigger Effects", + "controlify.gui.trigger_effects.tooltip": "Enable DualSense Adaptive Trigger effects for items in game.", "controlify.gui.show_keyboard.off": "Off", "controlify.gui.show_keyboard.controlify": "Controlify", From 9116988c128b5c265170d019745ffcd8feaabf9b Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 18:41:04 +0100 Subject: [PATCH 4/8] add datafix for dualsense, and conform capitalisation of Dualsense --- .../config/dto/dfu/ControlifyDataFixer.java | 13 +++--- .../config/dto/dfu/ControlifySchemas.java | 6 +++ .../dto/dfu/fixes/DualsenseConfigFix.java | 43 +++++++++++++++++++ ...lSenseConfig.java => DualsenseConfig.java} | 8 ++-- .../config/dto/profile/ProfileConfig.java | 4 +- .../settings/profile/DualSenseSettings.java | 19 -------- .../settings/profile/DualsenseSettings.java | 19 ++++++++ .../settings/profile/ProfileSettings.java | 6 +-- .../controller/ControllerEntity.java | 6 +-- ...Component.java => DualsenseComponent.java} | 8 ++-- ...sState.java => DualsenseEffectsState.java} | 2 +- .../dualsense/DualsenseTriggerEffect.java | 38 ++++++++-------- .../driver/sdl/SDLCommonDriver.java | 20 ++++----- 13 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/config/dto/dfu/fixes/DualsenseConfigFix.java rename src/main/java/dev/isxander/controlify/config/dto/profile/{DualSenseConfig.java => DualsenseConfig.java} (54%) delete mode 100644 src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java create mode 100644 src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java rename src/main/java/dev/isxander/controlify/controller/dualsense/{DualSenseComponent.java => DualsenseComponent.java} (89%) rename src/main/java/dev/isxander/controlify/driver/dualsense/{DS5EffectsState.java => DualsenseEffectsState.java} (99%) diff --git a/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifyDataFixer.java b/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifyDataFixer.java index b0117a38e..350bfedc6 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifyDataFixer.java +++ b/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifyDataFixer.java @@ -13,7 +13,7 @@ import dev.isxander.controlify.config.settings.profile.ProfileSettings; public final class ControlifyDataFixer { - public static final int CURRENT_VERSION = 5; + public static final int CURRENT_VERSION = 6; private static final DataFixer FIXER = createFixer(); @@ -28,14 +28,15 @@ private static DataFixer createFixer() { var v1 = builder.addSchema(1, ControlifySchemas.V1::new); var v2 = builder.addSchema(2, ControlifySchemas.V2::new); var v3 = builder.addSchema(3, ControlifySchemas.V3::new); + var v6 = builder.addSchema(6, ControlifySchemas.V6::new); - builder.addFixer(new TheHolyMigrationFix( - v1, - GlobalSettings.defaults(), - ProfileSettings.createDefault() - )); + var globalDefaults = GlobalSettings.defaults(); + var profileDefaults = ProfileSettings.createDefault(); + + builder.addFixer(new TheHolyMigrationFix(v1, globalDefaults, profileDefaults)); builder.addFixer(new AnalogueMovementWhitelistFix(v2)); builder.addFixer(new HorizontalLookInvertFix(v2)); + builder.addFixer(new DualsenseConfigFix(v6, profileDefaults)); return builder.build().fixer(); } diff --git a/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifySchemas.java b/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifySchemas.java index 791cdc160..adb3f1834 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifySchemas.java +++ b/src/main/java/dev/isxander/controlify/config/dto/dfu/ControlifySchemas.java @@ -74,4 +74,10 @@ public void registerTypes(Schema schema, Map> ent ); } } + + public static class V6 extends Schema { + public V6(int versionKey, Schema parent) { + super(versionKey, parent); + } + } } diff --git a/src/main/java/dev/isxander/controlify/config/dto/dfu/fixes/DualsenseConfigFix.java b/src/main/java/dev/isxander/controlify/config/dto/dfu/fixes/DualsenseConfigFix.java new file mode 100644 index 000000000..3f11e7d53 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/dto/dfu/fixes/DualsenseConfigFix.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package dev.isxander.controlify.config.dto.dfu.fixes; + +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.TypeRewriteRule; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import dev.isxander.controlify.config.dto.dfu.ControlifyTypeReferences; +import dev.isxander.controlify.config.settings.profile.ProfileSettings; + +public final class DualsenseConfigFix extends DataFix { + private final ProfileSettings profileDefaults; + + public DualsenseConfigFix(Schema outputSchema, ProfileSettings profileDefaults) { + super(outputSchema, true); + this.profileDefaults = profileDefaults; + } + + @Override + protected TypeRewriteRule makeRule() { + var profileType = getInputSchema().getType(ControlifyTypeReferences.PROFILE_CONFIG); + + return fixTypeEverywhereTyped( + "Controlify: add DualSense profile config", + profileType, + typed -> typed.update(DSL.remainderFinder(), this::rewriteProfile) + ); + } + + private Dynamic rewriteProfile(Dynamic root) { + Dynamic dualsense = root.get("dualsense").orElseEmptyMap(); + if (dualsense.get("trigger_effects").result().isEmpty()) { + dualsense = dualsense.set("trigger_effects", root.createBoolean(profileDefaults.dualsense.triggerEffects)); + } + return root.set("dualsense", dualsense); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java b/src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java similarity index 54% rename from src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java rename to src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java index 352d6411f..a163efa34 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/profile/DualSenseConfig.java +++ b/src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java @@ -3,10 +3,10 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; -public record DualSenseConfig( +public record DualsenseConfig( boolean triggerEffects ) { - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.BOOL.fieldOf("trigger_effects").forGetter(DualSenseConfig::triggerEffects) - ).apply(instance, DualSenseConfig::new)); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.BOOL.fieldOf("trigger_effects").forGetter(DualsenseConfig::triggerEffects) + ).apply(instance, DualsenseConfig::new)); } diff --git a/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java b/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java index 660240004..1daf893f6 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java +++ b/src/main/java/dev/isxander/controlify/config/dto/profile/ProfileConfig.java @@ -20,7 +20,7 @@ public record ProfileConfig( HDHapticConfig hdHaptic, GyroConfig gyro, BluetoothDeviceConfig bluetoothDevice, - DualSenseConfig dualsense + DualsenseConfig dualsense ) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.STRING.optionalFieldOf("name").forGetter(ProfileConfig::name), @@ -31,6 +31,6 @@ public record ProfileConfig( HDHapticConfig.CODEC.fieldOf("hd_haptic").forGetter(ProfileConfig::hdHaptic), GyroConfig.CODEC.fieldOf("gyro").forGetter(ProfileConfig::gyro), BluetoothDeviceConfig.CODEC.fieldOf("bluetooth_device").forGetter(ProfileConfig::bluetoothDevice), - DualSenseConfig.CODEC.fieldOf("dualsense").forGetter(ProfileConfig::dualsense) + DualsenseConfig.CODEC.fieldOf("dualsense").forGetter(ProfileConfig::dualsense) ).apply(instance, ProfileConfig::new)); } diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java deleted file mode 100644 index bc8cb5795..000000000 --- a/src/main/java/dev/isxander/controlify/config/settings/profile/DualSenseSettings.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.isxander.controlify.config.settings.profile; - -import dev.isxander.controlify.config.dto.profile.DualSenseConfig; - -public class DualSenseSettings { - public boolean triggerEffects; - - public DualSenseSettings(boolean triggerEffects) { - this.triggerEffects = triggerEffects; - } - - public static DualSenseSettings fromDTO(DualSenseConfig dto) { - return new DualSenseSettings(dto.triggerEffects()); - } - - public DualSenseConfig toDTO() { - return new DualSenseConfig(triggerEffects); - } -} diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java new file mode 100644 index 000000000..f503006a7 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java @@ -0,0 +1,19 @@ +package dev.isxander.controlify.config.settings.profile; + +import dev.isxander.controlify.config.dto.profile.DualsenseConfig; + +public class DualsenseSettings { + public boolean triggerEffects; + + public DualsenseSettings(boolean triggerEffects) { + this.triggerEffects = triggerEffects; + } + + public static DualsenseSettings fromDTO(DualsenseConfig dto) { + return new DualsenseSettings(dto.triggerEffects()); + } + + public DualsenseConfig toDTO() { + return new DualsenseConfig(triggerEffects); + } +} diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java index a2105043e..f4b624f37 100644 --- a/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java +++ b/src/main/java/dev/isxander/controlify/config/settings/profile/ProfileSettings.java @@ -21,7 +21,7 @@ public class ProfileSettings { public final HDHapticSettings hdHaptic; public final GyroSettings gyro; public final BluetoothDeviceSettings bluetoothDevice; - public final DualSenseSettings dualsense; + public final DualsenseSettings dualsense; public ProfileSettings( @Nullable String name, @@ -32,7 +32,7 @@ public ProfileSettings( HDHapticSettings hdHaptic, GyroSettings gyro, BluetoothDeviceSettings bluetoothDevice, - DualSenseSettings dualsense + DualsenseSettings dualsense ) { this.name = name; this.controllerUid = controllerUid; @@ -55,7 +55,7 @@ public static ProfileSettings fromDTO(ProfileConfig dto) { HDHapticSettings.fromDTO(dto.hdHaptic()), GyroSettings.fromDTO(dto.gyro()), BluetoothDeviceSettings.fromDTO(dto.bluetoothDevice()), - DualSenseSettings.fromDTO(dto.dualsense()) + DualsenseSettings.fromDTO(dto.dualsense()) ); } diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java b/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java index 5bd323d61..412aabcbe 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerEntity.java @@ -8,7 +8,7 @@ import dev.isxander.controlify.config.settings.profile.ProfileSettings; import dev.isxander.controlify.controller.battery.BatteryLevelComponent; -import dev.isxander.controlify.controller.dualsense.DualSenseComponent; +import dev.isxander.controlify.controller.dualsense.DualsenseComponent; import dev.isxander.controlify.controller.haptic.HDHapticComponent; import dev.isxander.controlify.controller.info.ControllerInfo; import dev.isxander.controlify.controller.info.DriverNameComponent; @@ -133,8 +133,8 @@ public Optional hdHaptics() { } @Contract(pure = true) - public Optional dualSense() { - return this.getComponent(DualSenseComponent.ID); + public Optional dualSense() { + return this.getComponent(DualsenseComponent.ID); } @Contract(pure = true) diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java b/src/main/java/dev/isxander/controlify/controller/dualsense/DualsenseComponent.java similarity index 89% rename from src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java rename to src/main/java/dev/isxander/controlify/controller/dualsense/DualsenseComponent.java index c7369f0b7..7f8741f2f 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/DualSenseComponent.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/DualsenseComponent.java @@ -6,7 +6,7 @@ */ package dev.isxander.controlify.controller.dualsense; -import dev.isxander.controlify.config.settings.profile.DualSenseSettings; +import dev.isxander.controlify.config.settings.profile.DualsenseSettings; import dev.isxander.controlify.controller.impl.ECSComponentImpl; import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.utils.CUtil; @@ -14,7 +14,7 @@ import java.util.Objects; -public class DualSenseComponent extends ECSComponentImpl { +public class DualsenseComponent extends ECSComponentImpl { public static final Identifier ID = CUtil.rl("dualsense"); private boolean muteLight; @@ -67,11 +67,11 @@ public boolean consumeDirty() { return old; } - public DualSenseSettings settings() { + public DualsenseSettings settings() { return this.controller().settings().dualsense; } - public DualSenseSettings defaultSettings() { + public DualsenseSettings defaultSettings() { return this.controller().defaultSettings().dualsense; } diff --git a/src/main/java/dev/isxander/controlify/driver/dualsense/DS5EffectsState.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseEffectsState.java similarity index 99% rename from src/main/java/dev/isxander/controlify/driver/dualsense/DS5EffectsState.java rename to src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseEffectsState.java index 601a6b966..b494bd087 100644 --- a/src/main/java/dev/isxander/controlify/driver/dualsense/DS5EffectsState.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseEffectsState.java @@ -15,7 +15,7 @@ import static java.lang.foreign.MemoryLayout.PathElement.groupElement; -public final class DS5EffectsState { +public final class DualsenseEffectsState { public static final MemoryLayout LAYOUT = MemoryLayout.structLayout( ValueLayout.JAVA_BYTE.withName("ucEnableBits1"), ValueLayout.JAVA_BYTE.withName("ucEnableBits2"), diff --git a/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java index 1fbae64e6..33edb3e73 100644 --- a/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java +++ b/src/main/java/dev/isxander/controlify/driver/dualsense/DualsenseTriggerEffect.java @@ -11,7 +11,7 @@ import org.jetbrains.annotations.Range; public sealed interface DualsenseTriggerEffect { - DS5EffectsState.TriggerEffect createState(); + DualsenseEffectsState.TriggerEffect createState(); /** * Turn the trigger effect off and return the trigger stop to the neutral position. @@ -21,8 +21,8 @@ record Off() implements DualsenseTriggerEffect { public static final Off INSTANCE = new Off(); @Override - public DS5EffectsState.TriggerEffect createState() { - return DS5EffectsState.TriggerEffect.OFF; + public DualsenseEffectsState.TriggerEffect createState() { + return DualsenseEffectsState.TriggerEffect.OFF; } } @@ -44,7 +44,7 @@ record Feedback( } @Override - public DS5EffectsState.TriggerEffect createState() { + public DualsenseEffectsState.TriggerEffect createState() { if (strength > 0) { byte forceValue = (byte) ((strength - 1) & 0x07); int forceZones = 0; @@ -54,7 +54,7 @@ public DS5EffectsState.TriggerEffect createState() { activeZones |= (char) (1 << i); } - return new DS5EffectsState.TriggerEffect(DualsenseTriggerEffectTypes.FEEDBACK, new byte[]{ + return new DualsenseEffectsState.TriggerEffect(DualsenseTriggerEffectTypes.FEEDBACK, new byte[]{ (byte) (activeZones & 0xff), (byte) ((activeZones >> 8) & 0xff), (byte) (forceZones & 0xff), @@ -63,7 +63,7 @@ public DS5EffectsState.TriggerEffect createState() { (byte) ((forceZones >> 24) & 0xff), }); } else { - return DS5EffectsState.TriggerEffect.OFF; + return DualsenseEffectsState.TriggerEffect.OFF; } } } @@ -91,17 +91,17 @@ record Weapon( } @Override - public DS5EffectsState.TriggerEffect createState() { + public DualsenseEffectsState.TriggerEffect createState() { if (strength > 0) { char startAndStopZones = (char) ((1 << startPosition) | (1 << endPosition)); - return new DS5EffectsState.TriggerEffect(DualsenseTriggerEffectTypes.WEAPON, new byte[]{ + return new DualsenseEffectsState.TriggerEffect(DualsenseTriggerEffectTypes.WEAPON, new byte[]{ (byte) (startAndStopZones & 0xff), (byte) ((startAndStopZones >> 8) & 0xff), (byte) (strength - 1), // this is actually packed into 3 bits, but since it's only one why bother with the fancy code? }); } else { - return DS5EffectsState.TriggerEffect.OFF; + return DualsenseEffectsState.TriggerEffect.OFF; } } } @@ -128,7 +128,7 @@ record Vibration( } @Override - public DS5EffectsState.TriggerEffect createState() { + public DualsenseEffectsState.TriggerEffect createState() { if (amplitude > 0 && frequency > 0) { byte strengthValue = (byte) ((amplitude - 1) & 0x07); int amplitudeZones = 0; @@ -139,7 +139,7 @@ public DS5EffectsState.TriggerEffect createState() { activeZones |= (char) (1 << i); } - return new DS5EffectsState.TriggerEffect(DualsenseTriggerEffectTypes.VIBRATION, new byte[]{ + return new DualsenseEffectsState.TriggerEffect(DualsenseTriggerEffectTypes.VIBRATION, new byte[]{ (byte) (activeZones & 0xff), (byte) ((activeZones >> 8) & 0xff), (byte) (amplitudeZones & 0xff), @@ -150,7 +150,7 @@ public DS5EffectsState.TriggerEffect createState() { frequency, }); } else { - return DS5EffectsState.TriggerEffect.OFF; + return DualsenseEffectsState.TriggerEffect.OFF; } } } @@ -173,7 +173,7 @@ record FeedbackMultiplePosition( } @Override - public DS5EffectsState.TriggerEffect createState() { + public DualsenseEffectsState.TriggerEffect createState() { boolean allZero = true; for (int i = 0; i < 10; i++) { allZero &= strength[i] == 0; @@ -193,7 +193,7 @@ public DS5EffectsState.TriggerEffect createState() { } } - return new DS5EffectsState.TriggerEffect(DualsenseTriggerEffectTypes.FEEDBACK, new byte[]{ + return new DualsenseEffectsState.TriggerEffect(DualsenseTriggerEffectTypes.FEEDBACK, new byte[]{ (byte) (activeZones & 0xff), (byte) ((activeZones >> 8) & 0xff), (byte) (forceZones & 0xff), @@ -202,7 +202,7 @@ public DS5EffectsState.TriggerEffect createState() { (byte) ((forceZones >> 24) & 0xff), }); } else { - return DS5EffectsState.TriggerEffect.OFF; + return DualsenseEffectsState.TriggerEffect.OFF; } } } @@ -234,7 +234,7 @@ record FeedbackSlope( } @Override - public DS5EffectsState.TriggerEffect createState() { + public DualsenseEffectsState.TriggerEffect createState() { byte[] strength = new byte[10]; float gradient = (endStrength - startStrength) / (float) (endPosition - startPosition); for (int i = startPosition; i < 10; i++) { @@ -266,7 +266,7 @@ record VibrationMultiplePosition( } @Override - public DS5EffectsState.TriggerEffect createState() { + public DualsenseEffectsState.TriggerEffect createState() { if (frequency > 0) { boolean allZero = true; for (int i = 0; i < 10; i++) { @@ -288,7 +288,7 @@ public DS5EffectsState.TriggerEffect createState() { } } - return new DS5EffectsState.TriggerEffect(DualsenseTriggerEffectTypes.VIBRATION, new byte[]{ + return new DualsenseEffectsState.TriggerEffect(DualsenseTriggerEffectTypes.VIBRATION, new byte[]{ (byte) (activeZones & 0xff), (byte) ((activeZones >> 8) & 0xff), (byte) (strengthZones & 0xff), @@ -301,7 +301,7 @@ public DS5EffectsState.TriggerEffect createState() { } } - return DS5EffectsState.TriggerEffect.OFF; + return DualsenseEffectsState.TriggerEffect.OFF; } } } diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java index bf21f5451..a0c508328 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java @@ -10,7 +10,7 @@ import dev.isxander.controlify.controller.ControllerEntity; import dev.isxander.controlify.controller.battery.BatteryLevelComponent; import dev.isxander.controlify.controller.battery.PowerState; -import dev.isxander.controlify.controller.dualsense.DualSenseComponent; +import dev.isxander.controlify.controller.dualsense.DualsenseComponent; import dev.isxander.controlify.controller.haptic.CompleteSoundData; import dev.isxander.controlify.controller.haptic.HDHapticComponent; import dev.isxander.controlify.controller.id.ControllerType; @@ -22,7 +22,7 @@ import dev.isxander.controlify.controller.rumble.RumbleComponent; import dev.isxander.controlify.controller.rumble.TriggerRumbleComponent; import dev.isxander.controlify.driver.Driver; -import dev.isxander.controlify.driver.dualsense.DS5EffectsState; +import dev.isxander.controlify.driver.dualsense.DualsenseEffectsState; import dev.isxander.controlify.rumble.RumbleState; import dev.isxander.controlify.rumble.TriggerRumbleState; import dev.isxander.controlify.utils.CUtil; @@ -60,7 +60,7 @@ public abstract class SDLCommonDriver implements Driver { protected TriggerRumbleComponent triggerRumbleComponent; protected HDHapticComponent hdHapticComponent; protected LEDComponent ledComponent; - protected DualSenseComponent dualSenseComponent; + protected DualsenseComponent dualSenseComponent; protected final boolean isRumbleSupported, isTriggerRumbleSupported; protected final boolean isDualsense; @@ -161,7 +161,7 @@ public void addComponents(ControllerEntity controller) { controller.setComponent(this.ledComponent = new LEDComponent(1)); } if (this.isDualsense) { - controller.setComponent(this.dualSenseComponent = new DualSenseComponent()); + controller.setComponent(this.dualSenseComponent = new DualsenseComponent()); } if (this.dualsenseAudioDev != null) { controller.setComponent(this.hdHapticComponent = new HDHapticComponent()); @@ -266,26 +266,26 @@ private void updateDualSense() { if (dualSenseComponent == null) return; if (this.dualSenseComponent.consumeDirty()) { - DS5EffectsState effectsState = new DS5EffectsState(); + DualsenseEffectsState effectsState = new DualsenseEffectsState(); // Left Trigger Effect Optional.ofNullable(this.dualSenseComponent.getLeftTriggerEffect()).ifPresent(effect -> { - effectsState.ucEnableBits1 |= DS5EffectsState.EnableBitFlags1.ALLOW_LEFT_TRIGGER_FFB; + effectsState.ucEnableBits1 |= DualsenseEffectsState.EnableBitFlags1.ALLOW_LEFT_TRIGGER_FFB; effectsState.rgucLeftTriggerEffect = effect.createState(); }); // Right Trigger Effect Optional.ofNullable(this.dualSenseComponent.getRightTriggerEffect()).ifPresent(effect -> { - effectsState.ucEnableBits1 |= DS5EffectsState.EnableBitFlags1.ALLOW_RIGHT_TRIGGER_FFB; + effectsState.ucEnableBits1 |= DualsenseEffectsState.EnableBitFlags1.ALLOW_RIGHT_TRIGGER_FFB; effectsState.rgucRightTriggerEffect = effect.createState(); }); // Mute Light - effectsState.ucEnableBits2 |= DS5EffectsState.EnableBitFlags2.ALLOW_MUTE_LIGHT; - effectsState.ucMicLightMode = DS5EffectsState.MuteLightState.fromBoolean(this.dualSenseComponent.getMuteLight()); + effectsState.ucEnableBits2 |= DualsenseEffectsState.EnableBitFlags2.ALLOW_MUTE_LIGHT; + effectsState.ucMicLightMode = DualsenseEffectsState.MuteLightState.fromBoolean(this.dualSenseComponent.getMuteLight()); try (Arena arena = Arena.ofConfined()) { - MemorySegment memory = arena.allocate(DS5EffectsState.LAYOUT); + MemorySegment memory = arena.allocate(DualsenseEffectsState.LAYOUT); effectsState.writeTo(memory); SDL_SendControllerEffect(ptrController, memory.asByteBuffer()); } From 52658a0df22ef5cc5605de1710bc5776fb82502a Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 18:43:39 +0100 Subject: [PATCH 5/8] fix spotless --- .../api/triggereffect/TriggerEffectApi.java | 6 ++++++ .../config/dto/profile/DualsenseConfig.java | 6 ++++++ .../config/settings/profile/DualsenseSettings.java | 6 ++++++ .../controller/dualsense/BuiltinTriggerEffects.java | 6 ++++++ .../controller/dualsense/TriggerEffectCodecs.java | 6 ++++++ .../controller/dualsense/TriggerEffectHolder.java | 6 ++++++ .../controller/dualsense/TriggerEffectManager.java | 6 ++++++ .../mixins/feature/triggereffect/ItemMixin.java | 12 ++++++++---- 8 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java b/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java index 2f4e04b82..8ef2cdb5e 100644 --- a/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java +++ b/src/main/java/dev/isxander/controlify/api/triggereffect/TriggerEffectApi.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.api.triggereffect; import dev.isxander.controlify.Controlify; diff --git a/src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java b/src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java index a163efa34..064842fd0 100644 --- a/src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java +++ b/src/main/java/dev/isxander/controlify/config/dto/profile/DualsenseConfig.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.config.dto.profile; import com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java b/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java index f503006a7..a69a951ac 100644 --- a/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java +++ b/src/main/java/dev/isxander/controlify/config/settings/profile/DualsenseSettings.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.config.settings.profile; import dev.isxander.controlify.config.dto.profile.DualsenseConfig; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java index 4e41d2469..c6f53079d 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/BuiltinTriggerEffects.java @@ -1,3 +1,9 @@ +/* + * 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; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java index 61a51b606..dbeca18eb 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java @@ -1,3 +1,9 @@ +/* + * 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 com.mojang.serialization.Codec; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java index 1ff72952c..cd56f3705 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectHolder.java @@ -1,3 +1,9 @@ +/* + * 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.driver.dualsense.DualsenseTriggerEffect; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java index a059715c2..0b8fb4fca 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java @@ -1,3 +1,9 @@ +/* + * 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.bindings.ControlifyBindings; diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java index c471bdf38..616e27245 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/triggereffect/ItemMixin.java @@ -1,3 +1,9 @@ +/* + * Copyright (C) 2026 isXander + * This file is part of Controlify. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ package dev.isxander.controlify.mixins.feature.triggereffect; import dev.isxander.controlify.controller.dualsense.TriggerEffectHolder; @@ -12,11 +18,9 @@ @Mixin(Item.class) public class ItemMixin implements TriggerEffectHolder { - @Unique - private @Nullable DualsenseTriggerEffect controlify$useEffect = null; + @Unique private @Nullable DualsenseTriggerEffect controlify$useEffect = null; - @Unique - private @Nullable DualsenseTriggerEffect controlify$swingEffect = null; + @Unique private @Nullable DualsenseTriggerEffect controlify$swingEffect = null; @Override public Optional controlify$getUseTriggerEffect() { From 4b294a6b4592740b8ca0583fc9b68c6fc26073b8 Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 18:59:39 +0100 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main/java/dev/isxander/controlify/Controlify.java | 1 - .../controlify/controller/dualsense/TriggerEffectCodecs.java | 3 +-- .../controlify/gui/screen/ControllerConfigScreenFactory.java | 2 +- src/main/resources/assets/controlify/lang/en_us.json | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index b37bd7451..b33cbe329 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -12,7 +12,6 @@ import dev.isxander.controlify.api.entrypoint.InitContext; import dev.isxander.controlify.api.entrypoint.PreInitContext; import dev.isxander.controlify.api.guide.*; -import dev.isxander.controlify.api.triggereffect.TriggerEffectApi; import dev.isxander.controlify.bindings.BindContext; import dev.isxander.controlify.bindings.ControlifyBindApiImpl; import dev.isxander.controlify.bindings.ControlifyBindings; diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java index dbeca18eb..f0df16355 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java @@ -54,9 +54,8 @@ private TriggerEffectCodecs() { .forGetter(DualsenseTriggerEffect.Vibration::frequency) ).apply(instance, DualsenseTriggerEffect.Vibration::new)); - public static final MapCodec MAP_CODEC_FEEDBACK_MULTIPLE_POSITION = RecordCodecBuilder.mapCodec(instance -> instance.group( - CExtraCodecs.byteArray(CExtraCodecs.byteRange(0, 9).listOf(10, 10)).fieldOf("strength") + CExtraCodecs.byteArray(CExtraCodecs.byteRange(0, 8).listOf(10, 10)).fieldOf("strength") .forGetter(DualsenseTriggerEffect.FeedbackMultiplePosition::strength) ).apply(instance, DualsenseTriggerEffect.FeedbackMultiplePosition::new)); diff --git a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java index 6483c597d..b79b00f63 100644 --- a/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java +++ b/src/main/java/dev/isxander/controlify/gui/screen/ControllerConfigScreenFactory.java @@ -696,7 +696,7 @@ private Optional makeTriggerEffectsGroup( if (controller.isPresent() && controller.get().dualSense().isEmpty()) { group.collapsed(true); - group.option(LabelOption.create(Component.translatable("controlify.gui.group.gyro.no_dualsense.tooltip").withStyle(ChatFormatting.RED))); + group.option(LabelOption.create(Component.translatable("controlify.gui.group.trigger_effects.no_dualsense.tooltip").withStyle(ChatFormatting.RED))); } group.option(Option.createBuilder() diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index 8c32de95a..4c4d30763 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -190,9 +190,9 @@ "controlify.gui.test_vibration": "Test Vibration", "controlify.gui.test_vibration.tooltip": "Test the vibration of your controller.", "controlify.gui.group.trigger_effects": "Trigger Effects", + "controlify.gui.group.trigger_effects.no_dualsense.tooltip": "This controller does not support adaptive triggers. You must have a DualSenseā„¢ controller or other compatible controller to use this feature.", "controlify.gui.trigger_effects": "Trigger Effects", "controlify.gui.trigger_effects.tooltip": "Enable DualSense Adaptive Trigger effects for items in game.", - "controlify.gui.show_keyboard.off": "Off", "controlify.gui.show_keyboard.controlify": "Controlify", "controlify.gui.show_keyboard.system": "System", From aaf0452c5c440faeecb43be3e1aa0621ecb3835f Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 19:01:44 +0100 Subject: [PATCH 7/8] Fix DualSense trigger effect cleanup --- .../dev/isxander/controlify/Controlify.java | 3 ++- .../dualsense/TriggerEffectManager.java | 19 ++++++++++--------- .../driver/sdl/SDLCommonDriver.java | 7 +++++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index b33cbe329..bede2a635 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -528,7 +528,8 @@ private void tickActiveController(ControllerEntity controller, boolean outOfFocu rumbleManager.ifPresent(RumbleManager::tick); } - triggerEffectManager.applyTriggerEffects(controller); + boolean controllerInputSuppressed = outOfFocus || currentInputMode() == InputMode.KEYBOARD_MOUSE; + triggerEffectManager.applyTriggerEffects(controller, controllerInputSuppressed); if (state.isGivingInput()) { minecraft.getFramerateLimitTracker().onInputReceived(); diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java index 0b8fb4fca..138f3cf65 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectManager.java @@ -32,10 +32,10 @@ public TriggerEffectManager(Minecraft minecraft) { this.swingItemComponentEffects = new LinkedHashMap<>(); } - public void applyTriggerEffects(ControllerEntity controller) { + public void applyTriggerEffects(ControllerEntity controller, boolean inputSuppressed) { controller.dualSense().ifPresent(ds -> { - ds.setLeftTriggerEffect(this.getCurrentLeftTriggerEffect(controller)); - ds.setRightTriggerEffect(this.getCurrentRightTriggerEffect(controller)); + ds.setLeftTriggerEffect(this.getCurrentLeftTriggerEffect(controller, inputSuppressed)); + ds.setRightTriggerEffect(this.getCurrentRightTriggerEffect(controller, inputSuppressed)); }); } @@ -47,8 +47,8 @@ public void registerSwingItemComponentEffect(DataComponentType componentT this.swingItemComponentEffects.put(componentType, (Function) effectFunction); } - public DualsenseTriggerEffect getCurrentLeftTriggerEffect(ControllerEntity controller) { - if (!this.shouldUseTriggerEffects(controller)) { + public DualsenseTriggerEffect getCurrentLeftTriggerEffect(ControllerEntity controller, boolean inputSuppressed) { + if (!this.shouldUseTriggerEffects(controller, inputSuppressed)) { return DualsenseTriggerEffect.Off.INSTANCE; } @@ -66,8 +66,8 @@ public DualsenseTriggerEffect getCurrentLeftTriggerEffect(ControllerEntity contr return DualsenseTriggerEffect.Off.INSTANCE; } - public DualsenseTriggerEffect getCurrentRightTriggerEffect(ControllerEntity controller) { - if (!this.shouldUseTriggerEffects(controller)) { + public DualsenseTriggerEffect getCurrentRightTriggerEffect(ControllerEntity controller, boolean inputSuppressed) { + if (!this.shouldUseTriggerEffects(controller, inputSuppressed)) { return DualsenseTriggerEffect.Off.INSTANCE; } @@ -85,8 +85,9 @@ public DualsenseTriggerEffect getCurrentRightTriggerEffect(ControllerEntity cont return DualsenseTriggerEffect.Off.INSTANCE; } - public boolean shouldUseTriggerEffects(ControllerEntity controller) { - return MinecraftUtil.getScreen() == null + public boolean shouldUseTriggerEffects(ControllerEntity controller, boolean inputSuppressed) { + return !inputSuppressed + && MinecraftUtil.getScreen() == null && this.minecraft.player != null && controller.dualSense().map(ds -> ds.settings().triggerEffects).orElse(false); } diff --git a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java index a0c508328..d974cbc86 100644 --- a/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/sdl/SDLCommonDriver.java @@ -23,6 +23,7 @@ import dev.isxander.controlify.controller.rumble.TriggerRumbleComponent; import dev.isxander.controlify.driver.Driver; import dev.isxander.controlify.driver.dualsense.DualsenseEffectsState; +import dev.isxander.controlify.driver.dualsense.DualsenseTriggerEffect; import dev.isxander.controlify.rumble.RumbleState; import dev.isxander.controlify.rumble.TriggerRumbleState; import dev.isxander.controlify.utils.CUtil; @@ -192,6 +193,12 @@ public void close() { throw new IllegalStateException("Tried to close controller when it's already closed."); } + if (dualSenseComponent != null) { + dualSenseComponent.setLeftTriggerEffect(DualsenseTriggerEffect.Off.INSTANCE); + dualSenseComponent.setRightTriggerEffect(DualsenseTriggerEffect.Off.INSTANCE); + updateDualSense(); + } + SDL_CloseController(ptrController); ptrController = null; From c10086ad584382f16f8dbc4f01d60b3fd70853a2 Mon Sep 17 00:00:00 2001 From: Xander Date: Wed, 29 Jul 2026 19:03:30 +0100 Subject: [PATCH 8/8] fix merge error --- .../controlify/controller/dualsense/TriggerEffectCodecs.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java index f0df16355..8f1822d89 100644 --- a/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java +++ b/src/main/java/dev/isxander/controlify/controller/dualsense/TriggerEffectCodecs.java @@ -54,6 +54,7 @@ private TriggerEffectCodecs() { .forGetter(DualsenseTriggerEffect.Vibration::frequency) ).apply(instance, DualsenseTriggerEffect.Vibration::new)); + public static final MapCodec MAP_CODEC_FEEDBACK_MULTIPLE_POSITION = RecordCodecBuilder.mapCodec(instance -> instance.group( CExtraCodecs.byteArray(CExtraCodecs.byteRange(0, 8).listOf(10, 10)).fieldOf("strength") .forGetter(DualsenseTriggerEffect.FeedbackMultiplePosition::strength)