From c0969997fadc7c90891af262eca1db793a9e6a67 Mon Sep 17 00:00:00 2001 From: solonovamax Date: Thu, 13 Jun 2024 23:00:46 -0400 Subject: [PATCH 1/5] Improve parsing of structure templates to allow for arrays of strings in keys Allow for arrays of strings in the keys of a structure template. All the elements are parsed the same as they previously were, this just allows for defining a list of possible blocks. Also clean up code a decent amount to make it a tad more maintainable. Signed-off-by: solonovamax --- .../structure/BlockStatePredicate.java | 33 +- .../lavender/structure/StructureTemplate.java | 410 +++++++++++------- 2 files changed, 287 insertions(+), 156 deletions(-) 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..8458fd8 100644 --- a/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java +++ b/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java @@ -4,7 +4,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.datafixers.util.Either; 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,22 +29,35 @@ import net.minecraft.world.biome.ColorResolver; import net.minecraft.world.chunk.light.LightingProvider; import org.apache.commons.lang3.mutable.MutableInt; +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.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.stream.StreamSupport; public class StructureTemplate { - private final BlockStatePredicate[][][] predicates; - private final EnumMap predicateCountByType; + private static final char AIR_BLOCKSTATE_KEY = '_'; + + private static final char NULL_BLOCKSTATE_KEY = ' '; + + private static final char ANCHOR_BLOCKSTATE_KEY = '#'; public final int xSize, ySize, zSize; + public final Vec3i anchor; + public final Identifier id; + private final BlockStatePredicate[][][] predicates; + + private final EnumMap predicateCountByType; + public StructureTemplate(Identifier id, BlockStatePredicate[][][] predicates, int xSize, int ySize, int zSize, @Nullable Vec3i anchor) { this.id = id; this.predicates = predicates; @@ -49,8 +66,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()) { @@ -135,7 +152,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,51 +180,7 @@ 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; - } - - @Override - public LightingProvider getLightingProvider() { - return world.getLightingProvider(); - } - - @Override - public int getColor(BlockPos pos, ColorResolver colorResolver) { - return colorResolver.getColor(world.getBiome(pos).value(), pos.getX(), pos.getZ()); - } - - @Nullable - @Override - public BlockEntity getBlockEntity(BlockPos pos) { - return null; - } - - @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(); - } - - @Override - public FluidState getFluidState(BlockPos pos) { - return Fluids.EMPTY.getDefaultState(); - } - - @Override - public int getHeight() { - return world.getHeight(); - } - - @Override - public int getBottomY() { - return world.getBottomY(); - } - }; + return new StructureTemplateRenderView(Objects.requireNonNull(MinecraftClient.getInstance().world), this); } public static BlockRotation inverse(BlockRotation rotation) { @@ -221,100 +194,12 @@ public static BlockRotation inverse(BlockRotation rotation) { // --- parsing --- - @SuppressWarnings({"rawtypes", "unchecked"}) + @NotNull public static StructureTemplate parse(Identifier resourceId, JsonObject json) { - var keyObject = JsonHelper.getObject(json, "keys"); - var keys = new Char2ObjectOpenHashMap(); Vec3i anchor = null; - 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"); - } - - } else if (entry.getKey().equals("anchor")) { - key = '#'; - } else { - continue; - } - - try { - var result = BlockArgumentParser.blockOrTag(Registries.BLOCK.getReadOnlyWrapper(), entry.getValue().getAsString(), false); - if (result.left().isPresent()) { - var predicate = result.left().get(); - - keys.put(key, new BlockStatePredicate() { - @Override - public BlockState preview() { - return predicate.blockState(); - } - - @Override - public Result test(BlockState state) { - if (state.getBlock() != predicate.blockState().getBlock()) return Result.NO_MATCH; - - for (var propAndValue : predicate.properties().entrySet()) { - if (!state.get(propAndValue.getKey()).equals(propAndValue.getValue())) { - return Result.BLOCK_MATCH; - } - } - - return Result.STATE_MATCH; - } - }); - } else { - var predicate = result.right().get(); - - var previewStates = new ArrayList(); - predicate.tag().forEach(registryEntry -> { - var block = registryEntry.value(); - var state = block.getDefaultState(); - - for (var propAndValue : predicate.vagueProperties().entrySet()) { - Property prop = block.getStateManager().getProperty(propAndValue.getKey()); - if (prop == null) return; - - Optional value = prop.parse(propAndValue.getValue()); - if (value.isEmpty()) return; - - state = state.with(prop, value.get()); - } - - previewStates.add(state); - }); - - 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 Result test(BlockState state) { - if (!state.isIn(predicate.tag())) return Result.NO_MATCH; - - for (var propAndValue : predicate.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; - } - }); - } - } catch (CommandSyntaxException e) { - throw new JsonParseException("Failed to parse block state predicate", e); - } - } + var keyObject = JsonHelper.getObject(json, "keys"); + var keys = StructureTemplate.buildStructureKeysMap(keyObject); var layersArray = JsonHelper.getArray(json, "layers"); int xSize = 0, ySize = layersArray.size(), zSize = 0; @@ -361,16 +246,16 @@ public Result test(BlockState state) { if (keys.containsKey(key)) { predicate = keys.get(key); - if (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); } - - anchor = new Vec3i(x, y, z); } - } else if (key == ' ') { + } else if (key == NULL_BLOCKSTATE_KEY) { predicate = BlockStatePredicate.NULL_PREDICATE; - } else if (key == '_') { + } else if (key == AIR_BLOCKSTATE_KEY) { predicate = BlockStatePredicate.AIR_PREDICATE; } else { throw new JsonParseException("Unknown key '" + key + "'"); @@ -383,4 +268,235 @@ public Result test(BlockState state) { return new StructureTemplate(resourceId, result, xSize, ySize, zSize, anchor); } + + @NotNull + private static Char2ObjectOpenHashMap buildStructureKeysMap(@NotNull JsonObject keyObject) { + var keys = new Char2ObjectOpenHashMap(); + for (var entry : keyObject.entrySet()) { + 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'."); + } + + if (keys.containsKey(key)) { + throw new JsonParseException("Keys can only appear once. Key '" + key + "' appears twice."); + } + + + 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; + } + + @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); + } + } + + public static class NestedBlockStatePredicate implements BlockStatePredicate { + @NotNull + private final BlockStatePredicate[] predicates; + + @NotNull + private final BlockState[] previewStates; + + public NestedBlockStatePredicate(@NotNull BlockStatePredicate[] predicates) { + this.predicates = predicates; + this.previewStates = Arrays.stream(predicates) + .flatMap((predicate) -> Arrays.stream(predicate.previewBlockstates())) + .toArray(BlockState[]::new); + } + + @Override + public BlockState[] previewBlockstates() { + return this.previewStates; + } + + @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; + } + + return hasBlockMatch ? Result.BLOCK_MATCH : Result.NO_MATCH; + } + } + + public static class SingleBlockStatePredicate implements BlockStatePredicate { + @NotNull + private final BlockState state; + + @NotNull + private final BlockState[] states; + + @NotNull + private final Map, Comparable> properties; + + public SingleBlockStatePredicate(@NotNull BlockState state, @NotNull Map, Comparable> properties) { + this.state = state; + this.states = new BlockState[]{state}; + this.properties = properties; + } + + @Override + public BlockState[] previewBlockstates() { + return this.states; + } + + @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; + } + } + + return Result.STATE_MATCH; + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static class TagBlockStatePredicate implements BlockStatePredicate { + @NotNull + private final TagKey tag; + + @NotNull + private final Map vagueProperties; + + @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); + } + + @Override + public BlockState[] previewBlockstates() { + return this.previewStates; + } + + @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; + } + } + + private record StructureTemplateRenderView(@NotNull World world, @NotNull StructureTemplate template) implements BlockRenderView { + @Override + public float getBrightness(Direction direction, boolean shaded) { + return 1.0f; + } + + @Override + public LightingProvider getLightingProvider() { + return this.world.getLightingProvider(); + } + + @Override + public int getColor(BlockPos pos, ColorResolver colorResolver) { + return colorResolver.getColor(this.world.getBiome(pos).value(), pos.getX(), pos.getZ()); + } + + @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(); + } + } } From 69a19179cb7605b30cd4d2c847a6a8b9c67a74ed Mon Sep 17 00:00:00 2001 From: solonovamax Date: Tue, 25 Jun 2024 17:14:46 -0400 Subject: [PATCH 2/5] Improve StructureTemplate - Implement Iterable> for StructureTemplate - Convert all fields to getters and introduce getters for more fields Signed-off-by: solonovamax --- .../lavender/book/StructureComponent.java | 16 +- .../client/StructureOverlayRenderer.java | 4 +- .../md/features/StructureFeature.java | 8 +- .../lavender/structure/StructureTemplate.java | 400 +++++++++++------- 4 files changed, 271 insertions(+), 157 deletions(-) diff --git a/src/main/java/io/wispforest/lavender/book/StructureComponent.java b/src/main/java/io/wispforest/lavender/book/StructureComponent.java index dbe4416..92ab225 100644 --- a/src/main/java/io/wispforest/lavender/book/StructureComponent.java +++ b/src/main/java/io/wispforest/lavender/book/StructureComponent.java @@ -57,7 +57,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(); @@ -68,7 +68,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(this.rotation)); - 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; @@ -92,7 +92,7 @@ public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partial DiffuseLighting.enableGuiDepthLighting(); if (this.placeable) { - 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 { @@ -106,11 +106,11 @@ public boolean onMouseDown(double mouseX, double mouseY, int button) { var result = super.onMouseDown(mouseX, mouseY, button); if (!this.placeable || button != GLFW.GLFW_MOUSE_BUTTON_LEFT || !Screen.hasShiftDown()) 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); } @@ -135,7 +135,7 @@ public boolean canFocus(FocusSource source) { } 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/StructureOverlayRenderer.java b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java index 8415230..e779599 100644 --- a/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java +++ b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java @@ -278,8 +278,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 } 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 ce01482..b2a9e06 100644 --- a/src/main/java/io/wispforest/lavender/md/features/StructureFeature.java +++ b/src/main/java/io/wispforest/lavender/md/features/StructureFeature.java @@ -108,15 +108,15 @@ public StructureNode(StructureTemplate structure, int angle, boolean placeable) 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").placeable(this.placeable); 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"); @@ -124,7 +124,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/structure/StructureTemplate.java b/src/main/java/io/wispforest/lavender/structure/StructureTemplate.java index 8458fd8..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,10 @@ 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 com.mojang.datafixers.util.Either; import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; import net.minecraft.block.Block; import net.minecraft.block.BlockState; @@ -29,18 +29,23 @@ 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.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 = '_'; @@ -48,11 +53,15 @@ public class StructureTemplate { private static final char ANCHOR_BLOCKSTATE_KEY = '#'; - public final int xSize, ySize, zSize; + private final int xSize; - public final Vec3i anchor; + private final int ySize; - public final Identifier id; + private final int zSize; + + private final Vec3i anchor; + + private final Identifier id; private final BlockStatePredicate[][][] predicates; @@ -78,111 +87,8 @@ 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(); - } - - /** - * @return The anchor position of this template, - * to be used when placing in the world - */ - public Vec3i anchor() { - return this.anchor; - } - - // --- iteration --- - - public void forEachPredicate(BiConsumer action) { - this.forEachPredicate(action, BlockRotation.NONE); - } - - /** - * 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(); - - 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++) { - - 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); - } - - action.accept(mutable, this.predicates[x][y][z]); - } - } - } - } - // --- validation --- - /** - * Shorthand of {@link #validate(World, BlockPos, BlockRotation)} which uses - * {@link BlockRotation#NONE} - */ - public boolean validate(World world, BlockPos anchor) { - return this.validate(world, anchor, BlockRotation.NONE); - } - - /** - * @return {@code true} if this template matches the block states present - * in the given world at the given position - */ - public boolean validate(World world, BlockPos anchor, BlockRotation rotation) { - return this.countValidStates(world, anchor, rotation) == this.predicatesOfType(BlockStatePredicate.MatchCategory.NON_NULL); - } - - /** - * Shorthand of {@link #countValidStates(World, BlockPos, BlockRotation)} which uses - * {@link BlockRotation#NONE} - */ - public int countValidStates(World world, BlockPos anchor) { - return countValidStates(world, anchor, BlockRotation.NONE, BlockStatePredicate.MatchCategory.NON_NULL); - } - - /** - * Shorthand of {@link #countValidStates(World, BlockPos, BlockRotation, BlockStatePredicate.MatchCategory)} - * 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); - } - - /** - * @return The amount of predicates in this template which match the block - * states present in the given world at the given position - */ - public int countValidStates(World world, BlockPos anchor, BlockRotation rotation, BlockStatePredicate.MatchCategory predicateFilter) { - var validStates = new MutableInt(); - var mutable = new BlockPos.Mutable(); - - this.forEachPredicate((pos, predicate) -> { - if (!predicate.isOf(predicateFilter)) return; - - if (predicate.matches(world.getBlockState(mutable.set(pos).move(anchor)).rotate(inverse(rotation)))) { - validStates.increment(); - } - }, rotation); - - return validStates.intValue(); - } - - // --- utility --- - - public BlockRenderView asBlockRenderView() { - return new StructureTemplateRenderView(Objects.requireNonNull(MinecraftClient.getInstance().world), this); - } - public static BlockRotation inverse(BlockRotation rotation) { return switch (rotation) { case NONE -> BlockRotation.NONE; @@ -192,8 +98,6 @@ public static BlockRotation inverse(BlockRotation rotation) { }; } - // --- parsing --- - @NotNull public static StructureTemplate parse(Identifier resourceId, JsonObject json) { Vec3i anchor = null; @@ -273,27 +177,12 @@ public static StructureTemplate parse(Identifier resourceId, JsonObject json) { private static Char2ObjectOpenHashMap buildStructureKeysMap(@NotNull JsonObject keyObject) { var keys = new Char2ObjectOpenHashMap(); for (var entry : keyObject.entrySet()) { - 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'."); - } + char key = blockstateKeyForEntry(entry); if (keys.containsKey(key)) { - throw new JsonParseException("Keys can only appear once. Key '" + key + "' appears twice."); + 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) @@ -306,9 +195,29 @@ private static Char2ObjectOpenHashMap buildStructureKeysMap 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 { @@ -322,6 +231,155 @@ private static BlockStatePredicate parseStringToBlockStatePredicate(@NotNull Str } } + /** + * Shorthand of {@link #validate(World, BlockPos, BlockRotation)} which uses + * {@link BlockRotation#NONE} + */ + public boolean validate(World world, BlockPos anchor) { + return this.validate(world, anchor, BlockRotation.NONE); + } + + /** + * @return {@code true} if this template matches the block states present + * in the given world at the given position + */ + 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} + */ + public int countValidStates(World world, BlockPos anchor) { + return countValidStates(world, anchor, BlockRotation.NONE, BlockStatePredicate.MatchCategory.NON_NULL); + } + + /** + * Shorthand of {@link #countValidStates(World, BlockPos, BlockRotation, BlockStatePredicate.MatchCategory)} + * 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); + } + + /** + * @return The amount of predicates in this template which match the block + * states present in the given world at the given position + */ + public int countValidStates(World world, BlockPos anchor, BlockRotation rotation, BlockStatePredicate.MatchCategory predicateFilter) { + var validStates = new MutableInt(); + var mutable = new BlockPos.Mutable(); + + this.forEachPredicate((pos, predicate) -> { + if (!predicate.isOf(predicateFilter)) return; + + if (predicate.matches(world.getBlockState(mutable.set(pos).move(anchor)).rotate(inverse(rotation)))) { + validStates.increment(); + } + }, rotation); + + return validStates.intValue(); + } + + // --- utility --- + + public BlockRenderView asBlockRenderView() { + return new StructureTemplateRenderView(Objects.requireNonNull(MinecraftClient.getInstance().world), this); + } + + /** + * @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(); + } + + public Identifier id() { + return this.id; + } + + public BlockStatePredicate[][][] predicates() { + return this.predicates; + } + + public EnumMap predicateCountByType() { + return this.predicateCountByType; + } + + public int xSize() { + return this.xSize; + } + + public int ySize() { + return this.ySize; + } + + public int zSize() { + return this.zSize; + } + + /** + * @return The anchor position of this template, + * to be used when placing in the world + */ + public Vec3i anchor() { + return this.anchor; + } + + // --- iteration --- + + public void forEachPredicate(BiConsumer action) { + this.forEachPredicate(action, BlockRotation.NONE); + } + + /** + * 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(); + + 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++) { + + 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); + } + + action.accept(mutable, this.predicates[x][y][z]); + } + } + } + } + + @NotNull + @Override + public Iterator> iterator() { + return iterator(BlockRotation.NONE); + } + + @Override + public void forEach(Consumer> action) { + var mutablePair = new MutablePair(); + forEachPredicate((pos, predicate) -> { + mutablePair.setLeft(pos); + mutablePair.setRight(predicate); + action.accept(mutablePair); + }); + } + + public Iterator> iterator(BlockRotation rotation) { + return new StructureTemplateIterator(this, rotation); + } + public static class NestedBlockStatePredicate implements BlockStatePredicate { @NotNull private final BlockStatePredicate[] predicates; @@ -336,11 +394,6 @@ public NestedBlockStatePredicate(@NotNull BlockStatePredicate[] predicates) { .toArray(BlockState[]::new); } - @Override - public BlockState[] previewBlockstates() { - return this.previewStates; - } - @NotNull @Override public Result test(@NotNull BlockState state) { @@ -355,6 +408,11 @@ else if (result == Result.BLOCK_MATCH) return hasBlockMatch ? Result.BLOCK_MATCH : Result.NO_MATCH; } + + @Override + public BlockState[] previewBlockstates() { + return this.previewStates; + } } public static class SingleBlockStatePredicate implements BlockStatePredicate { @@ -373,11 +431,6 @@ public SingleBlockStatePredicate(@NotNull BlockState state, @NotNull Map tagEntries }).toArray(BlockState[]::new); } - @Override - public BlockState[] previewBlockstates() { - return this.previewStates; - } - @NotNull @Override public Result test(@NotNull BlockState state) { @@ -451,6 +504,11 @@ public Result test(@NotNull BlockState state) { return Result.STATE_MATCH; } + + @Override + public BlockState[] previewBlockstates() { + return this.previewStates; + } } private record StructureTemplateRenderView(@NotNull World world, @NotNull StructureTemplate template) implements BlockRenderView { @@ -481,7 +539,7 @@ public BlockState getBlockState(BlockPos pos) { 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(); + return this.template.predicates()[pos.getX()][pos.getY()][pos.getZ()].preview(); } @Override @@ -499,4 +557,60 @@ 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 this.currentElement; + } + } } From 9a694bb738713226d80a3fb1d354f0f367675666 Mon Sep 17 00:00:00 2001 From: solonovamax Date: Tue, 25 Jun 2024 18:46:51 -0400 Subject: [PATCH 3/5] Add pickblock for structure preview Add the ability to pickblock block that are in a structure preview Note: the pickblock will *only* work on the currently previewed block, and not any compatible block. eg. if in survival and the position you pickblock allows for either stone or dirt if the preview is showing stone, and you have dirt in your inventory and pickblock it, then it will not select the dirt. Signed-off-by: solonovamax --- .../client/StructureOverlayRenderer.java | 7 +- .../lavender/mixin/MinecraftClientMixin.java | 99 +++++++++++++++++++ .../lavender/util/RaycastResult.java | 15 +++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/wispforest/lavender/util/RaycastResult.java diff --git a/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java index e779599..726b7b6 100644 --- a/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java +++ b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java @@ -38,6 +38,7 @@ 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; @@ -59,6 +60,10 @@ 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); } @@ -302,7 +307,7 @@ private static void renderOverlayBlock(MatrixStack matrices, VertexConsumerProvi matrices.pop(); } - private static class OverlayEntry { + public static class OverlayEntry { public final Identifier structureId; diff --git a/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java b/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java index b829073..764dcbd 100644 --- a/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java +++ b/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java @@ -4,16 +4,32 @@ import io.wispforest.lavender.book.LavenderBookItem; import io.wispforest.lavender.client.LavenderBookScreen; 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.gui.screen.Screen; import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.client.world.ClientWorld; +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 { @@ -25,6 +41,26 @@ public class MinecraftClientMixin { @Nullable public Screen currentScreen; + @Shadow + @Nullable + public ClientWorld world; + + @Shadow + @Nullable + public Entity cameraEntity; + + @Shadow + @Nullable + public HitResult crosshairTarget; + + @Shadow + @Nullable + public ClientPlayerInteractionManager interactionManager; + + @Shadow + @Final + private RenderTickCounter.Dynamic renderTickCounter; + @Inject(method = "render", at = @At("HEAD")) private void onFrameStart(boolean tick, CallbackInfo ci) { if (this.player == null) return; @@ -43,4 +79,67 @@ private void onFrameEnd(boolean tick, CallbackInfo ci) { if (this.player == null) return; 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.player.getBlockInteractionRange(); + float tickDelta = this.renderTickCounter.getTickDelta(true); + 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/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 +) { +} From f3a16596fdedb397ba4074b2453628d91b59db93 Mon Sep 17 00:00:00 2001 From: solonovamax Date: Tue, 25 Jun 2024 19:11:39 -0400 Subject: [PATCH 4/5] Fix build issue from merge Signed-off-by: solonovamax --- .../io/wispforest/lavender/mixin/MinecraftClientMixin.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java b/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java index 764dcbd..856bbc9 100644 --- a/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java +++ b/src/main/java/io/wispforest/lavender/mixin/MinecraftClientMixin.java @@ -59,7 +59,7 @@ public class MinecraftClientMixin { @Shadow @Final - private RenderTickCounter.Dynamic renderTickCounter; + private RenderTickCounter renderTickCounter; @Inject(method = "render", at = @At("HEAD")) private void onFrameStart(boolean tick, CallbackInfo ci) { @@ -85,8 +85,8 @@ void onItemPick(final CallbackInfo ci) { if (this.player == null || this.cameraEntity == null || this.interactionManager == null) return; - double blockRange = this.player.getBlockInteractionRange(); - float tickDelta = this.renderTickCounter.getTickDelta(true); + 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); From 634538222de4f807b626376d8992220b6ee3f785 Mon Sep 17 00:00:00 2001 From: solonovamax Date: Wed, 26 Jun 2024 15:58:45 -0400 Subject: [PATCH 5/5] Persist active structures when exiting the world Signed-off-by: solonovamax --- .../lavender/book/LavenderClientStorage.java | 41 ++++++++++++++++++- .../lavender/client/LavenderClient.java | 1 + .../client/StructureOverlayRenderer.java | 28 +++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java b/src/main/java/io/wispforest/lavender/book/LavenderClientStorage.java index c118840..c31e201 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 { @@ -26,6 +35,9 @@ public class LavenderClientStorage { private static final TypeToken>>> VIEWED_ENTRIES_TYPE = new TypeToken<>() {}; private static Map>> viewedEntries; + 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 { @@ -35,14 +47,17 @@ public class LavenderClientStorage { bookmarks = GSON.fromJson(data.get("bookmarks"), BOOKMARKS_TYPE); openedBooks = GSON.fromJson(data.get("opened_books"), OPENED_BOOKS_TYPE); viewedEntries = GSON.fromJson(data.get("viewed_entries"), VIEWED_ENTRIES_TYPE); + activeStructures = GSON.fromJson(data.get("active_structures"), ACTIVE_STRUCTURES_TYPE); if (bookmarks == null) bookmarks = new HashMap<>(); if (openedBooks == null) openedBooks = new HashMap<>(); if (viewedEntries == null) viewedEntries = new HashMap<>(); + if (activeStructures == null) activeStructures = new HashMap<>(); } catch (Exception e) { bookmarks = new HashMap<>(); openedBooks = new HashMap<>(); viewedEntries = new HashMap<>(); + activeStructures = new HashMap<>(); save(); } } @@ -98,12 +113,30 @@ 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("viewed_entries", GSON.toJsonTree(viewedEntries, VIEWED_ENTRIES_TYPE.getType())); + data.add("active_structure", GSON.toJsonTree(activeStructures, ACTIVE_STRUCTURES_TYPE.getType())); Files.writeString(storageFile(), GSON.toJson(data)); } catch (IOException e) { @@ -127,4 +160,8 @@ 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/client/LavenderClient.java b/src/main/java/io/wispforest/lavender/client/LavenderClient.java index 78007fc..88bfd85 100644 --- a/src/main/java/io/wispforest/lavender/client/LavenderClient.java +++ b/src/main/java/io/wispforest/lavender/client/LavenderClient.java @@ -123,6 +123,7 @@ public void onInitializeClient() { ClientPlayNetworking.registerGlobalReceiver(Lavender.WORLD_ID_CHANNEL, (client, handler, buf, responseSender) -> { currentWorldId = buf.readUuid(); + StructureOverlayRenderer.reloadActiveOverlays(); }); UIParsing.registerFactory(Lavender.id("ingredient"), element -> { diff --git a/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java b/src/main/java/io/wispforest/lavender/client/StructureOverlayRenderer.java index 726b7b6..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,6 +36,7 @@ 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; @@ -42,6 +44,7 @@ import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; +import java.util.stream.Collectors; public class StructureOverlayRenderer { @@ -70,6 +73,7 @@ public static void addPendingOverlay(Identifier structure) { 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) { @@ -88,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) { @@ -108,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) { @@ -123,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))); @@ -269,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); @@ -322,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); }