Better Animals Plus

Better Animals Plus

24M Downloads

Integration with Chicken Nerf

supersaiyansubtlety opened this issue ยท 24 comments

commented

Is your feature request related to a problem? Please describe.

I'm the dev of Chicken Nerf and a user has requested compatibility with this mod's bird entities. I can't add compatibility without mixins, so I plan to expose some of my methods to make mod integration easier.

Would you be willing to add integration?

Describe the solution you'd like

It would basically require three changes (that would only apply if Chicken Nerf were present):

  • No random egg laying
  • Breeding would produce a number of eggs according to chicken nerf configs (instead of baby entities)
  • Eggs would produce a number of baby entities according to Chicken Nerf's configs

Additional context

I'll be adding an entrypoint so you won't have to programmatically check if Chicken Nerf is present.

commented

I feel this would be quite doable with mixins on your side. Mixin into tick(), set timeUntilNextEgg to 2 or higher every tick, then mixin the child spawning method from vanilla, check if it fits the entity type and spawn additional eggs, then mixin EntityModEgg and bypass the original method completely by just canceling. What's the problem with mixins in this case?

I'd rather not maintain the functionality of hooking onto your mod, since the our side has no "compatibility" content. For something like Trinkets for example, we are using their APIs to add content - but since your mod is self-contained it makes more sense to be contained on your side.

commented

If you require assistance making an optional dependency for Better Animals Plus, I am quite familiar with janking my way around the classloader. You can define a mixin plugin for a separate mixin configuration that will only apply if BAP is loaded by using the shouldLoadMixin override - just check FabricLoader for BAP. Just return false if it's not loaded and none of the mixins will be loaded - thus avoiding a crash if the mod is not present.

commented

Here is an example of said workaround:

return (!mixinClassName.equals("dev.itsmeow.betteranimalsplus.mixin.ItemCapeMixin") && !mixinClassName.equals("dev.itsmeow.betteranimalsplus.mixin.ItemCapeClientMixin") && !mixinClassName.equals("dev.itsmeow.betteranimalsplus.mixin.TrinketSlotMixin")) || FabricLoader.getInstance().isModLoaded("trinkets");

commented

Mixins would work, but overall things would be more complex and more prone to breaking when either mod updates.

commented

I can see the argument for both ways, but I don't really intend on changing those methods for the foreseeable future. You can also just disable the mixin require so the mod does not hard fail if there are missing injection points.

Another alternative would be if I added an event API of some type on my side that your mod then hooked into.

commented

I'd prefer hooking into an event API on your side to mixins as it would be less brittle.

commented

Here's a preview of the API. Is this enough? You can hook the breeding from vanilla.

package dev.itsmeow.betteranimalsplus.api;

import dev.itsmeow.betteranimalsplus.common.entity.projectile.EntityModEgg;
import net.minecraft.world.entity.LivingEntity;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

public class ModEventBus {

    /**
     * Called when an egg-laying entity ticks its egg laying logic. Cancel to prevent the tick.
     */
    public static class LayEggTickEvent extends BAPEvent {

        protected static Set<Consumer<LayEggTickEvent>> SUBSCRIBERS = new HashSet<>();

        public static boolean subscribe(Consumer<LayEggTickEvent> listener) {
            return SUBSCRIBERS.add(listener);
        }

        public static boolean unsubscribe(Consumer<LayEggTickEvent> listener) {
            return SUBSCRIBERS.remove(listener);
        }

        public static boolean emit(LivingEntity entity) {
            LayEggTickEvent event = new LayEggTickEvent(entity);
            SUBSCRIBERS.forEach(c -> c.accept(event));
            return !event.isCancelled();
        }

        private final LivingEntity entity;

        private LayEggTickEvent(LivingEntity entity) {
            this.entity = entity;
        }

        public LivingEntity getEntity() {
            return entity;
        }
    }

    /**
     * Cancelling will spawn 0 entities. Otherwise, spawns getSpawnCount entities. Use setSpawnCount to change the amount.
     */
    public static class EggThrowSpawnCountEvent extends BAPEvent {

        protected static Set<Consumer<EggThrowSpawnCountEvent>> SUBSCRIBERS = new HashSet<>();

        public static boolean subscribe(Consumer<EggThrowSpawnCountEvent> listener) {
            return SUBSCRIBERS.add(listener);
        }

        public static boolean unsubscribe(Consumer<EggThrowSpawnCountEvent> listener) {
            return SUBSCRIBERS.remove(listener);
        }

        public static int emit(EntityModEgg entity, int spawnCount) {
            EggThrowSpawnCountEvent event = new EggThrowSpawnCountEvent(entity, spawnCount);
            SUBSCRIBERS.forEach(c -> c.accept(event));
            return event.isCancelled() ? 0 : event.getSpawnCount();
        }

        private final EntityModEgg entity;
        private int spawnCount;

        private EggThrowSpawnCountEvent(EntityModEgg entity, int spawnCount) {
            this.entity = entity;
            this.spawnCount = spawnCount;
        }

        /**
         * The egg entity
         */
        public EntityModEgg getEntity() {
            return entity;
        }

        /**
         * Change the amount of entities to spawn from egg
         */
        public void setSpawnCount(int spawnCount) {
            this.spawnCount = spawnCount;
        }

        /**
         * Default value is what would normally occur
         */
        public int getSpawnCount() {
            return this.spawnCount;
        }
    }

    /**
     * Cancelling will prevent spawning. Otherwise, see shouldSpawnEntities. Use setShouldSpawnEntities to force a spawn.
     */
    public static class ShouldEggSpawnEntitiesEvent extends BAPEvent {

