diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index 24300a0ed5..1f945197d9 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -54,11 +54,11 @@ jobs: run: ./gradlew assemble checkFormatting - name: Run JCC - if: ${{ ! startsWith(github.event.pull_request.head.ref, 'port/') && ! startsWith(github.ref_name, 'port/') }} + if: ${{ ! startsWith(github.event.pull_request.head.ref, 'port/') && ! startsWith(github.ref_name, 'port/') && ! startsWith(github.event.pull_request.base.ref, 'port/') }} run: ./gradlew checkJarCompatibility - name: Upload JCC - if: ${{ ! startsWith(github.event.pull_request.head.ref, 'port/') && ! startsWith(github.ref_name, 'port/') }} + if: ${{ ! startsWith(github.event.pull_request.head.ref, 'port/') && ! startsWith(github.ref_name, 'port/') && ! startsWith(github.event.pull_request.base.ref, 'port/') }} uses: neoforged/action-jar-compatibility/upload@v1 - name: Publish artifacts diff --git a/gradle.properties b/gradle.properties index d524c7c996..bd49179e04 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,10 +9,10 @@ org.gradle.debug=false java_version=21 -minecraft_version=1.21.2-rc1 -neoform_version=20241017.134216 +minecraft_version=1.21.3 +neoform_version=20241023.131943 # on snapshot versions, used to prefix the version -neoforge_snapshot_next_stable=21.2 +neoforge_snapshot_next_stable=21.4 mergetool_version=2.0.0 accesstransformers_version=10.0.1 diff --git a/patches/net/minecraft/client/KeyboardHandler.java.patch b/patches/net/minecraft/client/KeyboardHandler.java.patch index 2470d92b3a..ca87c0a864 100644 --- a/patches/net/minecraft/client/KeyboardHandler.java.patch +++ b/patches/net/minecraft/client/KeyboardHandler.java.patch @@ -23,6 +23,14 @@ return; } } +@@ -507,6 +_,7 @@ + } + } + } ++ net.neoforged.neoforge.client.ClientHooks.onKeyInput(p_90895_, p_90896_, p_90897_, p_90898_); + } + } + @@ -516,10 +_,20 @@ if (screen != null && this.minecraft.getOverlay() == null) { try { diff --git a/patches/net/minecraft/server/level/ChunkMap.java.patch b/patches/net/minecraft/server/level/ChunkMap.java.patch index 1a37c82136..d6dcc91909 100644 --- a/patches/net/minecraft/server/level/ChunkMap.java.patch +++ b/patches/net/minecraft/server/level/ChunkMap.java.patch @@ -28,7 +28,7 @@ this.markPosition(p_140418_, chunkaccess.getPersistedStatus().getChunkType()); return chunkaccess; } else { -@@ -778,6 +_,7 @@ +@@ -780,6 +_,7 @@ Profiler.get().incrementCounter("chunkSave"); this.activeChunkWrites.incrementAndGet(); SerializableChunkData serializablechunkdata = SerializableChunkData.copyOf(this.level, p_140259_); @@ -36,7 +36,7 @@ CompletableFuture completablefuture = CompletableFuture.supplyAsync(serializablechunkdata::write, Util.backgroundExecutor()); this.write(chunkpos, completablefuture::join).handle((p_381690_, p_381691_) -> { if (p_381691_ != null) { -@@ -844,9 +_,11 @@ +@@ -846,9 +_,11 @@ private static void markChunkPendingToSend(ServerPlayer p_295834_, LevelChunk p_296281_) { p_295834_.connection.chunkSender.markChunkPendingToSend(p_296281_); @@ -48,7 +48,7 @@ p_294215_.connection.chunkSender.dropChunk(p_294215_, p_294758_); } -@@ -1057,6 +_,7 @@ +@@ -1059,6 +_,7 @@ this.playerMap.unIgnorePlayer(p_140185_); } @@ -56,7 +56,7 @@ this.updateChunkTracking(p_140185_); } } -@@ -1108,7 +_,7 @@ +@@ -1110,7 +_,7 @@ } protected void addEntity(Entity p_140200_) { @@ -65,7 +65,7 @@ EntityType entitytype = p_140200_.getType(); int i = entitytype.clientTrackingRange() * 16; if (i != 0) { -@@ -1352,5 +_,20 @@ +@@ -1354,5 +_,20 @@ this.updatePlayer(serverplayer); } } diff --git a/patches/net/minecraft/server/level/ServerChunkCache.java.patch b/patches/net/minecraft/server/level/ServerChunkCache.java.patch index 1c3f4867c4..d56419a092 100644 --- a/patches/net/minecraft/server/level/ServerChunkCache.java.patch +++ b/patches/net/minecraft/server/level/ServerChunkCache.java.patch @@ -30,10 +30,10 @@ this.storeInCache(i, chunkaccess1, ChunkStatus.FULL); @@ -384,7 +_,7 @@ private void collectTickingChunks(List p_363421_) { - this.chunkMap.forEachSpawnCandidateChunk(p_370483_ -> { - LevelChunk levelchunk = p_370483_.getTickingChunk(); -- if (levelchunk != null && this.level.isNaturalSpawningAllowed(p_370483_.getPos())) { -+ if (levelchunk != null && this.level.isNaturalSpawningAllowed(p_370483_.getPos()) || this.distanceManager.shouldForceTicks(p_370483_.getPos().toLong())) { + this.chunkMap.forEachSpawnCandidateChunk(p_381767_ -> { + LevelChunk levelchunk = p_381767_.getTickingChunk(); +- if (levelchunk != null && this.level.isNaturalSpawningAllowed(p_381767_.getPos())) { ++ if (levelchunk != null && this.level.isNaturalSpawningAllowed(p_381767_.getPos()) || this.distanceManager.shouldForceTicks(p_381767_.getPos().toLong())) { p_363421_.add(levelchunk); } }); diff --git a/patches/net/minecraft/util/datafix/DataFixers.java.patch b/patches/net/minecraft/util/datafix/DataFixers.java.patch index 7d19c8c1d7..5ab30c4c68 100644 --- a/patches/net/minecraft/util/datafix/DataFixers.java.patch +++ b/patches/net/minecraft/util/datafix/DataFixers.java.patch @@ -1,6 +1,6 @@ --- a/net/minecraft/util/datafix/DataFixers.java +++ b/net/minecraft/util/datafix/DataFixers.java -@@ -1307,10 +_,35 @@ +@@ -1308,10 +_,35 @@ Schema schema199 = p_14514_.addSchema(3800, SAME_NAMESPACED); UnaryOperator unaryoperator2 = createRenamer(Map.of("minecraft:scute", "minecraft:turtle_scute")); p_14514_.addFixer(ItemRenameFix.create(schema199, "Rename scute item to turtle_scute", unaryoperator2)); @@ -36,7 +36,7 @@ Schema schema201 = p_14514_.addSchema(3807, V3807::new); p_14514_.addFixer(new AddNewChoices(schema201, "Added Vault", References.BLOCK_ENTITY)); Schema schema202 = p_14514_.addSchema(3807, 1, SAME_NAMESPACED); -@@ -1333,6 +_,18 @@ +@@ -1334,6 +_,18 @@ schema209, "Rename jump strength attribute", createRenamer("minecraft:horse.jump_strength", "minecraft:generic.jump_strength") ) ); diff --git a/patches/net/minecraft/world/entity/Entity.java.patch b/patches/net/minecraft/world/entity/Entity.java.patch index 801a7043a6..15f8303a24 100644 --- a/patches/net/minecraft/world/entity/Entity.java.patch +++ b/patches/net/minecraft/world/entity/Entity.java.patch @@ -48,7 +48,7 @@ } this.checkBelowWorld(); -@@ -763,12 +_,12 @@ +@@ -760,12 +_,12 @@ this.setRemainingFireTicks(-this.getFireImmuneTicks()); } @@ -63,7 +63,7 @@ this.setRemainingFireTicks(-this.getFireImmuneTicks()); } } -@@ -848,9 +_,7 @@ +@@ -845,9 +_,7 @@ return blockpos; } else { BlockState blockstate = this.level().getBlockState(blockpos); @@ -74,7 +74,7 @@ ? blockpos.atY(Mth.floor(this.position.y - (double)p_216987_)) : blockpos; } -@@ -1132,19 +_,19 @@ +@@ -1129,19 +_,19 @@ return !blockstate.is(BlockTags.INSIDE_STEP_SOUND_BLOCKS) && !blockstate.is(BlockTags.COMBINATION_STEP_SOUND_BLOCKS) ? p_278049_ : blockpos; } @@ -100,7 +100,7 @@ this.playSound(soundtype.getStepSound(), soundtype.getVolume() * 0.15F, soundtype.getPitch()); } -@@ -1297,20 +_,23 @@ +@@ -1294,20 +_,23 @@ public void updateSwimming() { if (this.isSwimming()) { @@ -129,7 +129,7 @@ } void updateInWaterStateAndDoWaterCurrentPushing() { -@@ -1335,6 +_,7 @@ +@@ -1332,6 +_,7 @@ private void updateFluidOnEyes() { this.wasEyeInWater = this.isEyeInFluid(FluidTags.WATER); this.fluidOnEyes.clear(); @@ -137,7 +137,7 @@ double d0 = this.getEyeY(); if (this.getVehicle() instanceof AbstractBoat abstractboat && !abstractboat.isUnderWater() -@@ -1347,7 +_,7 @@ +@@ -1344,7 +_,7 @@ FluidState fluidstate = this.level().getFluidState(blockpos); double d1 = (double)((float)blockpos.getY() + fluidstate.getHeight(this.level(), blockpos)); if (d1 > d0) { @@ -146,7 +146,7 @@ } } -@@ -1392,12 +_,13 @@ +@@ -1389,12 +_,13 @@ } public boolean canSpawnSprintParticle() { @@ -161,7 +161,7 @@ if (blockstate.getRenderShape() != RenderShape.INVISIBLE) { Vec3 vec3 = this.getDeltaMovement(); BlockPos blockpos1 = this.blockPosition(); -@@ -1411,16 +_,19 @@ +@@ -1408,16 +_,19 @@ d1 = Mth.clamp(d1, (double)blockpos.getZ(), (double)blockpos.getZ() + 1.0); } @@ -183,7 +183,7 @@ } public void moveRelative(float p_19921_, Vec3 p_19922_) { -@@ -1799,6 +_,10 @@ +@@ -1796,6 +_,10 @@ p_20241_.put("Tags", listtag); } @@ -194,7 +194,7 @@ this.addAdditionalSaveData(p_20241_); if (this.isVehicle()) { ListTag listtag1 = new ListTag(); -@@ -1880,6 +_,8 @@ +@@ -1877,6 +_,8 @@ this.setGlowingTag(p_20259_.getBoolean("Glowing")); this.setTicksFrozen(p_20259_.getInt("TicksFrozen")); this.hasVisualFire = p_20259_.getBoolean("HasVisualFire"); @@ -203,7 +203,7 @@ if (p_20259_.contains("Tags", 9)) { this.tags.clear(); ListTag listtag3 = p_20259_.getList("Tags", 8); -@@ -1962,6 +_,8 @@ +@@ -1959,6 +_,8 @@ } else { ItemEntity itementity = new ItemEntity(p_376141_, this.getX(), this.getY() + (double)p_376881_, this.getZ(), p_376472_); itementity.setDefaultPickUpDelay(); @@ -212,7 +212,7 @@ p_376141_.addFreshEntity(itementity); return itementity; } -@@ -2029,7 +_,11 @@ +@@ -2026,7 +_,11 @@ public void rideTick() { this.setDeltaMovement(Vec3.ZERO); @@ -225,7 +225,7 @@ if (this.isPassenger()) { this.getVehicle().positionRider(this); } -@@ -2089,6 +_,7 @@ +@@ -2086,6 +_,7 @@ } } @@ -233,7 +233,7 @@ if (p_19967_ || this.canRide(p_19966_) && p_19966_.canAddPassenger(this)) { if (this.isPassenger()) { this.stopRiding(); -@@ -2120,6 +_,7 @@ +@@ -2117,6 +_,7 @@ public void removeVehicle() { if (this.vehicle != null) { Entity entity = this.vehicle; @@ -241,7 +241,7 @@ this.vehicle = null; entity.removePassenger(this); } -@@ -2169,6 +_,8 @@ +@@ -2166,6 +_,8 @@ return this.passengers.isEmpty(); } @@ -250,7 +250,7 @@ protected boolean couldAcceptPassenger() { return true; } -@@ -2357,7 +_,7 @@ +@@ -2354,7 +_,7 @@ } public boolean isVisuallyCrawling() { @@ -259,7 +259,7 @@ } public void setSwimming(boolean p_20283_) { -@@ -2470,7 +_,7 @@ +@@ -2467,7 +_,7 @@ this.igniteForSeconds(8.0F); } @@ -268,7 +268,7 @@ } public void onAboveBubbleCol(boolean p_20313_) { -@@ -2565,7 +_,7 @@ +@@ -2562,7 +_,7 @@ } protected Component getTypeName() { @@ -277,7 +277,7 @@ } public boolean is(Entity p_20356_) { -@@ -2620,10 +_,11 @@ +@@ -2617,10 +_,11 @@ } protected final boolean isInvulnerableToBase(DamageSource p_20122_) { @@ -290,7 +290,7 @@ } public boolean isInvulnerable() { -@@ -2648,6 +_,7 @@ +@@ -2645,6 +_,7 @@ @Nullable public Entity teleport(TeleportTransition p_379899_) { @@ -298,7 +298,7 @@ if (this.level() instanceof ServerLevel serverlevel && !this.isRemoved()) { ServerLevel serverlevel1 = p_379899_.newLevel(); boolean flag = serverlevel1.dimension() != serverlevel.dimension(); -@@ -2855,6 +_,7 @@ +@@ -2852,6 +_,7 @@ return this.stringUUID; } @@ -306,7 +306,7 @@ public boolean isPushedByFluid() { return true; } -@@ -2963,6 +_,8 @@ +@@ -2960,6 +_,8 @@ EntityDimensions entitydimensions = this.dimensions; Pose pose = this.getPose(); EntityDimensions entitydimensions1 = this.getDimensions(pose); @@ -315,7 +315,7 @@ this.dimensions = entitydimensions1; this.eyeHeight = entitydimensions1.eyeHeight(); this.reapplyPosition(); -@@ -3268,9 +_,17 @@ +@@ -3265,9 +_,17 @@ return Mth.lerp(p_352259_, this.yRotO, this.yRot); } @@ -334,7 +334,7 @@ } else { AABB aabb = this.getBoundingBox().deflate(0.001); int i = Mth.floor(aabb.minX); -@@ -3285,25 +_,36 @@ +@@ -3282,25 +_,36 @@ Vec3 vec3 = Vec3.ZERO; int k1 = 0; BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(); @@ -378,7 +378,7 @@ } } } -@@ -3311,27 +_,30 @@ +@@ -3308,27 +_,30 @@ } } @@ -419,7 +419,7 @@ } } -@@ -3344,7 +_,10 @@ +@@ -3341,7 +_,10 @@ return !this.level().hasChunksAt(i, k, j, l); } @@ -430,7 +430,7 @@ return this.fluidHeight.getDouble(p_204037_); } -@@ -3481,6 +_,7 @@ +@@ -3478,6 +_,7 @@ this.levelCallback.onMove(); } @@ -438,7 +438,7 @@ } public void checkDespawn() { -@@ -3606,6 +_,128 @@ +@@ -3603,6 +_,128 @@ public boolean mayInteract(ServerLevel p_376870_, BlockPos p_146844_) { return true; diff --git a/patches/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch b/patches/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch index ed84bbe0f6..3360a92b16 100644 --- a/patches/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch +++ b/patches/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch @@ -1,6 +1,6 @@ --- a/net/minecraft/world/entity/vehicle/AbstractMinecart.java +++ b/net/minecraft/world/entity/vehicle/AbstractMinecart.java -@@ -420,8 +_,8 @@ +@@ -430,8 +_,8 @@ public Vec3 getRedstoneDirection(BlockPos p_361470_) { BlockState blockstate = this.level().getBlockState(p_361470_); diff --git a/patches/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch b/patches/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch index 0e5d4fb6b5..d8e36d27ee 100644 --- a/patches/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch +++ b/patches/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch @@ -9,7 +9,7 @@ this.minecart.activateMinecart(blockpos.getX(), blockpos.getY(), blockpos.getZ(), blockstate.getValue(PoweredRailBlock.POWERED)); } } else { -@@ -149,7 +_,7 @@ +@@ -148,7 +_,7 @@ d1 = (double)blockpos.getY(); boolean flag = false; boolean flag1 = false; @@ -18,7 +18,7 @@ flag = blockstate.getValue(PoweredRailBlock.POWERED); flag1 = !flag; } -@@ -160,7 +_,7 @@ +@@ -159,7 +_,7 @@ } Vec3 vec31 = this.getDeltaMovement(); @@ -27,7 +27,7 @@ switch (railshape) { case ASCENDING_EAST: this.setDeltaMovement(vec31.add(-d3, 0.0, 0.0)); -@@ -318,9 +_,10 @@ +@@ -317,9 +_,10 @@ j--; } @@ -40,7 +40,7 @@ p_363435_ = (double)j; if (railshape.isSlope()) { p_363435_ = (double)(j + 1); -@@ -357,9 +_,10 @@ +@@ -356,9 +_,10 @@ j--; } diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java index 8f66013fcf..c19d9e788f 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeMod.java @@ -52,6 +52,7 @@ import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.display.SlotDisplay; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.LevelReader; @@ -141,13 +142,16 @@ import net.neoforged.neoforge.fluids.CauldronFluidContent; import net.neoforged.neoforge.fluids.FluidType; import net.neoforged.neoforge.fluids.crafting.CompoundFluidIngredient; +import net.neoforged.neoforge.fluids.crafting.CustomDisplayFluidIngredient; import net.neoforged.neoforge.fluids.crafting.DataComponentFluidIngredient; import net.neoforged.neoforge.fluids.crafting.DifferenceFluidIngredient; -import net.neoforged.neoforge.fluids.crafting.EmptyFluidIngredient; +import net.neoforged.neoforge.fluids.crafting.FluidIngredientCodecs; import net.neoforged.neoforge.fluids.crafting.FluidIngredientType; import net.neoforged.neoforge.fluids.crafting.IntersectionFluidIngredient; -import net.neoforged.neoforge.fluids.crafting.SingleFluidIngredient; -import net.neoforged.neoforge.fluids.crafting.TagFluidIngredient; +import net.neoforged.neoforge.fluids.crafting.SimpleFluidIngredient; +import net.neoforged.neoforge.fluids.crafting.display.FluidSlotDisplay; +import net.neoforged.neoforge.fluids.crafting.display.FluidStackSlotDisplay; +import net.neoforged.neoforge.fluids.crafting.display.FluidTagSlotDisplay; import net.neoforged.neoforge.forge.snapshots.ForgeSnapshotsMod; import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; import net.neoforged.neoforge.network.DualStackUtils; @@ -353,6 +357,12 @@ public class NeoForgeMod { */ public static final Holder NOT_HOLDER_SET = HOLDER_SET_TYPES.register("not", NotHolderSet.Type::new); + private static final DeferredRegister> SLOT_DISPLAY_TYPES = DeferredRegister.create(Registries.SLOT_DISPLAY, NeoForgeVersion.MOD_ID); + + public static final DeferredHolder, SlotDisplay.Type> FLUID_SLOT_DISPLAY = SLOT_DISPLAY_TYPES.register("fluid", () -> new SlotDisplay.Type<>(FluidSlotDisplay.MAP_CODEC, FluidSlotDisplay.STREAM_CODEC)); + public static final DeferredHolder, SlotDisplay.Type> FLUID_STACK_SLOT_DISPLAY = SLOT_DISPLAY_TYPES.register("fluid_stack", () -> new SlotDisplay.Type<>(FluidStackSlotDisplay.MAP_CODEC, FluidStackSlotDisplay.STREAM_CODEC)); + public static final DeferredHolder, SlotDisplay.Type> FLUID_TAG_SLOT_DISPLAY = SLOT_DISPLAY_TYPES.register("fluid_tag", () -> new SlotDisplay.Type<>(FluidTagSlotDisplay.MAP_CODEC, FluidTagSlotDisplay.STREAM_CODEC)); + private static final DeferredRegister> INGREDIENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.INGREDIENT_TYPES, NeoForgeVersion.MOD_ID); public static final DeferredHolder, IngredientType> COMPOUND_INGREDIENT_TYPE = INGREDIENT_TYPES.register("compound", () -> new IngredientType<>(CompoundIngredient.CODEC)); @@ -363,13 +373,12 @@ public class NeoForgeMod { public static final DeferredHolder, IngredientType> CUSTOM_DISPLAY_INGREDIENT = INGREDIENT_TYPES.register("custom_display", () -> new IngredientType<>(CustomDisplayIngredient.CODEC)); private static final DeferredRegister> FLUID_INGREDIENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.FLUID_INGREDIENT_TYPES, NeoForgeVersion.MOD_ID); - public static final DeferredHolder, FluidIngredientType> SINGLE_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("single", () -> new FluidIngredientType<>(SingleFluidIngredient.CODEC)); - public static final DeferredHolder, FluidIngredientType> TAG_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("tag", () -> new FluidIngredientType<>(TagFluidIngredient.CODEC)); - public static final DeferredHolder, FluidIngredientType> EMPTY_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("empty", () -> new FluidIngredientType<>(EmptyFluidIngredient.CODEC)); + public static final DeferredHolder, FluidIngredientType> SIMPLE_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("simple", FluidIngredientCodecs::simpleType); public static final DeferredHolder, FluidIngredientType> COMPOUND_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("compound", () -> new FluidIngredientType<>(CompoundFluidIngredient.CODEC)); public static final DeferredHolder, FluidIngredientType> DATA_COMPONENT_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("components", () -> new FluidIngredientType<>(DataComponentFluidIngredient.CODEC)); public static final DeferredHolder, FluidIngredientType> DIFFERENCE_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("difference", () -> new FluidIngredientType<>(DifferenceFluidIngredient.CODEC)); public static final DeferredHolder, FluidIngredientType> INTERSECTION_FLUID_INGREDIENT_TYPE = FLUID_INGREDIENT_TYPES.register("intersection", () -> new FluidIngredientType<>(IntersectionFluidIngredient.CODEC)); + public static final DeferredHolder, FluidIngredientType> CUSTOM_DISPLAY_FLUID_INGREDIENT = FLUID_INGREDIENT_TYPES.register("custom_display", () -> new FluidIngredientType<>(CustomDisplayFluidIngredient.CODEC, CustomDisplayFluidIngredient.STREAM_CODEC)); private static final DeferredRegister> CONDITION_CODECS = DeferredRegister.create(NeoForgeRegistries.Keys.CONDITION_CODECS, NeoForgeVersion.MOD_ID); public static final DeferredHolder, MapCodec> AND_CONDITION = CONDITION_CODECS.register("and", () -> AndCondition.CODEC); @@ -547,6 +556,7 @@ public NeoForgeMod(IEventBus modEventBus, Dist dist, ModContainer container) { VANILLA_FLUID_TYPES.register(modEventBus); ENTITY_PREDICATE_CODECS.register(modEventBus); ITEM_SUB_PREDICATES.register(modEventBus); + SLOT_DISPLAY_TYPES.register(modEventBus); INGREDIENT_TYPES.register(modEventBus); CONDITION_CODECS.register(modEventBus); GLOBAL_LOOT_MODIFIER_SERIALIZERS.register(modEventBus); diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IItemExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IItemExtension.java index b4cc31de7d..ab5cc0f8e1 100644 --- a/src/main/java/net/neoforged/neoforge/common/extensions/IItemExtension.java +++ b/src/main/java/net/neoforged/neoforge/common/extensions/IItemExtension.java @@ -314,19 +314,6 @@ default ResourceLocation getArmorTexture(ItemStack stack, EquipmentModel.LayerTy return null; } - /** - * Called when a entity tries to play the 'swing' animation. - * - * @param entity The entity swinging the item. - * @return True to cancel any further processing by {@link LivingEntity} - * @deprecated To be replaced with hand sensitive version in 21.2 - * @see #onEntitySwing(ItemStack, LivingEntity, InteractionHand) - */ - @Deprecated(forRemoval = true, since = "21.1") - default boolean onEntitySwing(ItemStack stack, LivingEntity entity) { - return false; - } - /** * Called when a entity tries to play the 'swing' animation. * @@ -334,7 +321,7 @@ default boolean onEntitySwing(ItemStack stack, LivingEntity entity) { * @return True to cancel any further processing by {@link LivingEntity} */ default boolean onEntitySwing(ItemStack stack, LivingEntity entity, InteractionHand hand) { - return onEntitySwing(stack, entity); + return false; } /** diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IItemStackExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IItemStackExtension.java index 68a254a3dc..0b18ba462f 100644 --- a/src/main/java/net/neoforged/neoforge/common/extensions/IItemStackExtension.java +++ b/src/main/java/net/neoforged/neoforge/common/extensions/IItemStackExtension.java @@ -195,19 +195,6 @@ default boolean canDisableShield(ItemStack shield, LivingEntity entity, LivingEn return self().getItem().canDisableShield(self(), shield, entity, attacker); } - /** - * Called when a entity tries to play the 'swing' animation. - * - * @param entity The entity swinging the item. - * @return True to cancel any further processing by {@link LivingEntity} - * @deprecated To be replaced with hand sensitive version in 21.2 - * @see #onEntitySwing(LivingEntity, InteractionHand) - */ - @Deprecated(forRemoval = true, since = "21.1") - default boolean onEntitySwing(LivingEntity entity) { - return self().getItem().onEntitySwing(self(), entity); - } - /** * Called when a entity tries to play the 'swing' animation. * diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/CompoundFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/CompoundFluidIngredient.java index f2c551a66e..09cfde9e25 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/CompoundFluidIngredient.java +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/CompoundFluidIngredient.java @@ -9,6 +9,8 @@ import java.util.List; import java.util.Objects; import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.crafting.CompoundIngredient; import net.neoforged.neoforge.common.util.NeoForgeExtraCodecs; @@ -22,7 +24,7 @@ * @see CompoundIngredient CompoundIngredient, its item equivalent */ public final class CompoundFluidIngredient extends FluidIngredient { - public static final MapCodec CODEC = NeoForgeExtraCodecs.aliasedFieldOf(FluidIngredient.LIST_CODEC_NON_EMPTY, "children", "ingredients").xmap(CompoundFluidIngredient::new, CompoundFluidIngredient::children); + public static final MapCodec CODEC = NeoForgeExtraCodecs.aliasedFieldOf(FluidIngredient.CODEC.listOf(1, Integer.MAX_VALUE), "children", "ingredients").xmap(CompoundFluidIngredient::new, CompoundFluidIngredient::children); private final List children; @@ -37,8 +39,6 @@ public CompoundFluidIngredient(List children) { * Creates a compound ingredient from the given list of ingredients. */ public static FluidIngredient of(FluidIngredient... children) { - if (children.length == 0) - return FluidIngredient.empty(); if (children.length == 1) return children[0]; @@ -49,21 +49,15 @@ public static FluidIngredient of(FluidIngredient... children) { * Creates a compound ingredient from the given list of ingredients. */ public static FluidIngredient of(List children) { - if (children.isEmpty()) - return FluidIngredient.empty(); if (children.size() == 1) return children.getFirst(); return new CompoundFluidIngredient(children); } - public static FluidIngredient of(Stream stream) { - return of(stream.toList()); - } - @Override - public Stream generateStacks() { - return children.stream().flatMap(FluidIngredient::generateStacks); + public Stream> generateFluids() { + return children.stream().flatMap(FluidIngredient::generateFluids); } @Override diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/CustomDisplayFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/CustomDisplayFluidIngredient.java new file mode 100644 index 0000000000..b48090e710 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/CustomDisplayFluidIngredient.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.Objects; +import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.level.material.Fluid; +import net.neoforged.neoforge.common.NeoForgeMod; +import net.neoforged.neoforge.fluids.FluidStack; + +/** + * FluidIngredient that wraps another fluid ingredient to override its {@link SlotDisplay}. + */ +public final class CustomDisplayFluidIngredient extends FluidIngredient { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance + .group( + FluidIngredient.CODEC.fieldOf("base").forGetter(CustomDisplayFluidIngredient::base), + SlotDisplay.CODEC.fieldOf("display").forGetter(CustomDisplayFluidIngredient::display)) + .apply(instance, CustomDisplayFluidIngredient::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + FluidIngredient.STREAM_CODEC, + CustomDisplayFluidIngredient::base, + SlotDisplay.STREAM_CODEC, + CustomDisplayFluidIngredient::display, + CustomDisplayFluidIngredient::new); + + private final FluidIngredient base; + private final SlotDisplay display; + + public CustomDisplayFluidIngredient(FluidIngredient base, SlotDisplay display) { + this.base = base; + this.display = display; + } + + public static FluidIngredient of(FluidIngredient base, SlotDisplay display) { + return new CustomDisplayFluidIngredient(base, display); + } + + @Override + public boolean test(FluidStack stack) { + return base.test(stack); + } + + @Override + public Stream> generateFluids() { + return base.generateFluids(); + } + + @Override + public boolean isSimple() { + return base.isSimple(); + } + + @Override + public FluidIngredientType getType() { + return NeoForgeMod.CUSTOM_DISPLAY_FLUID_INGREDIENT.get(); + } + + public FluidIngredient base() { + return base; + } + + @Override + public SlotDisplay display() { + return display; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CustomDisplayFluidIngredient other && + Objects.equals(this.base, other.base) && + Objects.equals(this.display, other.display); + } + + @Override + public int hashCode() { + return Objects.hash(base, display); + } + + @Override + public String toString() { + return "CustomDisplayFluidIngredient[" + + "base=" + base + ", " + + "display=" + display + ']'; + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/DataComponentFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/DataComponentFluidIngredient.java index c3ebdcdaf3..a4d48dec0d 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/DataComponentFluidIngredient.java +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/DataComponentFluidIngredient.java @@ -20,11 +20,13 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.resources.HolderSetCodec; +import net.minecraft.world.item.crafting.display.SlotDisplay; import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.crafting.DataComponentIngredient; import net.neoforged.neoforge.fluids.FluidStack; import net.neoforged.neoforge.fluids.FluidType; +import net.neoforged.neoforge.fluids.crafting.display.FluidStackSlotDisplay; /** * Fluid ingredient that matches the given set of fluids, additionally performing either a @@ -39,7 +41,7 @@ public class DataComponentFluidIngredient extends FluidIngredient { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( builder -> builder .group( - HolderSetCodec.create(Registries.FLUID, BuiltInRegistries.FLUID.holderByNameCodec(), false).fieldOf("fluids").forGetter(DataComponentFluidIngredient::fluids), + HolderSetCodec.create(Registries.FLUID, BuiltInRegistries.FLUID.holderByNameCodec(), false).fieldOf("fluids").forGetter(DataComponentFluidIngredient::fluidSet), DataComponentPredicate.CODEC.fieldOf("components").forGetter(DataComponentFluidIngredient::components), Codec.BOOL.optionalFieldOf("strict", false).forGetter(DataComponentFluidIngredient::isStrict)) .apply(builder, DataComponentFluidIngredient::new)); @@ -70,8 +72,15 @@ public boolean test(FluidStack stack) { } } - public Stream generateStacks() { - return Stream.of(stacks); + public Stream> generateFluids() { + return fluids.stream(); + } + + @Override + public SlotDisplay display() { + return new SlotDisplay.Composite(Stream.of(stacks) + .map(stack -> (SlotDisplay) new FluidStackSlotDisplay(stack)) + .toList()); } @Override @@ -98,7 +107,7 @@ public boolean equals(Object obj) { && other.strict == this.strict; } - public HolderSet fluids() { + public HolderSet fluidSet() { return fluids; } diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/DifferenceFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/DifferenceFluidIngredient.java index fa09f3b170..9463baccf9 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/DifferenceFluidIngredient.java +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/DifferenceFluidIngredient.java @@ -9,6 +9,8 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.Objects; import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.crafting.DifferenceIngredient; import net.neoforged.neoforge.fluids.FluidStack; @@ -23,8 +25,8 @@ public final class DifferenceFluidIngredient extends FluidIngredient { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( builder -> builder .group( - FluidIngredient.CODEC_NON_EMPTY.fieldOf("base").forGetter(DifferenceFluidIngredient::base), - FluidIngredient.CODEC_NON_EMPTY.fieldOf("subtracted").forGetter(DifferenceFluidIngredient::subtracted)) + FluidIngredient.CODEC.fieldOf("base").forGetter(DifferenceFluidIngredient::base), + FluidIngredient.CODEC.fieldOf("subtracted").forGetter(DifferenceFluidIngredient::subtracted)) .apply(builder, DifferenceFluidIngredient::new)); private final FluidIngredient base; private final FluidIngredient subtracted; @@ -35,8 +37,8 @@ public DifferenceFluidIngredient(FluidIngredient base, FluidIngredient subtracte } @Override - public Stream generateStacks() { - return base.generateStacks().filter(subtracted.negate()); + public Stream> generateFluids() { + return base.fluids().stream().filter(e -> !subtracted().fluids().contains(e)); } @Override diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/EmptyFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/EmptyFluidIngredient.java deleted file mode 100644 index d8a958c7c8..0000000000 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/EmptyFluidIngredient.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.fluids.crafting; - -import com.mojang.serialization.MapCodec; -import java.util.stream.Stream; -import net.neoforged.neoforge.common.NeoForgeMod; -import net.neoforged.neoforge.fluids.FluidStack; - -/** - * Singleton that represents an empty fluid ingredient. - *

