1
0
forked from Clones/Controlify

vibration conflict support - multiple vibrations can play at once

This commit is contained in:
isXander
2023-04-05 21:15:52 +01:00
parent a3583ab5c8
commit 2f4cbfa099
9 changed files with 180 additions and 68 deletions

View File

@ -175,7 +175,7 @@ public class Controlify implements ControlifyApi {
controller.updateState(); controller.updateState();
else { else {
controller.clearState(); controller.clearState();
controller.rumbleManager().stopCurrentEffect(); controller.rumbleManager().clearEffects();
} }
controller.rumbleManager().tick(); controller.rumbleManager().tick();
} }

View File

@ -53,18 +53,13 @@ public class MultiPlayerGameModeMixin {
} }
private void startRumble(BlockState state) { private void startRumble(BlockState state) {
ContinuousRumbleEffect effect = new ContinuousRumbleEffect(tick -> var effect = ContinuousRumbleEffect.builder()
new RumbleState( .byTick(tick -> new RumbleState(
0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f, 0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f,
0.01f 0.01f
) ))
){ .minTime(1)
@Override .build();
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;
}
};
blockBreakRumble = effect; blockBreakRumble = effect;
ControlifyApi.get().currentController().rumbleManager().play(RumbleSource.BLOCK_DESTROY, effect); ControlifyApi.get().currentController().rumbleManager().play(RumbleSource.BLOCK_DESTROY, effect);

View File

@ -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.api.ControlifyApi;
import dev.isxander.controlify.rumble.BasicRumbleEffect; import dev.isxander.controlify.rumble.BasicRumbleEffect;
@ -40,7 +40,7 @@ public class LevelRendererMixin {
float easeOutQuad = Easings.easeOutQuad(t); float easeOutQuad = Easings.easeOutQuad(t);
return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad); return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad);
}, 63) }, 63)
) ).prioritised(10)
); );
case LevelEvent.SOUND_WITHER_BOSS_SPAWN -> rumble( case LevelEvent.SOUND_WITHER_BOSS_SPAWN -> rumble(
RumbleSource.GLOBAL_EVENT, RumbleSource.GLOBAL_EVENT,
@ -51,7 +51,7 @@ public class LevelRendererMixin {
float easeOutQuad = 1 - (1 - t) * (1 - t); float easeOutQuad = 1 - (1 - t) * (1 - t);
return new RumbleState(0f, 1 - easeOutQuad); return new RumbleState(0f, 1 - easeOutQuad);
}, 56) }, 56)
) ).prioritised(10)
); );
} }
} }

View File

@ -8,6 +8,7 @@ import dev.isxander.controlify.rumble.RumbleState;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.BowItem; import net.minecraft.world.item.BowItem;
import net.minecraft.world.item.CrossbowItem;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
@ -20,22 +21,37 @@ public abstract class LocalPlayerMixin extends LivingEntityMixin {
@Override @Override
protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) { protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) {
switch (stack.getUseAnimation()) { switch (stack.getUseAnimation()) {
case BOW, CROSSBOW, SPEAR -> case BOW -> startRumble(ContinuousRumbleEffect.builder()
startRumble(new ContinuousRumbleEffect(tick -> .byTick(tick -> new RumbleState(
new RumbleState(tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f, BowItem.getPowerForTime(tick)) tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f,
)); BowItem.getPowerForTime(tick)
case BLOCK, SPYGLASS -> ))
startRumble(new ContinuousRumbleEffect(tick -> .build());
new RumbleState(0f, tick % 4 / 4f * 0.12f + 0.05f) case CROSSBOW -> {
)); int chargeDuration = CrossbowItem.getChargeDuration(stack);
case EAT, DRINK -> startRumble(ContinuousRumbleEffect.builder()
startRumble(new ContinuousRumbleEffect(tick -> .byTick(tick -> new RumbleState(
new RumbleState(0.05f, 0.1f) 0f,
)); (float) tick / chargeDuration
case TOOT_HORN -> ))
startRumble(new ContinuousRumbleEffect(tick -> .timeout(chargeDuration)
new RumbleState(Math.min(1f, tick / 10f), 0.25f) .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());
} }
} }

View File

@ -17,14 +17,25 @@ public final class BasicRumbleEffect implements RumbleEffect {
} }
@Override @Override
public RumbleState nextState() { public void tick() {
tick++; tick++;
if (tick >= keyframes.length) if (tick >= keyframes.length)
finished = true; finished = true;
}
@Override
public RumbleState currentState() {
if (tick == 0)
throw new IllegalStateException("Effect hasn't ticked yet.");
return keyframes[tick - 1]; return keyframes[tick - 1];
} }
@Override
public int age() {
return tick;
}
@Override @Override
public boolean isFinished() { public boolean isFinished() {
return finished; return finished;
@ -137,9 +148,4 @@ public final class BasicRumbleEffect implements RumbleEffect {
} }
return effect; return effect;
} }
public ContinuousRumbleEffect continuous() {
int lastIndex = this.states().length - 1;
return new ContinuousRumbleEffect(index -> this.states()[index % lastIndex], this.priority());
}
} }