        protected static Set<Consumer<ShouldEggSpawnEntitiesEvent>> SUBSCRIBERS = new HashSet<>();

        public static boolean subscribe(Consumer<ShouldEggSpawnEntitiesEvent> listener) {
            return SUBSCRIBERS.add(listener);
        }

        public static boolean unsubscribe(Consumer<ShouldEggSpawnEntitiesEvent> listener) {
            return SUBSCRIBERS.remove(listener);
        }

        public static boolean emit(EntityModEgg entity, boolean shouldSpawnEntities) {
            ShouldEggSpawnEntitiesEvent event = new ShouldEggSpawnEntitiesEvent(entity, shouldSpawnEntities);
            SUBSCRIBERS.forEach(c -> c.accept(event));
            return event.isCancelled() ? false : event.shouldSpawnEntities();
        }

        private final EntityModEgg entity;
        private boolean shouldSpawnEntities;

        private ShouldEggSpawnEntitiesEvent(EntityModEgg entity, boolean shouldSpawnEntities) {
            this.entity = entity;
            this.shouldSpawnEntities = shouldSpawnEntities;
        }

        /**
         * The egg entity
         */
        public EntityModEgg getEntity() {
            return entity;
        }

        /**
         * Set if entities should spawn from the egg or not
         */
        public void setShouldSpawnEntities(boolean shouldSpawnEntities) {
            this.shouldSpawnEntities = shouldSpawnEntities;
        }

        /**
         * Default value is what would normally occur
         */
        public boolean shouldSpawnEntities() {
            return this.shouldSpawnEntities;
        }
    }

    public static abstract class BAPEvent {

        private boolean canceled = false;

        public void setCanceled(boolean canceled) {
            this.canceled = canceled;
        }

        public boolean isCancelled() {
            return this.canceled;
        }

    }
}
commented

Looks like it, I'll try implementing integration soon.
Thanks!

commented

The release isn't on CurseForge yet but you can start locally with this alpha build

commented

I'm not sure how I can get this pre-release in into my dev environment.
I've tried with JitPack, but:

  • the most recent version displayed on jitpack.io is 7.1.2
  • I tried to get it via both versions 1.16.5-11.0.3.9 and 11.0.3.9
  • I tried to get it via commit hash 843dba7

but gradle failed to resolve the dependencies.
IIRC JitPack requires a configuration file to work on newer java versions.
Is there a way to get it other than JitPack?

Additionally, it's name implies it's for 1.16, is that accurate?
I usually only add features to my mods on the most recent version of Minecraft.

commented

Hey, it's up on CurseForge now for all versions. You can use my maven.

repositories {
  maven {
    url = "https://maven.itsmeow.dev/"
  }
}
dependencies {
  modImplementation("dev.itsmeow.betteranimalsplus:betteranimalsplus-fabric:1.18.1-11.0.4") {
      transitive = false
  }
}
commented

Also tip for getting stuff locally in the future, just use flatDir.

repositories {
    flatDir {
        dir 'libs'
    }
}
dependencies {
    modImplementation("dev.itsmeow.betteranimalsplus:betteranimalsplus:1.16.5-11.0.3.9-fabric")
}

Then create the folder libs and put downloaded jar inside.

commented

Oh that's super useful, thanks!

commented

I have most of the integration implemented, but I believe I'm accessing BAP's items too early.
At this line https://gitlab.com/supersaiyansubtlety/chicken_nerf/-/blob/bap-integration/src/main/java/net/sssubtlety/chicken_nerf/FeatureControl.java#L64
I get
Caused by: java.lang.NullPointerException: Registry Object not present: betteranimalsplus:pheasant_egg

I believe it's because I'm calling ModItems.PHEASANT_EGG.get() before ModItems.ITEMS.register() has been called.
AFAICT there's no way for me to ensure my call runs after register().

Could you add one more event that fires after all of your registration is complete?

commented

Just perform the operations on the mod load completion event

commented

I can't find that event, is it in FAPI or arch?

commented

Fabric, probably? I don't know Fabric's loading lifecycle events.

commented

Also BTW, Geese are breedable and lay eggs, you should add compatibility for them as well.

commented

Nevermind, I'm blind and didn't see the line above it.

commented

I have a solution to your problem. Change the map from Function<Random, ItemConvertible> to Function<Random, Supplier<ItemConvertible>>. Then supply a reference to ModItems.X::get. Then the method will not be called until it's actually used.

commented

Another note, currently your implementation WILL cause a NoClassDefFound error if Better Animals Plus is not present. You need to add proper classloading prevention for a true optional dependency. If you make a static inner class containing the BAP code and then wrap your reference to it in a double supplier, it should avoid classloading.

Supplier<Runnable> run = () -> OptionalBAPCompat::run;
if(optBAPContainer.isPresent()) {
    run.get().run();
}

public static class OptionalBAPCompat {
    public static void run() {
       // do stuff
    }
}
commented

I tested without BAP and it was fine

commented

I have a solution to your problem. Change the map from Function<Random, ItemConvertible> to Function<Random, Supplier>. Then supply a reference to ModItems.X::get. Then the method will not be called until it's actually used.

I actually don't need the function to return a supplier, just the fact that it's a function is enough, which is why it errors for pheasants but not geese, despite geese being first.

I need to switch to mapping pheasants and turkeys with mapAnimalToEggSupplier (which I need to rename) instead of mapAnimalToEgg, which will avoid accessing the items too early.

But it's late where I am and I'm busy tomorrow, so I probably won't get to it until the weekend.
Anyways, thanks for your help!

commented

BAP integration is in version 1.0.12!