- * This is the only instance of an explicitly empty ingredient, - * and may be used as a fallback in FluidIngredient convenience methods - * (such as when trying to create an ingredient from an empty list). - * - * @see FluidIngredient#empty() - * @see FluidIngredient#isEmpty() - */ -public class EmptyFluidIngredient extends FluidIngredient { - public static final EmptyFluidIngredient INSTANCE = new EmptyFluidIngredient(); - - public static final MapCodec CODEC = MapCodec.unit(INSTANCE); - - private EmptyFluidIngredient() {} - - @Override - public boolean test(FluidStack fluidStack) { - return fluidStack.isEmpty(); - } - - @Override - protected Stream generateStacks() { - return Stream.empty(); - } - - @Override - public boolean isSimple() { - return true; - } - - @Override - public FluidIngredientType getType() { - return NeoForgeMod.EMPTY_FLUID_INGREDIENT_TYPE.get(); - } - - @Override - public int hashCode() { - return 0; - } - - @Override - public boolean equals(Object obj) { - return this == obj; - } -} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredient.java index 49f41aeb0a..c76edc65d9 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredient.java +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredient.java @@ -5,34 +5,31 @@ package net.neoforged.neoforge.fluids.crafting; -import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.MapCodec; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; import net.minecraft.core.Holder; -import net.minecraft.core.NonNullList; +import net.minecraft.core.HolderSet; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; -import net.minecraft.tags.TagKey; import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.display.SlotDisplay; import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.common.crafting.ICustomIngredient; -import net.neoforged.neoforge.common.util.NeoForgeExtraCodecs; import net.neoforged.neoforge.fluids.FluidStack; -import net.neoforged.neoforge.fluids.FluidStackLinkedSet; +import net.neoforged.neoforge.fluids.crafting.display.FluidSlotDisplay; import net.neoforged.neoforge.registries.NeoForgeRegistries; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** * This class serves as the fluid analogue of an item {@link Ingredient}, * that is, a representation of both a {@linkplain #test predicate} to test - * {@link FluidStack}s against, and a {@linkplain #getStacks list} of matching stacks + * {@link FluidStack}s against, and a {@linkplain #fluids list} of matching stacks * for e.g. display purposes. *