View File

@ -1,25 +1,34 @@
package dev.isxander.controlify.rumble; package dev.isxander.controlify.rumble;
import org.apache.commons.lang3.Validate;
import java.util.function.Function; import java.util.function.Function;
public class ContinuousRumbleEffect implements RumbleEffect { public class ContinuousRumbleEffect implements RumbleEffect {
private final Function<Integer, RumbleState> stateFunction; private final Function<Integer, RumbleState> stateFunction;
private final int priority; private final int priority;
private final int timeout;
private final int minTime;
private int tick; private int tick;
private boolean stopped; private boolean stopped;
public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction) { public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction, int priority, int timeout, int minTime) {
this(stateFunction, 0);
}
public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction, int priority) {
this.stateFunction = stateFunction; this.stateFunction = stateFunction;
this.priority = priority; this.priority = priority;
this.timeout = timeout;
this.minTime = minTime;
} }
@Override @Override
public RumbleState nextState() { public void tick() {
tick++; tick++;
}
@Override
public RumbleState currentState() {
if (tick == 0)
throw new IllegalStateException("Effect hasn't ticked yet.");
return stateFunction.apply(tick - 1); return stateFunction.apply(tick - 1);
} }
@ -27,17 +36,72 @@ public class ContinuousRumbleEffect implements RumbleEffect {
stopped = true; stopped = true;
} }
public int currentTick() { @Override
public int age() {
return tick; return tick;
} }
@Override @Override
public boolean isFinished() { public boolean isFinished() {
return stopped; return (stopped || (timeout > 0 && tick >= timeout)) && tick >= minTime;
} }
@Override @Override
public int priority() { public int priority() {
return this.priority; return this.priority;
} }
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Function<Integer, RumbleState> stateFunction;
private int priority;
private int timeout = -1;
private int minTime;
private Builder() {
}
public Builder byTick(Function<Integer, RumbleState> 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);
}
}
} }

View File

@ -1,9 +1,18 @@
package dev.isxander.controlify.rumble; package dev.isxander.controlify.rumble;
public interface RumbleEffect { public interface RumbleEffect extends Comparable<RumbleEffect> {
RumbleState nextState(); void tick();
RumbleState currentState();
boolean isFinished(); boolean isFinished();
int priority(); 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());
}
} }

View File

@ -1,11 +1,18 @@
package dev.isxander.controlify.rumble; 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 { public class RumbleManager {
private final RumbleCapable controller; private final RumbleCapable controller;
private RumbleEffectInstance playingEffect; private final Queue<RumbleEffectInstance> effectQueue;
public RumbleManager(RumbleCapable controller) { public RumbleManager(RumbleCapable controller) {
this.controller = controller; this.controller = controller;
this.effectQueue = new PriorityQueue<>(Comparator.comparing(RumbleEffectInstance::effect));
} }
@Deprecated @Deprecated
@ -17,34 +24,49 @@ public class RumbleManager {
if (!controller.canRumble()) if (!controller.canRumble())
return; return;
playingEffect = new RumbleEffectInstance(source, effect); effectQueue.add(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;
} }
public void tick() { public void tick() {
if (playingEffect == null) RumbleEffectInstance effect;
return; do {
effect = effectQueue.peek();
if (playingEffect.effect().isFinished()) { // if we have no effects, break out of loop and get the null check
stopCurrentEffect(); 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; return;
} }
RumbleState state = playingEffect.effect().nextState(); effectQueue.removeIf(e -> e.effect().isFinished());
controller.setRumble(state.strong(), state.weak(), playingEffect.source()); 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<RumbleEffectInstance> {
@Override
public int compareTo(@NotNull RumbleManager.RumbleEffectInstance o) {
return effect.compareTo(o.effect);
}
} }
} }

View File

@ -11,7 +11,7 @@
"core.GLXMixin", "core.GLXMixin",
"feature.rumble.explosion.LightningBoltMixin", "feature.rumble.explosion.LightningBoltMixin",
"feature.rumble.itembreak.LivingEntityMixin", "feature.rumble.itembreak.LivingEntityMixin",
"feature.rumble.sounds.LevelRendererMixin", "feature.rumble.levelevents.LevelRendererMixin",
"feature.rumble.useitem.LivingEntityMixin" "feature.rumble.useitem.LivingEntityMixin"
], ],
"client": [ "client": [