Skip to content

Commit

Permalink
Rewrite the state-of-the-art fluid API in Java
Browse files Browse the repository at this point in the history
Bugs fixed:
- FluidAmountPredicate.atMost being an equality check
- FluidReference.increment: units swapped
  • Loading branch information
Juuxel committed Jul 24, 2024
1 parent d094ea1 commit 904da0d
Show file tree
Hide file tree
Showing 32 changed files with 833 additions and 581 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ public void setRecipe(IRecipeLayoutBuilder layoutBuilder, BrewingRecipe recipe,

var ingredient = r.fluid();
var amount = FluidUnit.convert(ingredient.getAmount(), ingredient.getUnit(), FluidBridge.get().getFluidUnit());
for (Fluid fluid : ingredient.getFluid().getFluids()) {
tank.addFluidStack(fluid, amount, ingredient.getNbt());
for (Fluid fluid : ingredient.fluid().getFluids()) {
tank.addFluidStack(fluid, amount, ingredient.nbt());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ public BrewerDisplay(FluidBrewingRecipe recipe) {

private static EntryIngredient entryIngredientOf(FluidIngredient fluidIngredient) {
var amount = FluidUnit.convert(fluidIngredient.getAmount(), fluidIngredient.getUnit(), FluidBridge.get().getFluidUnit());
var stacks = fluidIngredient.getFluid()
var stacks = fluidIngredient.fluid()
.getFluids()
.stream()
.map(fluid -> EntryStacks.of(FluidStack.create(fluid, amount, fluidIngredient.getNbt())))
.map(fluid -> EntryStacks.of(FluidStack.create(fluid, amount, fluidIngredient.nbt())))
.toList();
return EntryIngredient.of(stacks);
}
Expand Down
41 changes: 41 additions & 0 deletions common/src/main/java/juuxel/adorn/fluid/FluidAmountPredicate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package juuxel.adorn.fluid;

import net.minecraft.fluid.Fluids;

public interface FluidAmountPredicate {
HasFluidAmount getUpperBound();

boolean test(long amount, FluidUnit unit);

static FluidAmountPredicate exactly(long amount, FluidUnit unit) {
return new FluidAmountPredicate() {
private final FluidVolume upperBound = new FluidVolume(Fluids.EMPTY, amount, null, unit);

@Override
public HasFluidAmount getUpperBound() {
return upperBound;
}

@Override
public boolean test(long amount, FluidUnit unit) {
return FluidUnit.compareVolumes(amount, unit, upperBound.getAmount(), upperBound.getUnit()) == 0;
}
};
}

static FluidAmountPredicate atMost(long max, FluidUnit unit) {
return new FluidAmountPredicate() {
private final FluidVolume upperBound = new FluidVolume(Fluids.EMPTY, max, null, unit);

@Override
public HasFluidAmount getUpperBound() {
return upperBound;
}

@Override
public boolean test(long amount, FluidUnit unit) {
return FluidUnit.compareVolumes(amount, unit, upperBound.getAmount(), upperBound.getUnit()) <= 0;
}
};
}
}
53 changes: 53 additions & 0 deletions common/src/main/java/juuxel/adorn/fluid/FluidIngredient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package juuxel.adorn.fluid;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Optional;

/**
* A fluid ingredient for crafting.
*/
public record FluidIngredient(FluidKey fluid, long amount, @Nullable NbtCompound nbt, FluidUnit unit) implements HasFluidAmount {
public static final Codec<FluidIngredient> CODEC = RecordCodecBuilder.create(instance -> instance.group(
FluidKey.CODEC.fieldOf("fluid").forGetter(FluidIngredient::fluid),
Codec.LONG.fieldOf("amount").forGetter(FluidIngredient::amount),
NbtCompound.CODEC.optionalFieldOf("nbt").forGetter(ingredient -> Optional.ofNullable(ingredient.nbt)),
FluidUnit.CODEC.optionalFieldOf("unit", FluidUnit.LITRE).forGetter(FluidIngredient::unit)
).apply(instance, FluidIngredient::new));

// for DFU
private FluidIngredient(FluidKey fluid, long amount, Optional<NbtCompound> nbt, FluidUnit unit) {
this(fluid, amount, nbt.orElse(null), unit);
}

public static FluidIngredient load(PacketByteBuf buf) {
var fluid = FluidKey.load(buf);
var amount = buf.readVarLong();
var nbt = buf.readNbt();
var unit = buf.readEnumConstant(FluidUnit.class);
return new FluidIngredient(fluid, amount, nbt, unit);
}

public void write(PacketByteBuf buf) {
fluid.write(buf);
buf.writeVarLong(amount);
buf.writeNbt(nbt);
buf.writeEnumConstant(unit);
}

@Override
public long getAmount() {
return amount;
}

@NotNull
@Override
public FluidUnit getUnit() {
return unit;
}
}
67 changes: 67 additions & 0 deletions common/src/main/java/juuxel/adorn/fluid/FluidKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package juuxel.adorn.fluid;

import com.mojang.serialization.Codec;
import net.minecraft.fluid.Fluid;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.registry.Registries;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/// A "key" that identifies a fluid or a group of fluids.
///
/// Could be a single fluid, a tag or a list of the former.
///
/// ## JSON format
///
/// A fluid key is one of:
///
/// - a string; if prefixed with `#`, a tag, otherwise a fluid ID
/// - an array of strings as described above
///
/// Examples: `"minecraft:water"`, `"#c:milk"`, `["minecraft:water", "minecraft:lava"]`
public sealed interface FluidKey permits FluidKeyImpl.Simple, FluidKeyImpl.OfArray {
Codec<FluidKey> CODEC = FluidKeyImpl.CODEC;

/**
* Returns the set of all fluids matching this key.
*/
Set<Fluid> getFluids();

/**
* Tests whether the fluid matches this key.
*/
boolean matches(Fluid fluid);

/**
* Writes this key to a packet buffer.
* @see #load
*/
default void write(PacketByteBuf buf) {
var fluids = getFluids();
buf.writeVarInt(fluids.size());

for (Fluid fluid : fluids) {
buf.writeVarInt(Registries.FLUID.getRawId(fluid));
}
}

/**
* Reads a key from a packet buffer.
* @see #write
*/
static FluidKey load(PacketByteBuf buf) {
var size = buf.readVarInt();

if (size == 1) {
return new FluidKeyImpl.OfFluid(Registries.FLUID.get(buf.readVarInt()));
} else {
List<FluidKeyImpl.Simple> children = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
children.add(new FluidKeyImpl.OfFluid(Registries.FLUID.get(buf.readVarInt())));
}
return new FluidKeyImpl.OfArray(children);
}
}
}
123 changes: 123 additions & 0 deletions common/src/main/java/juuxel/adorn/fluid/FluidKeyImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package juuxel.adorn.fluid;

import com.mojang.datafixers.util.Either;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import net.minecraft.fluid.Fluid;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.util.Identifier;
import net.minecraft.util.dynamic.Codecs;

import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

final class FluidKeyImpl {
private static final Codec<Simple> SIMPLE_CODEC = new Codec<>() {
@Override
public <T> DataResult<Pair<FluidKeyImpl.Simple, T>> decode(DynamicOps<T> ops, T input) {
return ops.getStringValue(input)
.map(s -> {
if (s.startsWith("#")) {
return new OfTag(TagKey.of(RegistryKeys.FLUID, new Identifier(s.substring(1))));
} else {
return new OfFluid(Registries.FLUID.get(new Identifier(s)));
}
})
.map(key -> Pair.of(key, ops.empty()));
}

@Override
public <T> DataResult<T> encode(FluidKeyImpl.Simple input, DynamicOps<T> ops, T prefix) {
return ops.mergeToPrimitive(prefix, ops.createString(input.getId()));
}

@Override
public String toString() {
return "SimpleFluidKey";
}
};

// TODO: Switch to either instead of xor
public static final Codec<FluidKey> CODEC = Codecs.xor(
SIMPLE_CODEC,
SIMPLE_CODEC.listOf().xmap(OfArray::new, OfArray::children)
).xmap(
// TODO (Java 21): Use pattern matching
either -> either.map(Function.identity(), Function.identity()),
key -> {
if (key instanceof Simple simple) {
return Either.left(simple);
} else if (key instanceof OfArray ofArray) {
return Either.right(ofArray);
} else {
throw new IllegalArgumentException();
}
}
);

sealed interface Simple extends FluidKey {
String getId();
}

record OfFluid(Fluid fluid) implements Simple {
@Override
public String getId() {
return Registries.FLUID.getId(fluid).toString();
}

@Override
public Set<Fluid> getFluids() {
return Set.of(fluid);
}

@Override
public boolean matches(Fluid fluid) {
return fluid == this.fluid;
}
}

record OfTag(TagKey<Fluid> tag) implements Simple {
@Override
public String getId() {
return "#" + tag.id();
}

@Override
public Set<Fluid> getFluids() {
return Registries.FLUID.getOrCreateEntryList(tag)
.stream()
.map(RegistryEntry::value)
.collect(Collectors.toSet());
}

@Override
public boolean matches(Fluid fluid) {
return fluid.isIn(tag);
}
}

record OfArray(List<Simple> children) implements FluidKey {
@Override
public Set<Fluid> getFluids() {
return children.stream()
.flatMap(child -> child.getFluids().stream())
.collect(Collectors.toSet());
}

@Override
public boolean matches(Fluid fluid) {
for (var child : children) {
if (child.matches(fluid)) return true;
}

return false;
}
}
}
Loading

0 comments on commit 904da0d

Please sign in to comment.