package com.cursedcauldron.wildbackport.common.entities.warden; import com.cursedcauldron.wildbackport.client.registry.WBCriteriaTriggers; import com.cursedcauldron.wildbackport.common.utils.PositionUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Registry; import net.minecraft.core.SerializableUUID; 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.entity.projectile.Projectile; 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.vibrations.VibrationPath; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; import java.util.Optional; import java.util.UUID; //<> public class VibrationListenerSource implements GameEventListener { protected final PositionSource source; protected final int range; protected final VibrationConfig config; @Nullable protected Vibration event; protected float distance; protected int delay; public static Codec codec(VibrationConfig config) { return RecordCodecBuilder.create(instance -> { return instance.group(PositionSource.CODEC.fieldOf("source").forGetter(listener -> { return listener.source; }), ExtraCodecs.NON_NEGATIVE_INT.fieldOf("range").forGetter(listener -> { return listener.range; }), Vibration.CODEC.optionalFieldOf("event").forGetter(listener -> { return Optional.ofNullable(listener.event); }), Codec.floatRange(0.0F, Float.MAX_VALUE).fieldOf("event_distance").orElse(0.0F).forGetter(listener -> { return listener.distance; }), ExtraCodecs.NON_NEGATIVE_INT.fieldOf("event_delay").orElse(0).forGetter(listener -> { return listener.delay; })).apply(instance, (source, range, event, distance, delay) -> { return new VibrationListenerSource(source, range, config, event.orElse(null), distance, delay); }); }); } public VibrationListenerSource(PositionSource source, int range, VibrationConfig config, @Nullable Vibration event, float distance, int delay) { this.source = source; this.range = range; this.config = config; this.event = event; this.distance = distance; this.delay = delay; } public void tick(Level level) { if (level instanceof ServerLevel server) { if (this.event != null) { --this.delay; if (this.delay <= 0) { this.delay = 0; this.config.onSignalReceive(server, this, new BlockPos(this.event.pos), this.event.event, this.event.getEntity(server).orElse(null), this.event.getProjectileOwner(server).orElse(null), this.distance); this.event = null; } } } } @Override public PositionSource getListenerSource() { return this.source; } @Override public int getListenerRadius() { return this.range; } @Override public boolean handleGameEvent(Level level, GameEvent event, @Nullable Entity entity, BlockPos pos) { if (this.event != null) { return false; } else { Optional optional = this.source.getPosition(level); if (!this.config.isValidVibration(event, entity)) { return false; } else { Vec3 source = PositionUtils.toVec(pos); Vec3 target = PositionUtils.toVec(optional.get()); if (!this.config.shouldListen((ServerLevel)level, this, new BlockPos(source), event, entity)) { return false; } else if (isOccluded(level, source, target)) { return false; } else { this.scheduleSignal(level, event, entity, source, target); return true; } } } } private void scheduleSignal(Level level, GameEvent event, @Nullable Entity entity, Vec3 source, Vec3 target) { this.distance = (float)source.distanceTo(target); this.event = new Vibration(event, this.distance, source, entity); this.delay = Mth.floor(this.distance); ((ServerLevel)level).sendVibrationParticle(new VibrationPath(PositionUtils.toBlockPos(source), this.source, this.delay)); this.config.onSignalSchedule(); } private static boolean isOccluded(Level level, Vec3 source, Vec3 target) { Vec3 sourceVec = new Vec3((double)Mth.floor(source.x) + 0.5D, (double)Mth.floor(source.y) + 0.5D, (double)Mth.floor(source.z) + 0.5D); Vec3 targetVec = new Vec3((double)Mth.floor(target.x) + 0.5D, (double)Mth.floor(target.y) + 0.5D, (double)Mth.floor(target.z) + 0.5D); for (Direction direction : Direction.values()) { Vec3 offsetVec = PositionUtils.relative(sourceVec, direction, 1.0E-5F); if (level.isBlockInLine(new ClipBlockStateContext(offsetVec, targetVec, state -> { return state.is(BlockTags.OCCLUDES_VIBRATION_SIGNALS); })).getType() != HitResult.Type.BLOCK) { return false; } } return true; } public record Vibration(GameEvent event, float distance, Vec3 pos, @Nullable UUID source, @Nullable UUID projectileOwner, @Nullable Entity entity) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> { return instance.group(Registry.GAME_EVENT.byNameCodec().fieldOf("game_event").forGetter(Vibration::event), Codec.floatRange(0.0F, Float.MAX_VALUE).fieldOf("distance").forGetter(Vibration::distance), PositionUtils.VEC_CODEC.fieldOf("pos").forGetter(Vibration::pos), SerializableUUID.CODEC.optionalFieldOf("source").forGetter(entity -> { return Optional.ofNullable(entity.source()); }), SerializableUUID.CODEC.optionalFieldOf("projectile_owner").forGetter(entity -> { return Optional.ofNullable(entity.projectileOwner()); })).apply(instance, (event, distance, pos, source, projectileOwner) -> { return new Vibration(event, distance, pos, source.orElse(null), projectileOwner.orElse(null)); }); }); public Vibration(GameEvent event, float distance, Vec3 pos, @Nullable UUID source, @Nullable UUID projectileOwner) { this(event, distance, pos, source, projectileOwner, null); } public Vibration(GameEvent event, float distance, Vec3 pos, @Nullable Entity entity) { this(event, distance, pos, entity == null ? null : entity.getUUID(), getProjectileOwner(entity), entity); } @Nullable private static UUID getProjectileOwner(@Nullable Entity entity) { if (entity instanceof Projectile projectile) { if (projectile.getOwner() != null) { return projectile.getOwner().getUUID(); } } return null; } public Optional getEntity(ServerLevel level) { return Optional.ofNullable(this.entity).or(() -> { return Optional.ofNullable(this.source).map(level::getEntity); }); } public Optional getProjectileOwner(ServerLevel level) { return this.getEntity(level).filter(entity -> { return entity instanceof Projectile; }).map(entity -> { return (Projectile)entity; }).map(Projectile::getOwner).or(() -> { return Optional.ofNullable(this.projectileOwner).map(level::getEntity); }); } } public interface VibrationConfig { default TagKey getListenableEvents() { return GameEventTags.VIBRATIONS; } default boolean canTriggerAvoidVibration() { return false; } default boolean isValidVibration(GameEvent event, @Nullable Entity entity) { if (!event.is(this.getListenableEvents())) { return false; } else { if (entity != null) { if (entity.isSpectator()) { return false; } if (entity.isSteppingCarefully() && event.is(GameEventTags.IGNORE_VIBRATIONS_SNEAKING)) { if (this.canTriggerAvoidVibration() && entity instanceof ServerPlayer player) { WBCriteriaTriggers.AVOID_VIBRATION.trigger(player); } return false; } return !entity.occludesVibrations(); } return true; } } boolean shouldListen(ServerLevel level, GameEventListener listener, BlockPos pos, GameEvent event, @Nullable Entity entity); void onSignalReceive(ServerLevel level, GameEventListener listener, BlockPos pos, GameEvent event, @Nullable Entity entity, @Nullable Entity source, float distance); default void onSignalSchedule() {} } }