diff --git a/src/test/resources/data/minecraft/stationapi/tags/blocks/mineable/pickaxe.json b/src/test/resources/data/minecraft/stationapi/tags/blocks/mineable/pickaxe.json index 7e6758701..b81eabf3c 100644 --- a/src/test/resources/data/minecraft/stationapi/tags/blocks/mineable/pickaxe.json +++ b/src/test/resources/data/minecraft/stationapi/tags/blocks/mineable/pickaxe.json @@ -1,6 +1,7 @@ { "values": [ "sltest:test_block", - "sltest:variation_block" + "sltest:variation_block", + "minecraft:wool@1" ] } \ No newline at end of file diff --git a/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/ActorContext.java b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/ActorContext.java new file mode 100644 index 000000000..c4776b1c5 --- /dev/null +++ b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/ActorContext.java @@ -0,0 +1,28 @@ +package net.modificationstation.stationapi.api.util.context; + +import org.jetbrains.annotations.Nullable; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +/** + * A context that includes an actor that performed the action. + */ +public interface ActorContext extends Context { + /** + * The context key used to evaluate actor-related conditions. + */ + Context.Key ACTOR_KEY = new Context.Key<>(NAMESPACE.id("actor")); + + static ActorContext of(Context context) { + if (context instanceof ActorContext a) return a; + interface ActorContextDelegate extends ActorContext, Delegate {} + return (ActorContextDelegate) () -> context; + } + + /** + * {@return the actor performing the action, or {@code null} if none} + */ + default @Nullable Object actor() { + return get(ACTOR_KEY); + } +} diff --git a/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/Condition.java b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/Condition.java new file mode 100644 index 000000000..462e18eb1 --- /dev/null +++ b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/Condition.java @@ -0,0 +1,7 @@ +package net.modificationstation.stationapi.api.util.context; + +public record Condition(ConditionType type, DATA data) { + public boolean test(Context ctx) { + return type.test(data, ctx); + } +} diff --git a/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/ConditionType.java b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/ConditionType.java new file mode 100644 index 000000000..35ed51cad --- /dev/null +++ b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/ConditionType.java @@ -0,0 +1,95 @@ +package net.modificationstation.stationapi.api.util.context; + +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.MapCodec; +import net.modificationstation.stationapi.api.util.Identifier; + +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +public final class ConditionType { + private final Identifier id; + private final MapCodec dataCodec; + private final BiPredicate logic; + private final Pattern shorthandPattern; + private final Function, Dynamic> unfolder; + private final MapCodec> conditionCodec; + + private ConditionType(Builder builder) { + this.id = builder.id; + this.dataCodec = builder.codec; + this.shorthandPattern = builder.shorthandPattern; + this.unfolder = builder.unfolder; + + BiPredicate logic = builder.logic; + Function projection = builder.projection; + this.logic = (data, ctx) -> logic.test(data, projection.apply(ctx)); + + conditionCodec = dataCodec.xmap( + data -> new Condition<>(this, data), + Condition::data + ); + } + + public static Builder builder(Identifier id, MapCodec codec, Function projection, BiPredicate logic, Consumer> registryCallback) { + return new Builder<>(id, codec, projection, logic, registryCallback); + } + + public static class Builder { + private final Identifier id; + private final MapCodec codec; + private final Function projection; + private final BiPredicate logic; + private final Consumer> registryCallback; + private Pattern shorthandPattern; + private Function, Dynamic> unfolder; + + private Builder(Identifier id, MapCodec codec, Function projection, BiPredicate logic, Consumer> registryCallback) { + this.id = id; + this.codec = codec; + this.projection = projection; + this.logic = logic; + this.registryCallback = registryCallback; + } + + public Builder shorthand(Pattern pattern, Function, Dynamic> unfolder) { + this.shorthandPattern = pattern; + this.unfolder = unfolder; + return this; + } + + public void register() { + registryCallback.accept(new ConditionType<>(this)); + } + } + + public boolean test(DATA data, Context ctx) { + return logic.test(data, ctx); + } + + public MapCodec> conditionCodec() { + return conditionCodec; + } + + public Identifier id() { + return id; + } + + public MapCodec dataCodec() { + return dataCodec; + } + + public BiPredicate logic() { + return logic; + } + + public Pattern shorthandPattern() { + return shorthandPattern; + } + + public Function, Dynamic> unfolder() { + return unfolder; + } +} diff --git a/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/Context.java b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/Context.java new file mode 100644 index 000000000..88df917c9 --- /dev/null +++ b/station-api-base/src/main/java/net/modificationstation/stationapi/api/util/context/Context.java @@ -0,0 +1,186 @@ +package net.modificationstation.stationapi.api.util.context; + +import net.modificationstation.stationapi.api.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +/** + * A data source that contains keys and their associated values. + *

+ * This is a functional interface that can be implemented with a lambda + * to provide raw values based on the given {@link Identifier}. + */ +@FunctionalInterface +public interface Context { + /** + * A delegate interface for creating zero-allocation projections. + */ + @FunctionalInterface + interface Delegate extends Context { + Context delegate(); + + @Override + default @Nullable Object getRaw(Identifier id) { + return delegate().getRaw(id); + } + + @Override + default boolean contains(Identifier id) { + return delegate().contains(id); + } + + @Override + default int getIntRaw(Identifier id, int defaultValue) { + return delegate().getIntRaw(id, defaultValue); + } + } + + @SuppressWarnings("unused") + record Key(Identifier id) {} + + /** + * A key used to mark a context as explicitly empty/inert. + * Composing with an empty context will yield the other context unchanged. + */ + Key EMPTY_KEY = new Key<>(NAMESPACE.id("empty")); + + /** + * An empty context that acts as an identity element for composition. + *

+ * Composing with an empty context yields the other context unchanged. + * Any {@link Delegate} wrapping an empty context is also considered empty. + */ + Context EMPTY = id -> EMPTY_KEY.id() == id ? Boolean.TRUE : null; + + /** + * {@return a new context containing a single key-value pair} + */ + static Context of(Key key, VALUE value) { + Identifier id = key.id(); + return k -> id == k ? value : null; + } + + /** + * {@return the raw value associated with the given identifier, or {@code null} if not + * present} + *

+ * This is the primary abstract method for implementations. + */ + @Nullable Object getRaw(Identifier id); + + /** + * {@return the value associated with the given key, or {@code null} if not + * present} + */ + @SuppressWarnings("unchecked") + default @Nullable VALUE get(Key key) { + return (VALUE) getRaw(key.id()); + } + + /** + * {@return whether this context contains a value associated with the given identifier} + */ + default boolean contains(Identifier id) { + return getRaw(id) != null; + } + + /** + * {@return whether this context contains the given key} + */ + default boolean contains(Key key) { + return contains(key.id()); + } + + /** + * {@return an optional containing the value associated with the given identifier} + */ + default Optional getOptional(Identifier id) { + return Optional.ofNullable(getRaw(id)); + } + + /** + * {@return an optional containing the value associated with the given key} + */ + default Optional getOptional(Key key) { + return Optional.ofNullable(get(key)); + } + + /** + * {@return the unboxed integer associated with the given key, or {@code defaultValue} if not present} + */ + default int getInt(Key key, int defaultValue) { + return getIntRaw(key.id(), defaultValue); + } + + /** + * {@return the unboxed integer associated with the given identifier, or {@code defaultValue} if not present} + */ + default int getIntRaw(Identifier id, int defaultValue) { + Object raw = getRaw(id); + return raw instanceof Integer i ? i : defaultValue; + } + + /** + * {@return a new context that includes the given key-value pair as an override} + */ + default Context with(Key key, VALUE value) { + Identifier id = key.id(); + return with(k -> id == k ? value : null); + } + + /** + * {@return a new context that includes the given context as an override} + */ + default Context with(Context other) { + return append(this, other); + } + + /** + * {@return a new context that includes the given contexts as overrides in the + * provided order} + */ + default Context with(Context... others) { + Context result = this; + for (Context other : others) result = result.with(other); + return result; + } + + private static Context append(Context base, Context addition) { + if (addition == EMPTY || Boolean.TRUE.equals(addition.get(EMPTY_KEY))) return base; + if (base == EMPTY || Boolean.TRUE.equals(base.get(EMPTY_KEY))) return addition; + + record Composite(Context head, Context next) implements Context { + private Context find(Identifier id) { + Context current = this; + while (current instanceof Composite comp) { + if (comp.head.contains(id)) return comp.head; + current = comp.next; + } + return current != null && current.contains(id) ? current : null; + } + + @Override + public @Nullable Object getRaw(Identifier id) { + Context ctx = find(id); + return ctx == null ? null : ctx.getRaw(id); + } + + @Override + public boolean contains(Identifier id) { + return find(id) != null; + } + + @Override + public int getIntRaw(Identifier id, int defaultValue) { + Context ctx = find(id); + return ctx == null ? defaultValue : ctx.getIntRaw(id, defaultValue); + } + } + return addition instanceof Composite composite + ? append(append(base, composite.next), composite.head) + : new Composite(addition, base); + } +} diff --git a/station-blocks-v0/src/main/java/net/modificationstation/stationapi/impl/tag/conditional/BlockTagConditionsImpl.java b/station-blocks-v0/src/main/java/net/modificationstation/stationapi/impl/tag/conditional/BlockTagConditionsImpl.java new file mode 100644 index 000000000..0f895fd92 --- /dev/null +++ b/station-blocks-v0/src/main/java/net/modificationstation/stationapi/impl/tag/conditional/BlockTagConditionsImpl.java @@ -0,0 +1,39 @@ +package net.modificationstation.stationapi.impl.tag.conditional; + +import com.mojang.serialization.Codec; +import net.mine_diver.unsafeevents.listener.EventListener; +import net.modificationstation.stationapi.api.StationAPI; +import net.modificationstation.stationapi.api.event.registry.BlockRegistryEvent; +import net.modificationstation.stationapi.api.mod.entrypoint.Entrypoint; +import net.modificationstation.stationapi.api.mod.entrypoint.EntrypointManager; +import net.modificationstation.stationapi.api.mod.entrypoint.EventBusPolicy; + +import java.lang.invoke.MethodHandles; +import java.util.regex.Pattern; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +@Entrypoint(eventBus = @EventBusPolicy(registerInstance = false)) +@EventListener(phase = StationAPI.INTERNAL_PHASE) +public class BlockTagConditionsImpl { + static { + EntrypointManager.registerLookup(MethodHandles.lookup()); + } + + @EventListener + private static void registerConditions(BlockRegistryEvent event) { + event.registry + .buildBlockTagCondition( + NAMESPACE.id("block_metadata"), + Codec.INT.fieldOf("metadata"), + (metadata, ctx) -> ctx.hasBlockMeta() && ctx.blockMeta() == metadata + ) + .shorthand( + Pattern.compile("@(\\d+)"), + dynamic -> dynamic.emptyMap().set( + "metadata", dynamic.createInt(Integer.parseInt(dynamic.asString("0"))) + ) + ) + .register(); + } +} diff --git a/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/FireBlockMixin.java b/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/FireBlockMixin.java index 5f139880c..dc902291c 100644 --- a/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/FireBlockMixin.java +++ b/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/FireBlockMixin.java @@ -3,6 +3,7 @@ import net.minecraft.block.FireBlock; import net.minecraft.world.World; import net.modificationstation.stationapi.api.StationAPI; +import net.modificationstation.stationapi.api.block.context.BlockTagContext; import net.modificationstation.stationapi.api.event.block.FireBurnableRegisterEvent; import net.modificationstation.stationapi.api.registry.tag.BlockTags; import org.spongepowered.asm.mixin.Mixin; @@ -41,6 +42,8 @@ private void stationapi_postBurnableRegister(CallbackInfo ci) { } ) private int stationapi_allowInfiniburnBlocks(int constant, World world, int x, int y, int z, Random random) { - return world.getBlockState(x, y - 1, z).isIn(BlockTags.INFINIBURN) ? 1 : 0; + return world.getBlockState(x, y - 1, z).isIn( + BlockTags.INFINIBURN, BlockTagContext.of(world, x, y - 1, z) + ) ? 1 : 0; } } diff --git a/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/LeavesBlockMixin.java b/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/LeavesBlockMixin.java index cc5bbf2ff..943b1a9a4 100644 --- a/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/LeavesBlockMixin.java +++ b/station-blocks-v0/src/main/java/net/modificationstation/stationapi/mixin/block/LeavesBlockMixin.java @@ -6,6 +6,7 @@ import net.minecraft.block.LeavesBlock; import net.minecraft.world.World; import net.modificationstation.stationapi.api.block.BlockState; +import net.modificationstation.stationapi.api.block.context.BlockTagContext; import net.modificationstation.stationapi.api.registry.tag.BlockTags; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -14,15 +15,15 @@ public class LeavesBlockMixin { @WrapOperation(method = "onTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;getBlockId(III)I")) - private int makeModdedLogsAndLeavesWork(World instance, int x, int y, int z, Operation original) { - BlockState state = instance.getBlockState(x, y, z); - if (state.isIn(BlockTags.LOGS)) { + private int makeModdedLogsAndLeavesWork(World world, int x, int y, int z, Operation original) { + BlockState state = world.getBlockState(x, y, z); + if (state.isIn(BlockTags.LOGS, BlockTagContext.of(world, x, y, z))) { return Block.LOG.id; } - if (state.isIn(BlockTags.LEAVES)) { + if (state.isIn(BlockTags.LEAVES, BlockTagContext.of(world, x, y, z))) { return Block.LEAVES.id; } - return original.call(instance, x, y, z); + return original.call(world, x, y, z); } } diff --git a/station-blocks-v0/src/main/resources/fabric.mod.json b/station-blocks-v0/src/main/resources/fabric.mod.json index 6247cd80d..1c10f52d3 100644 --- a/station-blocks-v0/src/main/resources/fabric.mod.json +++ b/station-blocks-v0/src/main/resources/fabric.mod.json @@ -18,6 +18,11 @@ "icon": "assets/station-blocks-v0/icon.png", "environment": "*", + "entrypoints": { + "stationapi:event_bus": [ + "net.modificationstation.stationapi.impl.tag.conditional.BlockTagConditionsImpl" + ] + }, "mixins": [ "station-blocks-v0.mixins.json" ], diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/AbstractBlockState.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/AbstractBlockState.java index 76e3d3d6f..ca1bb96a5 100644 --- a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/AbstractBlockState.java +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/AbstractBlockState.java @@ -11,6 +11,7 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.world.BlockView; import net.minecraft.world.World; +import net.modificationstation.stationapi.api.block.context.BlockTagContext; import net.modificationstation.stationapi.api.item.ItemPlacementContext; import net.modificationstation.stationapi.api.registry.RegistryEntryList; import net.modificationstation.stationapi.api.state.State; @@ -30,7 +31,8 @@ public abstract class AbstractBlockState extends State { private final boolean opaque; private int luminance = -1; - protected AbstractBlockState(Block block, ImmutableMap, Comparable> propertyMap, MapCodec mapCodec) { + protected AbstractBlockState(Block block, ImmutableMap, Comparable> propertyMap, + MapCodec mapCodec) { super(block, propertyMap, mapCodec); this.isAir = block.material == Material.AIR; this.material = block.material; @@ -51,9 +53,10 @@ public Material getMaterial() { * Returns the light level emitted by this block state. */ public int getLuminance() { - return luminance == -1 ? - luminance = ((StationFlatteningBlockInternal) owner).stationapi_getLuminanceProvider().applyAsInt(asBlockState()) : - luminance; + return luminance == -1 + ? luminance = ((StationFlatteningBlockInternal) owner).stationapi_getLuminanceProvider() + .applyAsInt(asBlockState()) + : luminance; } public boolean isAir() { @@ -84,20 +87,54 @@ public boolean canReplace(ItemPlacementContext context) { return this.getBlock().canReplace(this.asBlockState(), context); } + /** + * @deprecated Use {@link #isIn(TagKey, BlockTagContext, Predicate)} instead. + * Relying on tag checks without a {@link BlockTagContext} can lead to broken behavior if the tag contains conditions + * that require contextual information (such as the block's world position, metadata, or the actor interacting with it). + */ + @Deprecated + public boolean isIn(TagKey tag, Predicate predicate) { + return isIn(tag) && predicate.test(this); + } + + /** + * @deprecated Use {@link #isIn(TagKey, BlockTagContext)} instead. + * Relying on tag checks without a {@link BlockTagContext} can lead to broken behavior if the tag contains conditions + * that require contextual information (such as the block's world position, metadata, or the actor interacting with it). + */ + @Deprecated public boolean isIn(TagKey tag) { - return getBlock().getRegistryEntry().isIn(tag); + return isIn(tag, BlockTagContext.DEFAULT); } - public boolean isIn(TagKey tag, Predicate predicate) { - return this.isIn(tag) && predicate.test(this); + public boolean isIn(TagKey tag, BlockTagContext context, Predicate predicate) { + return isIn(tag, context) && predicate.test(this); } + public boolean isIn(TagKey tag, BlockTagContext context) { + return getBlock().getRegistryEntry().isIn(tag, context); + } + + /** + * @deprecated Use {@link #isIn(RegistryEntryList, BlockTagContext)} instead. + * Relying on list checks without a {@link BlockTagContext} can lead to broken behavior if the underlying entries + * have contextual conditions. + */ + @Deprecated public boolean isIn(RegistryEntryList blocks) { - return blocks.contains(getBlock().getRegistryEntry()); + return isIn(blocks, BlockTagContext.DEFAULT); + } + + public boolean isIn(RegistryEntryList blocks, BlockTagContext context) { + return blocks.contains(getBlock().getRegistryEntry(), context); } public Stream> streamTags() { - return getBlock().getRegistryEntry().streamTags(); + return streamTags(BlockTagContext.BYPASSED); + } + + public Stream> streamTags(BlockTagContext context) { + return getBlock().getRegistryEntry().streamTags(context); } public boolean isOf(Block block) { diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/context/BlockContext.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/context/BlockContext.java new file mode 100644 index 000000000..04796dd1d --- /dev/null +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/context/BlockContext.java @@ -0,0 +1,114 @@ +package net.modificationstation.stationapi.api.block.context; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.BlockView; +import net.modificationstation.stationapi.api.util.Identifier; +import net.modificationstation.stationapi.api.util.context.Context; +import org.jetbrains.annotations.Nullable; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +/** + * Context for block-related evaluation. + * Contains only the intrinsic state of a block in the world. + */ +@FunctionalInterface +public interface BlockContext extends Context { + /** + * The context key used to evaluate world-related conditions in a block context. + */ + Context.Key BLOCK_VIEW_KEY = new Context.Key<>(NAMESPACE.id("block_view")); + + /** + * The context key used to evaluate position-related conditions in a block context. + */ + Context.Key BLOCK_POS_KEY = new Context.Key<>(NAMESPACE.id("block_pos")); + + /** + * The context key used to evaluate block metadata conditions. + */ + Context.Key BLOCK_METADATA_KEY = new Context.Key<>(NAMESPACE.id("block_metadata")); + + /** + * {@return whether this context has block metadata} + */ + default boolean hasBlockMeta() { + return contains(BLOCK_METADATA_KEY) || (blockView() != null && blockPos() != null); + } + + /** + * {@return the block view the block interaction is occurring in} + */ + default @Nullable BlockView blockView() { return get(BLOCK_VIEW_KEY); } + + /** + * {@return the position of the block interaction} + */ + default @Nullable BlockPos blockPos() { return get(BLOCK_POS_KEY); } + + /** + * {@return the block metadata} + */ + default int blockMeta() { + return getIntRaw(BLOCK_METADATA_KEY.id(), 0); + } + + interface DataProvider extends BlockContext { + @Override + @Nullable BlockView blockView(); + + @Override + @Nullable BlockPos blockPos(); + + @Override + default int blockMeta() { + BlockView view = blockView(); + BlockPos pos = blockPos(); + return view != null && pos != null ? view.getBlockMeta(pos.x, pos.y, pos.z) : 0; + } + + @Override + default Object getRaw(Identifier id) { + if (BLOCK_VIEW_KEY.id() == id) return blockView(); + if (BLOCK_POS_KEY.id() == id) return blockPos(); + if (BLOCK_METADATA_KEY.id() == id) return blockView() != null && blockPos() != null ? blockMeta() : null; + return null; + } + + @Override + default int getIntRaw(Identifier id, int defaultValue) { + if (BLOCK_METADATA_KEY.id() == id) return blockMeta(); + return BlockContext.super.getIntRaw(id, defaultValue); + } + } + + /** + * Creates a new block context. + */ + static BlockContext of(BlockView world, BlockPos pos) { + record Impl( + @Nullable BlockView blockView, + @Nullable BlockPos blockPos + ) implements DataProvider {} + return new Impl(world, pos); + } + + /** + * Creates a new block context. + */ + static BlockContext of(BlockView world, int x, int y, int z) { + return of(world, new BlockPos(x, y, z)); + } + + /** + * Projects a generic context into a block context view. + * + * @param context the context to project + * @return the block context view + */ + static BlockContext of(Context context) { + if (context instanceof BlockContext b) return b; + interface BlockContextDelegate extends BlockContext, Delegate {} + return (BlockContextDelegate) () -> context; + } +} diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/context/BlockTagContext.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/context/BlockTagContext.java new file mode 100644 index 000000000..6cffa8590 --- /dev/null +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/block/context/BlockTagContext.java @@ -0,0 +1,40 @@ +package net.modificationstation.stationapi.api.block.context; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.BlockView; +import net.modificationstation.stationapi.api.tag.context.TagEvaluationContext; +import net.modificationstation.stationapi.api.util.context.Context; + +public interface BlockTagContext extends BlockContext, TagEvaluationContext { + BlockTagContext DEFAULT = of(TagEvaluationContext.DEFAULT); + BlockTagContext BYPASSED = of(TagEvaluationContext.BYPASSED); + + static BlockTagContext of(BlockView view, BlockPos pos) { + Context data = BlockContext.of(view, pos); + interface BlockTagContextDelegate extends BlockTagContext, Delegate {} + return (BlockTagContextDelegate) () -> data; + } + + static BlockTagContext of(BlockView view, BlockPos pos, boolean ignoreTagConditions) { + Context data = BlockContext.of(view, pos).with(TagEvaluationContext.of(ignoreTagConditions)); + interface BlockTagContextDelegate extends BlockTagContext, Delegate {} + return (BlockTagContextDelegate) () -> data; + } + + static BlockTagContext of(BlockView view, int x, int y, int z) { + return of(view, new BlockPos(x, y, z)); + } + + static BlockTagContext of(BlockView view, int x, int y, int z, boolean ignoreTagConditions) { + Context data = BlockContext.of(view, x, y, z).with(TagEvaluationContext.of(ignoreTagConditions)); + interface BlockTagContextDelegate extends BlockTagContext, Delegate {} + return (BlockTagContextDelegate) () -> data; + } + + static BlockTagContext of(Context context) { + if (context instanceof BlockTagContext b) return b; + interface BlockTagContextDelegate extends BlockTagContext, Delegate {} + return (BlockTagContextDelegate) () -> context; + } + +} diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/StationFlatteningItemStack.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/StationFlatteningItemStack.java index 89e26b126..5207ce3da 100644 --- a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/StationFlatteningItemStack.java +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/StationFlatteningItemStack.java @@ -2,21 +2,42 @@ import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; import net.minecraft.util.math.BlockPos; import net.minecraft.world.BlockView; import net.modificationstation.stationapi.api.block.BlockState; import net.modificationstation.stationapi.api.registry.RegistryEntry; import net.modificationstation.stationapi.api.tag.TagKey; +import net.modificationstation.stationapi.api.tag.context.TagEvaluationContext; import net.modificationstation.stationapi.api.util.Util; +import java.util.stream.Stream; + public interface StationFlatteningItemStack extends ItemStackStrengthWithBlockState { default RegistryEntry.Reference getRegistryEntry() { return Util.assertImpl(); } + /** + * Since an {@link ItemStack} is itself an item's context, + * there's no need to provide the context manually here, + * it gets prepended internally + */ default boolean isIn(TagKey tag) { - return getRegistryEntry().isIn(tag); + return isIn(tag, TagEvaluationContext.DEFAULT); + } + + default boolean isIn(TagKey tag, TagEvaluationContext context) { + return Util.assertImpl(); + } + + default Stream> streamTags() { + return streamTags(TagEvaluationContext.BYPASSED); + } + + default Stream> streamTags(TagEvaluationContext context) { + return Util.assertImpl(); } @Override diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/context/ItemContext.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/context/ItemContext.java new file mode 100644 index 000000000..72682e040 --- /dev/null +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/context/ItemContext.java @@ -0,0 +1,90 @@ +package net.modificationstation.stationapi.api.item.context; + +import net.minecraft.item.ItemStack; +import net.modificationstation.stationapi.api.util.Identifier; +import net.modificationstation.stationapi.api.util.context.Context; +import org.jetbrains.annotations.Nullable; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +/** + * Context for item-related evaluation. + */ +@FunctionalInterface +public interface ItemContext extends Context { + /** + * The context key used to evaluate item-related conditions in an item context. + */ + Key ITEM_STACK_KEY = new Key<>(NAMESPACE.id("item_stack")); + + /** + * The context key used to evaluate item damage conditions. + */ + Key ITEM_DAMAGE_KEY = new Key<>(NAMESPACE.id("item_damage")); + + /** + * {@return whether this context has item damage data} + */ + default boolean hasDamage() { + return contains(ITEM_DAMAGE_KEY) || itemStack() != null; + } + + @FunctionalInterface + interface DataProvider extends ItemContext { + @Override + @Nullable ItemStack itemStack(); + + @Override + default int damage() { + ItemStack stack = itemStack(); + return stack == null ? 0 : stack.getDamage(); + } + + @Override + default Object getRaw(Identifier id) { + if (ITEM_STACK_KEY.id() == id) return itemStack(); + if (ITEM_DAMAGE_KEY.id() == id) return itemStack() == null ? null : damage(); + return null; + } + + @Override + default int getIntRaw(Identifier id, int defaultValue) { + if (ITEM_DAMAGE_KEY.id() == id) return damage(); + return ItemContext.super.getIntRaw(id, defaultValue); + } + } + + /** + * {@return the item stack being evaluated} + */ + default @Nullable ItemStack itemStack() { return get(ITEM_STACK_KEY); } + + /** + * {@return the item damage} + */ + default int damage() { + Integer dmg = get(ITEM_DAMAGE_KEY); + if (dmg != null) return dmg; + ItemStack stack = itemStack(); + return stack == null ? 0 : stack.getDamage(); + } + + /** + * Creates a new item context. + */ + static ItemContext of(ItemStack stack) { + return (DataProvider) () -> stack; + } + + /** + * Projects a generic context into an item context view. + * + * @param context the context to project + * @return the item context view + */ + static ItemContext of(Context context) { + if (context instanceof ItemContext i) return i; + interface ItemContextDelegate extends ItemContext, Delegate {} + return (ItemContextDelegate) () -> context; + } +} diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/context/ItemTagContext.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/context/ItemTagContext.java new file mode 100644 index 000000000..876ecc32a --- /dev/null +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/item/context/ItemTagContext.java @@ -0,0 +1,29 @@ +package net.modificationstation.stationapi.api.item.context; + +import net.minecraft.item.ItemStack; +import net.modificationstation.stationapi.api.tag.context.TagEvaluationContext; +import net.modificationstation.stationapi.api.util.context.Context; + +public interface ItemTagContext extends ItemContext, TagEvaluationContext { + ItemTagContext DEFAULT = of(TagEvaluationContext.DEFAULT); + ItemTagContext BYPASSED = of(TagEvaluationContext.BYPASSED); + + static ItemTagContext of(ItemStack stack) { + Context data = ItemContext.of(stack); + interface ItemTagContextDelegate extends ItemTagContext, Delegate {} + return (ItemTagContextDelegate) () -> data; + } + + static ItemTagContext of(ItemStack stack, boolean ignoreTagConditions) { + Context data = ItemContext.of(stack).with(TagEvaluationContext.of(ignoreTagConditions)); + interface ItemTagContextDelegate extends ItemTagContext, Delegate {} + return (ItemTagContextDelegate) () -> data; + } + + static ItemTagContext of(Context context) { + if (context instanceof ItemTagContext i) return i; + interface ItemTagContextDelegate extends ItemTagContext, Delegate {} + return (ItemTagContextDelegate) () -> context; + } + +} diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/BlockRegistry.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/BlockRegistry.java index e165c9068..904f8dd01 100644 --- a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/BlockRegistry.java +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/BlockRegistry.java @@ -1,14 +1,23 @@ package net.modificationstation.stationapi.api.registry; +import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Lifecycle; +import com.mojang.serialization.MapCodec; import net.minecraft.block.Block; +import net.modificationstation.stationapi.api.block.context.BlockContext; import net.modificationstation.stationapi.api.event.registry.RegistryAttribute; import net.modificationstation.stationapi.api.event.registry.RegistryAttributeHolder; +import net.modificationstation.stationapi.api.util.Identifier; +import net.modificationstation.stationapi.api.util.context.ConditionType; +import net.modificationstation.stationapi.api.util.context.Context; + +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; public final class BlockRegistry extends SimpleRegistry { - public static final RegistryKey> KEY = RegistryKey.ofRegistry(NAMESPACE.id("blocks")); public static final BlockRegistry INSTANCE = Registries.create(KEY, new BlockRegistry(), Lifecycle.experimental()); @@ -17,4 +26,28 @@ private BlockRegistry() { RegistryAttributeHolder.get(this).addAttribute(RegistryAttribute.SYNCED); nextId = 256; } + + /** + * Creates a builder for a data-less tag condition that operates on a {@link BlockContext}. + *

+ * This is a convenience overload that automatically projects the raw context + * via {@link BlockContext#of(Context)}. + * + * @see #buildTagCondition(Identifier, Function, Predicate) + */ + public ConditionType.Builder buildBlockTagCondition(Identifier id, Predicate condition) { + return buildTagCondition(id, BlockContext::of, condition); + } + + /** + * Creates a builder for a data-backed tag condition that operates on a {@link BlockContext}. + *

+ * This is a convenience overload that automatically projects the raw context + * via {@link BlockContext#of(Context)}. + * + * @see #buildTagCondition(Identifier, MapCodec, Function, BiPredicate) + */ + public ConditionType.Builder buildBlockTagCondition(Identifier id, MapCodec codec, BiPredicate condition) { + return buildTagCondition(id, codec, BlockContext::of, condition); + } } diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/ItemRegistry.java b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/ItemRegistry.java index 9707b599d..f5ba62559 100644 --- a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/ItemRegistry.java +++ b/station-flattening-v0/src/main/java/net/modificationstation/stationapi/api/registry/ItemRegistry.java @@ -1,15 +1,24 @@ package net.modificationstation.stationapi.api.registry; +import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Lifecycle; +import com.mojang.serialization.MapCodec; import it.unimi.dsi.fastutil.ints.Int2IntFunction; import net.minecraft.item.Item; import net.modificationstation.stationapi.api.event.registry.RegistryAttribute; import net.modificationstation.stationapi.api.event.registry.RegistryAttributeHolder; +import net.modificationstation.stationapi.api.item.context.ItemContext; +import net.modificationstation.stationapi.api.util.Identifier; +import net.modificationstation.stationapi.api.util.context.ConditionType; +import net.modificationstation.stationapi.api.util.context.Context; + +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; public final class ItemRegistry extends SimpleRegistry { - public static final RegistryKey> KEY = RegistryKey.ofRegistry(NAMESPACE.id("items")); public static final ItemRegistry INSTANCE = Registries.create(KEY, new ItemRegistry(), Lifecycle.experimental()); public static final int ID_SHIFT = 256; @@ -20,4 +29,28 @@ private ItemRegistry() { super(KEY, Lifecycle.experimental(), true); RegistryAttributeHolder.get(this).addAttribute(RegistryAttribute.SYNCED); } + + /** + * Creates a builder for a data-less tag condition that operates on an {@link ItemContext}. + *

+ * This is a convenience overload that automatically projects the raw context + * via {@link ItemContext#of(Context)}. + * + * @see #buildTagCondition(Identifier, Function, Predicate) + */ + public ConditionType.Builder buildItemTagCondition(Identifier id, Predicate condition) { + return buildTagCondition(id, ItemContext::of, condition); + } + + /** + * Creates a builder for a data-backed tag condition that operates on an {@link ItemContext}. + *

+ * This is a convenience overload that automatically projects the raw context + * via {@link ItemContext#of(Context)}. + * + * @see #buildTagCondition(Identifier, MapCodec, Function, BiPredicate) + */ + public ConditionType.Builder buildItemTagCondition(Identifier id, MapCodec codec, BiPredicate condition) { + return buildTagCondition(id, codec, ItemContext::of, condition); + } } diff --git a/station-flattening-v0/src/main/resources/station-flattening-v0.mixins.json b/station-flattening-v0/src/main/resources/station-flattening-v0.mixins.json index 5f9a3b5cc..328e3cd90 100644 --- a/station-flattening-v0/src/main/resources/station-flattening-v0.mixins.json +++ b/station-flattening-v0/src/main/resources/station-flattening-v0.mixins.json @@ -45,7 +45,6 @@ "client": [ "client.BlockRenderManagerMixin", "client.ClientWorldMixin", - "client.InGameHudMixin", "client.InteractionManagerMixin", "client.MinecraftMixin", "client.MultiplayerChunkCacheMixin", diff --git a/station-items-v0/src/main/java/net/modificationstation/stationapi/impl/tag/conditional/ItemTagConditionsImpl.java b/station-items-v0/src/main/java/net/modificationstation/stationapi/impl/tag/conditional/ItemTagConditionsImpl.java new file mode 100644 index 000000000..59e65e47c --- /dev/null +++ b/station-items-v0/src/main/java/net/modificationstation/stationapi/impl/tag/conditional/ItemTagConditionsImpl.java @@ -0,0 +1,39 @@ +package net.modificationstation.stationapi.impl.tag.conditional; + +import com.mojang.serialization.Codec; +import net.mine_diver.unsafeevents.listener.EventListener; +import net.modificationstation.stationapi.api.StationAPI; +import net.modificationstation.stationapi.api.event.registry.ItemRegistryEvent; +import net.modificationstation.stationapi.api.mod.entrypoint.Entrypoint; +import net.modificationstation.stationapi.api.mod.entrypoint.EntrypointManager; +import net.modificationstation.stationapi.api.mod.entrypoint.EventBusPolicy; + +import java.lang.invoke.MethodHandles; +import java.util.regex.Pattern; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +@Entrypoint(eventBus = @EventBusPolicy(registerInstance = false)) +@EventListener(phase = StationAPI.INTERNAL_PHASE) +public class ItemTagConditionsImpl { + static { + EntrypointManager.registerLookup(MethodHandles.lookup()); + } + + @EventListener + private static void registerConditions(ItemRegistryEvent event) { + event.registry + .buildItemTagCondition( + NAMESPACE.id("item_damage"), + Codec.INT.fieldOf("damage"), + (damage, ctx) -> ctx.hasDamage() && ctx.damage() == damage + ) + .shorthand( + Pattern.compile("@(\\d+)"), + dynamic -> dynamic.emptyMap().set( + "damage", dynamic.createInt(Integer.parseInt(dynamic.asString("0"))) + ) + ) + .register(); + } +} diff --git a/station-items-v0/src/main/java/net/modificationstation/stationapi/mixin/item/ItemStackMixin.java b/station-items-v0/src/main/java/net/modificationstation/stationapi/mixin/item/ItemStackMixin.java index 241358b12..835771db6 100644 --- a/station-items-v0/src/main/java/net/modificationstation/stationapi/mixin/item/ItemStackMixin.java +++ b/station-items-v0/src/main/java/net/modificationstation/stationapi/mixin/item/ItemStackMixin.java @@ -9,7 +9,11 @@ import net.modificationstation.stationapi.api.StationAPI; import net.modificationstation.stationapi.api.block.BlockState; import net.modificationstation.stationapi.api.event.item.ItemStackEvent; +import net.modificationstation.stationapi.api.item.StationFlatteningItemStack; import net.modificationstation.stationapi.api.item.StationItemStack; +import net.modificationstation.stationapi.api.item.context.ItemContext; +import net.modificationstation.stationapi.api.tag.TagKey; +import net.modificationstation.stationapi.api.tag.context.TagEvaluationContext; import net.modificationstation.stationapi.impl.item.StationNBTSetter; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -21,12 +25,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.Objects; +import java.util.stream.Stream; import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; import static net.modificationstation.stationapi.api.util.Identifier.of; @Mixin(ItemStack.class) -abstract class ItemStackMixin implements StationItemStack, StationNBTSetter { +abstract class ItemStackMixin implements StationItemStack, StationNBTSetter, StationFlatteningItemStack { @Shadow public int itemId; @@ -49,6 +54,16 @@ private void stationapi_onCreation(World arg, PlayerEntity arg1, CallbackInfo ci @Unique private NbtCompound stationapi_stationNbt = new NbtCompound(); + @Unique + private ItemContext stationapi_itemContext; + + @Unique + private ItemContext stationapi_getItemContext() { + if (stationapi_itemContext == null) + stationapi_itemContext = ItemContext.of((ItemStack) (Object) this); + return stationapi_itemContext; + } + @Inject( method = "split", at = @At("RETURN") @@ -168,4 +183,16 @@ public NbtCompound getStationNbt() { public void setStationNbt(NbtCompound stationNbt) { this.stationapi_stationNbt = stationNbt; } + + @Override + @Unique + public boolean isIn(TagKey tag, TagEvaluationContext context) { + return getRegistryEntry().isIn(tag, TagEvaluationContext.of(stationapi_getItemContext().with(context))); + } + + @Override + @Unique + public Stream> streamTags(TagEvaluationContext context) { + return getRegistryEntry().streamTags(TagEvaluationContext.of(stationapi_getItemContext().with(context))); + } } diff --git a/station-items-v0/src/main/resources/fabric.mod.json b/station-items-v0/src/main/resources/fabric.mod.json index b18a60b38..1cd47812f 100644 --- a/station-items-v0/src/main/resources/fabric.mod.json +++ b/station-items-v0/src/main/resources/fabric.mod.json @@ -20,6 +20,7 @@ "environment": "*", "entrypoints": { "stationapi:event_bus": [ + "net.modificationstation.stationapi.impl.tag.conditional.ItemTagConditionsImpl", "net.modificationstation.stationapi.impl.entity.player.ItemCustomReachImpl", "net.modificationstation.stationapi.impl.dispenser.CustomDispenseBehaviorImpl" ], diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/Registry.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/Registry.java index aab7aff2a..6db75db26 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/Registry.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/Registry.java @@ -2,20 +2,27 @@ import com.mojang.datafixers.DataFixUtils; import com.mojang.datafixers.util.Pair; +import com.mojang.datafixers.util.Unit; import com.mojang.serialization.*; import net.minecraft.block.Block; import net.minecraft.item.Item; import net.modificationstation.stationapi.api.tag.TagKey; +import net.modificationstation.stationapi.api.tag.TagMatchGroup; import net.modificationstation.stationapi.api.util.Identifier; import net.modificationstation.stationapi.api.util.Namespace; import net.modificationstation.stationapi.api.util.collection.IndexedIterable; +import net.modificationstation.stationapi.api.util.context.Condition; +import net.modificationstation.stationapi.api.util.context.ConditionType; +import net.modificationstation.stationapi.api.util.context.Context; import net.modificationstation.stationapi.api.util.dynamic.Codecs; import net.modificationstation.stationapi.api.util.function.BulkBiConsumer; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.ToIntFunction; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -436,7 +443,7 @@ default Iterable> iterateEntries(TagKey tag) { void clearTags(); - void populateTags(Map, List>> var1); + void populateTags(Map, Collection>>> var1); default IndexedIterable> getIndexedEntries() { return new IndexedIterable<>() { @@ -499,5 +506,55 @@ public RegistryEntryList.Named getOrThrow(TagKey tag) { } }; } -} + void registerTagCondition(ConditionType conditionType); + + default ConditionType.Builder buildTagCondition(Identifier id, Predicate condition) { + return ConditionType.builder(id, MapCodec.unit(Unit.INSTANCE), Function.identity(), (data, ctx) -> condition.test(ctx), this::registerTagCondition); + } + + /** + * Creates a builder for a tag condition with a context projection. + * + * @param id the condition ID + * @param projection the function to project the raw context into a specific view + * @param condition the condition to test against the projected view + * @param the type of the projected context + */ + default ConditionType.Builder buildTagCondition( + Identifier id, + Function projection, + Predicate condition + ) { + return ConditionType.builder(id, MapCodec.unit(Unit.INSTANCE), projection, (data, ctx) -> condition.test(ctx), this::registerTagCondition); + } + + default ConditionType.Builder buildTagCondition( + Identifier id, MapCodec codec, BiPredicate condition + ) { + return ConditionType.builder(id, codec, Function.identity(), condition, this::registerTagCondition); + } + + /** + * Creates a builder for a data-backed tag condition with a view context projection. + * + * @param id the condition ID + * @param codec the codec for the condition data + * @param projection the function to project the raw context into a specific view + * @param condition the condition to test against the data and projected view + * @param the type of the condition data + * @param the type of the projected context + */ + default ConditionType.Builder buildTagCondition( + Identifier id, + MapCodec codec, + Function projection, + BiPredicate condition + ) { + return ConditionType.builder(id, codec, projection, condition, this::registerTagCondition); + } + + Codec> getTagConditionCodec(); + + Iterable> getTagConditionTypes(); +} diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntry.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntry.java index 09d38f423..a08bdcfa3 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntry.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntry.java @@ -2,10 +2,12 @@ import com.mojang.datafixers.util.Either; import net.modificationstation.stationapi.api.tag.TagKey; +import net.modificationstation.stationapi.api.tag.context.TagEvaluationContext; import net.modificationstation.stationapi.api.util.Identifier; +import net.modificationstation.stationapi.api.util.context.Context; import org.jetbrains.annotations.Nullable; -import java.util.Collection; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -22,9 +24,23 @@ public interface RegistryEntry { boolean matches(Predicate> predicate); - boolean isIn(TagKey tag); + /** + * @deprecated Use {@link #isIn(TagKey, TagEvaluationContext)} instead. + *

This method implicitly uses {@link TagEvaluationContext#DEFAULT}, meaning it will only evaluate to {@code true} + * for unconditional tag references. + */ + @Deprecated + default boolean isIn(TagKey tag) { + return isIn(tag, TagEvaluationContext.DEFAULT); + } + + boolean isIn(TagKey tag, TagEvaluationContext context); + + default Stream> streamTags() { + return streamTags(TagEvaluationContext.BYPASSED); + } - Stream> streamTags(); + Stream> streamTags(TagEvaluationContext context); Set> getTags(); @@ -57,7 +73,7 @@ public boolean matchesKey(RegistryKey key) { } @Override - public boolean isIn(TagKey tag) { + public boolean isIn(TagKey tag, TagEvaluationContext context) { return false; } @@ -92,7 +108,7 @@ public boolean ownerEquals(RegistryEntryOwner owner) { } @Override - public Stream> streamTags() { + public Stream> streamTags(TagEvaluationContext context) { return Stream.of(); } @@ -104,7 +120,7 @@ public Set> getTags() { abstract class Reference implements RegistryEntry { final RegistryEntryOwner owner; - private Set> tags = Set.of(); + private volatile Map, Predicate> tags = Map.of(); private Reference(RegistryEntryOwner owner) { this.owner = owner; @@ -123,8 +139,11 @@ public boolean matchesKey(RegistryKey key) { } @Override - public boolean isIn(TagKey tag) { - return tags.contains(tag); + public boolean isIn(TagKey tag, TagEvaluationContext context) { + Predicate predicate = tags.get(tag); + if (predicate == null) return false; + if (context.ignoreTagConditions()) return true; + return predicate.test(context); } @Override @@ -156,18 +175,23 @@ public Type getType() { abstract void setValue(ENTRY value); - void setTags(Collection> tags) { - this.tags = Set.copyOf(tags); + void setTags(Map, Predicate> tags) { + this.tags = Map.copyOf(tags); } @Override - public Stream> streamTags() { - return this.tags.stream(); + public Stream> streamTags(TagEvaluationContext context) { + return context.ignoreTagConditions() + ? this.tags.keySet().stream() + : this.tags.entrySet().stream() + .filter(entry -> entry.getValue().test(context)) + .map(Map.Entry::getKey); } @Override + @Deprecated public Set> getTags() { - return tags; + return Set.copyOf(this.tags.keySet()); } public String toString() { diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntryList.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntryList.java index e6b401b18..7ac28b25a 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntryList.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/RegistryEntryList.java @@ -2,7 +2,10 @@ import com.mojang.datafixers.util.Either; import net.modificationstation.stationapi.api.tag.TagKey; +import net.modificationstation.stationapi.api.tag.TagMatchGroup; +import net.modificationstation.stationapi.api.tag.context.TagEvaluationContext; import net.modificationstation.stationapi.api.util.Util; +import net.modificationstation.stationapi.api.util.context.Condition; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; @@ -11,12 +14,12 @@ import java.util.stream.Stream; /** - * A registry entry list is an immutable list of registry entries. This, is either a direct + * A registry entry list is an immutable list of registry entries. This is either a direct * reference to each item, or a reference to a tag. A tag is a way * to dynamically define a list of registered values. Anything registered in a registry * can be tagged, and each registry holds a list of tags it recognizes. * - *

This can be iterated directly (i.e. {@code for (RegistryEntry entry : entries)}. + *

This can be iterated directly (i.e. {@code for (RegistryEntry entry : entries)}). * Note that this does not implement {@link java.util.Collection}. * * @see Registry @@ -24,14 +27,39 @@ */ public interface RegistryEntryList extends Iterable> { /** + * This method implicitly uses {@link TagEvaluationContext#BYPASSED}, meaning it will stream all registry entries + * in this list, bypassing all conditional checks. * {@return a stream of registry entries in this list} */ - Stream> stream(); + default Stream> stream() { + return stream(TagEvaluationContext.BYPASSED); + } /** + * {@return a stream of registry entries in this list, including match groups that pass the given context} + */ + Stream> stream(TagEvaluationContext context); + + /** + * {@return an iterable of registry entries in this list, including match groups that pass the given context} + */ + default Iterable> iterable(TagEvaluationContext context) { + return () -> stream(context).iterator(); + } + + /** + * This method implicitly uses {@link TagEvaluationContext#BYPASSED}, meaning it will count all registry entries + * in this list, bypassing all conditional checks. * {@return the number of entries in this list} */ - int size(); + default int size() { + return size(TagEvaluationContext.BYPASSED); + } + + /** + * {@return the number of entries in this list, including match groups that pass the given context} + */ + int size(TagEvaluationContext context); /** * {@return the object that identifies this registry entry list} @@ -41,21 +69,53 @@ public interface RegistryEntryList extends Iterable> { Either, List>> getStorage(); /** + * This method implicitly uses {@link TagEvaluationContext#BYPASSED}, meaning it may return any registry entry + * in this list, bypassing all conditional checks. * {@return a random entry of the list, or an empty optional if this list is empty} */ - Optional> getRandom(Random var1); + default Optional> getRandom(Random var1) { + return getRandom(var1, TagEvaluationContext.BYPASSED); + } /** + * {@return a random entry of the list matching the context, or an empty optional if this list is empty} + */ + Optional> getRandom(Random var1, TagEvaluationContext context); + + /** + * This method implicitly uses {@link TagEvaluationContext#BYPASSED}, meaning it operates on the flattened list of all registry entries + * in this list, bypassing all conditional checks. * {@return the registry entry at {@code index}} * * @throws IndexOutOfBoundsException if the index is out of bounds */ - RegistryEntry get(int var1); + default RegistryEntry get(int var1) { + return get(var1, TagEvaluationContext.BYPASSED); + } + + /** + * {@return the registry entry at {@code index} matching the context} + * + * @throws IndexOutOfBoundsException if the index is out of bounds + */ + RegistryEntry get(int var1, TagEvaluationContext context); /** * {@return whether {@code entry} is in this list} + * + * @deprecated Use {@link #contains(RegistryEntry, TagEvaluationContext)} instead. + *

This method implicitly uses {@link TagEvaluationContext#DEFAULT}, meaning it will only evaluate to {@code true} + * for unconditional tag references. + */ + @Deprecated + default boolean contains(RegistryEntry entry) { + return contains(entry, TagEvaluationContext.DEFAULT); + } + + /** + * {@return whether {@code entry} is in this list, evaluating match groups with the given context} */ - boolean contains(RegistryEntry var1); + boolean contains(RegistryEntry var1, TagEvaluationContext context); boolean ownerEquals(RegistryEntryOwner var1); @@ -100,15 +160,17 @@ static Direct of(Function> mapper, List values) class Named extends ListBacked { private final RegistryEntryOwner owner; private final TagKey tag; - private List> entries = List.of(); + private List> allEntries = List.of(); + private List>> matchGroups = List.of(); Named(RegistryEntryOwner owner, TagKey tag) { this.owner = owner; this.tag = tag; } - void copyOf(List> entries) { - this.entries = List.copyOf(entries); + void copyOf(Collection>> matchGroups) { + this.matchGroups = List.copyOf(matchGroups); + this.allEntries = this.matchGroups.stream().flatMap(c -> c.baseItems().stream()).distinct().toList(); } public TagKey getTag() { @@ -117,7 +179,26 @@ public TagKey getTag() { @Override protected List> getEntries() { - return this.entries; + return this.allEntries; + } + + @Override + public Stream> stream(TagEvaluationContext context) { + return context.ignoreTagConditions() + ? this.getEntries().stream() + : this.matchGroups.stream() + .filter(matchGroup -> { + for (Condition condition : matchGroup.conditions()) + if (!condition.test(context)) return false; + return true; + }) + .flatMap(matchGroup -> matchGroup.baseItems().stream()) + .distinct(); + } + + @Override + public int size(TagEvaluationContext context) { + return context.ignoreTagConditions() ? this.allEntries.size() : (int) this.stream(context).count(); } @Override @@ -131,12 +212,14 @@ public Optional> getTagKey() { } @Override - public boolean contains(RegistryEntry entry) { - return entry.isIn(this.tag); + public boolean contains(RegistryEntry entry, TagEvaluationContext context) { + return entry.isIn(this.tag, context); } public String toString() { - return "NamedSet(" + this.tag + ")[" + this.entries + "]"; + return "NamedSet(" + this.tag + ")[" + + this.allEntries + (matchGroups.isEmpty() ? "" : ", matchGroups=" + matchGroups.size()) + + "]"; } @Override @@ -170,7 +253,7 @@ public Optional> getTagKey() { } @Override - public boolean contains(RegistryEntry entry) { + public boolean contains(RegistryEntry entry, TagEvaluationContext context) { if (this.entrySet == null) this.entrySet = Set.copyOf(this.entries); return this.entrySet.contains(entry); } @@ -184,33 +267,34 @@ abstract class ListBacked implements RegistryEntryList { protected abstract List> getEntries(); @Override - public int size() { + public int size(TagEvaluationContext context) { return this.getEntries().size(); } @Override public Spliterator> spliterator() { - return this.getEntries().spliterator(); + return this.stream(TagEvaluationContext.BYPASSED).spliterator(); } @Override public Iterator> iterator() { - return this.getEntries().iterator(); + return this.stream(TagEvaluationContext.BYPASSED).iterator(); } @Override - public Stream> stream() { + public Stream> stream(TagEvaluationContext context) { return this.getEntries().stream(); } @Override - public Optional> getRandom(Random random) { - return Util.getRandomOrEmpty(this.getEntries(), random); + public Optional> getRandom(Random random, TagEvaluationContext context) { + return Util.getRandomOrEmpty(this.stream(context).toList(), random); } @Override - public RegistryEntry get(int index) { - return this.getEntries().get(index); + public RegistryEntry get(int index, TagEvaluationContext context) { + return this.stream(context).skip(index).findFirst() + .orElseThrow(() -> new IndexOutOfBoundsException("Index out of bounds: " + index)); } @Override @@ -219,4 +303,3 @@ public boolean ownerEquals(RegistryEntryOwner owner) { } } } - diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/SimpleRegistry.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/SimpleRegistry.java index c10e67742..9b10d0855 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/SimpleRegistry.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/registry/SimpleRegistry.java @@ -2,6 +2,7 @@ import com.google.common.collect.*; import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; import com.mojang.serialization.Lifecycle; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; @@ -15,14 +16,21 @@ import net.modificationstation.stationapi.api.registry.RegistryEntryList.Named; import net.modificationstation.stationapi.api.registry.RegistryWrapper.Impl; import net.modificationstation.stationapi.api.tag.TagKey; +import net.modificationstation.stationapi.api.tag.TagMatchGroup; import net.modificationstation.stationapi.api.util.Identifier; import net.modificationstation.stationapi.api.util.Util; +import net.modificationstation.stationapi.api.util.context.Condition; +import net.modificationstation.stationapi.api.util.context.ConditionType; +import net.modificationstation.stationapi.api.util.context.Context; import net.modificationstation.stationapi.impl.registry.sync.RemapStateImpl; import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; import java.util.*; import java.util.Map.Entry; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,6 +39,9 @@ import static net.modificationstation.stationapi.api.util.Namespace.MINECRAFT; public class SimpleRegistry implements MutableRegistry, RemappableRegistry, ListenableRegistry { + + + final RegistryKey> key; private final ReferenceList> rawIdToEntry = new ReferenceArrayList<>(256); private final Reference2IntMap entryToRawId = Util.make(new Reference2IntOpenHashMap<>(), map -> map.defaultReturnValue(-1)); @@ -40,6 +51,7 @@ public class SimpleRegistry implements MutableRegistry, RemappableRegistry private final Reference2ReferenceMap entryToLifecycle = new Reference2ReferenceOpenHashMap<>(); private Lifecycle lifecycle; private volatile Reference2ReferenceMap, Named> tagToEntryList = new Reference2ReferenceOpenHashMap<>(); + private final Reference2ReferenceMap> tagConditionTypes = new Reference2ReferenceOpenHashMap<>(); private boolean frozen; @Nullable private Reference2ReferenceMap> intrusiveValueToEntry; @@ -50,6 +62,17 @@ public class SimpleRegistry implements MutableRegistry, RemappableRegistry private Reference2IntMap prevIndexedEntries; private BiMap> prevEntries; + private final Codec> tagConditionCodec = Identifier.CODEC.dispatch( + "type", + condition -> condition.type().id(), + id -> { + ConditionType type = tagConditionTypes.get(id); + if (type == null) + throw new IllegalArgumentException("Unknown condition type: " + id + " in registry " + getKey()); + return type.conditionCodec(); + } + ); + private @Nullable MutableEventBus eventBus; public SimpleRegistry(RegistryKey> key, Lifecycle lifecycle) { @@ -315,7 +338,7 @@ public Lifecycle getLifecycle() { } @Override - public Iterator iterator() { + public @NotNull Iterator iterator() { return Iterators.transform(this.getEntries().iterator(), RegistryEntry::value); } @@ -449,28 +472,58 @@ public Optional> getEntryList(TagKey tag) { } @Override - public void populateTags(Map, List>> tagEntries) { - Map, List>> map = new IdentityHashMap<>(); - keyToEntry.values().forEach(entry -> map.put(entry, new ArrayList<>())); - tagEntries.forEach((tag, entries) -> { - - for (RegistryEntry entry : entries) { - if (!entry.ownerEquals(getReadOnlyWrapper())) - throw new IllegalStateException("Can't create named set " + tag + " containing value " + entry + " from outside registry " + this); - - if (!(entry instanceof Reference reference)) - throw new IllegalStateException("Found direct holder " + entry + " value in tag " + tag); - - map.get(reference).add(tag); + public void populateTags(@UnknownNullability Map, Collection>>> tagEntries) { + Map, Map, Predicate>> map = new IdentityHashMap<>(); + + keyToEntry.values().forEach(entry -> map.put( + entry, new Reference2ReferenceOpenHashMap<>()) + ); + + tagEntries.forEach((tag, resolvedTag) -> { + for (TagMatchGroup> matchGroup : resolvedTag) { + boolean isUnconditional = matchGroup.conditions().isEmpty(); + Predicate predicate = isUnconditional + ? ctx -> true + : ctx -> { + for (Condition condition : matchGroup.conditions()) + if (!condition.test(ctx)) return false; + return true; + }; + + for (RegistryEntry entry : matchGroup.baseItems()) { + if (!entry.ownerEquals(getReadOnlyWrapper())) + throw new IllegalStateException( + "Can't create named set " + tag + " containing value " + + entry + " from outside registry " + this + ); + + if (!(entry instanceof Reference reference)) + throw new IllegalStateException( + "Found direct holder " + entry + " value in tag " + tag + ); + + Map, Predicate> tags = map.get(reference); + if (matchGroup.remove()) { + Predicate oldPred = tags.get(tag); + if (oldPred != null) if (isUnconditional) tags.remove(tag); + else tags.put(tag, oldPred.and(predicate.negate())); + } else if (isUnconditional) tags.put(tag, ctx -> true); + else tags.compute(tag, (k, oldPred) -> oldPred == null ? predicate : oldPred.or(predicate)); + } } - }); Set> set = Sets.difference(this.tagToEntryList.keySet(), tagEntries.keySet()); if (!set.isEmpty()) LOGGER.warn("Not all defined tags for registry {} are present in data pack: {}", this.getKey(), set.stream().map(tag -> tag.id().toString()).sorted().collect(Collectors.joining(", "))); Reference2ReferenceMap, Named> map2 = new Reference2ReferenceOpenHashMap<>(this.tagToEntryList); - tagEntries.forEach((tag, entries) -> map2.computeIfAbsent(tag, this::createNamedEntryList).copyOf(entries)); + + tagEntries.forEach( + (tag, resolvedTag) -> map2.computeIfAbsent( + tag, this::createNamedEntryList + ).copyOf(resolvedTag) + ); + map.forEach(Reference::setTags); this.tagToEntryList = map2; } @@ -478,7 +531,7 @@ public void populateTags(Map, List>> tagEntries) { @Override public void clearTags() { this.tagToEntryList.values().forEach(entryList -> entryList.copyOf(List.of())); - this.keyToEntry.values().forEach(entry -> entry.setTags(Set.of())); + this.keyToEntry.values().forEach(entry -> entry.setTags(Map.of())); } @Override @@ -685,4 +738,19 @@ public void unmap(String name) throws RemapException { prevEntries = null; } } + + @Override + public void registerTagCondition(ConditionType conditionType) { + tagConditionTypes.put(conditionType.id(), conditionType); + } + + @Override + public Codec> getTagConditionCodec() { + return tagConditionCodec; + } + + @Override + public Iterable> getTagConditionTypes() { + return Collections.unmodifiableCollection(tagConditionTypes.values()); + } } diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagEntry.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagEntry.java index f749bac16..f34cbf618 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagEntry.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagEntry.java @@ -1,33 +1,111 @@ package net.modificationstation.stationapi.api.tag; 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.Dynamic; +import com.mojang.serialization.JavaOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.modificationstation.stationapi.api.util.Identifier; +import net.modificationstation.stationapi.api.util.context.Condition; +import net.modificationstation.stationapi.api.util.context.ConditionType; import net.modificationstation.stationapi.api.util.dynamic.Codecs; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class TagEntry { - private static final Codec ENTRY_CODEC = RecordCodecBuilder.create(instance -> instance.group(Codecs.TAG_ENTRY_ID.fieldOf("id").forGetter(TagEntry::getIdForCodec), Codec.BOOL.optionalFieldOf("required", true).forGetter(entry -> entry.required)).apply(instance, TagEntry::new)); - public static final Codec CODEC = Codec.either(Codecs.TAG_ENTRY_ID, ENTRY_CODEC).xmap(either -> either.map(id -> new TagEntry(id, true), tagEntry -> tagEntry), entry -> entry.required ? Either.left(entry.getIdForCodec()) : Either.right(entry)); + public static Codec createCodec(Codec> tagConditionCodec, Iterable> tagConditionTypes) { + return Codec.either( + Codec.STRING.comapFlatMap( + s -> { + String path = s; + List> conditions = new ArrayList<>(); + boolean matched; + do { + matched = false; + for (ConditionType type : tagConditionTypes) { + Optional>> result = captureAndParse(type, path); + if (result.isPresent()) { + path = result.get().getFirst(); + conditions.add(result.get().getSecond()); + matched = true; + break; + } + } + } while (matched); + return Codecs.TAG_ENTRY_ID.parse(JavaOps.INSTANCE, path) + .map(id -> new TagEntry(id, true, conditions)); + }, + entry -> entry.getIdForCodec().toString() + ), + RecordCodecBuilder.create( + instance -> instance.group( + Codecs.TAG_ENTRY_ID.fieldOf("id") + .forGetter(TagEntry::getIdForCodec), + Codec.BOOL.optionalFieldOf("required", true) + .forGetter(entry -> entry.required), + tagConditionCodec.listOf().optionalFieldOf("conditions", List.of()) + .forGetter(entry -> entry.conditions) + ).apply(instance, TagEntry::new) + ) + ).xmap( + Either::unwrap, + entry -> entry.required && entry.conditions.isEmpty() + ? Either.left(entry) + : Either.right(entry) + ); + } + + private static Optional>> captureAndParse(ConditionType type, String path) { + Pattern pattern = type.shorthandPattern(); + if (pattern == null) return Optional.empty(); + + Matcher matcher = pattern.matcher(path); + if (!matcher.find()) return Optional.empty(); + + String remaining = path.substring(0, matcher.start()) + path.substring(matcher.end()); + String extracted = matcher.group(1); + + Dynamic dynamic = new Dynamic<>(JavaOps.INSTANCE, extracted); + Dynamic unfolded = type.unfolder().apply(dynamic); + + DataResult result = type.dataCodec().codec().parse(unfolded); + return result.result().map(data -> Pair.of(remaining, new Condition<>(type, data))); + } + private final Identifier id; private final boolean tag; private final boolean required; + private final List> conditions; private TagEntry(Identifier id, boolean tag, boolean required) { this.id = id; this.tag = tag; this.required = required; + this.conditions = List.of(); } private TagEntry(Codecs.TagEntryId id, boolean required) { this.id = id.id(); - this.tag = id.tag(); + tag = id.tag(); + this.required = required; + conditions = List.of(); + } + + private TagEntry(Codecs.TagEntryId id, boolean required, List> conditions) { + this.id = id.id(); + tag = id.tag(); this.required = required; + this.conditions = List.copyOf(conditions); } private Codecs.TagEntryId getIdForCodec() { @@ -50,19 +128,27 @@ public static TagEntry createOptionalTag(Identifier id) { return new TagEntry(id, true, false); } - public boolean resolve(ValueGetter valueGetter, Consumer consumer) { + public boolean resolve( + ValueGetter getter, + Consumer> matchGroupConsumer + ) { if (this.tag) { - Collection collection = valueGetter.tag(this.id); - if (collection == null) { - return !this.required; + Collection> refTag = getter.tag(this.id); + if (refTag == null) return !this.required; + + for (TagMatchGroup refMatchGroup : refTag) { + List> mergedConditions = new ArrayList<>(refMatchGroup.conditions()); + + if (!this.conditions.isEmpty()) mergedConditions.addAll(this.conditions); + + matchGroupConsumer.accept(new TagMatchGroup<>(refMatchGroup.baseItems(), mergedConditions, false)); } - collection.forEach(consumer); + } else { - T object = valueGetter.direct(this.id); - if (object == null) { - return !this.required; - } - consumer.accept(object); + T value = getter.direct(this.id); + if (value == null) return !this.required; + + matchGroupConsumer.accept(new TagMatchGroup<>(List.of(value), this.conditions, false)); } return true; } @@ -96,9 +182,9 @@ public String toString() { } public interface ValueGetter { - @Nullable T direct(Identifier var1); + @Nullable T direct(Identifier id); - @Nullable Collection tag(Identifier var1); + @Nullable Collection> tag(Identifier id); } } diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagFile.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagFile.java index 6deaadd25..2b747172a 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagFile.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagFile.java @@ -2,10 +2,23 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.modificationstation.stationapi.api.util.context.Condition; +import net.modificationstation.stationapi.api.util.context.ConditionType; import java.util.List; -public record TagFile(List entries, boolean replace) { - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group(TagEntry.CODEC.listOf().fieldOf("values").forGetter(TagFile::entries), Codec.BOOL.optionalFieldOf("replace", false).forGetter(TagFile::replace)).apply(instance, TagFile::new)); +public record TagFile(List entries, List remove, boolean replace) { + public static Codec createCodec(Codec> tagConditionCodec, Iterable> tagConditionTypes) { + return RecordCodecBuilder.create( + instance -> instance.group( + TagEntry.createCodec(tagConditionCodec, tagConditionTypes).listOf().optionalFieldOf("values", List.of()) + .forGetter(TagFile::entries), + TagEntry.createCodec(tagConditionCodec, tagConditionTypes).listOf().optionalFieldOf("remove", List.of()) + .forGetter(TagFile::remove), + Codec.BOOL.optionalFieldOf("replace", false) + .forGetter(TagFile::replace) + ).apply(instance, TagFile::new) + ); + } } diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagGroupLoader.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagGroupLoader.java index a7bf131c5..bd30091ae 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagGroupLoader.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagGroupLoader.java @@ -9,14 +9,15 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.mojang.datafixers.util.Either; -import com.mojang.serialization.DataResult; +import com.mojang.serialization.Codec; import com.mojang.serialization.Dynamic; import com.mojang.serialization.JsonOps; import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; -import net.modificationstation.stationapi.api.util.Identifier; import net.modificationstation.stationapi.api.resource.Resource; import net.modificationstation.stationapi.api.resource.ResourceManager; +import net.modificationstation.stationapi.api.util.Identifier; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.Reader; @@ -33,9 +34,12 @@ public class TagGroupLoader { final Function> registryGetter; private final String dataType; - public TagGroupLoader(Function> registryGetter, String dataType) { + private final Codec tagFileCodec; + + public TagGroupLoader(Function> registryGetter, String dataType, Codec tagFileCodec) { this.registryGetter = registryGetter; this.dataType = dataType; + this.tagFileCodec = tagFileCodec; } public Map> loadTags(ResourceManager manager) { @@ -53,12 +57,12 @@ public Map> loadTags(ResourceManager manager) { try { JsonElement jsonElement = JsonParser.parseReader(reader); List list = map.computeIfAbsent(identifier2, identifierx -> new ArrayList<>()); - DataResult var10000 = TagFile.CODEC.parse(new Dynamic<>(JsonOps.INSTANCE, jsonElement)); - TagFile tagFile = var10000.getOrThrow(); + TagFile tagFile = tagFileCodec.parse(new Dynamic<>(JsonOps.INSTANCE, jsonElement)).getOrThrow(); if (tagFile.replace()) list.clear(); String string2 = resource.getResourcePackName(); - tagFile.entries().forEach(tagEntry -> list.add(new TrackedEntry(tagEntry, string2))); + tagFile.entries().forEach(tagEntry -> list.add(new TrackedEntry(tagEntry, false, string2))); + tagFile.remove().forEach(tagEntry -> list.add(new TrackedEntry(tagEntry, true, string2))); } catch (Throwable var16) { if (reader != null) try { reader.close(); @@ -71,7 +75,10 @@ public Map> loadTags(ResourceManager manager) { reader.close(); } catch (Exception var17) { - LOGGER.error("Couldn't read tag list {} from {}"/* in data pack {}"*/, new Object[]{identifier2, identifier/*, resource.getResourcePackName()*/, var17}); + LOGGER.error( + "Couldn't read tag list {} from {} in data pack {}", + identifier2, identifier, resource.getResourcePackName(), var17 + ); } } @@ -94,24 +101,23 @@ private static boolean hasCircularDependency(Multimap mu private static void addReference(Multimap multimap, Identifier identifier, Identifier identifier2) { if (!hasCircularDependency(multimap, identifier, identifier2)) multimap.put(identifier, identifier2); - } - private Either, Collection> resolveAll(TagEntry.ValueGetter valueGetter, List list) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - List list2 = new ArrayList<>(); + private Either, Collection>> resolveAll(TagEntry.ValueGetter valueGetter, List tags) { + ImmutableSet.Builder> matchGroupBuilder = ImmutableSet.builder(); + List missing = new ArrayList<>(); - for (TrackedEntry trackedEntry : list) { - TagEntry var10000 = trackedEntry.entry(); - Objects.requireNonNull(builder); - if (!var10000.resolve(valueGetter, builder::add)) list2.add(trackedEntry); - } + for (TrackedEntry tag : tags) + if (!tag.entry().resolve(valueGetter, matchGroup -> matchGroupBuilder.add(new TagMatchGroup<>(matchGroup.baseItems(), matchGroup.conditions(), tag.remove())))) + missing.add(tag); - return list2.isEmpty() ? Either.right(builder.build()) : Either.left(list2); + return missing.isEmpty() + ? Either.right(matchGroupBuilder.build()) + : Either.left(missing); } - public Map> buildGroup(Map> map) { - final Map> map2 = Maps.newHashMap(); + public Map>> buildGroup(Map> map) { + final Map>> map2 = Maps.newHashMap(); TagEntry.ValueGetter valueGetter = new TagEntry.ValueGetter<>() { @Nullable public T direct(Identifier id) { @@ -119,26 +125,52 @@ public T direct(Identifier id) { } @Nullable - public Collection tag(Identifier id) { + public Collection> tag(Identifier id) { return map2.get(id); } }; Multimap multimap = HashMultimap.create(); - map.forEach((identifier, list) -> list.forEach(trackedEntry -> trackedEntry.entry.forEachRequiredTagId(identifier2 -> addReference(multimap, identifier, identifier2)))); - map.forEach((identifier, list) -> list.forEach(trackedEntry -> trackedEntry.entry.forEachOptionalTagId(identifier2 -> addReference(multimap, identifier, identifier2)))); + map.forEach( + (identifier, list) -> list.forEach( + trackedEntry -> trackedEntry.entry.forEachRequiredTagId( + identifier2 -> addReference(multimap, identifier, identifier2 + ) + ) + ) + ); + map.forEach( + (identifier, list) -> list.forEach( + trackedEntry -> trackedEntry.entry.forEachOptionalTagId( + identifier2 -> addReference(multimap, identifier, identifier2) + ) + ) + ); Set set = Sets.newHashSet(); - map.keySet().forEach(identifier -> resolveAll(map, multimap, set, identifier, (identifierx, list) -> this.resolveAll(valueGetter, list).ifLeft(collection -> LOGGER.error("Couldn't load tag {} as it is missing following references: {}", identifierx, collection.stream().map(Objects::toString).collect(Collectors.joining(", ")))).ifRight(collection -> map2.put(identifierx, collection)))); + map.keySet().forEach(identifier -> resolveAll( + map, multimap, set, identifier, (identifierx, list) -> this.resolveAll( + valueGetter, list + ).ifLeft( + collection -> LOGGER.error( + "Couldn't load tag {} as it is missing following references: {}", + identifierx, + collection.stream() + .map(Objects::toString) + .collect(Collectors.joining(", ")) + ) + ).ifRight( + collection -> map2.put(identifierx, collection) + ) + )); return map2; } - public Map> load(ResourceManager manager) { + public Map>> load(ResourceManager manager) { return this.buildGroup(this.loadTags(manager)); } - public record TrackedEntry(TagEntry entry, String source) { - + public record TrackedEntry(TagEntry entry, boolean remove, String source) { @Override - public String toString() { + public @NotNull String toString() { return this.entry.toString() + " (from " + this.source + ")"; } } diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagManagerLoader.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagManagerLoader.java index 7313e6674..559178a1d 100644 --- a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagManagerLoader.java +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagManagerLoader.java @@ -1,7 +1,10 @@ package net.modificationstation.stationapi.api.tag; import lombok.val; -import net.modificationstation.stationapi.api.registry.*; +import net.modificationstation.stationapi.api.registry.DynamicRegistryManager; +import net.modificationstation.stationapi.api.registry.Registry; +import net.modificationstation.stationapi.api.registry.RegistryEntry; +import net.modificationstation.stationapi.api.registry.RegistryKey; import net.modificationstation.stationapi.api.resource.IdentifiableResourceReloadListener; import net.modificationstation.stationapi.api.resource.ResourceManager; import net.modificationstation.stationapi.api.resource.ResourceReloader; @@ -63,14 +66,18 @@ public CompletableFuture reload( private static void repopulateTags(DynamicRegistryManager dynamicRegistryManager, TagManagerLoader.RegistryTags tags) { RegistryKey> registryKey = tags.key(); - Map, List>> map = tags.tags().entrySet().stream().collect(Collectors.toUnmodifiableMap(entry -> TagKey.of(registryKey, entry.getKey()), entry -> List.copyOf(entry.getValue()))); + Map, Collection>>> map = tags.tags().entrySet().stream().collect(Collectors.toUnmodifiableMap(entry -> TagKey.of(registryKey, entry.getKey()), entry -> entry.getValue())); dynamicRegistryManager.get(registryKey).populateTags(map); } private CompletableFuture> buildRequiredGroup(ResourceManager resourceManager, Executor prepareExecutor, DynamicRegistryManager.Entry requirement) { RegistryKey> registryKey = requirement.key(); Registry registry = requirement.value(); - TagGroupLoader> tagGroupLoader = new TagGroupLoader<>(id -> registry.getEntry(RegistryKey.of(registryKey, id)), getPath(registryKey)); + TagGroupLoader> tagGroupLoader = new TagGroupLoader<>( + id -> registry.getEntry(RegistryKey.of(registryKey, id)), + getPath(registryKey), + TagFile.createCodec(registry.getTagConditionCodec(), registry.getTagConditionTypes()) + ); return CompletableFuture.supplyAsync(() -> new RegistryTags<>(registryKey, tagGroupLoader.load(resourceManager)), prepareExecutor); } @@ -79,6 +86,6 @@ public Identifier getId() { return TAGS; } - public record RegistryTags(RegistryKey> key, Map>> tags) { } + public record RegistryTags(RegistryKey> key, Map>>> tags) { } } diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagMatchGroup.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagMatchGroup.java new file mode 100644 index 000000000..c0305db87 --- /dev/null +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/TagMatchGroup.java @@ -0,0 +1,11 @@ +package net.modificationstation.stationapi.api.tag; + +import net.modificationstation.stationapi.api.util.context.Condition; + +import java.util.Collection; + +public record TagMatchGroup( + Collection baseItems, + Collection> conditions, + boolean remove +) {} diff --git a/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/context/TagEvaluationContext.java b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/context/TagEvaluationContext.java new file mode 100644 index 000000000..d9a1b01f1 --- /dev/null +++ b/station-registry-api-v0/src/main/java/net/modificationstation/stationapi/api/tag/context/TagEvaluationContext.java @@ -0,0 +1,27 @@ +package net.modificationstation.stationapi.api.tag.context; + +import net.modificationstation.stationapi.api.util.context.Context; + +import static net.modificationstation.stationapi.api.StationAPI.NAMESPACE; + +@FunctionalInterface +public interface TagEvaluationContext extends Context { + Context.Key IGNORE_TAG_CONDITIONS_KEY = new Context.Key<>(NAMESPACE.id("ignore_tag_conditions")); + + TagEvaluationContext DEFAULT = of(Context.EMPTY); + TagEvaluationContext BYPASSED = of(Context.EMPTY.with(IGNORE_TAG_CONDITIONS_KEY, true)); + + default boolean ignoreTagConditions() { + return Boolean.TRUE.equals(get(IGNORE_TAG_CONDITIONS_KEY)); + } + + static TagEvaluationContext of(boolean ignoreTagConditions) { + return ignoreTagConditions ? BYPASSED : DEFAULT; + } + + static TagEvaluationContext of(Context context) { + if (context instanceof TagEvaluationContext t) return t; + interface TagEvaluationContextDelegate extends TagEvaluationContext, Delegate {} + return (TagEvaluationContextDelegate) () -> context; + } +} diff --git a/station-resource-loader-v0/src/main/java/net/modificationstation/stationapi/impl/resource/ModResourcePackUtil.java b/station-resource-loader-v0/src/main/java/net/modificationstation/stationapi/impl/resource/ModResourcePackUtil.java index ae3fbf603..aa168c807 100644 --- a/station-resource-loader-v0/src/main/java/net/modificationstation/stationapi/impl/resource/ModResourcePackUtil.java +++ b/station-resource-loader-v0/src/main/java/net/modificationstation/stationapi/impl/resource/ModResourcePackUtil.java @@ -11,14 +11,14 @@ import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.api.metadata.ModMetadata; +import net.modificationstation.stationapi.api.StationAPI; import net.modificationstation.stationapi.api.resource.ResourcePackActivationType; import net.modificationstation.stationapi.api.resource.ResourceType; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.Nullable; import java.io.InputStream; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -59,16 +59,66 @@ public static void appendModResourcePacks(List packs, ResourceT packs.add(pack); } } - packs.sort((pack1, pack2) -> { - String id1 = pack1.getFabricModMetadata().getId(); - String id2 = pack2.getFabricModMetadata().getId(); - ObjectSet s; - return - lowerThan.containsKey(id1) && ((s = lowerThan.get(id1)).contains("*") || s.contains(id2)) || - higherThan.containsKey(id2) && ((s = higherThan.get(id2)).contains("*") || s.contains(id1)) ? -1 : - higherThan.containsKey(id1) && ((s = higherThan.get(id1)).contains("*") || s.contains(id2)) || - lowerThan.containsKey(id2) && ((s = lowerThan.get(id2)).contains("*") || s.contains(id1)) ? 1 : 0; - }); + Map> edges = new HashMap<>(); + Map inDegree = new HashMap<>(); + for (ModResourcePack p : packs) { + edges.put(p, new HashSet<>()); + inDegree.put(p, 0); + } + + for (ModResourcePack p1 : packs) { + String id1 = p1.getFabricModMetadata().getId(); + for (ModResourcePack p2 : packs) { + if (p1 == p2) continue; + String id2 = p2.getFabricModMetadata().getId(); + + boolean p1BeforeP2 = false; + ObjectSet s; + if (lowerThan.containsKey(id1) && ((s = lowerThan.get(id1)).contains("*") || s.contains(id2))) { + p1BeforeP2 = true; + } + if (higherThan.containsKey(id2) && ((s = higherThan.get(id2)).contains("*") || s.contains(id1))) { + p1BeforeP2 = true; + } + + if (p1BeforeP2) { + if (edges.get(p1).add(p2)) { + inDegree.put(p2, inDegree.get(p2) + 1); + } + } + } + } + + List sorted = new ArrayList<>(); + Queue queue = new LinkedList<>(); + for (ModResourcePack p : packs) { + if (inDegree.get(p) == 0) queue.add(p); + } + + while (!queue.isEmpty()) { + ModResourcePack current = queue.poll(); + sorted.add(current); + for (ModResourcePack neighbor : edges.get(current)) { + int deg = inDegree.get(neighbor) - 1; + inDegree.put(neighbor, deg); + if (deg == 0) { + queue.add(neighbor); + } + } + } + + List cyclicPacks = packs.stream() + .filter(p -> inDegree.get(p) > 0) + .peek(sorted::add) + .map(ModResourcePack::getFabricModMetadata) + .map(ModMetadata::getId) + .toList(); + if (!cyclicPacks.isEmpty()) StationAPI.LOGGER.warn( + "Resource pack priority cycle detected for {}! Order is not guaranteed.", cyclicPacks + ); + + packs.clear(); + packs.addAll(sorted); } private static void updatePriorities(Object2ReferenceMap> setToUpdate, String id, CustomValue.CvObject priority, String keyToUpdate) { diff --git a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/mixin/flattening/client/InGameHudMixin.java b/station-vanilla-fix-v0/src/main/java/net/modificationstation/stationapi/mixin/vanillafix/client/InGameHudMixin.java similarity index 92% rename from station-flattening-v0/src/main/java/net/modificationstation/stationapi/mixin/flattening/client/InGameHudMixin.java rename to station-vanilla-fix-v0/src/main/java/net/modificationstation/stationapi/mixin/vanillafix/client/InGameHudMixin.java index ea908cd99..3ec86b2c0 100644 --- a/station-flattening-v0/src/main/java/net/modificationstation/stationapi/mixin/flattening/client/InGameHudMixin.java +++ b/station-vanilla-fix-v0/src/main/java/net/modificationstation/stationapi/mixin/vanillafix/client/InGameHudMixin.java @@ -1,4 +1,4 @@ -package net.modificationstation.stationapi.mixin.flattening.client; +package net.modificationstation.stationapi.mixin.vanillafix.client; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.block.Block; @@ -11,6 +11,7 @@ import net.minecraft.util.hit.HitResult; import net.minecraft.util.hit.HitResultType; import net.modificationstation.stationapi.api.block.BlockState; +import net.modificationstation.stationapi.api.block.context.BlockTagContext; import net.modificationstation.stationapi.api.state.property.Property; import net.modificationstation.stationapi.api.tag.TagKey; import org.spongepowered.asm.mixin.Mixin; @@ -59,7 +60,9 @@ private void stationapi_renderHud(float bl, boolean i, int j, int par4, Callback } } - Collection> tags = state.streamTags().toList(); + Collection> tags = state.streamTags( + BlockTagContext.of(minecraft.world, hit.blockX, hit.blockY, hit.blockZ) + ).toList(); if (!tags.isEmpty()) { text = "Tags:"; drawTextWithShadow(var8, text, var6 - var8.getWidth(text) - 2, offset += 10, 16777215); diff --git a/station-vanilla-fix-v0/src/main/resources/fabric.mod.json b/station-vanilla-fix-v0/src/main/resources/fabric.mod.json index dffd34acd..186bdfcfe 100644 --- a/station-vanilla-fix-v0/src/main/resources/fabric.mod.json +++ b/station-vanilla-fix-v0/src/main/resources/fabric.mod.json @@ -46,6 +46,9 @@ "modmenu:api": true, "station-resource-loader-v0:assets_priority": { "lowerThan": "*" + }, + "station-resource-loader-v0:data_priority": { + "lowerThan": "*" } } } diff --git a/station-vanilla-fix-v0/src/main/resources/station-vanilla-fix-v0.mixins.json b/station-vanilla-fix-v0/src/main/resources/station-vanilla-fix-v0.mixins.json index e6f62fff3..f50be035f 100644 --- a/station-vanilla-fix-v0/src/main/resources/station-vanilla-fix-v0.mixins.json +++ b/station-vanilla-fix-v0/src/main/resources/station-vanilla-fix-v0.mixins.json @@ -13,6 +13,7 @@ "server": [ ], "client": [ + "client.InGameHudMixin", "client.MinecraftMixin", "client.ScreenAccessor", "client.SelectWorldScreenAccessor", diff --git a/station-worldgen-api-v0/src/main/java/net/modificationstation/stationapi/api/worldgen/surface/condition/TagSurfaceCondition.java b/station-worldgen-api-v0/src/main/java/net/modificationstation/stationapi/api/worldgen/surface/condition/TagSurfaceCondition.java index 9b81da372..ec601d2c6 100644 --- a/station-worldgen-api-v0/src/main/java/net/modificationstation/stationapi/api/worldgen/surface/condition/TagSurfaceCondition.java +++ b/station-worldgen-api-v0/src/main/java/net/modificationstation/stationapi/api/worldgen/surface/condition/TagSurfaceCondition.java @@ -3,6 +3,7 @@ import net.minecraft.block.Block; import net.minecraft.world.World; import net.modificationstation.stationapi.api.block.BlockState; +import net.modificationstation.stationapi.api.block.context.BlockTagContext; import net.modificationstation.stationapi.api.tag.TagKey; public class TagSurfaceCondition implements SurfaceCondition { @@ -14,6 +15,6 @@ public TagSurfaceCondition(TagKey tag) { @Override public boolean canApply(World world, int x, int y, int z, BlockState state) { - return state.isIn(tag); + return state.isIn(tag, BlockTagContext.of(world, x, y, z)); } }