404 lines
15 KiB
Java
404 lines
15 KiB
Java
package net.minecraft.world.level.gameevent.vibrations;
|
|
|
|
import com.mojang.serialization.Codec;
|
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
|
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
import java.util.function.ToIntFunction;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.advancements.CriteriaTriggers;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.Direction;
|
|
import net.minecraft.core.Holder;
|
|
import net.minecraft.core.particles.VibrationParticleOption;
|
|
import net.minecraft.resources.ResourceKey;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.tags.BlockTags;
|
|
import net.minecraft.tags.GameEventTags;
|
|
import net.minecraft.tags.TagKey;
|
|
import net.minecraft.util.ExtraCodecs;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.ClipBlockStateContext;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.gameevent.GameEvent;
|
|
import net.minecraft.world.level.gameevent.GameEventListener;
|
|
import net.minecraft.world.level.gameevent.PositionSource;
|
|
import net.minecraft.world.level.gameevent.GameEvent.Context;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import net.minecraft.world.phys.HitResult.Type;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
public interface VibrationSystem {
|
|
List<ResourceKey<GameEvent>> RESONANCE_EVENTS = List.of(
|
|
GameEvent.RESONATE_1.key(),
|
|
GameEvent.RESONATE_2.key(),
|
|
GameEvent.RESONATE_3.key(),
|
|
GameEvent.RESONATE_4.key(),
|
|
GameEvent.RESONATE_5.key(),
|
|
GameEvent.RESONATE_6.key(),
|
|
GameEvent.RESONATE_7.key(),
|
|
GameEvent.RESONATE_8.key(),
|
|
GameEvent.RESONATE_9.key(),
|
|
GameEvent.RESONATE_10.key(),
|
|
GameEvent.RESONATE_11.key(),
|
|
GameEvent.RESONATE_12.key(),
|
|
GameEvent.RESONATE_13.key(),
|
|
GameEvent.RESONATE_14.key(),
|
|
GameEvent.RESONATE_15.key()
|
|
);
|
|
int NO_VIBRATION_FREQUENCY = 0;
|
|
ToIntFunction<ResourceKey<GameEvent>> VIBRATION_FREQUENCY_FOR_EVENT = Util.make(new Reference2IntOpenHashMap<>(), reference2IntOpenHashMap -> {
|
|
reference2IntOpenHashMap.defaultReturnValue(0);
|
|
reference2IntOpenHashMap.put(GameEvent.STEP.key(), 1);
|
|
reference2IntOpenHashMap.put(GameEvent.SWIM.key(), 1);
|
|
reference2IntOpenHashMap.put(GameEvent.FLAP.key(), 1);
|
|
reference2IntOpenHashMap.put(GameEvent.PROJECTILE_LAND.key(), 2);
|
|
reference2IntOpenHashMap.put(GameEvent.HIT_GROUND.key(), 2);
|
|
reference2IntOpenHashMap.put(GameEvent.SPLASH.key(), 2);
|
|
reference2IntOpenHashMap.put(GameEvent.ITEM_INTERACT_FINISH.key(), 3);
|
|
reference2IntOpenHashMap.put(GameEvent.PROJECTILE_SHOOT.key(), 3);
|
|
reference2IntOpenHashMap.put(GameEvent.INSTRUMENT_PLAY.key(), 3);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_ACTION.key(), 4);
|
|
reference2IntOpenHashMap.put(GameEvent.ELYTRA_GLIDE.key(), 4);
|
|
reference2IntOpenHashMap.put(GameEvent.UNEQUIP.key(), 4);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_DISMOUNT.key(), 5);
|
|
reference2IntOpenHashMap.put(GameEvent.EQUIP.key(), 5);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_INTERACT.key(), 6);
|
|
reference2IntOpenHashMap.put(GameEvent.SHEAR.key(), 6);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_MOUNT.key(), 6);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_DAMAGE.key(), 7);
|
|
reference2IntOpenHashMap.put(GameEvent.DRINK.key(), 8);
|
|
reference2IntOpenHashMap.put(GameEvent.EAT.key(), 8);
|
|
reference2IntOpenHashMap.put(GameEvent.CONTAINER_CLOSE.key(), 9);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_CLOSE.key(), 9);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_DEACTIVATE.key(), 9);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_DETACH.key(), 9);
|
|
reference2IntOpenHashMap.put(GameEvent.CONTAINER_OPEN.key(), 10);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_OPEN.key(), 10);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_ACTIVATE.key(), 10);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_ATTACH.key(), 10);
|
|
reference2IntOpenHashMap.put(GameEvent.PRIME_FUSE.key(), 10);
|
|
reference2IntOpenHashMap.put(GameEvent.NOTE_BLOCK_PLAY.key(), 10);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_CHANGE.key(), 11);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_DESTROY.key(), 12);
|
|
reference2IntOpenHashMap.put(GameEvent.FLUID_PICKUP.key(), 12);
|
|
reference2IntOpenHashMap.put(GameEvent.BLOCK_PLACE.key(), 13);
|
|
reference2IntOpenHashMap.put(GameEvent.FLUID_PLACE.key(), 13);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_PLACE.key(), 14);
|
|
reference2IntOpenHashMap.put(GameEvent.LIGHTNING_STRIKE.key(), 14);
|
|
reference2IntOpenHashMap.put(GameEvent.TELEPORT.key(), 14);
|
|
reference2IntOpenHashMap.put(GameEvent.ENTITY_DIE.key(), 15);
|
|
reference2IntOpenHashMap.put(GameEvent.EXPLODE.key(), 15);
|
|
|
|
for (int i = 1; i <= 15; i++) {
|
|
reference2IntOpenHashMap.put(getResonanceEventByFrequency(i), i);
|
|
}
|
|
});
|
|
|
|
VibrationSystem.Data getVibrationData();
|
|
|
|
VibrationSystem.User getVibrationUser();
|
|
|
|
static int getGameEventFrequency(Holder<GameEvent> gameEvent) {
|
|
return (Integer)gameEvent.unwrapKey().map(VibrationSystem::getGameEventFrequency).orElse(0);
|
|
}
|
|
|
|
static int getGameEventFrequency(ResourceKey<GameEvent> eventKey) {
|
|
return VIBRATION_FREQUENCY_FOR_EVENT.applyAsInt(eventKey);
|
|
}
|
|
|
|
static ResourceKey<GameEvent> getResonanceEventByFrequency(int frequency) {
|
|
return (ResourceKey<GameEvent>)RESONANCE_EVENTS.get(frequency - 1);
|
|
}
|
|
|
|
static int getRedstoneStrengthForDistance(float distance, int maxDistance) {
|
|
double d = 15.0 / maxDistance;
|
|
return Math.max(1, 15 - Mth.floor(d * distance));
|
|
}
|
|
|
|
public static final class Data {
|
|
public static Codec<VibrationSystem.Data> CODEC = RecordCodecBuilder.create(
|
|
instance -> instance.group(
|
|
VibrationInfo.CODEC.lenientOptionalFieldOf("event").forGetter(data -> Optional.ofNullable(data.currentVibration)),
|
|
VibrationSelector.CODEC.fieldOf("selector").forGetter(VibrationSystem.Data::getSelectionStrategy),
|
|
ExtraCodecs.NON_NEGATIVE_INT.fieldOf("event_delay").orElse(0).forGetter(VibrationSystem.Data::getTravelTimeInTicks)
|
|
)
|
|
.apply(instance, (optional, vibrationSelector, integer) -> new VibrationSystem.Data((VibrationInfo)optional.orElse(null), vibrationSelector, integer, true))
|
|
);
|
|
public static final String NBT_TAG_KEY = "listener";
|
|
@Nullable
|
|
VibrationInfo currentVibration;
|
|
private int travelTimeInTicks;
|
|
final VibrationSelector selectionStrategy;
|
|
private boolean reloadVibrationParticle;
|
|
|
|
private Data(@Nullable VibrationInfo currentVibration, VibrationSelector selectionStrategy, int travelTimeInTicks, boolean reloadVibrationParticle) {
|
|
this.currentVibration = currentVibration;
|
|
this.travelTimeInTicks = travelTimeInTicks;
|
|
this.selectionStrategy = selectionStrategy;
|
|
this.reloadVibrationParticle = reloadVibrationParticle;
|
|
}
|
|
|
|
public Data() {
|
|
this(null, new VibrationSelector(), 0, false);
|
|
}
|
|
|
|
public VibrationSelector getSelectionStrategy() {
|
|
return this.selectionStrategy;
|
|
}
|
|
|
|
@Nullable
|
|
public VibrationInfo getCurrentVibration() {
|
|
return this.currentVibration;
|
|
}
|
|
|
|
public void setCurrentVibration(@Nullable VibrationInfo currentVibration) {
|
|
this.currentVibration = currentVibration;
|
|
}
|
|
|
|
public int getTravelTimeInTicks() {
|
|
return this.travelTimeInTicks;
|
|
}
|
|
|
|
public void setTravelTimeInTicks(int travelTimeInTicks) {
|
|
this.travelTimeInTicks = travelTimeInTicks;
|
|
}
|
|
|
|
public void decrementTravelTime() {
|
|
this.travelTimeInTicks = Math.max(0, this.travelTimeInTicks - 1);
|
|
}
|
|
|
|
public boolean shouldReloadVibrationParticle() {
|
|
return this.reloadVibrationParticle;
|
|
}
|
|
|
|
public void setReloadVibrationParticle(boolean reloadVibrationParticle) {
|
|
this.reloadVibrationParticle = reloadVibrationParticle;
|
|
}
|
|
}
|
|
|
|
public static class Listener implements GameEventListener {
|
|
private final VibrationSystem system;
|
|
|
|
public Listener(VibrationSystem system) {
|
|
this.system = system;
|
|
}
|
|
|
|
@Override
|
|
public PositionSource getListenerSource() {
|
|
return this.system.getVibrationUser().getPositionSource();
|
|
}
|
|
|
|
@Override
|
|
public int getListenerRadius() {
|
|
return this.system.getVibrationUser().getListenerRadius();
|
|
}
|
|
|
|
@Override
|
|
public boolean handleGameEvent(ServerLevel level, Holder<GameEvent> gameEvent, Context context, Vec3 pos) {
|
|
VibrationSystem.Data data = this.system.getVibrationData();
|
|
VibrationSystem.User user = this.system.getVibrationUser();
|
|
if (data.getCurrentVibration() != null) {
|
|
return false;
|
|
} else if (!user.isValidVibration(gameEvent, context)) {
|
|
return false;
|
|
} else {
|
|
Optional<Vec3> optional = user.getPositionSource().getPosition(level);
|
|
if (optional.isEmpty()) {
|
|
return false;
|
|
} else {
|
|
Vec3 vec3 = (Vec3)optional.get();
|
|
if (!user.canReceiveVibration(level, BlockPos.containing(pos), gameEvent, context)) {
|
|
return false;
|
|
} else if (isOccluded(level, pos, vec3)) {
|
|
return false;
|
|
} else {
|
|
this.scheduleVibration(level, data, gameEvent, context, pos, vec3);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void forceScheduleVibration(ServerLevel level, Holder<GameEvent> gameEvent, Context context, Vec3 pos) {
|
|
this.system
|
|
.getVibrationUser()
|
|
.getPositionSource()
|
|
.getPosition(level)
|
|
.ifPresent(vec32 -> this.scheduleVibration(level, this.system.getVibrationData(), gameEvent, context, pos, vec32));
|
|
}
|
|
|
|
private void scheduleVibration(ServerLevel level, VibrationSystem.Data data, Holder<GameEvent> gameEvent, Context context, Vec3 pos, Vec3 sensorPos) {
|
|
data.selectionStrategy.addCandidate(new VibrationInfo(gameEvent, (float)pos.distanceTo(sensorPos), pos, context.sourceEntity()), level.getGameTime());
|
|
}
|
|
|
|
public static float distanceBetweenInBlocks(BlockPos pos1, BlockPos pos2) {
|
|
return (float)Math.sqrt(pos1.distSqr(pos2));
|
|
}
|
|
|
|
private static boolean isOccluded(Level level, Vec3 eventPos, Vec3 vibrationUserPos) {
|
|
Vec3 vec3 = new Vec3(Mth.floor(eventPos.x) + 0.5, Mth.floor(eventPos.y) + 0.5, Mth.floor(eventPos.z) + 0.5);
|
|
Vec3 vec32 = new Vec3(Mth.floor(vibrationUserPos.x) + 0.5, Mth.floor(vibrationUserPos.y) + 0.5, Mth.floor(vibrationUserPos.z) + 0.5);
|
|
|
|
for (Direction direction : Direction.values()) {
|
|
Vec3 vec33 = vec3.relative(direction, 1.0E-5F);
|
|
if (level.isBlockInLine(new ClipBlockStateContext(vec33, vec32, blockState -> blockState.is(BlockTags.OCCLUDES_VIBRATION_SIGNALS))).getType() != Type.BLOCK
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public interface Ticker {
|
|
static void tick(Level level, VibrationSystem.Data data, VibrationSystem.User user) {
|
|
if (level instanceof ServerLevel serverLevel) {
|
|
if (data.currentVibration == null) {
|
|
trySelectAndScheduleVibration(serverLevel, data, user);
|
|
}
|
|
|
|
if (data.currentVibration != null) {
|
|
boolean bl = data.getTravelTimeInTicks() > 0;
|
|
tryReloadVibrationParticle(serverLevel, data, user);
|
|
data.decrementTravelTime();
|
|
if (data.getTravelTimeInTicks() <= 0) {
|
|
bl = receiveVibration(serverLevel, data, user, data.currentVibration);
|
|
}
|
|
|
|
if (bl) {
|
|
user.onDataChanged();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void trySelectAndScheduleVibration(ServerLevel level, VibrationSystem.Data data, VibrationSystem.User user) {
|
|
data.getSelectionStrategy().chosenCandidate(level.getGameTime()).ifPresent(vibrationInfo -> {
|
|
data.setCurrentVibration(vibrationInfo);
|
|
Vec3 vec3 = vibrationInfo.pos();
|
|
data.setTravelTimeInTicks(user.calculateTravelTimeInTicks(vibrationInfo.distance()));
|
|
level.sendParticles(new VibrationParticleOption(user.getPositionSource(), data.getTravelTimeInTicks()), vec3.x, vec3.y, vec3.z, 1, 0.0, 0.0, 0.0, 0.0);
|
|
user.onDataChanged();
|
|
data.getSelectionStrategy().startOver();
|
|
});
|
|
}
|
|
|
|
private static void tryReloadVibrationParticle(ServerLevel level, VibrationSystem.Data data, VibrationSystem.User user) {
|
|
if (data.shouldReloadVibrationParticle()) {
|
|
if (data.currentVibration == null) {
|
|
data.setReloadVibrationParticle(false);
|
|
} else {
|
|
Vec3 vec3 = data.currentVibration.pos();
|
|
PositionSource positionSource = user.getPositionSource();
|
|
Vec3 vec32 = (Vec3)positionSource.getPosition(level).orElse(vec3);
|
|
int i = data.getTravelTimeInTicks();
|
|
int j = user.calculateTravelTimeInTicks(data.currentVibration.distance());
|
|
double d = 1.0 - (double)i / j;
|
|
double e = Mth.lerp(d, vec3.x, vec32.x);
|
|
double f = Mth.lerp(d, vec3.y, vec32.y);
|
|
double g = Mth.lerp(d, vec3.z, vec32.z);
|
|
boolean bl = level.sendParticles(new VibrationParticleOption(positionSource, i), e, f, g, 1, 0.0, 0.0, 0.0, 0.0) > 0;
|
|
if (bl) {
|
|
data.setReloadVibrationParticle(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean receiveVibration(ServerLevel level, VibrationSystem.Data data, VibrationSystem.User user, VibrationInfo vibrationInfo) {
|
|
BlockPos blockPos = BlockPos.containing(vibrationInfo.pos());
|
|
BlockPos blockPos2 = (BlockPos)user.getPositionSource().getPosition(level).map(BlockPos::containing).orElse(blockPos);
|
|
if (user.requiresAdjacentChunksToBeTicking() && !areAdjacentChunksTicking(level, blockPos2)) {
|
|
return false;
|
|
} else {
|
|
user.onReceiveVibration(
|
|
level,
|
|
blockPos,
|
|
vibrationInfo.gameEvent(),
|
|
(Entity)vibrationInfo.getEntity(level).orElse(null),
|
|
(Entity)vibrationInfo.getProjectileOwner(level).orElse(null),
|
|
VibrationSystem.Listener.distanceBetweenInBlocks(blockPos, blockPos2)
|
|
);
|
|
data.setCurrentVibration(null);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static boolean areAdjacentChunksTicking(Level level, BlockPos pos) {
|
|
ChunkPos chunkPos = new ChunkPos(pos);
|
|
|
|
for (int i = chunkPos.x - 1; i <= chunkPos.x + 1; i++) {
|
|
for (int j = chunkPos.z - 1; j <= chunkPos.z + 1; j++) {
|
|
if (!level.shouldTickBlocksAt(ChunkPos.asLong(i, j)) || level.getChunkSource().getChunkNow(i, j) == null) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public interface User {
|
|
int getListenerRadius();
|
|
|
|
PositionSource getPositionSource();
|
|
|
|
boolean canReceiveVibration(ServerLevel level, BlockPos pos, Holder<GameEvent> gameEvent, Context context);
|
|
|
|
void onReceiveVibration(ServerLevel level, BlockPos pos, Holder<GameEvent> gameEvent, @Nullable Entity entity, @Nullable Entity playerEntity, float distance);
|
|
|
|
default TagKey<GameEvent> getListenableEvents() {
|
|
return GameEventTags.VIBRATIONS;
|
|
}
|
|
|
|
default boolean canTriggerAvoidVibration() {
|
|
return false;
|
|
}
|
|
|
|
default boolean requiresAdjacentChunksToBeTicking() {
|
|
return false;
|
|
}
|
|
|
|
default int calculateTravelTimeInTicks(float distance) {
|
|
return Mth.floor(distance);
|
|
}
|
|
|
|
default boolean isValidVibration(Holder<GameEvent> gameEvent, Context context) {
|
|
if (!gameEvent.is(this.getListenableEvents())) {
|
|
return false;
|
|
} else {
|
|
Entity entity = context.sourceEntity();
|
|
if (entity != null) {
|
|
if (entity.isSpectator()) {
|
|
return false;
|
|
}
|
|
|
|
if (entity.isSteppingCarefully() && gameEvent.is(GameEventTags.IGNORE_VIBRATIONS_SNEAKING)) {
|
|
if (this.canTriggerAvoidVibration() && entity instanceof ServerPlayer serverPlayer) {
|
|
CriteriaTriggers.AVOID_VIBRATION.trigger(serverPlayer);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (entity.dampensVibrations()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return context.affectedState() != null ? !context.affectedState().is(BlockTags.DAMPENS_VIBRATIONS) : true;
|
|
}
|
|
}
|
|
|
|
default void onDataChanged() {
|
|
}
|
|
}
|
|
}
|