* The most common use for fluid ingredients is found in modded recipe inputs, @@ -42,99 +39,27 @@ * you may also want to take a look at {@link SizedFluidIngredient}! */ public abstract class FluidIngredient implements Predicate { - /** - * This is a codec that is used to represent basic "single fluid" or "tag" - * fluid ingredients directly, similar to {@link Ingredient.Value#CODEC}, - * except not using value subclasses and instead directly providing - * the corresponding {@link FluidIngredient}. - */ - private static final MapCodec SINGLE_OR_TAG_CODEC = singleOrTagCodec(); - - /** - * This is a codec that represents a single {@code FluidIngredient} in map form; - * either dispatched by type or falling back to {@link #SINGLE_OR_TAG_CODEC} - * if no type is specified. - * - * @see Ingredient#MAP_CODEC_NONEMPTY - */ - public static final MapCodec MAP_CODEC_NONEMPTY = makeMapCodec(); - private static final Codec MAP_CODEC_CODEC = MAP_CODEC_NONEMPTY.codec(); - - public static final Codec> LIST_CODEC = MAP_CODEC_CODEC.listOf(); - public static final Codec> LIST_CODEC_NON_EMPTY = LIST_CODEC.validate(list -> { - if (list.isEmpty()) { - return DataResult.error(() -> "Fluid ingredient cannot be empty, at least one item must be defined"); - } - return DataResult.success(list); - }); - - /** - * Full codec representing a fluid ingredient in all possible forms. - *

- * Allows for arrays of fluid ingredients to be read as a {@link CompoundFluidIngredient}, - * as well as for the {@code type} field to be left out in case of a single fluid or tag ingredient. - * - * @see #codec(boolean) - * @see #MAP_CODEC_NONEMPTY - */ - public static final Codec CODEC = codec(true); - /** - * Same as {@link #CODEC}, except not allowing for empty ingredients ({@code []}) - * to be specified. - * - * @see #codec(boolean) - */ - public static final Codec CODEC_NON_EMPTY = codec(false); + public static final Codec CODEC = FluidIngredientCodecs.codec(); - public static final StreamCodec STREAM_CODEC = new StreamCodec<>() { - private static final StreamCodec DISPATCH_CODEC = ByteBufCodecs.registry(NeoForgeRegistries.Keys.FLUID_INGREDIENT_TYPES) - .dispatch(FluidIngredient::getType, FluidIngredientType::streamCodec); + public static final StreamCodec STREAM_CODEC = FluidIngredientCodecs.streamCodec(); - private static final StreamCodec> FLUID_LIST_CODEC = FluidStack.STREAM_CODEC.apply( - ByteBufCodecs.collection(NonNullList::createWithCapacity)); - - @Override - public void encode(RegistryFriendlyByteBuf buf, FluidIngredient ingredient) { - if (ingredient.isSimple()) { - FLUID_LIST_CODEC.encode(buf, Arrays.asList(ingredient.getStacks())); - } else { - buf.writeVarInt(-1); - DISPATCH_CODEC.encode(buf, ingredient); - } - } - - @Override - public FluidIngredient decode(RegistryFriendlyByteBuf buf) { - var size = buf.readVarInt(); - if (size == -1) { - return DISPATCH_CODEC.decode(buf); - } - - return CompoundFluidIngredient.of( - Stream.generate(() -> FluidStack.STREAM_CODEC.decode(buf)) - .limit(size) - .map(FluidIngredient::single)); - } - }; + public static final StreamCodec> OPTIONAL_STREAM_CODEC = ByteBufCodecs.optional(STREAM_CODEC); @Nullable - private FluidStack[] stacks; + private List> fluids; /** - * Returns an array of fluid stacks that this ingredient accepts. - * The fluid stacks within the returned array must not be modified by the caller! - * {@return an array of fluid stacks this ingredient accepts} + * {@return a cached list of all Fluid holders that this ingredient accepts} + * This list is immutable and thus can and should not be modified by the caller! * - * @see #generateStacks() + * @see #generateFluids() */ - public final FluidStack[] getStacks() { - if (stacks == null) { - stacks = generateStacks() - .collect(Collectors.toCollection(FluidStackLinkedSet::createTypeAndComponentsSet)) - .toArray(FluidStack[]::new); + public final List> fluids() { + if (fluids == null) { + fluids = generateFluids().toList(); } - return stacks; + return fluids; } /** @@ -148,28 +73,48 @@ public final FluidStack[] getStacks() { public abstract boolean test(FluidStack fluidStack); /** - * Generates a stream of all fluid stacks this ingredient matches against. + * {@return a stream of fluids accepted by this ingredient} *

* For compatibility reasons, implementations should follow the same guidelines * as for custom item ingredients, i.e.: *

    - *
  • These stacks are generally used for display purposes, and need not be exhaustive or perfectly accurate.
  • + *
  • Returned fluids are generally used for display purposes, and need not be exhaustive or perfectly accurate, + * as ingredients may additionally filter by e.g. data component values.
  • *
  • An exception is ingredients that {@linkplain #isSimple() are simple}, - * for which it is important that the returned stacks correspond exactly to all the accepted {@link Fluid}s.
  • - *
  • At least one stack should always be returned, otherwise the ingredient may be considered {@linkplain #hasNoFluids() accidentally empty}.
  • - *
  • The ingredient should try to return at least one stack with each accepted {@link Fluid}. - * This allows mods that inspect the ingredient to figure out which stacks it might accept.
  • + * for which it is important that this stream corresponds exactly all fluids accepted by {@link #test(FluidStack)}! + *
  • At least one stack should always be returned, so that the ingredient is not considered empty. Empty ingredients may invalidate recipes!
  • *
* + *

Note: no caching needs to be done by the implementation, this is already handled by {@link #fluids}! + * * @return a stream of all fluid stacks this ingredient accepts. *

* Note: No guarantees are made as to the amount of the fluid, * as FluidIngredients are generally not meant to match by amount * and these stacks are mostly used for display. *

- * @see ICustomIngredient#stacks() + * @see ICustomIngredient#items() */ - protected abstract Stream generateStacks(); + @ApiStatus.OverrideOnly + protected abstract Stream> generateFluids(); + + /** + * {@return a slot display for this ingredient, used for display on the client-side} + * + * @implNote The default implementation just constructs a list of stacks from {@link #fluids()}. + * This is generally suitable for {@link #isSimple() simple} ingredients. + * Non-simple ingredients can either override this method to provide a more customized display, + * or let data pack writers use {@link CustomDisplayFluidIngredient} to override the display of an ingredient. + * + * @see Ingredient#display() + * @see FluidSlotDisplay + */ + public SlotDisplay display() { + return new SlotDisplay.Composite(fluids() + .stream() + .map(FluidIngredient::displayForSingleFluid) + .toList()); + } /** * Returns whether this fluid ingredient always requires {@linkplain #test direct stack testing}. @@ -186,47 +131,14 @@ public final FluidStack[] getStacks() { */ public abstract FluidIngredientType getType(); - /** - * Checks if this ingredient is explicitly empty, i.e. - * equal to {@link EmptyFluidIngredient#INSTANCE}. - *

Note: This does not return true for "accidentally empty" ingredients, - * including compound ingredients that are explicitly constructed with no children - * or intersection / difference ingredients that resolve to an empty set. - * - * @return {@code true} if this ingredient is {@link #empty()}, {@code false} otherwise - */ - public final boolean isEmpty() { - return this == empty(); - } - - /** - * Checks if this ingredient matches no fluids, i.e. if its - * list of {@linkplain #getStacks() matching fluids} is empty. - *

- * Note that this method explicitly resolves the ingredient; - * if this is not desired, you will need to check for emptiness another way! - * - * @return {@code true} if this ingredient matches no fluids, {@code false} otherwise - * @see #isEmpty() - */ - public final boolean hasNoFluids() { - return getStacks().length == 0; - } - @Override public abstract int hashCode(); @Override public abstract boolean equals(Object obj); - // empty - public static FluidIngredient empty() { - return EmptyFluidIngredient.INSTANCE; - } - - // convenience methods - public static FluidIngredient of() { - return empty(); + public static SlotDisplay displayForSingleFluid(Holder holder) { + return new FluidSlotDisplay(holder); } public static FluidIngredient of(FluidStack... fluids) { @@ -237,75 +149,11 @@ public static FluidIngredient of(Fluid... fluids) { return of(Arrays.stream(fluids)); } - private static FluidIngredient of(Stream fluids) { - return CompoundFluidIngredient.of(fluids.map(FluidIngredient::single)); + public static FluidIngredient of(Stream fluids) { + return of(HolderSet.direct(fluids.map(Fluid::builtInRegistryHolder).toList())); } - public static FluidIngredient single(FluidStack stack) { - return single(stack.getFluid()); - } - - public static FluidIngredient single(Fluid fluid) { - return single(fluid.builtInRegistryHolder()); - } - - public static FluidIngredient single(Holder holder) { - return new SingleFluidIngredient(holder); - } - - public static FluidIngredient tag(TagKey tag) { - return new TagFluidIngredient(tag); - } - - // codecs - private static MapCodec singleOrTagCodec() { - return NeoForgeExtraCodecs.xor( - SingleFluidIngredient.CODEC, - TagFluidIngredient.CODEC).xmap(either -> either.map(id -> id, id -> id), ingredient -> { - if (ingredient instanceof SingleFluidIngredient fluid) { - return Either.left(fluid); - } else if (ingredient instanceof TagFluidIngredient tag) { - return Either.right(tag); - } - throw new IllegalStateException("Basic fluid ingredient should be either a fluid or a tag!"); - }); - } - - private static MapCodec makeMapCodec() { - return NeoForgeExtraCodecs., FluidIngredient, FluidIngredient>dispatchMapOrElse( - NeoForgeRegistries.FLUID_INGREDIENT_TYPES.byNameCodec(), - FluidIngredient::getType, - FluidIngredientType::codec, - FluidIngredient.SINGLE_OR_TAG_CODEC).xmap(either -> either.map(id -> id, id -> id), ingredient -> { - // prefer serializing without a type field, if possible - if (ingredient instanceof SingleFluidIngredient || ingredient instanceof TagFluidIngredient) { - return Either.right(ingredient); - } - - return Either.left(ingredient); - }).validate(ingredient -> { - if (ingredient.isEmpty()) { - return DataResult.error(() -> "Cannot serialize empty fluid ingredient using the map codec"); - } - return DataResult.success(ingredient); - }); - } - - private static Codec codec(boolean allowEmpty) { - var listCodec = Codec.lazyInitialized(() -> allowEmpty ? LIST_CODEC : LIST_CODEC_NON_EMPTY); - return Codec.either(listCodec, MAP_CODEC_CODEC) - // [{...}, {...}] is turned into a CompoundFluidIngredient instance - .xmap(either -> either.map(CompoundFluidIngredient::of, i -> i), - ingredient -> { - // serialize CompoundFluidIngredient instances as an array over their children - if (ingredient instanceof CompoundFluidIngredient compound) { - return Either.left(compound.children()); - } else if (ingredient.isEmpty()) { - // serialize empty ingredients as [] - return Either.left(List.of()); - } - - return Either.right(ingredient); - }); + public static FluidIngredient of(HolderSet fluids) { + return new SimpleFluidIngredient(fluids); } } diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredientCodecs.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredientCodecs.java new file mode 100644 index 0000000000..07f3590739 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/FluidIngredientCodecs.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; +import java.util.stream.Stream; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class FluidIngredientCodecs { + static Codec codec() { + return Codec.xor( + NeoForgeRegistries.FLUID_INGREDIENT_TYPES.byNameCodec().dispatch("neoforge:ingredient_type", FluidIngredient::getType, FluidIngredientType::codec), + SimpleFluidIngredient.CODEC).xmap(either -> either.map(i -> i, i -> i), ingredient -> switch (ingredient) { + case SimpleFluidIngredient simple -> Either.right(simple); + default -> Either.left(ingredient); + }); + } + + static StreamCodec streamCodec() { + return ByteBufCodecs.registry(NeoForgeRegistries.Keys.FLUID_INGREDIENT_TYPES) + .dispatch(FluidIngredient::getType, FluidIngredientType::streamCodec); + } + + public static FluidIngredientType simpleType() { + MapCodec erroringMapCodec = new MapCodec<>() { + @Override + public Stream keys(DynamicOps dynamicOps) { + return Stream.empty(); + } + + @Override + public DataResult decode(DynamicOps ops, MapLike mapLike) { + return DataResult.error(() -> "Simple fluid ingredients cannot be decoded using map syntax!"); + } + + @Override + public RecordBuilder encode(SimpleFluidIngredient ingredient, DynamicOps ops, RecordBuilder builder) { + return builder.withErrorsFrom(DataResult.error(() -> "Simple fluid ingredients cannot be encoded using map syntax! Please use vanilla syntax (namespaced:item or #tag) instead!")); + } + }; + + return new FluidIngredientType<>(erroringMapCodec, SimpleFluidIngredient.CONTENTS_STREAM_CODEC); + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/IntersectionFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/IntersectionFluidIngredient.java index d53df462eb..d8e1729b9a 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/IntersectionFluidIngredient.java +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/IntersectionFluidIngredient.java @@ -11,8 +11,11 @@ import java.util.List; import java.util.Objects; import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.FluidType; /** * FluidIngredient that matches if all child ingredients match @@ -28,7 +31,7 @@ public IntersectionFluidIngredient(List children) { public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( builder -> builder .group( - FluidIngredient.LIST_CODEC_NON_EMPTY.fieldOf("children").forGetter(IntersectionFluidIngredient::children)) + FluidIngredient.CODEC.listOf(1, Integer.MAX_VALUE).fieldOf("children").forGetter(IntersectionFluidIngredient::children)) .apply(builder, IntersectionFluidIngredient::new)); private final List children; @@ -58,10 +61,10 @@ public boolean test(FluidStack stack) { } @Override - public Stream generateStacks() { + public Stream> generateFluids() { return children.stream() - .flatMap(FluidIngredient::generateStacks) - .filter(this); + .flatMap(child -> child.fluids().stream()) + .filter(fluid -> test(new FluidStack(fluid, FluidType.BUCKET_VOLUME))); } @Override diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/SimpleFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/SimpleFluidIngredient.java new file mode 100644 index 0000000000..bb1f6d0829 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/SimpleFluidIngredient.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting; + +import com.mojang.serialization.Codec; +import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.HolderSetCodec; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.common.NeoForgeMod; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.crafting.display.FluidTagSlotDisplay; + +/** + * Fluid ingredient that matches the fluids specified by the given {@link HolderSet}. + * Most commonly, this will either be a list of fluids or a fluid tag. + *

+ * Unlike with ingredients, this is technically an explicit "type" of fluid ingredient, + * though in JSON, it is still written without a type field, see {@link FluidIngredientCodecs#codec()} + */ +public class SimpleFluidIngredient extends FluidIngredient { + private static final Codec> HOLDER_SET_NO_EMPTY_FLUID = HolderSetCodec.create( + Registries.FLUID, FluidStack.FLUID_NON_EMPTY_CODEC, false); + + static final Codec CODEC = ExtraCodecs.nonEmptyHolderSet(HOLDER_SET_NO_EMPTY_FLUID) + .xmap(SimpleFluidIngredient::new, SimpleFluidIngredient::fluidSet); + + static final StreamCodec CONTENTS_STREAM_CODEC = ByteBufCodecs.holderSet(Registries.FLUID) + .map(SimpleFluidIngredient::new, SimpleFluidIngredient::fluidSet); + + private final HolderSet values; + + public SimpleFluidIngredient(HolderSet values) { + values.unwrap().ifRight(list -> { + if (list.isEmpty()) { + throw new UnsupportedOperationException("Fluid ingredients can't be empty!"); + } else if (list.contains(Fluids.EMPTY.builtInRegistryHolder())) { + throw new UnsupportedOperationException("Fluid ingredients can't contain the empty fluid"); + } + }); + this.values = values; + } + + @Override + public boolean test(FluidStack fluidStack) { + return values.contains(fluidStack.getFluidHolder()); + } + + @Override + protected Stream> generateFluids() { + return values.stream(); + } + + @Override + public boolean isSimple() { + return true; + } + + @Override + public FluidIngredientType getType() { + return NeoForgeMod.SIMPLE_FLUID_INGREDIENT_TYPE.get(); + } + + @Override + public SlotDisplay display() { + return values.unwrapKey() + .map(FluidTagSlotDisplay::new) + .orElseGet(super::display); + } + + @Override + public int hashCode() { + return this.fluidSet().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + return obj instanceof SimpleFluidIngredient other && other.fluidSet().equals(this.fluidSet()); + } + + public HolderSet fluidSet() { + return values; + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/SingleFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/SingleFluidIngredient.java deleted file mode 100644 index e2bb3ae250..0000000000 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/SingleFluidIngredient.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.fluids.crafting; - -import com.mojang.serialization.MapCodec; -import java.util.stream.Stream; -import net.minecraft.core.Holder; -import net.minecraft.world.level.material.Fluid; -import net.minecraft.world.level.material.Fluids; -import net.neoforged.neoforge.common.NeoForgeMod; -import net.neoforged.neoforge.fluids.FluidStack; -import net.neoforged.neoforge.fluids.FluidType; - -/** - * Fluid ingredient that only matches the fluid of the given stack. - *

- * Unlike with ingredients, this is an explicit "type" of fluid ingredient, - * though it may still be written without a type field, see {@link FluidIngredient#MAP_CODEC_NONEMPTY} - */ -public class SingleFluidIngredient extends FluidIngredient { - public static final MapCodec CODEC = FluidStack.FLUID_NON_EMPTY_CODEC - .xmap(SingleFluidIngredient::new, SingleFluidIngredient::fluid).fieldOf("fluid"); - - private final Holder fluid; - - public SingleFluidIngredient(Holder fluid) { - if (fluid.is(Fluids.EMPTY.builtInRegistryHolder())) { - throw new IllegalStateException("SingleFluidIngredient must not be constructed with minecraft:empty, use FluidIngredient.empty() instead!"); - } - this.fluid = fluid; - } - - @Override - public boolean test(FluidStack fluidStack) { - return fluidStack.is(fluid); - } - - @Override - protected Stream generateStacks() { - return Stream.of(new FluidStack(fluid, FluidType.BUCKET_VOLUME)); - } - - @Override - public boolean isSimple() { - return true; - } - - @Override - public FluidIngredientType getType() { - return NeoForgeMod.SINGLE_FLUID_INGREDIENT_TYPE.get(); - } - - @Override - public int hashCode() { - return this.fluid().value().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - return obj instanceof SingleFluidIngredient other && other.fluid.is(this.fluid); - } - - public Holder fluid() { - return fluid; - } -} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/SizedFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/SizedFluidIngredient.java index daadd12439..a87c35c690 100644 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/SizedFluidIngredient.java +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/SizedFluidIngredient.java @@ -8,17 +8,14 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.Objects; -import java.util.stream.Stream; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; -import net.minecraft.tags.TagKey; import net.minecraft.util.ExtraCodecs; import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.common.util.NeoForgeExtraCodecs; import net.neoforged.neoforge.fluids.FluidStack; import net.neoforged.neoforge.fluids.FluidType; -import org.jetbrains.annotations.Nullable; /** * Standard implementation for a FluidIngredient with an amount. @@ -31,55 +28,35 @@ */ public final class SizedFluidIngredient { /** - * The "flat" codec for {@link SizedFluidIngredient}. + * The codec for {@link SizedFluidIngredient}. * - *

The amount is serialized inline with the rest of the ingredient, for example: + *

With this codec, the amount is serialized separately from the ingredient itself, for example: * *

{@code
      * {
-     *     "fluid": "minecraft:water",
-     *     "amount": 250
-     * }
-     * }
- * - *

- *

- * Compound fluid ingredients are always serialized using the map codec, i.e. - * - *

{@code
-     * {
-     *     "type": "neoforge:compound",
-     *     "ingredients": [
-     *         { "fluid": "minecraft:water" },
-     *         { "fluid": "minecraft:milk" }
-     *     ],
-     *     "amount": 500
+     *     "ingredient": "minecraft:lava",
+     *     "amount": 1000
      * }
      * }
* *

- */ - public static final Codec FLAT_CODEC = RecordCodecBuilder.create(instance -> instance.group( - FluidIngredient.MAP_CODEC_NONEMPTY.forGetter(SizedFluidIngredient::ingredient), - NeoForgeExtraCodecs.optionalFieldAlwaysWrite(ExtraCodecs.POSITIVE_INT, "amount", FluidType.BUCKET_VOLUME).forGetter(SizedFluidIngredient::amount)) - .apply(instance, SizedFluidIngredient::new)); - - /** - * The "nested" codec for {@link SizedFluidIngredient}. - * - *

With this codec, the amount is always serialized separately from the ingredient itself, for example: + * or for custom ingredients: * *

{@code
      * {
      *     "ingredient": {
-     *         "fluid": "minecraft:lava"
+     *         "neoforge:type": "neoforge:intersection",
+     *         "children": [
+     *              "#example:tag1",
+     *              "#example:tag2"
+     *         ],
      *     },
-     *     "amount": 1000
+     *     "amount": 4711
      * }
      * }
*/ - public static final Codec NESTED_CODEC = RecordCodecBuilder.create(instance -> instance.group( - FluidIngredient.CODEC_NON_EMPTY.fieldOf("ingredient").forGetter(SizedFluidIngredient::ingredient), + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + FluidIngredient.CODEC.fieldOf("ingredient").forGetter(SizedFluidIngredient::ingredient), NeoForgeExtraCodecs.optionalFieldAlwaysWrite(ExtraCodecs.POSITIVE_INT, "amount", FluidType.BUCKET_VOLUME).forGetter(SizedFluidIngredient::amount)) .apply(instance, SizedFluidIngredient::new)); @@ -94,26 +71,9 @@ public static SizedFluidIngredient of(Fluid fluid, int amount) { return new SizedFluidIngredient(FluidIngredient.of(fluid), amount); } - /** - * Helper method to create a simple sized ingredient that matches the given fluid stack - */ - public static SizedFluidIngredient of(FluidStack stack) { - return new SizedFluidIngredient(FluidIngredient.single(stack), stack.getAmount()); - } - - /** - * Helper method to create a simple sized ingredient that matches fluids in a tag. - */ - public static SizedFluidIngredient of(TagKey tag, int amount) { - return new SizedFluidIngredient(FluidIngredient.tag(tag), amount); - } - private final FluidIngredient ingredient; private final int amount; - @Nullable - private FluidStack[] cachedStacks; - public SizedFluidIngredient(FluidIngredient ingredient, int amount) { if (amount <= 0) { throw new IllegalArgumentException("Size must be positive"); @@ -139,20 +99,6 @@ public boolean test(FluidStack stack) { return ingredient.test(stack) && stack.getAmount() >= amount; } - /** - * Returns a list of the stacks from this {@link #ingredient}, with an updated {@link #amount}. - * - * @implNote the array is cached and should not be modified, just like {@link FluidIngredient#getStacks()}}. - */ - public FluidStack[] getFluids() { - if (cachedStacks == null) { - cachedStacks = Stream.of(ingredient.getStacks()) - .map(s -> s.copyWithAmount(amount)) - .toArray(FluidStack[]::new); - } - return cachedStacks; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/TagFluidIngredient.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/TagFluidIngredient.java deleted file mode 100644 index 10bbebde2b..0000000000 --- a/src/main/java/net/neoforged/neoforge/fluids/crafting/TagFluidIngredient.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.fluids.crafting; - -import com.mojang.serialization.MapCodec; -import java.util.stream.Stream; -import net.minecraft.core.HolderSet; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.tags.TagKey; -import net.minecraft.world.level.material.Fluid; -import net.neoforged.neoforge.common.NeoForgeMod; -import net.neoforged.neoforge.fluids.FluidStack; -import net.neoforged.neoforge.fluids.FluidType; - -/** - * Fluid ingredient that matches all fluids within the given tag. - *

- * Unlike with ingredients, this is an explicit "type" of fluid ingredient, - * though it may still be written without a type field, see {@link FluidIngredient#MAP_CODEC_NONEMPTY} - */ -public class TagFluidIngredient extends FluidIngredient { - public static final MapCodec CODEC = TagKey.codec(Registries.FLUID) - .xmap(TagFluidIngredient::new, TagFluidIngredient::tag).fieldOf("tag"); - - private final TagKey tag; - - public TagFluidIngredient(TagKey tag) { - this.tag = tag; - } - - @Override - public boolean test(FluidStack fluidStack) { - return fluidStack.is(tag); - } - - @Override - protected Stream generateStacks() { - return BuiltInRegistries.FLUID.get(tag) - .stream() - .flatMap(HolderSet::stream) - .map(fluid -> new FluidStack(fluid, FluidType.BUCKET_VOLUME)); - } - - @Override - public boolean isSimple() { - return true; - } - - @Override - public FluidIngredientType getType() { - return NeoForgeMod.TAG_FLUID_INGREDIENT_TYPE.get(); - } - - @Override - public int hashCode() { - return tag.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - return obj instanceof TagFluidIngredient tag && tag.tag.equals(this.tag); - } - - public TagKey tag() { - return tag; - } -} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidSlotDisplay.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidSlotDisplay.java new file mode 100644 index 0000000000..bbb9de9816 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidSlotDisplay.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting.display; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.stream.Stream; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.RegistryFixedCodec; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.crafting.display.DisplayContentsFactory; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.level.material.Fluid; +import net.neoforged.neoforge.common.NeoForgeMod; + +/** + * Slot display for a single fluid holder. + *

+ * Note that information on amount and data of the displayed fluid stack depends on the provided factory! + * + * @param fluid The fluid to be displayed. + */ +public record FluidSlotDisplay(Holder fluid) implements SlotDisplay { + public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(RegistryFixedCodec.create(Registries.FLUID).fieldOf("fluid").forGetter(FluidSlotDisplay::fluid)) + .apply(instance, FluidSlotDisplay::new)); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.holderRegistry(Registries.FLUID), FluidSlotDisplay::fluid, FluidSlotDisplay::new); + + @Override + public Type type() { + return NeoForgeMod.FLUID_SLOT_DISPLAY.get(); + } + + @Override + public Stream resolve(ContextMap context, DisplayContentsFactory factory) { + return switch (factory) { + case ForFluidStacks fluids -> Stream.of(fluids.forStack(fluid)); + default -> Stream.empty(); + }; + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidStackContentsFactory.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidStackContentsFactory.java new file mode 100644 index 0000000000..4f4537285b --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidStackContentsFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting.display; + +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.fluids.FluidStack; + +/** + * Base fluid stack contents factory: directly returns the stacks. + * + *

Fluid equivalent of {@link SlotDisplay.ItemStackContentsFactory}. + */ +public class FluidStackContentsFactory implements ForFluidStacks { + public static final FluidStackContentsFactory INSTANCE = new FluidStackContentsFactory(); + + @Override + public FluidStack forStack(FluidStack fluid) { + return fluid; + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidStackSlotDisplay.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidStackSlotDisplay.java new file mode 100644 index 0000000000..7bc734b09f --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidStackSlotDisplay.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting.display; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.stream.Stream; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.crafting.display.DisplayContentsFactory; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.neoforged.neoforge.common.NeoForgeMod; +import net.neoforged.neoforge.fluids.FluidStack; + +/** + * Slot display for a given fluid stack, including fluid amount and data components. + * + * @param stack The fluid stack to be displayed. + */ +public record FluidStackSlotDisplay(FluidStack stack) implements SlotDisplay { + public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(FluidStack.CODEC.fieldOf("fluid").forGetter(FluidStackSlotDisplay::stack)) + .apply(instance, FluidStackSlotDisplay::new)); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + FluidStack.STREAM_CODEC, FluidStackSlotDisplay::stack, FluidStackSlotDisplay::new); + + @Override + public SlotDisplay.Type type() { + return NeoForgeMod.FLUID_STACK_SLOT_DISPLAY.get(); + } + + @Override + public Stream resolve(ContextMap context, DisplayContentsFactory factory) { + return switch (factory) { + case ForFluidStacks fluids -> Stream.of(fluids.forStack(stack)); + default -> Stream.empty(); + }; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else { + return other instanceof FluidStackSlotDisplay fluidStackDisplay + && FluidStack.matches(this.stack, fluidStackDisplay.stack); + } + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidTagSlotDisplay.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidTagSlotDisplay.java new file mode 100644 index 0000000000..7e2f258661 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/FluidTagSlotDisplay.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting.display; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.stream.Stream; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.tags.TagKey; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.crafting.display.DisplayContentsFactory; +import net.minecraft.world.item.crafting.display.SlotDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import net.minecraft.world.level.material.Fluid; +import net.neoforged.neoforge.common.NeoForgeMod; + +/** + * Slot display that shows all fluids in a given tag. + * + *

Note that information on amount and data of the displayed fluid stacks depends on the provided factory! + * + * @param tag The tag to be displayed. + */ +public record FluidTagSlotDisplay(TagKey tag) implements SlotDisplay { + public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec( + p_379704_ -> p_379704_.group(TagKey.codec(Registries.FLUID).fieldOf("tag").forGetter(FluidTagSlotDisplay::tag)) + .apply(p_379704_, FluidTagSlotDisplay::new)); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + TagKey.streamCodec(Registries.FLUID), FluidTagSlotDisplay::tag, FluidTagSlotDisplay::new); + + @Override + public SlotDisplay.Type type() { + return NeoForgeMod.FLUID_TAG_SLOT_DISPLAY.get(); + } + + @Override + public Stream resolve(ContextMap context, DisplayContentsFactory factory) { + if (factory instanceof ForFluidStacks fluids) { + HolderLookup.Provider registries = context.getOptional(SlotDisplayContext.REGISTRIES); + if (registries != null) { + return registries.lookupOrThrow(Registries.FLUID) + .get(this.tag) + .map(p_380858_ -> p_380858_.stream().map(fluids::forStack)) + .stream() + .flatMap(p_380859_ -> p_380859_); + } + } + + return Stream.empty(); + } +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/display/ForFluidStacks.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/ForFluidStacks.java new file mode 100644 index 0000000000..7a625d9d82 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/ForFluidStacks.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.fluids.crafting.display; + +import net.minecraft.core.Holder; +import net.minecraft.world.item.crafting.display.DisplayContentsFactory; +import net.minecraft.world.level.material.Fluid; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.FluidType; + +public interface ForFluidStacks extends DisplayContentsFactory { + /** + * {@return display data for the given fluid holder} + * + * @param fluid Fluid holder to display. + */ + default T forStack(Holder fluid) { + return this.forStack(new FluidStack(fluid, FluidType.BUCKET_VOLUME)); + } + + /** + * {@return display data for the given fluid} + * + * @param fluid Fluid to display. + */ + default T forStack(Fluid fluid) { + return this.forStack(fluid.builtInRegistryHolder()); + } + + /** + * {@return display data for the given fluid stack} + * + * @param fluid Fluid stack to display + */ + T forStack(FluidStack fluid); +} diff --git a/src/main/java/net/neoforged/neoforge/fluids/crafting/display/package-info.java b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/package-info.java new file mode 100644 index 0000000000..2a27349909 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/fluids/crafting/display/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package net.neoforged.neoforge.fluids.crafting.display; + +import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; diff --git a/tests/src/junit/java/net/neoforged/neoforge/unittest/FluidIngredientTests.java b/tests/src/junit/java/net/neoforged/neoforge/unittest/FluidIngredientTests.java new file mode 100644 index 0000000000..ab8d78ab50 --- /dev/null +++ b/tests/src/junit/java/net/neoforged/neoforge/unittest/FluidIngredientTests.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.unittest; + +import java.util.List; +import java.util.stream.Stream; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.Rarity; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.fluids.FluidStack; +import net.neoforged.neoforge.fluids.crafting.CompoundFluidIngredient; +import net.neoforged.neoforge.fluids.crafting.DataComponentFluidIngredient; +import net.neoforged.neoforge.fluids.crafting.FluidIngredient; +import net.neoforged.testframework.junit.EphemeralTestServerProvider; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@ExtendWith(EphemeralTestServerProvider.class) +public class FluidIngredientTests { + @Test + void emptyIngredientFails(MinecraftServer server) { + Assertions.assertThatThrownBy(() -> FluidIngredient.of(Stream.empty())) + .withFailMessage("Empty fluid ingredient should not have been able to be constructed!") + .isInstanceOf(UnsupportedOperationException.class); + Assertions.assertThatThrownBy(() -> FluidIngredient.of(Fluids.WATER, Fluids.LAVA, Fluids.EMPTY)) + .withFailMessage("SimpleFluidIngredient should not have been able to be constructed with empty fluid!") + .isInstanceOf(UnsupportedOperationException.class); + + Assertions.assertThatThrownBy(() -> new CompoundFluidIngredient(List.of())) + .withFailMessage("Empty compound fluid ingredient should not have been able to be constructed!") + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource({ "false", "true" }) + void fluidIngredientComponentMatchingWorks(boolean strict, MinecraftServer server) { + var ingredient = DataComponentFluidIngredient.of(strict, DataComponents.RARITY, Rarity.EPIC, Fluids.WATER); + var stack = new FluidStack(Fluids.WATER, 1000); + + Assertions.assertThat(ingredient.test(stack)) + .withFailMessage("Fluid without custom data should not match DataComponentFluidIngredient!") + .isFalse(); + + stack.applyComponents(DataComponentPatch.builder() + .set(DataComponents.RARITY, Rarity.UNCOMMON) + .build()); + + Assertions.assertThat(ingredient.test(stack)) + .withFailMessage("Fluid with incorrect data should not match DataComponentFluidIngredient!") + .isFalse(); + + stack.applyComponents(DataComponentPatch.builder() + .set(DataComponents.RARITY, Rarity.EPIC) + .build()); + + Assertions.assertThat(ingredient.test(stack)) + .withFailMessage("Fluid with correct data should match DataComponentFluidIngredient!") + .isTrue(); + + var data = CustomData.EMPTY.update(tag -> tag.putFloat("abcd", 0.5F)); + stack.set(DataComponents.CUSTOM_DATA, data); + + Assertions.assertThat(ingredient.test(stack)) + .withFailMessage("Strictness check failed for DataComponentFluidIngredient with strict: " + strict) + .isEqualTo(!strict); + } + + void singleFluidIngredientIgnoresSizeAndData(MinecraftServer server) { + var ingredient = FluidIngredient.of(Fluids.WATER); + + Assertions.assertThat(ingredient.test(new FluidStack(Fluids.WATER, 1234))) + .withFailMessage("Single fluid ingredient should match regardless of fluid amount!") + .isTrue(); + + Assertions.assertThat(ingredient.test(new FluidStack(Fluids.WATER.builtInRegistryHolder(), 1234, DataComponentPatch.builder().set(DataComponents.RARITY, Rarity.COMMON).build()))) + .withFailMessage("Single fluid ingredient should match regardless of fluid data!") + .isTrue(); + } +} diff --git a/tests/src/junit/java/net/neoforged/neoforge/unittest/IngredientTests.java b/tests/src/junit/java/net/neoforged/neoforge/unittest/IngredientTests.java index fbe3bc9794..164ab1a686 100644 --- a/tests/src/junit/java/net/neoforged/neoforge/unittest/IngredientTests.java +++ b/tests/src/junit/java/net/neoforged/neoforge/unittest/IngredientTests.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.stream.Stream; +import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.Registries; import net.minecraft.server.MinecraftServer; @@ -30,12 +31,16 @@ @ExtendWith(EphemeralTestServerProvider.class) public class IngredientTests { + private static List ingredientItemsAsStacks(Ingredient ingredient) { + return ingredient.items().stream().map(i -> i.value().getDefaultInstance()).toList(); + } + @ParameterizedTest @MethodSource("provideIngredientMatrix") void testCompoundIngredient(Ingredient a, Ingredient b) { final var ingredient = CompoundIngredient.of(a, b); - Assertions.assertThat(a.stacks()).allMatch(ingredient, "first ingredient"); - Assertions.assertThat(b.stacks()).allMatch(ingredient, "second ingredient"); + Assertions.assertThat(ingredientItemsAsStacks(a)).allMatch(ingredient, "first ingredient"); + Assertions.assertThat(ingredientItemsAsStacks(b)).allMatch(ingredient, "second ingredient"); } @Test @@ -44,9 +49,9 @@ void testDifferenceIngredients(MinecraftServer server) { final var acacia = Ingredient.of(Items.ACACIA_LOG); final var ingredient = DifferenceIngredient.of(logs, acacia); - Assertions.assertThat(logs.stacks()) - .filteredOn(i -> !acacia.test(i)) - .containsExactlyInAnyOrder(ingredient.stacks().toArray(ItemStack[]::new)); + Assertions.assertThat(logs.items()) + .filteredOn(i -> !acacia.test(i.value().getDefaultInstance())) + .containsExactlyInAnyOrder(ingredient.items().toArray(Holder[]::new)); } @Test @@ -54,7 +59,7 @@ void testIntersectionIngredient(MinecraftServer server) { final var second = Ingredient.of(Items.BIRCH_LOG, Items.SPRUCE_LOG, Items.DISPENSER); final var ingredient = IntersectionIngredient.of(Ingredient.of(server.registryAccess().lookupOrThrow(Registries.ITEM).getOrThrow(ItemTags.LOGS)), second); - Assertions.assertThat(ingredient.stacks().stream().map(ItemStack::getItem).distinct()) + Assertions.assertThat(ingredient.items().stream().map(Holder::value).distinct()) .containsExactlyInAnyOrder(Items.BIRCH_LOG, Items.SPRUCE_LOG); } diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/fluid/crafting/FluidIngredientTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/fluid/crafting/FluidIngredientTests.java index 974ed9377e..915c2d82b4 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/fluid/crafting/FluidIngredientTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/fluid/crafting/FluidIngredientTests.java @@ -5,215 +5,68 @@ package net.neoforged.neoforge.debug.fluid.crafting; -import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.mojang.serialization.JsonOps; -import java.util.List; -import net.minecraft.core.component.DataComponentMap; -import net.minecraft.core.component.DataComponentPatch; -import net.minecraft.core.component.DataComponents; +import net.minecraft.Util; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTestHelper; -import net.minecraft.world.item.Rarity; -import net.minecraft.world.item.component.CustomData; -import net.minecraft.world.level.material.Fluid; import net.minecraft.world.level.material.Fluids; +import net.neoforged.neoforge.common.NeoForgeMod; import net.neoforged.neoforge.common.Tags; import net.neoforged.neoforge.fluids.FluidStack; -import net.neoforged.neoforge.fluids.crafting.CompoundFluidIngredient; -import net.neoforged.neoforge.fluids.crafting.DataComponentFluidIngredient; -import net.neoforged.neoforge.fluids.crafting.DifferenceFluidIngredient; import net.neoforged.neoforge.fluids.crafting.FluidIngredient; -import net.neoforged.neoforge.fluids.crafting.IntersectionFluidIngredient; import net.neoforged.neoforge.fluids.crafting.SizedFluidIngredient; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.gametest.EmptyTemplate; import net.neoforged.testframework.gametest.ExtendedGameTestHelper; +// TODO(max): move rest of these to unit tests! @ForEachTest(groups = { "fluid.crafting", "crafting.ingredient" }) public class FluidIngredientTests { // serialization tests - @GameTest - @EmptyTemplate - @TestHolder(description = "Serialization tests for empty fluid ingredients") - static void emptyFluidIngredientSerialization(ExtendedGameTestHelper helper) { - // analogous to IngredientTests - // Make sure that empty ingredients serialize to [] - var emptyResult = FluidIngredient.CODEC.encodeStart(JsonOps.INSTANCE, FluidIngredient.empty()); - var emptyJson = emptyResult.resultOrPartial(error -> helper.fail("Failed to serialize empty fluid ingredient: " + error)).orElseThrow(); - helper.assertValueEqual("[]", emptyJson.toString(), "empty fluid ingredient"); - - // Make sure that [] deserializes to an empty ingredient - var result = FluidIngredient.CODEC.parse(JsonOps.INSTANCE, new JsonArray()); - var ingredient = result.resultOrPartial(error -> helper.fail("Failed to deserialize empty fluid ingredient: " + error)).orElseThrow(); - helper.assertTrue(ingredient.isEmpty(), "empty fluid ingredient should return true from isEmpty()"); - helper.assertTrue(ingredient.hasNoFluids(), "empty fluid ingredient should return true from hasNoFluids()"); - helper.assertValueEqual(FluidIngredient.empty(), ingredient, "empty fluid ingredient"); - helper.assertTrue(FluidIngredient.empty() == ingredient, "Reference equality with FluidIngredient.empty()"); - - helper.succeed(); - } - @GameTest @EmptyTemplate @TestHolder(description = "Serialization tests for single fluid and tag ingredients") static void basicFluidIngredientSerialization(ExtendedGameTestHelper helper) { + var registryAccess = helper.getLevel().registryAccess(); + var ops = registryAccess.createSerializationContext(JsonOps.INSTANCE); + var singleFluid = FluidIngredient.of(Fluids.WATER); - var tagFluid = FluidIngredient.tag(Tags.Fluids.WATER); + var tagFluid = FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER)); // tests that the jsons for single and tag fluids do not contain a "type" field - var singleResult = FluidIngredient.CODEC.encodeStart(JsonOps.INSTANCE, singleFluid); + var singleResult = FluidIngredient.CODEC.encodeStart(ops, singleFluid); var singleJson = singleResult.resultOrPartial(error -> helper.fail("Failed to serialize single fluid ingredient: " + error)).orElseThrow(); - var tagResult = FluidIngredient.CODEC.encodeStart(JsonOps.INSTANCE, tagFluid); + var tagResult = FluidIngredient.CODEC.encodeStart(ops, tagFluid); var tagJson = tagResult.resultOrPartial(error -> helper.fail("Failed to serialize tag fluid ingredient: " + error)).orElseThrow(); - helper.assertFalse(singleJson.getAsJsonObject().has("type"), "single fluid ingredient should serialize without a 'type' field"); - helper.assertFalse(tagJson.getAsJsonObject().has("type"), "tag fluid ingredient should serialize without a 'type' field"); + helper.assertFalse(singleJson.isJsonObject(), "single fluid ingredient should not serialize as nested object!"); + helper.assertFalse(tagJson.isJsonObject(), "tag fluid ingredient should not serialize as nested object!"); - helper.assertValueEqual(singleJson.toString(), "{\"fluid\":\"minecraft:water\"}", "serialized single fluid ingredient to match expected format!"); - helper.assertValueEqual(tagJson.toString(), "{\"tag\":\"c:water\"}", "serialized tag fluid ingredient to match expected format!"); + helper.assertValueEqual(singleJson.getAsString(), Fluids.WATER.builtInRegistryHolder().getRegisteredName(), "serialized single fluid ingredient to match HolderSet element format!"); + helper.assertValueEqual(tagJson.getAsString(), "#" + Tags.Fluids.WATER.location(), "serialized tag fluid ingredient to match HolderSet tag format!"); // tests that deserializing simple ingredients is reproducible and produces the desired ingredients - var singleTwo = FluidIngredient.CODEC.parse(JsonOps.INSTANCE, singleJson) + var singleTwo = FluidIngredient.CODEC.parse(ops, singleJson) .resultOrPartial(error -> helper.fail("Failed to deserialize single fluid ingredient from JSON: " + error)) .orElseThrow(); helper.assertValueEqual(singleFluid, singleTwo, "single fluid ingredient to be the same after being serialized and deserialized!"); - var tagTwo = FluidIngredient.CODEC.parse(JsonOps.INSTANCE, tagJson) + var tagTwo = FluidIngredient.CODEC.parse(ops, tagJson) .resultOrPartial(error -> helper.fail("Failed to deserialize single fluid ingredient from JSON: " + error)) .orElseThrow(); helper.assertValueEqual(tagFluid, tagTwo, "tag fluid ingredient to be the same after being serialized and deserialized!"); - helper.succeed(); - } - - @GameTest - @EmptyTemplate - @TestHolder(description = "Tests that custom ingredients are not empty") - static void testFluidIngredientEmptiness(final GameTestHelper helper) { - FluidIngredient compoundIngredient = CompoundFluidIngredient.of(FluidIngredient.of(Fluids.WATER), FluidIngredient.tag(Tags.Fluids.LAVA)); - helper.assertFalse(compoundIngredient.isEmpty(), "CompoundFluidIngredient should not be empty"); - FluidIngredient dataComponentIngredient = DataComponentFluidIngredient.of(false, DataComponentMap.EMPTY, Fluids.WATER); - helper.assertFalse(dataComponentIngredient.isEmpty(), "DataComponentFluidIngredient should not be empty"); - FluidIngredient differenceIngredient = DifferenceFluidIngredient.of(FluidIngredient.tag(Tags.Fluids.MILK), FluidIngredient.of(Fluids.LAVA)); - helper.assertFalse(differenceIngredient.isEmpty(), "DifferenceFluidIngredient should not be empty"); - FluidIngredient intersectionIngredient = IntersectionFluidIngredient.of(FluidIngredient.of(Fluids.WATER, Fluids.LAVA), FluidIngredient.of(Fluids.WATER)); - helper.assertFalse(intersectionIngredient.isEmpty(), "IntersectionFluidIngredient should not be empty"); - FluidIngredient emptyIngredient = FluidIngredient.empty(); - helper.assertTrue(emptyIngredient.isEmpty(), "FluidIngredient.empty() should be empty!"); - - helper.succeed(); - } - - @GameTest - @EmptyTemplate - @TestHolder(description = "Tests that custom ingredients correctly report hasNoFluids") - static void customFluidIngredientsHasNoFluids(final GameTestHelper helper) { - // these can all end up accidentally empty one way or another - FluidIngredient dataComponentIngredient = DataComponentFluidIngredient.of(false, DataComponentMap.EMPTY, new Fluid[0]); - helper.assertFalse(dataComponentIngredient.isEmpty(), "DataComponentFluidIngredient instance should not be empty"); - helper.assertTrue(dataComponentIngredient.hasNoFluids(), "DataComponentFluidIngredient with no matching fluids should return true on hasNoFluids()"); - FluidIngredient differenceIngredient = DifferenceFluidIngredient.of(FluidIngredient.tag(Tags.Fluids.WATER), FluidIngredient.tag(Tags.Fluids.WATER)); - helper.assertFalse(differenceIngredient.isEmpty(), "DifferenceFluidIngredient instance should not be empty"); - helper.assertTrue(differenceIngredient.hasNoFluids(), "DifferenceFluidIngredient with empty difference should return true on hasNoFluids()"); - FluidIngredient intersectionIngredient = IntersectionFluidIngredient.of(FluidIngredient.of(Fluids.LAVA), FluidIngredient.of(Fluids.WATER)); - helper.assertFalse(intersectionIngredient.isEmpty(), "IntersectionFluidIngredient instance should not be empty"); - helper.assertTrue(intersectionIngredient.hasNoFluids(), "IntersectionFluidIngredient with empty intersection should return true on hasNoFluids()"); - - // these classes have checks in place to make sure they aren't populated with empty values - var emptyCompoundFailed = false; - try { - FluidIngredient compoundIngredient = new CompoundFluidIngredient(List.of()); - } catch (Exception ignored) { - emptyCompoundFailed = true; - } - helper.assertTrue(emptyCompoundFailed, "Empty CompoundFluidIngredient should not have been able to be constructed!"); - - var emptySingleFailed = false; - try { - FluidIngredient compoundIngredient = FluidIngredient.single(Fluids.EMPTY); - } catch (Exception ignored) { - emptySingleFailed = true; - } - helper.assertTrue(emptySingleFailed, "Empty SingleFluidIngredient should not have been able to be constructed!"); - - helper.assertValueEqual(CompoundFluidIngredient.of(new FluidIngredient[0]), FluidIngredient.empty(), "calling CompoundFluidIngredient.of with no children to yield FluidIngredient.empty()"); - - helper.succeed(); - } - - @GameTest - @EmptyTemplate - @TestHolder(description = "Tests that partial data matches work correctly on fluid ingredients") - static void fluidIngredientDataPartialMatchWorks(final GameTestHelper helper) { - var ingredient = DataComponentFluidIngredient.of(false, DataComponents.RARITY, Rarity.EPIC, Fluids.WATER); - var stack = new FluidStack(Fluids.WATER, 1000); - - helper.assertFalse(ingredient.test(stack), "Fluid without custom data should not match DataComponentFluidIngredient!"); - - stack.applyComponents(DataComponentPatch.builder() - .set(DataComponents.RARITY, Rarity.UNCOMMON) - .build()); - - helper.assertFalse(ingredient.test(stack), "Fluid with incorrect data should not match DataComponentFluidIngredient!"); - - stack.applyComponents(DataComponentPatch.builder() - .set(DataComponents.RARITY, Rarity.EPIC) - .build()); - - helper.assertTrue(ingredient.test(stack), "Fluid with correct data should match DataComponentFluidIngredient!"); - - var data = CustomData.EMPTY.update(tag -> tag.putFloat("abcd", helper.getLevel().random.nextFloat())); - stack.applyComponents(DataComponentPatch.builder() - .set(DataComponents.CUSTOM_DATA, data) - .build()); - - helper.assertTrue(ingredient.test(stack), "Fluid with correct data should match partial DataComponentFluidIngredient regardless of extra data!"); - - helper.succeed(); - } - - @GameTest - @EmptyTemplate - @TestHolder(description = "Tests that strict data matches work correctly on fluid ingredients") - static void fluidIngredientDataStrictMatchWorks(final GameTestHelper helper) { - var ingredient = DataComponentFluidIngredient.of(true, DataComponents.RARITY, Rarity.EPIC, Fluids.WATER); - var stack = new FluidStack(Fluids.WATER, 1000); - - helper.assertFalse(ingredient.test(stack), "Fluid without custom data should not match DataComponentFluidIngredient!"); + var nestedSimpleFailed = FluidIngredient.CODEC.parse(ops, Util.make(new JsonObject(), json1 -> { + json1.addProperty("neoforge:ingredient_type", NeoForgeMod.SIMPLE_FLUID_INGREDIENT_TYPE.getId().toString()); + json1.addProperty("fluid", "minecraft:water"); + })).isError(); - stack.applyComponents(DataComponentPatch.builder() - .set(DataComponents.RARITY, Rarity.UNCOMMON) - .build()); - - helper.assertFalse(ingredient.test(stack), "Fluid with incorrect data should not match DataComponentFluidIngredient!"); - - stack.applyComponents(DataComponentPatch.builder() - .set(DataComponents.RARITY, Rarity.EPIC) - .build()); - - helper.assertTrue(ingredient.test(stack), "Fluid with correct data should match DataComponentFluidIngredient!"); - - var data = CustomData.EMPTY.update(tag -> tag.putFloat("abcd", helper.getLevel().random.nextFloat())); - stack.applyComponents(DataComponentPatch.builder() - .set(DataComponents.CUSTOM_DATA, data) - .build()); - - helper.assertFalse(ingredient.test(stack), "Fluid with extra unspecified data should not match strict DataComponentFluidIngredient!"); - - helper.succeed(); - } - - @GameTest - @EmptyTemplate - @TestHolder(description = "Tests that size and data components do not matter when matching fluid ingredients") - static void singleFluidIngredientIgnoresSizeAndData(final GameTestHelper helper) { - var ingredient = FluidIngredient.of(Fluids.WATER); - - helper.assertTrue(ingredient.test(new FluidStack(Fluids.WATER, 1234)), "Single fluid ingredient should match regardless of fluid amount!"); - helper.assertTrue(ingredient.test(new FluidStack(Fluids.WATER.builtInRegistryHolder(), 1234, DataComponentPatch.builder().set(DataComponents.RARITY, Rarity.COMMON).build())), "Single fluid ingredient should match regardless of fluid data!"); + helper.assertTrue(nestedSimpleFailed, "Nested SimpleFluidIngredient should not have been deserialized from map!"); helper.succeed(); } @@ -224,15 +77,10 @@ static void singleFluidIngredientIgnoresSizeAndData(final GameTestHelper helper) static void sizedFluidIngredientSerialization(final GameTestHelper helper) { var sized = SizedFluidIngredient.of(Fluids.WATER, 1000); - var flatResult = SizedFluidIngredient.FLAT_CODEC.encodeStart(JsonOps.INSTANCE, sized); - var flatJson = flatResult.resultOrPartial((error) -> helper.fail("(flat) Error while encoding SizedFluidIngredient: " + error)).orElseThrow(); - - helper.assertValueEqual(flatJson.toString(), "{\"fluid\":\"minecraft:water\",\"amount\":1000}", "(flat) serialized SizedFluidIngredient"); - - var nestedResult = SizedFluidIngredient.NESTED_CODEC.encodeStart(JsonOps.INSTANCE, sized); - var nestedJson = nestedResult.resultOrPartial((error) -> helper.fail("(nested) Error while encoding SizedFluidIngredient: " + error)).orElseThrow(); + var nestedResult = SizedFluidIngredient.CODEC.encodeStart(JsonOps.INSTANCE, sized); + var nestedJson = nestedResult.resultOrPartial((error) -> helper.fail("Error while encoding SizedFluidIngredient: " + error)).orElseThrow(); - helper.assertValueEqual(nestedJson.toString(), "{\"ingredient\":{\"fluid\":\"minecraft:water\"},\"amount\":1000}", "(nested) serialized SizedFluidIngredient"); + helper.assertValueEqual(nestedJson.toString(), "{\"ingredient\":\"minecraft:water\",\"amount\":1000}", "(nested) serialized SizedFluidIngredient"); helper.succeed(); } @@ -249,8 +97,9 @@ static void sizedFluidIngredientMatching(final GameTestHelper helper) { helper.assertTrue(sized.test(new FluidStack(Fluids.WATER, 2)), "SizedFluidIngredient should match fluid with required amount!"); helper.assertTrue(sized.test(new FluidStack(Fluids.WATER, 3)), "SizedFluidIngredient should match fluid with more than required amount!"); - var matches = sized.getFluids(); - helper.assertTrue(matches.length == 1 && (FluidStack.matches(matches[0], new FluidStack(Fluids.WATER, 2))), "SizedFluidIngredient matches should return all matched fluids with the correct amount!"); + // TODO(max): implement display wrapping for sized ingredients(?) + //var matches = sized.getFluids(); + //helper.assertTrue(matches.length == 1 && (FluidStack.matches(matches[0], new FluidStack(Fluids.WATER, 2))), "SizedFluidIngredient matches should return all matched fluids with the correct amount!"); helper.succeed(); }