package net.minecraft.client.particle; import com.google.common.collect.EvictingQueue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Queues; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.stream.Collectors; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; import net.minecraft.Util; import net.minecraft.client.Camera; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.particle.BlockMarker.Provider; import net.minecraft.client.particle.CampfireSmokeParticle.CosyProvider; import net.minecraft.client.particle.CampfireSmokeParticle.SignalProvider; import net.minecraft.client.particle.CritParticle.DamageIndicatorProvider; import net.minecraft.client.particle.CritParticle.MagicProvider; import net.minecraft.client.particle.FireworkParticles.FlashProvider; import net.minecraft.client.particle.FireworkParticles.SparkProvider; import net.minecraft.client.particle.FlameParticle.SmallFlameProvider; import net.minecraft.client.particle.FlyStraightTowardsParticle.OminousSpawnProvider; import net.minecraft.client.particle.FlyTowardsPositionParticle.EnchantProvider; import net.minecraft.client.particle.FlyTowardsPositionParticle.NautilusProvider; import net.minecraft.client.particle.FlyTowardsPositionParticle.VaultConnectionProvider; import net.minecraft.client.particle.GlowParticle.ElectricSparkProvider; import net.minecraft.client.particle.GlowParticle.GlowSquidProvider; import net.minecraft.client.particle.GlowParticle.ScrapeProvider; import net.minecraft.client.particle.GlowParticle.WaxOffProvider; import net.minecraft.client.particle.GlowParticle.WaxOnProvider; import net.minecraft.client.particle.GustParticle.SmallProvider; import net.minecraft.client.particle.HeartParticle.AngryVillagerProvider; import net.minecraft.client.particle.ParticleProvider.Sprite; import net.minecraft.client.particle.PlayerCloudParticle.SneezeProvider; import net.minecraft.client.particle.ReversePortalParticle.ReversePortalProvider; import net.minecraft.client.particle.SoulParticle.EmissiveProvider; import net.minecraft.client.particle.SpellParticle.InstantProvider; import net.minecraft.client.particle.SpellParticle.MobEffectProvider; import net.minecraft.client.particle.SpellParticle.WitchProvider; import net.minecraft.client.particle.SquidInkParticle.GlowInkProvider; import net.minecraft.client.particle.SuspendedParticle.CrimsonSporeProvider; import net.minecraft.client.particle.SuspendedParticle.SporeBlossomAirProvider; import net.minecraft.client.particle.SuspendedParticle.UnderwaterProvider; import net.minecraft.client.particle.SuspendedParticle.WarpedSporeProvider; import net.minecraft.client.particle.SuspendedTownParticle.ComposterFillProvider; import net.minecraft.client.particle.SuspendedTownParticle.DolphinSpeedProvider; import net.minecraft.client.particle.SuspendedTownParticle.EggCrackProvider; import net.minecraft.client.particle.SuspendedTownParticle.HappyVillagerProvider; import net.minecraft.client.particle.TerrainParticle.CrumblingProvider; import net.minecraft.client.particle.TerrainParticle.DustPillarProvider; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.MultiBufferSource.BufferSource; import net.minecraft.client.renderer.texture.SpriteLoader; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.client.resources.model.AtlasIds; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.particles.ParticleGroup; import net.minecraft.core.particles.ParticleOptions; import net.minecraft.core.particles.ParticleType; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.GsonHelper; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class ParticleEngine implements PreparableReloadListener { private static final Logger LOGGER = LogUtils.getLogger(); private static final FileToIdConverter PARTICLE_LISTER = FileToIdConverter.json("particles"); private static final int MAX_PARTICLES_PER_LAYER = 16384; private static final List RENDER_ORDER = List.of( ParticleRenderType.TERRAIN_SHEET, ParticleRenderType.PARTICLE_SHEET_OPAQUE, ParticleRenderType.PARTICLE_SHEET_TRANSLUCENT ); protected ClientLevel level; private final Map> particles = Maps.>newIdentityHashMap(); private final Queue trackingEmitters = Queues.newArrayDeque(); private final RandomSource random = RandomSource.create(); private final Int2ObjectMap> providers = new Int2ObjectOpenHashMap<>(); private final Queue particlesToAdd = Queues.newArrayDeque(); private final Map spriteSets = Maps.newHashMap(); private final TextureAtlas textureAtlas; private final Object2IntOpenHashMap trackedParticleCounts = new Object2IntOpenHashMap<>(); public ParticleEngine(ClientLevel level, TextureManager textureManager) { this.textureAtlas = new TextureAtlas(TextureAtlas.LOCATION_PARTICLES); textureManager.register(this.textureAtlas.location(), this.textureAtlas); this.level = level; this.registerProviders(); } private void registerProviders() { this.register(ParticleTypes.ANGRY_VILLAGER, AngryVillagerProvider::new); this.register(ParticleTypes.BLOCK_MARKER, new Provider()); this.register(ParticleTypes.BLOCK, new net.minecraft.client.particle.TerrainParticle.Provider()); this.register(ParticleTypes.BUBBLE, net.minecraft.client.particle.BubbleParticle.Provider::new); this.register(ParticleTypes.BUBBLE_COLUMN_UP, net.minecraft.client.particle.BubbleColumnUpParticle.Provider::new); this.register(ParticleTypes.BUBBLE_POP, net.minecraft.client.particle.BubblePopParticle.Provider::new); this.register(ParticleTypes.CAMPFIRE_COSY_SMOKE, CosyProvider::new); this.register(ParticleTypes.CAMPFIRE_SIGNAL_SMOKE, SignalProvider::new); this.register(ParticleTypes.CLOUD, net.minecraft.client.particle.PlayerCloudParticle.Provider::new); this.register(ParticleTypes.COMPOSTER, ComposterFillProvider::new); this.register(ParticleTypes.CRIT, net.minecraft.client.particle.CritParticle.Provider::new); this.register(ParticleTypes.CURRENT_DOWN, net.minecraft.client.particle.WaterCurrentDownParticle.Provider::new); this.register(ParticleTypes.DAMAGE_INDICATOR, DamageIndicatorProvider::new); this.register(ParticleTypes.DRAGON_BREATH, net.minecraft.client.particle.DragonBreathParticle.Provider::new); this.register(ParticleTypes.DOLPHIN, DolphinSpeedProvider::new); this.register(ParticleTypes.DRIPPING_LAVA, DripParticle::createLavaHangParticle); this.register(ParticleTypes.FALLING_LAVA, DripParticle::createLavaFallParticle); this.register(ParticleTypes.LANDING_LAVA, DripParticle::createLavaLandParticle); this.register(ParticleTypes.DRIPPING_WATER, DripParticle::createWaterHangParticle); this.register(ParticleTypes.FALLING_WATER, DripParticle::createWaterFallParticle); this.register(ParticleTypes.DUST, net.minecraft.client.particle.DustParticle.Provider::new); this.register(ParticleTypes.DUST_COLOR_TRANSITION, net.minecraft.client.particle.DustColorTransitionParticle.Provider::new); this.register(ParticleTypes.EFFECT, net.minecraft.client.particle.SpellParticle.Provider::new); this.register(ParticleTypes.ELDER_GUARDIAN, new MobAppearanceParticle.Provider()); this.register(ParticleTypes.ENCHANTED_HIT, MagicProvider::new); this.register(ParticleTypes.ENCHANT, EnchantProvider::new); this.register(ParticleTypes.END_ROD, net.minecraft.client.particle.EndRodParticle.Provider::new); this.register(ParticleTypes.ENTITY_EFFECT, MobEffectProvider::new); this.register(ParticleTypes.EXPLOSION_EMITTER, new net.minecraft.client.particle.HugeExplosionSeedParticle.Provider()); this.register(ParticleTypes.EXPLOSION, net.minecraft.client.particle.HugeExplosionParticle.Provider::new); this.register(ParticleTypes.SONIC_BOOM, net.minecraft.client.particle.SonicBoomParticle.Provider::new); this.register(ParticleTypes.FALLING_DUST, net.minecraft.client.particle.FallingDustParticle.Provider::new); this.register(ParticleTypes.GUST, net.minecraft.client.particle.GustParticle.Provider::new); this.register(ParticleTypes.SMALL_GUST, SmallProvider::new); this.register(ParticleTypes.GUST_EMITTER_LARGE, new net.minecraft.client.particle.GustSeedParticle.Provider(3.0, 7, 0)); this.register(ParticleTypes.GUST_EMITTER_SMALL, new net.minecraft.client.particle.GustSeedParticle.Provider(1.0, 3, 2)); this.register(ParticleTypes.FIREWORK, SparkProvider::new); this.register(ParticleTypes.FISHING, net.minecraft.client.particle.WakeParticle.Provider::new); this.register(ParticleTypes.FLAME, net.minecraft.client.particle.FlameParticle.Provider::new); this.register(ParticleTypes.INFESTED, net.minecraft.client.particle.SpellParticle.Provider::new); this.register(ParticleTypes.SCULK_SOUL, EmissiveProvider::new); this.register(ParticleTypes.SCULK_CHARGE, net.minecraft.client.particle.SculkChargeParticle.Provider::new); this.register(ParticleTypes.SCULK_CHARGE_POP, net.minecraft.client.particle.SculkChargePopParticle.Provider::new); this.register(ParticleTypes.SOUL, net.minecraft.client.particle.SoulParticle.Provider::new); this.register(ParticleTypes.SOUL_FIRE_FLAME, net.minecraft.client.particle.FlameParticle.Provider::new); this.register(ParticleTypes.FLASH, FlashProvider::new); this.register(ParticleTypes.HAPPY_VILLAGER, HappyVillagerProvider::new); this.register(ParticleTypes.HEART, net.minecraft.client.particle.HeartParticle.Provider::new); this.register(ParticleTypes.INSTANT_EFFECT, InstantProvider::new); this.register(ParticleTypes.ITEM, new BreakingItemParticle.Provider()); this.register(ParticleTypes.ITEM_SLIME, new BreakingItemParticle.SlimeProvider()); this.register(ParticleTypes.ITEM_COBWEB, new BreakingItemParticle.CobwebProvider()); this.register(ParticleTypes.ITEM_SNOWBALL, new BreakingItemParticle.SnowballProvider()); this.register(ParticleTypes.LARGE_SMOKE, net.minecraft.client.particle.LargeSmokeParticle.Provider::new); this.register(ParticleTypes.LAVA, net.minecraft.client.particle.LavaParticle.Provider::new); this.register(ParticleTypes.MYCELIUM, net.minecraft.client.particle.SuspendedTownParticle.Provider::new); this.register(ParticleTypes.NAUTILUS, NautilusProvider::new); this.register(ParticleTypes.NOTE, net.minecraft.client.particle.NoteParticle.Provider::new); this.register(ParticleTypes.POOF, net.minecraft.client.particle.ExplodeParticle.Provider::new); this.register(ParticleTypes.PORTAL, net.minecraft.client.particle.PortalParticle.Provider::new); this.register(ParticleTypes.RAIN, net.minecraft.client.particle.WaterDropParticle.Provider::new); this.register(ParticleTypes.SMOKE, net.minecraft.client.particle.SmokeParticle.Provider::new); this.register(ParticleTypes.WHITE_SMOKE, net.minecraft.client.particle.WhiteSmokeParticle.Provider::new); this.register(ParticleTypes.SNEEZE, SneezeProvider::new); this.register(ParticleTypes.SNOWFLAKE, net.minecraft.client.particle.SnowflakeParticle.Provider::new); this.register(ParticleTypes.SPIT, net.minecraft.client.particle.SpitParticle.Provider::new); this.register(ParticleTypes.SWEEP_ATTACK, net.minecraft.client.particle.AttackSweepParticle.Provider::new); this.register(ParticleTypes.TOTEM_OF_UNDYING, net.minecraft.client.particle.TotemParticle.Provider::new); this.register(ParticleTypes.SQUID_INK, net.minecraft.client.particle.SquidInkParticle.Provider::new); this.register(ParticleTypes.UNDERWATER, UnderwaterProvider::new); this.register(ParticleTypes.SPLASH, net.minecraft.client.particle.SplashParticle.Provider::new); this.register(ParticleTypes.WITCH, WitchProvider::new); this.register(ParticleTypes.DRIPPING_HONEY, DripParticle::createHoneyHangParticle); this.register(ParticleTypes.FALLING_HONEY, DripParticle::createHoneyFallParticle); this.register(ParticleTypes.LANDING_HONEY, DripParticle::createHoneyLandParticle); this.register(ParticleTypes.FALLING_NECTAR, DripParticle::createNectarFallParticle); this.register(ParticleTypes.FALLING_SPORE_BLOSSOM, DripParticle::createSporeBlossomFallParticle); this.register(ParticleTypes.SPORE_BLOSSOM_AIR, SporeBlossomAirProvider::new); this.register(ParticleTypes.ASH, net.minecraft.client.particle.AshParticle.Provider::new); this.register(ParticleTypes.CRIMSON_SPORE, CrimsonSporeProvider::new); this.register(ParticleTypes.WARPED_SPORE, WarpedSporeProvider::new); this.register(ParticleTypes.DRIPPING_OBSIDIAN_TEAR, DripParticle::createObsidianTearHangParticle); this.register(ParticleTypes.FALLING_OBSIDIAN_TEAR, DripParticle::createObsidianTearFallParticle); this.register(ParticleTypes.LANDING_OBSIDIAN_TEAR, DripParticle::createObsidianTearLandParticle); this.register(ParticleTypes.REVERSE_PORTAL, ReversePortalProvider::new); this.register(ParticleTypes.WHITE_ASH, net.minecraft.client.particle.WhiteAshParticle.Provider::new); this.register(ParticleTypes.SMALL_FLAME, SmallFlameProvider::new); this.register(ParticleTypes.DRIPPING_DRIPSTONE_WATER, DripParticle::createDripstoneWaterHangParticle); this.register(ParticleTypes.FALLING_DRIPSTONE_WATER, DripParticle::createDripstoneWaterFallParticle); this.register(ParticleTypes.CHERRY_LEAVES, FallingLeavesParticle.CherryProvider::new); this.register(ParticleTypes.PALE_OAK_LEAVES, FallingLeavesParticle.PaleOakProvider::new); this.register(ParticleTypes.TINTED_LEAVES, FallingLeavesParticle.TintedLeavesProvider::new); this.register(ParticleTypes.DRIPPING_DRIPSTONE_LAVA, DripParticle::createDripstoneLavaHangParticle); this.register(ParticleTypes.FALLING_DRIPSTONE_LAVA, DripParticle::createDripstoneLavaFallParticle); this.register(ParticleTypes.VIBRATION, net.minecraft.client.particle.VibrationSignalParticle.Provider::new); this.register(ParticleTypes.TRAIL, net.minecraft.client.particle.TrailParticle.Provider::new); this.register(ParticleTypes.GLOW_SQUID_INK, GlowInkProvider::new); this.register(ParticleTypes.GLOW, GlowSquidProvider::new); this.register(ParticleTypes.WAX_ON, WaxOnProvider::new); this.register(ParticleTypes.WAX_OFF, WaxOffProvider::new); this.register(ParticleTypes.ELECTRIC_SPARK, ElectricSparkProvider::new); this.register(ParticleTypes.SCRAPE, ScrapeProvider::new); this.register(ParticleTypes.SHRIEK, net.minecraft.client.particle.ShriekParticle.Provider::new); this.register(ParticleTypes.EGG_CRACK, EggCrackProvider::new); this.register(ParticleTypes.DUST_PLUME, net.minecraft.client.particle.DustPlumeParticle.Provider::new); this.register(ParticleTypes.TRIAL_SPAWNER_DETECTED_PLAYER, net.minecraft.client.particle.TrialSpawnerDetectionParticle.Provider::new); this.register(ParticleTypes.TRIAL_SPAWNER_DETECTED_PLAYER_OMINOUS, net.minecraft.client.particle.TrialSpawnerDetectionParticle.Provider::new); this.register(ParticleTypes.VAULT_CONNECTION, VaultConnectionProvider::new); this.register(ParticleTypes.DUST_PILLAR, new DustPillarProvider()); this.register(ParticleTypes.RAID_OMEN, net.minecraft.client.particle.SpellParticle.Provider::new); this.register(ParticleTypes.TRIAL_OMEN, net.minecraft.client.particle.SpellParticle.Provider::new); this.register(ParticleTypes.OMINOUS_SPAWNING, OminousSpawnProvider::new); this.register(ParticleTypes.BLOCK_CRUMBLE, new CrumblingProvider()); this.register(ParticleTypes.FIREFLY, FireflyParticle.FireflyProvider::new); } private void register(ParticleType particleType, ParticleProvider particleFactory) { this.providers.put(BuiltInRegistries.PARTICLE_TYPE.getId(particleType), particleFactory); } private void register(ParticleType particleType, Sprite sprite) { this.register(particleType, spriteSet -> (particleOptions, clientLevel, d, e, f, g, h, i) -> { TextureSheetParticle textureSheetParticle = sprite.createParticle(particleOptions, clientLevel, d, e, f, g, h, i); if (textureSheetParticle != null) { textureSheetParticle.pickSprite(spriteSet); } return textureSheetParticle; }); } private void register(ParticleType particleType, ParticleEngine.SpriteParticleRegistration particleMetaFactory) { ParticleEngine.MutableSpriteSet mutableSpriteSet = new ParticleEngine.MutableSpriteSet(); this.spriteSets.put(BuiltInRegistries.PARTICLE_TYPE.getKey(particleType), mutableSpriteSet); this.providers.put(BuiltInRegistries.PARTICLE_TYPE.getId(particleType), particleMetaFactory.create(mutableSpriteSet)); } @Override public CompletableFuture reload( PreparableReloadListener.PreparationBarrier preparationBarrier, ResourceManager resourceManager, Executor executor, Executor executor2 ) { @Environment(EnvType.CLIENT) record ParticleDefinition(ResourceLocation id, Optional> sprites) { } CompletableFuture> completableFuture = CompletableFuture.supplyAsync( () -> PARTICLE_LISTER.listMatchingResources(resourceManager), executor ) .thenCompose( map -> { List> list = new ArrayList(map.size()); map.forEach( (resourceLocation, resource) -> { ResourceLocation resourceLocation2 = PARTICLE_LISTER.fileToId(resourceLocation); list.add( CompletableFuture.supplyAsync(() -> new ParticleDefinition(resourceLocation2, this.loadParticleDescription(resourceLocation2, resource)), executor) ); } ); return Util.sequence(list); } ); CompletableFuture completableFuture2 = SpriteLoader.create(this.textureAtlas) .loadAndStitch(resourceManager, AtlasIds.PARTICLES, 0, executor) .thenCompose(SpriteLoader.Preparations::waitForUpload); return CompletableFuture.allOf(completableFuture2, completableFuture).thenCompose(preparationBarrier::wait).thenAcceptAsync(void_ -> { this.clearParticles(); ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("upload"); SpriteLoader.Preparations preparations = (SpriteLoader.Preparations)completableFuture2.join(); this.textureAtlas.upload(preparations); profilerFiller.popPush("bindSpriteSets"); Set set = new HashSet(); TextureAtlasSprite textureAtlasSprite = preparations.missing(); ((List)completableFuture.join()).forEach(arg -> { Optional> optional = arg.sprites(); if (!optional.isEmpty()) { List list = new ArrayList(); for (ResourceLocation resourceLocation : (List)optional.get()) { TextureAtlasSprite textureAtlasSprite2 = (TextureAtlasSprite)preparations.regions().get(resourceLocation); if (textureAtlasSprite2 == null) { set.add(resourceLocation); list.add(textureAtlasSprite); } else { list.add(textureAtlasSprite2); } } if (list.isEmpty()) { list.add(textureAtlasSprite); } ((ParticleEngine.MutableSpriteSet)this.spriteSets.get(arg.id())).rebind(list); } }); if (!set.isEmpty()) { LOGGER.warn("Missing particle sprites: {}", set.stream().sorted().map(ResourceLocation::toString).collect(Collectors.joining(","))); } profilerFiller.pop(); }, executor2); } public void close() { this.textureAtlas.clearTextureData(); } private Optional> loadParticleDescription(ResourceLocation registryName, Resource resource) { if (!this.spriteSets.containsKey(registryName)) { LOGGER.debug("Redundant texture list for particle: {}", registryName); return Optional.empty(); } else { try { Reader reader = resource.openAsReader(); Optional var5; try { ParticleDescription particleDescription = ParticleDescription.fromJson(GsonHelper.parse(reader)); var5 = Optional.of(particleDescription.getTextures()); } catch (Throwable var7) { if (reader != null) { try { reader.close(); } catch (Throwable var6) { var7.addSuppressed(var6); } } throw var7; } if (reader != null) { reader.close(); } return var5; } catch (IOException var8) { throw new IllegalStateException("Failed to load description for particle " + registryName, var8); } } } public void createTrackingEmitter(Entity entity, ParticleOptions particleData) { this.trackingEmitters.add(new TrackingEmitter(this.level, entity, particleData)); } public void createTrackingEmitter(Entity entity, ParticleOptions data, int lifetime) { this.trackingEmitters.add(new TrackingEmitter(this.level, entity, data, lifetime)); } @Nullable public Particle createParticle(ParticleOptions particleData, double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) { Particle particle = this.makeParticle(particleData, x, y, z, xSpeed, ySpeed, zSpeed); if (particle != null) { this.add(particle); return particle; } else { return null; } } @Nullable private Particle makeParticle(T particleData, double x, double y, double z, double xSpeed, double ySpeed, double zSpeed) { ParticleProvider particleProvider = (ParticleProvider)this.providers.get(BuiltInRegistries.PARTICLE_TYPE.getId(particleData.getType())); return particleProvider == null ? null : particleProvider.createParticle(particleData, this.level, x, y, z, xSpeed, ySpeed, zSpeed); } public void add(Particle effect) { Optional optional = effect.getParticleGroup(); if (optional.isPresent()) { if (this.hasSpaceInParticleLimit((ParticleGroup)optional.get())) { this.particlesToAdd.add(effect); this.updateCount((ParticleGroup)optional.get(), 1); } } else { this.particlesToAdd.add(effect); } } public void tick() { this.particles.forEach((particleRenderType, queue) -> { Profiler.get().push(particleRenderType.toString()); this.tickParticleList(queue); Profiler.get().pop(); }); if (!this.trackingEmitters.isEmpty()) { List list = Lists.newArrayList(); for (TrackingEmitter trackingEmitter : this.trackingEmitters) { trackingEmitter.tick(); if (!trackingEmitter.isAlive()) { list.add(trackingEmitter); } } this.trackingEmitters.removeAll(list); } Particle particle; if (!this.particlesToAdd.isEmpty()) { while ((particle = (Particle)this.particlesToAdd.poll()) != null) { ((Queue)this.particles.computeIfAbsent(particle.getRenderType(), particleRenderType -> EvictingQueue.create(16384))).add(particle); } } } private void tickParticleList(Collection particles) { if (!particles.isEmpty()) { Iterator iterator = particles.iterator(); while (iterator.hasNext()) { Particle particle = (Particle)iterator.next(); this.tickParticle(particle); if (!particle.isAlive()) { particle.getParticleGroup().ifPresent(particleGroup -> this.updateCount(particleGroup, -1)); iterator.remove(); } } } } private void updateCount(ParticleGroup group, int count) { this.trackedParticleCounts.addTo(group, count); } private void tickParticle(Particle particle) { try { particle.tick(); } catch (Throwable var5) { CrashReport crashReport = CrashReport.forThrowable(var5, "Ticking Particle"); CrashReportCategory crashReportCategory = crashReport.addCategory("Particle being ticked"); crashReportCategory.setDetail("Particle", particle::toString); crashReportCategory.setDetail("Particle Type", particle.getRenderType()::toString); throw new ReportedException(crashReport); } } public void render(Camera camera, float partialTick, BufferSource bufferSource) { for (ParticleRenderType particleRenderType : RENDER_ORDER) { Queue queue = (Queue)this.particles.get(particleRenderType); if (queue != null && !queue.isEmpty()) { renderParticleType(camera, partialTick, bufferSource, particleRenderType, queue); } } Queue queue2 = (Queue)this.particles.get(ParticleRenderType.CUSTOM); if (queue2 != null && !queue2.isEmpty()) { renderCustomParticles(camera, partialTick, bufferSource, queue2); } bufferSource.endBatch(); } private static void renderParticleType(Camera camera, float partialTick, BufferSource bufferSource, ParticleRenderType particleType, Queue particles) { VertexConsumer vertexConsumer = bufferSource.getBuffer((RenderType)Objects.requireNonNull(particleType.renderType())); for (Particle particle : particles) { try { particle.render(vertexConsumer, camera, partialTick); } catch (Throwable var11) { CrashReport crashReport = CrashReport.forThrowable(var11, "Rendering Particle"); CrashReportCategory crashReportCategory = crashReport.addCategory("Particle being rendered"); crashReportCategory.setDetail("Particle", particle::toString); crashReportCategory.setDetail("Particle Type", particleType::toString); throw new ReportedException(crashReport); } } } private static void renderCustomParticles(Camera camera, float partialTick, BufferSource bufferSource, Queue particles) { PoseStack poseStack = new PoseStack(); for (Particle particle : particles) { try { particle.renderCustom(poseStack, bufferSource, camera, partialTick); } catch (Throwable var10) { CrashReport crashReport = CrashReport.forThrowable(var10, "Rendering Particle"); CrashReportCategory crashReportCategory = crashReport.addCategory("Particle being rendered"); crashReportCategory.setDetail("Particle", particle::toString); crashReportCategory.setDetail("Particle Type", "Custom"); throw new ReportedException(crashReport); } } } public void setLevel(@Nullable ClientLevel level) { this.level = level; this.clearParticles(); this.trackingEmitters.clear(); } public void destroy(BlockPos pos, BlockState state) { if (!state.isAir() && state.shouldSpawnTerrainParticles()) { VoxelShape voxelShape = state.getShape(this.level, pos); double d = 0.25; voxelShape.forAllBoxes((dx, e, f, g, h, i) -> { double j = Math.min(1.0, g - dx); double k = Math.min(1.0, h - e); double l = Math.min(1.0, i - f); int m = Math.max(2, Mth.ceil(j / 0.25)); int n = Math.max(2, Mth.ceil(k / 0.25)); int o = Math.max(2, Mth.ceil(l / 0.25)); for (int p = 0; p < m; p++) { for (int q = 0; q < n; q++) { for (int r = 0; r < o; r++) { double s = (p + 0.5) / m; double t = (q + 0.5) / n; double u = (r + 0.5) / o; double v = s * j + dx; double w = t * k + e; double x = u * l + f; this.add(new TerrainParticle(this.level, pos.getX() + v, pos.getY() + w, pos.getZ() + x, s - 0.5, t - 0.5, u - 0.5, state, pos)); } } } }); } } /** * Adds block hit particles for the specified block */ public void crack(BlockPos pos, Direction side) { BlockState blockState = this.level.getBlockState(pos); if (blockState.getRenderShape() != RenderShape.INVISIBLE && blockState.shouldSpawnTerrainParticles()) { int i = pos.getX(); int j = pos.getY(); int k = pos.getZ(); float f = 0.1F; AABB aABB = blockState.getShape(this.level, pos).bounds(); double d = i + this.random.nextDouble() * (aABB.maxX - aABB.minX - 0.2F) + 0.1F + aABB.minX; double e = j + this.random.nextDouble() * (aABB.maxY - aABB.minY - 0.2F) + 0.1F + aABB.minY; double g = k + this.random.nextDouble() * (aABB.maxZ - aABB.minZ - 0.2F) + 0.1F + aABB.minZ; if (side == Direction.DOWN) { e = j + aABB.minY - 0.1F; } if (side == Direction.UP) { e = j + aABB.maxY + 0.1F; } if (side == Direction.NORTH) { g = k + aABB.minZ - 0.1F; } if (side == Direction.SOUTH) { g = k + aABB.maxZ + 0.1F; } if (side == Direction.WEST) { d = i + aABB.minX - 0.1F; } if (side == Direction.EAST) { d = i + aABB.maxX + 0.1F; } this.add(new TerrainParticle(this.level, d, e, g, 0.0, 0.0, 0.0, blockState, pos).setPower(0.2F).scale(0.6F)); } } public String countParticles() { return String.valueOf(this.particles.values().stream().mapToInt(Collection::size).sum()); } private boolean hasSpaceInParticleLimit(ParticleGroup group) { return this.trackedParticleCounts.getInt(group) < group.getLimit(); } private void clearParticles() { this.particles.clear(); this.particlesToAdd.clear(); this.trackingEmitters.clear(); this.trackedParticleCounts.clear(); } @Environment(EnvType.CLIENT) static class MutableSpriteSet implements SpriteSet { private List sprites; @Override public TextureAtlasSprite get(int age, int lifetime) { return (TextureAtlasSprite)this.sprites.get(age * (this.sprites.size() - 1) / lifetime); } @Override public TextureAtlasSprite get(RandomSource random) { return (TextureAtlasSprite)this.sprites.get(random.nextInt(this.sprites.size())); } public void rebind(List sprites) { this.sprites = ImmutableList.copyOf(sprites); } } @FunctionalInterface @Environment(EnvType.CLIENT) interface SpriteParticleRegistration { ParticleProvider create(SpriteSet spriteSet); } }