• CosyCritters.java
  • package pigcart.cosycritters;
    
    import net.fabricmc.api.ClientModInitializer;
    import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
    import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
    import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
    import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
    import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry;
    import net.fabricmc.fabric.api.networking.v1.PacketSender;
    import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes;
    import net.minecraft.network.chat.Component;
    import net.minecraft.client.Minecraft;
    import net.minecraft.client.multiplayer.ClientPacketListener;
    import net.minecraft.core.BlockPos;
    import net.minecraft.core.Direction;
    import net.minecraft.core.Registry;
    import net.minecraft.core.particles.SimpleParticleType;
    import net.minecraft.core.registries.BuiltInRegistries;
    import net.minecraft.resources.ResourceLocation;
    import net.minecraft.util.RandomSource;
    import net.minecraft.world.level.ClipContext;
    import net.minecraft.world.level.Level;
    import net.minecraft.world.level.block.state.BlockState;
    import net.minecraft.world.level.block.state.properties.BlockStateProperties;
    import net.minecraft.world.level.block.state.properties.Property;
    import net.minecraft.world.phys.HitResult;
    import net.minecraft.world.phys.Vec3;
    import net.minecraft.world.phys.shapes.CollisionContext;
    import pigcart.cosycritters.particle.BirdParticle;
    import pigcart.cosycritters.particle.HatManParticle;
    import pigcart.cosycritters.particle.MothParticle;
    import pigcart.cosycritters.particle.SpiderParticle;
    
    import java.util.ArrayList;
    import java.util.Optional;
    
    public class CosyCritters implements ClientModInitializer {
        //TODO: more robust mixins
        // ants (spiders that walk in a line)
        // flies (attracted to the scene of a death)
        // fireflies (swamps and plains, they glow)
        // fire flies (they glow, but aggressively)
        // fired flies (they are handing out resumes)
        // fryer flies (they are no longer handing out resumes)
        // bird flocking behaviour
        // bird angle-based sprite selection
        // a few more common bird types (pigeons, robins)
        // butterflies (moths without a lamp)
        // silverfish swarm (boids, renders in place of silverfish)
        // bee swarm (boids, renders in place of bee)
        // fish maybe?
        // rats/mice
        // jesus or something (when on low health and a totem is on your hotbar but youre not holding it)
        // herobrine (appears at edge of render distance by a wall, walking behind it when you look at him)
        // game of life
        //FIXME: torch/lantern mixins arent applying in 1.21.0 ???
    
        public static final String MOD_ID = "cosycritters";
        public static SimpleParticleType BIRD;
        public static SimpleParticleType HAT_MAN;
        public static SimpleParticleType MOTH;
        public static SimpleParticleType SPIDER;
    
        private static boolean wasSleeping = false;
        public static int birdCount = 0;
        public static int maxBirdCount;
        public static int mothCount = 0;
        public static int maxMothCount;
        public static int spiderCount = 0;
        public static int maxSpiderCount;
    
        public static ArrayList<MothParticle> moths = new ArrayList<>();
    
        @Override
        public void onInitializeClient() {
            ConfigManager.loadConfig();
    
            maxBirdCount = ConfigManager.getConfig().maxBirds;
            maxMothCount = ConfigManager.getConfig().maxMoths;
            maxSpiderCount = ConfigManager.getConfig().maxSpiders;
    
            BIRD = Registry.register(BuiltInRegistries.PARTICLE_TYPE, ResourceLocation.fromNamespaceAndPath(MOD_ID, "bird"), FabricParticleTypes.simple(true));
            ParticleFactoryRegistry.getInstance().register(BIRD, BirdParticle.Provider::new);
            HAT_MAN = Registry.register(BuiltInRegistries.PARTICLE_TYPE, ResourceLocation.fromNamespaceAndPath(MOD_ID, "hat_man"), FabricParticleTypes.simple(true));
            ParticleFactoryRegistry.getInstance().register(HAT_MAN, HatManParticle.Provider::new);
            MOTH = Registry.register(BuiltInRegistries.PARTICLE_TYPE, ResourceLocation.fromNamespaceAndPath(MOD_ID, "moth"), FabricParticleTypes.simple(true));
            ParticleFactoryRegistry.getInstance().register(MOTH, MothParticle.Provider::new);
            SPIDER = Registry.register(BuiltInRegistries.PARTICLE_TYPE, ResourceLocation.fromNamespaceAndPath(MOD_ID, "spider"), FabricParticleTypes.simple(true));
            ParticleFactoryRegistry.getInstance().register(SPIDER, SpiderParticle.Provider::new);
    
            ClientTickEvents.START_CLIENT_TICK.register(this::onTick);
    
            ClientCommandRegistrationCallback.EVENT.register((dispatcher,registryAccess) -> {
                dispatcher.register(ClientCommandManager.literal(MOD_ID)
                        .then(ClientCommandManager.literal("reload")
                                .executes(ctx -> {
                                    ConfigManager.loadConfig();
                                    maxBirdCount = ConfigManager.getConfig().maxBirds;
                                    maxMothCount = ConfigManager.getConfig().maxMoths;
                                    maxSpiderCount = ConfigManager.getConfig().maxSpiders;
                                    ctx.getSource().sendFeedback(Component.literal("Cosy Critters Config reloaded."));
                                    return 1;
                                })
                        )
                        .then(ClientCommandManager.literal("status")
                                .executes(ctx -> {
                                    ctx.getSource().sendFeedback(Component.literal(String.format("Birds: %d/%d", birdCount, maxBirdCount)));
                                    ctx.getSource().sendFeedback(Component.literal(String.format("Moths: %d/%d", mothCount, maxMothCount)));
                                    ctx.getSource().sendFeedback(Component.literal(String.format("Spiders: %d/%d", spiderCount, maxSpiderCount)));
                                    ctx.getSource().sendFeedback(Component.literal(String.format("Daytime: %d", ctx.getSource().getClient().level.dayTime())));
                                    return 0;
                                })
                        )
                );
            });
        }
    
        private void onTick(Minecraft minecraft) {
            if (minecraft.player != null) {
                tickHatManSpawnConditions(minecraft);
            }
        }
    
        public static boolean isDayButNotBroken(Level level) {
            // level.isDay always returns true in 1.21.0
            return (level.dayTime() % 24000 < 13000);
        }
    
        private void tickHatManSpawnConditions(Minecraft minecraft) {
            if (minecraft.level.dimensionType().moonPhase(minecraft.level.dayTime()) == 4) {
                if (minecraft.player.isSleeping()) {
                    if (!wasSleeping) {
                        trySpawnHatman(minecraft);
                        wasSleeping = true;
                    }
                } else if (wasSleeping) {
                    wasSleeping = false;
                }
            }
        }
        private void trySpawnHatman(Minecraft minecraft) {
            if (!ConfigManager.getConfig().spawnHatman) return;
            final Optional<BlockPos> sleepingPos = minecraft.player.getSleepingPos();
            if (sleepingPos.isPresent()) {
                BlockState state = minecraft.level.getBlockState(sleepingPos.get());
                Property property = BlockStateProperties.HORIZONTAL_FACING;
                if (state.hasProperty(property)) {
                    Direction direction = (Direction) state.getValue(property);
                    BlockPos blockPos = BlockPos.containing(minecraft.player.position()).relative(direction.getOpposite(), 2);
                    Vec3 pos = blockPos.getCenter();
                    RandomSource random = minecraft.player.getRandom();
                    Vec3 randomPos = new Vec3(pos.x + random.nextInt(2) - 1, pos.y, pos.z + random.nextInt(2) - 1);
                    if (minecraft.level.getBlockState(BlockPos.containing(randomPos)).isAir()) {
                        minecraft.particleEngine.createParticle(HAT_MAN, randomPos.x, randomPos.y + 0.5, randomPos.z, 0, 0, 0);
                    }
                }
            }
        }
        public static void trySpawnBird(BlockState state, Level level, BlockPos blockPos) {
            if (!ConfigManager.getConfig().spawnBird) return;
            if (    isDayButNotBroken(level)
                    && birdCount < maxBirdCount
                    && level.getBlockState(blockPos.above()).isAir()
                    && !Minecraft.getInstance().player.position().closerThan(blockPos.getCenter(), 10)
            ) {
                Vec3 pos = blockPos.getCenter();
                // clip method explicitly annotates it might return null value
                // if not handled, it might cause NPE, which will result in 
                // client crash
                final var hitResult = state.getCollisionShape(level, blockPos).clip(pos.add(0, 2, 0), pos.add(0, -0.6, 0), blockPos);
                if (hitResult == null) return;
                pos = hitResult.getLocation();
                Vec3 spawnFrom = pos.add(level.random.nextInt(10) - 5, level.random.nextInt(5), level.random.nextInt(10) - 5);
                if (level.clip(new ClipContext(spawnFrom, pos, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, CollisionContext.empty())).getType().equals(HitResult.Type.MISS)) {
                    level.addParticle(BIRD, spawnFrom.x, spawnFrom.y, spawnFrom.z, pos.x, pos.y, pos.z);
                }
            }
        }
        public static void trySpawnMoth(Level level, BlockPos blockPos) {
            if (!ConfigManager.getConfig().spawnMoth) return;
            if (    !isDayButNotBroken(level)
                    && mothCount < maxMothCount
                    && level.canSeeSky(blockPos)
            ) {
                level.addParticle(MOTH, blockPos.getX(), blockPos.getY(), blockPos.getZ(), 0, 0, 0);
            }
        }
        public static void trySpawnSpider(Level level, BlockPos blockPos) {
            if (!ConfigManager.getConfig().spawnSpider || spiderCount >= maxSpiderCount) return;
            if (Minecraft.getInstance().player.position().closerThan(blockPos.getCenter(), 2)) return;
            Direction direction = Direction.getRandom(level.random);
            blockPos = blockPos.relative(direction);
            BlockState state = level.getBlockState(blockPos);
            if (state.isFaceSturdy(level, blockPos, direction.getOpposite())) {
                final Vec3 spawnPos = blockPos.getCenter().add(new Vec3(direction.step()).multiply(-0.6f, -0.6f, -0.6f));
                level.addParticle(SPIDER, spawnPos.x, spawnPos.y, spawnPos.z, direction.get3DDataValue(), 0, 0);
            }
        }
    }