From 2f4cbfa0993db887e6004bd987a17f979162f5dd Mon Sep 17 00:00:00 2001 From: isXander Date: Wed, 5 Apr 2023 21:15:52 +0100 Subject: [PATCH] vibration conflict support - multiple vibrations can play at once --- .../dev/isxander/controlify/Controlify.java | 2 +- .../blockbreak/MultiPlayerGameModeMixin.java | 15 ++-- .../LevelRendererMixin.java | 6 +- .../rumble/useitem/LocalPlayerMixin.java | 48 +++++++---- .../controlify/rumble/BasicRumbleEffect.java | 18 +++-- .../rumble/ContinuousRumbleEffect.java | 80 +++++++++++++++++-- .../controlify/rumble/RumbleEffect.java | 13 ++- .../controlify/rumble/RumbleManager.java | 64 ++++++++++----- src/main/resources/controlify.mixins.json | 2 +- 9 files changed, 180 insertions(+), 68 deletions(-) rename src/main/java/dev/isxander/controlify/mixins/feature/rumble/{sounds => levelevents}/LevelRendererMixin.java (94%) diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 4735796..000e965 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -175,7 +175,7 @@ public class Controlify implements ControlifyApi { controller.updateState(); else { controller.clearState(); - controller.rumbleManager().stopCurrentEffect(); + controller.rumbleManager().clearEffects(); } controller.rumbleManager().tick(); } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java index 276f1c0..1111518 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java @@ -53,18 +53,13 @@ public class MultiPlayerGameModeMixin { } private void startRumble(BlockState state) { - ContinuousRumbleEffect effect = new ContinuousRumbleEffect(tick -> - new RumbleState( + var effect = ContinuousRumbleEffect.builder() + .byTick(tick -> new RumbleState( 0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f, 0.01f - ) - ){ - @Override - public boolean isFinished() { - // insta-break blocks will stop the same tick it starts, so it must not stop until 1 tick has played - return super.isFinished() && currentTick() > 0; - } - }; + )) + .minTime(1) + .build(); blockBreakRumble = effect; ControlifyApi.get().currentController().rumbleManager().play(RumbleSource.BLOCK_DESTROY, effect); diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/levelevents/LevelRendererMixin.java similarity index 94% rename from src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java rename to src/main/java/dev/isxander/controlify/mixins/feature/rumble/levelevents/LevelRendererMixin.java index ace9ace..f96125d 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/levelevents/LevelRendererMixin.java @@ -1,4 +1,4 @@ -package dev.isxander.controlify.mixins.feature.rumble.sounds; +package dev.isxander.controlify.mixins.feature.rumble.levelevents; import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.rumble.BasicRumbleEffect; @@ -40,7 +40,7 @@ public class LevelRendererMixin { float easeOutQuad = Easings.easeOutQuad(t); return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad); }, 63) - ) + ).prioritised(10) ); case LevelEvent.SOUND_WITHER_BOSS_SPAWN -> rumble( RumbleSource.GLOBAL_EVENT, @@ -51,7 +51,7 @@ public class LevelRendererMixin { float easeOutQuad = 1 - (1 - t) * (1 - t); return new RumbleState(0f, 1 - easeOutQuad); }, 56) - ) + ).prioritised(10) ); } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java index 9e76081..c4558a5 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java @@ -8,6 +8,7 @@ import dev.isxander.controlify.rumble.RumbleState; import net.minecraft.client.player.LocalPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.item.BowItem; +import net.minecraft.world.item.CrossbowItem; import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; @@ -20,22 +21,37 @@ public abstract class LocalPlayerMixin extends LivingEntityMixin { @Override protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) { switch (stack.getUseAnimation()) { - case BOW, CROSSBOW, SPEAR -> - startRumble(new ContinuousRumbleEffect(tick -> - new RumbleState(tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f, BowItem.getPowerForTime(tick)) - )); - case BLOCK, SPYGLASS -> - startRumble(new ContinuousRumbleEffect(tick -> - new RumbleState(0f, tick % 4 / 4f * 0.12f + 0.05f) - )); - case EAT, DRINK -> - startRumble(new ContinuousRumbleEffect(tick -> - new RumbleState(0.05f, 0.1f) - )); - case TOOT_HORN -> - startRumble(new ContinuousRumbleEffect(tick -> - new RumbleState(Math.min(1f, tick / 10f), 0.25f) - )); + case BOW -> startRumble(ContinuousRumbleEffect.builder() + .byTick(tick -> new RumbleState( + tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f, + BowItem.getPowerForTime(tick) + )) + .build()); + case CROSSBOW -> { + int chargeDuration = CrossbowItem.getChargeDuration(stack); + startRumble(ContinuousRumbleEffect.builder() + .byTick(tick -> new RumbleState( + 0f, + (float) tick / chargeDuration + )) + .timeout(chargeDuration) + .build()); + } + case BLOCK, SPYGLASS -> startRumble(ContinuousRumbleEffect.builder() + .byTick(tick -> new RumbleState( + 0f, + tick % 4 / 4f * 0.12f + 0.05f + )) + .build()); + case EAT, DRINK -> startRumble(ContinuousRumbleEffect.builder() + .constant(0.05f, 0.1f) + .build()); + case TOOT_HORN -> startRumble(ContinuousRumbleEffect.builder() + .byTick(tick -> new RumbleState( + Math.min(1f, tick / 10f), + 0.25f + )) + .build()); } } diff --git a/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java index 710aae6..b00a624 100644 --- a/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java +++ b/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java @@ -17,14 +17,25 @@ public final class BasicRumbleEffect implements RumbleEffect { } @Override - public RumbleState nextState() { + public void tick() { tick++; if (tick >= keyframes.length) finished = true; + } + + @Override + public RumbleState currentState() { + if (tick == 0) + throw new IllegalStateException("Effect hasn't ticked yet."); return keyframes[tick - 1]; } + @Override + public int age() { + return tick; + } + @Override public boolean isFinished() { return finished; @@ -137,9 +148,4 @@ public final class BasicRumbleEffect implements RumbleEffect { } return effect; } - - public ContinuousRumbleEffect continuous() { - int lastIndex = this.states().length - 1; - return new ContinuousRumbleEffect(index -> this.states()[index % lastIndex], this.priority()); - } } diff --git a/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java index 55149ad..c3d38a6 100644 --- a/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java +++ b/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java @@ -1,25 +1,34 @@ package dev.isxander.controlify.rumble; +import org.apache.commons.lang3.Validate; + import java.util.function.Function; public class ContinuousRumbleEffect implements RumbleEffect { private final Function stateFunction; private final int priority; + private final int timeout; + private final int minTime; private int tick; private boolean stopped; - public ContinuousRumbleEffect(Function stateFunction) { - this(stateFunction, 0); - } - - public ContinuousRumbleEffect(Function stateFunction, int priority) { + public ContinuousRumbleEffect(Function stateFunction, int priority, int timeout, int minTime) { this.stateFunction = stateFunction; this.priority = priority; + this.timeout = timeout; + this.minTime = minTime; } @Override - public RumbleState nextState() { + public void tick() { tick++; + } + + @Override + public RumbleState currentState() { + if (tick == 0) + throw new IllegalStateException("Effect hasn't ticked yet."); + return stateFunction.apply(tick - 1); } @@ -27,17 +36,72 @@ public class ContinuousRumbleEffect implements RumbleEffect { stopped = true; } - public int currentTick() { + @Override + public int age() { return tick; } @Override public boolean isFinished() { - return stopped; + return (stopped || (timeout > 0 && tick >= timeout)) && tick >= minTime; } @Override public int priority() { return this.priority; } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Function stateFunction; + private int priority; + private int timeout = -1; + private int minTime; + + private Builder() { + } + + public Builder byTick(Function stateFunction) { + this.stateFunction = stateFunction; + return this; + } + + public Builder constant(RumbleState state) { + this.stateFunction = tick -> state; + return this; + } + + public Builder constant(float strong, float weak) { + return this.constant(new RumbleState(strong, weak)); + } + + public Builder timeout(int timeoutTicks) { + Validate.isTrue(timeoutTicks >= 0, "the timeout cannot be negative!"); + + this.timeout = timeoutTicks; + return this; + } + + public Builder minTime(int minTimeTicks) { + Validate.isTrue(minTimeTicks >= 0, "the minimum time cannot be negative!"); + + this.minTime = minTimeTicks; + return this; + } + + public Builder priority(int priority) { + this.priority = priority; + return this; + } + + public ContinuousRumbleEffect build() { + Validate.notNull(stateFunction, "stateFunction cannot be null!"); + Validate.isTrue(minTime <= timeout || timeout == -1, "the minimum time cannot be greater than the timeout!"); + + return new ContinuousRumbleEffect(stateFunction, priority, timeout, minTime); + } + } } diff --git a/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java index d7ca8d0..7f6deda 100644 --- a/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java +++ b/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java @@ -1,9 +1,18 @@ package dev.isxander.controlify.rumble; -public interface RumbleEffect { - RumbleState nextState(); +public interface RumbleEffect extends Comparable { + void tick(); + RumbleState currentState(); boolean isFinished(); int priority(); + int age(); + + @Override + default int compareTo(RumbleEffect o) { + int priorityCompare = Integer.compare(o.priority(), this.priority()); + if (priorityCompare != 0) return priorityCompare; + return Integer.compare(this.age(), o.age()); + } } diff --git a/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java b/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java index 6d5dddc..9404d4c 100644 --- a/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java +++ b/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java @@ -1,11 +1,18 @@ package dev.isxander.controlify.rumble; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.PriorityQueue; +import java.util.Queue; + public class RumbleManager { private final RumbleCapable controller; - private RumbleEffectInstance playingEffect; + private final Queue effectQueue; public RumbleManager(RumbleCapable controller) { this.controller = controller; + this.effectQueue = new PriorityQueue<>(Comparator.comparing(RumbleEffectInstance::effect)); } @Deprecated @@ -17,34 +24,49 @@ public class RumbleManager { if (!controller.canRumble()) return; - playingEffect = new RumbleEffectInstance(source, effect); - } - - public boolean isPlaying() { - return playingEffect != null; - } - - public void stopCurrentEffect() { - if (playingEffect == null) - return; - - controller.setRumble(0f, 0f, RumbleSource.MASTER); - playingEffect = null; + effectQueue.add(new RumbleEffectInstance(source, effect)); } public void tick() { - if (playingEffect == null) - return; + RumbleEffectInstance effect; + do { + effect = effectQueue.peek(); - if (playingEffect.effect().isFinished()) { - stopCurrentEffect(); + // if we have no effects, break out of loop and get the null check + if (effect == null) + break; + + // if the effect is finished, remove and set null, so we loop again + if (effect.effect().isFinished()) { + effectQueue.remove(effect); + effect = null; + } + } while (effect == null); + + if (effect == null) { + controller.setRumble(0f, 0f, RumbleSource.MASTER); return; } - RumbleState state = playingEffect.effect().nextState(); - controller.setRumble(state.strong(), state.weak(), playingEffect.source()); + effectQueue.removeIf(e -> e.effect().isFinished()); + effectQueue.forEach(e -> e.effect().tick()); + + RumbleState state = effect.effect().currentState(); + controller.setRumble(state.strong(), state.weak(), effect.source()); } - private record RumbleEffectInstance(RumbleSource source, RumbleEffect effect) { + public void clearEffects() { + effectQueue.clear(); + } + + public boolean isPlaying() { + return !effectQueue.isEmpty(); + } + + private record RumbleEffectInstance(RumbleSource source, RumbleEffect effect) implements Comparable { + @Override + public int compareTo(@NotNull RumbleManager.RumbleEffectInstance o) { + return effect.compareTo(o.effect); + } } } diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 5e990aa..bf5e8c6 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -11,7 +11,7 @@ "core.GLXMixin", "feature.rumble.explosion.LightningBoltMixin", "feature.rumble.itembreak.LivingEntityMixin", - "feature.rumble.sounds.LevelRendererMixin", + "feature.rumble.levelevents.LevelRendererMixin", "feature.rumble.useitem.LivingEntityMixin" ], "client": [