diff --git a/src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java b/src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java index 48a6497..85e79d3 100644 --- a/src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java +++ b/src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java @@ -6,14 +6,23 @@ import com.google.gson.reflect.TypeToken; import io.wispforest.lavender.Lavender; import io.wispforest.lavender.client.LavenderClient; +import io.wispforest.lavender.client.StructureOverlayRenderer; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.util.BlockRotation; import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; public class LavenderClientStorage { @@ -23,6 +32,9 @@ public class LavenderClientStorage { private static final TypeToken>> OPENED_BOOKS_TYPE = new TypeToken<>() {}; private static Map> openedBooks; + private static final TypeToken>> ACTIVE_STRUCTURES_TYPE = new TypeToken<>() {}; + private static Map> activeStructures; + private static final Gson GSON = new GsonBuilder().registerTypeAdapter(Identifier.class, new Identifier.Serializer()).setPrettyPrinting().create(); static { @@ -31,9 +43,11 @@ public class LavenderClientStorage { bookmarks = GSON.fromJson(data.get("bookmarks"), BOOKMARKS_TYPE); openedBooks = GSON.fromJson(data.get("opened_books"), OPENED_BOOKS_TYPE); + activeStructures = GSON.fromJson(data.get("active_structures"), ACTIVE_STRUCTURES_TYPE); } catch (Exception e) { bookmarks = new HashMap<>(); openedBooks = new HashMap<>(); + activeStructures = new HashMap<>(); save(); } } @@ -78,11 +92,29 @@ private static Set getOpenedBooksSet() { return openedBooks.computeIfAbsent(LavenderClient.currentWorldId(), $ -> new HashSet<>()); } + public static List getActiveStructures() { + return activeStructures.computeIfAbsent(LavenderClient.currentWorldId(), $ -> new ArrayList<>()); + } + + public static void setActiveStructures(Map activeStructures) { + getActiveStructures().clear(); + getActiveStructures().addAll(activeStructures.entrySet() + .stream() + .map((entry) -> { + var pos = entry.getKey(); + var overlay = entry.getValue(); + return new ActiveStructureOverlay(pos, overlay.structureId, overlay.rotation, overlay.visibleLayer); + }) + .toList()); + save(); + } + private static void save() { try { var data = new JsonObject(); data.add("bookmarks", GSON.toJsonTree(bookmarks, BOOKMARKS_TYPE.getType())); data.add("opened_books", GSON.toJsonTree(openedBooks, OPENED_BOOKS_TYPE.getType())); + data.add("active_structure", GSON.toJsonTree(activeStructures, ACTIVE_STRUCTURES_TYPE.getType())); Files.writeString(storageFile(), GSON.toJson(data)); } catch (IOException e) { @@ -106,4 +138,9 @@ public enum Type { }; } } -} \ No newline at end of file + + public record ActiveStructureOverlay(BlockPos pos, Identifier id, BlockRotation rotation, int visibleLayer) { + + } +} + diff --git a/src/main/java/io/wispforest/lavender/book/StructureComponent.java b/src/main/java/io/wispforest/lavender/book/StructureComponent.java index c66fd19..7e2bbb6 100644 --- a/src/main/java/io/wispforest/lavender/book/StructureComponent.java +++ b/src/main/java/io/wispforest/lavender/book/StructureComponent.java @@ -36,7 +36,7 @@ public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partial var entityBuffers = client.getBufferBuilders().getEntityVertexConsumers(); float scale = Math.min(this.width, this.height); - scale /= Math.max(structure.xSize, Math.max(structure.ySize, structure.zSize)); + scale /= Math.max(this.structure.xSize(), Math.max(this.structure.ySize(), this.structure.zSize())); scale /= 1.625f; var matrices = context.getMatrices(); @@ -47,7 +47,7 @@ public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partial matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(this.displayAngle)); matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees((float) (System.currentTimeMillis() / 75d % 360d))); - matrices.translate(this.structure.xSize / -2f, this.structure.ySize / -2f, this.structure.zSize / -2f); + matrices.translate(this.structure.xSize() / -2.0f, this.structure.ySize() / -2.0f, this.structure.zSize() / -2.0f); structure.forEachPredicate((blockPos, predicate) -> { if (this.visibleLayer != -1 && this.visibleLayer != blockPos.getY()) return; @@ -70,7 +70,7 @@ public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partial entityBuffers.draw(); DiffuseLighting.enableGuiDepthLighting(); - if (StructureOverlayRenderer.isShowingOverlay(this.structure.id)) { + if (StructureOverlayRenderer.isShowingOverlay(this.structure.id())) { context.drawText(client.textRenderer, Text.translatable("text.lavender.structure_component.active_overlay_hint"), this.x + this.width - 5 - client.textRenderer.getWidth("⚓"), this.y + this.height - 9 - 5, 0, false); this.tooltip(Text.translatable("text.lavender.structure_component.hide_hint")); } else { @@ -83,11 +83,11 @@ public boolean onMouseDown(double mouseX, double mouseY, int button) { var result = super.onMouseDown(mouseX, mouseY, button); if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT) return result; - if (StructureOverlayRenderer.isShowingOverlay(this.structure.id)) { - StructureOverlayRenderer.removeAllOverlays(this.structure.id); + if (StructureOverlayRenderer.isShowingOverlay(this.structure.id())) { + StructureOverlayRenderer.removeAllOverlays(this.structure.id()); } else { - StructureOverlayRenderer.addPendingOverlay(this.structure.id); - StructureOverlayRenderer.restrictVisibleLayer(this.structure.id, this.visibleLayer); + StructureOverlayRenderer.addPendingOverlay(this.structure.id()); + StructureOverlayRenderer.restrictVisibleLayer(this.structure.id(), this.visibleLayer); MinecraftClient.getInstance().setScreen(null); } @@ -96,7 +96,7 @@ public boolean onMouseDown(double mouseX, double mouseY, int button) { } public StructureComponent visibleLayer(int visibleLayer) { - StructureOverlayRenderer.restrictVisibleLayer(this.structure.id, visibleLayer); + StructureOverlayRenderer.restrictVisibleLayer(this.structure.id(), visibleLayer); this.visibleLayer = visibleLayer; return this; diff --git a/src/main/java/io/wispforest/lavender/client/LavenderClient.java b/src/main/java/io/wispforest/lavender/client/LavenderClient.java index e6eb117..107fdd7 100644 --- a/src/main/java/io/wispforest/lavender/client/LavenderClient.java +++ b/src/main/java/io/wispforest/lavender/client/LavenderClient.java @@ -113,6 +113,7 @@ public void onInitializeClient() { ClientPlayNetworking.registerGlobalReceiver(Lavender.WORLD_ID_CHANNEL, (client, handler, buf, responseSender) -> { currentWorldId = buf.readUuid(); + StructureOverlayRenderer.reloadActiveOverlays(); }); } diff --git a/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java index 8415230..22497e8 100644 --- a/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java +++ b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java @@ -3,6 +3,7 @@ import com.google.common.base.Suppliers; import com.mojang.blaze3d.systems.RenderSystem; import io.wispforest.lavender.Lavender; +import io.wispforest.lavender.book.LavenderClientStorage; import io.wispforest.lavender.structure.BlockStatePredicate; import io.wispforest.lavender.structure.LavenderStructures; import io.wispforest.lavender.structure.StructureTemplate; @@ -35,12 +36,15 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3i; import org.apache.commons.lang3.mutable.MutableBoolean; +import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.Nullable; import org.lwjgl.opengl.GL30C; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; +import java.util.stream.Collectors; public class StructureOverlayRenderer { @@ -59,12 +63,17 @@ public class StructureOverlayRenderer { private static final Identifier HUD_COMPONENT_ID = Lavender.id("structure_overlay"); private static final Identifier BARS_TEXTURE = Lavender.id("textures/gui/structure_overlay_bars.png"); + public static Map getActiveOverlays() { + return Collections.unmodifiableMap(ACTIVE_OVERLAYS); + } + public static void addPendingOverlay(Identifier structure) { PENDING_OVERLAY = new OverlayEntry(structure, BlockRotation.NONE); } public static void addOverlay(BlockPos anchorPoint, Identifier structure, BlockRotation rotation) { ACTIVE_OVERLAYS.put(anchorPoint, new OverlayEntry(structure, rotation)); + saveActiveOverlays(); } public static boolean isShowingOverlay(Identifier structure) { @@ -83,6 +92,7 @@ public static void removeAllOverlays(Identifier structure) { } ACTIVE_OVERLAYS.values().removeIf(entry -> structure.equals(entry.structureId)); + saveActiveOverlays(); } public static int getLayerRestriction(Identifier structure) { @@ -103,10 +113,12 @@ public static void restrictVisibleLayer(Identifier structure, int visibleLayer) if (!structure.equals(entry.structureId)) continue; entry.visibleLayer = visibleLayer; } + saveActiveOverlays(); } public static void clearOverlays() { ACTIVE_OVERLAYS.clear(); + saveActiveOverlays(); } public static void rotatePending(boolean clockwise) { @@ -118,6 +130,20 @@ public static boolean hasPending() { return PENDING_OVERLAY != null; } + private static void saveActiveOverlays() { + LavenderClientStorage.setActiveStructures(Collections.unmodifiableMap(ACTIVE_OVERLAYS)); + } + + public static void reloadActiveOverlays() { + ACTIVE_OVERLAYS.clear(); + ACTIVE_OVERLAYS.putAll( + LavenderClientStorage.getActiveStructures() + .stream() + .map((structure) -> Pair.of(structure.pos(), new OverlayEntry(structure.id(), structure.rotation(), structure.visibleLayer()))) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)) + ); + } + public static void initialize() { Hud.add(HUD_COMPONENT_ID, () -> Containers.verticalFlow(Sizing.content(), Sizing.content()).gap(15).positioning(Positioning.relative(5, 100))); @@ -264,6 +290,7 @@ public static void initialize() { if (!player.isSneaking()) targetPos = targetPos.offset(hitResult.getSide()); ACTIVE_OVERLAYS.put(targetPos, PENDING_OVERLAY); + saveActiveOverlays(); PENDING_OVERLAY = null; player.swingHand(hand); @@ -278,8 +305,8 @@ private static Vec3i getPendingOffset(StructureTemplate structure) { return switch (PENDING_OVERLAY.rotation) { case NONE -> new Vec3i(-structure.anchor().getX(), -structure.anchor().getY(), -structure.anchor().getZ()); case CLOCKWISE_90 -> new Vec3i(-structure.anchor().getZ(), -structure.anchor().getY(), -structure.anchor().getX()); - case CLOCKWISE_180 -> new Vec3i(-structure.xSize + structure.anchor.getX() + 1, -structure.anchor().getY(), -structure.zSize + structure.anchor.getZ() + 1); - case COUNTERCLOCKWISE_90 -> new Vec3i(-structure.zSize + structure.anchor.getZ() + 1, -structure.anchor().getY(), -structure.xSize + structure.anchor.getX() + 1); + case CLOCKWISE_180 -> new Vec3i(-structure.xSize() + structure.anchor().getX() + 1, -structure.anchor().getY(), -structure.zSize() + structure.anchor().getZ() + 1); + case COUNTERCLOCKWISE_90 -> new Vec3i(-structure.zSize() + structure.anchor().getZ() + 1, -structure.anchor().getY(), -structure.xSize() + structure.anchor().getX() + 1); }; // @formatter:on } @@ -302,7 +329,7 @@ private static void renderOverlayBlock(MatrixStack matrices, VertexConsumerProvi matrices.pop(); } - private static class OverlayEntry { + public static class OverlayEntry { public final Identifier structureId; @@ -317,6 +344,12 @@ public OverlayEntry(Identifier structureId, BlockRotation rotation) { this.rotation = rotation; } + public OverlayEntry(Identifier structureId, BlockRotation rotation, int visibleLayer) { + this.structureId = structureId; + this.rotation = rotation; + this.visibleLayer = visibleLayer; + } + public @Nullable StructureTemplate fetchStructure() { return LavenderStructures.get(this.structureId); } diff --git a/src/main/java/io/wispforest/lavender/md/features/StructureFeature.java b/src/main/java/io/wispforest/lavender/md/features/StructureFeature.java index 51e85cc..03f4054 100644 --- a/src/main/java/io/wispforest/lavender/md/features/StructureFeature.java +++ b/src/main/java/io/wispforest/lavender/md/features/StructureFeature.java @@ -99,15 +99,15 @@ public StructureNode(StructureTemplate structure, int angle) { protected void visitStart(MarkdownCompiler compiler) { var structureComponent = StructureFeature.this.bookComponentSource.builtinTemplate( ParentComponent.class, - this.structure.ySize > 1 ? "structure-preview-with-layers" : "structure-preview", - Map.of("structure", this.structure.id.toString(), "angle", String.valueOf(this.angle)) + this.structure.ySize() > 1 ? "structure-preview-with-layers" : "structure-preview", + Map.of("structure", this.structure.id().toString(), "angle", String.valueOf(this.angle)) ); var structurePreview = structureComponent.childById(StructureComponent.class, "structure"); var layerSlider = structureComponent.childById(SlimSliderComponent.class, "layer-slider"); if (layerSlider != null) { - layerSlider.max(0).min(this.structure.ySize).tooltipSupplier(layer -> { + layerSlider.max(0).min(this.structure.ySize()).tooltipSupplier(layer -> { return layer > 0 ? Text.translatable("text.lavender.structure_component.layer_tooltip", layer.intValue()) : Text.translatable("text.lavender.structure_component.all_layers_tooltip"); @@ -115,7 +115,7 @@ protected void visitStart(MarkdownCompiler compiler) { structurePreview.visibleLayer((int) layer - 1); }); - layerSlider.value(StructureOverlayRenderer.getLayerRestriction(this.structure.id) + 1); + layerSlider.value(StructureOverlayRenderer.getLayerRestriction(this.structure.id()) + 1); } ((OwoUICompiler) compiler).visitComponent(structureComponent); diff --git a/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java b/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java index 85d829a..58fb19a 100644 --- a/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java +++ b/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java @@ -1,15 +1,53 @@ package io.wispforest.lavender.mixin; import io.wispforest.lavender.client.OffhandBookRenderer; +import io.wispforest.lavender.client.StructureOverlayRenderer; +import io.wispforest.lavender.util.RaycastResult; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.RaycastContext; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Comparator; + @Mixin(MinecraftClient.class) public class MinecraftClientMixin { + @Shadow + @Nullable + public ClientPlayerEntity player; + + @Shadow + @Nullable + public Entity cameraEntity; + + @Shadow + @Nullable + public HitResult crosshairTarget; + + @Shadow + @Nullable + public ClientPlayerInteractionManager interactionManager; + + @Shadow + @Final + private RenderTickCounter renderTickCounter; + @Inject(method = "render", at = @At("HEAD")) private void onFrameStart(boolean tick, CallbackInfo ci) { OffhandBookRenderer.beginFrame(); @@ -19,4 +57,67 @@ private void onFrameStart(boolean tick, CallbackInfo ci) { private void onFrameEnd(boolean tick, CallbackInfo ci) { OffhandBookRenderer.endFrame(); } + + @Inject(method = "doItemPick", at = @At("HEAD"), cancellable = true) + void onItemPick(final CallbackInfo ci) { + if (this.player == null || this.cameraEntity == null || this.interactionManager == null) + return; + + double blockRange = this.interactionManager.getReachDistance(); + float tickDelta = this.renderTickCounter.tickDelta; + Vec3d rotation = this.player.getRotationVec(tickDelta); + Vec3d rayLength = rotation.multiply(blockRange); + Vec3d cameraPos = this.player.getCameraPosVec(tickDelta); + + var firstOverlayHit = StructureOverlayRenderer.getActiveOverlays() + .entrySet() + .stream() + .filter(entry -> entry.getValue().fetchStructure() != null) + .map(entry -> { + BlockPos pos = entry.getKey(); + var overlayEntry = entry.getValue(); + var template = overlayEntry.fetchStructure(); + assert template != null; + + Vec3d rayStart = cameraPos.subtract(Vec3d.of(pos)); + Vec3d rayEnd = rayStart.add(rayLength); + var context = new RaycastContext(rayStart, rayEnd, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.NONE, this.player); + var raycast = template.asBlockRenderView().raycast(context); + + return new RaycastResult( + raycast, + rayStart, + rayEnd, + template.asBlockRenderView().getBlockState(raycast.getBlockPos()), + template + ); + }) + .min(Comparator.comparingDouble((raycast) -> raycast.hitResult().getPos().squaredDistanceTo(raycast.raycastStart()))); + + firstOverlayHit.ifPresent((raycast) -> { + double hitDistance = raycast.hitResult().getPos().squaredDistanceTo(raycast.raycastStart()); + double crosshairDistance = this.crosshairTarget != null ? this.crosshairTarget.getPos().squaredDistanceTo(cameraPos) : 0.0; + + if (crosshairDistance < hitDistance) // slightly prefer structure block + return; + + ItemStack stack = raycast.block().getBlock().asItem().getDefaultStack(); + + PlayerInventory playerInventory = this.player.getInventory(); + + int i = playerInventory.getSlotWithStack(stack); + if (this.player.getAbilities().creativeMode) { + playerInventory.addPickBlock(stack); + this.interactionManager.clickCreativeStack(this.player.getStackInHand(Hand.MAIN_HAND), 36 + playerInventory.selectedSlot); + } else if (i != -1) { + if (PlayerInventory.isValidHotbarIndex(i)) { + playerInventory.selectedSlot = i; + } else { + this.interactionManager.pickFromInventory(i); + } + } + + ci.cancel(); + }); + } } diff --git a/src/main/java/io/wispforest/lavender/structure/BlockStatePredicate.java b/src/main/java/io/wispforest/lavender/structure/BlockStatePredicate.java index c31ef0b..95a2f0f 100644 --- a/src/main/java/io/wispforest/lavender/structure/BlockStatePredicate.java +++ b/src/main/java/io/wispforest/lavender/structure/BlockStatePredicate.java @@ -2,6 +2,7 @@ import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; +import org.jetbrains.annotations.NotNull; /** * A predicate used for matching the elements of a structure @@ -17,13 +18,15 @@ public interface BlockStatePredicate { * a full state match */ BlockStatePredicate NULL_PREDICATE = new BlockStatePredicate() { + @NotNull @Override - public BlockState preview() { - return Blocks.AIR.getDefaultState(); + public BlockState[] previewBlockstates() { + return new BlockState[]{Blocks.AIR.getDefaultState()}; } + @NotNull @Override - public Result test(BlockState blockState) { + public Result test(@NotNull BlockState blockState) { return Result.STATE_MATCH; } @@ -38,13 +41,15 @@ public boolean isOf(MatchCategory type) { * match on any air block */ BlockStatePredicate AIR_PREDICATE = new BlockStatePredicate() { + @NotNull @Override - public BlockState preview() { - return Blocks.AIR.getDefaultState(); + public BlockState[] previewBlockstates() { + return new BlockState[]{Blocks.AIR.getDefaultState()}; } + @NotNull @Override - public Result test(BlockState blockState) { + public Result test(@NotNull BlockState blockState) { return blockState.isAir() ? Result.STATE_MATCH : Result.NO_MATCH; } @@ -54,13 +59,14 @@ public boolean isOf(MatchCategory type) { } }; - Result test(BlockState state); + @NotNull + Result test(@NotNull BlockState state); /** * @return {@code true} if this predicate finds a {@linkplain Result#STATE_MATCH state match} * on the given state */ - default boolean matches(BlockState state) { + default boolean matches(@NotNull BlockState state) { return this.test(state) == Result.STATE_MATCH; } @@ -69,7 +75,16 @@ default boolean matches(BlockState state) { * is called every frame the preview is rendered, returning a different sample * depending on system time (e.g. to cycle to a block tag) is valid behavior */ - BlockState preview(); + default BlockState preview() { + BlockState[] states = this.previewBlockstates(); + return states[(int) (System.currentTimeMillis() / 1000 % states.length)]; + } + + /** + * @return An array of all possible preview block states. + */ + @NotNull + BlockState[] previewBlockstates(); /** * @return Whether this predicate falls into the given matching category, generally diff --git a/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java b/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java index 44d4412..0037542 100644 --- a/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java +++ b/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java @@ -1,10 +1,12 @@ package io.wispforest.lavender.structure; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.mojang.brigadier.exceptions.CommandSyntaxException; import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; +import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.block.entity.BlockEntity; @@ -13,6 +15,8 @@ import net.minecraft.fluid.FluidState; import net.minecraft.fluid.Fluids; import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntryList; +import net.minecraft.registry.tag.TagKey; import net.minecraft.state.property.Property; import net.minecraft.util.BlockRotation; import net.minecraft.util.Identifier; @@ -25,21 +29,43 @@ import net.minecraft.world.biome.ColorResolver; import net.minecraft.world.chunk.light.LightingProvider; import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; +import java.util.Arrays; import java.util.EnumMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.StreamSupport; -public class StructureTemplate { +public class StructureTemplate implements Iterable> { + + private static final char AIR_BLOCKSTATE_KEY = '_'; + + private static final char NULL_BLOCKSTATE_KEY = ' '; + + private static final char ANCHOR_BLOCKSTATE_KEY = '#'; + + private final int xSize; + + private final int ySize; + + private final int zSize; + + private final Vec3i anchor; + + private final Identifier id; private final BlockStatePredicate[][][] predicates; - private final EnumMap predicateCountByType; - public final int xSize, ySize, zSize; - public final Vec3i anchor; - public final Identifier id; + private final EnumMap predicateCountByType; public StructureTemplate(Identifier id, BlockStatePredicate[][][] predicates, int xSize, int ySize, int zSize, @Nullable Vec3i anchor) { this.id = id; @@ -49,8 +75,8 @@ public StructureTemplate(Identifier id, BlockStatePredicate[][][] predicates, in this.zSize = zSize; this.anchor = anchor != null - ? anchor - : new Vec3i(this.xSize / 2, 0, this.ySize / 2); + ? anchor + : new Vec3i(this.xSize / 2, 0, this.ySize / 2); this.predicateCountByType = new EnumMap<>(BlockStatePredicate.MatchCategory.class); for (var type : BlockStatePredicate.MatchCategory.values()) { @@ -61,53 +87,149 @@ public StructureTemplate(Identifier id, BlockStatePredicate[][][] predicates, in } } - /** - * @return How many predicates of this structure template fall - * into the given match category - */ - public int predicatesOfType(BlockStatePredicate.MatchCategory type) { - return this.predicateCountByType.get(type).intValue(); - } + // --- validation --- - /** - * @return The anchor position of this template, - * to be used when placing in the world - */ - public Vec3i anchor() { - return this.anchor; + public static BlockRotation inverse(BlockRotation rotation) { + return switch (rotation) { + case NONE -> BlockRotation.NONE; + case CLOCKWISE_90 -> BlockRotation.COUNTERCLOCKWISE_90; + case COUNTERCLOCKWISE_90 -> BlockRotation.CLOCKWISE_90; + case CLOCKWISE_180 -> BlockRotation.CLOCKWISE_180; + }; } - // --- iteration --- + @NotNull + public static StructureTemplate parse(Identifier resourceId, JsonObject json) { + Vec3i anchor = null; - public void forEachPredicate(BiConsumer action) { - this.forEachPredicate(action, BlockRotation.NONE); - } + var keyObject = JsonHelper.getObject(json, "keys"); + var keys = StructureTemplate.buildStructureKeysMap(keyObject); - /** - * Execute {@code action} for every predicate in this structure template, - * rotated on the y-axis by {@code rotation} - */ - public void forEachPredicate(BiConsumer action, BlockRotation rotation) { - var mutable = new BlockPos.Mutable(); + var layersArray = JsonHelper.getArray(json, "layers"); + int xSize = 0, ySize = layersArray.size(), zSize = 0; - for (int x = 0; x < this.predicates.length; x++) { - for (int y = 0; y < this.predicates[x].length; y++) { - for (int z = 0; z < this.predicates[x][y].length; z++) { + for (var element : layersArray) { + if (!(element instanceof JsonArray layer)) { + throw new JsonParseException("Every element in the 'layers' array must itself be an array"); + } - switch (rotation) { - case CLOCKWISE_90 -> mutable.set(this.zSize - z - 1, y, x); - case COUNTERCLOCKWISE_90 -> mutable.set(z, y, this.xSize - x - 1); - case CLOCKWISE_180 -> mutable.set(this.xSize - x - 1, y, this.zSize - z - 1); - default -> mutable.set(x, y, z); + if (zSize == 0) { + zSize = layer.size(); + } else if (zSize != layer.size()) { + throw new JsonParseException("Every layer must have the same amount of rows"); + } + + for (var rowElement : layer) { + if (!rowElement.isJsonPrimitive()) { + throw new JsonParseException("Every element in a row must be a primitive"); + } + if (xSize == 0) { + xSize = rowElement.getAsString().length(); + } else if (xSize != rowElement.getAsString().length()) { + throw new JsonParseException("Every row must have the same length"); + } + } + } + + var result = new BlockStatePredicate[xSize][][]; + for (int x = 0; x < xSize; x++) { + result[x] = new BlockStatePredicate[ySize][]; + for (int y = 0; y < ySize; y++) { + result[x][y] = new BlockStatePredicate[zSize]; + } + } + + for (int y = 0; y < layersArray.size(); y++) { + var layer = (JsonArray) layersArray.get(y); + for (int z = 0; z < layer.size(); z++) { + var row = layer.get(z).getAsString(); + for (int x = 0; x < row.length(); x++) { + char key = row.charAt(x); + + BlockStatePredicate predicate; + if (keys.containsKey(key)) { + predicate = keys.get(key); + + if (key == ANCHOR_BLOCKSTATE_KEY) { + if (anchor != null) { + throw new JsonParseException("Anchor key '#' cannot be used twice within the same structure"); + } else { + anchor = new Vec3i(x, y, z); + } + } + } else if (key == NULL_BLOCKSTATE_KEY) { + predicate = BlockStatePredicate.NULL_PREDICATE; + } else if (key == AIR_BLOCKSTATE_KEY) { + predicate = BlockStatePredicate.AIR_PREDICATE; + } else { + throw new JsonParseException("Unknown key '" + key + "'"); } - action.accept(mutable, this.predicates[x][y][z]); + result[x][y][z] = predicate; } } } + + return new StructureTemplate(resourceId, result, xSize, ySize, zSize, anchor); } - // --- validation --- + @NotNull + private static Char2ObjectOpenHashMap buildStructureKeysMap(@NotNull JsonObject keyObject) { + var keys = new Char2ObjectOpenHashMap(); + for (var entry : keyObject.entrySet()) { + char key = blockstateKeyForEntry(entry); + + if (keys.containsKey(key)) { + throw new JsonParseException("Keys can only appear once. Key '%s' appears twice.".formatted(key)); + } + + if (entry.getValue().isJsonArray()) { + JsonArray blockStringsArray = entry.getValue().getAsJsonArray(); + var blockStatePredicates = StreamSupport.stream(blockStringsArray.spliterator(), false) + .map(blockString -> StructureTemplate.parseStringToBlockStatePredicate(blockString.getAsString())) + .toArray(BlockStatePredicate[]::new); + keys.put(key, new NestedBlockStatePredicate(blockStatePredicates)); + } else if (entry.getValue().isJsonPrimitive()) { + keys.put(key, StructureTemplate.parseStringToBlockStatePredicate(entry.getValue().getAsString())); + } else { + throw new JsonParseException("The values for the map of key-to-blocks must either be a string or an array of strings."); + } + } + + return keys; + } + + private static char blockstateKeyForEntry(final Map.Entry entry) { + char key; + if (entry.getKey().length() == 1) { + key = entry.getKey().charAt(0); + if (key == ANCHOR_BLOCKSTATE_KEY) { + throw new JsonParseException("Key '#' is reserved for 'anchor' declarations. Rename the key to 'anchor' and use '#' in the structure definition."); + } else if (key == AIR_BLOCKSTATE_KEY) { + throw new JsonParseException("Key '_' is a reserved key for marking a block that must be AIR."); + } else if (key == NULL_BLOCKSTATE_KEY) { + throw new JsonParseException("Key ' ' is a reserved key for marking a block that can be anything."); + } + } else if ("anchor".equals(entry.getKey())) { + key = ANCHOR_BLOCKSTATE_KEY; + } else { + throw new JsonParseException("Keys should only be a single character or should be 'anchor'."); + } + return key; + } + + @NotNull + private static BlockStatePredicate parseStringToBlockStatePredicate(@NotNull String blockOrTag) { + try { + var result = BlockArgumentParser.blockOrTag(Registries.BLOCK.getReadOnlyWrapper(), blockOrTag, false); + return result.map( + blockResult -> new SingleBlockStatePredicate(blockResult.blockState(), blockResult.properties()), + tagResult -> new TagBlockStatePredicate((RegistryEntryList.Named) tagResult.tag(), tagResult.vagueProperties()) + ); + } catch (CommandSyntaxException e) { + throw new JsonParseException("Failed to parse block state predicate", e); + } + } /** * Shorthand of {@link #validate(World, BlockPos, BlockRotation)} which uses @@ -125,6 +247,8 @@ public boolean validate(World world, BlockPos anchor, BlockRotation rotation) { return this.countValidStates(world, anchor, rotation) == this.predicatesOfType(BlockStatePredicate.MatchCategory.NON_NULL); } + // --- parsing --- + /** * Shorthand of {@link #countValidStates(World, BlockPos, BlockRotation)} which uses * {@link BlockRotation#NONE} @@ -135,7 +259,7 @@ public int countValidStates(World world, BlockPos anchor) { /** * Shorthand of {@link #countValidStates(World, BlockPos, BlockRotation, BlockStatePredicate.MatchCategory)} - * which uses {@link io.wispforest.lavender.structure.BlockStatePredicate.MatchCategory#NON_NULL} + * which uses {@link BlockStatePredicate.MatchCategory#NON_NULL} */ public int countValidStates(World world, BlockPos anchor, BlockRotation rotation) { return countValidStates(world, anchor, rotation, BlockStatePredicate.MatchCategory.NON_NULL); @@ -163,224 +287,330 @@ public int countValidStates(World world, BlockPos anchor, BlockRotation rotation // --- utility --- public BlockRenderView asBlockRenderView() { - var world = MinecraftClient.getInstance().world; - return new BlockRenderView() { - @Override - public float getBrightness(Direction direction, boolean shaded) { - return 1f; - } + return new StructureTemplateRenderView(Objects.requireNonNull(MinecraftClient.getInstance().world), this); + } - @Override - public LightingProvider getLightingProvider() { - return world.getLightingProvider(); - } + /** + * @return How many predicates of this structure template fall + * into the given match category + */ + public int predicatesOfType(BlockStatePredicate.MatchCategory type) { + return this.predicateCountByType.get(type).intValue(); + } - @Override - public int getColor(BlockPos pos, ColorResolver colorResolver) { - return colorResolver.getColor(world.getBiome(pos).value(), pos.getX(), pos.getZ()); - } + public Identifier id() { + return this.id; + } - @Nullable - @Override - public BlockEntity getBlockEntity(BlockPos pos) { - return null; - } + public BlockStatePredicate[][][] predicates() { + return this.predicates; + } - @Override - public BlockState getBlockState(BlockPos pos) { - if (pos.getX() < 0 || pos.getX() >= StructureTemplate.this.xSize || pos.getY() < 0 || pos.getY() >= StructureTemplate.this.ySize || pos.getZ() < 0 || pos.getZ() >= StructureTemplate.this.zSize) - return Blocks.AIR.getDefaultState(); - return StructureTemplate.this.predicates[pos.getX()][pos.getY()][pos.getZ()].preview(); - } + public EnumMap predicateCountByType() { + return this.predicateCountByType; + } - @Override - public FluidState getFluidState(BlockPos pos) { - return Fluids.EMPTY.getDefaultState(); - } + public int xSize() { + return this.xSize; + } - @Override - public int getHeight() { - return world.getHeight(); - } + public int ySize() { + return this.ySize; + } - @Override - public int getBottomY() { - return world.getBottomY(); - } - }; + public int zSize() { + return this.zSize; } - public static BlockRotation inverse(BlockRotation rotation) { - return switch (rotation) { - case NONE -> BlockRotation.NONE; - case CLOCKWISE_90 -> BlockRotation.COUNTERCLOCKWISE_90; - case COUNTERCLOCKWISE_90 -> BlockRotation.CLOCKWISE_90; - case CLOCKWISE_180 -> BlockRotation.CLOCKWISE_180; - }; + /** + * @return The anchor position of this template, + * to be used when placing in the world + */ + public Vec3i anchor() { + return this.anchor; } - // --- parsing --- + // --- iteration --- - @SuppressWarnings({"rawtypes", "unchecked"}) - public static StructureTemplate parse(Identifier resourceId, JsonObject json) { - var keyObject = JsonHelper.getObject(json, "keys"); - var keys = new Char2ObjectOpenHashMap(); - Vec3i anchor = null; + public void forEachPredicate(BiConsumer action) { + this.forEachPredicate(action, BlockRotation.NONE); + } - for (var entry : keyObject.entrySet()) { - char key; - if (entry.getKey().length() == 1) { - key = entry.getKey().charAt(0); - if (key == '#') { - throw new JsonParseException("Key '#' is reserved for 'anchor' declarations"); - } + /** + * Execute {@code action} for every predicate in this structure template, + * rotated on the y-axis by {@code rotation} + */ + public void forEachPredicate(BiConsumer action, BlockRotation rotation) { + var mutable = new BlockPos.Mutable(); - } else if (entry.getKey().equals("anchor")) { - key = '#'; - } else { - continue; - } + for (int x = 0; x < this.predicates.length; x++) { + for (int y = 0; y < this.predicates[x].length; y++) { + for (int z = 0; z < this.predicates[x][y].length; z++) { - try { - var result = BlockArgumentParser.blockOrTag(Registries.BLOCK.getReadOnlyWrapper(), entry.getValue().getAsString(), false); - if (result.left().isPresent()) { - var predicate = result.left().get(); + switch (rotation) { + case CLOCKWISE_90 -> mutable.set(this.zSize - z - 1, y, x); + case COUNTERCLOCKWISE_90 -> mutable.set(z, y, this.xSize - x - 1); + case CLOCKWISE_180 -> mutable.set(this.xSize - x - 1, y, this.zSize - z - 1); + default -> mutable.set(x, y, z); + } - keys.put(key, new BlockStatePredicate() { - @Override - public BlockState preview() { - return predicate.blockState(); - } + action.accept(mutable, this.predicates[x][y][z]); + } + } + } + } - @Override - public Result test(BlockState state) { - if (state.getBlock() != predicate.blockState().getBlock()) return Result.NO_MATCH; + @NotNull + @Override + public Iterator> iterator() { + return iterator(BlockRotation.NONE); + } - for (var propAndValue : predicate.properties().entrySet()) { - if (!state.get(propAndValue.getKey()).equals(propAndValue.getValue())) { - return Result.BLOCK_MATCH; - } - } + @Override + public void forEach(Consumer> action) { + var mutablePair = new MutablePair(); + forEachPredicate((pos, predicate) -> { + mutablePair.setLeft(pos); + mutablePair.setRight(predicate); + action.accept(mutablePair); + }); + } - return Result.STATE_MATCH; - } - }); - } else { - var predicate = result.right().get(); + public Iterator> iterator(BlockRotation rotation) { + return new StructureTemplateIterator(this, rotation); + } - var previewStates = new ArrayList(); - predicate.tag().forEach(registryEntry -> { - var block = registryEntry.value(); - var state = block.getDefaultState(); + public static class NestedBlockStatePredicate implements BlockStatePredicate { + @NotNull + private final BlockStatePredicate[] predicates; - for (var propAndValue : predicate.vagueProperties().entrySet()) { - Property prop = block.getStateManager().getProperty(propAndValue.getKey()); - if (prop == null) return; + @NotNull + private final BlockState[] previewStates; - Optional value = prop.parse(propAndValue.getValue()); - if (value.isEmpty()) return; + public NestedBlockStatePredicate(@NotNull BlockStatePredicate[] predicates) { + this.predicates = predicates; + this.previewStates = Arrays.stream(predicates) + .flatMap((predicate) -> Arrays.stream(predicate.previewBlockstates())) + .toArray(BlockState[]::new); + } - state = state.with(prop, value.get()); - } + @NotNull + @Override + public Result test(@NotNull BlockState state) { + boolean hasBlockMatch = false; + for (var predicate : this.predicates) { + var result = predicate.test(state); + if (result == Result.STATE_MATCH) + return Result.STATE_MATCH; + else if (result == Result.BLOCK_MATCH) + hasBlockMatch = true; + } - previewStates.add(state); - }); + return hasBlockMatch ? Result.BLOCK_MATCH : Result.NO_MATCH; + } - keys.put(key, new BlockStatePredicate() { - @Override - public BlockState preview() { - if (previewStates.isEmpty()) return Blocks.AIR.getDefaultState(); - return previewStates.get((int) (System.currentTimeMillis() / 1000 % previewStates.size())); - } + @Override + public BlockState[] previewBlockstates() { + return this.previewStates; + } + } - @Override - public Result test(BlockState state) { - if (!state.isIn(predicate.tag())) return Result.NO_MATCH; + public static class SingleBlockStatePredicate implements BlockStatePredicate { + @NotNull + private final BlockState state; - for (var propAndValue : predicate.vagueProperties().entrySet()) { - var prop = state.getBlock().getStateManager().getProperty(propAndValue.getKey()); - if (prop == null) return Result.BLOCK_MATCH; + @NotNull + private final BlockState[] states; - var expected = prop.parse(propAndValue.getValue()); - if (expected.isEmpty()) return Result.BLOCK_MATCH; + @NotNull + private final Map, Comparable> properties; - if (!state.get(prop).equals(expected.get())) return Result.BLOCK_MATCH; - } + public SingleBlockStatePredicate(@NotNull BlockState state, @NotNull Map, Comparable> properties) { + this.state = state; + this.states = new BlockState[]{state}; + this.properties = properties; + } - return Result.STATE_MATCH; - } - }); + @NotNull + @Override + public Result test(@NotNull BlockState state) { + if (state.getBlock() != this.state.getBlock()) return Result.NO_MATCH; + + for (var propAndValue : this.properties.entrySet()) { + if (!state.get(propAndValue.getKey()).equals(propAndValue.getValue())) { + return Result.BLOCK_MATCH; } - } catch (CommandSyntaxException e) { - throw new JsonParseException("Failed to parse block state predicate", e); } + + return Result.STATE_MATCH; } - var layersArray = JsonHelper.getArray(json, "layers"); - int xSize = 0, ySize = layersArray.size(), zSize = 0; + @Override + public BlockState[] previewBlockstates() { + return this.states; + } + } - for (var element : layersArray) { - if (!(element instanceof JsonArray layer)) { - throw new JsonParseException("Every element in the 'layers' array must itself be an array"); - } + @SuppressWarnings({"rawtypes", "unchecked"}) + public static class TagBlockStatePredicate implements BlockStatePredicate { + @NotNull + private final TagKey tag; - if (zSize == 0) { - zSize = layer.size(); - } else if (zSize != layer.size()) { - throw new JsonParseException("Every layer must have the same amount of rows"); - } + @NotNull + private final Map vagueProperties; - for (var rowElement : layer) { - if (!rowElement.isJsonPrimitive()) { - throw new JsonParseException("Every element in a row must be a primitive"); - } - if (xSize == 0) { - xSize = rowElement.getAsString().length(); - } else if (xSize != rowElement.getAsString().length()) { - throw new JsonParseException("Every row must have the same length"); + @NotNull + private final BlockState[] previewStates; + + public TagBlockStatePredicate(@NotNull RegistryEntryList.Named tagEntries, @NotNull Map properties) { + this.vagueProperties = properties; + this.tag = tagEntries.getTag(); + this.previewStates = tagEntries.stream().map(entry -> { + var block = entry.value(); + var state = block.getDefaultState(); + + for (var propAndValue : this.vagueProperties.entrySet()) { + Property prop = block.getStateManager().getProperty(propAndValue.getKey()); + if (prop == null) continue; + + Optional value = prop.parse(propAndValue.getValue()); + if (value.isEmpty()) continue; + + state = state.with(prop, value.get()); } - } + + return state; + }).toArray(BlockState[]::new); } - var result = new BlockStatePredicate[xSize][][]; - for (int x = 0; x < xSize; x++) { - result[x] = new BlockStatePredicate[ySize][]; - for (int y = 0; y < ySize; y++) { - result[x][y] = new BlockStatePredicate[zSize]; + @NotNull + @Override + public Result test(@NotNull BlockState state) { + if (!state.isIn(this.tag)) + return Result.NO_MATCH; + + for (var propAndValue : this.vagueProperties.entrySet()) { + var prop = state.getBlock().getStateManager().getProperty(propAndValue.getKey()); + if (prop == null) + return Result.BLOCK_MATCH; + + var expected = prop.parse(propAndValue.getValue()); + if (expected.isEmpty()) + return Result.BLOCK_MATCH; + + if (!state.get(prop).equals(expected.get())) + return Result.BLOCK_MATCH; } + + return Result.STATE_MATCH; } - for (int y = 0; y < layersArray.size(); y++) { - var layer = (JsonArray) layersArray.get(y); - for (int z = 0; z < layer.size(); z++) { - var row = layer.get(z).getAsString(); - for (int x = 0; x < row.length(); x++) { - char key = row.charAt(x); + @Override + public BlockState[] previewBlockstates() { + return this.previewStates; + } + } - BlockStatePredicate predicate; - if (keys.containsKey(key)) { - predicate = keys.get(key); + private record StructureTemplateRenderView(@NotNull World world, @NotNull StructureTemplate template) implements BlockRenderView { + @Override + public float getBrightness(Direction direction, boolean shaded) { + return 1.0f; + } - if (key == '#') { - if (anchor != null) { - throw new JsonParseException("Anchor key '#' cannot be used twice within the same structure"); - } + @Override + public LightingProvider getLightingProvider() { + return this.world.getLightingProvider(); + } - anchor = new Vec3i(x, y, z); - } - } else if (key == ' ') { - predicate = BlockStatePredicate.NULL_PREDICATE; - } else if (key == '_') { - predicate = BlockStatePredicate.AIR_PREDICATE; - } else { - throw new JsonParseException("Unknown key '" + key + "'"); - } + @Override + public int getColor(BlockPos pos, ColorResolver colorResolver) { + return colorResolver.getColor(this.world.getBiome(pos).value(), pos.getX(), pos.getZ()); + } - result[x][y][z] = predicate; + @Nullable + @Override + public BlockEntity getBlockEntity(BlockPos pos) { + return null; + } + + @Override + public BlockState getBlockState(BlockPos pos) { + if (pos.getX() < 0 || pos.getX() >= this.template.xSize || + pos.getY() < 0 || pos.getY() >= this.template.ySize || + pos.getZ() < 0 || pos.getZ() >= this.template.zSize) + return Blocks.AIR.getDefaultState(); + return this.template.predicates()[pos.getX()][pos.getY()][pos.getZ()].preview(); + } + + @Override + public FluidState getFluidState(BlockPos pos) { + return Fluids.EMPTY.getDefaultState(); + } + + @Override + public int getHeight() { + return this.world.getHeight(); + } + + @Override + public int getBottomY() { + return this.world.getBottomY(); + } + } + + private static final class StructureTemplateIterator implements Iterator> { + + private final StructureTemplate template; + + private final BlockPos.Mutable currentPos = new BlockPos.Mutable(); + + private final MutablePair currentElement = new MutablePair<>(); + + private final BlockRotation rotation; + + private int posX = 0; + + private int posY = 0; + + private int posZ = 0; + + private StructureTemplateIterator(StructureTemplate template, BlockRotation rotation) { + this.template = template; + this.rotation = rotation; + } + + @Override + public boolean hasNext() { + return this.posX < this.template.xSize() - 1 && this.posY < this.template.ySize() - 1 && this.posZ < this.template.zSize() - 1; + } + + @Override + public Pair next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + switch (this.rotation) { + case CLOCKWISE_90 -> this.currentPos.set(this.template.zSize() - this.posZ - 1, this.posY, this.posX); + case COUNTERCLOCKWISE_90 -> this.currentPos.set(this.posZ, this.posY, this.template.xSize() - this.posX - 1); + case CLOCKWISE_180 -> + this.currentPos.set(this.template.xSize() - this.posX - 1, this.posY, this.template.zSize() - this.posZ - 1); + default -> this.currentPos.set(this.posX, this.posY, this.posZ); + } + + this.currentElement.setRight(this.template.predicates()[this.posX][this.posY][this.posZ]); + this.currentElement.setLeft(this.currentPos); + + // Advance to next position + if (++this.posZ >= this.template.zSize()) { + this.posZ = 0; + if (++this.posY >= this.template.ySize()) { + this.posY = 0; + ++this.posX; } } - } - return new StructureTemplate(resourceId, result, xSize, ySize, zSize, anchor); + return this.currentElement; + } } } diff --git a/src/main/java/io/wispforest/lavender/util/RaycastResult.java b/src/main/java/io/wispforest/lavender/util/RaycastResult.java new file mode 100644 index 0000000..8a670af --- /dev/null +++ b/src/main/java/io/wispforest/lavender/util/RaycastResult.java @@ -0,0 +1,15 @@ +package io.wispforest.lavender.util; + +import io.wispforest.lavender.structure.StructureTemplate; +import net.minecraft.block.BlockState; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.Vec3d; + +public record RaycastResult( + BlockHitResult hitResult, + Vec3d raycastStart, + Vec3d raycastEnd, + BlockState block, + StructureTemplate template +) { +}